@chessceo/mcp 0.30.2 → 0.31.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.
@@ -1,70 +1,142 @@
1
1
  # PGN authoring guide
2
2
 
3
- You author prep files by mutating a tree, not by writing PGN text. The MCP layer holds a parser+exporter (chessops-backed) so every mutation call load-mutates-saves atomically — you only ever address nodes, never raw text. No paren-counting, no move-numbering, no SAN typos surviving your edit.
3
+ You author prep files by mutating a tree, not by writing PGN text. The MCP layer holds a parser+exporter (chessops-backed) so every mutation call load-mutates-saves atomically — you only ever address nodes by id, never raw text or paths. No paren-counting, no move-numbering, no SAN typos surviving your edit, no index drift when a sibling is inserted.
4
4
 
5
- ## Path addressing
5
+ ## Node id addressing
6
6
 
7
- Nodes are addressed by **path**: an array of child indices from the root.
7
+ Every node has a stable **`id`** the LLM's handle on that node across mutation calls.
8
8
 
9
- - `[]` the root position (empty board state, before any move).
10
- - `[0]` the first mainline move (root's first child).
11
- - `[0, 0]` the second ply on the mainline.
12
- - `[0, 1]` a variation branching after the first move (sibling of `[0, 0]`).
13
- - `[0, 0, 0, 1]` — a variation branching at the third ply.
9
+ - Root id is `"r"`.
10
+ - Every other node id is an 8-hex-char content hash derived from `(parent.id, san)`.
11
+ - **IDs never change when other nodes are added, deleted or promoted.** Sibling insertions do not shift ids. Variation promotion does not shift ids. A node's id is a pure function of its position in the tree's SAN structure.
12
+ - **IDs are self-checking.** If you send a node_id the tree doesn't know, resolution fails immediately with a clear error the LLM can't "make up" ids and have them silently work.
14
13
 
15
- `children[0]` is always the mainline; `children[1..N]` are alternative variations in declaration order. `promote_variation` swaps this order.
14
+ Every read gives you the id on every node. Every mutation call takes `node_id` (or `parent_id` for add_move / add_line) and returns the id it landed on. You chain ids from one call to the next.
16
15
 
17
16
  ## Workflow
18
17
 
19
18
  Reading:
20
19
 
21
- - `read_prep_file(id)` → returns `{version, tags, tree}`. Every node in `tree` has `san`, `fen`, `ply`, optional `comment`, `nags`, `annotations`, and `children`.
20
+ - `read_prep_file(id)` → returns `{version, tags, tree}`. Every node has `id`, `san`, `fen`, `ply`, optional `comment`, `nags`, `annotations`, `ceoEval`, and `children`.
22
21
 
23
22
  Building (use these — one call for many ops):
24
23
 
25
24
  - **`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 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.
25
+ - **`add_line(id, parent_id, sans[])`** — a whole linear variation in one op. Cleaner than N `add_move` ops for straight lines. Overlapping prefixes with an existing branch **join** (same SAN from the same position IS the same move) — see below.
26
+ - **`auto_evaluate(id, node_id?)`** — walk from a node and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every descendant via cloud_analyse. **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. 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
 
30
- - `add_move(id, path, san)` — appends a new child. If `path` has children, new node becomes a variation.
31
- - `set_comment(id, path, comment)` — replace comment (empty string clears).
32
- - `set_nags(id, path, nags)` — replace NAGs (empty array clears).
33
- - `set_annotations(id, path, {arrows, highlights})` — replace visual annotations.
34
- - `delete_subtree(id, path)` — remove node + descendants.
35
- - `promote_variation(id, path)` — make the node at `path` its parent's mainline.
30
+ - `add_move(id, parent_id, san)` — appends a new child. If the parent has children, new node becomes a variation.
31
+ - `add_line(id, parent_id, sans[])` — appends a whole linear continuation as a sequence of children.
32
+ - `set_comment(id, node_id, comment)` — replace comment (empty string clears).
33
+ - `set_nags(id, node_id, nags)` — replace NAGs (empty array clears).
34
+ - `set_annotations(id, node_id, {arrows, highlights})` — replace visual annotations.
35
+ - `delete_subtree(id, node_id)` — remove node + descendants.
36
+ - `promote_variation(id, node_id)` — make the referenced node its parent's mainline.
36
37
  - `set_tag(id, key, value)` — set/clear a game-level PGN tag.
37
38
 
38
39
  All mutations **auto-save** with optimistic locking. Response includes the new `version`; pass it as `expected_version` on your next call to catch concurrent edits.
39
40
 
40
41
  ## Typical build order
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, plus any `set_nags` where you already have a clear judgment — novelty `$146`, `!?` speculative sac, obvious `?` blunder in a sideline you're rejecting).
43
+ 1. `read_prep_file` — see what's there. Every node has an `id` you'll pass to the mutation and engine/DB tools.
44
+ 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).
44
45
  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
46
  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
47
 
47
48
  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
49
 
49
- ### Path math in a batch
50
+ ### Chaining node_ids across a batch
50
51
 
51
- Paths in each mutation resolve against the tree AFTER previous ops in the batch. When you `add_move(path=[0], san='c5')`, the new c5 lands at `[0, 0]` if [0] had no children, or `[0, N]` if it had N. Your next mutation in the batch can address the new node directly. The tree is deterministic you can compute all paths ahead of time.
52
+ Node ids are content-derived, so when you `add_move` inside a batch, the id of the new node is a deterministic function of the parent's id + the SAN. In practice: **use the id the previous op returned as the `parent_id` of the next op** (the batch response gives you each `node_id` in order, and `add_line` gives you the full `line: [{node_id, san}, …]`).
52
53
 
53
- For a linear mainline build from an empty file:
54
+ Concretely, the response to a batch is:
54
55
  ```
55
- {op: "add_move", path: [], san: "e4"} // lands at [0]
56
- {op: "add_move", path: [0], san: "c5"} // lands at [0, 0]
57
- {op: "add_move", path: [0, 0], san: "Nf3"} // lands at [0, 0, 0]
58
- {op: "add_move", path: [0, 0, 0], san: "d6"} // lands at [0, 0, 0, 0]
59
- ...
56
+ { ok: true, results: [{node_id: "3c7f592e"}, {node_id: "22012be0"}, ...], version: 42 }
60
57
  ```
61
58
 
62
- For a variation at some point:
59
+ If you know the tree ahead of time (writing from scratch), the deterministic recipe means you don't have to guess. But by far the easiest pattern is **`add_line` for straight lines, `add_move` for branch points** — you don't need to compute ids yourself.
60
+
61
+ ### Two clean batch patterns
62
+
63
+ **Straight mainline:**
64
+ ```
65
+ {op: "add_line", parent_id: "r", sans: ["e4","c5","Nf3","Nc6","Bb5"]}
66
+ // → returns { node_id: "<id-of-Bb5>", line: [{node_id, san}, ...] }
67
+ ```
68
+
69
+ **Mainline + variation at a branch point:**
63
70
  ```
64
- {op: "add_move", path: [0, 0], san: "Nc3"} // variation branching after move 2; lands at [0, 0, 1]
65
- {op: "add_move", path: [0, 0, 1], san: "d5"} // continue the variation; lands at [0, 0, 1, 0]
71
+ // First, the mainline:
72
+ {op: "add_line", parent_id: "r", sans: ["e4","c5","Nf3","d6"]}
73
+ // → line[2] is the Nf3 node — grab its node_id, call it NF3.
74
+
75
+ // Then the variation branching after Nf3:
76
+ {op: "add_line", parent_id: NF3, sans: ["Nc6","Bb5","Bd7"]}
66
77
  ```
67
78
 
79
+ ### add_move / add_line join on existing SAN
80
+
81
+ `add_move` and `add_line` are **idempotent on SAN**: if a child with that SAN already exists under the parent, they return the existing child's id rather than creating a duplicate. In chess, same SAN from the same position IS the same move — two sibling children with SAN "d5" would be nonsensical.
82
+
83
+ This means you can freely send `add_line` batches that share a prefix — they build a clean Y-shape at the divergence point, not duplicate spines.
84
+
85
+ ```
86
+ // Two Ruy Lopez lines that share the first 5 plies, diverging at Black's 3rd move:
87
+ {op: "add_line", parent_id: "r", sans: ["e4","e5","Nf3","Nc6","Bb5","Nf6","O-O"]} // Berlin
88
+ {op: "add_line", parent_id: "r", sans: ["e4","e5","Nf3","Nc6","Bb5","a6","Ba4"]} // Steinitz
89
+
90
+ // Result: single spine e4→e5→Nf3→Nc6→Bb5, then Bb5 has TWO children (Nf6, a6).
91
+ // Both add_line responses report every id on the resulting line, including
92
+ // the joined-prefix nodes, so you can address any of them next.
93
+ ```
94
+
95
+ **Consequence for building repertoires**: you don't have to hand-decompose overlapping variations into a mainline + variation pair. Just send each variation as its own `add_line` from the same starting `parent_id`; the tree will de-dup the shared prefix automatically. Use `promote_variation` afterwards to pick which continuation is the mainline at the branch.
96
+
97
+ ## What good looks like
98
+
99
+ Before writing your first substantial file, call `read_example_prep_files` — it returns two reference PGNs by a strong human coach. Read the full files; the rhythm and comment density are hard to convey in isolated snippets. What follows is a taste, not a substitute.
100
+
101
+ **From an Italian Fried Liver overview (both sides, 1600+ audience):**
102
+
103
+ ```
104
+ 5. Nxf7 {Most principled. Interesting is that not long ago computers were saying this
105
+ is winning for white. But modern strong engines almost immediately proclaim 0.00}
106
+ Bxf2+ 6. Kf1 Qe7 7. Nxh8 d5 8. exd5 Nd4
107
+ 9. d6 $1
108
+ (9. c3 {Most played, and weak engines still like this move, but fail to spot}
109
+ Bg4 10. Qa4+ Nd7 $1 {And black is winning} 11. Kxf2 Qh4+)
110
+ 9... Qxd6 (9... cxd6 $2 10. Kxf2 Ng4+ 11. Kg1 $18 {and the d6 pawn blocks Qc5})
111
+ ```
112
+
113
+ Note the register: *"weak engines still like this move, but fail to spot"* — one sentence orients the reader and cites the failure mode. `$1` marks the resource, `$2` marks the trap, `$18` marks the resulting winning position — every glyph earns its place. No `{Stockfish gives 0.00 depth 22}` verbose engine chatter.
114
+
115
+ **From a Najdorf 6.f4 White repertoire (2200+ audience):**
116
+
117
+ ```
118
+ 6... e6 {a common continuation, but usually in the Najdorf, e6 setups give white
119
+ enough time to put their pieces in good positions, and here its no exception.}
120
+ 7. Qf3 {And black is already in danger. White just plans to finish development with
121
+ Be3, long castle and g4.}
122
+ (7. g4 {is too early:} e5 $1 8. Nf5 Nc6 {[%cal Gg7g6]})
123
+ 7... Qc7
124
+ 8. g4 {White has easy play}
125
+ Nc6 9. Nxc6 bxc6 10. g5 Nd7 11. Bd2 $16 {[%cal Ge1c1,Gh2h4]}
126
+ ```
127
+
128
+ Two `[%cal]` arrows on the last move show the whole plan (castle long, push h4). *"White has easy play"* is a five-word verdict that beats three sentences of hedged eval prose. The parenthesised sideline shows exactly WHY 7.g4 is premature — one variation, one point.
129
+
130
+ **What both files share:**
131
+
132
+ - Named citations: Karpov–Beliavsky, Nepomniachtchi, Mamedov–Volokitin, John Ludvig Hammer 2024. Concrete anchors the reader can look up.
133
+ - Practical framing: *"objectively black has no compensation for the pawn, it does seem black still has some practical chances"* — separates objective from practical explicitly.
134
+ - Move-order guidance in the reader's voice: *"Here it's easy for white to mess up the move order. If black wants to go exd4, you should be ready — d3 is a way to remember that white should castle first."*
135
+ - Terse verdicts at line ends: *"White is a clean pawn up."* *"black is cooked"* *"All correspondence games here ended in a draw."* *"With a very unclear position."*
136
+ - Zero decorative annotation: `[%csl Rf7]` on the Fried Liver f7 attack is one square, one colour, all signal.
137
+
138
+ Study `read_example_prep_files` before writing at scale. The gap between "correct" prep and "prep worth reading" is entirely commentary discipline, and these two files are the target.
139
+
68
140
  ## Variations vs prose
69
141
 
70
142
  **Variations are moves, not sentences.** This was the biggest failure mode of raw-PGN authoring — LLMs writing "if Black plays Be6 White responds with f3" as prose. In the tree model there's no such temptation: variations are `add_move` calls at the branching node.
@@ -6,15 +6,21 @@ You can save chess prep to the user's chess.ceo account and read it back across
6
6
 
7
7
  The user has **one** collection dedicated to your work — labelled "AI Prep" in their chess.ceo app with a 🤖 icon. Inside it, each **prep file** is one PGN game with variations. You never see the collection itself; the tools operate directly on the files inside it.
8
8
 
9
- Six tools:
9
+ File-level tools:
10
10
 
11
11
  - `list_prep_files` — show me all my prep files
12
12
  - `search_prep_files(query)` — find by opponent name / opening keyword
13
- - `read_prep_file(id)` — full PGN + parsed mainline tree + `version`
14
- - `create_prep_file(name, pgn?)` — new file, `name` becomes the [Event] tag
15
- - `save_prep_file(id, pgn, expected_version)` — replace the whole PGN
13
+ - `read_prep_file(id)` — parsed tree (every node carries a stable `id`) + tags + `version`
14
+ - `create_prep_file(name)` — new empty file, `name` becomes the [Event] tag
16
15
  - `delete_prep_file(id)` — soft delete (user can restore from app)
17
16
 
17
+ Mutation tools (edit an existing file — you never touch raw PGN):
18
+
19
+ - `apply_mutations(id, [...])` — batch: N ops in one save. **Primary build tool.**
20
+ - `add_move`, `add_line`, `set_comment`, `set_nags`, `set_annotations`, `delete_subtree`, `promote_variation`, `set_tag` — individual ops for surgical follow-ups. See `read_pgn_authoring_guide` for the full mutation vocabulary.
21
+
22
+ Every mutation call takes a `node_id` (or `parent_id` for add-style ops) and auto-saves. Nodes are addressed by stable content-derived id, not by path — sibling insertions / deletions / promotions never shift ids. Root id is `"r"`.
23
+
18
24
  ## The single most important habit
19
25
 
20
26
  **Before creating a new file, search for an existing one.** LLMs make three "Prep vs Firouzja" files in a row all the time. Always:
@@ -40,23 +46,22 @@ Full details, NAG table, arrow/highlight syntax, and worked example: `read_pgn_a
40
46
 
41
47
  ## Editing without breaking the tree
42
48
 
43
- You get the whole PGN back from `read_prep_file`, edit it in your head, send back via `save_prep_file`. There's no partial-patch API small edits still resend the full text. The file is small (repertoire = maybe 5-20 KB), that's fine.
49
+ You never send raw PGN. `read_prep_file` gives you the parsed tree with stable node ids; the mutation tools accept those ids and handle the parse/edit/serialize cycle for you. SAN is validated per mutation, move numbers are automatic, parenthesised variations are impossible to leave unbalanced.
44
50
 
45
- Common LLM failure modes to catch yourself doing:
51
+ Failure modes the tool layer now blocks (that used to trip LLMs writing raw PGN):
46
52
 
47
- - **Unbalanced parentheses.** Every `(` needs a matching `)`. Count them if you added variations. The backend parses on save; if invalid, you get 400 with the parser error and have to retry.
48
- - **Bad SAN.** `Nfd7` where you meant `Nbd7`. Always trace the position in your head (or use the tree from read_prep_file's response) before writing a move.
49
- - **Forgotten move numbers.** `1. e4 c5 2. Nf3` after each White move you need `<num>.`, after each Black move the number continues implicitly until the next full move. In variations at Black's move, PGN wants `2... Nc6`.
50
- - **Nested variations losing context.** `1.e4 e5 (1...c5 (2.Nf3 d6))` the inner variation branches at `2.Nf3` off the c5 sideline, not off the mainline. This gets confusing fast; two levels is usually enough.
51
- - **Blank Event tag** — the user's list_prep_files response shows the Event tag as the name. Empty tag = "Untitled" everywhere.
53
+ - **Unbalanced parentheses** no longer possible; you address nodes, not text.
54
+ - **Bad SAN** — every `add_move` / `add_line` validates against the parent's FEN. Illegal moves are rejected with the position's FEN in the error so you can call `describe_position` to see legal moves.
55
+ - **Forgotten move numbers** — the exporter reconstructs them.
56
+ - **Nested variations losing context** `add_move(parent_id=X, san=…)` binds unambiguously to node X.
52
57
 
53
58
  ## Optimistic locking
54
59
 
55
- `read_prep_file` returns a `version` integer. Pass it back as `expected_version` on `save_prep_file`. If someone else (the user in the app, or a parallel agent session) updated the file since you read it, save returns `409 Conflict` with the current version. You should:
60
+ `read_prep_file` returns a `version` integer. Pass it back as `expected_version` on any mutation. If someone else (the user in the app, or a parallel agent session) updated the file since you read it, the save returns `409 Conflict` with the current version. You should:
56
61
 
57
- 1. Re-read the file to see what changed.
58
- 2. Merge your edits with theirs.
59
- 3. Retry the save with the new version.
62
+ 1. Re-read the file to see what changed (node ids are stable across the concurrent edit, so your local references may still work).
63
+ 2. Decide whether to merge or override.
64
+ 3. Retry with the new version.
60
65
 
61
66
  If you don't pass `expected_version`, it's last-write-wins — you might silently overwrite the user's manual edits. Only skip it when you know the file is untouched (e.g. you just created it).
62
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.30.2",
3
+ "version": "0.31.2",
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": {