@chessceo/mcp 0.16.0 → 0.19.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/dist/index.js +203 -0
- package/docs/prep-files-guide.md +125 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ function loadBundledDoc(filename, fallbackLabel) {
|
|
|
35
35
|
}
|
|
36
36
|
const ENGINE_USAGE_DOC = loadBundledDoc("engine-usage.md", "Engine usage guide");
|
|
37
37
|
const PREP_STRATEGY_DOC = loadBundledDoc("prep-strategy.md", "Prep strategy guide");
|
|
38
|
+
const PREP_FILES_DOC = loadBundledDoc("prep-files-guide.md", "Prep files guide");
|
|
38
39
|
// ── HTTP ────────────────────────────────────────────────────────────
|
|
39
40
|
//
|
|
40
41
|
// Two auth flavours coexist:
|
|
@@ -58,6 +59,13 @@ const AUTHED_TOOLS = new Set([
|
|
|
58
59
|
"list_cloud_engines",
|
|
59
60
|
"stop_cloud_engine",
|
|
60
61
|
"cloud_analyse",
|
|
62
|
+
"list_prep_files",
|
|
63
|
+
"search_prep_files",
|
|
64
|
+
"read_prep_file",
|
|
65
|
+
"create_prep_file",
|
|
66
|
+
"save_prep_file",
|
|
67
|
+
"delete_prep_file",
|
|
68
|
+
"predict_human_move",
|
|
61
69
|
]);
|
|
62
70
|
function isAuthedToolCall(body) {
|
|
63
71
|
if (!body || typeof body !== "object")
|
|
@@ -262,6 +270,51 @@ const TOOLS = [
|
|
|
262
270
|
},
|
|
263
271
|
},
|
|
264
272
|
},
|
|
273
|
+
{
|
|
274
|
+
name: "predict_human_move",
|
|
275
|
+
description: "Predicts what a HUMAN of the given rating will most likely play from a position. Rating-conditioned neural net (ResNet-20x256) — you get the top-N most likely moves with their probabilities and a WDL value head (White POV).\n\n" +
|
|
276
|
+
"This is a completely different question from the engine tools (analyse / cloud_analyse):\n" +
|
|
277
|
+
"• Engines answer: 'what is objectively best?'\n" +
|
|
278
|
+
"• predict_human_move answers: 'what will my 2200-rated opponent actually play here?'\n\n" +
|
|
279
|
+
"Prime use cases in prep:\n" +
|
|
280
|
+
"• After you've found what the opponent SHOULD do (with the engine), check what they'll ACTUALLY do at their rating. If the top human move is a mistake, you have a real practical advantage.\n" +
|
|
281
|
+
"• The WDL head is rating-aware — a 400-point gap will show up as a big win probability even in equal positions (the model has learned that human errors compound).\n" +
|
|
282
|
+
"• Pass `prev_fen` (most recent first) when analysing mid-trade positions — without history the model treats the position as quiet.\n\n" +
|
|
283
|
+
"Position input is flexible — pass `fen`, or `moves` from startpos, or `fen + moves`. Default rating 2400 both sides. ~1-2s per call. **Premium (or admin/moderator) only** — anonymous calls get 402.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
fen: { type: "string", description: "Starting position as FEN." },
|
|
288
|
+
moves: {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "Optional SAN moves to apply on top of `fen` (or startpos).",
|
|
291
|
+
},
|
|
292
|
+
white_elo: {
|
|
293
|
+
type: "integer",
|
|
294
|
+
minimum: 100,
|
|
295
|
+
maximum: 3400,
|
|
296
|
+
description: "White's rating (default 2400).",
|
|
297
|
+
},
|
|
298
|
+
black_elo: {
|
|
299
|
+
type: "integer",
|
|
300
|
+
minimum: 100,
|
|
301
|
+
maximum: 3400,
|
|
302
|
+
description: "Black's rating (default 2400).",
|
|
303
|
+
},
|
|
304
|
+
top: {
|
|
305
|
+
type: "integer",
|
|
306
|
+
minimum: 1,
|
|
307
|
+
maximum: 20,
|
|
308
|
+
description: "Number of top predicted moves to return (default 5).",
|
|
309
|
+
},
|
|
310
|
+
prev_fens: {
|
|
311
|
+
type: "array",
|
|
312
|
+
items: { type: "string" },
|
|
313
|
+
description: "Previous FEN(s), most recent first. Optional — omit for quiet-position analysis. Useful mid-trade so the model doesn't assume the position is stable.",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
265
318
|
{
|
|
266
319
|
name: "get_head_to_head",
|
|
267
320
|
description: "Complete head-to-head record between two players. Includes overall and per-colour W/D/L (from player A's perspective), splits by time control, most-played openings between them, first / last meeting, average game length, and the game list.",
|
|
@@ -386,6 +439,84 @@ const TOOLS = [
|
|
|
386
439
|
required: ["fen"],
|
|
387
440
|
},
|
|
388
441
|
},
|
|
442
|
+
{
|
|
443
|
+
name: "list_prep_files",
|
|
444
|
+
description: "List every prep file the user has (all games inside their dedicated AI Prep collection). Returns id, PGN tags (Event, White, Black, Date, etc. — read the Event tag for the user-facing name), size, updated_at. ALWAYS call this before create_prep_file to check for existing coverage — creating a second 'Prep vs Firouzja' when one already exists is a common LLM failure. If the user has many, use search_prep_files with a query to narrow down.",
|
|
445
|
+
inputSchema: { type: "object", properties: {} },
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "search_prep_files",
|
|
449
|
+
description: "Text search over the user's prep files (matches PGN headers, comments, and content). Use this instead of list_prep_files when you know a keyword — e.g. search_prep_files(query='Firouzja') or search_prep_files(query='Najdorf').",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
query: { type: "string", description: "Free-text query (opponent name, opening name, event keyword)." },
|
|
454
|
+
},
|
|
455
|
+
required: ["query"],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "read_prep_file",
|
|
460
|
+
description: "Read one prep file. Returns the full PGN text and a compact tree JSON of the mainline (SAN + FEN per move) for programmatic reasoning. Also carries the `version` field you need to pass back to save_prep_file for the optimistic-lock check.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
id: { type: "string", description: "Prep file id, from list_prep_files or search_prep_files." },
|
|
465
|
+
},
|
|
466
|
+
required: ["id"],
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "create_prep_file",
|
|
471
|
+
description: "Create a new prep file. `name` becomes the Event PGN tag (user-facing label). Optional `pgn` seeds the file — if omitted, a minimal empty PGN is written and you can save_prep_file into it later.\n\n" +
|
|
472
|
+
"ALWAYS run list_prep_files (or search_prep_files with the opponent / opening keyword) BEFORE creating, so you don't duplicate an existing file. If a file already exists that fits, extend it via save_prep_file instead.",
|
|
473
|
+
inputSchema: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
name: {
|
|
477
|
+
type: "string",
|
|
478
|
+
description: "User-facing name for the file — becomes the [Event] tag in the PGN. Example: 'Prep vs Firouzja (Black) 2026-07-23'.",
|
|
479
|
+
},
|
|
480
|
+
pgn: {
|
|
481
|
+
type: "string",
|
|
482
|
+
description: "Optional initial PGN content. Supports full PGN with headers and parenthesised variations. If omitted, an empty PGN is created.",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
required: ["name"],
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "save_prep_file",
|
|
490
|
+
description: "Save (replace) a prep file's PGN content. Optimistic-lock via `expected_version` — pass the `version` you got from read_prep_file. If someone else (user in the app, or another agent session) updated the file since you read it, save returns 409 with the current version — re-read and merge.\n\n" +
|
|
491
|
+
"PGN structure guidance:\n" +
|
|
492
|
+
"- Keep the mainline clean (your top recommendation).\n" +
|
|
493
|
+
"- Alternative candidates go in parenthesised variations at the branching move.\n" +
|
|
494
|
+
"- Attach one-sentence commentary at key branch points using PGN {curly-brace comments}. Cite the actual tool output ('Lc0 gives +0.15 in this line') — don't dress the file up with unsourced chess prose.\n" +
|
|
495
|
+
"- Use PGN NAGs for evaluations: $1 !, $2 ?, $3 !!, $4 ??, $6 =, $10 = (equal), $14 +/= (slight edge), etc. Optional but nice.",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
id: { type: "string", description: "Prep file id." },
|
|
500
|
+
pgn: { type: "string", description: "Full replacement PGN (headers + moves)." },
|
|
501
|
+
expected_version: {
|
|
502
|
+
type: "integer",
|
|
503
|
+
description: "The `version` you got from read_prep_file. If omitted, no optimistic check runs (last-write-wins — riskier).",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
required: ["id", "pgn"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "delete_prep_file",
|
|
511
|
+
description: "Soft-delete a prep file. The user can restore it from the chess.ceo app's recycle bin if you deleted something valuable. Rare — usually you extend or replace instead.",
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
id: { type: "string", description: "Prep file id." },
|
|
516
|
+
},
|
|
517
|
+
required: ["id"],
|
|
518
|
+
},
|
|
519
|
+
},
|
|
389
520
|
{
|
|
390
521
|
name: "read_engine_usage_guide",
|
|
391
522
|
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.",
|
|
@@ -396,6 +527,11 @@ const TOOLS = [
|
|
|
396
527
|
description: "Returns the full chess.ceo prep-strategy guide: 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 use move-order tricks with the `trs` field, and how to calibrate surprise (rare secondary lines inside the existing repertoire, not big first-move switches). Call this ONCE per session before recommending an opening plan, especially when the user is preparing for a specific real opponent.",
|
|
397
528
|
inputSchema: { type: "object", properties: {} },
|
|
398
529
|
},
|
|
530
|
+
{
|
|
531
|
+
name: "read_prep_files_guide",
|
|
532
|
+
description: "Returns the full guide on how to store prep in the user's chess.ceo account via list_prep_files / read_prep_file / create_prep_file / save_prep_file / delete_prep_file. Covers: how to structure a repertoire PGN (mainline + variations + comments + NAGs), the critical 'search-before-create' habit to avoid duplicates, optimistic-locking with `version`, and how to write comments that cite tool output instead of inventing chess prose. Call this ONCE per session before your first create_prep_file / save_prep_file call.",
|
|
533
|
+
inputSchema: { type: "object", properties: {} },
|
|
534
|
+
},
|
|
399
535
|
{
|
|
400
536
|
name: "prep_snapshot",
|
|
401
537
|
description: "One call, three parallel fetches at the same position: opponent's stats on their side, your stats on your side, and the 11.7M-game general database at that position. Use this while walking the opening tree — one round trip instead of three separate calls, and you can compare the three views directly (e.g. opponent has 2 games here but the general DB has 8k → prep candidate).",
|
|
@@ -503,6 +639,24 @@ function convertCloudSnapshotResponse(raw, startFen) {
|
|
|
503
639
|
}
|
|
504
640
|
return raw;
|
|
505
641
|
}
|
|
642
|
+
function pgnToTree(pgn) {
|
|
643
|
+
try {
|
|
644
|
+
const board = new Chess();
|
|
645
|
+
// chess.js loadPgn accepts full PGN incl. tags; loadPgn returns true on
|
|
646
|
+
// success or throws on newer versions. Wrap defensively.
|
|
647
|
+
board.loadPgn(pgn);
|
|
648
|
+
// history({verbose:true}) walks the mainline only; variations aren't
|
|
649
|
+
// exposed by chess.js's built-in PGN loader at time of writing. If the
|
|
650
|
+
// PGN carries parenthesised variations they get flattened.
|
|
651
|
+
const hist = board.history({ verbose: true });
|
|
652
|
+
if (hist.length === 0)
|
|
653
|
+
return [];
|
|
654
|
+
return hist.map(h => ({ san: h.san, fen: h.after }));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
506
660
|
// Rewrite availableMoves[].move UCI → SAN. The prep + position-stats
|
|
507
661
|
// endpoints return moves in UCI on the wire — same LLM-readability
|
|
508
662
|
// concern as engine PVs, and the same wrapper-only fix. Passes the
|
|
@@ -635,6 +789,24 @@ async function callToolInner(name, args) {
|
|
|
635
789
|
const raw = await get("/api/chess/database/analyse", params);
|
|
636
790
|
return convertAnalyseResponse(raw, fen);
|
|
637
791
|
}
|
|
792
|
+
case "predict_human_move": {
|
|
793
|
+
const fen = resolveFenFromArgs(args);
|
|
794
|
+
const qs = new URLSearchParams();
|
|
795
|
+
qs.set("fen", fen);
|
|
796
|
+
if (typeof args.white_elo === "number")
|
|
797
|
+
qs.set("white_elo", String(args.white_elo));
|
|
798
|
+
if (typeof args.black_elo === "number")
|
|
799
|
+
qs.set("black_elo", String(args.black_elo));
|
|
800
|
+
if (typeof args.top === "number")
|
|
801
|
+
qs.set("top", String(args.top));
|
|
802
|
+
if (Array.isArray(args.prev_fens)) {
|
|
803
|
+
for (const p of args.prev_fens) {
|
|
804
|
+
if (typeof p === "string" && p.length > 0)
|
|
805
|
+
qs.append("prev_fen", p);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return authedRequest("GET", `/api/agent/predict-move?${qs.toString()}`);
|
|
809
|
+
}
|
|
638
810
|
case "get_head_to_head":
|
|
639
811
|
return get("/api/chess/players/h2h", {
|
|
640
812
|
a: Number(args.fide_id_a),
|
|
@@ -671,10 +843,41 @@ async function callToolInner(name, args) {
|
|
|
671
843
|
const raw = await authedRequest("POST", "/api/agent/cloud-engines/analyse", body);
|
|
672
844
|
return convertCloudSnapshotResponse(raw, fen);
|
|
673
845
|
}
|
|
846
|
+
case "list_prep_files":
|
|
847
|
+
return authedRequest("GET", "/api/agent/prep-files");
|
|
848
|
+
case "search_prep_files":
|
|
849
|
+
return authedRequest("GET", `/api/agent/prep-files/search?q=${encodeURIComponent(String(args.query))}`);
|
|
850
|
+
case "read_prep_file": {
|
|
851
|
+
const raw = await authedRequest("GET", `/api/agent/prep-files/${encodeURIComponent(String(args.id))}`);
|
|
852
|
+
// MCP layer adds the tree JSON view — backend stays pure PGN in/out.
|
|
853
|
+
// If parse fails the LLM still has the raw PGN text to work with.
|
|
854
|
+
if (raw && typeof raw === "object") {
|
|
855
|
+
const g = raw;
|
|
856
|
+
if (typeof g.pgnContent === "string") {
|
|
857
|
+
const tree = pgnToTree(g.pgnContent);
|
|
858
|
+
return { ...raw, tree };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return raw;
|
|
862
|
+
}
|
|
863
|
+
case "create_prep_file":
|
|
864
|
+
return authedRequest("POST", "/api/agent/prep-files", {
|
|
865
|
+
name: String(args.name),
|
|
866
|
+
pgn: typeof args.pgn === "string" ? args.pgn : undefined,
|
|
867
|
+
});
|
|
868
|
+
case "save_prep_file":
|
|
869
|
+
return authedRequest("PUT", `/api/agent/prep-files/${encodeURIComponent(String(args.id))}`, {
|
|
870
|
+
pgn: String(args.pgn),
|
|
871
|
+
expected_version: typeof args.expected_version === "number" ? args.expected_version : undefined,
|
|
872
|
+
});
|
|
873
|
+
case "delete_prep_file":
|
|
874
|
+
return authedRequest("DELETE", `/api/agent/prep-files/${encodeURIComponent(String(args.id))}`);
|
|
674
875
|
case "read_engine_usage_guide":
|
|
675
876
|
return { guide: ENGINE_USAGE_DOC };
|
|
676
877
|
case "read_prep_strategy_guide":
|
|
677
878
|
return { guide: PREP_STRATEGY_DOC };
|
|
879
|
+
case "read_prep_files_guide":
|
|
880
|
+
return { guide: PREP_FILES_DOC };
|
|
678
881
|
case "prep_snapshot": {
|
|
679
882
|
const me = Number(args.fide_id_me);
|
|
680
883
|
const opp = Number(args.fide_id_opponent);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Prep files guide
|
|
2
|
+
|
|
3
|
+
You can save chess prep to the user's chess.ceo account and read it back across sessions. This is the persistence layer that turns your analysis from ephemeral chat into a durable, viewable-in-app repertoire.
|
|
4
|
+
|
|
5
|
+
## The mental model
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
Six tools:
|
|
10
|
+
|
|
11
|
+
- `list_prep_files` — show me all my prep files
|
|
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
|
|
16
|
+
- `delete_prep_file(id)` — soft delete (user can restore from app)
|
|
17
|
+
|
|
18
|
+
## The single most important habit
|
|
19
|
+
|
|
20
|
+
**Before creating a new file, search for an existing one.** LLMs make three "Prep vs Firouzja" files in a row all the time. Always:
|
|
21
|
+
|
|
22
|
+
1. `list_prep_files` (if the user has ≤20-30 files) or `search_prep_files(query=<opponent name>)` for their key term
|
|
23
|
+
2. Read the ones that look relevant
|
|
24
|
+
3. Decide: extend an existing one (save_prep_file) or genuinely start fresh (create_prep_file)
|
|
25
|
+
|
|
26
|
+
Duplicate files are the #1 way to lose your user's trust in this system.
|
|
27
|
+
|
|
28
|
+
## PGN structure
|
|
29
|
+
|
|
30
|
+
Real chess prep files are PGN with parenthesised variations. Learn the shape:
|
|
31
|
+
|
|
32
|
+
```pgn
|
|
33
|
+
[Event "Prep vs Firouzja (Black) — 2026-07-23"]
|
|
34
|
+
[White "Firouzja, Alireza"]
|
|
35
|
+
[Black "Van Foreest, Jorden"]
|
|
36
|
+
[Date "2026.07.23"]
|
|
37
|
+
|
|
38
|
+
1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4 Nf6 5. Nc3 a6 6. Be3 e5 7. Nb3
|
|
39
|
+
{Main line. Firouzja plays this in 68% of his English Attacks (32 games, 2023-2025).}
|
|
40
|
+
7... Be6 8. f3
|
|
41
|
+
(8. Qd2 Nbd7 9. f3 h5 {Sideline he tried once and lost — probably patched.})
|
|
42
|
+
8... Nbd7 9. Qd2 h5
|
|
43
|
+
{Critical position for us. Lc0 at multipv=3 gives:
|
|
44
|
+
- h5: +0.12 (main line, active)
|
|
45
|
+
- Be7: +0.08 (calm, more classical)
|
|
46
|
+
- Rc8: 0.00 (Stockfish preferred)}
|
|
47
|
+
|
|
48
|
+
*
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The pieces:
|
|
52
|
+
|
|
53
|
+
- **PGN tag pairs at the top** (`[Key "Value"]`) — `Event` is the name you show the user. `White` / `Black` set the game headers. `Date` is standard.
|
|
54
|
+
- **Mainline** = your top recommendation. Numbered SAN moves separated by whitespace.
|
|
55
|
+
- **Variations** in parentheses `(...)` at the branching move. Nesting is allowed but be conservative — three deep is a lot.
|
|
56
|
+
- **Comments** in curly braces `{...}` between moves. This is where you cite tool output:
|
|
57
|
+
- `{Lc0 says +0.15 in this line vs 0.00 for the alternative}`
|
|
58
|
+
- `{Firouzja plays this in 32/47 games with Black. Scores 41%.}`
|
|
59
|
+
- `{Stockfish sees h4 as a tactical shot; Lc0 disagrees. Deep look needed.}`
|
|
60
|
+
- **NAG glyphs** for evaluations at a move (optional): `$1` (!), `$2` (?), `$3` (!!), `$4` (??), `$6` (=), `$10` (=), `$14` (+/=). Placed right after the SAN: `7. Nb3 $1`.
|
|
61
|
+
- **Result marker** at the end: `*` for unfinished (prep files are always unfinished), `1-0`, `0-1`, `1/2-1/2`.
|
|
62
|
+
|
|
63
|
+
## Editing without breaking the tree
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
Common LLM failure modes to catch yourself doing:
|
|
68
|
+
|
|
69
|
+
- **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.
|
|
70
|
+
- **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.
|
|
71
|
+
- **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`.
|
|
72
|
+
- **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.
|
|
73
|
+
- **Blank Event tag** — the user's list_prep_files response shows the Event tag as the name. Empty tag = "Untitled" everywhere.
|
|
74
|
+
|
|
75
|
+
## Optimistic locking
|
|
76
|
+
|
|
77
|
+
`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:
|
|
78
|
+
|
|
79
|
+
1. Re-read the file to see what changed.
|
|
80
|
+
2. Merge your edits with theirs.
|
|
81
|
+
3. Retry the save with the new version.
|
|
82
|
+
|
|
83
|
+
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).
|
|
84
|
+
|
|
85
|
+
## Grounding
|
|
86
|
+
|
|
87
|
+
Same rule as everywhere else in this MCP: **cite tool output in your PGN comments, don't invent chess prose**. A prep file that reads "White has practical chances" without a tool call to back it up is worse than a file that says "TODO: run cloud_analyse here". The user can view the file; if the commentary doesn't match the actual engine output, they see it.
|
|
88
|
+
|
|
89
|
+
Concrete pattern:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
7. Nb3 {Lc0 at movetime=2000 gives +0.14 for White in the resulting IQP structure.
|
|
93
|
+
Stockfish scores 0.00 — the disagreement is the long-term positional weight Lc0
|
|
94
|
+
sees on the c6 pawn. Verified by running cloud_analyse at move 12 and confirming
|
|
95
|
+
Lc0's evaluation persists.}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
vs
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
7. Nb3 {A strong positional move that gives White good chances.}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The first is what earns trust; the second is what makes the file worthless.
|
|
105
|
+
|
|
106
|
+
## Naming conventions
|
|
107
|
+
|
|
108
|
+
For the [Event] tag (which is the user-visible file name):
|
|
109
|
+
|
|
110
|
+
- Opponent prep: `"Prep vs <Player> (<Color>) — <YYYY-MM-DD>"`
|
|
111
|
+
- Opening study: `"<Opening> — <side>"` e.g. `"Najdorf 6.Bg5 — Black side"`
|
|
112
|
+
- Position analysis: `"<Position description> — <date>"`
|
|
113
|
+
|
|
114
|
+
Keep it short enough to fit in a picker (30-40 chars). Long titles get truncated in the app UI.
|
|
115
|
+
|
|
116
|
+
## When to create vs extend
|
|
117
|
+
|
|
118
|
+
- User asks "prep me against X" and no file matches → create.
|
|
119
|
+
- User asks "check line Y for our Firouzja prep" and a file matches → extend that file with new variations, save with expected_version.
|
|
120
|
+
- User asks "I found a novelty in the Najdorf" and a Najdorf file exists → extend.
|
|
121
|
+
- Rule of thumb: if the user's request semantically overlaps with an existing file's [Event] name or main opening line, extend.
|
|
122
|
+
|
|
123
|
+
## Icons and appearance
|
|
124
|
+
|
|
125
|
+
The user sees your files in a collection called "AI Prep" with a 🤖 icon under folder `/mcp` in their chess.ceo app. This is intentional — they can tell at a glance which prep came from you, and they can browse / edit / delete from the app just like their manual work. Your files are first-class citizens on their account.
|
package/package.json
CHANGED