File size: 4,522 Bytes
bea46e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import os
import random
from typing import Dict, List, Optional

import chess
import chess.pgn
from mcp.server.fastmcp import FastMCP


mcp = FastMCP(name="ChessServer", stateless_http=True)


# Simple in-memory game state keyed by session_id
games: Dict[str, Dict] = {}


def _get_session_id() -> str:
    # Provide a default single-session id when not given by client
    return "default"


def _ensure_game(session_id: str) -> Dict:
    if session_id not in games:
        games[session_id] = {
            "board": chess.Board(),
            "pgn_game": chess.pgn.Game(),
            "node": None,
        }
        games[session_id]["node"] = games[session_id]["pgn_game"]
    return games[session_id]


@mcp.tool(description="Start a new chess game. Returns initial board state.")
def start_game(session_id: Optional[str] = None, player_color: str = "white") -> Dict:
    sid = session_id or _get_session_id()
    game = {
        "board": chess.Board(),
        "pgn_game": chess.pgn.Game(),
        "node": None,
    }
    if player_color.lower() not in ("white", "black"):
        player_color = "white"
    # If AI should play first (player is black), make an AI opening move
    if player_color.lower() == "black":
        # Let engine choose a random legal move
        ai_move = random.choice(list(game["board"].legal_moves))
        game["board"].push(ai_move)
    game["node"] = game["pgn_game"]
    games[sid] = game
    return _board_state(game["board"]) | {"session_id": sid, "player_color": player_color}


def _board_state(board: chess.Board) -> Dict:
    return {
        "fen": board.fen(),
        "unicode": board.unicode(borders=True),
        "turn": "white" if board.turn else "black",
        "is_game_over": board.is_game_over(),
        "result": board.result() if board.is_game_over() else None,
        "legal_moves": [board.san(m) for m in board.legal_moves],
    }


@mcp.tool(description="Make a player move in SAN or UCI notation.")
def player_move(move: str, session_id: Optional[str] = None) -> Dict:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    board: chess.Board = g["board"]
    try:
        try:
            chess_move = board.parse_san(move)
        except ValueError:
            chess_move = chess.Move.from_uci(move)
            if chess_move not in board.legal_moves:
                raise ValueError("Illegal move")
        board.push(chess_move)
        # Update PGN
        g["node"] = g["node"].add_variation(chess_move)
        return _board_state(board) | {"last_move": board.san(chess_move)}
    except Exception as e:
        return {"error": f"Invalid move: {e}"}


@mcp.tool(description="Have the AI make a move (random legal move).")
def ai_move(session_id: Optional[str] = None) -> Dict:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    board: chess.Board = g["board"]
    if board.is_game_over():
        return _board_state(board)
    move = random.choice(list(board.legal_moves))
    board.push(move)
    g["node"] = g["node"].add_variation(move)
    return _board_state(board) | {"last_move": board.san(move)}


@mcp.tool(description="Return current board state.")
def board(session_id: Optional[str] = None) -> Dict:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    return _board_state(g["board"]) | {"session_id": sid}


@mcp.tool(description="List legal moves in SAN notation.")
def legal_moves(session_id: Optional[str] = None) -> List[str]:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    b: chess.Board = g["board"]
    return [b.san(m) for m in b.legal_moves]


@mcp.tool(description="Game status including check, checkmate, stalemate.")
def status(session_id: Optional[str] = None) -> Dict:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    b: chess.Board = g["board"]
    return {
        "turn": "white" if b.turn else "black",
        "is_check": b.is_check(),
        "is_checkmate": b.is_checkmate(),
        "is_stalemate": b.is_stalemate(),
        "is_insufficient_material": b.is_insufficient_material(),
        "is_game_over": b.is_game_over(),
        "result": b.result() if b.is_game_over() else None,
    }


@mcp.tool(description="Export game PGN.")
def pgn(session_id: Optional[str] = None) -> str:
    sid = session_id or _get_session_id()
    g = _ensure_game(sid)
    game = g["pgn_game"]
    exporter = chess.pgn.StringExporter(headers=True, variations=False, comments=False)
    return game.accept(exporter)