@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.
- package/dist/index.js +322 -149
- package/dist/pgn/mutations.js +80 -27
- package/dist/pgn/parser.js +9 -2
- package/dist/pgn/paths.js +77 -10
- package/dist/pgn/types.js +17 -6
- package/docs/engine-usage.md +10 -0
- package/docs/examples/italian-fried-liver.pgn +12 -0
- package/docs/examples/najdorf-6-f4-white.pgn +18 -0
- package/docs/pgn-authoring.md +102 -30
- package/docs/prep-files-guide.md +20 -15
- package/package.json +1 -1
package/docs/pgn-authoring.md
CHANGED
|
@@ -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
|
-
##
|
|
5
|
+
## Node id addressing
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Every node has a stable **`id`** — the LLM's handle on that node across mutation calls.
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
- **`
|
|
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,
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
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 (
|
|
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
|
-
###
|
|
50
|
+
### Chaining node_ids across a batch
|
|
50
51
|
|
|
51
|
-
|
|
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
|
-
|
|
54
|
+
Concretely, the response to a batch is:
|
|
54
55
|
```
|
|
55
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
{op: "
|
|
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.
|
package/docs/prep-files-guide.md
CHANGED
|
@@ -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
|
-
|
|
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)` —
|
|
14
|
-
- `create_prep_file(name
|
|
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
|
|
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
|
-
|
|
51
|
+
Failure modes the tool layer now blocks (that used to trip LLMs writing raw PGN):
|
|
46
52
|
|
|
47
|
-
- **Unbalanced parentheses
|
|
48
|
-
- **Bad SAN
|
|
49
|
-
- **Forgotten move numbers
|
|
50
|
-
- **Nested variations losing context
|
|
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
|
|
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.
|
|
59
|
-
3. Retry
|
|
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