llm-chess-mcp 0.5.0 → 0.7.2
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 +30 -18
- package/dist/analysis-boundary.d.ts +2 -0
- package/dist/analysis-boundary.js +49 -0
- package/dist/chess-copy.d.ts +5 -2
- package/dist/chess-copy.js +70 -203
- package/dist/chess-move.d.ts +7 -0
- package/dist/chess-move.js +4 -0
- package/dist/chess.d.ts +2 -1
- package/dist/chess.js +5 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -3
- package/dist/domain.d.ts +18 -4
- package/dist/domain.js +6 -0
- package/dist/engines/stockfish-info.js +13 -7
- package/dist/engines/stockfish.d.ts +3 -0
- package/dist/engines/stockfish.js +454 -79
- package/dist/explorer-limiter.d.ts +1 -1
- package/dist/explorer-limiter.js +1 -1
- package/dist/explorer-retry.d.ts +2 -2
- package/dist/explorer-retry.js +15 -6
- package/dist/explorer-transport.d.ts +2 -0
- package/dist/explorer-transport.js +21 -2
- package/dist/explorer.js +30 -8
- package/dist/games.d.ts +4 -0
- package/dist/games.js +58 -27
- package/dist/http-body.d.ts +13 -0
- package/dist/http-body.js +103 -0
- package/dist/http-config.d.ts +33 -0
- package/dist/http-config.js +105 -0
- package/dist/http-posts.d.ts +11 -0
- package/dist/http-posts.js +37 -0
- package/dist/http-response.d.ts +3 -0
- package/dist/http-response.js +17 -0
- package/dist/http-runtime.d.ts +15 -0
- package/dist/http-runtime.js +308 -0
- package/dist/http.d.ts +2 -22
- package/dist/http.js +51 -466
- package/dist/human-boundary.d.ts +2 -0
- package/dist/human-boundary.js +34 -0
- package/dist/index.js +15 -15
- package/dist/intents.d.ts +1 -1
- package/dist/intents.js +60 -26
- package/dist/lifecycle.d.ts +2 -0
- package/dist/lifecycle.js +24 -0
- package/dist/maia3/inference.js +20 -5
- package/dist/pgn-lex.d.ts +14 -0
- package/dist/pgn-lex.js +72 -0
- package/dist/pgn-serialize.d.ts +4 -0
- package/dist/pgn-serialize.js +103 -0
- package/dist/pgn-shared.d.ts +13 -0
- package/dist/pgn-shared.js +43 -0
- package/dist/pgn.d.ts +1 -3
- package/dist/pgn.js +184 -432
- package/dist/position-validation.d.ts +3 -0
- package/dist/position-validation.js +372 -0
- package/dist/server.js +12 -14
- package/dist/services.js +36 -12
- package/dist/string-length.d.ts +1 -0
- package/dist/string-length.js +6 -0
- package/dist/tool-fields.d.ts +4 -0
- package/dist/tool-fields.js +16 -0
- package/dist/tool-inputs.js +18 -17
- package/dist/tool-meta.d.ts +1 -1
- package/dist/tool-meta.js +1 -1
- package/dist/tool-result.d.ts +22 -3
- package/dist/tool-result.js +30 -15
- package/dist/tool-schemas.d.ts +2 -8
- package/dist/tool-schemas.js +118 -29
- package/dist/tools/analysis.js +23 -9
- package/dist/tools/candidates.js +106 -24
- package/dist/tools/explorer.js +15 -2
- package/dist/tools/game.js +8 -8
- package/dist/tools/move-boundary.d.ts +8 -0
- package/dist/tools/move-boundary.js +13 -0
- package/docs/architecture.md +40 -24
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -72,8 +72,11 @@ await server.close();
|
|
|
72
72
|
The root API also exports `buildServer`, `GameStore`, `ChessError`,
|
|
73
73
|
`ExplorerError`, the service/domain types needed to provide custom
|
|
74
74
|
`AppServices`, and safe chess helpers including `parseImportedPgn`, `pgnOf`,
|
|
75
|
-
and `snapshotChess`.
|
|
76
|
-
`dist/`
|
|
75
|
+
and `snapshotChess`. The package root is the supported public API. Deep imports
|
|
76
|
+
under `dist/` are intentionally not exported and will fail with
|
|
77
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`; use named root exports instead.
|
|
78
|
+
This removes the previous `dist/*` compatibility exports and is a breaking
|
|
79
|
+
change for integrations that imported internal modules.
|
|
77
80
|
|
|
78
81
|
`bodyTimeoutMs` limits HTTP body upload time; it is not a whole-tool deadline.
|
|
79
82
|
The deprecated `requestTimeoutMs` alias remains supported when `bodyTimeoutMs`
|
|
@@ -173,7 +176,7 @@ codex mcp add llm-chess-mcp --command npx --args -y llm-chess-mcp --env LICHESS_
|
|
|
173
176
|
| Tool | Description |
|
|
174
177
|
|---|---|
|
|
175
178
|
| `create_game` | Create a game (optionally from a FEN), returns `game_id` |
|
|
176
|
-
| `delete_game` | Delete a game and free
|
|
179
|
+
| `delete_game` | Delete a process-shared game and free game capacity |
|
|
177
180
|
| `game_state` | Authoritative state: FEN, turn, revision, check/mate/draw flags, history, last move, castling (optional ASCII) |
|
|
178
181
|
| `game_play_move` | Play a move (SAN or UCI) — the only mutating tool, with stale-position guard |
|
|
179
182
|
| `game_legal_moves` | All legal moves with metadata |
|
|
@@ -207,9 +210,10 @@ human-readable summary and must not be parsed as data.
|
|
|
207
210
|
`best / excellent / good / inaccuracy / mistake / blunder`.
|
|
208
211
|
- `maia3Prob` is a **human-likelihood**, not move quality. A high-probability move
|
|
209
212
|
can still be objectively bad.
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
- Successful analysis continuations return corresponding `pv` and `pvSan`
|
|
214
|
+
arrays of equal length in UCI and SAN. An invalid engine continuation is
|
|
215
|
+
rejected at the internal tool boundary instead of returning a truncated
|
|
216
|
+
`pvSan`.
|
|
213
217
|
|
|
214
218
|
## Candidate structure
|
|
215
219
|
|
|
@@ -272,10 +276,11 @@ rejected:
|
|
|
272
276
|
- Up to 1,000 games are retained per process; idle games expire after one hour.
|
|
273
277
|
- `move_evaluate` accepts at most 10 moves per call.
|
|
274
278
|
- Imported and exported PGNs are limited to 1 MiB, 256 headers, and 4,096
|
|
275
|
-
plies; stored
|
|
276
|
-
mainline and variations together at 32,768
|
|
277
|
-
per lexical token. Every variation is
|
|
278
|
-
the mainline. UTF-8 BOMs and standard
|
|
279
|
+
plies; stored snapshots enforce the same byte, header, token, and ply resource
|
|
280
|
+
bounds. Imports also cap the mainline and variations together at 32,768
|
|
281
|
+
structural elements and 16 KiB per lexical token. Every variation is
|
|
282
|
+
legality-checked; game state retains the mainline. UTF-8 BOMs and standard
|
|
283
|
+
escaped header values are supported.
|
|
279
284
|
- Custom FENs reject inconsistent castling/en-passant metadata and impossible
|
|
280
285
|
pawn or promotion material.
|
|
281
286
|
- Stockfish accepts up to 32 active or queued analyses. Maia runs at most two
|
|
@@ -283,13 +288,18 @@ rejected:
|
|
|
283
288
|
- Lichess Explorer requests run one at a time and share 429 cooldowns.
|
|
284
289
|
- HTTP retains at most 64 MCP sessions; sessions with no active request expire
|
|
285
290
|
after 30 minutes. An open GET/SSE stream keeps its session active.
|
|
286
|
-
- HTTP accepts bodies up to 2 MiB
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
- HTTP accepts bodies up to 2 MiB under normal body-parser capacity. Once those
|
|
292
|
+
parsers are full, an overflow request receives only a small, up-to-8 KiB
|
|
293
|
+
probe; only a complete MCP cancellation notification can proceed, and no
|
|
294
|
+
accepted parser is preempted. The listener's connection limit bounds overflow
|
|
295
|
+
probes. After body parsing, it permits 16 concurrent POST dispatches and
|
|
296
|
+
downstream compute/network jobs process-wide, with two of each per session.
|
|
297
|
+
A separate bounded control lane prioritizes MCP cancellation when normal
|
|
298
|
+
dispatch capacity is full. If an existing-session POST response closes before
|
|
299
|
+
it finishes, its session is closed and its work is aborted; an uncooperative
|
|
300
|
+
downstream operation still holds capacity until it settles. HTTP also caps
|
|
301
|
+
connections at 128 and applies a 15-second body upload deadline plus bounded
|
|
302
|
+
header, socket, and keep-alive timeouts.
|
|
293
303
|
|
|
294
304
|
Programmatic users can override the HTTP limits through `HttpServerOptions`.
|
|
295
305
|
These safeguards do not replace public-edge quotas: a public deployment must
|
|
@@ -302,7 +312,9 @@ its UCI queue boundary, drains queued work during shutdown, and rejects new
|
|
|
302
312
|
analysis until teardown completes. Lichess fetch and retry waits abort
|
|
303
313
|
immediately. Maia runs native inference in dedicated child processes; cancelling
|
|
304
314
|
active work terminates its child, while queued cancellation is immediate. A raw
|
|
305
|
-
|
|
315
|
+
response disconnect for an existing-session POST closes that session and aborts
|
|
316
|
+
its work. Reconnect with a new session, then re-read the process-shared game
|
|
317
|
+
state before retrying a move.
|
|
306
318
|
|
|
307
319
|
## Intents
|
|
308
320
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { MAX_MULTIPV, WDL_TOTAL } from "./domain.js";
|
|
2
|
+
function validScore(value) {
|
|
3
|
+
return value === null || (typeof value === "number" && Number.isFinite(value));
|
|
4
|
+
}
|
|
5
|
+
function validWdl(value) {
|
|
6
|
+
return value === null ||
|
|
7
|
+
(Array.isArray(value) &&
|
|
8
|
+
value.length === 3 &&
|
|
9
|
+
value.every((count) => typeof count === "number" &&
|
|
10
|
+
Number.isSafeInteger(count) &&
|
|
11
|
+
count >= 0 &&
|
|
12
|
+
count <= WDL_TOTAL) &&
|
|
13
|
+
value[0] + value[1] + value[2] === WDL_TOTAL);
|
|
14
|
+
}
|
|
15
|
+
export function validateAnalysisLines(lines, requestedMultipv) {
|
|
16
|
+
if (!Number.isSafeInteger(requestedMultipv) ||
|
|
17
|
+
requestedMultipv < 1 ||
|
|
18
|
+
requestedMultipv > MAX_MULTIPV) {
|
|
19
|
+
throw new RangeError("invalid requested analysis multipv");
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(lines) || lines.length > requestedMultipv) {
|
|
22
|
+
throw new RangeError("analysis returned too many lines");
|
|
23
|
+
}
|
|
24
|
+
const ranks = new Set();
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (typeof line !== "object" ||
|
|
27
|
+
line === null ||
|
|
28
|
+
Array.isArray(line) ||
|
|
29
|
+
!Number.isSafeInteger(line.multipv) ||
|
|
30
|
+
line.multipv < 1 ||
|
|
31
|
+
line.multipv > requestedMultipv ||
|
|
32
|
+
ranks.has(line.multipv)) {
|
|
33
|
+
throw new RangeError("invalid analysis multipv rank");
|
|
34
|
+
}
|
|
35
|
+
ranks.add(line.multipv);
|
|
36
|
+
if (!validScore(line.scoreCp) || !validScore(line.scoreMate)) {
|
|
37
|
+
throw new RangeError("analysis line has an invalid score");
|
|
38
|
+
}
|
|
39
|
+
if (line.scoreCp !== null && line.scoreMate !== null) {
|
|
40
|
+
throw new RangeError("analysis line has conflicting scores");
|
|
41
|
+
}
|
|
42
|
+
if (!validWdl(line.wdl))
|
|
43
|
+
throw new RangeError("analysis line has invalid WDL");
|
|
44
|
+
if (!Array.isArray(line.pv) ||
|
|
45
|
+
!Array.from(line.pv).every((move) => typeof move === "string" && move.length > 0)) {
|
|
46
|
+
throw new RangeError("analysis line has invalid PV");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/chess-copy.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
|
-
export
|
|
3
|
-
export declare function
|
|
2
|
+
export { assertLegalPosition, assertSafeFenCounters, } from "./position-validation.js";
|
|
3
|
+
export declare function snapshotChessWithPgn(chess: Chess): {
|
|
4
|
+
chess: Chess;
|
|
5
|
+
pgn: string;
|
|
6
|
+
};
|
|
4
7
|
export declare function snapshotChess(chess: Chess): Chess;
|
package/dist/chess-copy.js
CHANGED
|
@@ -1,45 +1,11 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
|
+
import { materializeMove } from "./chess-move.js";
|
|
2
3
|
import { ChessError } from "./errors.js";
|
|
3
|
-
import { assertPgnPlyLimit, replacePgnHeaders } from "./pgn-shared.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
n: 2,
|
|
8
|
-
};
|
|
4
|
+
import { assertPgnPlyLimit, pgnHeaderIndex, pgnSetupHeaders, replacePgnHeaders, terminalPgnResult, } from "./pgn-shared.js";
|
|
5
|
+
import { serializePgn } from "./pgn-serialize.js";
|
|
6
|
+
import { assertLegalPosition, assertSafeFenCounters, } from "./position-validation.js";
|
|
7
|
+
export { assertLegalPosition, assertSafeFenCounters, } from "./position-validation.js";
|
|
9
8
|
const CHESS_STATE_KEYS = Reflect.ownKeys(new Chess());
|
|
10
|
-
function squareColor(square) {
|
|
11
|
-
return ((square.charCodeAt(0) - 97 + Number(square[1])) % 2);
|
|
12
|
-
}
|
|
13
|
-
function minimumPawnCaptures(chess, color) {
|
|
14
|
-
const pawns = chess
|
|
15
|
-
.findPiece({ type: "p", color })
|
|
16
|
-
.map((square) => ({
|
|
17
|
-
advances: color === "w" ? Number(square[1]) - 2 : 7 - Number(square[1]),
|
|
18
|
-
file: square.charCodeAt(0) - 97,
|
|
19
|
-
}))
|
|
20
|
-
.sort((left, right) => left.file - right.file);
|
|
21
|
-
let costs = new Map([[0, 0]]);
|
|
22
|
-
for (const pawn of pawns) {
|
|
23
|
-
const next = new Map();
|
|
24
|
-
for (const [mask, cost] of costs) {
|
|
25
|
-
for (let original = 0; original < 8; original += 1) {
|
|
26
|
-
const bit = 1 << original;
|
|
27
|
-
if (mask & bit)
|
|
28
|
-
continue;
|
|
29
|
-
const captures = Math.abs(original - pawn.file);
|
|
30
|
-
if (captures > pawn.advances)
|
|
31
|
-
continue;
|
|
32
|
-
const nextMask = mask | bit;
|
|
33
|
-
next.set(nextMask, Math.min(next.get(nextMask) ?? Infinity, cost + captures));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
costs = next;
|
|
37
|
-
}
|
|
38
|
-
return Math.min(...costs.values());
|
|
39
|
-
}
|
|
40
|
-
function nonKingMaterial(chess, color) {
|
|
41
|
-
return ["p", "q", "r", "b", "n"].reduce((total, type) => total + chess.findPiece({ type, color }).length, 0);
|
|
42
|
-
}
|
|
43
9
|
function clonedChess(chess) {
|
|
44
10
|
const state = Object.create(null);
|
|
45
11
|
for (const key of CHESS_STATE_KEYS) {
|
|
@@ -66,132 +32,8 @@ function clonedChess(chess) {
|
|
|
66
32
|
function exactFen(chess) {
|
|
67
33
|
return chess.fen({ forceEnpassantSquare: true });
|
|
68
34
|
}
|
|
69
|
-
function hasPiece(chess, square, type, color) {
|
|
70
|
-
const piece = chess.get(square);
|
|
71
|
-
return piece?.type === type && piece.color === color;
|
|
72
|
-
}
|
|
73
|
-
function assertCastlingPosition(chess, color) {
|
|
74
|
-
const rank = color === "w" ? "1" : "8";
|
|
75
|
-
const rights = chess.getCastlingRights(color);
|
|
76
|
-
if ((rights.k || rights.q) &&
|
|
77
|
-
!hasPiece(chess, `e${rank}`, "k", color)) {
|
|
78
|
-
throw new ChessError("INVALID_FEN", "FEN castling rights require a home king");
|
|
79
|
-
}
|
|
80
|
-
if (rights.k && !hasPiece(chess, `h${rank}`, "r", color)) {
|
|
81
|
-
throw new ChessError("INVALID_FEN", "FEN kingside castling rights require a home rook");
|
|
82
|
-
}
|
|
83
|
-
if (rights.q && !hasPiece(chess, `a${rank}`, "r", color)) {
|
|
84
|
-
throw new ChessError("INVALID_FEN", "FEN queenside castling rights require a home rook");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function assertEnPassantPosition(chess) {
|
|
88
|
-
const fields = chess.fen({ forceEnpassantSquare: true }).split(" ");
|
|
89
|
-
const target = fields[3];
|
|
90
|
-
if (!target || target === "-")
|
|
91
|
-
return;
|
|
92
|
-
const turn = chess.turn();
|
|
93
|
-
const file = target[0];
|
|
94
|
-
const targetRank = turn === "w" ? "6" : "3";
|
|
95
|
-
const pawnRank = turn === "w" ? "5" : "4";
|
|
96
|
-
const originRank = turn === "w" ? "7" : "2";
|
|
97
|
-
const pawnColor = turn === "w" ? "b" : "w";
|
|
98
|
-
const targetSquare = target;
|
|
99
|
-
const pawnSquare = `${file}${pawnRank}`;
|
|
100
|
-
const originSquare = `${file}${originRank}`;
|
|
101
|
-
if (target[1] !== targetRank ||
|
|
102
|
-
chess.get(targetSquare) !== undefined ||
|
|
103
|
-
!hasPiece(chess, pawnSquare, "p", pawnColor) ||
|
|
104
|
-
chess.get(originSquare) !== undefined ||
|
|
105
|
-
fields[4] !== "0" ||
|
|
106
|
-
(turn === "w" && fields[5] === "1")) {
|
|
107
|
-
throw new ChessError("INVALID_FEN", "FEN en passant target does not match a double pawn move");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function moveDescriptor(move) {
|
|
111
|
-
const base = { from: move.from, to: move.to };
|
|
112
|
-
return move.promotion ? { ...base, promotion: move.promotion } : base;
|
|
113
|
-
}
|
|
114
|
-
function isSafeDecimal(value, minimum) {
|
|
115
|
-
return (/^(?:0|[1-9]\d*)$/.test(value) &&
|
|
116
|
-
Number.isSafeInteger(Number(value)) &&
|
|
117
|
-
Number(value) >= minimum);
|
|
118
|
-
}
|
|
119
|
-
export function assertSafeFenCounters(fen) {
|
|
120
|
-
const fields = fen.split(/\s+/);
|
|
121
|
-
if (fields.length >= 5 && !isSafeDecimal(fields[4] ?? "", 0)) {
|
|
122
|
-
throw new ChessError("INVALID_FEN", "FEN halfmove clock must be a non-negative safe decimal integer");
|
|
123
|
-
}
|
|
124
|
-
if (fields.length >= 6 && !isSafeDecimal(fields[5] ?? "", 1)) {
|
|
125
|
-
throw new ChessError("INVALID_FEN", "FEN fullmove number must be a positive safe decimal integer");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
export function assertLegalPosition(chess) {
|
|
129
|
-
for (const color of ["w", "b"]) {
|
|
130
|
-
if (chess.findPiece({ type: "k", color }).length !== 1) {
|
|
131
|
-
throw new ChessError("INVALID_FEN", "FEN must contain exactly one king per side");
|
|
132
|
-
}
|
|
133
|
-
const pawns = chess.findPiece({ type: "p", color });
|
|
134
|
-
if (pawns.some((square) => square[1] === "1" || square[1] === "8")) {
|
|
135
|
-
throw new ChessError("INVALID_FEN", "FEN pawns cannot occupy the first or eighth rank");
|
|
136
|
-
}
|
|
137
|
-
if (pawns.length > 8) {
|
|
138
|
-
throw new ChessError("INVALID_FEN", "FEN cannot contain more than eight pawns per side");
|
|
139
|
-
}
|
|
140
|
-
const promotedPieces = Object.entries(ORIGINAL_PIECES).reduce((total, [type, original]) => total +
|
|
141
|
-
Math.max(0, chess.findPiece({ type: type, color }).length -
|
|
142
|
-
original), 0);
|
|
143
|
-
const promotedBishops = [0, 1].reduce((total, squareColorValue) => total +
|
|
144
|
-
Math.max(0, chess
|
|
145
|
-
.findPiece({ type: "b", color })
|
|
146
|
-
.filter((square) => squareColor(square) === squareColorValue)
|
|
147
|
-
.length - 1), 0);
|
|
148
|
-
const promoted = promotedPieces + promotedBishops;
|
|
149
|
-
if (promoted > 8 - pawns.length) {
|
|
150
|
-
throw new ChessError("INVALID_FEN", "FEN contains more promoted material than missing pawns allow");
|
|
151
|
-
}
|
|
152
|
-
assertCastlingPosition(chess, color);
|
|
153
|
-
const opponent = color === "w" ? "b" : "w";
|
|
154
|
-
const missingOpponentMaterial = 15 - nonKingMaterial(chess, opponent);
|
|
155
|
-
if (minimumPawnCaptures(chess, color) > missingOpponentMaterial) {
|
|
156
|
-
throw new ChessError("INVALID_FEN", "FEN pawn files require more captures than opposing material allows");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
assertEnPassantPosition(chess);
|
|
160
|
-
const turn = chess.turn();
|
|
161
|
-
const previous = turn === "w" ? "b" : "w";
|
|
162
|
-
const previousKing = chess.findPiece({ type: "k", color: previous })[0];
|
|
163
|
-
if (previousKing && chess.isAttacked(previousKing, turn)) {
|
|
164
|
-
throw new ChessError("INVALID_FEN", "FEN cannot leave the side that just moved in check");
|
|
165
|
-
}
|
|
166
|
-
const king = chess.findPiece({ type: "k", color: turn })[0];
|
|
167
|
-
if (king) {
|
|
168
|
-
const checkers = chess.attackers(king, previous);
|
|
169
|
-
const leapers = checkers.filter((square) => {
|
|
170
|
-
const type = chess.get(square)?.type;
|
|
171
|
-
return type === "k" || type === "n" || type === "p";
|
|
172
|
-
});
|
|
173
|
-
if (checkers.length > 2 || leapers.length > 1) {
|
|
174
|
-
throw new ChessError("INVALID_FEN", "FEN contains an impossible check topology");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
35
|
function expectedInitialFen(headers) {
|
|
179
|
-
const
|
|
180
|
-
for (const [name, value] of headers) {
|
|
181
|
-
const key = name.toLowerCase();
|
|
182
|
-
if (values.has(key)) {
|
|
183
|
-
throw new ChessError("INVALID_PGN", `PGN must not repeat ${name} headers`);
|
|
184
|
-
}
|
|
185
|
-
values.set(key, value);
|
|
186
|
-
}
|
|
187
|
-
const setup = values.get("setup");
|
|
188
|
-
const fen = values.get("fen");
|
|
189
|
-
if (setup !== undefined && setup !== "0" && setup !== "1") {
|
|
190
|
-
throw new ChessError("INVALID_PGN", "PGN SetUp must be 0 or 1");
|
|
191
|
-
}
|
|
192
|
-
if ((setup === "1") !== (fen !== undefined)) {
|
|
193
|
-
throw new ChessError("INVALID_PGN", "PGN SetUp 1 and FEN headers must appear together");
|
|
194
|
-
}
|
|
36
|
+
const { fen } = pgnSetupHeaders(pgnHeaderIndex(headers));
|
|
195
37
|
if (fen === undefined)
|
|
196
38
|
return exactFen(new Chess());
|
|
197
39
|
assertSafeFenCounters(fen);
|
|
@@ -226,21 +68,69 @@ function validatedHistory(chess) {
|
|
|
226
68
|
}
|
|
227
69
|
return { history, initialFen, shadow, sourceHeaders };
|
|
228
70
|
}
|
|
229
|
-
|
|
230
|
-
assertLegalPosition(chess);
|
|
231
|
-
const { history, initialFen, shadow, sourceHeaders } = validatedHistory(chess);
|
|
232
|
-
assertPgnPlyLimit(history.length);
|
|
233
|
-
assertSafeFenCounters(initialFen);
|
|
234
|
-
const snapshot = new Chess(initialFen);
|
|
235
|
-
assertLegalPosition(snapshot);
|
|
71
|
+
function commentsByFen(chess, shadow) {
|
|
236
72
|
const getComments = chess.getComments;
|
|
237
73
|
const sourceComments = getComments === Chess.prototype.getComments
|
|
238
74
|
? Chess.prototype.getComments.call(shadow)
|
|
239
75
|
: getComments.call(chess);
|
|
240
|
-
|
|
76
|
+
return new Map(sourceComments.map(({ fen, comment }) => [
|
|
241
77
|
fen,
|
|
242
78
|
/[{}]/.test(comment) ? comment.replace(/[\r\n]+/g, " ") : comment,
|
|
243
79
|
]));
|
|
80
|
+
}
|
|
81
|
+
function replayHistory(chess, history, restoreComment) {
|
|
82
|
+
restoreComment();
|
|
83
|
+
for (const move of history) {
|
|
84
|
+
chess.move(materializeMove(move));
|
|
85
|
+
restoreComment();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function snapshotPgn(chess) {
|
|
89
|
+
const result = terminalPgnResult(chess);
|
|
90
|
+
const originalResult = chess.getHeaders().Result;
|
|
91
|
+
if (result !== undefined)
|
|
92
|
+
chess.setHeader("Result", result);
|
|
93
|
+
try {
|
|
94
|
+
const headers = Object.entries(chess.getHeaders());
|
|
95
|
+
return serializePgn(() => Chess.prototype.pgn.call(chess), headers, Chess.prototype.getComments.call(chess).map(({ comment }) => comment));
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (result !== undefined) {
|
|
99
|
+
if (originalResult === undefined)
|
|
100
|
+
chess.removeHeader("Result");
|
|
101
|
+
else
|
|
102
|
+
chess.setHeader("Result", originalResult);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function restoreUnsafeComments(snapshot, comments, markerPrefix, markerComments, sourceHeaders) {
|
|
107
|
+
const escapedPrefix = markerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
108
|
+
const marker = new RegExp(`\\{${escapedPrefix}(\\d+)${escapedPrefix}\\}`, "g");
|
|
109
|
+
const pgn = snapshot.pgn().replace(marker, (_match, index) => {
|
|
110
|
+
return `;${markerComments[Number(index)]}\n`;
|
|
111
|
+
});
|
|
112
|
+
const restored = new Chess();
|
|
113
|
+
restored.loadPgn(pgn);
|
|
114
|
+
const restoredHistory = restored.history({ verbose: true });
|
|
115
|
+
while (restored.undo()) { }
|
|
116
|
+
replayHistory(restored, restoredHistory, () => {
|
|
117
|
+
const comment = comments.get(restored.fen());
|
|
118
|
+
if (comment !== undefined && !/[{}]/.test(comment)) {
|
|
119
|
+
restored.setComment(comment);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
replacePgnHeaders(restored, sourceHeaders, { removeMissing: true });
|
|
123
|
+
assertSafeFenCounters(restored.fen());
|
|
124
|
+
return restored;
|
|
125
|
+
}
|
|
126
|
+
export function snapshotChessWithPgn(chess) {
|
|
127
|
+
assertLegalPosition(chess);
|
|
128
|
+
const { history, initialFen, shadow, sourceHeaders } = validatedHistory(chess);
|
|
129
|
+
assertPgnPlyLimit(history.length);
|
|
130
|
+
assertSafeFenCounters(initialFen);
|
|
131
|
+
const snapshot = new Chess(initialFen);
|
|
132
|
+
assertLegalPosition(snapshot);
|
|
133
|
+
const comments = commentsByFen(chess, shadow);
|
|
244
134
|
const unsafeComments = [...comments.values()].some((comment) => /[{}]/.test(comment));
|
|
245
135
|
let markerPrefix = "\uE000";
|
|
246
136
|
if (unsafeComments) {
|
|
@@ -249,7 +139,7 @@ export function snapshotChess(chess) {
|
|
|
249
139
|
markerPrefix += "\uE001";
|
|
250
140
|
}
|
|
251
141
|
const markerComments = [];
|
|
252
|
-
|
|
142
|
+
replayHistory(snapshot, history, () => {
|
|
253
143
|
const comment = comments.get(snapshot.fen());
|
|
254
144
|
if (comment === undefined)
|
|
255
145
|
return;
|
|
@@ -260,38 +150,15 @@ export function snapshotChess(chess) {
|
|
|
260
150
|
const marker = `${markerPrefix}${markerComments.length}${markerPrefix}`;
|
|
261
151
|
markerComments.push(comment);
|
|
262
152
|
snapshot.setComment(marker);
|
|
263
|
-
};
|
|
264
|
-
restoreComment();
|
|
265
|
-
for (const move of history) {
|
|
266
|
-
snapshot.move(moveDescriptor(move));
|
|
267
|
-
restoreComment();
|
|
268
|
-
}
|
|
153
|
+
});
|
|
269
154
|
assertSafeFenCounters(snapshot.fen());
|
|
270
155
|
if (!unsafeComments) {
|
|
271
156
|
replacePgnHeaders(snapshot, sourceHeaders, { removeMissing: true });
|
|
272
|
-
return snapshot;
|
|
273
|
-
}
|
|
274
|
-
const escapedPrefix = markerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
275
|
-
const marker = new RegExp(`\\{${escapedPrefix}(\\d+)${escapedPrefix}\\}`, "g");
|
|
276
|
-
const pgn = snapshot.pgn().replace(marker, (_match, index) => {
|
|
277
|
-
return `;${markerComments[Number(index)]}\n`;
|
|
278
|
-
});
|
|
279
|
-
const restored = new Chess();
|
|
280
|
-
restored.loadPgn(pgn);
|
|
281
|
-
const restoredHistory = restored.history({ verbose: true });
|
|
282
|
-
while (restored.undo()) { }
|
|
283
|
-
const restoreSafeComment = () => {
|
|
284
|
-
const comment = comments.get(restored.fen());
|
|
285
|
-
if (comment !== undefined && !/[{}]/.test(comment)) {
|
|
286
|
-
restored.setComment(comment);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
restoreSafeComment();
|
|
290
|
-
for (const move of restoredHistory) {
|
|
291
|
-
restored.move(moveDescriptor(move));
|
|
292
|
-
restoreSafeComment();
|
|
157
|
+
return { chess: snapshot, pgn: snapshotPgn(snapshot) };
|
|
293
158
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
159
|
+
const restored = restoreUnsafeComments(snapshot, comments, markerPrefix, markerComments, sourceHeaders);
|
|
160
|
+
return { chess: restored, pgn: snapshotPgn(restored) };
|
|
161
|
+
}
|
|
162
|
+
export function snapshotChess(chess) {
|
|
163
|
+
return snapshotChessWithPgn(chess).chess;
|
|
297
164
|
}
|
package/dist/chess.d.ts
CHANGED
|
@@ -3,11 +3,12 @@ import type { Move } from "chess.js";
|
|
|
3
3
|
export { assertLegalPosition, assertSafeFenCounters, snapshotChess, } from "./chess-copy.js";
|
|
4
4
|
export { MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, pgnOf, } from "./pgn.js";
|
|
5
5
|
import type { ChessState, DrawResult } from "./domain.js";
|
|
6
|
+
import { type MoveDescriptor } from "./chess-move.js";
|
|
6
7
|
export declare const MAX_EVALUATED_MOVES = 10;
|
|
7
8
|
export declare function drawResult(chess: Chess): DrawResult | null;
|
|
8
9
|
export declare function stateOf<Revision extends number>(chess: Chess, revision: Revision): ChessState & {
|
|
9
10
|
revision: Revision;
|
|
10
11
|
};
|
|
11
12
|
export declare function parseMove(chess: Chess, move: string): Move;
|
|
12
|
-
export declare function playParsedMove(chess: Chess, move:
|
|
13
|
+
export declare function playParsedMove(chess: Chess, move: MoveDescriptor): Move;
|
|
13
14
|
export declare function pvToSan(chess: Chess, pv: readonly string[]): string[];
|
package/dist/chess.js
CHANGED
|
@@ -2,11 +2,8 @@ import { Chess } from "chess.js";
|
|
|
2
2
|
export { assertLegalPosition, assertSafeFenCounters, snapshotChess, } from "./chess-copy.js";
|
|
3
3
|
export { MAX_PGN_BYTES, MAX_PGN_HEADERS, MAX_PGN_PLIES, MAX_PGN_TOKEN_BYTES, parseImportedPgn, pgnOf, } from "./pgn.js";
|
|
4
4
|
import { ChessError } from "./errors.js";
|
|
5
|
+
import { materializeMove } from "./chess-move.js";
|
|
5
6
|
export const MAX_EVALUATED_MOVES = 10;
|
|
6
|
-
function moveDescriptor(move) {
|
|
7
|
-
const base = { from: move.from, to: move.to };
|
|
8
|
-
return move.promotion ? { ...base, promotion: move.promotion } : base;
|
|
9
|
-
}
|
|
10
7
|
export function drawResult(chess) {
|
|
11
8
|
if (chess.isCheckmate() || !chess.isDraw())
|
|
12
9
|
return null;
|
|
@@ -21,7 +18,8 @@ export function drawResult(chess) {
|
|
|
21
18
|
return "draw";
|
|
22
19
|
}
|
|
23
20
|
export function stateOf(chess, revision) {
|
|
24
|
-
const
|
|
21
|
+
const history = chess.history({ verbose: true });
|
|
22
|
+
const last = history.at(-1);
|
|
25
23
|
const isCheckmate = chess.isCheckmate();
|
|
26
24
|
return {
|
|
27
25
|
fen: chess.fen(),
|
|
@@ -36,7 +34,7 @@ export function stateOf(chess, revision) {
|
|
|
36
34
|
isThreefoldRepetition: !isCheckmate && chess.isThreefoldRepetition(),
|
|
37
35
|
isDrawByFiftyMoves: !isCheckmate && chess.isDrawByFiftyMoves(),
|
|
38
36
|
moveNumber: chess.moveNumber(),
|
|
39
|
-
history:
|
|
37
|
+
history: history.map((move) => move.san),
|
|
40
38
|
lastMove: last ? { san: last.san, uci: last.lan } : null,
|
|
41
39
|
castling: {
|
|
42
40
|
whiteKingside: chess.getCastlingRights("w").k,
|
|
@@ -58,7 +56,7 @@ export function parseMove(chess, move) {
|
|
|
58
56
|
return found;
|
|
59
57
|
}
|
|
60
58
|
export function playParsedMove(chess, move) {
|
|
61
|
-
return chess.move(
|
|
59
|
+
return chess.move(materializeMove(move));
|
|
62
60
|
}
|
|
63
61
|
export function pvToSan(chess, pv) {
|
|
64
62
|
const copy = new Chess(chess.fen());
|
package/dist/cli.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export type CliOptions = {
|
|
|
7
7
|
allowedHosts: string[];
|
|
8
8
|
help: boolean;
|
|
9
9
|
};
|
|
10
|
-
export declare const HELP = "Usage: llm-chess-mcp [options]\n\nOptions:\n --transport <stdio|http> Transport to use (default: stdio)\n --http Shortcut for --transport http\n --host <host> HTTP bind host (default: 127.0.0.1)\n --port <port> HTTP listen port (default: 3000)\n --path <path> HTTP endpoint path (default: /mcp)\n --allowed-host <host> Allowed HTTP Host/Origin hostname (repeatable)\n -h, --help Show this help\n";
|
|
10
|
+
export declare const HELP = "Usage: llm-chess-mcp [options]\n\nOptions:\n --transport <stdio|http> Transport to use (default: stdio)\n --http Shortcut for --transport http\n --host <host> HTTP bind host (default: 127.0.0.1)\n --port <port> HTTP listen port; 0 selects an available port (default: 3000)\n --path <path> HTTP endpoint path (default: /mcp)\n --allowed-host <host> Allowed HTTP Host/Origin hostname (repeatable)\n -h, --help Show this help\n";
|
|
11
11
|
export declare function parseCli(args: string[]): CliOptions;
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ Options:
|
|
|
5
5
|
--transport <stdio|http> Transport to use (default: stdio)
|
|
6
6
|
--http Shortcut for --transport http
|
|
7
7
|
--host <host> HTTP bind host (default: 127.0.0.1)
|
|
8
|
-
--port <port> HTTP listen port (default: 3000)
|
|
8
|
+
--port <port> HTTP listen port; 0 selects an available port (default: 3000)
|
|
9
9
|
--path <path> HTTP endpoint path (default: /mcp)
|
|
10
10
|
--allowed-host <host> Allowed HTTP Host/Origin hostname (repeatable)
|
|
11
11
|
-h, --help Show this help
|
|
@@ -90,8 +90,8 @@ export function parseCli(args) {
|
|
|
90
90
|
throw new Error(`unknown option: ${option}`);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
if (!Number.isInteger(port) || port <
|
|
94
|
-
throw new Error("--port must be between
|
|
93
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
94
|
+
throw new Error("--port must be between 0 and 65535");
|
|
95
95
|
}
|
|
96
96
|
if (!isCanonicalHttpPath(path)) {
|
|
97
97
|
throw new Error("--path must be an absolute URL path without query or fragment");
|
package/dist/domain.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare const MOVE_EVALUATION_RESULTS: readonly ["ongoing", "checkmate",
|
|
|
13
13
|
export type MoveEvaluationResult = (typeof MOVE_EVALUATION_RESULTS)[number];
|
|
14
14
|
export declare const ANALYSIS_LEVELS: readonly ["fast", "normal", "deep"];
|
|
15
15
|
export type AnalysisLevel = (typeof ANALYSIS_LEVELS)[number];
|
|
16
|
+
export declare const MAX_ANALYSIS_DEPTH = 30;
|
|
17
|
+
export declare const MAX_MULTIPV = 10;
|
|
18
|
+
export declare const MAX_HUMAN_MOVES = 20;
|
|
19
|
+
export declare const HUMAN_PROBABILITY_TOLERANCE = 0.00001;
|
|
20
|
+
export declare const GAME_ID_MAX_LENGTH = 256;
|
|
21
|
+
export declare const WDL_TOTAL = 1000;
|
|
16
22
|
export declare const MOVE_CLASSIFICATIONS: readonly ["best", "excellent", "good", "inaccuracy", "mistake", "blunder"];
|
|
17
23
|
export type MoveClassification = (typeof MOVE_CLASSIFICATIONS)[number];
|
|
18
24
|
export declare const INTENTS: readonly ["best", "strong", "natural", "balanced", "ease_off", "give_chance"];
|
|
@@ -49,13 +55,21 @@ export interface ChessState {
|
|
|
49
55
|
};
|
|
50
56
|
}
|
|
51
57
|
export type Wdl = [number, number, number];
|
|
52
|
-
|
|
58
|
+
type SfScore = {
|
|
59
|
+
scoreCp: number;
|
|
60
|
+
scoreMate: null;
|
|
61
|
+
} | {
|
|
62
|
+
scoreCp: null;
|
|
63
|
+
scoreMate: number;
|
|
64
|
+
} | {
|
|
65
|
+
scoreCp: null;
|
|
66
|
+
scoreMate: null;
|
|
67
|
+
};
|
|
68
|
+
export type SfLine = {
|
|
53
69
|
multipv: number;
|
|
54
|
-
scoreCp: number | null;
|
|
55
|
-
scoreMate: number | null;
|
|
56
70
|
wdl: Wdl | null;
|
|
57
71
|
pv: string[];
|
|
58
|
-
}
|
|
72
|
+
} & SfScore;
|
|
59
73
|
export interface Maia3Move {
|
|
60
74
|
uci: string;
|
|
61
75
|
san: string;
|
package/dist/domain.js
CHANGED
|
@@ -25,6 +25,12 @@ export const MOVE_EVALUATION_RESULTS = [
|
|
|
25
25
|
...DRAW_RESULTS,
|
|
26
26
|
];
|
|
27
27
|
export const ANALYSIS_LEVELS = ["fast", "normal", "deep"];
|
|
28
|
+
export const MAX_ANALYSIS_DEPTH = 30;
|
|
29
|
+
export const MAX_MULTIPV = 10;
|
|
30
|
+
export const MAX_HUMAN_MOVES = 20;
|
|
31
|
+
export const HUMAN_PROBABILITY_TOLERANCE = 1e-5;
|
|
32
|
+
export const GAME_ID_MAX_LENGTH = 256;
|
|
33
|
+
export const WDL_TOTAL = 1_000;
|
|
28
34
|
export const MOVE_CLASSIFICATIONS = [
|
|
29
35
|
"best",
|
|
30
36
|
"excellent",
|