@chessceo/mcp 0.15.0 → 0.16.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.
Files changed (2) hide show
  1. package/dist/index.js +63 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -185,11 +185,15 @@ const TOOLS = [
185
185
  },
186
186
  line: {
187
187
  type: "string",
188
- description: "Move sequence in SAN, space-separated, no move numbers required. Example: 'e4 e5 Nf3'. Leave empty for the starting position.",
188
+ description: "Move sequence in SAN from the starting position, space-separated, no move numbers required. Example: 'e4 e5 Nf3'. Leave empty for startpos. Alias: `moves` (same thing).",
189
+ },
190
+ moves: {
191
+ type: "string",
192
+ description: "SAN moves to apply on top of `fen` (or on top of startpos if no fen). Same shape as `line`. Use this when you have a starting FEN and want to walk from there — e.g. fen='<game tabiya>', moves='b4 a5 c3' to explore that continuation. Wins over `line` if both are set.",
189
193
  },
190
194
  fen: {
191
195
  type: "string",
192
- description: "Alternative to `line` raw FEN of the target position.",
196
+ description: "Starting position as FEN. Combine with `moves` to walk from there, or use alone.",
193
197
  },
194
198
  limit: {
195
199
  type: "integer",
@@ -210,7 +214,15 @@ const TOOLS = [
210
214
  properties: {
211
215
  fen: {
212
216
  type: "string",
213
- description: "FEN of the position to look up.",
217
+ description: "Starting position as FEN. Combine with `moves` to walk from there.",
218
+ },
219
+ moves: {
220
+ type: "string",
221
+ description: "Optional SAN moves to apply on top of `fen` (or on top of startpos). Example: fen='<tabiya>', moves='b4 a5'.",
222
+ },
223
+ line: {
224
+ type: "string",
225
+ description: "Synonym for `moves` from the starting position; kept for compatibility.",
214
226
  },
215
227
  limit: {
216
228
  type: "integer",
@@ -219,18 +231,22 @@ const TOOLS = [
219
231
  description: "Number of top continuations to return.",
220
232
  },
221
233
  },
222
- required: ["fen"],
223
234
  },
224
235
  },
225
236
  {
226
237
  name: "analyse",
227
238
  description: "Short Stockfish evaluation at a position. Returns the top-N candidate moves with score (centipawns from side-to-move POV, positive = advantage; or mate distance) and the principal variation for each. Defaults: 2s think time, top-3 lines. PV moves come back in SAN (e4, Nf3, Bxc4 — not UCI). Free (no cloud instance needed) — use liberally.\n\n" +
228
239
  "GROUNDING: cite this tool's actual output when you claim things about positions. Don't invent evaluations from general principles or training data — if you don't have engine output for a FEN, call this. Compute is cheap.\n\n" +
240
+ "Position input is flexible — pass `fen`, or `moves` from startpos, or `fen + moves` to walk from an arbitrary position. Also see the flip-side-to-move threat check in read_engine_usage_guide.\n\n" +
229
241
  "Use this to sanity-check candidate lines from get_position_stats or get_player_preparation — human game frequency tells you what people play, engine evaluation tells you what's actually good.",
230
242
  inputSchema: {
231
243
  type: "object",
232
244
  properties: {
233
- fen: { type: "string", description: "FEN of the position to analyse." },
245
+ fen: { type: "string", description: "Starting position as FEN (defaults to startpos)." },
246
+ moves: {
247
+ type: "string",
248
+ description: "Optional SAN moves to apply on top of `fen` (or on top of startpos). Example: fen='<tabiya>', moves='b4 a5'.",
249
+ },
234
250
  movetime_ms: {
235
251
  type: "integer",
236
252
  minimum: 100,
@@ -244,7 +260,6 @@ const TOOLS = [
244
260
  description: "Number of candidate lines to return (default 3).",
245
261
  },
246
262
  },
247
- required: ["fen"],
248
263
  },
249
264
  },
250
265
  {
@@ -338,12 +353,17 @@ const TOOLS = [
338
353
  "• Lc0 is practical eval — trust it for 'which side is easier?' 'which candidate is best when Stockfish shows several as equal?' Lc0 sees long-term positional factors Stockfish's fixed search can miss.\n" +
339
354
  "• When they agree → high confidence. When they disagree → look at both scores and reason WHY (Stockfish sharply higher = tactic Lc0 missed; Lc0 higher = long-term positional edge past Stockfish's horizon). Never dismiss either — the disagreement is the signal.\n\n" +
340
355
  "Contempt (`contempt`) skews Lc0 (only Lc0 — Stockfish always stays objective) toward White (positive) or Black (negative). Practical range -20..+20. Use it to find non-objective 'practical' ideas or when the user needs to steer toward fighting/solid lines with a specific colour. Do NOT quote a contempt-biased eval as objective — cross-check with Stockfish.\n\n" +
356
+ "Also useful: pass `moves` on top of `fen` to explore a variation without computing FENs yourself (e.g. fen='<tabiya>', moves='b4 a5 c3'). And the flip-side-to-move threat check documented in the guide is a great free trick.\n\n" +
341
357
  "For the full guide including worked examples, call the `read_engine_usage_guide` tool.\n\n" +
342
358
  "Not for casual questions — this costs real money per second. Use the free `analyse` tool (single Stockfish, 2s) or `get_position_stats` for anything that doesn't require deep prep.",
343
359
  inputSchema: {
344
360
  type: "object",
345
361
  properties: {
346
- fen: { type: "string", description: "FEN of the position to analyse." },
362
+ fen: { type: "string", description: "Starting position as FEN. Combine with `moves` to walk from there." },
363
+ moves: {
364
+ type: "string",
365
+ description: "Optional SAN moves to apply on top of `fen` (or on top of startpos). Example: fen='<tabiya>', moves='b4 a5 c3' analyses the position after those three moves.",
366
+ },
347
367
  movetime_ms: {
348
368
  type: "integer",
349
369
  minimum: 100,
@@ -500,18 +520,26 @@ function convertAvailableMovesToSAN(raw, fen) {
500
520
  }
501
521
  return raw;
502
522
  }
503
- // Resolve a starting FEN from either the `fen` or `line` argument the tool
504
- // received. Same logic prep_snapshot already had inline — extracted so we
505
- // can reuse it wherever we need to walk a SAN line to a concrete FEN
506
- // (e.g. for UCISAN conversion of availableMoves).
523
+ // Resolve a starting FEN from any combination of `fen`, `line`, and
524
+ // `moves` the tool received. Three modes, all valid:
525
+ //
526
+ // fen alone use as-is
527
+ // line/moves alone → walk from startpos
528
+ // fen + moves (or line) → walk from that fen
529
+ //
530
+ // `line` is the historical field name from the backend's prep endpoint;
531
+ // `moves` is the flexible-input name we now surface for LLM ergonomics
532
+ // ("start from this FEN and play these moves next"). They're synonyms
533
+ // here — same SAN sequence, same chess.js walker. `moves` wins if both
534
+ // happen to be provided.
507
535
  function resolveFenFromArgs(args) {
508
536
  const fenArg = typeof args.fen === "string" ? args.fen.trim() : "";
509
- if (fenArg)
510
- return fenArg;
537
+ const movesArg = typeof args.moves === "string" ? args.moves.trim() : "";
511
538
  const lineArg = typeof args.line === "string" ? args.line.trim() : "";
512
- const board = new Chess();
513
- if (lineArg) {
514
- for (const raw of lineArg.split(/\s+/)) {
539
+ const sequence = movesArg || lineArg;
540
+ const board = fenArg ? new Chess(fenArg) : new Chess();
541
+ if (sequence) {
542
+ for (const raw of sequence.split(/\s+/)) {
515
543
  const san = raw.replace(/^\d+\.+/, "");
516
544
  if (!san)
517
545
  continue;
@@ -519,7 +547,7 @@ function resolveFenFromArgs(args) {
519
547
  board.move(san);
520
548
  }
521
549
  catch {
522
- throw new Error(`bad SAN token '${raw}' in line`);
550
+ throw new Error(`bad SAN token '${raw}' in moves`);
523
551
  }
524
552
  }
525
553
  }
@@ -561,15 +589,26 @@ async function callToolInner(name, args) {
561
589
  case "get_player_profile":
562
590
  return get("/api/chess/players/profile", { fideId: Number(args.fide_id) });
563
591
  case "get_player_preparation": {
592
+ // Prefer sending `line` to the backend when the caller specified moves
593
+ // WITHOUT a fen — the backend then attaches the cumulative-line SAN to
594
+ // each move in the response (nicer for the LLM's follow-up walks).
595
+ // When a fen was supplied, always resolve the effective position
596
+ // client-side and send just fen — the backend can't reconstruct
597
+ // history it didn't see anyway.
598
+ const fenArg = typeof args.fen === "string" ? args.fen.trim() : "";
599
+ const movesArg = typeof args.moves === "string" ? args.moves.trim() : "";
600
+ const lineArg = typeof args.line === "string" ? args.line.trim() : "";
564
601
  const params = {
565
602
  fideId: Number(args.fide_id),
566
603
  color: String(args.color),
567
604
  compact: "true",
568
605
  };
569
- if (typeof args.line === "string" && args.line.length > 0)
570
- params.line = args.line;
571
- if (typeof args.fen === "string" && args.fen.length > 0)
572
- params.fen = args.fen;
606
+ if (fenArg || movesArg) {
607
+ params.fen = resolveFenFromArgs(args);
608
+ }
609
+ else if (lineArg) {
610
+ params.line = lineArg;
611
+ }
573
612
  if (typeof args.limit === "number")
574
613
  params.limit = args.limit;
575
614
  if (typeof args.offset === "number")
@@ -578,7 +617,7 @@ async function callToolInner(name, args) {
578
617
  return convertAvailableMovesToSAN(raw, resolveFenFromArgs(args));
579
618
  }
580
619
  case "get_position_stats": {
581
- const fen = String(args.fen);
620
+ const fen = resolveFenFromArgs(args);
582
621
  const raw = await get("/api/chess/database/main", {
583
622
  fen,
584
623
  limit: typeof args.limit === "number" ? args.limit : 20,
@@ -587,7 +626,7 @@ async function callToolInner(name, args) {
587
626
  return convertAvailableMovesToSAN(raw, fen);
588
627
  }
589
628
  case "analyse": {
590
- const fen = String(args.fen);
629
+ const fen = resolveFenFromArgs(args);
591
630
  const params = { fen };
592
631
  if (typeof args.movetime_ms === "number")
593
632
  params.movetime_ms = args.movetime_ms;
@@ -621,7 +660,7 @@ async function callToolInner(name, args) {
621
660
  case "stop_cloud_engine":
622
661
  return authedRequest("DELETE", `/api/agent/cloud-engines/${encodeURIComponent(String(args.contract_id))}`);
623
662
  case "cloud_analyse": {
624
- const fen = String(args.fen);
663
+ const fen = resolveFenFromArgs(args);
625
664
  const body = { fen };
626
665
  if (typeof args.movetime_ms === "number")
627
666
  body.movetime_ms = args.movetime_ms;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.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": {