llm-chess-mcp 0.4.1 → 0.4.3

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
@@ -279,7 +279,7 @@ codex mcp add llm-chess-mcp --command npx --args -y llm-chess-mcp --env LICHESS_
279
279
  | `game_legal_moves` | All legal moves with metadata |
280
280
  | `game_pgn` | Export the game as PGN |
281
281
  | `game_import_pgn` | Import a PGN into a new game |
282
- | `position_analyze` | Stockfish multipv lines (cp/mate/WDL + PV), `analysis_level` preset |
282
+ | `position_analyze` | Stockfish multipv lines (cp/mate/WDL + UCI/SAN PV), `analysis_level` preset |
283
283
  | `human_move_distribution` | Maia3 human-move probabilities at a target Elo |
284
284
  | `move_evaluate` | Score one or more moves + cpLoss + classification |
285
285
  | `move_candidates` | **Primary tool**: unified candidates (objective + human + opening) |
@@ -307,6 +307,9 @@ human-readable summary and must not be parsed as data.
307
307
  `best / excellent / good / inaccuracy / mistake / blunder`.
308
308
  - `maia3Prob` is a **human-likelihood**, not move quality. A high-probability move
309
309
  can still be objectively bad.
310
+ - Analysis continuations return `pv` in UCI and the same legal prefix in
311
+ `pvSan` as SAN. If an engine line contains an invalid move, `pvSan` stops
312
+ before it while the original `pv` remains unchanged.
310
313
 
311
314
  ## Candidate structure
312
315
 
