@chessceo/mcp 0.1.0

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 +83 -0
  2. package/dist/index.js +228 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @chessceo/mcp
2
+
3
+ Model Context Protocol server for [chess.ceo](https://chess.ceo) — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts. Lets Claude, Cursor, and any other MCP host answer chess questions directly against real data instead of hallucinating.
4
+
5
+ No API key. No auth. No state. Free to use.
6
+
7
+ ## What it can do
8
+
9
+ The server exposes 8 tools that mirror the public GET API surface at `chess.ceo`:
10
+
11
+ | Tool | What it answers |
12
+ |---|---|
13
+ | `search_player` | "Find FIDE ID for Magnus Carlsen" |
14
+ | `get_player_profile` | "How strong is X, what do they play, who have they beaten" |
15
+ | `get_player_preparation` | "What does X play against 1.e4? What's their win rate with the Najdorf?" |
16
+ | `get_position_stats` | "From this position, which move scores best in the 11.7M-game database?" |
17
+ | `get_head_to_head` | "What's the record between X and Y?" |
18
+ | `list_live_tournaments` | "What's being broadcast live right now?" |
19
+ | `list_tournament_players` | "Who's playing in tournament T?" |
20
+ | `list_player_live_tournaments` | "Is X playing anywhere right now?" |
21
+
22
+ ## Install (Claude Desktop)
23
+
24
+ Add to your `claude_desktop_config.json` (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "chessceo": {
30
+ "command": "npx",
31
+ "args": ["-y", "@chessceo/mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Restart Claude Desktop. You should see the chess.ceo tools appear in the tool list at the bottom of the chat.
38
+
39
+ ## Install (Cursor)
40
+
41
+ Similar `~/.cursor/mcp.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "chessceo": {
47
+ "command": "npx",
48
+ "args": ["-y", "@chessceo/mcp"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Try it
55
+
56
+ Ask your model:
57
+
58
+ - *"Who has the better record against Magnus Carlsen: Ding Liren or Fabiano Caruana?"*
59
+ - *"What does Alireza Firouzja play with White against the Najdorf?"*
60
+ - *"Are there any live tournaments right now with Hikaru Nakamura?"*
61
+ - *"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?"*
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ git clone <this repo>
67
+ cd chessceo-mcp
68
+ npm install
69
+ npm run build # tsc → dist/
70
+ npm start # runs the server on stdio (for MCP hosts)
71
+ ```
72
+
73
+ Environment variable overrides:
74
+
75
+ - `CHESSCEO_BASE_URL` — override the API base (default `https://chess.ceo`). Useful for testing against staging.
76
+
77
+ ## What's under the hood
78
+
79
+ The chess.ceo public API is a GET-only surface documented at [`chess.ceo/llms.txt`](https://chess.ceo/llms.txt). This MCP server is a thin wrapper — one tool per endpoint, with input schemas so LLMs can call them safely. When you ask the model a chess question, it picks the right tool, calls it, and reasons over the JSON. Nothing is invented; the data is straight from the database.
80
+
81
+ ## License
82
+
83
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ // chess.ceo MCP server. Exposes the public GET API as MCP tools so LLM
3
+ // hosts (Claude Desktop, Cursor, etc.) can look up players, positions,
4
+ // preparation stats, and live broadcast state directly.
5
+ //
6
+ // Everything here is a thin wrapper around https://chess.ceo/api/chess/*
7
+ // endpoints — see the public contract at https://chess.ceo/llms.txt.
8
+ // No API key, no auth, no state; the API's own rate limits apply.
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
+ const BASE = process.env.CHESSCEO_BASE_URL ?? "https://chess.ceo";
13
+ const UA = `chessceo-mcp/${process.env.npm_package_version ?? "0.1.0"} (+https://chess.ceo)`;
14
+ // ── HTTP ────────────────────────────────────────────────────────────
15
+ async function get(path, params) {
16
+ const url = new URL(path, BASE);
17
+ for (const [k, v] of Object.entries(params)) {
18
+ if (v !== undefined && v !== null && v !== "")
19
+ url.searchParams.set(k, String(v));
20
+ }
21
+ const res = await fetch(url, { headers: { "User-Agent": UA, "Accept": "application/json" } });
22
+ if (!res.ok) {
23
+ // Bubble up the ProblemDetail body when the API returns one — LLM can
24
+ // then correct the query (e.g. wrong fideId) rather than retry blind.
25
+ let body;
26
+ try {
27
+ body = await res.text();
28
+ }
29
+ catch {
30
+ body = "";
31
+ }
32
+ throw new Error(`chess.ceo ${res.status}: ${body.slice(0, 500)}`);
33
+ }
34
+ return res.json();
35
+ }
36
+ // ── Tool definitions ───────────────────────────────────────────────
37
+ //
38
+ // Descriptions are written for the LLM, not humans — they should hint
39
+ // at when to call the tool, what inputs mean, and what the response
40
+ // contains. Terse is fine; the LLM already reads the parameter names.
41
+ const TOOLS = [
42
+ {
43
+ name: "search_player",
44
+ description: "Fuzzy name lookup for FIDE-rated chess players. Returns candidate matches with their FIDE ID, current rating, title (GM/IM/etc.), and country. Use this to resolve a plain-English name (e.g. 'Carlsen', 'Ding Liren') to the FIDE ID that every other tool needs.",
45
+ inputSchema: {
46
+ type: "object",
47
+ properties: {
48
+ name: {
49
+ type: "string",
50
+ description: "Player name or partial name. Case-insensitive, fuzzy.",
51
+ },
52
+ },
53
+ required: ["name"],
54
+ },
55
+ },
56
+ {
57
+ name: "get_player_profile",
58
+ description: "Full stats for one player: identity, monthly rating history, peak / trend stats, career W/D/L by color and time control, top-10 openings as White and Black, opponent analysis by rating bracket, notable wins and worst losses, top events with performance ratings. Often enough on its own for 'how strong is X, what do they play, who have they beaten'.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ fide_id: {
63
+ type: "integer",
64
+ description: "FIDE ID from search_player.",
65
+ },
66
+ },
67
+ required: ["fide_id"],
68
+ },
69
+ },
70
+ {
71
+ name: "get_player_preparation",
72
+ 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.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ fide_id: { type: "integer", description: "FIDE ID from search_player." },
77
+ color: {
78
+ type: "string",
79
+ enum: ["white", "black"],
80
+ description: "Which colour the player is analysed with.",
81
+ },
82
+ line: {
83
+ type: "string",
84
+ description: "Move sequence in SAN, space-separated, no move numbers required. Example: 'e4 e5 Nf3'. Leave empty for the starting position.",
85
+ },
86
+ fen: {
87
+ type: "string",
88
+ description: "Alternative to `line` — raw FEN of the target position.",
89
+ },
90
+ limit: {
91
+ type: "integer",
92
+ minimum: 1,
93
+ maximum: 10,
94
+ description: "Number of games to return (max 10 per request; page with offset).",
95
+ },
96
+ offset: { type: "integer", minimum: 0 },
97
+ },
98
+ required: ["fide_id", "color"],
99
+ },
100
+ },
101
+ {
102
+ name: "get_position_stats",
103
+ description: "Move statistics from all 11.7M+ indexed games at a given position — game counts, win percentages, top continuations. Answers 'from position P, how often does White play 4. O-O vs 4. d3, and which scores better'.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ fen: {
108
+ type: "string",
109
+ description: "FEN of the position to look up.",
110
+ },
111
+ limit: {
112
+ type: "integer",
113
+ minimum: 1,
114
+ maximum: 50,
115
+ description: "Number of top continuations to return.",
116
+ },
117
+ },
118
+ required: ["fen"],
119
+ },
120
+ },
121
+ {
122
+ name: "get_head_to_head",
123
+ description: "Complete head-to-head record between two players. Includes overall and per-colour W/D/L (from player A's perspective), splits by time control, most-played openings between them, first / last meeting, average game length, and the game list.",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ fide_id_a: { type: "integer", description: "FIDE ID of player A (record is from A's perspective)." },
128
+ fide_id_b: { type: "integer", description: "FIDE ID of player B." },
129
+ limit: { type: "integer", minimum: 1, maximum: 10 },
130
+ offset: { type: "integer", minimum: 0 },
131
+ },
132
+ required: ["fide_id_a", "fide_id_b"],
133
+ },
134
+ },
135
+ {
136
+ name: "list_live_tournaments",
137
+ description: "Tournaments currently being broadcast live on chess.ceo. Use this when the user asks 'what's on right now' / 'live tournaments today'.",
138
+ inputSchema: { type: "object", properties: {} },
139
+ },
140
+ {
141
+ name: "list_tournament_players",
142
+ description: "Players participating in one live-broadcast tournament.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ tour_id: { type: "string", description: "Tournament ID from list_live_tournaments." },
147
+ },
148
+ required: ["tour_id"],
149
+ },
150
+ },
151
+ {
152
+ name: "list_player_live_tournaments",
153
+ description: "Which currently-live broadcasts a given player is competing in. Use when the user asks 'is X playing anywhere right now'.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ fide_id: { type: "integer", description: "FIDE ID from search_player." },
158
+ },
159
+ required: ["fide_id"],
160
+ },
161
+ },
162
+ ];
163
+ async function callTool(name, args) {
164
+ switch (name) {
165
+ case "search_player":
166
+ return get("/api/chess/players/search/simple", { q: String(args.name), view: "llm" });
167
+ case "get_player_profile":
168
+ return get("/api/chess/players/profile", { fideId: Number(args.fide_id) });
169
+ case "get_player_preparation": {
170
+ const params = {
171
+ fideId: Number(args.fide_id),
172
+ color: String(args.color),
173
+ compact: "true",
174
+ };
175
+ if (typeof args.line === "string" && args.line.length > 0)
176
+ params.line = args.line;
177
+ if (typeof args.fen === "string" && args.fen.length > 0)
178
+ params.fen = args.fen;
179
+ if (typeof args.limit === "number")
180
+ params.limit = args.limit;
181
+ if (typeof args.offset === "number")
182
+ params.offset = args.offset;
183
+ return get("/api/chess/prep/by-player", params);
184
+ }
185
+ case "get_position_stats":
186
+ return get("/api/chess/database/main", {
187
+ fen: String(args.fen),
188
+ limit: typeof args.limit === "number" ? args.limit : 20,
189
+ sort: "relevance",
190
+ });
191
+ case "get_head_to_head":
192
+ return get("/api/chess/players/h2h", {
193
+ a: Number(args.fide_id_a),
194
+ b: Number(args.fide_id_b),
195
+ limit: typeof args.limit === "number" ? args.limit : 10,
196
+ offset: typeof args.offset === "number" ? args.offset : 0,
197
+ });
198
+ case "list_live_tournaments":
199
+ return get("/api/chess/live/tournaments", {});
200
+ case "list_tournament_players":
201
+ return get("/api/chess/live/tournament/players", { tour_id: String(args.tour_id) });
202
+ case "list_player_live_tournaments":
203
+ // Note: snake_case fide_id, unlike the prep endpoints. Documented quirk.
204
+ return get("/api/chess/live/player", { fide_id: Number(args.fide_id) });
205
+ default:
206
+ throw new Error(`Unknown tool: ${name}`);
207
+ }
208
+ }
209
+ // ── Server wiring ──────────────────────────────────────────────────
210
+ const server = new Server({ name: "chessceo-mcp", version: process.env.npm_package_version ?? "0.1.0" }, { capabilities: { tools: {} } });
211
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
212
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
213
+ const { name, arguments: args } = req.params;
214
+ try {
215
+ const result = await callTool(name, (args ?? {}));
216
+ return {
217
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
218
+ };
219
+ }
220
+ catch (err) {
221
+ return {
222
+ isError: true,
223
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
224
+ };
225
+ }
226
+ });
227
+ const transport = new StdioServerTransport();
228
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@chessceo/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server for chess.ceo — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts.",
5
+ "type": "module",
6
+ "bin": {
7
+ "chessceo-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": ["dist", "README.md"],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "start": "node dist/index.js",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": ["mcp", "modelcontextprotocol", "chess", "chessceo", "llm", "claude", "fide"],
18
+ "author": "chess.ceo",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/chessceo/chessceo-mcp"
23
+ },
24
+ "homepage": "https://chess.ceo",
25
+ "bugs": {
26
+ "url": "https://github.com/chessceo/chessceo-mcp/issues"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.0",
33
+ "zod": "^3.23.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.6.0"
38
+ }
39
+ }