@chessceo/mcp 0.35.1 → 0.36.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/dist/index.js +27 -23
- package/docs/engine-usage.md +13 -0
- package/docs/pgn-authoring.md +8 -0
- package/docs/prep-strategy.md +34 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -501,11 +501,17 @@ const TOOLS = [
|
|
|
501
501
|
maximum: 10000,
|
|
502
502
|
description: "Think time in milliseconds (default 2000).",
|
|
503
503
|
},
|
|
504
|
-
|
|
504
|
+
stockfish_multipv: {
|
|
505
|
+
type: "integer",
|
|
506
|
+
minimum: 1,
|
|
507
|
+
maximum: 10,
|
|
508
|
+
description: "Stockfish candidate lines (default 2). Kept tight because each extra PV steals search bandwidth from the top choice — SF is the 'what's objectively best' leg, use a low multipv to keep it strong. Raise only when you specifically need SF's take on a wide range of candidates.",
|
|
509
|
+
},
|
|
510
|
+
lc0_multipv: {
|
|
505
511
|
type: "integer",
|
|
506
512
|
minimum: 1,
|
|
507
513
|
maximum: 10,
|
|
508
|
-
description: "
|
|
514
|
+
description: "Lc0 candidate lines (default 8). Kept wide because multipv doesn't degrade Lc0's strength the way it does Stockfish's — Lc0 is the 'find inspiration / explore practical tries' leg, use a high multipv to get a full slate of ideas.",
|
|
509
515
|
},
|
|
510
516
|
contempt: {
|
|
511
517
|
type: "integer",
|
|
@@ -795,7 +801,7 @@ const TOOLS = [
|
|
|
795
801
|
type: "integer",
|
|
796
802
|
minimum: 1,
|
|
797
803
|
maximum: 10,
|
|
798
|
-
description: "Number of candidate lines (default
|
|
804
|
+
description: "Number of candidate lines (default 2). Stockfish gets weaker as multipv grows — each extra PV steals search bandwidth from the top choice — so keep this low unless you specifically want to see several candidates ranked deep.",
|
|
799
805
|
},
|
|
800
806
|
},
|
|
801
807
|
},
|
|
@@ -981,7 +987,7 @@ async function fetchCompactEval(fen) {
|
|
|
981
987
|
try {
|
|
982
988
|
const raw = await authedRequest("POST", "/api/agent/cloud-engines/analyse", { fen, movetime_ms: 1500, multipv: 1 });
|
|
983
989
|
const converted = convertCloudSnapshotResponse(raw, fen);
|
|
984
|
-
const stored = analysisToStoredEval(converted
|
|
990
|
+
const stored = analysisToStoredEval(converted);
|
|
985
991
|
return storedEvalToCompact(stored, converted);
|
|
986
992
|
}
|
|
987
993
|
catch {
|
|
@@ -1234,7 +1240,7 @@ async function runEvalJob(job, fileId, targets, movetimeMs) {
|
|
|
1234
1240
|
break;
|
|
1235
1241
|
try {
|
|
1236
1242
|
const analysis = await authedRequest("POST", "/api/agent/cloud-engines/analyse", { fen: t.fen, movetime_ms: movetimeMs, multipv: 1 });
|
|
1237
|
-
const ev = analysisToStoredEval(analysis
|
|
1243
|
+
const ev = analysisToStoredEval(analysis);
|
|
1238
1244
|
if (ev) {
|
|
1239
1245
|
pending.push({ op: "set_ceo_eval", node_id: t.nodeId, ceoEval: ev });
|
|
1240
1246
|
job.evaluated++;
|
|
@@ -1333,7 +1339,10 @@ async function deepAnalyseStart(args) {
|
|
|
1333
1339
|
const resolved = await resolveFromNodeOrFen(args);
|
|
1334
1340
|
const fen = resolved.fen;
|
|
1335
1341
|
const movetimeMs = typeof args.movetime_ms === "number" ? args.movetime_ms : 60_000;
|
|
1336
|
-
|
|
1342
|
+
// Default 2 — SF loses meaningful strength at higher multipv, so a
|
|
1343
|
+
// deep think is best spent on a tight candidate list. Matches the
|
|
1344
|
+
// cloud_analyse stockfish_multipv default.
|
|
1345
|
+
const multipv = typeof args.multipv === "number" ? args.multipv : 2;
|
|
1337
1346
|
const jobId = newDeepJobId();
|
|
1338
1347
|
const job = {
|
|
1339
1348
|
id: jobId,
|
|
@@ -1365,7 +1374,7 @@ async function runDeepJob(job) {
|
|
|
1365
1374
|
const body = {
|
|
1366
1375
|
fen: job.fen,
|
|
1367
1376
|
movetime_ms: job.movetimeMs,
|
|
1368
|
-
|
|
1377
|
+
stockfish_multipv: job.multipv,
|
|
1369
1378
|
engines: ["stockfish"],
|
|
1370
1379
|
};
|
|
1371
1380
|
let raw;
|
|
@@ -1398,7 +1407,7 @@ async function runDeepJob(job) {
|
|
|
1398
1407
|
// quote_engine_eval can cite it later. We build a StoredEval that has
|
|
1399
1408
|
// only the sf leg — no Lc0 was run.
|
|
1400
1409
|
if (job.fileHandle && sf) {
|
|
1401
|
-
const ev = analysisToStoredEval({ stockfish: sf }
|
|
1410
|
+
const ev = analysisToStoredEval({ stockfish: sf });
|
|
1402
1411
|
if (ev) {
|
|
1403
1412
|
try {
|
|
1404
1413
|
await storeEvalOnNode(job.fileHandle, ev);
|
|
@@ -1545,21 +1554,14 @@ function pathIntoTree(root, path) {
|
|
|
1545
1554
|
// no PVs, White-POV cp / mate, depth, and derived NAG). Returns null if
|
|
1546
1555
|
// there's no usable Stockfish signal (SF is the source of truth for
|
|
1547
1556
|
// the NAG per docs).
|
|
1548
|
-
function analysisToStoredEval(analysis
|
|
1557
|
+
function analysisToStoredEval(analysis) {
|
|
1549
1558
|
if (!analysis || typeof analysis !== "object")
|
|
1550
1559
|
return null;
|
|
1551
1560
|
const r = analysis;
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
// Black-to-move position's stored eval, so half of every prep tree
|
|
1557
|
-
// had wrong-sign ceoEval and the auto-attached .eval on those
|
|
1558
|
-
// positions reported White as better when Black was. Any ceoEval
|
|
1559
|
-
// written on a Black-to-move node prior to 2026-07-26 is inverted;
|
|
1560
|
-
// re-run auto_evaluate on affected files to overwrite.
|
|
1561
|
-
// Unused `fen` kept as an arg for signature stability with callers.
|
|
1562
|
-
void fen;
|
|
1561
|
+
// Backend returns White-POV cp/mate (engine-ws flips in ParseInfo
|
|
1562
|
+
// based on side-to-move, cloud_snapshot passes through). Pure
|
|
1563
|
+
// pass-through here — a previous sign-flip on black-to-move was
|
|
1564
|
+
// wrong and silently inverted every Black-to-move stored eval.
|
|
1563
1565
|
const engineEval = (block) => {
|
|
1564
1566
|
const line = block?.lines?.[0];
|
|
1565
1567
|
if (!line)
|
|
@@ -2061,8 +2063,10 @@ async function callToolInner(name, args) {
|
|
|
2061
2063
|
const body = { fen };
|
|
2062
2064
|
if (typeof args.movetime_ms === "number")
|
|
2063
2065
|
body.movetime_ms = args.movetime_ms;
|
|
2064
|
-
if (typeof args.
|
|
2065
|
-
body.
|
|
2066
|
+
if (typeof args.stockfish_multipv === "number")
|
|
2067
|
+
body.stockfish_multipv = args.stockfish_multipv;
|
|
2068
|
+
if (typeof args.lc0_multipv === "number")
|
|
2069
|
+
body.lc0_multipv = args.lc0_multipv;
|
|
2066
2070
|
if (typeof args.contempt === "number")
|
|
2067
2071
|
body.contempt = args.contempt;
|
|
2068
2072
|
if (Array.isArray(args.engines))
|
|
@@ -2076,7 +2080,7 @@ async function callToolInner(name, args) {
|
|
|
2076
2080
|
// node_id=Y, because the store only fires when file_id+node_id
|
|
2077
2081
|
// was supplied and the eval survives via the [%ceo-eval] escape.
|
|
2078
2082
|
if (resolved.file) {
|
|
2079
|
-
const ev = analysisToStoredEval(converted
|
|
2083
|
+
const ev = analysisToStoredEval(converted);
|
|
2080
2084
|
if (ev)
|
|
2081
2085
|
await storeEvalOnNode(resolved.file, ev);
|
|
2082
2086
|
}
|
package/docs/engine-usage.md
CHANGED
|
@@ -118,6 +118,19 @@ Typical movetimes:
|
|
|
118
118
|
|
|
119
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
120
|
|
|
121
|
+
## Per-engine `multipv` defaults
|
|
122
|
+
|
|
123
|
+
`cloud_analyse` defaults to different candidate-line counts per engine because the two engines behave differently:
|
|
124
|
+
|
|
125
|
+
- **Stockfish: `stockfish_multipv=2`** — SF gets weaker as multipv grows. Each extra PV steals search bandwidth from the top choice, so the "objective best move" leg is at its strongest with a tight list. Raise only when you specifically need SF's read on a wide range of candidates (e.g. sanity-checking an unusual sideline).
|
|
126
|
+
- **Lc0: `lc0_multipv=8`** — Lc0 doesn't degrade the same way with multipv. Use it wide by default: 8 candidates give the LLM a real slate of practical ideas to inspect ("what does Lc0 think of the fun moves here?"). Lower it only when you don't need that breadth.
|
|
127
|
+
|
|
128
|
+
Mental model:
|
|
129
|
+
- **Stockfish = the checker.** "Is this line objectively good? What's actually best?" Narrow, deep, one answer.
|
|
130
|
+
- **Lc0 = the explorer.** "What's practically interesting? What alternatives are worth a look?" Wide, breadth-first, several answers.
|
|
131
|
+
|
|
132
|
+
Override the defaults with `stockfish_multipv` / `lc0_multipv` when a specific position warrants a different shape.
|
|
133
|
+
|
|
121
134
|
## Splitting engines on `cloud_analyse`
|
|
122
135
|
|
|
123
136
|
`cloud_analyse` accepts an `engines` list to run only one leg:
|
package/docs/pgn-authoring.md
CHANGED
|
@@ -154,6 +154,14 @@ Prose is NEVER for:
|
|
|
154
154
|
- Move recommendations ("here White should play h4") — add_move it.
|
|
155
155
|
- Restating the eval a NAG already conveys.
|
|
156
156
|
- **Describing a sibling variation you already added as a branch.** If move A has variation B added as `add_move(A, sibling)`, don't ALSO write `{if B then ...}` on A. The reader clicks B on the board — the branch is already there.
|
|
157
|
+
- **Restating what the app already renders.** The reader opens the file in the app and sees: every sibling move (with count / avg rating / top players from the DB), each node's stored `ceoEval`, the NAG glyphs, the tree structure. Duplicating any of that in prose is pure noise.
|
|
158
|
+
|
|
159
|
+
- ❌ `{5...Bb4 (180/247), 5...d6 (26), 5...Be7 (18), 5...d5 (7), 5...g6 (2)}` — those are the sibling variations, already visible.
|
|
160
|
+
- ❌ `{Main move, 180 of 247 games (So, Giri, Karjakin, Duda). Eval −0.20/−0.27.}` — count, top players, eval all shown by the app.
|
|
161
|
+
- ✅ `{Main move; the sharpest test is actually 5...d5 (only 7 games but avg 2526) — see the variation.}`
|
|
162
|
+
- ✅ `{Popular, but leads to the endgame Black draws — 6.Bd2 is where prep depth matters.}`
|
|
163
|
+
|
|
164
|
+
Test: if the reader can see it by looking at the position or clicking a branch, don't write it. Prose is only for plans, prep-signal, or WHY — the layer the app can't derive.
|
|
157
165
|
|
|
158
166
|
## Move-judgment symbols (NAGs)
|
|
159
167
|
|
package/docs/prep-strategy.md
CHANGED
|
@@ -26,6 +26,7 @@ The move statistics endpoints return win %, game counts, and (in the big DB) a `
|
|
|
26
26
|
- **Sample size scales trust.** 3 games at 66% is noise; 300 at 55% is signal. A great score is nice — with volume. When the opponent has only 2-4 games in a variation, the "opponent-specific" score is basically the general-DB score anyway.
|
|
27
27
|
- **Score doesn't automatically indicate a level gap.** A 60% variation isn't necessarily "stronger players crushing weaker ones." Look at the per-move `avgWhite` / `avgBlack` fields (returned on every move statistic) before drawing conclusions about who is playing whom.
|
|
28
28
|
- **Don't recommend the higher-percentage move just because it's higher.** If 1.b3 scores 60% and 1.d4 scores 50% against a given opponent, that is *not* on its own a case for playing 1.b3 — style, prep depth, transposition risk, and the practical questions below all matter more.
|
|
29
|
+
- **Count is popular, avgRating is critical.** When you're choosing which branches deserve prep depth, the highest-avgRating move often matters more than the highest-count one. Top players play the moves they know work, sometimes ignoring the herd. Example: 5...Bb4 has 180 games at avg 2350; 5...d5 has 7 games at avg 2526. d5 is the sharper test even though Bb4 is "main". Skim the rating column before the count column when picking branches to cover.
|
|
29
30
|
|
|
30
31
|
## Which DB source: signal vs population
|
|
31
32
|
|
|
@@ -141,6 +142,39 @@ Meta-signal matters. Big changes in the user's repertoire are transparent to str
|
|
|
141
142
|
- **Right:** stay inside the user's normal repertoire, pick a rare secondary line. Classic example: a 1.e4 player who always plays 6.Bg5 vs the Najdorf switching to 6.Bc4 for one game. The opponent recognizes the opening; they don't have prep on this specific line; the user's meta-signature ("I play 1.e4 vs Najdorf") stays intact.
|
|
142
143
|
- Same logic for candidate move choice: the opponent's expectation of *what the user plays* is itself a variable to be manipulated.
|
|
143
144
|
|
|
145
|
+
## Practical, not correspondence
|
|
146
|
+
|
|
147
|
+
The reader is a human at the board, not a computer consuming a PGN.
|
|
148
|
+
|
|
149
|
+
- **Don't paste long engine PVs into prose.** A 15-move variation in a comment is unreadable and unmemorable. Cite the eval, name the key move or two, stop. If the line matters as a variation, add it as a branch with `add_line` — the reader walks it interactively there — not as prose.
|
|
150
|
+
- **Compress for human memory.** If the opponent has 6 plausible replies that all share one strategic theme, describe the theme once; add the two structurally different branches, not all 6. Long non-forcing lines are unmemorable regardless of eval; recommend depth only when the line is actually forced or the reader has a clear reason to remember it.
|
|
151
|
+
|
|
152
|
+
## When the database runs thin, judge the position
|
|
153
|
+
|
|
154
|
+
Sparse data (few games, low ratings, or literally zero) means no "what does the field play here" signal. The LLM has to actually judge the position — and it has two tools that stand in for the missing signal.
|
|
155
|
+
|
|
156
|
+
- **Call `describe_position` before commenting on a position without concrete game data.** You cannot reliably read a FEN — pieces get swapped, hanging pieces missed, "the knight on d5" turns out to not exist. `describe_position` gives you the same board a human sees: piece placements, contested pieces with attackers/defenders, hanging list, check state, legal moves. Cheap (~1 ms, no engine). Mandatory when the DB is thin, since you no longer have "what won here historically" to anchor the reasoning.
|
|
157
|
+
- **Call `predict_human_move` when past theory.** Once out of book, humans play *human* moves — natural, principled, sometimes objectively second-best. The engine's top choice is often not what the opponent actually picks. Rating-condition the prediction on the opponent's level.
|
|
158
|
+
- **Think in plans, not lines.** With no games to anchor you, one 12-move engine line is nearly worthless as prep. Two plans described in a sentence each ("break with f4 and pile on the h-file" vs "consolidate with Nc4/Bd2 and play for a3-b4") give the reader something they can actually execute.
|
|
159
|
+
|
|
160
|
+
## Prune obviously worse branches
|
|
161
|
+
|
|
162
|
+
Not every legal move deserves a variation. Skip a move when it is **both** significantly worse AND barely played. Either condition alone isn't enough:
|
|
163
|
+
|
|
164
|
+
- Rare-but-equal sideline → cover it (surprise value).
|
|
165
|
+
- Bad move that top players still occasionally try → cover it (you'll face it).
|
|
166
|
+
- Rare AND worse → skip. A covered-for-thoroughness branch dilutes the tree and wastes reader attention.
|
|
167
|
+
|
|
168
|
+
Concrete: after 1.e4 e5 2.d4 from White's side, only 2...exd4 is worth analysing — every alternative is worse AND rare. Cross-check with `get_prep_position` (opponent's actual choices) and `get_position_stats` (population + objective eval) before spending depth on a branch.
|
|
169
|
+
|
|
170
|
+
## When to stop, when to keep going
|
|
171
|
+
|
|
172
|
+
Depth calibrates to the *character* of the position, not to whether the DB has games or how deep in the tree you are.
|
|
173
|
+
|
|
174
|
+
- **Forcing → keep going.** Concrete tactics, only-moves, sharp sequences — analyse until the position calms (real endgame, stable middlegame structure). No game data doesn't matter; the moves are forced, so there's nothing to look up anyway. `describe_position` + engine is enough.
|
|
175
|
+
- **Quiet → stop early.** Once the position is strategic — many reasonable moves, no immediate threats — one sentence of plans is worth more than another 6 plies of tree. Stop and describe.
|
|
176
|
+
- **Novelties MUST branch.** If you're introducing a move nobody's played, "the DB has no games" is not a reason to stop — the reader will actually reach this position over the board and needs an answer. Predict the opponent's replies with `predict_human_move` (rating-conditioned to their level), sanity-check with Stockfish (objective best) and Lc0 (practical alternatives), then branch on the top ~2 predicted responses. This is precisely where all three signals matter most, because there's no historical answer to lean on. See also "Novelty burns" — save deep novelty-prep for the games that actually warrant it.
|
|
177
|
+
|
|
144
178
|
## How to combine these
|
|
145
179
|
|
|
146
180
|
None of these are rules; they're weights. In one game against a specific opponent one factor dominates (they clearly hate Catalan structures); in another it's a different one (they're rigid and just play their repertoire, so tree-depth matters more than surprise). Reason through them explicitly when recommending, and cite the concrete numbers (game counts, dates, win rates) so the user can trust or overrule.
|
package/package.json
CHANGED