package/dist/chess.js CHANGED
@@ -81,3 +81,20 @@ export function parseMove(chess, move) {
81
81
  export function playParsedMove(chess, move) {
82
82
  return chess.move(moveDescriptor(move));
83
83
  }
84
+ export function pvToSan(chess, pv) {
85
+ const copy = new Chess(chess.fen());
86
+ const san = [];
87
+ for (const uci of pv) {
88
+ try {
89
+ const move = parseMove(copy, uci);
90
+ san.push(move.san);
91
+ playParsedMove(copy, move);
92
+ }
93
+ catch (error) {
94
+ if (!(error instanceof ChessError))
95
+ throw error;
96
+ break;
97
+ }
98
+ }
99
+ return san;
100
+ }
package/dist/tool-meta.js CHANGED
@@ -46,7 +46,7 @@ export const TOOL_META = {
46
46
  },
47
47
  position_analyze: {
48
48
  title: "Analyze Chess Position",
49
- description: "Run Stockfish on the current position and return the top engine lines (multipv). Scores are from the side-to-move perspective: positive cp = side to move is better; mate N = side to move mates in N. wdl is [win, draw, loss] in permille for the side to move. Use analysis_level (fast/normal/deep) or explicit depth/multipv. Does NOT mutate the game.",
49
+ description: "Run Stockfish on the current position and return the top engine lines (multipv) as UCI pv and SAN pvSan. Scores are from the side-to-move perspective: positive cp = side to move is better; mate N = side to move mates in N. wdl is [win, draw, loss] in permille for the side to move. Use analysis_level (fast/normal/deep) or explicit depth/multipv. Does NOT mutate the game.",
50
50
  annotations: readOnly(),
51
51
  },
52
52
  human_move_distribution: {
@@ -56,7 +56,7 @@ export const TOOL_META = {
56
56
  },
57
57
  move_evaluate: {
58
58
  title: "Evaluate Chess Moves",
59
- description: "Evaluate one or more moves with Stockfish without mutating the game. Pass a single move string or an array of moves to compare. Returns, for each move, the score after the move (from the mover's perspective), cpLoss vs the best move, and a classification (best/excellent/good/inaccuracy/mistake/blunder).",
59
+ description: "Evaluate one or more moves with Stockfish without mutating the game. Pass a single move string or an array of moves to compare. Returns, for each move, the score after the move (from the mover's perspective), cpLoss vs the best move, a classification (best/excellent/good/inaccuracy/mistake/blunder), and the continuation as UCI pv and SAN pvSan.",
60
60
  annotations: readOnly(),
61
61
  },
62
62
  move_candidates: {
@@ -45,6 +45,10 @@ export const SfLineSchema = z.strictObject({
45
45
  wdl: wdl.nullable(),
46
46
  pv: z.array(z.string()),
47
47
  });
48
+ export const AnalysisLineSchema = z.strictObject({
49
+ ...SfLineSchema.shape,
50
+ pvSan: z.array(z.string()),
51
+ });
48
52
  export const OpeningStatsSchema = z.strictObject({
49
53
  status: z.enum(["available", "no_data", "unavailable", "disabled"]),
50
54
  reason: z.enum(EXPLORER_ERROR_KINDS).optional(),
@@ -121,7 +125,7 @@ export const PositionAnalyzeOutputSchema = z.strictObject({
121
125
  turn: color,
122
126
  revision,
123
127
  analysis_level: z.enum(["fast", "normal", "deep"]),
124
- lines: z.array(SfLineSchema),
128
+ lines: z.array(AnalysisLineSchema),
125
129
  });
126
130
  export const Maia3MoveSchema = z.strictObject({
127
131
  uci: z.string(),
@@ -155,6 +159,7 @@ const moveEvaluation = z.strictObject({
155
159
  .enum(["best", "excellent", "good", "inaccuracy", "mistake", "blunder"])
156
160
  .nullable(),
157
161
  pv: z.array(z.string()),
162
+ pvSan: z.array(z.string()),
158
163
  });
159
164
  export const MoveEvaluateOutputSchema = z.strictObject({
160
165
  game_id: z.string(),
@@ -1,4 +1,4 @@
1
- import { drawResult, parseMove, playParsedMove, snapshotChess, } from "../chess.js";
1
+ import { drawResult, parseMove, playParsedMove, pvToSan, snapshotChess, } from "../chess.js";
2
2
  import { ANALYSIS_PRESETS, classifyCpLoss, evalToCp, negateEval, toEval, } from "../eval.js";
3
3
  import { TOOL_INPUT_SCHEMAS } from "../tool-inputs.js";
4
4
  import { TOOL_META } from "../tool-meta.js";
@@ -28,6 +28,7 @@ export function registerAnalysisTools(server, services) {
28
28
  scoreMate: line.scoreMate,
29
29
  wdl: line.wdl,
30
30
  pv: line.pv,
31
+ pvSan: pvToSan(chess, line.pv),
31
32
  })),
32
33
  };
33
34
  return toolResult(payload, `Analyzed game ${game_id} at revision ${revision}; ${payload.lines.length} lines`);
@@ -80,6 +81,7 @@ export function registerAnalysisTools(server, services) {
80
81
  cpLoss: null,
81
82
  classification: "best",
82
83
  pv: [],
84
+ pvSan: [],
83
85
  });
84
86
  continue;
85
87
  }
@@ -98,6 +100,7 @@ export function registerAnalysisTools(server, services) {
98
100
  ? classifyCpLoss(cpLoss)
99
101
  : null,
100
102
  pv: [],
103
+ pvSan: [],
101
104
  });
102
105
  continue;
103
106
  }
@@ -108,6 +111,7 @@ export function registerAnalysisTools(server, services) {
108
111
  const moverEval = afterEval ? negateEval(afterEval) : null;
109
112
  const afterCp = moverEval ? evalToCp(moverEval) : null;
110
113
  const cpLoss = afterCp !== null && beforeCp !== null ? beforeCp - afterCp : null;
114
+ const pv = after?.pv ?? [];
111
115
  results.push({
112
116
  move: parsed.san,
113
117
  uci: parsed.lan,
@@ -119,7 +123,8 @@ export function registerAnalysisTools(server, services) {
119
123
  classification: cpLoss !== null
120
124
  ? classifyCpLoss(cpLoss)
121
125
  : null,
122
- pv: after?.pv ?? [],
126
+ pv,
127
+ pvSan: pvToSan(copy, pv),
123
128
  });
124
129
  }
125
130
  return toolResult({ game_id, revision, results }, `Evaluated ${results.length} move${results.length === 1 ? "" : "s"} in game ${game_id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-chess-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server that lets an LLM analyze, judge, and choose chess moves (Stockfish + Maia3 + Lichess)",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",