@chessceo/mcp 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +196 -3
  3. package/package.json +15 -3
package/README.md CHANGED
@@ -71,6 +71,18 @@ Ask your model:
71
71
  - *"Are there any live tournaments right now with Hikaru Nakamura?"*
72
72
  - *"From the position after 1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6, what's the top continuation across the whole database?"*
73
73
 
74
+ ## Prep workflow (built-in prompts)
75
+
76
+ For MCP hosts that show prompts in a slash-menu (Claude Desktop, Cursor, Claude Code), three pre-baked prompts are included so users get a proper preparation workflow without prompt-engineering their own:
77
+
78
+ | Prompt | Purpose |
79
+ |---|---|
80
+ | `prepare_for_game(me, opponent, my_color?, time_control?)` | Full pre-match workflow: resolves both players, weights games by recency + format (classical OTB > rapid/blitz > online), walks the opponent's repertoire looking for lines where they score under 40%, checks head-to-head, and delivers a concrete plan with the moves to steer toward the opponent's weak points. |
81
+ | `scout_player(player)` | Deep scouting report on one player — style, top openings, recent form, biggest wins and losses, recurring weaknesses. |
82
+ | `head_to_head_briefing(player_a, player_b)` | One-paragraph read on the history between two players — who has the edge, dominant openings, style clash, current form. |
83
+
84
+ Pick one from the host's slash-menu, fill in the arguments, and the model does the rest.
85
+
74
86
  ## Remote MCP (chess.ceo-hosted)
75
87
 
76
88
  You can also connect to chess.ceo's hosted instance and skip installing anything:
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@
7
7
  // endpoints — see the public contract at https://chess.ceo/llms.txt.
8
8
  // No API key, no auth, no state; the API's own rate limits apply.
9
9
  import { createServer as createHttpServer } from "node:http";
10
+ import { Chess } from "chess.js";
10
11
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
14
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
14
15
  const BASE = process.env.CHESSCEO_BASE_URL ?? "https://chess.ceo";
15
16
  const UA = `chessceo-mcp/${process.env.npm_package_version ?? "0.1.0"} (+https://chess.ceo)`;
16
17
  // ── HTTP ────────────────────────────────────────────────────────────
@@ -71,7 +72,7 @@ const TOOLS = [
71
72
  },
72
73
  {
73
74
  name: "get_player_preparation",
74
- description: "For a given player, colour and starting position, return both the moves the player actually chose (frequency + win rate) and the underlying games. Position is specified either as a move sequence in SAN (`line`) or a raw FEN. Use `line` iteratively to walk the opening tree: call once with empty `line`, pick a move, call again with `line` extended by that move, etc.",
75
+ description: "For a given player, colour and starting position, return both the moves the player actually chose (frequency + win rate) and the underlying games. Position is specified either as a move sequence in SAN (`line`) or a raw FEN. Use `line` iteratively to walk the opening tree: call once with empty `line`, pick a move, call again with `line` extended by that move, etc. When preparing for a real game, weight recent games (last 12-24 months) more heavily than old ones, classical over-the-board > rapid/blitz > online, and look for variations the player scores poorly in (below ~40%) or plays with less variety (shallower prep).",
75
76
  inputSchema: {
76
77
  type: "object",
77
78
  properties: {
@@ -161,6 +162,27 @@ const TOOLS = [
161
162
  required: ["fide_id"],
162
163
  },
163
164
  },
165
+ {
166
+ name: "prep_snapshot",
167
+ description: "One call, three parallel fetches at the same position: opponent's stats on their side, your stats on your side, and the 11.7M-game general database at that position. Use this while walking the opening tree — one round trip instead of three separate calls, and you can compare the three views directly (e.g. opponent has 2 games here but the general DB has 8k → prep candidate).",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ fide_id_me: { type: "integer", description: "Your FIDE ID." },
172
+ fide_id_opponent: { type: "integer", description: "Opponent's FIDE ID." },
173
+ my_color: { type: "string", enum: ["white", "black"], description: "The colour YOU will play." },
174
+ line: {
175
+ type: "string",
176
+ description: "Move sequence in SAN, space-separated. Empty = starting position. Example: 'e4 c5 Nf3'. Either line or fen (or neither for the starting position).",
177
+ },
178
+ fen: {
179
+ type: "string",
180
+ description: "Alternative to line — raw FEN of the target position.",
181
+ },
182
+ },
183
+ required: ["fide_id_me", "fide_id_opponent", "my_color"],
184
+ },
185
+ },
164
186
  ];
