@chessceo/mcp 0.12.0 → 0.15.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/index.js +141 -12
- package/docs/engine-usage.md +13 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -224,7 +224,7 @@ const TOOLS = [
|
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
name: "analyse",
|
|
227
|
-
description: "Short Stockfish evaluation at a position. Returns the top-N candidate moves with score (centipawns from side-to-move POV, positive = advantage; or mate distance) and the principal variation for each. Defaults: 2s think time, top-3 lines. PV moves come back in
|
|
227
|
+
description: "Short Stockfish evaluation at a position. Returns the top-N candidate moves with score (centipawns from side-to-move POV, positive = advantage; or mate distance) and the principal variation for each. Defaults: 2s think time, top-3 lines. PV moves come back in SAN (e4, Nf3, Bxc4 — not UCI). Free (no cloud instance needed) — use liberally.\n\n" +
|
|
228
228
|
"GROUNDING: cite this tool's actual output when you claim things about positions. Don't invent evaluations from general principles or training data — if you don't have engine output for a FEN, call this. Compute is cheap.\n\n" +
|
|
229
229
|
"Use this to sanity-check candidate lines from get_position_stats or get_player_preparation — human game frequency tells you what people play, engine evaluation tells you what's actually good.",
|
|
230
230
|
inputSchema: {
|
|
@@ -404,6 +404,127 @@ const TOOLS = [
|
|
|
404
404
|
// capped so the two doc-reading tools (~5-10 KB of static markdown each)
|
|
405
405
|
// don't drown the log stream.
|
|
406
406
|
const LOG_MAX_CHARS = 4096;
|
|
407
|
+
// Convert a UCI move sequence into SAN by walking it move-by-move on
|
|
408
|
+
// chess.js from the given starting FEN. LLMs reason far better in SAN
|
|
409
|
+
// ("Nf3", "Bxc4") than UCI ("g1f3", "b5c4"), and matches how prep
|
|
410
|
+
// discussion is written in the real world. If a move fails to parse
|
|
411
|
+
// (illegal from the current position — bug or truncated PV), we
|
|
412
|
+
// truncate cleanly rather than throwing so the response still carries
|
|
413
|
+
// what we could convert.
|
|
414
|
+
function uciLineToSAN(startFen, uciMoves) {
|
|
415
|
+
const board = new Chess(startFen);
|
|
416
|
+
const out = [];
|
|
417
|
+
for (const uci of uciMoves) {
|
|
418
|
+
if (uci.length < 4)
|
|
419
|
+
break;
|
|
420
|
+
try {
|
|
421
|
+
const move = board.move({
|
|
422
|
+
from: uci.slice(0, 2),
|
|
423
|
+
to: uci.slice(2, 4),
|
|
424
|
+
promotion: uci.length >= 5 ? uci[4] : undefined,
|
|
425
|
+
});
|
|
426
|
+
if (!move)
|
|
427
|
+
break;
|
|
428
|
+
out.push(move.san);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
function uciMoveToSAN(startFen, uci) {
|
|
437
|
+
if (!uci || uci.length < 4)
|
|
438
|
+
return uci;
|
|
439
|
+
const board = new Chess(startFen);
|
|
440
|
+
try {
|
|
441
|
+
const move = board.move({
|
|
442
|
+
from: uci.slice(0, 2),
|
|
443
|
+
to: uci.slice(2, 4),
|
|
444
|
+
promotion: uci.length >= 5 ? uci[4] : undefined,
|
|
445
|
+
});
|
|
446
|
+
return move ? move.san : uci;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return uci;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Rewrite the local /chess/database/analyse response (single engine)
|
|
453
|
+
// so PVs come back in SAN.
|
|
454
|
+
function convertAnalyseResponse(raw, startFen) {
|
|
455
|
+
if (!raw || typeof raw !== "object")
|
|
456
|
+
return raw;
|
|
457
|
+
const r = raw;
|
|
458
|
+
if (Array.isArray(r.lines)) {
|
|
459
|
+
for (const line of r.lines) {
|
|
460
|
+
if (Array.isArray(line.pv))
|
|
461
|
+
line.pv = uciLineToSAN(startFen, line.pv);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return raw;
|
|
465
|
+
}
|
|
466
|
+
// Rewrite the /api/agent/cloud-engines/analyse response (two engines,
|
|
467
|
+
// each with lines[] and a bestMove) so PVs and bestMove come back in SAN.
|
|
468
|
+
function convertCloudSnapshotResponse(raw, startFen) {
|
|
469
|
+
if (!raw || typeof raw !== "object")
|
|
470
|
+
return raw;
|
|
471
|
+
const r = raw;
|
|
472
|
+
for (const eng of [r.stockfish, r.lc0]) {
|
|
473
|
+
if (!eng)
|
|
474
|
+
continue;
|
|
475
|
+
if (Array.isArray(eng.lines)) {
|
|
476
|
+
for (const line of eng.lines) {
|
|
477
|
+
if (Array.isArray(line.pv))
|
|
478
|
+
line.pv = uciLineToSAN(startFen, line.pv);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (typeof eng.bestMove === "string")
|
|
482
|
+
eng.bestMove = uciMoveToSAN(startFen, eng.bestMove);
|
|
483
|
+
}
|
|
484
|
+
return raw;
|
|
485
|
+
}
|
|
486
|
+
// Rewrite availableMoves[].move UCI → SAN. The prep + position-stats
|
|
487
|
+
// endpoints return moves in UCI on the wire — same LLM-readability
|
|
488
|
+
// concern as engine PVs, and the same wrapper-only fix. Passes the
|
|
489
|
+
// response through unchanged if there's no availableMoves array.
|
|
490
|
+
function convertAvailableMovesToSAN(raw, fen) {
|
|
491
|
+
if (!raw || typeof raw !== "object")
|
|
492
|
+
return raw;
|
|
493
|
+
const r = raw;
|
|
494
|
+
if (!Array.isArray(r.availableMoves))
|
|
495
|
+
return raw;
|
|
496
|
+
for (const m of r.availableMoves) {
|
|
497
|
+
if (typeof m.move === "string" && m.move.length >= 4) {
|
|
498
|
+
m.move = uciMoveToSAN(fen, m.move);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return raw;
|
|
502
|
+
}
|
|
503
|
+
// Resolve a starting FEN from either the `fen` or `line` argument the tool
|
|
504
|
+
// received. Same logic prep_snapshot already had inline — extracted so we
|
|
505
|
+
// can reuse it wherever we need to walk a SAN line to a concrete FEN
|
|
506
|
+
// (e.g. for UCI→SAN conversion of availableMoves).
|
|
507
|
+
function resolveFenFromArgs(args) {
|
|
508
|
+
const fenArg = typeof args.fen === "string" ? args.fen.trim() : "";
|
|
509
|
+
if (fenArg)
|
|
510
|
+
return fenArg;
|
|
511
|
+
const lineArg = typeof args.line === "string" ? args.line.trim() : "";
|
|
512
|
+
const board = new Chess();
|
|
513
|
+
if (lineArg) {
|
|
514
|
+
for (const raw of lineArg.split(/\s+/)) {
|
|
515
|
+
const san = raw.replace(/^\d+\.+/, "");
|
|
516
|
+
if (!san)
|
|
517
|
+
continue;
|
|
518
|
+
try {
|
|
519
|
+
board.move(san);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
throw new Error(`bad SAN token '${raw}' in line`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return board.fen();
|
|
527
|
+
}
|
|
407
528
|
function stringifyForLog(v) {
|
|
408
529
|
let s;
|
|
409
530
|
try {
|
|
@@ -453,21 +574,27 @@ async function callToolInner(name, args) {
|
|
|
453
574
|
params.limit = args.limit;
|
|
454
575
|
if (typeof args.offset === "number")
|
|
455
576
|
params.offset = args.offset;
|
|
456
|
-
|
|
577
|
+
const raw = await get("/api/chess/prep/by-player", params);
|
|
578
|
+
return convertAvailableMovesToSAN(raw, resolveFenFromArgs(args));
|
|
457
579
|
}
|
|
458
|
-
case "get_position_stats":
|
|
459
|
-
|
|
460
|
-
|
|
580
|
+
case "get_position_stats": {
|
|
581
|
+
const fen = String(args.fen);
|
|
582
|
+
const raw = await get("/api/chess/database/main", {
|
|
583
|
+
fen,
|
|
461
584
|
limit: typeof args.limit === "number" ? args.limit : 20,
|
|
462
585
|
sort: "relevance",
|
|
463
586
|
});
|
|
587
|
+
return convertAvailableMovesToSAN(raw, fen);
|
|
588
|
+
}
|
|
464
589
|
case "analyse": {
|
|
465
|
-
const
|
|
590
|
+
const fen = String(args.fen);
|
|
591
|
+
const params = { fen };
|
|
466
592
|
if (typeof args.movetime_ms === "number")
|
|
467
593
|
params.movetime_ms = args.movetime_ms;
|
|
468
594
|
if (typeof args.multipv === "number")
|
|
469
595
|
params.multipv = args.multipv;
|
|
470
|
-
|
|
596
|
+
const raw = await get("/api/chess/database/analyse", params);
|
|
597
|
+
return convertAnalyseResponse(raw, fen);
|
|
471
598
|
}
|
|
472
599
|
case "get_head_to_head":
|
|
473
600
|
return get("/api/chess/players/h2h", {
|
|
@@ -494,14 +621,16 @@ async function callToolInner(name, args) {
|
|
|
494
621
|
case "stop_cloud_engine":
|
|
495
622
|
return authedRequest("DELETE", `/api/agent/cloud-engines/${encodeURIComponent(String(args.contract_id))}`);
|
|
496
623
|
case "cloud_analyse": {
|
|
497
|
-
const
|
|
624
|
+
const fen = String(args.fen);
|
|
625
|
+
const body = { fen };
|
|
498
626
|
if (typeof args.movetime_ms === "number")
|
|
499
627
|
body.movetime_ms = args.movetime_ms;
|
|
500
628
|
if (typeof args.multipv === "number")
|
|
501
629
|
body.multipv = args.multipv;
|
|
502
630
|
if (typeof args.contempt === "number")
|
|
503
631
|
body.contempt = args.contempt;
|
|
504
|
-
|
|
632
|
+
const raw = await authedRequest("POST", "/api/agent/cloud-engines/analyse", body);
|
|
633
|
+
return convertCloudSnapshotResponse(raw, fen);
|
|
505
634
|
}
|
|
506
635
|
case "read_engine_usage_guide":
|
|
507
636
|
return { guide: ENGINE_USAGE_DOC };
|
|
@@ -549,9 +678,9 @@ async function callToolInner(name, args) {
|
|
|
549
678
|
]);
|
|
550
679
|
return {
|
|
551
680
|
position: { line, fen, my_color: myColor },
|
|
552
|
-
opponent,
|
|
553
|
-
you,
|
|
554
|
-
general,
|
|
681
|
+
opponent: convertAvailableMovesToSAN(opponent, fen),
|
|
682
|
+
you: convertAvailableMovesToSAN(you, fen),
|
|
683
|
+
general: convertAvailableMovesToSAN(general, fen),
|
|
555
684
|
};
|
|
556
685
|
}
|
|
557
686
|
default:
|
package/docs/engine-usage.md
CHANGED
|
@@ -75,6 +75,19 @@ The eval Lc0 returns when contempt is set is **not** the objective eval — it's
|
|
|
75
75
|
- **Playing for a win with the "worse" side** — negative contempt if the user needs to avoid drawing lines with Black in a must-win situation.
|
|
76
76
|
- **Never with zero direction** — always know *why* you're skewing before you set contempt. It's a specific tool for a specific question, not a default knob.
|
|
77
77
|
|
|
78
|
+
## Tricks worth knowing
|
|
79
|
+
|
|
80
|
+
### Flip side-to-move to see threats
|
|
81
|
+
|
|
82
|
+
If it's Black to move and you want to know what threats *White* has (i.e. "if Black passes, what does White do?"), take the FEN and flip the side-to-move field from `b` to `w` (or vice versa), then run analysis. This is the engine equivalent of the "null move" idea humans use to check for prophylaxis.
|
|
83
|
+
|
|
84
|
+
- Real position: `... b KQkq -` — analyse says +0.2 for Black
|
|
85
|
+
- Flipped: `... w KQkq -` — analyse says +1.4 for White
|
|
86
|
+
|
|
87
|
+
The gap (1.4 - 0.2 = 1.2) is roughly the value of the tempo Black has to spend defending. If it's huge, Black is under real pressure even if the current eval looks calm. If it's tiny, White has no immediate threats and Black can improve at leisure.
|
|
88
|
+
|
|
89
|
+
**Gotcha:** flipping side-to-move invalidates the en passant target field (if the last move was a two-square pawn push, the ep square is now stale). Also, both sides are counted as still having whichever castling rights are in the FEN — don't do this in the middle of a castling sequence. For opening / middlegame threat-checking it's a very useful idiom.
|
|
90
|
+
|
|
78
91
|
## Worked example
|
|
79
92
|
|
|
80
93
|
User is preparing Black against a 2600 opponent who plays 1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Be3 e5. You want to know if 7.Nb3 or 7.Nf3 is more testing.
|
package/package.json
CHANGED