From b2d2cdea6b3a0930dad9249d24690a625667e1ee Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Sun, 5 Jul 2026 08:48:06 -0400 Subject: [PATCH 1/5] fix: use full pcm_select_save tool name in error messages Error messages and a doc comment in saves.ts referred to the tool as select_save, but the registered tool name is pcm_select_save. Agents follow these messages to pick the next tool, so use the exact name. Co-Authored-By: Claude Fable 5 --- src/saves.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/saves.ts b/src/saves.ts index 6f256e9..0d8d05c 100644 --- a/src/saves.ts +++ b/src/saves.ts @@ -35,7 +35,7 @@ const CLOUD_DIR = "Cloud"; * a SteamID64 or a profile name). On macOS/Linux the * saves live inside a Wine/Proton prefix that we can't reliably locate, so * auto-discovery is unsupported there — pass an absolute `.cdb` path to - * `select_save` instead. + * `pcm_select_save` instead. * * @throws on non-Windows platforms. */ @@ -44,7 +44,7 @@ export function getPcmRoot(): string { throw new Error( "Pro Cycling Manager save auto-discovery is only supported on Windows. " + "On macOS/Linux the saves live inside a Wine/Proton prefix — " + - "pass an absolute .cdb path to select_save instead.", + "pass an absolute .cdb path to pcm_select_save instead.", ); } return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"); @@ -128,7 +128,7 @@ export async function listSaves( throw new Error( `No Pro Cycling Manager data found. Expected a "Pro Cycling Manager " folder under: ${root}. ` + "PCM may not be installed, or its saves live in a custom location — " + - "pass an absolute .cdb path to select_save instead.", + "pass an absolute .cdb path to pcm_select_save instead.", ); } From 4e54a41eaf50cc59d0f602b8f7a5338f13f86262 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Sun, 5 Jul 2026 08:51:03 -0400 Subject: [PATCH 2/5] refactor!: rename pcm_select_save to pcm_validate_save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool is a stateless validator — it stores nothing and returns the save's metadata — but "select" implied a stateful session selection. Rename the tool, its register function and source file, and update the README/AGENTS docs and error messages accordingly. BREAKING CHANGE: clients calling pcm_select_save must switch to pcm_validate_save. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 6 +++--- README.md | 6 +++--- src/saves.ts | 6 +++--- src/tools/index.ts | 4 ++-- src/tools/{select-save.ts => validate-save.ts} | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename src/tools/{select-save.ts => validate-save.ts} (92%) diff --git a/AGENTS.md b/AGENTS.md index d92daa0..43a005d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ src/ tools/ index.ts # registerTools() — wires every tool onto the server list-saves.ts # pcm_list_saves - select-save.ts # pcm_select_save + validate-save.ts # pcm_validate_save get-save-schema.ts # pcm_get_save_schema get-table-schema.ts # pcm_get_table_schema get-player-info.ts # pcm_get_player_info @@ -53,7 +53,7 @@ All tools are prefixed with `pcm_` and carry `readOnlyHint: true` / `destructive | Tool | Purpose | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pcm_list_saves` | Discover `.cdb` careers by scanning `Pro Cycling Manager /Cloud` under `%APPDATA%` (**Windows only**). | -| `pcm_select_save` | Validate a `.cdb` path and return metadata. Stateless — the path must be kept in conversation context for later tools. | +| `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_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`). | @@ -85,7 +85,7 @@ All tools are prefixed with `pcm_` and carry `readOnlyHint: true` / `destructive - **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_select_save`. + `pcm_list_saves`/`getPcmRoot` throw — pass an absolute `.cdb` path to `pcm_validate_save`. - **Logging** must go to `stderr` (`console.error`); stdout is the MCP transport. ## README maintenance diff --git a/README.md b/README.md index c202b4b..7959928 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ PCM only ships on Windows, where careers live under: %APPDATA%/Pro Cycling Manager /Cloud// ``` -Auto-discovery via `pcm_list_saves` is therefore **Windows only**. On macOS/Linux the saves live inside a Wine/Proton prefix that can't be reliably located — pass an absolute `.cdb` path directly to `pcm_select_save` instead. +Auto-discovery via `pcm_list_saves` is therefore **Windows only**. On macOS/Linux the saves live inside a Wine/Proton prefix that can't be reliably located — pass an absolute `.cdb` path directly to `pcm_validate_save` instead. ## Available tools @@ -86,7 +86,7 @@ All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` is read- | Tool | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **pcm_list_saves** | Discover PCM `.cdb` career save files on this machine by scanning the `Pro Cycling Manager /Cloud` folders under `%APPDATA%` (Windows only). Returns each save's absolute path, file name, last modified date and size (newest first). | -| **pcm_select_save** | Validate that an absolute path points to an existing `.cdb` save file and return its metadata. Stateless — keep the returned path in conversation context to pass to later tools. | +| **pcm_validate_save** | Validate that an absolute path points to an existing `.cdb` save file and return its metadata. Stateless — keep the returned path in conversation context to pass to later tools. | | **pcm_get_save_schema** | List every table inside a `.cdb` save file, with its ID and name, plus the total table count. | | **pcm_get_table_schema** | Inspect a single table by name. Returns its columns (name, SQL type, NOT NULL and primary key flags) and its row count. Use `pcm_get_save_schema` first to discover available table names. | | **pcm_get_player_info** | Get the active human player and their team from a save file. Returns the player login plus team details (name, resolved division name, resolved country name, evaluation and manager). | @@ -101,7 +101,7 @@ All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` is read- Tools are **stateless**: there is no "current save" held by the server. Every tool takes an absolute `savePath`, re-validates it, and re-reads the `.cdb` from disk into a fresh in-memory SQLite database (via [`cdb-converter`](https://www.npmjs.com/package/cdb-converter) + [`sql.js`](https://www.npmjs.com/package/sql.js)) for each call. The source save on disk is never mutated: read tools only ever read it, and `pcm_update_save` writes its changes to a separate output `.cdb`. A typical flow is: -1. `pcm_list_saves` (Windows) or `pcm_select_save` with an explicit path to locate a save. +1. `pcm_list_saves` (Windows) or `pcm_validate_save` with an explicit path to locate a save. 2. `pcm_search_cyclist`, `pcm_get_team_roster`, `pcm_query_save`, … to explore it. 3. `pcm_generate_startlist_xml` to produce a startlist file for a race, or `pcm_update_save` to write an edited copy of the save. diff --git a/src/saves.ts b/src/saves.ts index 0d8d05c..222c6ec 100644 --- a/src/saves.ts +++ b/src/saves.ts @@ -35,7 +35,7 @@ const CLOUD_DIR = "Cloud"; * a SteamID64 or a profile name). On macOS/Linux the * saves live inside a Wine/Proton prefix that we can't reliably locate, so * auto-discovery is unsupported there — pass an absolute `.cdb` path to - * `pcm_select_save` instead. + * `pcm_validate_save` instead. * * @throws on non-Windows platforms. */ @@ -44,7 +44,7 @@ export function getPcmRoot(): string { throw new Error( "Pro Cycling Manager save auto-discovery is only supported on Windows. " + "On macOS/Linux the saves live inside a Wine/Proton prefix — " + - "pass an absolute .cdb path to pcm_select_save instead.", + "pass an absolute .cdb path to pcm_validate_save instead.", ); } return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"); @@ -128,7 +128,7 @@ export async function listSaves( throw new Error( `No Pro Cycling Manager data found. Expected a "Pro Cycling Manager " folder under: ${root}. ` + "PCM may not be installed, or its saves live in a custom location — " + - "pass an absolute .cdb path to pcm_select_save instead.", + "pass an absolute .cdb path to pcm_validate_save instead.", ); } diff --git a/src/tools/index.ts b/src/tools/index.ts index ae3c7ef..61c6df5 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerListSaves } from "./list-saves"; -import { registerSelectSave } from "./select-save"; +import { registerValidateSave } from "./validate-save"; import { registerGetSaveSchema } from "./get-save-schema"; import { registerGetTableSchema } from "./get-table-schema"; import { registerGetPlayerInfo } from "./get-player-info"; @@ -13,7 +13,7 @@ import { registerSearchTeam } from "./search-team"; export function registerTools(server: McpServer): void { registerListSaves(server); - registerSelectSave(server); + registerValidateSave(server); registerGetSaveSchema(server); registerGetTableSchema(server); registerGetPlayerInfo(server); diff --git a/src/tools/select-save.ts b/src/tools/validate-save.ts similarity index 92% rename from src/tools/select-save.ts rename to src/tools/validate-save.ts index 0eeb277..14e890c 100644 --- a/src/tools/select-save.ts +++ b/src/tools/validate-save.ts @@ -14,11 +14,11 @@ const outputSchema = z.object({ sizeBytes: z.number().describe("File size in bytes"), }); -export function registerSelectSave(server: McpServer): void { +export function registerValidateSave(server: McpServer): void { server.registerTool( - "pcm_select_save", + "pcm_validate_save", { - title: "Select PCM save", + title: "Validate PCM save", description: "Validate that an absolute path points to an existing Pro Cycling Manager `.cdb` save file and return its metadata. Stateless: nothing is stored — keep the returned path in conversation context to pass to later tools.", inputSchema: { From dae3bb456b97f16800e2c86f2b116097a5e94fbb Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Sun, 5 Jul 2026 09:01:41 -0400 Subject: [PATCH 3/5] fix: realign markdown tables after pcm_validate_save rename Column padding in AGENTS.md and README.md wasn't adjusted when pcm_select_save was renamed to pcm_validate_save, leaving the table rows misaligned. --- AGENTS.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 43a005d..c5fbdbc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ All tools are prefixed with `pcm_` and carry `readOnlyHint: true` / `destructive | Tool | Purpose | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pcm_list_saves` | Discover `.cdb` careers by scanning `Pro Cycling Manager /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_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_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`). | diff --git a/README.md b/README.md index 7959928..9ff7a6a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` is read- | Tool | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **pcm_list_saves** | Discover PCM `.cdb` career save files on this machine by scanning the `Pro Cycling Manager /Cloud` folders under `%APPDATA%` (Windows only). Returns each save's absolute path, file name, last modified date and size (newest first). | -| **pcm_validate_save** | Validate that an absolute path points to an existing `.cdb` save file and return its metadata. Stateless — keep the returned path in conversation context to pass to later tools. | +| **pcm_validate_save** | Validate that an absolute path points to an existing `.cdb` save file and return its metadata. Stateless — keep the returned path in conversation context to pass to later tools. | | **pcm_get_save_schema** | List every table inside a `.cdb` save file, with its ID and name, plus the total table count. | | **pcm_get_table_schema** | Inspect a single table by name. Returns its columns (name, SQL type, NOT NULL and primary key flags) and its row count. Use `pcm_get_save_schema` first to discover available table names. | | **pcm_get_player_info** | Get the active human player and their team from a save file. Returns the player login plus team details (name, resolved division name, resolved country name, evaluation and manager). | @@ -94,7 +94,7 @@ All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` is read- | **pcm_get_team_roster** | List a team's roster (defaults to the active player's team when `teamId` is omitted). Joins DYN_cyclist with its active DYN_contract_cyclist and STA_type_rider; per rider returns name, country, age (derived from birth date and the current game date), rider type, overall ability, contract end year, wage, market value and all per-terrain ability ratings. Ordered by overall ability, highest first. Errors if `teamId` does not exist. | | **pcm_search_team** | Search for a team by name (case-insensitive partial match against both the full name and short name). Returns up to 10 matches with the resolved division name, country name, evaluation and general manager. | | **pcm_query_save** | Run a read-only SQL query (`SELECT` / `WITH … SELECT` only) against any table in a save file. Write/DDL statements are rejected. Results are capped (default 100, max 1000 rows). | -| **pcm_update_save** | Apply a single `INSERT`/`UPDATE`/`DELETE` statement to a save and write the modified database to a **new** `.cdb` at `outputPath`. The source save is never overwritten (`outputPath` must differ from `savePath`); `SELECT`, schema changes (`DROP`/`CREATE`/`ALTER`) and stacked statements are rejected. Returns the written path and the number of rows changed. | +| **pcm_update_save** | Apply a single `INSERT`/`UPDATE`/`DELETE` statement to a save and write the modified database to a **new** `.cdb` at `outputPath`. The source save is never overwritten (`outputPath` must differ from `savePath`); `SELECT`, schema changes (`DROP`/`CREATE`/`ALTER`) and stacked statements are rejected. Returns the written path and the number of rows changed. | | **pcm_generate_startlist_xml** | Generate a PCM startlist XML document from a list of teams and their cyclist rosters. Looks up the race by `IDrace` in the save to derive the output file name from `STA_race.gene_sz_filename` (e.g. `c0_almeria.xml`), and returns both the file name and the XML as text. Team and cyclist IDs map to `DYN_team.IDteam` / `DYN_cyclist.IDcyclist` (look them up with `pcm_search_cyclist` or `pcm_query_save`). | ## How it works From 130155342a68464f209c76d92531a32bd7a230ec Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Sun, 5 Jul 2026 09:20:10 -0400 Subject: [PATCH 4/5] fix: update destructiveHint to false in annotations for cyclist ratings and save tools --- src/tools/update-cyclist-ratings.ts | 2 +- src/tools/update-save.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/update-cyclist-ratings.ts b/src/tools/update-cyclist-ratings.ts index 16aab0e..1d0120f 100644 --- a/src/tools/update-cyclist-ratings.ts +++ b/src/tools/update-cyclist-ratings.ts @@ -97,7 +97,7 @@ export function registerUpdateCyclistRatings(server: McpServer): void { outputSchema, annotations: { readOnlyHint: false, - destructiveHint: true, + destructiveHint: false, idempotentHint: false, openWorldHint: false, }, diff --git a/src/tools/update-save.ts b/src/tools/update-save.ts index 2b2a8b3..2cd1ef6 100644 --- a/src/tools/update-save.ts +++ b/src/tools/update-save.ts @@ -38,7 +38,7 @@ export function registerUpdateSave(server: McpServer): void { outputSchema, annotations: { readOnlyHint: false, - destructiveHint: true, + destructiveHint: false, idempotentHint: false, openWorldHint: false, }, From b97a5f19d0c52bc224e6082ffca64745c15bb903 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Sun, 5 Jul 2026 09:34:03 -0400 Subject: [PATCH 5/5] fix: clarify tool annotations and safety guarantees in documentation --- AGENTS.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5c4bb5e..ecd528a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ 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` / `destructiveHint: true`. +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. | Tool | Purpose | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/README.md b/README.md index 51b4e5b..abd2455 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - **Raw SQL** — run guarded, read-only `SELECT` queries against any table in the save. - **Guarded edits** — apply a single `INSERT`/`UPDATE`/`DELETE`, or edit a cyclist's ratings directly, and write the result to a new `.cdb`, never touching the original. - **Startlist export** — generate a PCM-ready startlist XML from a set of teams and rosters. -- **Safe by design** — read tools are annotated `readOnlyHint: true` for auto-approval; the write tools write only to a separate output file and never overwrite an existing one. +- **Safe by design** — read tools are auto-approved by MCP clients; the write tools write only to a separate output file and never overwrite an existing one. ## Getting started @@ -81,7 +81,7 @@ Auto-discovery via `pcm_list_saves` is therefore **Windows only**. On macOS/Linu ## Available tools -All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` and `pcm_update_cyclist_ratings` is read-only and carries `readOnlyHint: true` so clients like Claude Desktop can approve them automatically without a confirmation prompt. The write tools never overwrite the source save. +All tools are prefixed with `pcm_`. Every tool except `pcm_update_save` and `pcm_update_cyclist_ratings` is read-only, so clients like Claude Desktop can approve them automatically without a confirmation prompt. The two write tools never overwrite the source save or any existing file — they can only create a new `.cdb`. | Tool | Description | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |