chess-fen-mcp
Version:
MCP server for chess FEN notation visualization
229 lines (204 loc) • 6.03 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
class ChessFENServer {
constructor() {
this.server = new Server(
{
name: "chess-fen-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "visualize_fen",
description: "Convert FEN notation to ASCII chess board visualization",
inputSchema: {
type: "object",
properties: {
fen_string: {
type: "string",
description: "FEN (Forsyth-Edwards Notation) string representing chess position",
},
},
required: ["fen_string"],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "visualize_fen":
return this.handleVisualizeFEN(request.params.arguments);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
async handleVisualizeFEN(args) {
const { fen_string } = args;
const fen = fen_string;
if (!fen || typeof fen !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"FEN string is required"
);
}
try {
const visualization = this.fenToAscii(fen);
return {
content: [
{
type: "text",
text: visualization,
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error visualizing FEN: ${error.message}`
);
}
}
fenToAscii(fen) {
try {
const parsedFen = this.parseFenString(fen);
return this.renderBoard(parsedFen);
} catch (error) {
throw new Error(`Invalid FEN: ${error.message}`);
}
}
parseFenString(fen) {
const fenTokens = fen.trim().split(" ");
if (fenTokens.length !== 6) {
throw new Error("FEN must have exactly 6 fields");
}
return {
board: this.parseBoard(fenTokens[0]),
toMove: fenTokens[1] === "w" ? "white" : "black",
castlingRights: this.parseCastlingRights(fenTokens[2]),
enPassantSquare: fenTokens[3],
halfMoves: parseInt(fenTokens[4], 10),
fullMoves: parseInt(fenTokens[5], 10),
};
}
parseBoard(boardString) {
const rows = boardString.split("/");
if (rows.length !== 8) {
throw new Error("Board must have exactly 8 rows");
}
return rows.map((row) => {
const squares = [];
for (let i = 0; i < row.length; i++) {
const char = row.charAt(i);
if (char.match(/\d/)) {
const emptySquares = parseInt(char, 10);
for (let j = 0; j < emptySquares; j++) {
squares.push(" ");
}
} else if (this.isPiece(char)) {
squares.push(char);
} else {
throw new Error(`Invalid character in board: ${char}`);
}
}
if (squares.length !== 8) {
throw new Error("Each row must have exactly 8 squares");
}
return squares;
});
}
parseCastlingRights(castlingString) {
return {
white: {
queenside: castlingString.includes("Q"),
kingside: castlingString.includes("K"),
},
black: {
queenside: castlingString.includes("q"),
kingside: castlingString.includes("k"),
},
};
}
isPiece(char) {
return /^[pnbrqkPNBRQK]$/.test(char);
}
pieceToUnicode(piece) {
const pieceMap = {
// White pieces
P: "♙", // Pawn
N: "♘", // Knight
B: "♗", // Bishop
R: "♖", // Rook
Q: "♕", // Queen
K: "♔", // King
// Black pieces
p: "♟", // Pawn
n: "♞", // Knight
b: "♝", // Bishop
r: "♜", // Rook
q: "♛", // Queen
k: "♚", // King
};
return pieceMap[piece] || ".";
}
renderBoard(parsedFen) {
const { board, toMove, castlingRights, enPassantSquare, halfMoves, fullMoves } = parsedFen;
// First output the structured data
let result = `Fen {\n`;
result += ` rows: 8,\n`;
result += ` columns: 8,\n`;
result += ` board: [\n`;
result += ` a b c d e f g h\n`;
for (let row = 0; row < 8; row++) {
const rankNumber = 8 - row;
result += `${rankNumber} `;
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
const display = piece === " " ? "." : this.pieceToUnicode(piece);
result += `${display} `;
}
result += `${rankNumber}\n`;
}
result += ` a b c d e f g h\n`;
result += ` ],\n`;
result += ` toMove: '${toMove}',\n`;
result += ` castlingRights: {\n`;
result += ` white: { queenside: ${castlingRights.white.queenside}, kingside: ${castlingRights.white.kingside} },\n`;
result += ` black: { queenside: ${castlingRights.black.queenside}, kingside: ${castlingRights.black.kingside} }\n`;
result += ` },\n`;
result += ` enPassantSquare: '${enPassantSquare}',\n`;
result += ` halfMoves: ${halfMoves},\n`;
result += ` fullMoves: ${fullMoves}\n`;
result += `}`;
return result;
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Chess FEN MCP server running on stdio");
}
}
const server = new ChessFENServer();
server.run().catch(console.error);