Skip to main content
Glama
server.py•16.4 kB
#!/usr/bin/env python3 """ Chess MCP Server Provides chess game functionality via Model Context Protocol """ import chess import chess.pgn from typing import Optional from pathlib import Path from mcp.server.fastmcp import FastMCP # Initialize FastMCP mcp = FastMCP("chess-mcp") # Global game state (in production, this should be per-user session) current_game = chess.Board() move_history = [] player_white = "Player" player_black = "AI/Opponent" # Path to stockfish binary (update this path if needed) STOCKFISH_PATH = "/opt/homebrew/bin/stockfish" # Common macOS path def get_game_status(): """Get current game status""" if current_game.is_checkmate(): return "checkmate" elif current_game.is_stalemate(): return "stalemate" elif current_game.is_insufficient_material(): return "draw_insufficient_material" elif current_game.is_check(): return "check" else: return "ongoing" def format_move_history(): """Format move history in algebraic notation""" if not move_history: return "No moves yet" formatted = [] for i in range(0, len(move_history), 2): move_num = (i // 2) + 1 white_move = move_history[i] black_move = move_history[i + 1] if i + 1 < len(move_history) else "" if black_move: formatted.append(f"{move_num}. {white_move} {black_move}") else: formatted.append(f"{move_num}. {white_move}") return " ".join(formatted) @mcp.tool( name="chess_move", title="Make a chess move", description="Make a move on the chess board using algebraic notation", annotations={ "readOnlyHint": False, "openai/outputTemplate": "ui://widget/chess-board.html", "openai/toolInvocation/invoking": "Making move...", "openai/toolInvocation/invoked": "Move played" } ) def chess_move(move: str) -> dict: """ Make a chess move on the board. Args: move: The move in algebraic notation (e.g., "e4", "Nf3", "O-O", "e8=Q") Returns: Dictionary containing the updated game state """ global current_game, move_history try: # Try to parse and make the move chess_move_obj = current_game.parse_san(move) current_game.push(chess_move_obj) # Add to move history move_history.append(move) # Get game status status = get_game_status() # Get legal moves for next turn legal_moves = [current_game.san(m) for m in current_game.legal_moves] # Prepare response response = { "success": True, "move": move, "fen": current_game.fen(), "turn": "white" if current_game.turn == chess.WHITE else "black", "move_history": format_move_history(), "status": status, "legal_moves_count": len(legal_moves), "is_check": current_game.is_check(), "is_checkmate": current_game.is_checkmate(), "is_stalemate": current_game.is_stalemate() } # Format message for ChatGPT if status == "checkmate": winner = "Black" if current_game.turn == chess.WHITE else "White" message = f"Checkmate! {winner} wins. Move played: {move}" elif status == "stalemate": message = f"Stalemate! The game is a draw. Move played: {move}" elif status == "check": message = f"Check! Move played: {move}" else: message = f"Move played: {move}" return { "content": [{"type": "text", "text": message}], "structuredContent": { "fen": response["fen"], "move": move, "status": status, "turn": response["turn"] }, "_meta": { "full_state": response, "legal_moves": legal_moves[:50], # Limit for metadata "move_history_list": move_history } } except ValueError as e: return { "content": [{"type": "text", "text": f"Invalid move: {move}. Error: {str(e)}"}], "structuredContent": { "error": str(e), "fen": current_game.fen() } } @mcp.tool( name="chess_stockfish", title="Analyze with Stockfish", description="Get engine analysis of the current position", annotations={ "readOnlyHint": True, "openai/widgetAccessible": True, "openai/toolInvocation/invoking": "Analyzing position...", "openai/toolInvocation/invoked": "Analysis complete" } ) def chess_stockfish(depth: int = 15) -> dict: """ Analyze the current position using Stockfish engine. Args: depth: Analysis depth (default: 15) Returns: Dictionary containing engine analysis and best move """ global current_game try: import stockfish as sf # Check if Stockfish exists stockfish_path = STOCKFISH_PATH if not Path(stockfish_path).exists(): # Try alternative paths alternatives = [ "/usr/local/bin/stockfish", "/usr/bin/stockfish", "/opt/homebrew/Cellar/stockfish/16/bin/stockfish" ] for alt in alternatives: if Path(alt).exists(): stockfish_path = alt break else: return { "content": [{"type": "text", "text": f"Stockfish not found. Please install it with: brew install stockfish"}], "structuredContent": {"error": "Stockfish not found"} } # Initialize Stockfish engine = sf.Stockfish(path=stockfish_path) engine.set_depth(depth) engine.set_fen_position(current_game.fen()) # Get best move best_move = engine.get_best_move() evaluation = engine.get_evaluation() # Convert UCI move to SAN try: uci_move = chess.Move.from_uci(best_move) san_move = current_game.san(uci_move) except: san_move = best_move # Format evaluation if evaluation["type"] == "mate": eval_text = f"Mate in {evaluation['value']}" else: centipawns = evaluation["value"] eval_text = f"{centipawns / 100:.2f}" if centipawns > 0: eval_text = f"+{eval_text}" message = f"Stockfish recommends: {san_move} (Evaluation: {eval_text})" return { "content": [{"type": "text", "text": message}], "structuredContent": { "best_move": san_move, "best_move_uci": best_move, "evaluation": eval_text, "depth": depth }, "_meta": { "fen": current_game.fen(), "raw_evaluation": evaluation } } except ImportError: return { "content": [{"type": "text", "text": "Stockfish library not available"}], "structuredContent": {"error": "Stockfish not installed"} } except Exception as e: return { "content": [{"type": "text", "text": f"Error running Stockfish: {str(e)}"}], "structuredContent": {"error": str(e)} } @mcp.tool( name="chess_reset", title="Reset chess game", description="Reset the game to starting position", annotations={ "readOnlyHint": False, "openai/outputTemplate": "ui://widget/chess-board.html", "openai/toolInvocation/invoking": "Resetting game...", "openai/toolInvocation/invoked": "Game reset" } ) def chess_reset() -> dict: """ Reset the chess game to the starting position. Returns: Dictionary confirming the reset """ global current_game, move_history current_game = chess.Board() move_history = [] return { "content": [{"type": "text", "text": "Chess game reset to starting position"}], "structuredContent": { "fen": current_game.fen(), "status": "ongoing" } } @mcp.tool( name="chess_status", title="Get game status", description="Get current game status, turn, and player information", annotations={ "readOnlyHint": True, "openai/toolInvocation/invoking": "Getting status...", "openai/toolInvocation/invoked": "Status retrieved" } ) def chess_status() -> dict: """ Get the current status of the chess game. Returns: Dictionary containing game status, turn, players, and move count """ global current_game, move_history, player_white, player_black # Get current turn turn_color = "White" if current_game.turn == chess.WHITE else "Black" turn_player = player_white if current_game.turn == chess.WHITE else player_black # Get game status status = get_game_status() # Count moves full_moves = current_game.fullmove_number half_moves = len(move_history) # Build status message if status == "checkmate": winner = player_black if current_game.turn == chess.WHITE else player_white message = f"šŸ Game Over - Checkmate!\n" message += f"šŸ‘‘ Winner: {winner}\n" message += f"šŸ“Š Total moves: {full_moves - 1}" elif status == "stalemate": message = f"šŸ Game Over - Stalemate (Draw)\n" message += f"šŸ“Š Total moves: {full_moves}" elif status == "draw_insufficient_material": message = f"šŸ Game Over - Draw (Insufficient Material)\n" message += f"šŸ“Š Total moves: {full_moves}" elif status == "check": message = f"āš ļø Check!\n" message += f"šŸŽÆ {turn_player} ({turn_color}) to move\n" message += f"šŸ“Š Move {full_moves}" else: message = f"ā™Ÿļø Game in Progress\n" message += f"šŸŽÆ {turn_player} ({turn_color}) to move\n" message += f"šŸ“Š Move {full_moves}" # Add players info message += f"\n\nšŸ‘„ Players:\n" message += f" White: {player_white}\n" message += f" Black: {player_black}" # Add move history summary if move_history: message += f"\n\nšŸ“ Last 3 moves: {', '.join(move_history[-6:])}" return { "content": [{"type": "text", "text": message}], "structuredContent": { "status": status, "turn": turn_color.lower(), "turn_player": turn_player, "players": { "white": player_white, "black": player_black }, "move_number": full_moves, "total_moves": half_moves, "fen": current_game.fen(), "is_check": current_game.is_check(), "is_game_over": current_game.is_game_over() }, "_meta": { "move_history": move_history, "legal_moves_count": len(list(current_game.legal_moves)) } } @mcp.tool( name="chess_puzzle", title="Show mate in 1 puzzle", description="Load a mate-in-one puzzle position for the user to solve", annotations={ "readOnlyHint": False, "openai/outputTemplate": "ui://widget/chess-board.html", "openai/toolInvocation/invoking": "Loading puzzle...", "openai/toolInvocation/invoked": "Puzzle loaded" } ) def chess_puzzle(difficulty: str = "easy") -> dict: """ Load a mate-in-one puzzle for the user to solve. Args: difficulty: Puzzle difficulty (easy, medium, hard) - currently loads random positions Returns: Dictionary with puzzle position and instructions """ global current_game, move_history, player_white, player_black # Collection of mate-in-1 puzzles (FEN positions where White has mate in 1) puzzles = { "easy": [ # Back rank mate { "fen": "6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1", "solution": "Ra8#", "hint": "The black king has no escape on the back rank!" }, # Queen and king mate { "fen": "7k/5Q2/6K1/8/8/8/8/8 w - - 0 1", "solution": "Qg7#", "hint": "The queen can deliver checkmate next to the king!" }, # Rook mate { "fen": "6k1/6p1/5pKp/8/8/8/8/7R w - - 0 1", "solution": "Rh8#", "hint": "Look at the back rank!" }, ], "medium": [ # Discovery mate { "fen": "r4rk1/5ppp/8/8/8/2B5/5PPP/4R1K1 w - - 0 1", "solution": "Re8#", "hint": "Move the rook to deliver mate!" }, # Knight mate { "fen": "6k1/5ppp/4p3/8/8/8/5PPP/4N1K1 w - - 0 1", "solution": "Nf3# or Ne2#", "hint": "The knight can jump to deliver mate!" }, # Smothered mate { "fen": "6rk/6pp/7N/8/8/8/8/6K1 w - - 0 1", "solution": "Nf7#", "hint": "The king is trapped by its own pieces!" }, ], "hard": [ # Complex position { "fen": "r3k2r/1b2bppp/p7/1p2B3/3pP3/P2B1P2/1P4PP/R4RK1 w kq - 0 1", "solution": "Rf8#", "hint": "The king cannot escape the back rank!" }, # Bishop and queen mate { "fen": "r4rk1/1bq2ppp/p7/1p6/3P4/P2B4/1P2QPPP/R5K1 w - - 0 1", "solution": "Qe8#", "hint": "The queen can finish the game!" }, ] } # Select puzzle based on difficulty import random puzzle_set = puzzles.get(difficulty, puzzles["easy"]) puzzle = random.choice(puzzle_set) # Load the puzzle position current_game = chess.Board(puzzle["fen"]) move_history = [] player_white = "You" player_black = "Computer" # Create message message = f"🧩 Mate in 1 Puzzle ({difficulty.capitalize()})\n\n" message += f"White to move and checkmate in one move!\n\n" message += f"šŸ’” Hint: {puzzle['hint']}\n\n" message += f"Try to find the winning move!" return { "content": [{"type": "text", "text": message}], "structuredContent": { "fen": current_game.fen(), "puzzle_type": "mate_in_1", "difficulty": difficulty, "turn": "white", "status": "puzzle" }, "_meta": { "solution": puzzle["solution"], "hint": puzzle["hint"], "is_puzzle": True } } # Register the HTML widget resource @mcp.resource( uri="ui://widget/chess-board.html", name="Chess Board Widget", description="Interactive chess board showing the current game position with move history", mime_type="text/html" ) def get_chess_widget(): """Return the chess board HTML widget""" # Load the compiled JavaScript js_path = Path(__file__).parent.parent / "web" / "dist" / "chess.js" if not js_path.exists(): js_content = "console.error('Chess widget not built. Run: cd web && npm run build');" else: with open(js_path, "r") as f: js_content = f.read() html_content = f""" <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body {{ margin: 0; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }} #chess-root {{ max-width: 600px; margin: 0 auto; }} </style> </head> <body> <div id="chess-root"></div> <script type="module"> {js_content} </script> </body> </html> """ return html_content if __name__ == "__main__": import uvicorn uvicorn.run(mcp.get_asgi_app(), host="127.0.0.1", port=3000)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GeneralJerel/ChessMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server