@chessceo/mcp 0.33.1 → 0.34.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 +201 -0
- package/docs/engine-usage.md +25 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,6 +89,9 @@ const AUTHED_TOOLS = new Set([
|
|
|
89
89
|
"auto_evaluate",
|
|
90
90
|
"auto_evaluate_status",
|
|
91
91
|
"auto_evaluate_cancel",
|
|
92
|
+
"deep_analyse",
|
|
93
|
+
"deep_analyse_status",
|
|
94
|
+
"deep_analyse_cancel",
|
|
92
95
|
"quote_engine_eval",
|
|
93
96
|
"predict_human_move",
|
|
94
97
|
"prepare_opponent",
|
|
@@ -508,6 +511,11 @@ const TOOLS = [
|
|
|
508
511
|
maximum: 100,
|
|
509
512
|
description: "Lc0 contempt bias. Signed 0-100 strength (same scale as the web UI's ContemptStrength slider — server multiplies by 8 to get the internal cp bias). 0 = objective (default). Positive favours White, negative favours Black. Typical: ±15 light nudge, ±30-60 real fighting play, ±80-100 maximum steer. Not applied to Stockfish. See engine_usage_primer for when to use.",
|
|
510
513
|
},
|
|
514
|
+
engines: {
|
|
515
|
+
type: "array",
|
|
516
|
+
items: { type: "string", enum: ["stockfish", "lc0"] },
|
|
517
|
+
description: "Which engines to run. Default = both. Use `[\"lc0\"]` to skip Stockfish (e.g. while a deep_analyse job is holding the SF slot on the same combo). Use `[\"stockfish\"]` when only the objective read matters. The skipped engine's field is omitted from the response.",
|
|
518
|
+
},
|
|
511
519
|
},
|
|
512
520
|
},
|
|
513
521
|
},
|
|
@@ -763,6 +771,56 @@ const TOOLS = [
|
|
|
763
771
|
required: ["job_id"],
|
|
764
772
|
},
|
|
765
773
|
},
|
|
774
|
+
{
|
|
775
|
+
name: "deep_analyse",
|
|
776
|
+
description: "Start a long Stockfish think on a single position (up to 5 min movetime). Returns a `job_id` immediately; poll `deep_analyse_status(job_id)` for the result, cancel with `deep_analyse_cancel(job_id)`. Runs SF only — Lc0 doesn't benefit from long thinks past a handful of seconds — and **holds only the SF engine slot on the combo, so `cloud_analyse(..., engines: [\"lc0\"])` stays available for other work in parallel**.\n\n" +
|
|
777
|
+
"Use this when a specific critical position deserves depth — a novelty candidate, a hairy tactical shot, a difficult endgame — and you want Stockfish at depth 35+ rather than the ~depth 22 you get from a 2s cloud_analyse. Movetime is in ms; typical: 30_000-60_000 for 'careful check', 120_000-300_000 for 'find the truth'.\n\n" +
|
|
778
|
+
"Result shape when done matches cloud_analyse's Stockfish leg (depth, top-N candidates with scoreCp/mate, best move, PV). Auto-stores the eval on `file_id`+`node_id` when both are supplied, same as cloud_analyse.",
|
|
779
|
+
inputSchema: {
|
|
780
|
+
type: "object",
|
|
781
|
+
properties: {
|
|
782
|
+
file_id: { type: "string", description: "Prep file id. Combine with `node_id` to derive FEN from the tree AND persist the result on the node's ceoEval." },
|
|
783
|
+
node_id: { type: "string", description: "Node id inside `file_id`. Root is 'r'. When set, overrides `fen`/`moves`." },
|
|
784
|
+
fen: { type: "string", description: "Position as FEN. Only used if `node_id` is not set." },
|
|
785
|
+
moves: { type: "string", description: "Optional SAN moves on top of `fen`. Only used if `node_id` is not set." },
|
|
786
|
+
movetime_ms: {
|
|
787
|
+
type: "integer",
|
|
788
|
+
minimum: 5_000,
|
|
789
|
+
maximum: 300_000,
|
|
790
|
+
description: "Think time in ms. Default 60_000 (1 min). Max 300_000 (5 min).",
|
|
791
|
+
},
|
|
792
|
+
multipv: {
|
|
793
|
+
type: "integer",
|
|
794
|
+
minimum: 1,
|
|
795
|
+
maximum: 10,
|
|
796
|
+
description: "Number of candidate lines (default 3).",
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
name: "deep_analyse_status",
|
|
803
|
+
description: "Poll a deep_analyse job. Response: `{ status: 'running' | 'done' | 'cancelled' | 'error' | 'not_found', elapsed_ms, movetime_ms, result?, error? }`. `result` shape when done: `{ engine, depth, timeMs, bestMove, lines: [{rank, depth, scoreCp?, mate?, pv, nodes?}] }` — the SF leg of a cloud_analyse response.\n\n" +
|
|
804
|
+
"Poll cadence: every ~15-30s for long thinks; there's no penalty for polling more often but the engine progresses at its own pace.",
|
|
805
|
+
inputSchema: {
|
|
806
|
+
type: "object",
|
|
807
|
+
properties: {
|
|
808
|
+
job_id: { type: "string", description: "Job id from `deep_analyse`." },
|
|
809
|
+
},
|
|
810
|
+
required: ["job_id"],
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "deep_analyse_cancel",
|
|
815
|
+
description: "Ask a running deep_analyse job to stop early. The engine returns whatever it's found so far as the final result. Useful when a partial result at depth 25 is enough and you don't want to wait for depth 40. Idempotent for already-finished jobs.",
|
|
816
|
+
inputSchema: {
|
|
817
|
+
type: "object",
|
|
818
|
+
properties: {
|
|
819
|
+
job_id: { type: "string", description: "Job id from `deep_analyse`." },
|
|
820
|
+
},
|
|
821
|
+
required: ["job_id"],
|
|
822
|
+
},
|
|
823
|
+
},
|
|
766
824
|
{
|
|
767
825
|
name: "quote_engine_eval",
|
|
768
826
|
description: "Return the stored engine eval for a node, or null if that node was never analysed. **Call this before writing prose or NAGs that quote engine numbers** — if it returns null, you have no measurement to cite. Do NOT infer an eval for the node from siblings or children; either analyse it (cloud_analyse with node_id) or omit the number from your prose.\n\n" +
|
|
@@ -1230,6 +1288,141 @@ function autoEvaluateCancel(args) {
|
|
|
1230
1288
|
// inside runEvalJob, after the final flush persists progress.
|
|
1231
1289
|
return { status: "cancelling", evaluated_so_far: job.evaluated };
|
|
1232
1290
|
}
|
|
1291
|
+
const deepJobs = new Map();
|
|
1292
|
+
const DEEP_JOB_TTL_MS = 15 * 60 * 1000;
|
|
1293
|
+
function newDeepJobId() {
|
|
1294
|
+
const rand = Math.random().toString(16).slice(2, 8);
|
|
1295
|
+
return `deep_${Date.now().toString(16)}${rand}`;
|
|
1296
|
+
}
|
|
1297
|
+
function reapExpiredDeepJobs() {
|
|
1298
|
+
const now = Date.now();
|
|
1299
|
+
for (const [k, j] of deepJobs) {
|
|
1300
|
+
if (j.finishedAt && now - j.finishedAt > DEEP_JOB_TTL_MS) {
|
|
1301
|
+
deepJobs.delete(k);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function deepAnalyseStart(args) {
|
|
1306
|
+
reapExpiredDeepJobs();
|
|
1307
|
+
const resolved = await resolveFromNodeOrFen(args);
|
|
1308
|
+
const fen = resolved.fen;
|
|
1309
|
+
const movetimeMs = typeof args.movetime_ms === "number" ? args.movetime_ms : 60_000;
|
|
1310
|
+
const multipv = typeof args.multipv === "number" ? args.multipv : 3;
|
|
1311
|
+
const jobId = newDeepJobId();
|
|
1312
|
+
const job = {
|
|
1313
|
+
id: jobId,
|
|
1314
|
+
status: "running",
|
|
1315
|
+
fileHandle: resolved.file,
|
|
1316
|
+
fen,
|
|
1317
|
+
movetimeMs,
|
|
1318
|
+
multipv,
|
|
1319
|
+
startedAt: Date.now(),
|
|
1320
|
+
cancelController: new AbortController(),
|
|
1321
|
+
};
|
|
1322
|
+
deepJobs.set(jobId, job);
|
|
1323
|
+
// Kick off the long HTTP call unawaited — resolves when the backend
|
|
1324
|
+
// returns the SF snapshot. authedRequest is a plain fetch under the
|
|
1325
|
+
// hood; abort signal flows via cancelController.
|
|
1326
|
+
void runDeepJob(job).catch(err => {
|
|
1327
|
+
job.status = "error";
|
|
1328
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
1329
|
+
job.finishedAt = Date.now();
|
|
1330
|
+
});
|
|
1331
|
+
return {
|
|
1332
|
+
job_id: jobId,
|
|
1333
|
+
status: "running",
|
|
1334
|
+
movetime_ms: movetimeMs,
|
|
1335
|
+
fen,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
async function runDeepJob(job) {
|
|
1339
|
+
const body = {
|
|
1340
|
+
fen: job.fen,
|
|
1341
|
+
movetime_ms: job.movetimeMs,
|
|
1342
|
+
multipv: job.multipv,
|
|
1343
|
+
engines: ["stockfish"],
|
|
1344
|
+
};
|
|
1345
|
+
let raw;
|
|
1346
|
+
try {
|
|
1347
|
+
// TODO(future): plumb an AbortSignal through authedRequest for
|
|
1348
|
+
// real mid-flight cancellation. For now, cancel just marks the
|
|
1349
|
+
// job so the caller stops polling; the backend still runs the
|
|
1350
|
+
// engine to completion and the result is stored on the job
|
|
1351
|
+
// record but flagged cancelled.
|
|
1352
|
+
raw = await authedRequest("POST", "/api/agent/cloud-engines/analyse", body);
|
|
1353
|
+
}
|
|
1354
|
+
catch (err) {
|
|
1355
|
+
job.status = "error";
|
|
1356
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
1357
|
+
job.finishedAt = Date.now();
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const converted = convertCloudSnapshotResponse(raw, job.fen);
|
|
1361
|
+
const sf = converted.stockfish;
|
|
1362
|
+
if (job.cancelController.signal.aborted) {
|
|
1363
|
+
job.status = "cancelled";
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
job.status = "done";
|
|
1367
|
+
}
|
|
1368
|
+
job.result = sf ?? null;
|
|
1369
|
+
job.finishedAt = Date.now();
|
|
1370
|
+
// Same node-persistence as cloud_analyse: if the caller anchored on
|
|
1371
|
+
// file_id+node_id, store the SF-only eval as the node's ceoEval so
|
|
1372
|
+
// quote_engine_eval can cite it later. We build a StoredEval that has
|
|
1373
|
+
// only the sf leg — no Lc0 was run.
|
|
1374
|
+
if (job.fileHandle && sf) {
|
|
1375
|
+
const ev = analysisToStoredEval({ stockfish: sf }, job.fen);
|
|
1376
|
+
if (ev) {
|
|
1377
|
+
try {
|
|
1378
|
+
await storeEvalOnNode(job.fileHandle, ev);
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
// best-effort — the analysis result is what the LLM asked for
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function deepAnalyseStatus(args) {
|
|
1387
|
+
reapExpiredDeepJobs();
|
|
1388
|
+
const jobId = String(args.job_id || "").trim();
|
|
1389
|
+
if (!jobId)
|
|
1390
|
+
throw new Error("`job_id` is required");
|
|
1391
|
+
const job = deepJobs.get(jobId);
|
|
1392
|
+
if (!job) {
|
|
1393
|
+
return {
|
|
1394
|
+
status: "not_found",
|
|
1395
|
+
note: "Job unknown — expired (kept ~15 min after completion), never existed, or the MCP process restarted.",
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
job_id: job.id,
|
|
1400
|
+
status: job.status,
|
|
1401
|
+
movetime_ms: job.movetimeMs,
|
|
1402
|
+
elapsed_ms: (job.finishedAt ?? Date.now()) - job.startedAt,
|
|
1403
|
+
fen: job.fen,
|
|
1404
|
+
result: job.result,
|
|
1405
|
+
error: job.error,
|
|
1406
|
+
started_at_ms: job.startedAt,
|
|
1407
|
+
finished_at_ms: job.finishedAt,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function deepAnalyseCancel(args) {
|
|
1411
|
+
const jobId = String(args.job_id || "").trim();
|
|
1412
|
+
if (!jobId)
|
|
1413
|
+
throw new Error("`job_id` is required");
|
|
1414
|
+
const job = deepJobs.get(jobId);
|
|
1415
|
+
if (!job)
|
|
1416
|
+
return { status: "not_found" };
|
|
1417
|
+
if (job.status !== "running") {
|
|
1418
|
+
return { status: job.status, note: "Job already finished; nothing to cancel." };
|
|
1419
|
+
}
|
|
1420
|
+
job.cancelController.abort();
|
|
1421
|
+
// Status flips to "cancelled" when runDeepJob observes the abort on
|
|
1422
|
+
// completion. Backend keeps churning until movetime elapses (mid-
|
|
1423
|
+
// flight abort of the HTTP call is a follow-up).
|
|
1424
|
+
return { status: "cancelling", elapsed_ms: Date.now() - job.startedAt };
|
|
1425
|
+
}
|
|
1233
1426
|
// Walk chess.js-free: resolve a path against a tree, throw if invalid.
|
|
1234
1427
|
function pathIntoTree(root, path) {
|
|
1235
1428
|
let cur = root;
|
|
@@ -1756,6 +1949,8 @@ async function callToolInner(name, args) {
|
|
|
1756
1949
|
body.multipv = args.multipv;
|
|
1757
1950
|
if (typeof args.contempt === "number")
|
|
1758
1951
|
body.contempt = args.contempt;
|
|
1952
|
+
if (Array.isArray(args.engines))
|
|
1953
|
+
body.engines = args.engines;
|
|
1759
1954
|
const raw = await authedRequest("POST", "/api/agent/cloud-engines/analyse", body);
|
|
1760
1955
|
const converted = convertCloudSnapshotResponse(raw, fen);
|
|
1761
1956
|
// Node-addressed calls: persist the result on the node's ceoEval
|
|
@@ -1771,6 +1966,12 @@ async function callToolInner(name, args) {
|
|
|
1771
1966
|
}
|
|
1772
1967
|
return converted;
|
|
1773
1968
|
}
|
|
1969
|
+
case "deep_analyse":
|
|
1970
|
+
return deepAnalyseStart(args);
|
|
1971
|
+
case "deep_analyse_status":
|
|
1972
|
+
return deepAnalyseStatus(args);
|
|
1973
|
+
case "deep_analyse_cancel":
|
|
1974
|
+
return deepAnalyseCancel(args);
|
|
1774
1975
|
case "list_prep_files":
|
|
1775
1976
|
return authedRequest("GET", "/api/agent/prep-files");
|
|
1776
1977
|
case "search_prep_files":
|
package/docs/engine-usage.md
CHANGED
|
@@ -103,6 +103,31 @@ The gap (1.4 - 0.2 = 1.2) is roughly the value of the tempo Black has to spend d
|
|
|
103
103
|
|
|
104
104
|
**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.
|
|
105
105
|
|
|
106
|
+
## Deep Stockfish thinks: `deep_analyse`
|
|
107
|
+
|
|
108
|
+
`cloud_analyse` runs both engines with a movetime cap of 10 s — deliberately fast because most opening-tree questions are answered in 2–3 s. For **one specific critical position** where you want depth 35+ instead of the usual depth 22, use `deep_analyse`:
|
|
109
|
+
|
|
110
|
+
- SF-only (Lc0 saturates in a handful of seconds — no benefit past ~5 s of movetime).
|
|
111
|
+
- Movetime up to 5 min.
|
|
112
|
+
- Async: returns a `job_id` immediately, poll `deep_analyse_status(job_id)`, cancel with `deep_analyse_cancel(job_id)` if you decide partial depth is enough.
|
|
113
|
+
- **Holds only the Stockfish slot on the combo.** Lc0 remains callable for other positions via `cloud_analyse({ engines: ["lc0"], … })` while the deep think runs. Use that during the wait — walk other branches, sanity-check candidates.
|
|
114
|
+
|
|
115
|
+
Typical movetimes:
|
|
116
|
+
- `30_000 – 60_000` (30–60 s) — careful check on a candidate move
|
|
117
|
+
- `120_000 – 300_000` (2–5 min) — "find the truth" on a novelty, tactical shot, or difficult endgame
|
|
118
|
+
|
|
119
|
+
Only use it when you actually need the depth. Regular `cloud_analyse` handles the ~5–10 branches of a typical prep walk perfectly well.
|
|
120
|
+
|
|
121
|
+
## Splitting engines on `cloud_analyse`
|
|
122
|
+
|
|
123
|
+
`cloud_analyse` accepts an `engines` list to run only one leg:
|
|
124
|
+
|
|
125
|
+
- `engines: ["lc0"]` — Lc0 only, useful while a `deep_analyse` is holding the Stockfish slot.
|
|
126
|
+
- `engines: ["stockfish"]` — SF only, when only the objective read matters and you want to skip the Lc0 latency.
|
|
127
|
+
- Default (omitted) — both engines. This is what you want for real prep decisions.
|
|
128
|
+
|
|
129
|
+
The skipped engine's field is omitted from the response (not present as an empty object).
|
|
130
|
+
|
|
106
131
|
## Worked example
|
|
107
132
|
|
|
108
133
|
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