@chessceo/mcp 0.1.0 → 0.2.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 +110 -0
  2. package/dist/index.js +126 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,17 @@ Similar `~/.cursor/mcp.json`:
51
51
  }
52
52
  ```
53
53
 
54
+ ## Install (Claude Code)
55
+
56
+ This repo is also a [Claude Code plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces), so you can add it directly:
57
+
58
+ ```
59
+ /plugin marketplace add chessceo/chessceo-mcp
60
+ /plugin install chessceo@chessceo
61
+ ```
62
+
63
+ Claude Code will pull the plugin from GitHub and wire the MCP server automatically. Enable "Sync automatically" in the marketplace UI if you want future updates fetched on push.
64
+
54
65
  ## Try it
55
66
 
56
67
  Ask your model:
@@ -60,6 +71,100 @@ Ask your model:
60
71
  - *"Are there any live tournaments right now with Hikaru Nakamura?"*
61
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?"*
62
73
 
74
+ ## Remote MCP (chess.ceo-hosted)
75
+
76
+ You can also connect to chess.ceo's hosted instance and skip installing anything:
77
+
78
+ ```
79
+ https://mcp.chess.ceo/mcp
80
+ ```
81
+
82
+ In Claude Code:
83
+
84
+ ```
85
+ /plugin add-mcp url https://mcp.chess.ceo/mcp
86
+ ```
87
+
88
+ In Claude Desktop, edit `claude_desktop_config.json`:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "chessceo": {
94
+ "url": "https://mcp.chess.ceo/mcp"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Same 8 tools, same data, zero-install. Useful when the host can't spawn subprocesses (e.g. Claude.ai web, Claude mobile, ChatGPT connectors).
101
+
102
+ ## Self-host the HTTP transport
103
+
104
+ The same package can run as a persistent HTTP server, not just a stdio subprocess:
105
+
106
+ ```bash
107
+ chessceo-mcp --transport=http --http-port=8080 --http-host=127.0.0.1
108
+ ```
109
+
110
+ Flags (or the corresponding env vars):
111
+
112
+ | Flag | Env var | Default | Purpose |
113
+ |---|---|---|---|
114
+ | `--transport` | `MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
115
+ | `--http-port` | `MCP_HTTP_PORT` | `8080` | Port to bind |
116
+ | `--http-host` | `MCP_HTTP_HOST` | `127.0.0.1` | Bind address |
117
+ | `--http-path` | `MCP_HTTP_PATH` | `/mcp` | Streamable-HTTP endpoint |
118
+
119
+ `GET /healthz` returns `200 ok\n` — wire it into your uptime monitor.
120
+
121
+ ### systemd unit (example)
122
+
123
+ ```ini
124
+ # /etc/systemd/system/chessceo-mcp.service
125
+ [Unit]
126
+ Description=chess.ceo MCP server (Streamable HTTP)
127
+ After=network.target
128
+
129
+ [Service]
130
+ Type=simple
131
+ User=www-data
132
+ Environment=NODE_ENV=production
133
+ Environment=MCP_TRANSPORT=http
134
+ Environment=MCP_HTTP_PORT=8127
135
+ Environment=MCP_HTTP_HOST=127.0.0.1
136
+ ExecStart=/usr/bin/npx -y @chessceo/mcp
137
+ Restart=on-failure
138
+ RestartSec=5
139
+
140
+ [Install]
141
+ WantedBy=multi-user.target
142
+ ```
143
+
144
+ ### nginx snippet (example)
145
+
146
+ ```nginx
147
+ server {
148
+ listen 443 ssl http2;
149
+ server_name mcp.chess.ceo;
150
+
151
+ ssl_certificate /etc/letsencrypt/live/mcp.chess.ceo/fullchain.pem;
152
+ ssl_certificate_key /etc/letsencrypt/live/mcp.chess.ceo/privkey.pem;
153
+
154
+ # Streamable HTTP is short JSON POSTs — no long-poll SSE required.
155
+ location /mcp {
156
+ proxy_pass http://127.0.0.1:8127/mcp;
157
+ proxy_http_version 1.1;
158
+ proxy_set_header Host $host;
159
+ proxy_set_header X-Real-IP $remote_addr;
160
+ proxy_buffering off; # streaming responses shouldn't be buffered
161
+ proxy_read_timeout 300s;
162
+ }
163
+
164
+ location = /healthz { proxy_pass http://127.0.0.1:8127/healthz; }
165
+ }
166
+ ```
167
+
63
168
  ## Development
64
169
 
65
170
  ```bash
@@ -68,11 +173,16 @@ cd chessceo-mcp
68
173
  npm install
69
174
  npm run build # tsc → dist/
70
175
  npm start # runs the server on stdio (for MCP hosts)
176
+
177
+ # or run the HTTP transport locally:
178
+ node dist/index.js --transport=http --http-port=8127
179
+ curl http://127.0.0.1:8127/healthz # should print "ok"
71
180
  ```
72
181
 
73
182
  Environment variable overrides:
74
183
 
75
184
  - `CHESSCEO_BASE_URL` — override the API base (default `https://chess.ceo`). Useful for testing against staging.
185
+ - MCP transport env vars — see the self-host table above.
76
186
 
77
187
  ## What's under the hood
78
188
 
package/dist/index.js CHANGED
@@ -6,8 +6,10 @@
6
6
  // Everything here is a thin wrapper around https://chess.ceo/api/chess/*
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
+ import { createServer as createHttpServer } from "node:http";
9
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
13
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
14
  const BASE = process.env.CHESSCEO_BASE_URL ?? "https://chess.ceo";
13
15
  const UA = `chessceo-mcp/${process.env.npm_package_version ?? "0.1.0"} (+https://chess.ceo)`;
