@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 CHANGED
@@ -501,11 +501,17 @@ const TOOLS = [
501
501
  maximum: 10000,
502
502
  description: "Think time in milliseconds (default 2000).",
503
503
  },
504
- multipv: {
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: "Number of candidate lines per engine (default 3).",
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 3).",
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, fen);
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, t.fen);
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
- const multipv = typeof args.multipv === "number" ? args.multipv : 3;
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
- multipv: job.multipv,
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 }, job.fen);
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, fen) {
1557
+ function analysisToStoredEval(analysis) {
1549
1558
  if (!analysis || typeof analysis !== "object")
1550
1559
  return null;
1551
1560
  const r = analysis;
1552
- // NOTE: the backend already returns White-POV cp/mate (engine-ws flips
1553
- // in ParseInfo based on side-to-move, then cloud_snapshot passes the
1554
- // number through). Previously this function ALSO flipped on
1555
- // black-to-move a double flip that silently inverted every
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.multipv === "number")
2065
- body.multipv = args.multipv;
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, fen);
2083
+ const ev = analysisToStoredEval(converted);
2080
2084
  if (ev)
2081
2085
  await storeEvalOnNode(resolved.file, ev);
2082
2086
  }
@@ -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:
@@ -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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.35.1",
3
+ "version": "0.36.4",
4
4
  "description": "Model Context Protocol server for chess.ceo — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts.",
5
5
  "type": "module",
6
6
  "bin": {