Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions .claude/skills/pcm-startlist/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Build a Pro Cycling Manager (PCM) race startlist and export it as the .xml
file PCM imports. Use when the user wants to create, compose, or generate a
startlist for a PCM race — picking which teams take part and which riders
each team brings. Orchestrates the read-only PCM MCP tools (pcm_query_save,
each team brings. Orchestrates the read-only PCM MCP tools (pcm_query_database,
pcm_search_cyclist) to gather data and pcm_generate_startlist_xml to produce
the file. Triggers on phrases like "startlist", "liste de départ",
"engagés pour la course", "génère le fichier xml de la course X".
Expand All @@ -13,7 +13,7 @@ description: >-
# PCM startlist builder

Compose a startlist for a Pro Cycling Manager race and write the `.xml` file PCM
imports. This skill owns the *workflow* (find the race, choose teams, choose
imports. This skill owns the _workflow_ (find the race, choose teams, choose
riders); the deterministic serialization is delegated to the
`pcm_generate_startlist_xml` MCP tool — never hand-write the XML.

Expand All @@ -34,14 +34,20 @@ returned by the tool — don't invent it.

## Workflow

### 1. Get a save path
Every step reads a `.cdb` save. If the user hasn't given an absolute `savePath`:
- Try `pcm_list_saves` (Windows only — fails on macOS/Linux Wine/Proton prefixes).
### 1. Get a database path

Every step reads a `.cdb` database — a player save, an official release or a
community update, it makes no difference. If the user hasn't given an absolute
`databasePath`:

- Try `pcm_list_saves` to find their saves (Windows only — fails on
macOS/Linux Wine/Proton prefixes).
- Otherwise ask the user for the absolute `.cdb` path. Keep it in context; the
tools are stateless and need it on every call.

### 2. Identify the race (get `IDrace`)
The user names a race; resolve it to an `IDrace` with `pcm_query_save`:

The user names a race; resolve it to an `IDrace` with `pcm_query_database`:

```sql
SELECT IDrace, gene_sz_race_name, gene_sz_filename
Expand All @@ -53,6 +59,7 @@ If several match, show the candidates (name + id) and let the user pick. Confirm
the `gene_sz_filename` so the user knows the output file name up front.

### 3. Decide which teams take part

Either the user supplies the teams, or you propose them. Resolve names to
`IDteam`:

Expand All @@ -65,6 +72,7 @@ A typical startlist has ~18–25 teams. If the user just says "the usual teams",
ask which division/tier or list candidates rather than guessing.

### 4. Pick riders per team

A team's full squad is the candidate pool — a startlist usually brings **7** of
them (the count is free; the example pack mixes 6 and 7). Get a team's roster:

Expand All @@ -82,11 +90,12 @@ the user has preferences (leaders, exclusions), apply them. Confirm the selectio
before generating when there's any ambiguity.

### 5. Generate the file
Call `pcm_generate_startlist_xml` with `savePath`, `raceId`, and `teams`:

Call `pcm_generate_startlist_xml` with `databasePath`, `raceId`, and `teams`:

```json
{
"savePath": "/abs/path/Career.cdb",
"databasePath": "/abs/path/OfficialRelease.cdb",
"raceId": 128,
"teams": [
{ "id": 34, "cyclists": [7602, 5996, 1381, 3291, 6342, 3912, 5613] },
Expand All @@ -99,11 +108,14 @@ The tool returns `{ fileName, xml }`. If a team has no riders or `teams` is empt
the tool errors — fix the selection and retry.

### 6. Deliver

Present the returned `fileName` and `xml`. Offer to write it to disk (e.g. the
user's PCM `Startlists`/race-import folder or the working directory) — the MCP
server is read-only and does not write files, so saving is done outside it.

## Notes
- All PCM MCP tools are read-only; this workflow never modifies the save.

- All PCM MCP tools used here are read-only; this workflow never modifies the
database.
- IDs, not names, go into the XML — always resolve names to `IDteam`/`IDcyclist`
via the queries above before calling the tool.
5 changes: 3 additions & 2 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"*.json": ["biome check --write"],
"*.ts": ["biome check --write"]
"*.json": ["biome check --write", "biome lint --write"],
"*.ts": ["biome check --write", "biome lint --write"],
"*.js": ["biome check --write", "biome lint --write"]
}
73 changes: 43 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What this is

A Model Context Protocol (MCP) server that exposes Pro Cycling Manager (PCM) game
saves to an LLM client over stdio. PCM stores careers as binary `.cdb` files; this
server discovers and inspects those saves and **never modifies the source save**.
Each call re-reads the `.cdb` from disk and loads it into an in-memory sql.js
(SQLite) database (via `cdb-converter`), so the on-disk save is the single source
of truth. Write tools (`pcm_update_save`, `pcm_update_cyclist_ratings`) mutate the
in-memory copy and serialize it to a **new** `.cdb` via `writeSaveDb`, which
refuses to overwrite the source or any existing file. Any new tool must keep this
never-touch-the-source guarantee.
A Model Context Protocol (MCP) server that exposes Pro Cycling Manager (PCM)
databases to an LLM client over stdio. Each call re-reads the `.cdb` from disk and
loads it into an in-memory sql.js (SQLite) database (via `cdb-converter`), so the
on-disk file is the single source of truth and **is never modified**. Write tools
(`pcm_update_database`, `pcm_update_cyclist_ratings`) mutate the in-memory copy and
serialize it to a **new** `.cdb` via `writeCdb`, which refuses to overwrite the
source or any existing file. Any new tool must keep this never-touch-the-source
guarantee.

### Terminology

Two words, deliberately not interchangeable:

- **database** — a `.cdb` file. Cyanide's binary database format, and what every
tool but one actually operates on. It may be a player save, an official release
or a community update; nothing downstream cares which. The input parameter is
`databasePath`, and the internals live in `src/cdb.ts`.
- **save** — a `.cdb` the *game itself wrote* as the player played,
discovered under a PCM edition's `Cloud/` folder. This meaning is confined to
`src/saves.ts` and `pcm_list_saves`.

So `pcm_list_saves` finds the player's saves; everything else takes any database.
Do not reintroduce "save" as a synonym for `.cdb` — the test fixtures are official
releases, not saves, which is precisely the case the old naming got wrong.

## Stack

- **Runtime/lang:** Node.js (ESM, `bundler` module resolution), TypeScript (strict).
- **MCP:** `@modelcontextprotocol/sdk` — `McpServer` + `StdioServerTransport`.
- **Save parsing:** `cdb-converter` (`cdbToSql`) + `sql.js` (in-memory SQLite).
- **Database parsing:** `cdb-converter` (`cdbToSql`) + `sql.js` (in-memory SQLite).
- **Schemas:** `zod` for tool input/output schemas.
- **Build:** `tsup` → `dist/` (ESM output; `.d.ts` currently disabled). **Test:** `vitest`.
**Lint/format:** `biome`.
Expand All @@ -28,58 +43,56 @@ never-touch-the-source guarantee.
```
src/
index.ts # entrypoint: builds McpServer, registers tools, connects stdio
saves.ts # discover .cdb saves on disk and validate paths passed by tools
save-db.ts # everything touching the database: open a save in memory, serialize an edited copy, schema/game-date introspection
cdb.ts # everything touching a .cdb: validate a path, open it in memory, serialize an edited copy, schema/game-date introspection
saves.ts # locate the player's saves across installed PCM editions
helpers.ts # cross-cutting utilities: MCP tool responses, SQL statement parsing/errors, dates, startlist XML
schemas/
cyclist.ts # shared cyclist ratings schema and its SQL read/write mappings
tools/
index.ts # wires every tool onto the server
list-saves.ts # pcm_list_saves
validate-save.ts # pcm_validate_save
get-save-schema.ts # pcm_get_save_schema
validate-database.ts # pcm_validate_database
list-tables.ts # pcm_list_tables
get-table-schema.ts # pcm_get_table_schema
get-player-info.ts # pcm_get_player_info
get-team-roster.ts # pcm_get_team_roster
search-cyclist.ts # pcm_search_cyclist
search-team.ts # pcm_search_team
query-save.ts # pcm_query_save
update-save.ts # pcm_update_save
query-database.ts # pcm_query_database
update-database.ts # pcm_update_database
update-cyclist-ratings.ts # pcm_update_cyclist_ratings
generate-startlist-xml.ts # pcm_generate_startlist_xml
test/ # vitest specs (test/**/*.test.ts)
```

## Tools

All tools are prefixed with `pcm_`. Read tools carry `readOnlyHint: true` / `destructiveHint: false` annotations so clients can auto-approve them. The two write tools (`pcm_update_save`, `pcm_update_cyclist_ratings`) carry `readOnlyHint: false`, but also `destructiveHint: false`: `writeSaveDb` refuses `outputPath === savePath` and refuses to overwrite an existing file at `outputPath`, so the operation can only ever create a brand-new `.cdb` — it never destroys existing data.
All tools are prefixed with `pcm_`. Every tool but `pcm_list_saves` takes an absolute `databasePath`. Read tools carry `readOnlyHint: true` / `destructiveHint: false` annotations so clients can auto-approve them. The two write tools (`pcm_update_database`, `pcm_update_cyclist_ratings`) carry `readOnlyHint: false`, but also `destructiveHint: false`: `writeCdb` refuses `outputPath === databasePath` and refuses to overwrite an existing file at `outputPath`, so the operation can only ever create a brand-new `.cdb` — it never destroys existing data.

| Tool | Purpose |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pcm_list_saves` | Discover `.cdb` careers by scanning `Pro Cycling Manager <year>/Cloud` under `%APPDATA%` (**Windows only**). |
| `pcm_validate_save` | Validate a `.cdb` path and return metadata. Stateless — the path must be kept in conversation context for later tools. |
| `pcm_get_save_schema` | List all tables (id + name) in a save via `DB_STRUCTURE`. |
| `pcm_list_saves` | Discover the player's saves by scanning `Pro Cycling Manager <year>/Cloud` under `%APPDATA%`, across every installed edition (**Windows only**). The only tool that is about saves specifically. |
| `pcm_validate_database` | Validate a `.cdb` path and return metadata. Stateless — the path must be kept in conversation context for later tools. |
| `pcm_list_tables` | List all tables (id + name) via `DB_STRUCTURE`. |
| `pcm_get_table_schema` | Inspect one table: columns (name, type, NOT NULL, PK) + row count. |
| `pcm_get_player_info` | Active human player + team (joins `GAM_user` `game_i_active = 1` with `DYN_team`). |
| `pcm_get_team_roster` | Team roster (defaults to active player's team). Joins `DYN_cyclist` with active `DYN_contract_cyclist` + `STA_type_rider`: name, country, age, type, overall, contract end, wage, value, plus per-terrain ratings (flat). Errors on unknown `teamId`. |
| `pcm_search_cyclist` | Search cyclist by first/last name (partial, case-insensitive). |
| `pcm_search_team` | Search team by name (partial, case-insensitive; matches full name and short name). |
| `pcm_query_save` | Run a single read-only `SELECT`/`WITH … SELECT`. Write/DDL rejected; results capped (default 100, max 1000). |
| `pcm_update_save` | Apply a single `INSERT`/`UPDATE`/`DELETE` to a save and write the result to a **new** `.cdb` (`outputPath` must differ from `savePath`). SELECT/DDL/stacked statements rejected. |
| `pcm_query_database` | Run a single read-only `SELECT`/`WITH … SELECT`. Write/DDL rejected; results capped (default 100, max 1000). |
| `pcm_update_database` | Apply a single `INSERT`/`UPDATE`/`DELETE` and write the result to a **new** `.cdb` (`outputPath` must differ from `databasePath`). SELECT/DDL/stacked statements rejected. |
| `pcm_update_cyclist_ratings` | Change one or more `charac_i_*` ratings of a cyclist (by `IDcyclist`, ratings 50–85) and write the result to a **new** `.cdb`. Returns the cyclist's full ratings after the update. |
| `pcm_generate_startlist_xml` | Build a PCM startlist XML from teams + rosters; derives the file name from `STA_race.gene_sz_filename` for the given `IDrace`. |

## Conventions

- **State is in the conversation, not the server.** Tools are stateless; every
save-reading tool takes an absolute `savePath` and re-validates it via
`validateSave`. There is no "current save".
- **Use `withSaveDb` for new save-reading tools.** It centralises validate →
read → convert → run → always-close. Some existing tools (`get_save_schema`,
`get_player_info`, `query_save`) still inline this boilerplate; prefer
`withSaveDb` and consider migrating them when touched.
database-reading tool takes an absolute `databasePath` and re-validates it via
`validateCdb`. There is no "current database".
- **Use `withCdb` for new database-reading tools.** It centralises validate →
read → convert → run → always-close.
- **Read-only is enforced defensively** even though the DB is in-memory — see
`assertReadOnlyQuery` in `query-save.ts` (single statement, SELECT/WITH only,
`assertReadOnlyQuery` in `query-database.ts` (single statement, SELECT/WITH only,
forbidden-keyword guard).
- **Guard against SQL injection** when interpolating identifiers: validate table
names against `DB_STRUCTURE` before building queries (see `get_table_schema`).
Expand All @@ -91,7 +104,7 @@ All tools are prefixed with `pcm_`. Read tools carry `readOnlyHint: true` / `des
- **Tool naming** — all tools are prefixed with `pcm_` (e.g. `pcm_list_saves`) to
avoid conflicts when used alongside other MCP servers.
- **Platform:** auto-discovery is Windows-only. On macOS/Linux (Wine/Proton),
`pcm_list_saves`/`getPcmRoot` throw — pass an absolute `.cdb` path to `pcm_validate_save`.
`pcm_list_saves`/`getPcmRoot` throw — pass an absolute `.cdb` path to `pcm_validate_database`.
- **Logging** must go to `stderr` (`console.error`); stdout is the MCP transport.
- **Country fields** must use `STA_country.gene_sz_flag` (human-readable name, e.g.
`France`), never `STA_country.CONSTANT` (internal constant). Keep this consistent
Expand Down
Loading