llm-chess-mcp 0.4.3 → 0.4.4
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 +2 -2
- package/dist/explorer.js +123 -94
- package/dist/games.js +41 -15
- package/dist/http.js +100 -98
- package/dist/intents.js +28 -28
- package/dist/services.js +14 -5
- package/dist/tool-names.js +15 -0
- package/dist/tools/analysis.js +3 -6
- package/dist/tools/candidates.js +29 -45
- package/dist/tools/explorer.js +1 -3
- package/dist/tools/game.js +7 -8
- package/docs/architecture.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -373,8 +373,8 @@ rejected:
|
|
|
373
373
|
- `move_evaluate` accepts at most 10 moves per call.
|
|
374
374
|
- Imported PGNs are limited to 1 MiB and 4,096 plies.
|
|
375
375
|
- Stockfish accepts up to 32 active or queued analyses.
|
|
376
|
-
- HTTP retains at most 64 MCP sessions; sessions with no
|
|
377
|
-
after 30 minutes.
|
|
376
|
+
- HTTP retains at most 64 MCP sessions; sessions with no active request expire
|
|
377
|
+
after 30 minutes. An open GET/SSE stream keeps its session active.
|
|
378
378
|
- HTTP accepts bodies up to 2 MiB. It permits 16 concurrent POSTs and downstream
|
|
379
379
|
compute/network jobs process-wide, with two of each per session. Work keeps
|
|
380
380
|
its slot after a raw disconnect until it settles. HTTP also caps connections
|
package/dist/explorer.js
CHANGED
|
@@ -128,7 +128,7 @@ async function sleepWithSignal(sleep, ms, signal) {
|
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
function setupExplorerRequest(chess, db, speeds, ratings, options) {
|
|
132
132
|
const callerSignal = options.signal;
|
|
133
133
|
throwIfAborted(callerSignal);
|
|
134
134
|
const token = options.token ?? process.env.LICHESS_TOKEN ?? "";
|
|
@@ -145,120 +145,149 @@ export async function openingExplorer(chess, db, speeds, ratings, options = {})
|
|
|
145
145
|
if (db === "masters" && (speeds.length > 0 || ratings.length > 0)) {
|
|
146
146
|
throw error("invalid_input");
|
|
147
147
|
}
|
|
148
|
-
const request = options.fetch ?? globalThis.fetch;
|
|
149
|
-
const sleep = options.sleep ??
|
|
150
|
-
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
151
|
-
const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
|
|
152
|
-
const now = options.now ?? Date.now;
|
|
153
|
-
const deadline = now() + EXPLORER_TOTAL_TIMEOUT_MS;
|
|
154
148
|
const params = new URLSearchParams();
|
|
155
149
|
params.set("fen", chess.fen());
|
|
156
150
|
if (speeds.length)
|
|
157
151
|
params.set("speeds", speeds.join(","));
|
|
158
152
|
if (ratings.length)
|
|
159
153
|
params.set("ratings", ratings.join(","));
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
const now = options.now ?? Date.now;
|
|
155
|
+
return {
|
|
156
|
+
callerSignal,
|
|
157
|
+
db,
|
|
158
|
+
deadline: now() + EXPLORER_TOTAL_TIMEOUT_MS,
|
|
159
|
+
legalMoves: new Map(chess.moves({ verbose: true }).map((move) => [move.lan, move.san])),
|
|
160
|
+
now,
|
|
161
|
+
request: options.fetch ?? globalThis.fetch,
|
|
162
|
+
sleep: options.sleep ??
|
|
163
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
164
|
+
timeout: options.timeout ?? ((ms) => AbortSignal.timeout(ms)),
|
|
165
|
+
token,
|
|
166
|
+
url: `${BASE}/${db}?${params}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function attemptRequest(setup) {
|
|
170
|
+
const { callerSignal, deadline, now, request, timeout, token, url } = setup;
|
|
171
|
+
throwIfAborted(callerSignal);
|
|
172
|
+
const remaining = deadline - now();
|
|
173
|
+
if (remaining <= 0)
|
|
174
|
+
throw error("timeout");
|
|
175
|
+
const attemptSignal = timeout(Math.max(1, Math.min(EXPLORER_ATTEMPT_TIMEOUT_MS, remaining)));
|
|
176
|
+
const signal = AbortSignal.any(callerSignal ? [callerSignal, attemptSignal] : [attemptSignal]);
|
|
177
|
+
let response;
|
|
178
|
+
try {
|
|
179
|
+
response = await request(url, {
|
|
180
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
181
|
+
signal,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
164
185
|
throwIfAborted(callerSignal);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
return { error: error(signal.aborted ? "timeout" : "network") };
|
|
187
|
+
}
|
|
188
|
+
throwIfAborted(callerSignal);
|
|
189
|
+
if (response.ok)
|
|
190
|
+
return { response, signal };
|
|
191
|
+
const kind = response.status === 401 || response.status === 403
|
|
192
|
+
? "auth"
|
|
193
|
+
: response.status === 429
|
|
194
|
+
? "rate_limited"
|
|
195
|
+
: response.status >= 500 && response.status <= 599
|
|
196
|
+
? "upstream"
|
|
197
|
+
: "http";
|
|
198
|
+
return {
|
|
199
|
+
error: error(kind, response.status),
|
|
200
|
+
retryAfter: response.headers.get("retry-after"),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function normalizeSuccessfulResponse(response, signal, setup) {
|
|
204
|
+
let body;
|
|
205
|
+
try {
|
|
206
|
+
body = await response.json();
|
|
207
|
+
}
|
|
208
|
+
catch (cause) {
|
|
209
|
+
throwIfAborted(setup.callerSignal);
|
|
210
|
+
if (signal.aborted || cause instanceof TypeError) {
|
|
211
|
+
throw error(signal.aborted ? "timeout" : "network");
|
|
176
212
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
throw error("invalid_response");
|
|
214
|
+
}
|
|
215
|
+
throwIfAborted(setup.callerSignal);
|
|
216
|
+
const parsed = responseSchema.safeParse(body);
|
|
217
|
+
if (!parsed.success)
|
|
218
|
+
throw error("invalid_response");
|
|
219
|
+
const data = parsed.data;
|
|
220
|
+
const ucis = new Set();
|
|
221
|
+
let white = 0;
|
|
222
|
+
let draws = 0;
|
|
223
|
+
let black = 0;
|
|
224
|
+
for (const move of data.moves) {
|
|
225
|
+
if (ucis.has(move.uci) || setup.legalMoves.get(move.uci) !== move.san) {
|
|
226
|
+
throw error("invalid_response");
|
|
185
227
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
ucis.add(move.uci);
|
|
229
|
+
white += move.white;
|
|
230
|
+
draws += move.draws;
|
|
231
|
+
black += move.black;
|
|
232
|
+
}
|
|
233
|
+
if (white > data.white || draws > data.draws || black > data.black) {
|
|
234
|
+
throw error("invalid_response");
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
db: setup.db,
|
|
238
|
+
white: data.white,
|
|
239
|
+
draws: data.draws,
|
|
240
|
+
black: data.black,
|
|
241
|
+
moves: data.moves.map((move) => ({
|
|
242
|
+
uci: move.uci,
|
|
243
|
+
san: move.san,
|
|
244
|
+
white: move.white,
|
|
245
|
+
draws: move.draws,
|
|
246
|
+
black: move.black,
|
|
247
|
+
count: move.white + move.draws + move.black,
|
|
248
|
+
averageRating: move.averageRating ?? null,
|
|
249
|
+
})),
|
|
250
|
+
opening: data.opening ?? null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function retryAfterNetworkFailure(setup) {
|
|
254
|
+
const delay = Math.min(EXPLORER_DEFAULT_RETRY_DELAY_MS, Math.max(0, setup.deadline - setup.now()));
|
|
255
|
+
await sleepWithSignal(setup.sleep, delay, setup.callerSignal);
|
|
256
|
+
}
|
|
257
|
+
export async function openingExplorer(chess, db, speeds, ratings, options = {}) {
|
|
258
|
+
const setup = setupExplorerRequest(chess, db, speeds, ratings, options);
|
|
259
|
+
let lastError = error("network");
|
|
260
|
+
for (let attempt = 0; attempt < EXPLORER_MAX_ATTEMPTS; attempt += 1) {
|
|
261
|
+
const attemptResult = await attemptRequest(setup);
|
|
262
|
+
if ("error" in attemptResult) {
|
|
263
|
+
lastError = attemptResult.error;
|
|
264
|
+
if (!isRetryable(lastError.kind) || attempt + 1 >= EXPLORER_MAX_ATTEMPTS) {
|
|
197
265
|
throw lastError;
|
|
198
266
|
}
|
|
199
|
-
|
|
200
|
-
|
|
267
|
+
if (attemptResult.retryAfter === undefined) {
|
|
268
|
+
await retryAfterNetworkFailure(setup);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const delay = retryAfterMs(attemptResult.retryAfter, setup.now());
|
|
272
|
+
const retryBudget = Math.max(0, setup.deadline - setup.now());
|
|
201
273
|
if (delay > EXPLORER_MAX_RETRY_DELAY_MS || delay >= retryBudget) {
|
|
202
274
|
throw lastError;
|
|
203
275
|
}
|
|
204
|
-
await sleepWithSignal(sleep, delay, callerSignal);
|
|
276
|
+
await sleepWithSignal(setup.sleep, delay, setup.callerSignal);
|
|
205
277
|
continue;
|
|
206
278
|
}
|
|
207
|
-
let body;
|
|
208
279
|
try {
|
|
209
|
-
|
|
280
|
+
return await normalizeSuccessfulResponse(attemptResult.response, attemptResult.signal, setup);
|
|
210
281
|
}
|
|
211
282
|
catch (cause) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
lastError = error(signal.aborted ? "timeout" : "network");
|
|
215
|
-
if (attempt + 1 < EXPLORER_MAX_ATTEMPTS) {
|
|
216
|
-
const delay = Math.min(EXPLORER_DEFAULT_RETRY_DELAY_MS, Math.max(0, deadline - now()));
|
|
217
|
-
await sleepWithSignal(sleep, delay, callerSignal);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
throw lastError;
|
|
283
|
+
if (!(cause instanceof ExplorerError) || !isRetryable(cause.kind)) {
|
|
284
|
+
throw cause;
|
|
221
285
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!parsed.success)
|
|
227
|
-
throw error("invalid_response");
|
|
228
|
-
const data = parsed.data;
|
|
229
|
-
const ucis = new Set();
|
|
230
|
-
let white = 0;
|
|
231
|
-
let draws = 0;
|
|
232
|
-
let black = 0;
|
|
233
|
-
for (const move of data.moves) {
|
|
234
|
-
if (ucis.has(move.uci) ||
|
|
235
|
-
legalMoves.get(move.uci) !== move.san) {
|
|
236
|
-
throw error("invalid_response");
|
|
237
|
-
}
|
|
238
|
-
ucis.add(move.uci);
|
|
239
|
-
white += move.white;
|
|
240
|
-
draws += move.draws;
|
|
241
|
-
black += move.black;
|
|
242
|
-
}
|
|
243
|
-
if (white > data.white || draws > data.draws || black > data.black) {
|
|
244
|
-
throw error("invalid_response");
|
|
286
|
+
lastError = cause;
|
|
287
|
+
if (attempt + 1 >= EXPLORER_MAX_ATTEMPTS)
|
|
288
|
+
throw lastError;
|
|
289
|
+
await retryAfterNetworkFailure(setup);
|
|
245
290
|
}
|
|
246
|
-
return {
|
|
247
|
-
db,
|
|
248
|
-
white: data.white,
|
|
249
|
-
draws: data.draws,
|
|
250
|
-
black: data.black,
|
|
251
|
-
moves: data.moves.map((move) => ({
|
|
252
|
-
uci: move.uci,
|
|
253
|
-
san: move.san,
|
|
254
|
-
white: move.white,
|
|
255
|
-
draws: move.draws,
|
|
256
|
-
black: move.black,
|
|
257
|
-
count: move.white + move.draws + move.black,
|
|
258
|
-
averageRating: move.averageRating ?? null,
|
|
259
|
-
})),
|
|
260
|
-
opening: data.opening ?? null,
|
|
261
|
-
};
|
|
262
291
|
}
|
|
263
292
|
throw lastError;
|
|
264
293
|
}
|
package/dist/games.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Chess } from "chess.js";
|
|
3
|
+
import { playParsedMove, snapshotChess } from "./chess.js";
|
|
3
4
|
import { ChessError } from "./errors.js";
|
|
4
5
|
export const MAX_GAMES = 1_000;
|
|
5
6
|
export const GAME_TTL_MS = 60 * 60 * 1_000;
|
|
7
|
+
function cloneChess(chess) {
|
|
8
|
+
const copy = snapshotChess(chess);
|
|
9
|
+
for (const [key, value] of Object.entries(chess.getHeaders())) {
|
|
10
|
+
copy.setHeader(key, value);
|
|
11
|
+
}
|
|
12
|
+
const comments = new Map(chess.getComments().map(({ fen, comment }) => [fen, comment]));
|
|
13
|
+
const reversed = [];
|
|
14
|
+
while (true) {
|
|
15
|
+
const comment = comments.get(copy.fen());
|
|
16
|
+
if (comment !== undefined)
|
|
17
|
+
copy.setComment(comment);
|
|
18
|
+
const move = copy.undo();
|
|
19
|
+
if (!move)
|
|
20
|
+
break;
|
|
21
|
+
reversed.push(move);
|
|
22
|
+
}
|
|
23
|
+
for (const move of reversed.reverse())
|
|
24
|
+
playParsedMove(copy, move);
|
|
25
|
+
return copy;
|
|
26
|
+
}
|
|
6
27
|
export class GameStore {
|
|
7
28
|
maxGames;
|
|
8
29
|
idleTtlMs;
|
|
@@ -44,10 +65,28 @@ export class GameStore {
|
|
|
44
65
|
if (this.games.has(id)) {
|
|
45
66
|
throw new ChessError("GAME_ID_COLLISION", `game ID already exists: ${id}`);
|
|
46
67
|
}
|
|
47
|
-
this.games.set(id, {
|
|
68
|
+
this.games.set(id, {
|
|
69
|
+
chess: cloneChess(chess),
|
|
70
|
+
createdAt: now,
|
|
71
|
+
lastAccessedAt: now,
|
|
72
|
+
revision: 0,
|
|
73
|
+
});
|
|
48
74
|
return id;
|
|
49
75
|
}
|
|
50
|
-
|
|
76
|
+
getSnapshot(id) {
|
|
77
|
+
const game = this.getLiveGame(id);
|
|
78
|
+
return { chess: cloneChess(game.chess), revision: game.revision };
|
|
79
|
+
}
|
|
80
|
+
applyMove(id, expectedRevision, move) {
|
|
81
|
+
const game = this.getLiveGame(id);
|
|
82
|
+
if (expectedRevision !== game.revision) {
|
|
83
|
+
throw new ChessError("STALE_POSITION", `position changed: expected revision ${expectedRevision}, current ${game.revision}`);
|
|
84
|
+
}
|
|
85
|
+
playParsedMove(game.chess, move);
|
|
86
|
+
game.revision += 1;
|
|
87
|
+
return { chess: cloneChess(game.chess), revision: game.revision };
|
|
88
|
+
}
|
|
89
|
+
getLiveGame(id) {
|
|
51
90
|
const game = this.games.get(id);
|
|
52
91
|
if (!game)
|
|
53
92
|
throw new ChessError("GAME_NOT_FOUND", `game not found: ${id}`);
|
|
@@ -59,11 +98,6 @@ export class GameStore {
|
|
|
59
98
|
game.lastAccessedAt = now;
|
|
60
99
|
return game;
|
|
61
100
|
}
|
|
62
|
-
bumpRevision(id) {
|
|
63
|
-
const game = this.getGame(id);
|
|
64
|
-
game.revision += 1;
|
|
65
|
-
return game.revision;
|
|
66
|
-
}
|
|
67
101
|
deleteGame(id) {
|
|
68
102
|
this.cleanupGames();
|
|
69
103
|
return this.games.delete(id);
|
|
@@ -81,11 +115,3 @@ export class GameStore {
|
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
export const defaultGameStore = new GameStore();
|
|
84
|
-
export const cleanupGames = (now) => defaultGameStore.cleanupGames(now);
|
|
85
|
-
export const createGame = (fen) => defaultGameStore.createGame(fen);
|
|
86
|
-
export const createGameFromChess = (chess) => defaultGameStore.createGameFromChess(chess);
|
|
87
|
-
export const getGame = (id) => defaultGameStore.getGame(id);
|
|
88
|
-
export const bumpRevision = (id) => defaultGameStore.bumpRevision(id);
|
|
89
|
-
export const deleteGame = (id) => defaultGameStore.deleteGame(id);
|
|
90
|
-
export const listGames = () => defaultGameStore.listGames();
|
|
91
|
-
export const gameCount = () => defaultGameStore.gameCount();
|
package/dist/http.js
CHANGED
|
@@ -166,6 +166,18 @@ function hasUnsupportedContentEncoding(req) {
|
|
|
166
166
|
return (contentEncoding !== undefined &&
|
|
167
167
|
(typeof contentEncoding !== "string" || contentEncoding.trim().toLowerCase() !== "identity"));
|
|
168
168
|
}
|
|
169
|
+
async function parsePostBody(req, limit, timeoutMs) {
|
|
170
|
+
const body = await readPostBody(req, limit, timeoutMs);
|
|
171
|
+
if (!body.ok)
|
|
172
|
+
return body;
|
|
173
|
+
if (!isJsonContentType(req)) {
|
|
174
|
+
return { ok: false, status: 415, message: "Content-Type must be application/json" };
|
|
175
|
+
}
|
|
176
|
+
if (hasUnsupportedContentEncoding(req)) {
|
|
177
|
+
return { ok: false, status: 415, message: "Content-Encoding must be identity" };
|
|
178
|
+
}
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
169
181
|
const UNABORTABLE_SIGNAL = new AbortController().signal;
|
|
170
182
|
function scopedServices(services, run) {
|
|
171
183
|
return {
|
|
@@ -222,6 +234,17 @@ export async function serveHttp(options = {}, services = defaultAppServices) {
|
|
|
222
234
|
.filter(([, session]) => session.activeRequests === 0 && now - session.lastUsedAt >= limits.sessionIdleTtlMs)
|
|
223
235
|
.map(([id, session]) => closeSession(id, session)));
|
|
224
236
|
};
|
|
237
|
+
const withActiveSession = async (session, work) => {
|
|
238
|
+
session.activeRequests += 1;
|
|
239
|
+
session.lastUsedAt = Date.now();
|
|
240
|
+
try {
|
|
241
|
+
return await work();
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
session.activeRequests -= 1;
|
|
245
|
+
session.lastUsedAt = Date.now();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
225
248
|
const releasePost = (session) => {
|
|
226
249
|
activePosts -= 1;
|
|
227
250
|
if (session)
|
|
@@ -290,33 +313,22 @@ export async function serveHttp(options = {}, services = defaultAppServices) {
|
|
|
290
313
|
closeWithError(req, res, 503, "server request limit reached", { "retry-after": "1" });
|
|
291
314
|
return;
|
|
292
315
|
}
|
|
293
|
-
session.activeRequests += 1;
|
|
294
|
-
session.lastUsedAt = Date.now();
|
|
295
316
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
if (hasUnsupportedContentEncoding(req)) {
|
|
306
|
-
closeWithError(req, res, 415, "Content-Encoding must be identity");
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
await session.transport.handleRequest(req, res, body.value);
|
|
317
|
+
await withActiveSession(session, async () => {
|
|
318
|
+
const body = await parsePostBody(req, limits.maxRequestBodyBytes, limits.requestTimeoutMs);
|
|
319
|
+
if (!body.ok) {
|
|
320
|
+
closeWithError(req, res, body.status, body.message);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
await session.transport.handleRequest(req, res, body.value);
|
|
324
|
+
});
|
|
310
325
|
}
|
|
311
326
|
finally {
|
|
312
|
-
session.activeRequests -= 1;
|
|
313
|
-
session.lastUsedAt = Date.now();
|
|
314
327
|
releasePost(session);
|
|
315
328
|
}
|
|
316
329
|
return;
|
|
317
330
|
}
|
|
318
|
-
session
|
|
319
|
-
await session.transport.handleRequest(req, res);
|
|
331
|
+
await withActiveSession(session, () => session.transport.handleRequest(req, res));
|
|
320
332
|
return;
|
|
321
333
|
}
|
|
322
334
|
if (req.method !== "POST") {
|
|
@@ -328,90 +340,80 @@ export async function serveHttp(options = {}, services = defaultAppServices) {
|
|
|
328
340
|
closeWithError(req, res, 503, "server request limit reached", { "retry-after": "1" });
|
|
329
341
|
return;
|
|
330
342
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
343
|
+
try {
|
|
344
|
+
const body = await parsePostBody(req, limits.maxRequestBodyBytes, limits.requestTimeoutMs);
|
|
345
|
+
if (!body.ok) {
|
|
346
|
+
closeWithError(req, res, body.status, body.message);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!isInitializeRequest(body.value)) {
|
|
350
|
+
closeWithError(req, res, 400, "MCP session initialization required");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
await reapExpiredSessions();
|
|
354
|
+
if (closing) {
|
|
355
|
+
closeWithError(req, res, 503, "server is shutting down", { "retry-after": "1" });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (sessions.size + pendingInitializations >= limits.maxSessions) {
|
|
359
|
+
closeWithError(req, res, 503, "MCP session limit reached", { "retry-after": "1" });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
pendingInitializations += 1;
|
|
363
|
+
let initializedId;
|
|
364
|
+
let session;
|
|
365
|
+
let hasReservation = true;
|
|
366
|
+
const transport = new NodeStreamableHTTPServerTransport({
|
|
367
|
+
sessionIdGenerator: randomUUID,
|
|
368
|
+
onsessioninitialized: (newId) => {
|
|
369
|
+
initializedId = newId;
|
|
370
|
+
initializing.delete(session);
|
|
371
|
+
if (hasReservation) {
|
|
372
|
+
hasReservation = false;
|
|
373
|
+
pendingInitializations -= 1;
|
|
374
|
+
}
|
|
375
|
+
session.lastUsedAt = Date.now();
|
|
376
|
+
sessions.set(newId, session);
|
|
377
|
+
},
|
|
378
|
+
onsessionclosed: (closedId) => {
|
|
379
|
+
const current = sessions.get(closedId);
|
|
380
|
+
if (current === session)
|
|
381
|
+
sessions.delete(closedId);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const abort = new AbortController();
|
|
385
|
+
const mcp = buildServer(scopedServices(services, workAdmission.session(abort.signal)));
|
|
386
|
+
session = {
|
|
387
|
+
server: mcp,
|
|
388
|
+
transport,
|
|
389
|
+
abort,
|
|
390
|
+
lastUsedAt: Date.now(),
|
|
391
|
+
activeRequests: 0,
|
|
392
|
+
activePosts: 0,
|
|
393
|
+
};
|
|
394
|
+
initializing.add(session);
|
|
395
|
+
transport.onclose = () => {
|
|
396
|
+
session.abort.abort(new DOMException("MCP session closed", "AbortError"));
|
|
397
|
+
if (initializedId && sessions.get(initializedId) === session) {
|
|
398
|
+
sessions.delete(initializedId);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
try {
|
|
402
|
+
await withActiveSession(session, async () => {
|
|
403
|
+
await mcp.connect(transport);
|
|
404
|
+
await transport.handleRequest(req, res, body.value);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
371
408
|
initializing.delete(session);
|
|
372
|
-
if (hasReservation)
|
|
373
|
-
hasReservation = false;
|
|
409
|
+
if (hasReservation)
|
|
374
410
|
pendingInitializations -= 1;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
sessions.set(newId, session);
|
|
378
|
-
},
|
|
379
|
-
onsessionclosed: (closedId) => {
|
|
380
|
-
const current = sessions.get(closedId);
|
|
381
|
-
if (current === session)
|
|
382
|
-
sessions.delete(closedId);
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
const abort = new AbortController();
|
|
386
|
-
const mcp = buildServer(scopedServices(services, workAdmission.session(abort.signal)));
|
|
387
|
-
session = {
|
|
388
|
-
server: mcp,
|
|
389
|
-
transport,
|
|
390
|
-
abort,
|
|
391
|
-
lastUsedAt: Date.now(),
|
|
392
|
-
activeRequests: 1,
|
|
393
|
-
activePosts: 0,
|
|
394
|
-
};
|
|
395
|
-
initializing.add(session);
|
|
396
|
-
transport.onclose = () => {
|
|
397
|
-
session.abort.abort(new DOMException("MCP session closed", "AbortError"));
|
|
398
|
-
if (initializedId && sessions.get(initializedId) === session) {
|
|
399
|
-
sessions.delete(initializedId);
|
|
411
|
+
if (!initializedId)
|
|
412
|
+
await mcp.close();
|
|
400
413
|
}
|
|
401
|
-
};
|
|
402
|
-
try {
|
|
403
|
-
await mcp.connect(transport);
|
|
404
|
-
await transport.handleRequest(req, res, body.value);
|
|
405
414
|
}
|
|
406
415
|
finally {
|
|
407
|
-
session.activeRequests -= 1;
|
|
408
|
-
session.lastUsedAt = Date.now();
|
|
409
|
-
initializing.delete(session);
|
|
410
416
|
releasePost(undefined);
|
|
411
|
-
if (hasReservation)
|
|
412
|
-
pendingInitializations -= 1;
|
|
413
|
-
if (!initializedId)
|
|
414
|
-
await mcp.close();
|
|
415
417
|
}
|
|
416
418
|
};
|
|
417
419
|
const headerTimers = new WeakMap();
|
package/dist/intents.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { stockfish } from "./engines/stockfish.js";
|
|
2
|
-
import { humanMoveDistribution } from "./maia3/inference.js";
|
|
3
|
-
import { ExplorerError, openingExplorer, explorerEnabled } from "./explorer.js";
|
|
4
1
|
import { toEval, evalToCp } from "./eval.js";
|
|
5
2
|
function softmax(values, temperature) {
|
|
6
3
|
const scaled = values.map((v) => v / temperature);
|
|
@@ -122,31 +119,34 @@ export function candidateSetFromData(chess, elo, sfLines, maiaMoves, lichessResu
|
|
|
122
119
|
moveSensitivity: computeMoveSensitivity(normalizedSfLines.map(({ line }) => line)),
|
|
123
120
|
};
|
|
124
121
|
}
|
|
125
|
-
export
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
122
|
+
export function createCandidateComputation(dependencies) {
|
|
123
|
+
return {
|
|
124
|
+
computeCandidates: async (chess, elo, sfDepth, sfMultipv, maiaTopN, lichess, signal) => {
|
|
125
|
+
const [sfLines, maiaMoves, lichessResult] = await Promise.all([
|
|
126
|
+
dependencies.analyze(chess.fen(), sfDepth, sfMultipv, signal),
|
|
127
|
+
dependencies.humanMoveDistribution(chess, elo, elo, maiaTopN, signal),
|
|
128
|
+
lichess && dependencies.explorerEnabled()
|
|
129
|
+
? dependencies
|
|
130
|
+
.openingExplorer(chess, lichess.db, lichess.speeds, lichess.ratings, signal)
|
|
131
|
+
.then(explorerCandidateData)
|
|
132
|
+
.catch((error) => {
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
return {
|
|
135
|
+
status: "unavailable",
|
|
136
|
+
reason: dependencies.explorerFailureReason(error),
|
|
137
|
+
totalGames: null,
|
|
138
|
+
moves: [],
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
: Promise.resolve({
|
|
142
|
+
status: "disabled",
|
|
143
|
+
totalGames: null,
|
|
144
|
+
moves: [],
|
|
145
|
+
}),
|
|
146
|
+
]);
|
|
147
|
+
return candidateSetFromData(chess, elo, sfLines, maiaMoves, lichessResult);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
150
|
}
|
|
151
151
|
function winMargin(c) {
|
|
152
152
|
const wdl = c.objective.wdl;
|
package/dist/services.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { stockfish } from "./engines/stockfish.js";
|
|
2
|
-
import { explorerEnabled, openingExplorer, } from "./explorer.js";
|
|
2
|
+
import { ExplorerError, explorerEnabled, openingExplorer, } from "./explorer.js";
|
|
3
3
|
import { defaultGameStore } from "./games.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createCandidateComputation, rankByIntent } from "./intents.js";
|
|
5
5
|
import { humanMoveDistribution } from "./maia3/inference.js";
|
|
6
|
+
const analyze = (fen, depth, multipv, signal) => stockfish.analyze(fen, depth, multipv, signal);
|
|
7
|
+
const openExplorer = (chess, db, speeds, ratings, signal) => openingExplorer(chess, db, speeds, ratings, signal === undefined ? {} : { signal });
|
|
8
|
+
const candidateComputation = createCandidateComputation({
|
|
9
|
+
analyze,
|
|
10
|
+
humanMoveDistribution,
|
|
11
|
+
explorerEnabled,
|
|
12
|
+
openingExplorer: openExplorer,
|
|
13
|
+
explorerFailureReason: (error) => error instanceof ExplorerError ? error.reason : "upstream",
|
|
14
|
+
});
|
|
6
15
|
export const defaultAppServices = {
|
|
7
16
|
games: defaultGameStore,
|
|
8
|
-
analyze
|
|
17
|
+
analyze,
|
|
9
18
|
quit: () => stockfish.quit(),
|
|
10
19
|
humanMoveDistribution,
|
|
11
20
|
explorerEnabled,
|
|
12
|
-
openingExplorer:
|
|
13
|
-
computeCandidates,
|
|
21
|
+
openingExplorer: openExplorer,
|
|
22
|
+
computeCandidates: candidateComputation.computeCandidates,
|
|
14
23
|
rankByIntent,
|
|
15
24
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const TOOL_NAMES = [
|
|
2
|
+
"create_game",
|
|
3
|
+
"delete_game",
|
|
4
|
+
"game_import_pgn",
|
|
5
|
+
"game_legal_moves",
|
|
6
|
+
"game_pgn",
|
|
7
|
+
"game_play_move",
|
|
8
|
+
"game_state",
|
|
9
|
+
"human_move_distribution",
|
|
10
|
+
"move_candidates",
|
|
11
|
+
"move_candidates_by_intent",
|
|
12
|
+
"move_evaluate",
|
|
13
|
+
"opening_explorer",
|
|
14
|
+
"position_analyze",
|
|
15
|
+
];
|
package/dist/tools/analysis.js
CHANGED
|
@@ -10,8 +10,7 @@ export function registerAnalysisTools(server, services) {
|
|
|
10
10
|
inputSchema: TOOL_INPUT_SCHEMAS.position_analyze,
|
|
11
11
|
outputSchema: TOOL_OUTPUT_SCHEMAS.position_analyze,
|
|
12
12
|
}, safeHandler(TOOL_INPUT_SCHEMAS.position_analyze, async ({ game_id, analysis_level, depth, multipv }, signal) => {
|
|
13
|
-
const { chess
|
|
14
|
-
const chess = snapshotChess(live);
|
|
13
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
15
14
|
const preset = ANALYSIS_PRESETS[analysis_level];
|
|
16
15
|
const d = depth ?? preset.depth;
|
|
17
16
|
const mpv = multipv ?? preset.multipv;
|
|
@@ -38,8 +37,7 @@ export function registerAnalysisTools(server, services) {
|
|
|
38
37
|
inputSchema: TOOL_INPUT_SCHEMAS.human_move_distribution,
|
|
39
38
|
outputSchema: TOOL_OUTPUT_SCHEMAS.human_move_distribution,
|
|
40
39
|
}, safeHandler(TOOL_INPUT_SCHEMAS.human_move_distribution, async ({ game_id, elo, oppo_elo, top_n }, signal) => {
|
|
41
|
-
const { chess
|
|
42
|
-
const chess = snapshotChess(live);
|
|
40
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
43
41
|
const opponentElo = oppo_elo ?? elo;
|
|
44
42
|
const moves = await services.humanMoveDistribution(chess, elo, opponentElo, top_n, signal);
|
|
45
43
|
const payload = {
|
|
@@ -56,8 +54,7 @@ export function registerAnalysisTools(server, services) {
|
|
|
56
54
|
inputSchema: TOOL_INPUT_SCHEMAS.move_evaluate,
|
|
57
55
|
outputSchema: TOOL_OUTPUT_SCHEMAS.move_evaluate,
|
|
58
56
|
}, safeHandler(TOOL_INPUT_SCHEMAS.move_evaluate, async ({ game_id, move, depth }, signal) => {
|
|
59
|
-
const { chess
|
|
60
|
-
const chess = snapshotChess(live);
|
|
57
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
61
58
|
const moves = Array.isArray(move) ? move : [move];
|
|
62
59
|
const beforeLines = await services.analyze(chess.fen(), depth, 1, signal);
|
|
63
60
|
signal.throwIfAborted();
|
package/dist/tools/candidates.js
CHANGED
|
@@ -1,66 +1,50 @@
|
|
|
1
|
-
import { snapshotChess } from "../chess.js";
|
|
2
1
|
import { ANALYSIS_PRESETS } from "../eval.js";
|
|
3
2
|
import { TOOL_INPUT_SCHEMAS } from "../tool-inputs.js";
|
|
4
3
|
import { TOOL_META } from "../tool-meta.js";
|
|
5
4
|
import { safeHandler, toolResult } from "../tool-result.js";
|
|
6
5
|
import { TOOL_OUTPUT_SCHEMAS } from "../tool-schemas.js";
|
|
6
|
+
async function candidatePayload(services, { game_id, elo, analysis_level, sf_depth, sf_multipv, maia_top_n, lichess_db, lichess_speeds, lichess_ratings, }, signal) {
|
|
7
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
8
|
+
const preset = ANALYSIS_PRESETS[analysis_level];
|
|
9
|
+
const { candidates, moveSensitivity } = await services.computeCandidates(chess, elo, sf_depth ?? preset.depth, sf_multipv ?? preset.multipv, maia_top_n, {
|
|
10
|
+
db: lichess_db,
|
|
11
|
+
speeds: lichess_speeds,
|
|
12
|
+
ratings: lichess_ratings,
|
|
13
|
+
}, signal);
|
|
14
|
+
signal.throwIfAborted();
|
|
15
|
+
return {
|
|
16
|
+
game_id,
|
|
17
|
+
revision,
|
|
18
|
+
fen: chess.fen(),
|
|
19
|
+
turn: chess.turn(),
|
|
20
|
+
elo,
|
|
21
|
+
analysis_level,
|
|
22
|
+
moveSensitivity,
|
|
23
|
+
candidates,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
7
26
|
export function registerCandidateTools(server, services) {
|
|
8
27
|
const moveCandidatesSchema = TOOL_INPUT_SCHEMAS.move_candidates;
|
|
9
28
|
server.registerTool("move_candidates", {
|
|
10
29
|
...TOOL_META.move_candidates,
|
|
11
30
|
inputSchema: moveCandidatesSchema,
|
|
12
31
|
outputSchema: TOOL_OUTPUT_SCHEMAS.move_candidates,
|
|
13
|
-
}, safeHandler(moveCandidatesSchema, async (
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const preset = ANALYSIS_PRESETS[analysis_level];
|
|
17
|
-
const depth = sf_depth ?? preset.depth;
|
|
18
|
-
const multipv = sf_multipv ?? preset.multipv;
|
|
19
|
-
const { candidates, moveSensitivity } = await services.computeCandidates(chess, elo, depth, multipv, maia_top_n, {
|
|
20
|
-
db: lichess_db,
|
|
21
|
-
speeds: lichess_speeds,
|
|
22
|
-
ratings: lichess_ratings,
|
|
23
|
-
}, signal);
|
|
24
|
-
signal.throwIfAborted();
|
|
25
|
-
return toolResult({
|
|
26
|
-
game_id,
|
|
27
|
-
revision,
|
|
28
|
-
fen: chess.fen(),
|
|
29
|
-
turn: chess.turn(),
|
|
30
|
-
elo,
|
|
31
|
-
analysis_level,
|
|
32
|
-
moveSensitivity,
|
|
33
|
-
candidates,
|
|
34
|
-
}, `${candidates.length} candidates for game ${game_id} at revision ${revision}`);
|
|
32
|
+
}, safeHandler(moveCandidatesSchema, async (input, signal) => {
|
|
33
|
+
const payload = await candidatePayload(services, input, signal);
|
|
34
|
+
return toolResult(payload, `${payload.candidates.length} candidates for game ${payload.game_id} at revision ${payload.revision}`);
|
|
35
35
|
}));
|
|
36
36
|
const byIntentSchema = TOOL_INPUT_SCHEMAS.move_candidates_by_intent;
|
|
37
37
|
server.registerTool("move_candidates_by_intent", {
|
|
38
38
|
...TOOL_META.move_candidates_by_intent,
|
|
39
39
|
inputSchema: byIntentSchema,
|
|
40
40
|
outputSchema: TOOL_OUTPUT_SCHEMAS.move_candidates_by_intent,
|
|
41
|
-
}, safeHandler(byIntentSchema, async ({
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const preset = ANALYSIS_PRESETS[analysis_level];
|
|
45
|
-
const depth = sf_depth ?? preset.depth;
|
|
46
|
-
const multipv = sf_multipv ?? preset.multipv;
|
|
47
|
-
const { candidates, moveSensitivity } = await services.computeCandidates(chess, elo, depth, multipv, maia_top_n, {
|
|
48
|
-
db: lichess_db,
|
|
49
|
-
speeds: lichess_speeds,
|
|
50
|
-
ratings: lichess_ratings,
|
|
51
|
-
}, signal);
|
|
52
|
-
signal.throwIfAborted();
|
|
53
|
-
const ranked = services.rankByIntent(candidates, intent);
|
|
41
|
+
}, safeHandler(byIntentSchema, async ({ intent, ...input }, signal) => {
|
|
42
|
+
const payload = await candidatePayload(services, input, signal);
|
|
43
|
+
const candidates = services.rankByIntent(payload.candidates, intent);
|
|
54
44
|
return toolResult({
|
|
55
|
-
|
|
56
|
-
revision,
|
|
57
|
-
fen: chess.fen(),
|
|
58
|
-
turn: chess.turn(),
|
|
45
|
+
...payload,
|
|
59
46
|
intent,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
moveSensitivity,
|
|
63
|
-
candidates: ranked,
|
|
64
|
-
}, `${ranked.length} ${intent} candidates for game ${game_id} at revision ${revision}`);
|
|
47
|
+
candidates,
|
|
48
|
+
}, `${candidates.length} ${intent} candidates for game ${payload.game_id} at revision ${payload.revision}`);
|
|
65
49
|
}));
|
|
66
50
|
}
|
package/dist/tools/explorer.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { snapshotChess } from "../chess.js";
|
|
2
1
|
import { ChessError } from "../errors.js";
|
|
3
2
|
import { OpeningExplorerInputSchema } from "../tool-inputs.js";
|
|
4
3
|
import { TOOL_META } from "../tool-meta.js";
|
|
@@ -13,8 +12,7 @@ export function registerExplorerTool(server, services) {
|
|
|
13
12
|
if (!services.explorerEnabled()) {
|
|
14
13
|
throw new ChessError("LICHESS_DISABLED", "LICHESS_TOKEN not set; opening explorer is disabled");
|
|
15
14
|
}
|
|
16
|
-
const { chess
|
|
17
|
-
const chess = snapshotChess(live);
|
|
15
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
18
16
|
const result = await services.openingExplorer(chess, db, speeds, ratings, signal);
|
|
19
17
|
return toolResult({ game_id, revision, ...result }, `Lichess ${db} returned ${result.moves.length} moves for game ${game_id}`);
|
|
20
18
|
}));
|
package/dist/tools/game.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseImportedPgn, parseMove,
|
|
1
|
+
import { parseImportedPgn, parseMove, stateOf } from "../chess.js";
|
|
2
2
|
import { ChessError } from "../errors.js";
|
|
3
3
|
import { TOOL_INPUT_SCHEMAS } from "../tool-inputs.js";
|
|
4
4
|
import { TOOL_META } from "../tool-meta.js";
|
|
@@ -39,7 +39,7 @@ export function registerGameTools(server, services) {
|
|
|
39
39
|
inputSchema: TOOL_INPUT_SCHEMAS.game_state,
|
|
40
40
|
outputSchema: TOOL_OUTPUT_SCHEMAS.game_state,
|
|
41
41
|
}, safeHandler(TOOL_INPUT_SCHEMAS.game_state, async ({ game_id, include_ascii }) => {
|
|
42
|
-
const { chess, revision } = services.games.
|
|
42
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
43
43
|
const state = stateOf(chess, revision);
|
|
44
44
|
return toolResult({
|
|
45
45
|
game_id,
|
|
@@ -52,7 +52,7 @@ export function registerGameTools(server, services) {
|
|
|
52
52
|
inputSchema: TOOL_INPUT_SCHEMAS.game_play_move,
|
|
53
53
|
outputSchema: TOOL_OUTPUT_SCHEMAS.game_play_move,
|
|
54
54
|
}, safeHandler(TOOL_INPUT_SCHEMAS.game_play_move, async ({ game_id, move, expected_revision }, signal) => {
|
|
55
|
-
const { chess, revision } = services.games.
|
|
55
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
56
56
|
if (expected_revision !== revision) {
|
|
57
57
|
throw new ChessError("STALE_POSITION", `position changed: expected revision ${expected_revision}, current ${revision}`);
|
|
58
58
|
}
|
|
@@ -61,16 +61,15 @@ export function registerGameTools(server, services) {
|
|
|
61
61
|
}
|
|
62
62
|
const parsed = parseMove(chess, move);
|
|
63
63
|
signal.throwIfAborted();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return toolResult({ game_id, move: parsed.san, ...stateOf(chess, newRevision) }, `Played ${parsed.san} in game ${game_id}; revision ${newRevision}`);
|
|
64
|
+
const { chess: next, revision: newRevision } = services.games.applyMove(game_id, expected_revision, parsed);
|
|
65
|
+
return toolResult({ game_id, move: parsed.san, ...stateOf(next, newRevision) }, `Played ${parsed.san} in game ${game_id}; revision ${newRevision}`);
|
|
67
66
|
}));
|
|
68
67
|
server.registerTool("game_legal_moves", {
|
|
69
68
|
...TOOL_META.game_legal_moves,
|
|
70
69
|
inputSchema: TOOL_INPUT_SCHEMAS.game_legal_moves,
|
|
71
70
|
outputSchema: TOOL_OUTPUT_SCHEMAS.game_legal_moves,
|
|
72
71
|
}, safeHandler(TOOL_INPUT_SCHEMAS.game_legal_moves, async ({ game_id }, signal) => {
|
|
73
|
-
const { chess, revision } = services.games.
|
|
72
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
74
73
|
const moves = [];
|
|
75
74
|
for (const move of chess.moves({ verbose: true })) {
|
|
76
75
|
signal.throwIfAborted();
|
|
@@ -93,7 +92,7 @@ export function registerGameTools(server, services) {
|
|
|
93
92
|
inputSchema: TOOL_INPUT_SCHEMAS.game_pgn,
|
|
94
93
|
outputSchema: TOOL_OUTPUT_SCHEMAS.game_pgn,
|
|
95
94
|
}, safeHandler(TOOL_INPUT_SCHEMAS.game_pgn, async ({ game_id }) => {
|
|
96
|
-
const { chess, revision } = services.games.
|
|
95
|
+
const { chess, revision } = services.games.getSnapshot(game_id);
|
|
97
96
|
return toolResult({ game_id, revision, pgn: chess.pgn() }, `Exported PGN for game ${game_id} at revision ${revision}`);
|
|
98
97
|
}));
|
|
99
98
|
server.registerTool("game_import_pgn", {
|
package/docs/architecture.md
CHANGED
|
@@ -112,10 +112,10 @@ a 2 MiB byte cap, initialization reserves one of 64 session slots atomically,
|
|
|
112
112
|
and only 16 POSTs process-wide or two per session may run concurrently. The same
|
|
113
113
|
limits independently bound downstream compute and network jobs. A job retains
|
|
114
114
|
its slot after the HTTP response or socket closes and releases it only when the
|
|
115
|
-
service promise settles.
|
|
116
|
-
their process-shared games. GET SSE streams
|
|
117
|
-
|
|
118
|
-
keep-alive limits are enforced by the Node listener.
|
|
115
|
+
service promise settles. Sessions with no active request expire after 30 minutes
|
|
116
|
+
without deleting their process-shared games. Open GET SSE streams keep their
|
|
117
|
+
session active without consuming POST permits. Header, upload, connection,
|
|
118
|
+
socket, and keep-alive limits are enforced by the Node listener.
|
|
119
119
|
|
|
120
120
|
The server has no MCP OAuth endpoints, OAuth discovery metadata, bearer-token
|
|
121
121
|
validation, or browser CORS support. A reverse proxy may implement its own
|