@chessceo/mcp 0.43.0 → 0.44.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/dist/http.js +150 -0
- package/dist/index.js +37 -1207
- package/dist/prompts.js +42 -0
- package/dist/tools.js +893 -0
- package/dist/warnings.js +121 -0
- package/package.json +1 -1
package/dist/http.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// HTTP + PGN I/O layer for chessceo-mcp. Auth resolution, raw
|
|
2
|
+
// `authedRequest` / `get`, response envelope helpers, and the
|
|
3
|
+
// small typed wrappers every prep-file tool composes over
|
|
4
|
+
// (`fetchGame` / `saveGame` / `createGame` / `deleteGame` /
|
|
5
|
+
// `restoreGame`).
|
|
6
|
+
//
|
|
7
|
+
// Two auth flavours coexist:
|
|
8
|
+
// - Anonymous GETs (players, positions, prep, live) — no auth.
|
|
9
|
+
// - Authed tools (cloud engines, prep files) — `Authorization: Bearer mcp_...`.
|
|
10
|
+
//
|
|
11
|
+
// The token comes from one of two sources:
|
|
12
|
+
// - stdio: `CHESSCEO_TOKEN` env var, set by the MCP host config. Bare
|
|
13
|
+
// `mcp_...` — we prepend the `Bearer ` scheme when building the header.
|
|
14
|
+
// - streamable-http: the caller's `Authorization` header, forwarded
|
|
15
|
+
// per-request via AsyncLocalStorage so tool handlers can see it even
|
|
16
|
+
// though the MCP SDK's request handler doesn't know about HTTP.
|
|
17
|
+
//
|
|
18
|
+
// LLM-facing "prep file id" is always "<collection_id>:<game_id>" —
|
|
19
|
+
// `makeFileId` to compose from a listing, `splitFileId` at the HTTP edge.
|
|
20
|
+
// This is what makes every prep-file tool keep taking a single `id`
|
|
21
|
+
// argument despite the backend paths needing both pieces.
|
|
22
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
+
const BASE = process.env.CHESSCEO_BASE_URL ?? "https://chess.ceo";
|
|
24
|
+
const UA = `chessceo-mcp/${process.env.npm_package_version ?? "0.1.0"} (+https://chess.ceo)`;
|
|
25
|
+
export const PGN_BASE = "/api/agent/pgns";
|
|
26
|
+
// Streamable-HTTP transport binds the caller's Authorization header
|
|
27
|
+
// into this context so per-request tool handlers can pick it up.
|
|
28
|
+
// Stdio callers don't set this — they hit the env-var branch below.
|
|
29
|
+
export const authContext = new AsyncLocalStorage();
|
|
30
|
+
export function resolveAuthHeader() {
|
|
31
|
+
const store = authContext.getStore();
|
|
32
|
+
if (store?.authHeader)
|
|
33
|
+
return store.authHeader;
|
|
34
|
+
const env = process.env.CHESSCEO_TOKEN?.trim();
|
|
35
|
+
if (!env)
|
|
36
|
+
return undefined;
|
|
37
|
+
return env.toLowerCase().startsWith("bearer ") ? env : `Bearer ${env}`;
|
|
38
|
+
}
|
|
39
|
+
export async function get(path, params) {
|
|
40
|
+
const url = new URL(path, BASE);
|
|
41
|
+
for (const [k, v] of Object.entries(params)) {
|
|
42
|
+
if (v !== undefined && v !== null && v !== "")
|
|
43
|
+
url.searchParams.set(k, String(v));
|
|
44
|
+
}
|
|
45
|
+
const res = await fetch(url, { headers: { "User-Agent": UA, "Accept": "application/json" } });
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
// Bubble up the ProblemDetail body when the API returns one — LLM can
|
|
48
|
+
// then correct the query (e.g. wrong fideId) rather than retry blind.
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
body = await res.text();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
body = "";
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`chess.ceo ${res.status}: ${body.slice(0, 500)}`);
|
|
57
|
+
}
|
|
58
|
+
return res.json();
|
|
59
|
+
}
|
|
60
|
+
// authedRequest is the shared code path for POST/GET/DELETE calls that need
|
|
61
|
+
// an MCP token. Missing-token errors are surfaced early with a message the
|
|
62
|
+
// LLM can act on (either configure CHESSCEO_TOKEN or generate a token in
|
|
63
|
+
// user settings) rather than a generic 401 from the backend.
|
|
64
|
+
export async function authedRequest(method, path, body) {
|
|
65
|
+
const auth = resolveAuthHeader();
|
|
66
|
+
if (!auth) {
|
|
67
|
+
throw new Error("No MCP token available. Set CHESSCEO_TOKEN env (stdio mode) or pass an " +
|
|
68
|
+
"Authorization: Bearer mcp_... header (streamable-http mode). Generate " +
|
|
69
|
+
"a token at chess.ceo → user settings → MCP tokens.");
|
|
70
|
+
}
|
|
71
|
+
const url = new URL(path, BASE);
|
|
72
|
+
const headers = {
|
|
73
|
+
"User-Agent": UA,
|
|
74
|
+
"Accept": "application/json",
|
|
75
|
+
"Authorization": auth,
|
|
76
|
+
};
|
|
77
|
+
const init = { method, headers };
|
|
78
|
+
if (body !== undefined) {
|
|
79
|
+
headers["Content-Type"] = "application/json";
|
|
80
|
+
init.body = JSON.stringify(body);
|
|
81
|
+
}
|
|
82
|
+
const res = await fetch(url, init);
|
|
83
|
+
if (res.status === 204)
|
|
84
|
+
return null;
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`chess.ceo ${res.status}: ${text.slice(0, 500)}`);
|
|
88
|
+
}
|
|
89
|
+
return text.length ? JSON.parse(text) : null;
|
|
90
|
+
}
|
|
91
|
+
// Browser handlers wrap successful responses as
|
|
92
|
+
// { success: true, message: "...", data: <actual thing> }
|
|
93
|
+
// via handlers.RespondSuccess. Peel that off; leave anything without
|
|
94
|
+
// the envelope unchanged (some endpoints return raw bodies).
|
|
95
|
+
export function unwrap(raw) {
|
|
96
|
+
if (raw &&
|
|
97
|
+
typeof raw === "object" &&
|
|
98
|
+
"data" in raw &&
|
|
99
|
+
raw.success === true) {
|
|
100
|
+
return raw.data;
|
|
101
|
+
}
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
export function makeFileId(collectionId, gameId) {
|
|
105
|
+
return `${collectionId}:${gameId}`;
|
|
106
|
+
}
|
|
107
|
+
export function splitFileId(id) {
|
|
108
|
+
const idx = id.indexOf(":");
|
|
109
|
+
if (idx <= 0 || idx === id.length - 1) {
|
|
110
|
+
throw new Error(`invalid prep file id "${id}" — expected "<collection_id>:<game_id>" ` +
|
|
111
|
+
`(get one from list_prep_files, search_prep_files, find_position_in_files, ` +
|
|
112
|
+
`or the create_prep_file return value)`);
|
|
113
|
+
}
|
|
114
|
+
return { collectionId: id.slice(0, idx), gameId: id.slice(idx + 1) };
|
|
115
|
+
}
|
|
116
|
+
// GET one game by composite id. Throws on 404.
|
|
117
|
+
export async function fetchGame(id) {
|
|
118
|
+
const { collectionId, gameId } = splitFileId(id);
|
|
119
|
+
const raw = await authedRequest("GET", `${PGN_BASE}/${encodeURIComponent(collectionId)}/games/${encodeURIComponent(gameId)}`);
|
|
120
|
+
const g = unwrap(raw);
|
|
121
|
+
if (!g || typeof g.pgnContent !== "string") {
|
|
122
|
+
throw new Error("prep file missing pgnContent");
|
|
123
|
+
}
|
|
124
|
+
return g;
|
|
125
|
+
}
|
|
126
|
+
// PUT the game body. Returns the saved game (new version).
|
|
127
|
+
export async function saveGame(id, pgn, expectedVersion) {
|
|
128
|
+
const { collectionId, gameId } = splitFileId(id);
|
|
129
|
+
const body = { pgnContent: pgn };
|
|
130
|
+
if (typeof expectedVersion === "number")
|
|
131
|
+
body.baseVersion = expectedVersion;
|
|
132
|
+
const raw = await authedRequest("PUT", `${PGN_BASE}/${encodeURIComponent(collectionId)}/games/${encodeURIComponent(gameId)}`, body);
|
|
133
|
+
return unwrap(raw);
|
|
134
|
+
}
|
|
135
|
+
// POST a new game into the given collection. Returns the created game.
|
|
136
|
+
export async function createGame(collectionId, pgn) {
|
|
137
|
+
const raw = await authedRequest("POST", `${PGN_BASE}/${encodeURIComponent(collectionId)}/games`, { pgnContent: pgn });
|
|
138
|
+
return unwrap(raw);
|
|
139
|
+
}
|
|
140
|
+
// DELETE (soft) a game by composite id.
|
|
141
|
+
export async function deleteGame(id) {
|
|
142
|
+
const { collectionId, gameId } = splitFileId(id);
|
|
143
|
+
await authedRequest("DELETE", `${PGN_BASE}/${encodeURIComponent(collectionId)}/games/${encodeURIComponent(gameId)}`);
|
|
144
|
+
}
|
|
145
|
+
// Restore a previously soft-deleted game. Symmetric with deleteGame.
|
|
146
|
+
export async function restoreGame(id) {
|
|
147
|
+
const { collectionId, gameId } = splitFileId(id);
|
|
148
|
+
const raw = await authedRequest("POST", `${PGN_BASE}/${encodeURIComponent(collectionId)}/games/${encodeURIComponent(gameId)}/restore`);
|
|
149
|
+
return unwrap(raw);
|
|
150
|
+
}
|