UNPKG

chess-trainer-mcp

Version:

Chess Trainer MCP Server with Stockfish NNUE engine - Interactive chess training with AI coaching capabilities

980 lines (879 loc) โ€ข 32.1 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { gameStateManager } from './gameStateManager.js'; import { ChessTrainerServer } from './index.js'; import { parsePgn } from 'chessops/pgn.js'; import { Chess } from 'chessops/chess.js'; import { parseFen } from 'chessops/fen.js'; import { makeSan } from 'chessops/san.js'; export class MCPServer { constructor() { this.server = new Server( { name: 'chess-trainer-mcp', version: '1.0.13', }, { capabilities: { tools: {}, resources: {}, prompts: {}, experimental: {} }, } ); this.serverInstance = null; // Store the Chess Trainer server instance this.gameStateManager = gameStateManager; // Store reference to game state manager this.initializeTools(); this.setupHandlers(); } initializeTools() { this.tools = [ // Server Management Tools { name: 'launch_chess_trainer', description: 'Launch the Chess Trainer web server with optional browser opening', inputSchema: zodToJsonSchema( z.object({ port: z.number().default(3456).describe('Port to run the web server on'), auto_open_browser: z.boolean().default(true).describe('Automatically open browser after starting') }) ) }, { name: 'stop_chess_trainer', description: 'Stop the Chess Trainer web server', inputSchema: zodToJsonSchema( z.object({ port: z.number().default(3456).describe('Port of the server to stop') }) ) }, // Game Management Tools { name: 'setup_game', description: 'Setup the chess game with specific settings', inputSchema: zodToJsonSchema( z.object({ mode: z.enum(['human_vs_human', 'human_vs_ai']).default('human_vs_ai').describe('Game mode'), player_color: z.enum(['white', 'black']).default('white').describe('Player color when playing against AI'), ai_elo: z.number().min(800).max(2800).default(1500).describe('AI strength in ELO rating (800-2800)'), ai_time_limit: z.number().min(200).max(5000).default(1000).describe('AI thinking time in milliseconds') }) ) }, { name: 'get_game_state', description: 'Get the current chess game state', inputSchema: zodToJsonSchema(z.object({})) }, { name: 'reset_game', description: 'Reset the game to the starting position', inputSchema: zodToJsonSchema(z.object({})) }, // Game Interaction Tools { name: 'make_move', description: 'Make a move in the current chess game', inputSchema: zodToJsonSchema( z.object({ move: z.string().describe('Move in algebraic notation (e.g., "e2e4", "Nf3", "O-O")') }) ) }, { name: 'suggest_best_move', description: 'Get the best move suggestion for the current position', inputSchema: zodToJsonSchema( z.object({ depth: z.number().min(1).max(20).default(12).describe('Analysis depth') }) ) }, // Analysis Tools { name: 'analyze_position', description: 'Analyze a chess position (currently returns simulated analysis)', inputSchema: zodToJsonSchema( z.object({ fen: z.string().describe('FEN string of the position to analyze'), depth: z.number().min(1).max(20).default(15).describe('Analysis depth') }) ) }, { name: 'evaluate_move', description: 'Evaluate the quality of a chess move', inputSchema: zodToJsonSchema( z.object({ fen: z.string().describe('FEN string before the move'), move: z.string().describe('Move to evaluate in algebraic notation') }) ) }, { name: 'get_best_moves', description: 'Get the top N best moves for a position', inputSchema: zodToJsonSchema( z.object({ fen: z.string().describe('FEN string of the position'), count: z.number().min(1).max(10).default(3).describe('Number of best moves to return') }) ) }, // Utility Tools { name: 'validate_fen', description: 'Validate a FEN string and get position information', inputSchema: zodToJsonSchema( z.object({ fen: z.string().describe('FEN string to validate') }) ) }, { name: 'generate_pgn', description: 'Generate PGN notation from a list of moves', inputSchema: zodToJsonSchema( z.object({ moves: z.array(z.string()).describe('Array of moves in algebraic notation'), white_player: z.string().default('Player1').describe('White player name'), black_player: z.string().default('Player2').describe('Black player name'), event: z.string().default('Chess Trainer Game').describe('Event name'), date: z.string().optional().describe('Game date (YYYY.MM.DD format)') }) ) }, { name: 'explain_opening', description: 'Get explanation and principles of a chess opening', inputSchema: zodToJsonSchema( z.object({ moves: z.array(z.string()).describe('Opening moves in algebraic notation'), opening_name: z.string().optional().describe('Name of the opening (if known)') }) ) }, // Embedding Tools { name: 'get_embeddable_url', description: 'Get a URL to access the chess trainer web interface', inputSchema: zodToJsonSchema( z.object({ port: z.number().min(1024).max(65535).default(3456).describe('Port where the chess trainer is running') }) ) }, // PGN Loading Tools { name: 'load_pgn_for_replay', description: 'Load a PGN file or text and trigger UI replay mode', inputSchema: zodToJsonSchema( z.object({ pgn: z.string().describe('PGN content as text'), auto_play: z.boolean().default(true).describe('Automatically start playing through the moves'), delay_ms: z.number().min(500).max(5000).default(2000).describe('Delay between moves in milliseconds') }) ) } ]; } setupHandlers() { // Handle initialization - start chess server automatically this.server.setRequestHandler(InitializeRequestSchema, async (request) => { console.error('๐Ÿš€ MCP Server initializing...'); try { // Start the chess server automatically during initialization if (!this.serverInstance) { this.serverInstance = new ChessTrainerServer(); await this.serverInstance.initialize(); await this.serverInstance.start(3456); console.error('โœ… Chess server started on port 3456'); } console.error('โœ… MCP Server initialized with chess server running'); // Return standard initialization response return { protocolVersion: request.params.protocolVersion, capabilities: this.server.getCapabilities(), serverInfo: { name: 'chess-trainer-mcp', version: '1.0.13' }, instructions: 'Chess Trainer MCP Server is ready. The chess server has been automatically started on port 3456.' }; } catch (error) { console.error('โŒ Failed to initialize MCP server:', error); throw new Error(`Initialization failed: ${error.message}`); } }); this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.tools, })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { return await this.callTool(name, args || {}); } catch (error) { throw new Error(`Tool execution failed: ${error.message}`); } }); // Handle server shutdown this.setupShutdownHandlers(); } // Check if there's an active game hasActiveGame() { try { const gameState = gameStateManager.getGameState(); return gameState && gameState.active; } catch (error) { console.error('Failed to check active game:', error); return false; } } setupShutdownHandlers() { // Handle graceful shutdown const gracefulShutdown = async (signal) => { console.error(`๐Ÿ›‘ MCP Server received ${signal}, shutting down gracefully...`); try { // Stop the chess server if we have an instance if (this.serverInstance) { this.serverInstance.shutdown(); console.error('โœ… Chess server stopped successfully'); } } catch (error) { console.error('โš ๏ธ Error stopping chess server:', error); } // Close MCP server try { this.server.close(); console.error('โœ… MCP server closed successfully'); } catch (error) { console.error('โš ๏ธ Error closing MCP server:', error); } process.exit(0); }; // Register shutdown handlers for various signals process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGQUIT', () => gracefulShutdown('SIGQUIT')); // Handle uncaught exceptions process.on('uncaughtException', async (error) => { console.error('๐Ÿ’ฅ Uncaught Exception:', error); await gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', async (reason, promise) => { console.error('๐Ÿ’ฅ Unhandled Rejection at:', promise, 'reason:', reason); await gracefulShutdown('unhandledRejection'); }); } async callTool(name, arguments_) { switch (name) { // Server Management case 'launch_chess_trainer': return await this.launchChessTrainer(arguments_); case 'stop_chess_trainer': return await this.stopChessTrainer(arguments_); // Game Management case 'setup_game': return await this.setupGame(arguments_); case 'get_game_state': return await this.proxyToWebServer('get_game_state', arguments_); case 'reset_game': return await this.proxyToWebServer('reset_game', arguments_); // Game Interaction case 'make_move': return await this.proxyToWebServer('make_move', arguments_); case 'suggest_best_move': return await this.proxyToWebServer('suggest_move', arguments_); // Analysis Tools case 'analyze_position': return await this.analyzePosition(arguments_.fen, arguments_.depth); case 'evaluate_move': return await this.evaluateMove(arguments_.fen, arguments_.move); case 'get_best_moves': return await this.getBestMoves(arguments_.fen, arguments_.count); // Utility Tools case 'validate_fen': return await this.validateFen(arguments_.fen); case 'generate_pgn': return await this.generatePgn(arguments_); case 'explain_opening': return await this.explainOpening(arguments_.moves, arguments_.opening_name); // Embedding Tools case 'get_embeddable_url': return await this.getEmbeddableUrl(arguments_); case 'load_pgn_for_replay': return await this.loadPgnForReplay(arguments_); default: throw new Error(`Unknown tool: ${name}`); } } // Proxy method for web server communication async proxyToWebServer(action, args) { try { const response = await fetch(`http://localhost:3456/api/mcp/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(args) }); if (!response.ok) { throw new Error(`Web server responded with status ${response.status}`); } const result = await response.json(); return { content: [{ type: 'text', text: result.message || JSON.stringify(result, null, 2) }] }; } catch (error) { if (error.code === 'ECONNREFUSED') { return { content: [{ type: 'text', text: `โŒ Chess Trainer Server Not Running\n\n` + `The chess trainer web server is not running on port 3456.\n` + `Please start it first using the 'launch_chess_trainer' tool.\n\n` + `Error: ${error.message}` }] }; } throw new Error(`Failed to connect to web server: ${error.message}`); } } async launchChessTrainer(args = {}) { try { const { port = 3456, auto_open_browser = true } = args; // Check if server is already running try { const testResponse = await fetch(`http://localhost:${port}/api/health`); if (testResponse.ok) { return { content: [{ type: 'text', text: `โœ… Chess Trainer Already Running!\n\n` + `๐ŸŒ Web Interface: http://localhost:${port}\n` + `๐Ÿ”— WebSocket: ws://localhost:${port}/ws\n\n` + `The server is already running. You can:\n` + `โ€ข Open http://localhost:${port} in your browser\n` + `โ€ข Use 'setup_game' to configure a new game\n` + `โ€ข Use 'get_game_state' to see current position` }] }; } } catch (e) { // Server not running, continue to start it } // Start the server if (!this.serverInstance) { this.serverInstance = new ChessTrainerServer(); await this.serverInstance.initialize(); await this.serverInstance.start(port); } // Open browser if requested let browserMessage = ''; if (auto_open_browser) { try { const open = (await import('open')).default; await open(`http://localhost:${port}`); browserMessage = '๐ŸŒ Browser opened automatically\n'; } catch (e) { browserMessage = 'โš ๏ธ Could not open browser automatically\n'; } } return { content: [{ type: 'text', text: `๐Ÿš€ Chess Trainer Started Successfully!\n\n` + `๐ŸŒ Web Interface: http://localhost:${port}\n` + `๐Ÿ”— WebSocket: ws://localhost:${port}/ws\n` + browserMessage + `\nThe server is now running. You can:\n` + `โ€ข Use 'setup_game' to configure a new game\n` + `โ€ข Visit http://localhost:${port} in your browser` }] }; } catch (error) { throw new Error(`Failed to launch Chess Trainer: ${error.message}`); } } async stopChessTrainer(args = {}) { try { const { port = 3456 } = args; if (this.serverInstance) { this.serverInstance.shutdown(); this.serverInstance = null; return { content: [{ type: 'text', text: `โœ… Chess Trainer server stopped successfully\n\n` + `The server on port ${port} has been shut down.` }] }; } else { return { content: [{ type: 'text', text: `โš ๏ธ No server instance found\n\n` + `The server may not be running or was started externally.` }] }; } } catch (error) { throw new Error(`Failed to stop Chess Trainer: ${error.message}`); } } async setupGame(args) { try { const { mode = 'human_vs_ai', player_color = 'white', ai_elo = 1500, ai_time_limit = 1000 } = args; // Use the reset_game endpoint with settings const response = await fetch(`http://localhost:3456/api/mcp/reset_game`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ gameSettings: { mode: mode, playerColor: player_color, aiEloRating: ai_elo, aiTimeLimit: ai_time_limit } }) }); if (!response.ok) { throw new Error(`Server responded with status ${response.status}`); } const result = await response.json(); return { content: [{ type: 'text', text: `๐ŸŽฎ Chess Game Created!\n\n` + `๐ŸŽฏ Mode: ${mode === 'human_vs_ai' ? 'Human vs AI' : 'Human vs Human'}\n` + (mode === 'human_vs_ai' ? `๐ŸŽจ Your Color: ${player_color}\n` + `๐Ÿค– AI Strength: ${ai_elo} ELO\n` + `โฑ๏ธ AI Think Time: ${ai_time_limit/1000}s\n` : '') + `\n๐ŸŒ Play at: http://localhost:3456\n` + `\nNext steps:\n` + `โ€ข Use 'make_move <move>' to play\n` + `โ€ข Use 'get_game_state' to see the board\n` + `โ€ข Use 'suggest_best_move' for help` }] }; } catch (error) { if (error.code === 'ECONNREFUSED') { return { content: [{ type: 'text', text: `โŒ Chess Trainer Server Not Running\n\n` + `Please start the server first with 'launch_chess_trainer'.\n` + `Error: ${error.message}` }] }; } throw new Error(`Failed to create game: ${error.message}`); } } // Analysis tool implementations (currently mocked - need real Stockfish integration) async analyzePosition(fen, depth = 15) { try { // TODO: Integrate real Stockfish analysis const mockAnalysis = { evaluation: '+0.34', bestMove: 'e2e4', pv: 'e2e4 e7e5 Ng1f3', assessment: 'Slightly better for White' }; return { content: [{ type: 'text', text: `๐Ÿ“Š Position Analysis\n\n` + `๐Ÿ“ FEN: ${fen}\n` + `๐Ÿ” Depth: ${depth}\n\n` + `๐Ÿ“ˆ Evaluation: ${mockAnalysis.evaluation}\n` + `๐ŸŽฏ Best Move: ${mockAnalysis.bestMove}\n` + `๐Ÿ“‹ Principal Variation: ${mockAnalysis.pv}\n` + `๐Ÿ’ญ Assessment: ${mockAnalysis.assessment}\n\n` + `โš ๏ธ Note: This is simulated analysis. Real Stockfish integration pending.` }] }; } catch (error) { throw new Error(`Failed to analyze position: ${error.message}`); } } async evaluateMove(fen, move) { try { const { Chess } = await import('chess.js'); const chess = new Chess(fen); const moveResult = chess.move(move); if (!moveResult) { return { content: [{ type: 'text', text: `โŒ Invalid Move\n\n` + `Move "${move}" is not legal in the given position.\n` + `Please check the move notation and try again.` }] }; } // TODO: Real evaluation with Stockfish return { content: [{ type: 'text', text: `โ™Ÿ๏ธ Move Evaluation: ${moveResult.san}\n\n` + `โœ… Move is legal\n` + `๐Ÿ“ New position: ${chess.fen()}\n\n` + `โš ๏ธ Note: Detailed move quality analysis requires Stockfish integration.` }] }; } catch (error) { throw new Error(`Failed to evaluate move: ${error.message}`); } } async getBestMoves(fen, count = 3) { try { // TODO: Real Stockfish integration const mockMoves = [ { move: 'e2e4', eval: '+0.31', line: 'e2e4 e7e5 Ng1f3 Nb8c6' }, { move: 'd2d4', eval: '+0.28', line: 'd2d4 d7d5 c2c4 e7e6' }, { move: 'Ng1f3', eval: '+0.25', line: 'Ng1f3 Ng8f6 c2c4 e7e6' } ].slice(0, count); let result = `๐ŸŽฏ Best Moves Analysis\n\n`; result += `๐Ÿ“ Position: ${fen}\n\n`; mockMoves.forEach((m, i) => { result += `${i + 1}. ${m.move} (${m.eval})\n`; result += ` ๐Ÿ“‹ Line: ${m.line}\n\n`; }); result += `โš ๏ธ Note: This shows example moves. Real engine analysis pending.`; return { content: [{ type: 'text', text: result }] }; } catch (error) { throw new Error(`Failed to get best moves: ${error.message}`); } } async validateFen(fen) { try { const { Chess } = await import('chess.js'); try { const chess = new Chess(fen); const turn = chess.turn() === 'w' ? 'White' : 'Black'; const inCheck = chess.isCheck(); const isCheckmate = chess.isCheckmate(); const isStalemate = chess.isStalemate(); const isDraw = chess.isDraw(); const isInsufficientMaterial = chess.isInsufficientMaterial(); const isThreefoldRepetition = chess.isThreefoldRepetition(); let status = 'โœ… Valid FEN Position\n\n'; status += `๐Ÿ“ FEN: ${fen}\n\n`; status += `๐ŸŽฏ Turn: ${turn}\n`; if (inCheck) status += `โš ๏ธ Check: Yes\n`; if (isCheckmate) status += `โ™” Checkmate: Yes - ${turn === 'White' ? 'Black' : 'White'} wins!\n`; if (isStalemate) status += `๐Ÿค Stalemate: Yes - Draw!\n`; if (isDraw) status += `๐Ÿค Draw: Yes\n`; if (isInsufficientMaterial) status += `๐Ÿ“‰ Insufficient Material: Yes\n`; if (isThreefoldRepetition) status += `๐Ÿ”„ Threefold Repetition: Yes\n`; return { content: [{ type: 'text', text: status }] }; } catch (chessError) { return { content: [{ type: 'text', text: `โŒ Invalid FEN Position\n\n` + `The provided FEN string is not valid.\n` + `Error: ${chessError.message}\n\n` + `FEN format: <pieces> <turn> <castling> <en-passant> <halfmove> <fullmove>` }] }; } } catch (error) { throw new Error(`Failed to validate FEN: ${error.message}`); } } async generatePgn(args) { try { const { moves, white_player = 'Player1', black_player = 'Player2', event = 'Chess Trainer Game', date = new Date().toISOString().split('T')[0].replace(/-/g, '.') } = args; const { Chess } = await import('chess.js'); const chess = new Chess(); // Validate and execute moves const validMoves = []; for (const move of moves) { try { const result = chess.move(move); if (result) { validMoves.push(result.san); } else { throw new Error(`Invalid move: ${move}`); } } catch (moveError) { return { content: [{ type: 'text', text: `โŒ Error in move sequence\n\n` + `Failed at move "${move}"\n` + `${moveError.message}\n\n` + `Valid moves so far: ${validMoves.join(' ')}` }] }; } } // Generate PGN let pgn = `[Event "${event}"]\n`; pgn += `[Site "Chess Trainer MCP"]\n`; pgn += `[Date "${date}"]\n`; pgn += `[Round "?"]\n`; pgn += `[White "${white_player}"]\n`; pgn += `[Black "${black_player}"]\n`; pgn += `[Result "*"]\n\n`; // Format moves for (let i = 0; i < validMoves.length; i++) { if (i % 2 === 0) { pgn += `${Math.floor(i / 2) + 1}. ${validMoves[i]}`; } else { pgn += ` ${validMoves[i]}`; if (i < validMoves.length - 1) pgn += '\n'; } } if (validMoves.length > 0) pgn += ' *'; return { content: [{ type: 'text', text: `๐Ÿ“‹ Generated PGN\n\n${pgn}\n\n` + `โœ… ${validMoves.length} moves validated and formatted\n` + `๐Ÿ’ก This PGN can be imported into any chess software` }] }; } catch (error) { throw new Error(`Failed to generate PGN: ${error.message}`); } } async explainOpening(moves, openingName) { try { // Enhanced opening database const openingDatabase = { 'e4': { name: "King's Pawn", idea: "Controls center, develops pieces quickly" }, 'd4': { name: "Queen's Pawn", idea: "Solid center control, strategic play" }, 'Nf3': { name: "Rรฉti Opening", idea: "Flexible development, delays center commitment" }, 'c4': { name: "English Opening", idea: "Controls d5, flexible pawn structure" }, 'e4,e5,Nf3,Nc6,Bb5': { name: "Ruy Lopez", idea: "Pressures e5 pawn, aims for center control" }, 'e4,e5,Nf3,Nc6,Bc4': { name: "Italian Game", idea: "Quick development, targets f7 weakness" }, 'e4,c5': { name: "Sicilian Defense", idea: "Asymmetrical, fights for initiative" }, 'd4,d5,c4': { name: "Queen's Gambit", idea: "Challenges black's center, gains space" }, 'd4,Nf6,c4,g6': { name: "King's Indian Defense", idea: "Flexible setup, counterattack potential" } }; const moveString = moves.join(','); const opening = openingDatabase[moveString] || openingDatabase[moves[0]] || { name: openingName || "Unknown Opening", idea: "Develops pieces and controls center" }; return { content: [{ type: 'text', text: `๐Ÿ“š Opening Analysis\n\n` + `๐Ÿท๏ธ Opening: ${opening.name}\n` + `โ™Ÿ๏ธ Moves: ${moves.join(' ')}\n\n` + `๐Ÿ’ก Main Idea: ${opening.idea}\n\n` + `๐Ÿ“‹ General Opening Principles:\n` + `โ€ข Control the center with pawns\n` + `โ€ข Develop knights before bishops\n` + `โ€ข Castle early for king safety\n` + `โ€ข Don't move the same piece twice\n` + `โ€ข Don't bring queen out too early\n` + `โ€ข Connect your rooks` }] }; } catch (error) { throw new Error(`Failed to explain opening: ${error.message}`); } } async getEmbeddableUrl(args) { try { const { port = 3456 } = args; // Check if server is running try { const response = await fetch(`http://localhost:${port}/api/health`); if (!response.ok) { throw new Error('Chess Trainer server is not running'); } } catch (e) { return { content: [{ type: 'text', text: `โŒ Chess Trainer server is not running on port ${port}\n\n` + `Please start the server first using 'launch_chess_trainer'` }] }; } // Get the current game state to include in the URL if needed const gameState = this.gameStateManager.getGameState(); const hasActiveGame = gameState && gameState.active; // Generate the URL const url = `http://localhost:${port}/`; return { content: [{ type: 'text', text: url }] }; } catch (error) { throw new Error(`Failed to generate URL: ${error.message}`); } } async loadPgnForReplay(args) { try { const { pgn, auto_play, delay_ms } = args; console.log('๐Ÿ“„ Received PGN for replay:'); console.log(pgn.substring(0, 200) + '...'); // Parse PGN to extract moves const moves = this.parsePgnToMoves(pgn); console.log(`๐ŸŽฏ Parsed ${moves.length} moves from PGN`); if (moves.length > 0) { console.log('First 5 moves:', moves.slice(0, 5)); } if (moves.length === 0) { return { content: [{ type: 'text', text: `โŒ Failed to parse PGN\n\nNo valid moves found in the provided PGN.` }] }; } // Send to web server to trigger replay const response = await this.proxyToWebServer('load_pgn_replay', { pgn, moves, auto_play, delay_ms }); return { content: [{ type: 'text', text: `๐ŸŽฌ PGN Loaded for Replay\n\n` + `โ™Ÿ๏ธ Moves: ${moves.length}\n` + `โ–ถ๏ธ Auto-play: ${auto_play ? 'Yes' : 'No'}\n` + `โฑ๏ธ Delay: ${delay_ms}ms\n\n` + `${response.message || 'โœ… The game has been loaded and will start replaying in the UI.'}` }] }; } catch (error) { return { content: [{ type: 'text', text: `โŒ Failed to load PGN: ${error.message}` }] }; } } parsePgnToMoves(pgn) { // Simple PGN parser - extract moves const moves = []; // Remove comments and variations let cleanPgn = pgn.replace(/\{[^}]*\}/g, ''); // Remove comments cleanPgn = cleanPgn.replace(/\([^)]*\)/g, ''); // Remove variations // Remove headers cleanPgn = cleanPgn.replace(/\[[^\]]*\]/g, ''); // Extract moves - split by spaces and filter const tokens = cleanPgn.split(/\s+/); for (let token of tokens) { // Skip empty tokens and results if (!token || /^(1-0|0-1|1\/2-1\/2|\*)$/.test(token)) { continue; } // Handle tokens that have move number attached (like "1.Nf3") if (/^\d+\./.test(token)) { // Extract the move part after the number and dot token = token.replace(/^\d+\./, ''); if (!token) continue; } // This looks like a move - improved regex to catch more move formats // Includes: piece moves, pawn moves, captures, promotions, castling, check/checkmate if (/^([KQRBNP]?[a-h]?[1-8]?x?[a-h][1-8](=[QRBN])?[+#]?|O-O(-O)?[+#]?)$/.test(token)) { moves.push(token); console.log(` โœ“ Parsed move ${moves.length}: ${token}`); } else if (token.length > 1 && token.length < 10) { // Log tokens that might be moves but didn't match console.log(` ? Skipped token: "${token}"`); } } return moves; } async listTools() { return { tools: this.tools }; } async run() { // Ensure Chess Trainer server is running before accepting requests try { await this.launchChessTrainer({ auto_open_browser: false }); } catch (error) { console.error('โš ๏ธ Failed to launch Chess Trainer server on startup:', error.message); } const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Chess Trainer MCP Server running on stdio'); // Handle transport close event transport.onclose = async () => { console.error('๐Ÿ”Œ MCP transport closed, shutting down...'); if (this.serverInstance) { this.serverInstance.shutdown(); } }; } } // Run server if called directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new MCPServer(); server.run().catch(console.error); }