@chessceo/mcp 0.29.0 → 0.30.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/dist/index.js +49 -7
- package/docs/pgn-authoring.md +28 -5
- package/docs/prep-strategy.md +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -193,7 +193,7 @@ const TOOLS = [
|
|
|
193
193
|
"Reading the response — CRITICAL:\n" +
|
|
194
194
|
"• Win % is one weight, not a verdict. Recommend 1.b3 over 1.d4 because 60% > 50% is wrong. Sample size matters (3 games at 66% is noise; 300 at 55% is signal); avgWhite / avgBlack per move show the rating context (a big score often means a rating gap, not repertoire truth).\n" +
|
|
195
195
|
"• Prep is symmetric information — both sides see the same history. Assume the opponent knows the weakness you spotted; a weak opponent won't patch it, a strong or improving one already has (but structural weaknesses like 'bad in Catalan structures' hold anyway).\n" +
|
|
196
|
-
"• Recency > career. The last 12-24 months dominate. This endpoint's compact/LLM view deliberately omits per-move `
|
|
196
|
+
"• Recency > career. The last 12-24 months dominate. This endpoint's compact/LLM view deliberately omits per-move `fashionScore` — at the individual level it's trailing noise. The general DB endpoint (`get_position_stats`) keeps it, where it's real fashion signal (what the top field is playing this month).\n" +
|
|
197
197
|
"• Opponent will deviate early. Prep is a tree, not a line — cover the 2 most likely replies at each real branching point, not one 20-move line.\n" +
|
|
198
198
|
"• Surprise is a scalpel. Don't tell a lifelong 1.e4 player to switch to 1.d4 — meta-signal screams prep. Rare secondary lines within the user's existing repertoire (e.g. 6.Bc4 instead of usual 6.Bg5 vs the Najdorf) are where surprise is real.\n\n" +
|
|
199
199
|
"For the full guide call the `read_prep_strategy_guide` tool.",
|
|
@@ -644,15 +644,16 @@ const TOOLS = [
|
|
|
644
644
|
},
|
|
645
645
|
{
|
|
646
646
|
name: "auto_evaluate",
|
|
647
|
-
description: "Walk the tree from `path` (default `[]` = whole file) and
|
|
648
|
-
"
|
|
649
|
-
"
|
|
647
|
+
description: "Walk the tree from `path` (default `[]` = whole file) and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every node via cloud_analyse. Requires a running cloud combo instance.\n\n" +
|
|
648
|
+
"**Does NOT set visible NAGs.** NAG placement is your call, not the engine's — an opening tree full of 0.00 positions doesn't need a `$10` (=) glyph on every move (that's just noise on the board), and a `!` on a novelty or a `?!` on a risky committal is a judgment call the engine can't make. Use this tool to persist the raw numbers on every node, then re-read the file and hand-pick NAGs on the moves where a glyph carries real signal (novelty, sharp choice, real mistake, decisive advantage).\n\n" +
|
|
649
|
+
"On re-read every node with a stored eval carries `ceoEval: { sf: {cp, depth}, lc0: {cp, depth} }` — the numbers travel with the file. Use `only_missing=true` (default) to skip nodes already evaluated on repeat runs.\n\n" +
|
|
650
|
+
"Costs real money — hits cloud_analyse per node. A 200-node tree at 1.5s/node = ~5 min of engine time. Runs 4 evaluations in parallel to save wall time.",
|
|
650
651
|
inputSchema: {
|
|
651
652
|
type: "object",
|
|
652
653
|
properties: {
|
|
653
654
|
id: { type: "string" },
|
|
654
655
|
path: { type: "array", items: { type: "integer", minimum: 0 }, description: "Subtree root (default = whole file)." },
|
|
655
|
-
only_missing: { type: "boolean", description: "Skip nodes that already carry a
|
|
656
|
+
only_missing: { type: "boolean", description: "Skip nodes that already carry a stored ceoEval (default true)." },
|
|
656
657
|
movetime_ms: { type: "integer", minimum: 500, maximum: 5000, description: "Per-node cloud_analyse think time (default 1500)." },
|
|
657
658
|
expected_version: { type: "integer" },
|
|
658
659
|
},
|
|
@@ -935,8 +936,6 @@ async function autoEvaluate(args) {
|
|
|
935
936
|
const batchMutations = [];
|
|
936
937
|
for (const s of stored) {
|
|
937
938
|
batchMutations.push({ op: "set_ceo_eval", path: s.path, ceoEval: s.ev });
|
|
938
|
-
if (s.ev.nag)
|
|
939
|
-
batchMutations.push({ op: "set_nags", path: s.path, nags: [s.ev.nag] });
|
|
940
939
|
}
|
|
941
940
|
const saveResult = await applyBatchMutations({ id, mutations: batchMutations, expected_version: g.version });
|
|
942
941
|
const sr = saveResult;
|
|
@@ -1062,6 +1061,47 @@ async function applyMutation(args, mutator) {
|
|
|
1062
1061
|
version: savedRow.version,
|
|
1063
1062
|
};
|
|
1064
1063
|
}
|
|
1064
|
+
// Strip cruft the LLM doesn't need from the DB-position response.
|
|
1065
|
+
// Called AFTER trimGamesMovetext so plyNumber survives long enough to
|
|
1066
|
+
// slice each game's movetext. Also renames the `transpositions` field
|
|
1067
|
+
// to something the LLM can parse without knowing chess-DB jargon.
|
|
1068
|
+
function stripPositionResponse(r) {
|
|
1069
|
+
if (!r || typeof r !== "object")
|
|
1070
|
+
return;
|
|
1071
|
+
const t = r;
|
|
1072
|
+
delete t.hash; // internal zobrist string
|
|
1073
|
+
delete t.source; // internal "database" marker; we overwrite with our own .source
|
|
1074
|
+
delete t.totalGames; // duplicates statistics.totalCount often; hasMore covers pagination
|
|
1075
|
+
if (Array.isArray(t.moves)) {
|
|
1076
|
+
for (const m of t.moves) {
|
|
1077
|
+
if (typeof m.transpositions === "number") {
|
|
1078
|
+
m.reachedViaTransposition = m.transpositions;
|
|
1079
|
+
delete m.transpositions;
|
|
1080
|
+
}
|
|
1081
|
+
// Backend calls it "hotness" — a 0-100 time-decayed popularity score
|
|
1082
|
+
// (recent + played often = high). Rename to something an LLM can read
|
|
1083
|
+
// without guessing it means "on a winning streak".
|
|
1084
|
+
if (typeof m.hotness === "number") {
|
|
1085
|
+
m.fashionScore = m.hotness;
|
|
1086
|
+
delete m.hotness;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (Array.isArray(t.games)) {
|
|
1091
|
+
for (const g of t.games) {
|
|
1092
|
+
delete g.gameId;
|
|
1093
|
+
delete g.whiteTitle;
|
|
1094
|
+
delete g.blackTitle;
|
|
1095
|
+
delete g.whiteTeam;
|
|
1096
|
+
delete g.blackTeam;
|
|
1097
|
+
delete g.round;
|
|
1098
|
+
delete g.plyNumber;
|
|
1099
|
+
delete g.relevance;
|
|
1100
|
+
delete g.site;
|
|
1101
|
+
delete g.ply;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1065
1105
|
// Trim every game's `moves` field to just the plies AFTER the queried
|
|
1066
1106
|
// position, using each game's `plyNumber`. Massive token save — a game
|
|
1067
1107
|
// 80 plies long queried at ply 12 drops to ~68 plies of movetext. Ports
|
|
@@ -1236,6 +1276,7 @@ async function callToolInner(name, args) {
|
|
|
1236
1276
|
const converted = convertAvailableMovesToSAN(raw, effectiveFen);
|
|
1237
1277
|
if (converted && typeof converted === "object") {
|
|
1238
1278
|
trimGamesMovetext(converted);
|
|
1279
|
+
stripPositionResponse(converted);
|
|
1239
1280
|
if (ev)
|
|
1240
1281
|
converted.eval = ev;
|
|
1241
1282
|
}
|
|
@@ -1256,6 +1297,7 @@ async function callToolInner(name, args) {
|
|
|
1256
1297
|
const converted = convertAvailableMovesToSAN(raw, fen);
|
|
1257
1298
|
if (converted && typeof converted === "object") {
|
|
1258
1299
|
trimGamesMovetext(converted);
|
|
1300
|
+
stripPositionResponse(converted);
|
|
1259
1301
|
converted.source = source;
|
|
1260
1302
|
if (ev)
|
|
1261
1303
|
converted.eval = ev;
|
package/docs/pgn-authoring.md
CHANGED
|
@@ -23,7 +23,7 @@ Reading:
|
|
|
23
23
|
Building (use these — one call for many ops):
|
|
24
24
|
|
|
25
25
|
- **`apply_mutations(id, mutations[])`** — batch. This is the primary build tool. Send 100 mutations in one call → one load-parse-mutate-save cycle instead of 100. When you're writing a repertoire from scratch, EVERYTHING should go through this. Individual mutation tools are for surgical follow-up edits, not for bulk work.
|
|
26
|
-
- **`auto_evaluate(id, path?)`** — walk the tree from `path` and
|
|
26
|
+
- **`auto_evaluate(id, path?)`** — walk the tree from `path` and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every node by running cloud_analyse on each position. **Does NOT set visible NAGs** — the numbers land in a hidden `[%ceo-eval]` tag so you can read them back later. NAG placement (`$1` `!`, `$14` `⩲`, `$16` `±`, `$18` `+−`, etc.) is your editorial call, not an automatic mapping — see the section on NAG discipline below. Skip nodes that already have a stored eval by default.
|
|
27
27
|
|
|
28
28
|
Per-op mutation tools (single-op calls, use for edits after the bulk build):
|
|
29
29
|
|
|
@@ -40,9 +40,9 @@ All mutations **auto-save** with optimistic locking. Response includes the new `
|
|
|
40
40
|
## Typical build order
|
|
41
41
|
|
|
42
42
|
1. `read_prep_file` — see what's there.
|
|
43
|
-
2. `apply_mutations([...])` — one call with your whole intended build (all `add_move` for the moves and variations, plus any `set_comment`/`set_annotations` you already know at author time
|
|
44
|
-
3. `auto_evaluate(id)` — walk the tree
|
|
45
|
-
4.
|
|
43
|
+
2. `apply_mutations([...])` — one call with your whole intended build (all `add_move` for the moves and variations, plus any `set_comment`/`set_annotations` you already know at author time, plus any `set_nags` where you already have a clear judgment — novelty `$146`, `!?` speculative sac, obvious `?` blunder in a sideline you're rejecting).
|
|
44
|
+
3. `auto_evaluate(id)` — walk the tree and PERSIST engine numbers on every node. Does not touch visible NAGs. Cheap way to get every position's Stockfish + Lc0 read baked into the file for later reference.
|
|
45
|
+
4. Re-read the file and add NAGs where they carry real signal (see NAG discipline below). Use individual mutation tools for surgical follow-ups (fix one comment, add one arrow, promote a specific variation, prune a branch).
|
|
46
46
|
|
|
47
47
|
The build-cost math: a 200-move file via individual `add_move` calls is 200 saves ≈ 100+ seconds of tool overhead. The same file via one `apply_mutations` call is one save ≈ 500ms. Use batch by default.
|
|
48
48
|
|
|
@@ -122,7 +122,21 @@ Engine numbers go into the NAG, not into prose. Convert the eval to the correct
|
|
|
122
122
|
| `\|eval\| ≥ 1.3` | `$18` (+−) | `$19` (−+) |
|
|
123
123
|
| Sharp, hard to evaluate | `$13` (∞) | `$13` (∞) |
|
|
124
124
|
|
|
125
|
-
**
|
|
125
|
+
**NAGs are editorial, not automatic.** `auto_evaluate` persists engine numbers on every node but does NOT set visible NAGs — because a repertoire full of 0.00 opening theory does not want a `$10` (=) glyph plastered on every move. That's noise, not signal. NAGs are your call: mark moves that carry a judgment a reader can't derive by looking at the board.
|
|
126
|
+
|
|
127
|
+
Where NAGs actually earn their place:
|
|
128
|
+
|
|
129
|
+
- **`$146` (novelty)** — a move that hasn't appeared at this level. Only set if you actually know this (e.g. saw 0 games via `get_position_stats`).
|
|
130
|
+
- **`$5 !?` (interesting/speculative)** — a committal move you're recommending anyway (a piece sac, a positional pawn concession).
|
|
131
|
+
- **`$1 !` / `$3 !!`** — objectively good moves the reader might miss.
|
|
132
|
+
- **`$2 ?` / `$4 ??` / `$6 ?!`** — mistakes in a sideline you're showing to reject.
|
|
133
|
+
- **`$14/$15` (⩲/⩱)** and **`$16/$17` (±/∓)** — assessments on positions where the direction of pressure matters for the plan you're teaching. Skip these on 0.00 theory nodes.
|
|
134
|
+
- **`$18/$19` (+−/−+)** — decisive advantages, use where the reader should recognise the position is winning.
|
|
135
|
+
- **`$44` (compensation)** on a genuine gambit; **`$13` (∞)** on a genuinely sharp/unclear position that engines don't resolve.
|
|
136
|
+
|
|
137
|
+
Where NAGs are noise: every `$10` "=" glyph on every equal position. If the whole tree is 0.00, that's the *default state* — leave it unmarked and the reader understands.
|
|
138
|
+
|
|
139
|
+
When reading back a file, `node.ceoEval.nag` carries the threshold-derived NAG (`$10` / `$14` / `$16` / `$18` etc.) — treat it as a *suggestion*, not an automatic write. Promote it to a visible NAG only when a glyph on that move actually helps the reader.
|
|
126
140
|
|
|
127
141
|
**Persisted evals travel with the file.** Every node with an eval gets `ceoEval: { sf: {cp: 25, depth: 32}, lc0: {cp: 30, depth: 18}, nag: "$14" }` on subsequent `read_prep_file` calls. Stored as `[%ceo-eval sf=+0.25/32 lc0=+0.30/18 nag=$14]` inside the PGN comment (an escape tag the app hides from the board view, just like [%cal] / [%csl]). So a re-read after auto_evaluate gives you every position's number without re-running cloud_analyse. Query tools (get_position_stats, get_player_preparation, prep_snapshot) also auto-attach a live `eval` at the request position when a cloud engine is running.
|
|
128
142
|
|
|
@@ -133,6 +147,15 @@ Engine numbers go into the NAG, not into prose. Convert the eval to the correct
|
|
|
133
147
|
|
|
134
148
|
**Don't name the engine unless it adds signal.** "SF: +0.15" for a routine position is noise. Name it when there's a mismatch worth flagging: `{Human predictor gives Black 47% win despite the -0.35 objective eval — Elo gap does the work.}`
|
|
135
149
|
|
|
150
|
+
**Only quote engine numbers for positions you actually called `cloud_analyse` on** (or that carry a `ceoEval` from an earlier auto_evaluate, or an auto-attached `.eval` from a query tool). Do NOT infer an eval for a parent position from its children and then write "both engines 0.00" on the parent — that reads as a measurement but is a guess. If the position wasn't analysed, either analyse it or omit the number: prose can say "both continuations run to 0.00, so this looks balanced" without claiming an eval that wasn't taken.
|
|
151
|
+
|
|
152
|
+
**When you set `contempt`, attribute it to Lc0 only.** Contempt affects Lc0 exclusively — Stockfish always analyses objectively. So don't write "both engines at contempt −15" — that misleads the reader into thinking SF was biased too. Correct phrasings:
|
|
153
|
+
|
|
154
|
+
- `{Lc0 (contempt −15): 0.00 — even biased toward Black, still balanced. SF objective: 0.00.}`
|
|
155
|
+
- `{With Lc0 nudged toward Black (contempt −15) it still picks Qg6 at 0.00; SF's objective read also 0.00.}`
|
|
156
|
+
|
|
157
|
+
If you only quote the Lc0 number, disclose the bias in the same sentence — `{Lc0 gives Black +0.30}` with an undisclosed `contempt=-15` is a false objectivity claim.
|
|
158
|
+
|
|
136
159
|
### Before you write any commentary: describe_position
|
|
137
160
|
|
|
138
161
|
LLMs are not reliable at reading FEN strings — you'll swap files/ranks, invent captures, miscount pieces. Before you write a comment describing what's happening in a position, call `describe_position(fen)`. It's pure computation (no engine, ~1ms) and returns the same board state a human sees:
|
package/docs/prep-strategy.md
CHANGED
|
@@ -21,7 +21,7 @@ The failure mode is *authoritative-sounding recipe = 30% real tool output + 70%
|
|
|
21
21
|
|
|
22
22
|
## Numbers are inputs, not verdicts
|
|
23
23
|
|
|
24
|
-
The move statistics endpoints return win %, game counts, and (in the big DB)
|
|
24
|
+
The move statistics endpoints return win %, game counts, and (in the big DB) a `fashionScore` (0–100, how in-fashion the move is right now at the top level — recent + played often). Treat every one of these as a *weight*, not a *rule*.
|
|
25
25
|
|
|
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.
|
|
@@ -41,7 +41,7 @@ Every recommendation should be filtered through: "does this survive the fact tha
|
|
|
41
41
|
|
|
42
42
|
The opponent's last 12-24 months matter far more than a 10-year career average. Repertoires evolve — a lifelong Najdorf player might have quietly become a Petroff player last year, and their old career stats will lie to you if you skim.
|
|
43
43
|
|
|
44
|
-
**Related product note (2026-07-23):** the `get_player_preparation` endpoint (compact / LLM view) deliberately strips the per-move `
|
|
44
|
+
**Related product note (2026-07-23):** the `get_player_preparation` endpoint (compact / LLM view) deliberately strips the per-move `fashionScore` field. At the individual-player level, fashion is trailing noise — the opponent's opening trend is already captured in game dates. `fashionScore` stays on the general-DB endpoint (`get_position_stats`), where it's genuinely useful: it's what the whole top field is playing this month.
|
|
45
45
|
|
|
46
46
|
## Prep is a tree, not a line
|
|
47
47
|
|
|
@@ -68,7 +68,7 @@ Deep prep is a weapon you spend, not one you own. A prepared novelty against a s
|
|
|
68
68
|
|
|
69
69
|
## Move-order tricks aren't visible in raw win rates
|
|
70
70
|
|
|
71
|
-
The `
|
|
71
|
+
The `reachedViaTransposition` field on move statistics tells you how many of the games at that position arrived via a different move order — a hint at how much the line folds into related structures. Move-order tricks are legitimate prep and position stats alone can't recommend them.
|
|
72
72
|
|
|
73
73
|
- Considering 1.Nf3 or 1.c4 as a duck around 1.d4? First check the opponent's repertoire *against 1.d4*. If they play the King's Indian, most 1.Nf3/1.c4 lines transpose anyway — the trick doesn't help.
|
|
74
74
|
- Move orders are useful when the opponent plays something that specifically depends on the move order — e.g. a Nimzo player who never gets to play the Nimzo because you go 1.Nf3-2.g3.
|
package/package.json
CHANGED