@@ -224,5 +226,127 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
224
226
  };
225
227
  }
226
228
  });
227
- const transport = new StdioServerTransport();
228
- await server.connect(transport);
229
+ // ── Transport selection ────────────────────────────────────────────
230
+ //
231
+ // Two modes:
232
+ // stdio (default) — local subprocess, host spawns via npx / config.
233
+ // Every existing Claude Desktop / Cursor / Claude Code
234
+ // install of this package uses stdio.
235
+ // http (--transport=http --http-port=8080)
236
+ // — remote MCP over Streamable HTTP. Bind to a port,
237
+ // expose /mcp, users add the URL to their host
238
+ // instead of running npx. This is what
239
+ // claude.ai / mobile / other zero-install hosts
240
+ // need. Stateless mode: each request creates a
241
+ // fresh transport + response, no session
242
+ // persistence, safe to scale horizontally.
243
+ const argv = process.argv.slice(2);
244
+ const arg = (name, def) => {
245
+ const i = argv.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
246
+ if (i < 0)
247
+ return def;
248
+ const cur = argv[i];
249
+ return cur.includes("=") ? cur.split("=").slice(1).join("=") : argv[i + 1];
250
+ };
251
+ const transportKind = (arg("transport", process.env.MCP_TRANSPORT ?? "stdio") ?? "stdio").toLowerCase();
252
+ if (transportKind === "stdio") {
253
+ await server.connect(new StdioServerTransport());
254
+ }
255
+ else if (transportKind === "http" || transportKind === "streamable-http") {
256
+ const port = Number(arg("http-port", process.env.MCP_HTTP_PORT ?? "8080"));
257
+ const host = arg("http-host", process.env.MCP_HTTP_HOST ?? "127.0.0.1") ?? "127.0.0.1";
258
+ const path = arg("http-path", process.env.MCP_HTTP_PATH ?? "/mcp") ?? "/mcp";
259
+ // Read a JSON body off req into memory. Bodies are tiny (JSON-RPC), so
260
+ // no streaming needed; guard against absurd payloads with a hard cap.
261
+ const MAX_BODY = 1_048_576; // 1 MB
262
+ const readBody = (req) => new Promise((resolve, reject) => {
263
+ const chunks = [];
264
+ let total = 0;
265
+ req.on("data", (c) => {
266
+ total += c.length;
267
+ if (total > MAX_BODY) {
268
+ req.destroy();
269
+ reject(new Error("body too large"));
270
+ return;
271
+ }
272
+ chunks.push(c);
273
+ });
274
+ req.on("end", () => {
275
+ try {
276
+ const text = Buffer.concat(chunks).toString("utf8");
277
+ resolve(text.length === 0 ? undefined : JSON.parse(text));
278
+ }
279
+ catch (e) {
280
+ reject(e);
281
+ }
282
+ });
283
+ req.on("error", reject);
284
+ });
285
+ const httpServer = createHttpServer(async (req, res) => {
286
+ // Basic CORS so browser-based MCP hosts can call us cross-origin.
287
+ res.setHeader("Access-Control-Allow-Origin", "*");
288
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
289
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID");
290
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
291
+ if (req.method === "OPTIONS") {
292
+ res.statusCode = 204;
293
+ res.end();
294
+ return;
295
+ }
296
+ // Liveness — cheap health check for load balancers / uptime monitors.
297
+ if (req.method === "GET" && (req.url === "/healthz" || req.url === "/health")) {
298
+ res.statusCode = 200;
299
+ res.setHeader("Content-Type", "text/plain");
300
+ res.end("ok\n");
301
+ return;
302
+ }
303
+ // Everything else must hit the MCP path.
304
+ const urlPath = (req.url ?? "").split("?")[0];
305
+ if (urlPath !== path) {
306
+ res.statusCode = 404;
307
+ res.end();
308
+ return;
309
+ }
310
+ try {
311
+ // Stateless: one transport per request, no session store. Simpler,
312
+ // scales trivially, matches how claude.ai / ChatGPT connectors call.
313
+ const transport = new StreamableHTTPServerTransport({
314
+ sessionIdGenerator: undefined,
315
+ enableJsonResponse: true,
316
+ });
317
+ res.on("close", () => transport.close());
318
+ await server.connect(transport);
319
+ const body = req.method === "POST" ? await readBody(req) : undefined;
320
+ await transport.handleRequest(req, res, body);
321
+ }
322
+ catch (err) {
323
+ if (!res.headersSent) {
324
+ res.statusCode = 500;
325
+ res.setHeader("Content-Type", "application/json");
326
+ res.end(JSON.stringify({
327
+ jsonrpc: "2.0",
328
+ error: {
329
+ code: -32603,
330
+ message: err instanceof Error ? err.message : String(err),
331
+ },
332
+ id: null,
333
+ }));
334
+ }
335
+ }
336
+ });
337
+ httpServer.listen(port, host, () => {
338
+ console.error(`chessceo-mcp: streamable-http on http://${host}:${port}${path}`);
339
+ });
340
+ // Graceful shutdown so `systemctl stop` doesn't leak connections.
341
+ const shutdown = (sig) => {
342
+ console.error(`chessceo-mcp: ${sig} received, closing`);
343
+ httpServer.close(() => process.exit(0));
344
+ setTimeout(() => process.exit(1), 5000).unref();
345
+ };
346
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
347
+ process.on("SIGINT", () => shutdown("SIGINT"));
348
+ }
349
+ else {
350
+ console.error(`chessceo-mcp: unknown --transport '${transportKind}' (expected stdio or http)`);
351
+ process.exit(2);
352
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": {