@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.
- package/README.md +110 -0
- package/dist/index.js +126 -2
- 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
|
-
|
|
228
|
-
|
|
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