@chessceo/mcp 0.46.0 → 0.49.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/README.md CHANGED
@@ -1,23 +1,27 @@
1
1
  # @chessceo/mcp
2
2
 
3
- Model Context Protocol server for [chess.ceo](https://chess.ceo) — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts. Lets Claude, Cursor, and any other MCP host answer chess questions directly against real data instead of hallucinating.
3
+ Model Context Protocol server for [chess.ceo](https://chess.ceo) — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts, cloud engine analysis, and (signed-in) a full read/write prep-file workflow. Lets Claude, Cursor, and any other MCP host answer chess questions directly against real data instead of hallucinating.
4
4
 
5
- No API key. No auth. No state. Free to use.
5
+ Player/game lookups need no API key or auth. Cloud engines and prep-file tools need a `mcp_...` bearer token (see Auth below).
6
6
 
7
7
  ## What it can do
8
8
 
9
- The server exposes 8 tools that mirror the public GET API surface at `chess.ceo`:
9
+ 49 tools as of v0.49.0, mirroring the `chess.ceo` API surface. A few of the most-used:
10
10
 
11
11
  | Tool | What it answers |
12
12
  |---|---|
13
13
  | `search_player` | "Find FIDE ID for Magnus Carlsen" |
14
14
  | `get_player_profile` | "How strong is X, what do they play, who have they beaten" |
15
- | `get_player_preparation` | "What does X play against 1.e4? What's their win rate with the Najdorf?" |
15
+ | `prepare_opponent` + `get_prep_position` | "What does X play against 1.e4? What's their win rate with the Najdorf?" |
16
16
  | `get_position_stats` | "From this position, which move scores best in the 11.7M-game database?" |
17
17
  | `get_head_to_head` | "What's the record between X and Y?" |
18
- | `list_live_tournaments` | "What's being broadcast live right now?" |
19
- | `list_tournament_players` | "Who's playing in tournament T?" |
20
- | `list_player_live_tournaments` | "Is X playing anywhere right now?" |
18
+ | `list_live_tournaments` / `list_tournament_players` / `list_player_live_tournaments` | "What's being broadcast live right now? Who's playing? Is X in it?" |
19
+ | `cloud_analyse` | "Run Stockfish + Lc0 on this position on my rented GPU instance" |
20
+ | `read_prep_file` / `add_line` / `add_move` / `apply_mutations` | Read and edit your own repertoire/course PGNs stored server-side |
21
+ | `auto_evaluate` / `deep_analyse` | Kick off a long-running engine-evaluation job over a whole prep file, poll it, cancel it |
22
+ | `read_docs` | Bundled guides (engine usage, opening prep, prep-file conventions, PGN authoring, summary authoring) |
23
+
24
+ Full tool inventory with categories lives in [`CLAUDE.md`](CLAUDE.md) under "Tools cheatsheet" — that's the maintained source of truth; this table is illustrative, not exhaustive.
21
25
 
22
26
  ## Install (Claude Desktop)
23
27
 
@@ -62,6 +66,25 @@ This repo is also a [Claude Code plugin marketplace](https://code.claude.com/doc
62
66
 
63
67
  Claude Code will pull the plugin from GitHub and wire the MCP server automatically. Enable "Sync automatically" in the marketplace UI if you want future updates fetched on push.
64
68
 
69
+ ## Auth (for cloud engines and prep files)
70
+
71
+ Read-only chess data — player search, profiles, position stats, head-to-head, live tournaments — needs nothing. Cloud engine tools and your own prep-file tools (list/read/create/edit) need a `mcp_...` bearer token.
72
+
73
+ - **Local (`npx`/Claude Desktop/Cursor):** set `CHESSCEO_TOKEN` in the server's `env` block:
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "chessceo": {
78
+ "command": "npx",
79
+ "args": ["-y", "@chessceo/mcp"],
80
+ "env": { "CHESSCEO_TOKEN": "mcp_..." }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+ Get a token from your chess.ceo account settings.
86
+ - **Remote (`mcp.chess.ceo/mcp`):** no config needed — calling an authed tool without a token triggers the host's normal OAuth flow (claude.ai, ChatGPT connectors) automatically.
87
+
65
88
  ## Try it
66
89
 
67
90
  Ask your model:
@@ -109,7 +132,7 @@ In Claude Desktop, edit `claude_desktop_config.json`:
109
132
  }
110
133
  ```
111
134
 
112
- Same 8 tools, same data, zero-install. Useful when the host can't spawn subprocesses (e.g. Claude.ai web, Claude mobile, ChatGPT connectors).
135
+ Same tools, same data, zero-install. Bearer-authed tools (cloud engines, prep files) go through this host's OAuth flow instead of a config-file token. Useful when the host can't spawn subprocesses (e.g. Claude.ai web, Claude mobile, ChatGPT connectors).
113
136
 
114
137
  ## Self-host the HTTP transport
115
138
 
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
20
20
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
21
21
  import { TOOLS } from "./tools.js";
22
22
  import { PROMPTS } from "./prompts.js";
23
- import { commentAntiPatterns, longLineWarning, noDescribeWarning, noStatsCheckWarning, positionsDescribed, positionsStatsChecked, } from "./warnings.js";
23
+ import { commentAntiPatterns, longLineWarning, noDescribeWarning, noStatsCheckWarning, positionalNagOnIntermediateWarning, positionsDescribed, positionsStatsChecked, } from "./warnings.js";
24
24
  import { authContext, authedRequest, deleteGame, fetchGame, get, makeFileId, restoreGame, } from "./http.js";
25
25
  import { analysisToStoredEval, capPvsInResponse, convertCloudSnapshotResponse, fetchCompactEval, } from "./analysis/response.js";
26
26
  import { autoEvaluate, autoEvaluateCancel, autoEvaluateStatus, } from "./analysis/auto.js";
@@ -29,7 +29,7 @@ import { getNodeByPath, resolveFromNodeOrFen, storeEvalOnNode, } from "./analysi
29
29
  import { findPositionInCourses, readCourseAtPosition, runSfEval } from "./courses.js";
30
30
  import { applyBatchMutations, applyMutation, argNodeId, } from "./prep/mutations.js";
31
31
  import { listNodes, listTranspositions, readPrepFile, } from "./prep/read.js";
32
- import { createPrepFile, findPositionInFiles, listCollections, listPrepFiles, searchPrepFiles, } from "./prep/library.js";
32
+ import { createCollection, createPrepFile, findPositionInFiles, listCollections, listPrepFiles, searchPrepFiles, } from "./prep/library.js";
33
33
  import { convertAvailableMovesToSAN, normalizeSourceForBackend, stripPositionResponse, trimGamesMovetext, } from "./response_transforms.js";
34
34
  // Tools that require an MCP token — cloud engine + prep-file tools
35
35
  // operate on the caller's own account so we can't service them
@@ -42,6 +42,7 @@ const AUTHED_TOOLS = new Set([
42
42
  "stop_cloud_engine",
43
43
  "cloud_analyse",
44
44
  "list_collections",
45
+ "create_collection",
45
46
  "list_prep_files",
46
47
  "search_prep_files",
47
48
  "find_position_in_files",
@@ -103,6 +104,7 @@ const ENGINE_USAGE_DOC = loadBundledDoc("engine-usage.md", "Engine usage guide")
103
104
  const PREP_STRATEGY_DOC = loadBundledDoc("prep-strategy.md", "Prep strategy guide");
104
105
  const PREP_FILES_DOC = loadBundledDoc("prep-files-guide.md", "Prep files guide");
105
106
  const PGN_AUTHORING_DOC = loadBundledDoc("pgn-authoring.md", "PGN authoring guide");
107
+ const SUMMARY_AUTHORING_DOC = loadBundledDoc("summary-authoring.md", "Summary authoring guide");
106
108
  // Reference PGNs authored by a strong human coach. LLM pulls these when
107
109
  // it wants to see the commentary style, NAG discipline, and annotation
108
110
  // density we want it to hit. Kept as raw PGN so the LLM can parse them
@@ -110,6 +112,45 @@ const PGN_AUTHORING_DOC = loadBundledDoc("pgn-authoring.md", "PGN authoring guid
110
112
  // all intact) — not summarised into English.
111
113
  const EXAMPLE_OVERVIEW_PGN = loadBundledDoc("examples/italian-fried-liver.pgn", "Italian Fried Liver overview example");
112
114
  const EXAMPLE_REPERTOIRE_PGN = loadBundledDoc("examples/najdorf-6-f4-white.pgn", "Najdorf 6.f4 White repertoire example");
115
+ // v0.48: consolidated the five `read_*_guide` / `read_example_prep_files`
116
+ // tools into ONE `read_docs`. LLM lists what it wants; we return them
117
+ // in a single response. Also cleaner: enumerating available docs in one
118
+ // tool description (with per-doc "CALL WHEN" hints) beats surfacing
119
+ // five almost-identical tools in the tool listing.
120
+ const DOC_LIBRARY = {
121
+ "engine-usage": ENGINE_USAGE_DOC,
122
+ "opening-prep": PREP_STRATEGY_DOC,
123
+ "prep-files": PREP_FILES_DOC,
124
+ "pgn-authoring": PGN_AUTHORING_DOC,
125
+ "summary-authoring": SUMMARY_AUTHORING_DOC,
126
+ "examples/italian-fried-liver-overview": EXAMPLE_OVERVIEW_PGN,
127
+ "examples/najdorf-6-f4-repertoire": EXAMPLE_REPERTOIRE_PGN,
128
+ };
129
+ function readDocs(args) {
130
+ const raw = args.docs;
131
+ const requested = Array.isArray(raw)
132
+ ? raw.map(String).map(s => s.trim()).filter(s => s.length > 0)
133
+ : [];
134
+ if (requested.length === 0) {
135
+ throw new Error(`docs required — pass e.g. { docs: ["engine-usage", "pgn-authoring"] }. Available: ${Object.keys(DOC_LIBRARY).join(", ")}`);
136
+ }
137
+ const out = {};
138
+ const unknown = [];
139
+ for (const name of requested) {
140
+ if (name in DOC_LIBRARY) {
141
+ out[name] = DOC_LIBRARY[name];
142
+ }
143
+ else {
144
+ unknown.push(name);
145
+ }
146
+ }
147
+ const resp = { docs: out };
148
+ if (unknown.length > 0) {
149
+ resp.unknown = unknown;
150
+ resp.note = `unknown doc name(s): ${unknown.join(", ")}. Available: ${Object.keys(DOC_LIBRARY).join(", ")}`;
151
+ }
152
+ return resp;
153
+ }
113
154
  // Log every tool call in and out. Keeps args + response payloads together
114
155
  // with a per-call duration so we can trace what the LLM asked for and what
115
156
  // it got back on the same journalctl line. Response is JSON-stringified and
@@ -397,6 +438,8 @@ async function callToolInner(name, args) {
397
438
  return readCourseAtPosition(args);
398
439
  case "list_collections":
399
440
  return listCollections(args);
441
+ case "create_collection":
442
+ return createCollection(args);
400
443
  case "list_prep_files":
401
444
  return listPrepFiles(args);
402
445
  case "search_prep_files":
@@ -461,7 +504,14 @@ async function callToolInner(name, args) {
461
504
  });
462
505
  }
463
506
  case "set_nags":
464
- return applyMutation(args, (file, idIndex) => setNags(file, resolveNodeId(idIndex, argNodeId(args)), Array.isArray(args.nags) ? args.nags.map(String) : []));
507
+ return applyMutation(args, (file, idIndex) => {
508
+ const nagsArg = Array.isArray(args.nags) ? args.nags.map(String) : [];
509
+ const targetPath = resolveNodeId(idIndex, argNodeId(args));
510
+ const targetNode = getNodeByPath(file.root, targetPath);
511
+ const nagWarn = positionalNagOnIntermediateWarning(targetNode, nagsArg);
512
+ const step = setNags(file, targetPath, nagsArg);
513
+ return { ...step, ...(nagWarn ? { warning: nagWarn } : {}) };
514
+ });
465
515
  case "set_annotations": {
466
516
  const arrowsRaw = Array.isArray(args.arrows) ? args.arrows : [];
467
517
  const highlightsRaw = Array.isArray(args.highlights) ? args.highlights : [];
@@ -497,16 +547,8 @@ async function callToolInner(name, args) {
497
547
  const node = getNodeByPath(file.root, path);
498
548
  return { ceoEval: node.ceoEval ?? null };
499
549
  }
500
- case "read_engine_usage_guide":
501
- return { guide: ENGINE_USAGE_DOC };
502
- case "read_opening_prep_guide":
503
- return { guide: PREP_STRATEGY_DOC };
504
- case "read_prep_files_guide":
505
- return { guide: PREP_FILES_DOC };
506
- case "read_pgn_authoring_guide":
507
- return { guide: PGN_AUTHORING_DOC };
508
- case "read_example_prep_files":
509
- return { overview: EXAMPLE_OVERVIEW_PGN, repertoire: EXAMPLE_REPERTOIRE_PGN };
550
+ case "read_docs":
551
+ return readDocs(args);
510
552
  case "prep_snapshot": {
511
553
  const me = Number(args.fide_id_me);
512
554
  const opp = Number(args.fide_id_opponent);
@@ -71,6 +71,23 @@ export async function findPositionInFiles(args) {
71
71
  prep_files: games.map(projectGameRow),
72
72
  };
73
73
  }
74
+ export async function createCollection(args) {
75
+ const title = String(args.title || "").trim();
76
+ if (!title)
77
+ throw new Error("title is required");
78
+ const folderPath = typeof args.folder_path === "string" ? args.folder_path.trim() : undefined;
79
+ const raw = await authedRequest("POST", PGN_BASE, {
80
+ title,
81
+ ...(folderPath ? { folderPath } : {}),
82
+ });
83
+ const collection = unwrap(raw);
84
+ return {
85
+ ok: true,
86
+ id: collection.id,
87
+ title: collection.title,
88
+ folder_path: collection.folderPath,
89
+ };
90
+ }
74
91
  export async function createPrepFile(args) {
75
92
  const collectionId = typeof args.collection_id === "string" ? args.collection_id.trim() : "";
76
93
  if (!collectionId) {
@@ -19,7 +19,7 @@ import { exportPGN } from "../pgn/exporter.js";
19
19
  import { buildIdIndex, NodeIdError, PathError, resolveNodeId, ROOT_ID, } from "../pgn/paths.js";
20
20
  import { addLine, addMove, deleteSubtree, MutationError, promoteVariation, setAnnotations, setCeoEval, setComment, setNags, setTag, } from "../pgn/mutations.js";
21
21
  import { getNodeByPath } from "../analysis/file_handle.js";
22
- import { commentAntiPatterns, longLineWarning, noDescribeWarning, noStatsCheckWarning, } from "../warnings.js";
22
+ import { commentAntiPatterns, longLineWarning, noDescribeWarning, noStatsCheckWarning, positionalNagOnIntermediateWarning, } from "../warnings.js";
23
23
  // Extract a node id from the args. Accepts either `node_id` or a
24
24
  // `parent_id` alias for the add-style tools. Throws with a helpful
25
25
  // message if malformed.
@@ -78,8 +78,14 @@ export function dispatchMutation(file, idIndex, op) {
78
78
  const all = [...commentWarns, ...(describeWarn ? [describeWarn] : [])];
79
79
  return { ...step, ...(all.length > 0 ? { warnings: all } : {}) };
80
80
  }
81
- case "set_nags":
82
- return setNags(file, resolve(nodeIdField("node_id")), Array.isArray(op.nags) ? op.nags.map(String) : []);
81
+ case "set_nags": {
82
+ const nagsArr = Array.isArray(op.nags) ? op.nags.map(String) : [];
83
+ const targetPath = resolve(nodeIdField("node_id"));
84
+ const targetNode = getNodeByPath(file.root, targetPath);
85
+ const nagWarn = positionalNagOnIntermediateWarning(targetNode, nagsArr);
86
+ const step = setNags(file, targetPath, nagsArr);
87
+ return { ...step, ...(nagWarn ? { warning: nagWarn } : {}) };
88
+ }
83
89
  case "set_annotations": {
84
90
  const arrows = Array.isArray(op.arrows) ? op.arrows : [];
85
91
  const highlights = Array.isArray(op.highlights) ? op.highlights : [];
package/dist/tools.js CHANGED
@@ -466,6 +466,27 @@ export const TOOLS = [
466
466
  required: ["id"],
467
467
  },
468
468
  },
469
+ {
470
+ name: "create_collection",
471
+ description: "Create a new, empty PGN collection (a folder of prep files) — NOT a prep file itself. Use this when the user asks for a new file/course and `list_collections` shows nothing suitable to put it in; then call `create_prep_file` with the returned `id`.\n\n" +
472
+ "**Check `list_collections` first** — a duplicate folder with a slightly different name is the common failure mode here, same as `create_prep_file`.\n\n" +
473
+ "`folder_path` is optional (root if omitted) but should almost always be set — an ungrouped pile of top-level collections is exactly what this tool exists to avoid creating.\n\n" +
474
+ "Response: `{ok, id, title, folder_path}` — pass `id` to `create_prep_file` as `collection_id`.",
475
+ inputSchema: {
476
+ type: "object",
477
+ properties: {
478
+ title: {
479
+ type: "string",
480
+ description: "Collection name, shown in the user's file browser. Example: 'Sicilian Rauzer'.",
481
+ },
482
+ folder_path: {
483
+ type: "string",
484
+ description: "Virtual folder path, e.g. '/AI Organized/Sicilian Rauzer'. Omit for root.",
485
+ },
486
+ },
487
+ required: ["title"],
488
+ },
489
+ },
469
490
  {
470
491
  name: "create_prep_file",
471
492
  description: "Create a new (empty) prep file in the specified collection. `name` becomes the Event PGN tag. You then extend it with mutation tools (add_move, set_comment, …).\n\n" +
@@ -837,34 +858,41 @@ export const TOOLS = [
837
858
  },
838
859
  },
839
860
  {
840
- name: "read_engine_usage_guide",
841
- description: "Returns the full chess.ceo engine-usage guide: when to trust Stockfish (objective truth) vs Lc0 (practical eval), how to read disagreements between them, and how to use Lc0 contempt to find non-objective 'practical' ideas. Call this ONCE per session before running expensive `cloud_analyse` calls or when the user asks WHY the engines gave certain scores. Same content is also available as the `engine_usage_primer` prompt (for clients that surface prompts as slash commands), but many clients do not expose prompts to the model — this tool works everywhere.",
842
- inputSchema: { type: "object", properties: {} },
843
- },
844
- {
845
- name: "read_opening_prep_guide",
846
- description: "**CALL WHEN**: the user asks about OPENING PREPARATION — 'prep me against X', 'what should I play vs the Najdorf', 'help me build a repertoire against 1.e4', 'walk this opponent's Sveshnikov'. This guide is chess-and-analysis philosophy, not storage semantics.\n\n" +
847
- "Covers: why win% is one weight not a verdict, why prep is a two-player game with symmetric information (opponent sees your history too), how sample size and recency change the reading, when 'revealed weaknesses' are actionable vs already patched, how to choose between the GM-classical DB and the main DB, when to combine chesscom/lichess sources with FIDE, the three chess.com profile shapes (consistent / eclectic / split-personality), the reversed-colours scarcity trick, how to calibrate surprise (rare secondary lines inside the existing repertoire, not big first-move switches).\n\n" +
848
- "Different tool: `read_prep_files_guide` covers the FILE STORAGE feature (how to list/create/save prep files) — call that only when about to manipulate files, not for opening questions.",
849
- inputSchema: { type: "object", properties: {} },
850
- },
851
- {
852
- name: "read_prep_files_guide",
853
- description: "**CALL WHEN**: you're about to CREATE, LIST, SAVE, or DELETE a prep file — the persistent file storage feature. Not for opening prep philosophy (that's `read_opening_prep_guide`) and not for how to write PGN (that's `read_pgn_authoring_guide`).\n\n" +
854
- "Covers: the AI Prep folder, when to list vs search vs create (avoid duplicate 'Prep vs Firouzja' files), optimistic locking with `version`, naming conventions for the [Event] tag, node-id addressing basics.",
855
- inputSchema: { type: "object", properties: {} },
856
- },
857
- {
858
- name: "read_pgn_authoring_guide",
859
- description: "Returns the guide on how to write correct, useful PGN — mainline discipline, variations as moves (never prose describing moves), NAG symbols including novelty ($146), unclear ($13), compensation ($44) and the standard set, ChessBase arrow/coloured-square syntax ([%cal] / [%csl]), and common pitfalls the parser will reject. Call this ONCE per session before any save_prep_file call, or any time you're producing PGN output for the user.",
860
- inputSchema: { type: "object", properties: {} },
861
- },
862
- {
863
- name: "read_example_prep_files",
864
- description: "**CALL WHEN**: about to write ANY prose commentary in a prep file, ever. Even one comment. Even one variation. This is not optional and not once-per-project — call it early in the session and read the examples before your first `set_comment` or `apply_mutations` batch that includes comments. Log analysis showed <5% of sessions call this despite it being the single biggest quality lift documented in this MCP; that's the mistake this description is trying to fix.\n\n" +
865
- "Why: `read_pgn_authoring_guide` tells you the rules in prose. These files show you the *sound* of them applied by a strong human coach — comment density (short and load-bearing, not verbose), how citations look in-line (`WeiYi-Svidler` not `\"Svidler's choice at the FIDE World Blitz Team, June 2026\"`), when `$146` / `$3` / `$44` earn their place, when a bare `[%csl Rf7]` says everything a sentence would say. LLMs default to florid, restate-what's-visible commentary; reading these once inoculates against that.\n\n" +
866
- "Two files bundled with the MCP (not the user's own): one general opening overview (Italian Fried Liver, both sides, 1600+ audience) and one one-sided repertoire (Najdorf 6.f4 for White, 2200+ audience). Response: `{ overview: <pgn>, repertoire: <pgn> }` — raw PGN with comments, arrows, NAGs, stored evals intact.",
867
- inputSchema: { type: "object", properties: {} },
861
+ name: "read_docs",
862
+ description: "Fetch one or more bundled reference docs / example files in a single call. **Batch what you need in one call rather than reading them one at a time.**\n\n" +
863
+ "**Docs available** — each with when to call:\n\n" +
864
+ " • `engine-usage` — CALL WHEN: about to run `cloud_analyse`, explaining engine disagreements, or asked WHY the engines gave certain scores. Covers Stockfish (objective truth) vs Lc0 (practical eval), how to read disagreements, when to use Lc0 contempt.\n" +
865
+ " • `opening-prep` — CALL WHEN: the user asks about opening preparation ('prep me against X', 'what should I play vs the Najdorf', 'walk this opponent's Sveshnikov'). Covers why win% is one weight not a verdict, prep as a two-player game, sample-size/recency reading, when 'revealed weaknesses' are actionable vs patched, GM-classical vs main DB, chess.com/lichess/FIDE source combining, chess.com profile shapes, reversed-colours scarcity trick, surprise calibration.\n" +
866
+ " • `prep-files` — CALL WHEN: about to CREATE, LIST, SAVE, or DELETE a prep file. Covers list vs search vs create (avoid duplicates), optimistic locking with `version`, naming conventions for the [Event] tag, collection selection, node-id addressing basics.\n" +
867
+ " • `pgn-authoring` — CALL WHEN: about to write any comment / NAG / arrow / variation. Covers mainline discipline, variations as moves (not prose describing moves), NAG placement rules (position NAGs at ENDPOINTS only), the pasted-engine-PV anti-pattern, transposition handling, 'main tabiya' behavior, cover-N-alternatives rule, describe_position grounding.\n" +
868
+ " • `summary-authoring` — CALL WHEN: the user asks for a SUMMARY prep file — the 15-minute-read shape. Covers the two-file convention (reference vs summary), the coach's voice with real jvanf/Peter-Heine-Nielsen examples, endpoint NAG discipline, novelty and move-order-trick callouts, when to CUT branches rather than add them.\n" +
869
+ " • `examples/italian-fried-liver-overview` — bundled reference PGN, general opening overview at ~1600 audience. Shows comment density, NAG discipline, annotation style from a strong human coach.\n" +
870
+ " • `examples/najdorf-6-f4-repertoire` — bundled reference PGN, one-sided repertoire at ~2200 audience. Same purpose.\n\n" +
871
+ "**CALL WHEN in general**: early in the session before any substantive work, with `docs: [\"pgn-authoring\", \"examples/najdorf-6-f4-repertoire\"]` if writing prep, or `[\"opening-prep\", \"engine-usage\"]` if analysing. If you don't know which, ask for `[\"opening-prep\", \"prep-files\", \"pgn-authoring\"]` — three docs in one call is fine.\n\n" +
872
+ "Response: `{ docs: { <name>: <full content> } }`. Content is markdown for guides, raw PGN for examples.",
873
+ inputSchema: {
874
+ type: "object",
875
+ properties: {
876
+ docs: {
877
+ type: "array",
878
+ minItems: 1,
879
+ items: {
880
+ type: "string",
881
+ enum: [
882
+ "engine-usage",
883
+ "opening-prep",
884
+ "prep-files",
885
+ "pgn-authoring",
886
+ "summary-authoring",
887
+ "examples/italian-fried-liver-overview",
888
+ "examples/najdorf-6-f4-repertoire",
889
+ ],
890
+ },
891
+ description: "Names of docs to fetch. See the tool description for the full list + when-to-call hints.",
892
+ },
893
+ },
894
+ required: ["docs"],
895
+ },
868
896
  },
869
897
  {
870
898
  name: "prep_snapshot",
package/dist/warnings.js CHANGED
@@ -54,9 +54,18 @@ export function commentAntiPatterns(comment) {
54
54
  }
55
55
  // Raw centipawn in prose: "+0.35", "-0.20", "+80" (not preceded by move
56
56
  // number). Also "at depth N" or "N nodes" — engine metadata as prose.
57
+ //
58
+ // ceoEval itself is NOT rendered — it's LLM-internal state. But raw cp
59
+ // values in prose are still bad for a different reason: they're opaque
60
+ // decoration. "≈-60" gives the reader no chess signal without knowing
61
+ // the unit (centipawns? spread rank? pawns?) and no scale (is -60
62
+ // slight, meaningful, or losing?). The NAG glyph IS visible and is
63
+ // the intended channel for that judgment — one ⩱ conveys what "-60"
64
+ // fails to convey. Engine metadata like "at depth 24" or "259M nodes"
65
+ // is even weaker: it's process detail, not a claim about the position.
57
66
  if (/(?:^|[^\d.])[+-]\d\.\d\d(?!\d)/.test(comment) || /≈\s*[+\-−]?\d{2,3}\b/.test(comment) ||
58
67
  /\bat depth \d+\b/i.test(comment) || /\b\d{2,3}M nodes\b/.test(comment)) {
59
- warns.push("comment contains raw centipawn values or engine metadata — the app renders ceoEval + NAG glyph next to every node, so these numbers are doubled noise AND opaque (readers can't tell if ≈-60 means eval, spread, or something else). Set the NAG (set_nags) and let the glyph carry the judgment; drop the number from the prose.");
68
+ warns.push("comment contains raw centipawn values or engine metadata — these are opaque to the reader (no clear unit or scale) and the intended channel for the position-quality signal is the NAG glyph, which IS rendered. Set the NAG (set_nags) at the variation's endpoint and let the glyph carry the judgment; drop the number and any \"at depth N\" / \"N nodes\" fragments from the prose.");
60
69
  }
61
70
  // Roster: "N GM games" pattern
62
71
  if (/\b\d{2,4}\s+GM games\b/i.test(comment)) {
@@ -101,6 +110,40 @@ export function noDescribeWarning(node, comment) {
101
110
  noDescribeWarned.add(node.id);
102
111
  return `substantive comment (${comment.length} chars) on a node whose position was never grounded via describe_position this session (id=${node.id}, ${node.san}). LLMs invent captures, miscount pieces, and swap files/ranks when reading FEN strings — describe_position is a pure-computation pass (~1 ms, no engine cost, structural facts + Stockfish's per-term eval breakdown) that reliably prevents this class of hallucination. In live audits, prose accuracy jumps sharply on nodes where describe_position was called first. Call describe_position with file_id+node_id=${node.id} BEFORE writing prose. Warned once per node.`;
103
112
  }
113
+ // Position NAGs on intermediate moves ("everything is ⩲" spam).
114
+ //
115
+ // Position NAGs — `$10` = / `$11` = / `$13` ∞ / `$14` ⩲ / `$15` ⩱ /
116
+ // `$16` ± / `$17` ∓ / `$18` +− / `$19` −+ — are visible glyphs on the
117
+ // move. They belong at variation ENDPOINTS: the reader plays through
118
+ // a line and, at the end, wants to know "so where did we land?"
119
+ // Tagging every mainline move with `$14` (routine slight White edge)
120
+ // turns the movetext into a wall of ⩲ symbols the reader skims past;
121
+ // it also pre-empts the walk-through by hard-coding the verdict at
122
+ // every step. The single leaf NAG carries the same information with
123
+ // none of the noise.
124
+ //
125
+ // Real-world case (Ruy Lopez Bc5 file, 2026-07-30): `$14` set on ~15
126
+ // mainline nodes plus 10+ intermediate move-choice nodes. Nothing
127
+ // signaled where the variation actually converged.
128
+ //
129
+ // Rule this warning encodes: position NAGs on non-leaf nodes are
130
+ // almost always noise. Move-quality NAGs — `$1` !, `$2` ?, `$3` !!,
131
+ // `$4` ??, `$5` !?, `$6` ?!, and `$146` novelty — are FINE at any
132
+ // depth because they're statements about the MOVE, not the resulting
133
+ // position. Warned once per node.
134
+ const positionalNagWarned = new Set();
135
+ export function positionalNagOnIntermediateWarning(node, nags) {
136
+ const positional = new Set(["$10", "$11", "$12", "$13", "$14", "$15", "$16", "$17", "$18", "$19"]);
137
+ const hit = nags.filter(n => positional.has(n));
138
+ if (hit.length === 0)
139
+ return undefined;
140
+ if (node.children.length === 0)
141
+ return undefined; // leaf — legitimate placement
142
+ if (positionalNagWarned.has(node.id))
143
+ return undefined;
144
+ positionalNagWarned.add(node.id);
145
+ return `positional NAG ${hit.join(" ")} set on a non-leaf node (id=${node.id}, ${node.san}, ${node.children.length} children). Position NAGs (=, ⩲, ±, +−) belong at variation ENDPOINTS — the reader plays through the line and at the leaf wants to know how it lands. Marking every intermediate move with the same ⩲ turns the movetext into visual noise the eye skims past AND pre-empts the walk-through. Either move this NAG to the leaf, drop it entirely, or leave it only if THIS specific move is the one that tipped the balance (rare, and worth prose). Move-quality NAGs on intermediate moves — !, ?, !?, ?!, $146 novelty — are FINE, because they're statements about the move not the resulting position. Warned once per node.`;
146
+ }
104
147
  // Compute the "you never DB-checked this parent" warning. Called from
105
148
  // add_move / add_line handlers with the parent node. Returns undefined
106
149
  // when either (a) the parent was checked this session (or is root — the
@@ -40,7 +40,9 @@ All mutations **auto-save** with optimistic locking. Response includes the new `
40
40
 
41
41
  ## Typical build order
42
42
 
43
- 0. **`read_example_prep_files`** — do this once per session, before writing any prose. Not optional. Log analysis shows most sessions skip this and produce documented anti-patterns (long PVs in prose, restating what the app renders, verbose citations). Reading the two bundled reference files once inoculates the LLM against those.
43
+ 0a. **Cloud engine running?** Call `list_cloud_engines` first. Every substantive step below needs Stockfish + Lc0 to be reachable — `cloud_analyse` at critical positions, `auto_evaluate` for the whole tree, engine-derived NAGs at endpoints, describe_position's Stockfish eval-terms breakdown. If the caller has zero running combos: STOP, tell the user prep needs an engine, list options via `list_cloud_machine_options`, get the SKU + explicit confirmation (real money per second), then `start_cloud_engine`. If they already have one, note the contract_id and continue. Never silently write prep without engines — the file ends up with placeholder NAGs the user has no way to distinguish from real ones.
44
+
45
+ 0b. **`read_docs({ docs: ["pgn-authoring", "examples/najdorf-6-f4-repertoire"] })`** — do this once per session, before writing any prose. Not optional. Log analysis shows most sessions skip the example files and produce documented anti-patterns (long PVs in prose, restating what the app renders, verbose citations). Reading the reference PGN once inoculates the LLM against those.
44
46
  1. `read_prep_file` — see what's there. Every node has an `id` you'll pass to the mutation and engine/DB tools. Use `view: "compact"` (default) plus `node_id` + `max_depth` to scope; the full tree of a 500+-node file can blow the token limit.
45
47
  2. `apply_mutations([...])` — one call with your whole intended build (a mix of `add_move` / `add_line` for structure, 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).
46
48
  3. `auto_evaluate(id)` — spawns a background job that PERSISTS 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. Grab the returned `job_id` and either (a) poll `auto_evaluate_status(job_id)` every ~10-30s until done, or (b) fire and do useful work meanwhile (write more of the tree, walk the opponent's repertoire) and check back later — engine walk-time serialises on the per-combo semaphore, so it takes roughly `target_count × movetime_ms` in wall time.
@@ -214,6 +216,10 @@ This is the single biggest quality problem in current LLM output on this system:
214
216
 
215
217
  The failure mode you're avoiding: writing prep that reads as if the opponent will helpfully play the engine's #1 preference at every ply. They won't; that's the whole point of prep.
216
218
 
219
+ **"Main tabiya" (or "main line", "main try", "Main choice") is the START of analysis, not the end.** Live case (Ruy Lopez Bc5 file, 2026-07-30): the author tagged a position "Main tabiya" and stopped there — despite the DB showing many high-level games with divergent replies from that exact position. If a position is important enough to CALL a tabiya, it's important enough to fully cover: every reply at ~15%+ frequency, or every distinct plan, gets its own branch. Writing "Main tabiya" and leaving it with one continuation is the shape of "I skimmed the DB and picked one" — the exact anti-pattern this system is meant to prevent. Rule of thumb: if you named a node "tabiya" / "main try" / "critical" in prose, the number of children beneath it should be at least the number of DB moves you named as popular.
220
+
221
+ **Cover N alternatives when the DB shows N.** From the same live file: at one move-8 position, `get_position_stats` showed three roughly-equally-played tries; the LLM added exactly one branch. That's not prep, that's a hint. If frequencies were 40 / 30 / 25 / 5, the first three get branches (the 5% one gets skipped or a one-line dismissal); if the top four are all 20-25%, all four get branches. The heuristic isn't "pick the top" — it's "cover what the opponent might actually play." When ceoEvals across the top candidates sit within 0.15 of each other, that's not one Best Move, that's a menu — treat it as one.
222
+
217
223
  ## Course chapters describe COVERAGE, not consensus — always `get_position_stats` at mainline branch points
218
224
 
219
225
  Concrete failure this rule was written to fix: a Modern Defence file made 6.O-O-O the mainline of the entire "5.Qd2 Nd7" branch, wrote a chapter around it, and analysed 15+ plies deep. `get_position_stats` at that position was NEVER called this session. Instead, the LLM ran `find_position_in_courses`, saw So / Kraai / Mihajlov all had chapters titled "5.Qd2 b5 6.O-O-O Bb7" — and cargo-culted that into "6.O-O-O is the main line". It isn't. 6.O-O-O is one of five White tries, and it wasn't even the most-played.
@@ -292,6 +298,12 @@ Where NAGs actually earn their place:
292
298
 
293
299
  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.
294
300
 
301
+ **Position NAGs (`$10`-`$19`) belong at variation ENDPOINTS, not on every intermediate move.** This is the rule the Ruy Lopez Bc5 file (2026-07-30) failed: `$14` set on ~15 mainline nodes plus 10+ intermediate move-choice nodes. Result: a wall of ⩲ symbols throughout the movetext, with nothing signalling where the line actually converges.
302
+
303
+ A variation is walkable. The reader plays through the moves and, at the LEAF, wants to know "so where did we land?" One `$14` at the endpoint answers that. Twenty `$14`s along the way don't say more — they say less, because the reader's eye skims past them and the endpoint no longer stands out.
304
+
305
+ Concrete rule: **position NAGs (`$10` = / `$13` ∞ / `$14` ⩲ / `$15` ⩱ / `$16` ± / `$17` ∓ / `$18` +− / `$19` −+) on non-leaf nodes trigger a warning.** Either move the NAG to the leaf, drop it, or leave it only if THIS specific move is the one that TIPPED the balance (rare, and worth prose naming the shift). Move-quality NAGs (`$1` ! / `$2` ? / `$3` !! / `$4` ?? / `$5` !? / `$6` ?! / `$146` novelty) on intermediate moves are FINE at any depth — those are statements about the move, not the resulting position.
306
+
295
307
  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.
296
308
 
297
309
  **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_prep_position, prep_snapshot) also auto-attach a live `eval` at the request position when a cloud engine is running.
@@ -0,0 +1,97 @@
1
+ # Summary prep files — the 15-minute-read shape
2
+
3
+ **Read this before writing any prep file the user calls a "summary" — or when they want prep they'll actually READ, not just consult.**
4
+
5
+ ## What a summary file is
6
+
7
+ A summary file is a prep artifact optimized for one thing: **the reader can walk through it in ~15 minutes and come out knowing why the mainline is the mainline, where the novelty is, and what the plan is against each of the top few responses.**
8
+
9
+ The reference is Peter Heine Nielsen's two-file convention from Magnus Carlsen's World Championship prep. Same repertoire, two files:
10
+
11
+ - **Big / reference file** — exhaustive; every branch, engine PVs pasted at key tabiyas, minimal prose. Purpose: the coach can OPEN it at any position and see everything the engines said. Sometimes 200+ nodes deep.
12
+ - **Summary file** — mainline + top branches only, dense prose at every decision, NAGs at endpoints, novelty and move-order tricks called out explicitly. Purpose: **the player can INTERNALIZE it in 15 minutes before the round.**
13
+
14
+ You may or may not have a "big" version to work from — sometimes prep just is what it is. Don't reference a source file or add a `SourceFile` tag; the summary stands alone. It's defined by its SHAPE, not by having a companion.
15
+
16
+ ## Summaries are color-oriented
17
+
18
+ **A summary is always FOR ONE SIDE.** The reader is about to sit down and play a specific colour in a specific round; the file is a set of instructions for them. Ask which colour before writing anything; do not build a two-sided summary "for reference." A two-sided document is a study, not a summary.
19
+
20
+ Once the colour is committed, everything downstream follows from it:
21
+
22
+ - **Name it.** The [Event] tag says the colour: `"Modern Defence — Tiger ...a6/...b5 — Black (Summary)"`, `"Petroff 6.Bd3 Bd6 — White (Summary)"`. Reader picks it out of a list of files immediately.
23
+ - **Mainline is YOUR moves.** Every mainline choice at a node where you're to move is a *recommendation you're making* to the reader. The mainline at a node where the *opponent* is to move is the reply you're preparing them to face.
24
+ - **"Cover the alternatives" means opponent alternatives, not yours.** At a node where you're to move, you commit to ONE move — that's what makes it a repertoire. Sidelines for your side belong in the big file, not the summary. At a node where the opponent is to move, cover the 2-3 replies they're actually likely to play. This is the axis where "cover N when the DB shows N" (from `pgn-authoring`) matters most.
25
+ - **Voice is second-person or first-person plural.** "You'll get a slight edge here", "Our knight is better than his bishop", "Meet ...Bg4 with h3 first" — not the neutral "White's position is preferable." The reader is about to be one specific side; write to them.
26
+ - **Endpoint NAGs are from YOUR side's POV.** `$14` (⩲) is fine on a White-side summary at an endpoint White is happy with. On a Black-side summary that same objective position wants `$15` (⩱) or `$10` (=) instead — "objectively White is slightly better" is depressing prep if the reader is Black; either the line shouldn't be in the summary at all, or the endpoint NAG communicates from the reader's chair. See `pgn-authoring` on positional NAG placement.
27
+ - **"Practical, not correspondence" bites hardest here.** Objective evals matter for whether a line is defensible; PRACTICAL evals (Lc0, `predict_human_move`, "what will the opponent actually play?") matter for which of several defensible replies the opponent will actually reach for. Use both, but weigh the practical one when it disagrees on which line the SUMMARY should follow.
28
+ - **Move-order tricks are asymmetric.** Some tricks work for you; some work against you. Name the ones that work FOR the reader's colour ("play ...a6 first because ...Nge7 first walks into d5") — the mirror-image tricks for the opposite side aren't the reader's problem right now.
29
+
30
+ If you're offered a request that's genuinely two-sided ("summary of the Berlin for both sides"), split it into two summary files, one per side, cross-referenced only in prose. Each individual file stays color-oriented.
31
+
32
+ ## The shape
33
+
34
+ A summary has five properties. Miss any of them and it's not a summary — it's a lightweight big-file.
35
+
36
+ **1. Mainline + top branches only, not comprehensive.** If the big-file version covers all 13 replies at move 13, the summary covers the mainline plus the 2-3 the opponent actually plays. Sidelines that don't change the mainline decision get cut. The reader trusts you did the work; the summary is the *distillation*.
37
+
38
+ **2. Dense prose at every meaningful decision point.** Every branching node should have a comment. Silence in a summary is a failure — if you left a node uncommented, either you had nothing to say (drop the node) or you had something to say (write it). The coach's voice is judgment, not exhaustive analysis.
39
+
40
+ **3. NAGs at variation endpoints.** Every terminal leaf gets a positional NAG signalling how the line LANDS: `$14` ⩲ (slight white edge), `$16` ± (clear white edge), `$10` = (equal), `$15`/`$17` (same for Black), `$18`/`$19` (winning). The reader plays through the moves, hits the leaf, sees the assessment. Do NOT put `$14` on every intermediate move — that turns the movetext into a wall of ⩲ symbols and the endpoint no longer stands out. See `pgn-authoring` for the full rule.
41
+
42
+ **4. Novelties and move-order tricks called out explicitly.** If there's a `$146` (novelty) in the file, it gets `$5` + a prose sentence naming the IDEA (not the analysis — the idea): `{novelty, for now threatening Qxb7 and getting out of ...Nb4 stuff.}`. If a move order matters (choosing a-then-b over b-then-a preempts a defensive resource), name that: `{This move order is critical — 12.Nc3 first avoids ...Bg4 pinning ideas after 12.Nbd2.}`
43
+
44
+ **5. Arrows and highlights as thinking-aids.** `[%csl Re4]` (red highlight on e4) to mark the target square. `[%cal Gh4g2,Gc1f4]` (green arrows) to mark a planning sequence. These load into the reader's spatial memory in a way SANs don't. Use them at critical positions — the tabiya where the plan crystallizes, the mating attack, the endgame breakthrough. Don't decorate: one arrow that shows the plan beats three that show every idea you considered.
45
+
46
+ ## The coach's voice (what to write in comments)
47
+
48
+ Summary prose is decisions in a coach's voice. Not analysis. Not restating moves. Judgment + plan + character.
49
+
50
+ Real examples from a Peter Heine / jvanf summary (Petroff 6.Bd3 Bd6, 2021 WC prep):
51
+
52
+ - `{In general this seems to be leading to positions which are just more pleasant for White. Black has only two reasonable moves and both will be met by Qb3.}` — Frames the whole file's thesis in two sentences.
53
+ - `{Stopping Nb4. Black has several attempts, but White play remains simple. Be3-Rac1 and then we see.}` — Names the concrete threat + the plan.
54
+ - `{This seems to be Blacks best bet at forcing this to a draw.}` — Signals objective assessment.
55
+ - `{Kinda forcing up to here, now comes the new idea.}` — Marks transition to novelty.
56
+ - `{quite nasty for black.}` — Endpoint verdict.
57
+ - `{Black has run out of tricks and has to either weaken with f5 or give us a small advantage.}` — Squeezing the reader into the right mental model of the position.
58
+
59
+ Patterns to notice:
60
+ - **Short.** Most comments are one sentence. Two if the plan needs naming after the assessment.
61
+ - **Concrete.** "Be3-Rac1 and then we see" beats "White continues developing" every time.
62
+ - **Judgmental.** "This seems to be Blacks best bet" tells the reader something they can't derive from the moves.
63
+ - **Framing.** The FIRST comment in the file often frames the whole argument ("positions which are just more pleasant for White"). The last comment on each variation is the verdict ("quite nasty for black", "lasting iniative").
64
+
65
+ ## What NOT to do
66
+
67
+ - **Don't paste engine PVs.** No `{Stockfish 050821: 10.Bxf5 Nxf5 11.Nf1 ...}` 30-move-long variations. That's the big file's job. If the summary NEEDS a 15-move engine line to justify a claim, the claim probably belongs in the big file.
68
+ - **Don't cover every alternative.** If your `add_line` warning fires because the DB shows 4 tries and you covered only 2 — that's fine here IF you named which 2 you dropped and why. But typically the summary picks the ~3 the opponent actually plays.
69
+ - **Don't leave nodes uncommented.** A node with no comment in a summary is a node that shouldn't exist. Either it's important enough to comment or it's noise.
70
+ - **Don't restate what moves show.** `{After 10.Bxf5 Black plays Nxf5}` — the reader can see that. Say WHY: `{Bxf5 forces the trade; recapturing with the knight rather than the pawn keeps c6 flexible for later ...Nc7.}`
71
+ - **Don't spam NAGs.** Positional NAGs (`$10`-`$19`) at endpoints ONLY. Move-quality NAGs (`$1` ! / `$2` ? / `$5` !? / `$6` ?! / `$146` novelty) at any depth, but sparingly — one per branch on average.
72
+
73
+ ## The "does this pass" test
74
+
75
+ Before saving the summary, ask: **could Magnus (or your user) play through this in 15 minutes and come out knowing what to do at the board?**
76
+
77
+ Three concrete checks:
78
+ 1. **Frame the file** — does the first comment explain what this repertoire is trying to achieve?
79
+ 2. **Every decision has a voice** — walk the mainline. At each branching node, is there a comment explaining the choice or the character?
80
+ 3. **Every leaf has a verdict** — walk to every terminal position. Is there a NAG (or prose) telling the reader how the line ends?
81
+
82
+ If yes to all three, ship it. If no, either add what's missing or CUT the branch until the file is what a summary should be — shorter and denser, not longer.
83
+
84
+ ## Build order for a summary file
85
+
86
+ The same tools as any prep file, but different order and different density.
87
+
88
+ 0. **Cloud engine running?** A summary looks light but requires SHARPER analysis than a big file — every endpoint NAG has to be right, and there are few enough of them that a wrong one stands out. Call `list_cloud_engines` first. Zero combos running → STOP: tell the user a summary needs engines, list options via `list_cloud_machine_options`, get their SKU + explicit confirmation (real money per second), then `start_cloud_engine`. Do NOT build a summary from cached / guessed evals — the point of the summary is trust, and a placeholder `$14` at an endpoint the reader will internalize is worse than no summary.
89
+ 0.5. **Which colour is the reader playing?** Ask if you don't know. This decides everything downstream — the mainline is their moves, the branches are their opponent's replies, the endpoint NAGs are judged from their POV, the voice is written to them. See the "Summaries are color-oriented" section above.
90
+ 1. `create_prep_file(collection_id, name)` — name it with the colour AND "Summary" in the Event tag (`"Modern Defence — Tiger ...a6/...b5 — Black (Summary)"`, `"Petroff 6.Bd3 Bd6 — White (Summary)"`) so the reader picks it out of a list immediately.
91
+ 2. `set_comment(root, "framing")` — the file's thesis, first thing.
92
+ 3. Build the mainline top-down with `apply_mutations` batches — each move gets its comment in the same batch that adds it. Don't come back to write comments later; the density is the point.
93
+ 4. At each branching decision, add ONLY the branches the opponent might actually play. Skip the ones you'd cover in a big file.
94
+ 5. At every leaf, `cloud_analyse` the position (or verify a stored `ceoEval`) and set the endpoint NAG based on that measurement, not on generic knowledge of the opening.
95
+ 6. `set_annotations` on the 3-5 most important positions — the tabiya, the novelty, the critical junction. Not every position.
96
+
97
+ Total build: dozens of nodes, not hundreds. If your summary is 200+ nodes, it's probably a big file wearing a summary hat — cut ruthlessly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
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": {