diff --git a/AGENTS.md b/AGENTS.md index ed138c4..818fa7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,7 @@ src/ get-table-schema.ts # pcm_get_table_schema get-player-info.ts # pcm_get_player_info search-cyclist.ts # pcm_search_cyclist + search-team.ts # pcm_search_team query-save.ts # pcm_query_save test/ # vitest specs (test/**/*.test.ts) ``` @@ -53,6 +54,7 @@ All tools are prefixed with `pcm_` and carry `readOnlyHint: true` / `destructive | `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_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). | ## Conventions diff --git a/README.md b/README.md index 171a073..34eaff7 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ All tools are read-only and carry `readOnlyHint: true`, so clients like Claude D | **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). | | **pcm_search_cyclist** | Search for a cyclist by first name and/or last name (case-insensitive partial match). Returns up to 10 matches with all ratings (plain, mountain, medium mountain, downhilling, cobble, time trial, prologue, sprint, acceleration, endurance, resistance, recuperation, hill, baroudeur, current ability) and the resolved country name. `mediumMountain` and `currentAbility` are `null` on saves that pre-date those columns. | +| **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). | ## Development diff --git a/src/tools/index.ts b/src/tools/index.ts index f3a4fea..d1da6ad 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -6,6 +6,7 @@ import { registerGetTableSchema } from "./get-table-schema"; import { registerGetPlayerInfo } from "./get-player-info"; import { registerQuerySave } from "./query-save"; import { registerSearchCyclist } from "./search-cyclist"; +import { registerSearchTeam } from "./search-team"; export function registerTools(server: McpServer): void { registerListSaves(server); @@ -15,4 +16,5 @@ export function registerTools(server: McpServer): void { registerGetPlayerInfo(server); registerQuerySave(server); registerSearchCyclist(server); + registerSearchTeam(server); } diff --git a/src/tools/search-team.ts b/src/tools/search-team.ts new file mode 100644 index 0000000..6a80462 --- /dev/null +++ b/src/tools/search-team.ts @@ -0,0 +1,102 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { withSaveDb } from "../save-db"; + +const teamSchema = z.object({ + id: z.number().describe("Team ID (IDteam)"), + name: z.string().describe("Team name (gene_sz_name)"), + shortName: z.string().describe("Team short name (gene_sz_shortname)"), + division: z + .string() + .nullable() + .describe("Division name (STA_division.CONSTANT via fkIDdivision)"), + country: z + .string() + .nullable() + .describe("Country name (STA_country.gene_sz_flag via fkIDcountry)"), + evaluation: z + .number() + .describe("Team current evaluation (value_f_current_evaluation)"), + manager: z + .string() + .describe("General manager name (gene_sz_manager_general)"), +}); + +const outputSchema = z.object({ + teams: z.array(teamSchema).describe("Matching teams"), +}); + +export function registerSearchTeam(server: McpServer): void { + server.registerTool( + "pcm_search_team", + { + title: "Search PCM team by name", + description: + "Search for a team in a Pro Cycling Manager `.cdb` save file by name (case-insensitive partial match against both the full name and the short name). Returns up to 10 matching teams with their division name, country name, evaluation and general manager.", + inputSchema: { + savePath: z.string().describe("Absolute path to the .cdb save file"), + name: z + .string() + .describe( + "Team name to search for (partial match, case-insensitive)", + ), + }, + outputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async ({ savePath, name }) => + withSaveDb(savePath, (db) => { + const stmt = db.prepare( + `SELECT + t.IDteam AS id, + t.gene_sz_name AS name, + t.gene_sz_shortname AS shortName, + d.CONSTANT AS division, + c.gene_sz_flag AS country, + t.value_f_current_evaluation AS evaluation, + t.gene_sz_manager_general AS manager + FROM DYN_team t + LEFT JOIN STA_division d ON t.fkIDdivision = d.IDdivision + LEFT JOIN STA_country c ON t.fkIDcountry = c.IDcountry + WHERE LOWER(t.gene_sz_name) LIKE LOWER(:name) + OR LOWER(t.gene_sz_shortname) LIKE LOWER(:name) + LIMIT 10`, + ); + + const teams: z.infer[] = []; + try { + const query = name.trim(); + if (query.length === 0) { + throw new Error("Provide a non-empty name to search for."); + } + + stmt.bind({ ":name": `%${query}%` }); + + while (stmt.step()) { + const row = stmt.getAsObject(); + teams.push({ + id: Number(row.id), + name: String(row.name), + shortName: String(row.shortName), + division: row.division != null ? String(row.division) : null, + country: row.country != null ? String(row.country) : null, + evaluation: Number(row.evaluation), + manager: String(row.manager), + }); + } + } finally { + stmt.free(); + } + + const output: z.infer = { + teams, + }; + return output; + }), + ); +}