165
187
  async function callTool(name, args) {
166
188
  switch (name) {
@@ -204,13 +226,184 @@ async function callTool(name, args) {
204
226
  case "list_player_live_tournaments":
205
227
  // Note: snake_case fide_id, unlike the prep endpoints. Documented quirk.
206
228
  return get("/api/chess/live/player", { fide_id: Number(args.fide_id) });
229
+ case "prep_snapshot": {
230
+ const me = Number(args.fide_id_me);
231
+ const opp = Number(args.fide_id_opponent);
232
+ const myColor = String(args.my_color);
233
+ const oppColor = myColor === "white" ? "black" : "white";
234
+ const line = typeof args.line === "string" ? args.line.trim() : "";
235
+ let fen = typeof args.fen === "string" ? args.fen.trim() : "";
236
+ // General DB lookup needs a FEN. If we only have a line, compute it
237
+ // locally with chess.js — one dep, keeps the three data-fetches truly
238
+ // parallel instead of doing a preliminary round-trip.
239
+ if (!fen) {
240
+ const board = new Chess();
241
+ if (line.length > 0) {
242
+ for (const raw of line.split(/\s+/)) {
243
+ // Tolerant of move-number tokens like "1." / "12..." that some
244
+ // clients include; chess.js rejects those outright.
245
+ const san = raw.replace(/^\d+\.+/, "");
246
+ if (!san)
247
+ continue;
248
+ try {
249
+ board.move(san);
250
+ }
251
+ catch {
252
+ throw new Error(`bad SAN token '${raw}' in line`);
253
+ }
254
+ }
255
+ }
256
+ fen = board.fen();
257
+ }
258
+ const prepParams = (fideId, color) => ({
259
+ fideId,
260
+ color,
261
+ compact: "true",
262
+ ...(line.length > 0 ? { line } : { fen }),
263
+ });
264
+ const [opponent, you, general] = await Promise.all([
265
+ get("/api/chess/prep/by-player", prepParams(opp, oppColor)),
266
+ get("/api/chess/prep/by-player", prepParams(me, myColor)),
267
+ get("/api/chess/database/main", { fen, limit: 20, sort: "relevance" }),
268
+ ]);
269
+ return {
270
+ position: { line, fen, my_color: myColor },
271
+ opponent,
272
+ you,
273
+ general,
274
+ };
275
+ }
207
276
  default:
208
277
  throw new Error(`Unknown tool: ${name}`);
209
278
  }
210
279
  }
280
+ // ── Prompt templates ───────────────────────────────────────────────
281
+ //
282
+ // MCP prompts are pre-baked instructions the host surfaces in a slash-menu
283
+ // (Claude Desktop, Cursor, etc.). Users pick one and the templated content
284
+ // gets injected into the conversation. Perfect for workflows the LLM
285
+ // wouldn't reliably discover from tool descriptions alone — chess prep
286
+ // especially, where "call the right tools in the right order weighted by
287
+ // recency and format" is non-obvious.
288
+ const PROMPTS = [
289
+ {
290
+ name: "prepare_for_game",
291
+ description: "Prep workflow for an upcoming chess game. Walks both players' repertoires and identifies where the opponent is weakest.",
292
+ arguments: [
293
+ { name: "me", description: "Your name (or FIDE ID)", required: true },
294
+ { name: "opponent", description: "Opponent's name (or FIDE ID)", required: true },
295
+ { name: "my_color", description: "The color you'll be playing: 'white' or 'black'. Optional — if you don't know yet, ask.", required: false },
296
+ { name: "time_control", description: "Optional: 'classical', 'rapid', 'blitz'. Weights which of the opponent's games matter most.", required: false },
297
+ ],
298
+ },
299
+ {
300
+ name: "scout_player",
301
+ description: "Deep scouting report on one player. Their style, top openings, recent form, and where they've been beaten.",
302
+ arguments: [
303
+ { name: "player", description: "Player name or FIDE ID", required: true },
304
+ ],
305
+ },
306
+ {
307
+ name: "head_to_head_briefing",
308
+ description: "Briefing on the history between two players — who has the edge, what openings decide their meetings, style clash.",
309
+ arguments: [
310
+ { name: "player_a", description: "First player (name or FIDE ID)", required: true },
311
+ { name: "player_b", description: "Second player (name or FIDE ID)", required: true },
312
+ ],
313
+ },
314
+ ];
315
+ // The workflow text for prepare_for_game. Kept in one place so both the
316
+ // MCP prompt handler and the /prepare fallback can share it.
317
+ const PREP_WORKFLOW = (args) => {
318
+ const me = args.me ?? "the user";
319
+ const opp = args.opponent ?? "the opponent";
320
+ const color = args.my_color ? ` as ${args.my_color}` : "";
321
+ const tc = args.time_control ? ` in a ${args.time_control} game` : "";
322
+ return `You are preparing ${me} for a chess game against ${opp}${color}${tc}.
323
+
324
+ Preparation workflow — follow the steps in order and be explicit about which tools you called at each step:
325
+
326
+ 1. **Resolve both players.** Call \`search_player\` for "${me}" and "${opp}" to get their FIDE IDs. Confirm the identity — many players share names.
327
+
328
+ 2. **Understand the opponent.** Call \`get_player_profile\` for the opponent. Read out:
329
+ - Current classical / rapid / blitz ratings.
330
+ - Top openings as White and Black (from openingRepertoire).
331
+ - Career win / draw / loss splits — is the draw rate above ~40%? That's a stylistic hint (drawish opponents need to be unbalanced).
332
+ - Notable wins and worst losses — patterns?
333
+
334
+ 3. **Weight games by quality when interpreting the data.**
335
+ - Recent games (last 12-24 months) matter far more than old ones. Opening repertoires evolve.
336
+ - Classical over-the-board games are the strongest signal — that's what real preparation reveals.
337
+ - Rapid and blitz reveal what they play under time pressure but may include experiments.
338
+ - Online games are useful but noisier (blitz gambits, alt accounts).
339
+
340
+ 4. **Walk the opponent's repertoire against ${me}'s color.** Call \`get_player_preparation\` on the opponent for the color they'll have in this game. Iterate: start from move 1, pick the opponent's most-played reply, call again with \`line\` extended. Look for:
341
+ - **Weak lines**: variations where the opponent scores below 40% as their side.
342
+ - **Shallow lines**: openings the opponent has played only a few times — probably less deeply prepared.
343
+ - **Abandoned lines**: openings they used to play but stopped. Something went wrong; may not want to revisit.
344
+ - **Variety**: places where the opponent picks different moves game to game — those are branching points where they can't predict your prep.
345
+
346
+ 5. **Style considerations.**
347
+ - High draw rate → propose openings that unbalance early (Benoni, King's Indian, gambit lines).
348
+ - Sharp tactician → don't play their prepared attacks; steer toward quiet positional lines.
349
+ - Endgame strong → keep queens on and keep complications.
350
+
351
+ 6. **Cross-check head-to-head.** Call \`get_head_to_head\` on the two players. If they've met before, what openings decided those games? Anything the opponent showed only against ${me}?
352
+
353
+ 7. **Deliver a concrete plan.** Summarize:
354
+ - What the opponent will likely play on move 1 (with confidence level).
355
+ - The 2-3 branching points where the opponent is weakest for ${me}'s color.
356
+ - The concrete move sequence ${me} should aim for to steer into those positions.
357
+ - What to avoid — the opponent's strongest weapons.
358
+
359
+ Don't just dump data. Reason about it. Cite specific numbers (game counts, win rates, dates) so the user can trust your conclusions.`;
360
+ };
211
361
  // ── Server wiring ──────────────────────────────────────────────────
212
- const server = new Server({ name: "chessceo-mcp", version: process.env.npm_package_version ?? "0.1.0" }, { capabilities: { tools: {} } });
362
+ const server = new Server({ name: "chessceo-mcp", version: process.env.npm_package_version ?? "0.1.0" }, { capabilities: { tools: {}, prompts: {} } });
213
363
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
364
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
365
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
366
+ const { name, arguments: args } = req.params;
367
+ const promptArgs = {};
368
+ if (args)
369
+ for (const [k, v] of Object.entries(args))
370
+ promptArgs[k] = String(v);
371
+ let text;
372
+ switch (name) {
373
+ case "prepare_for_game":
374
+ text = PREP_WORKFLOW(promptArgs);
375
+ break;
376
+ case "scout_player": {
377
+ const p = promptArgs.player ?? "the player";
378
+ text = `Produce a scouting report on ${p}. Steps:
379
+ 1. \`search_player\` to get their FIDE ID.
380
+ 2. \`get_player_profile\` — pull rating history, career splits by color and time control, opening repertoire, opponent analysis, top events, notable wins and losses.
381
+ 3. Weight the data: recent (last 12-24 months) > older, classical OTB > rapid/blitz > online.
382
+ 4. \`get_player_preparation\` for both colors from the starting position to summarise their opening choices with actual frequencies and win rates.
383
+ 5. Deliver: current strength, characteristic openings, one-sentence style read, biggest wins, biggest losses / recurring weakness. Cite the numbers.`;
384
+ break;
385
+ }
386
+ case "head_to_head_briefing": {
387
+ const a = promptArgs.player_a ?? "player A";
388
+ const b = promptArgs.player_b ?? "player B";
389
+ text = `Briefing on the ${a} vs ${b} history. Steps:
390
+ 1. Resolve both FIDE IDs with \`search_player\`.
391
+ 2. \`get_head_to_head\` for the pair — pull overall + per-color W/D/L (from ${a}'s perspective), splits by time format, first / last meeting, most-played openings between them, average game length.
392
+ 3. Read the pattern: who has the edge, in which colour, in which time format. Which openings decide the meetings? Anything unusual — very drawish, very sharp, big rating gap?
393
+ 4. If either player is currently live in a tournament, note it with \`list_player_live_tournaments\`.
394
+ 5. Deliver a one-paragraph read: score, dominant openings, one-line style clash, current form.`;
395
+ break;
396
+ }
397
+ default:
398
+ throw new Error(`Unknown prompt: ${name}`);
399
+ }
400
+ return {
401
+ description: `chessceo prompt: ${name}`,
402
+ messages: [
403
+ { role: "user", content: { type: "text", text } },
404
+ ],
405
+ };
406
+ });
214
407
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
215
408
  const { name, arguments: args } = req.params;
216
409
  try {
package/package.json CHANGED
@@ -1,20 +1,31 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Model Context Protocol server for chess.ceo — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "chessceo-mcp": "dist/index.js"
8
8
  },
9
9
  "main": "dist/index.js",
10
- "files": ["dist", "README.md"],
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
11
14
  "scripts": {
12
15
  "build": "tsc",
13
16
  "dev": "tsc --watch",
14
17
  "start": "node dist/index.js",
15
18
  "prepublishOnly": "npm run build"
16
19
  },
17
- "keywords": ["mcp", "modelcontextprotocol", "chess", "chessceo", "llm", "claude", "fide"],
20
+ "keywords": [
21
+ "mcp",
22
+ "modelcontextprotocol",
23
+ "chess",
24
+ "chessceo",
25
+ "llm",
26
+ "claude",
27
+ "fide"
28
+ ],
18
29
  "author": "chess.ceo",
19
30
  "license": "MIT",
20
31
  "repository": {
@@ -30,6 +41,7 @@
30
41
  },
31
42
  "dependencies": {
32
43
  "@modelcontextprotocol/sdk": "^1.0.0",
44
+ "chess.js": "^1.4.0",
33
45
  "zod": "^3.23.0"
34
46
  },
35
47
  "devDependencies": {