Spaces:
Running
Running
Joffrey Thomas
commited on
Commit
·
bea46e9
1
Parent(s):
08ec225
add chess
Browse files- chess_game/chess_server.py +136 -0
- chess_game/static/style.css +12 -0
- chess_game/templates/index.html +47 -0
- chess_game/templates/room.html +94 -0
- chess_game/web_app.py +99 -0
- requirements.txt +2 -1
- server.py +5 -0
chess_game/chess_server.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import random
|
3 |
+
from typing import Dict, List, Optional
|
4 |
+
|
5 |
+
import chess
|
6 |
+
import chess.pgn
|
7 |
+
from mcp.server.fastmcp import FastMCP
|
8 |
+
|
9 |
+
|
10 |
+
mcp = FastMCP(name="ChessServer", stateless_http=True)
|
11 |
+
|
12 |
+
|
13 |
+
# Simple in-memory game state keyed by session_id
|
14 |
+
games: Dict[str, Dict] = {}
|
15 |
+
|
16 |
+
|
17 |
+
def _get_session_id() -> str:
|
18 |
+
# Provide a default single-session id when not given by client
|
19 |
+
return "default"
|
20 |
+
|
21 |
+
|
22 |
+
def _ensure_game(session_id: str) -> Dict:
|
23 |
+
if session_id not in games:
|
24 |
+
games[session_id] = {
|
25 |
+
"board": chess.Board(),
|
26 |
+
"pgn_game": chess.pgn.Game(),
|
27 |
+
"node": None,
|
28 |
+
}
|
29 |
+
games[session_id]["node"] = games[session_id]["pgn_game"]
|
30 |
+
return games[session_id]
|
31 |
+
|
32 |
+
|
33 |
+
@mcp.tool(description="Start a new chess game. Returns initial board state.")
|
34 |
+
def start_game(session_id: Optional[str] = None, player_color: str = "white") -> Dict:
|
35 |
+
sid = session_id or _get_session_id()
|
36 |
+
game = {
|
37 |
+
"board": chess.Board(),
|
38 |
+
"pgn_game": chess.pgn.Game(),
|
39 |
+
"node": None,
|
40 |
+
}
|
41 |
+
if player_color.lower() not in ("white", "black"):
|
42 |
+
player_color = "white"
|
43 |
+
# If AI should play first (player is black), make an AI opening move
|
44 |
+
if player_color.lower() == "black":
|
45 |
+
# Let engine choose a random legal move
|
46 |
+
ai_move = random.choice(list(game["board"].legal_moves))
|
47 |
+
game["board"].push(ai_move)
|
48 |
+
game["node"] = game["pgn_game"]
|
49 |
+
games[sid] = game
|
50 |
+
return _board_state(game["board"]) | {"session_id": sid, "player_color": player_color}
|
51 |
+
|
52 |
+
|
53 |
+
def _board_state(board: chess.Board) -> Dict:
|
54 |
+
return {
|
55 |
+
"fen": board.fen(),
|
56 |
+
"unicode": board.unicode(borders=True),
|
57 |
+
"turn": "white" if board.turn else "black",
|
58 |
+
"is_game_over": board.is_game_over(),
|
59 |
+
"result": board.result() if board.is_game_over() else None,
|
60 |
+
"legal_moves": [board.san(m) for m in board.legal_moves],
|
61 |
+
}
|
62 |
+
|
63 |
+
|
64 |
+
@mcp.tool(description="Make a player move in SAN or UCI notation.")
|
65 |
+
def player_move(move: str, session_id: Optional[str] = None) -> Dict:
|
66 |
+
sid = session_id or _get_session_id()
|
67 |
+
g = _ensure_game(sid)
|
68 |
+
board: chess.Board = g["board"]
|
69 |
+
try:
|
70 |
+
try:
|
71 |
+
chess_move = board.parse_san(move)
|
72 |
+
except ValueError:
|
73 |
+
chess_move = chess.Move.from_uci(move)
|
74 |
+
if chess_move not in board.legal_moves:
|
75 |
+
raise ValueError("Illegal move")
|
76 |
+
board.push(chess_move)
|
77 |
+
# Update PGN
|
78 |
+
g["node"] = g["node"].add_variation(chess_move)
|
79 |
+
return _board_state(board) | {"last_move": board.san(chess_move)}
|
80 |
+
except Exception as e:
|
81 |
+
return {"error": f"Invalid move: {e}"}
|
82 |
+
|
83 |
+
|
84 |
+
@mcp.tool(description="Have the AI make a move (random legal move).")
|
85 |
+
def ai_move(session_id: Optional[str] = None) -> Dict:
|
86 |
+
sid = session_id or _get_session_id()
|
87 |
+
g = _ensure_game(sid)
|
88 |
+
board: chess.Board = g["board"]
|
89 |
+
if board.is_game_over():
|
90 |
+
return _board_state(board)
|
91 |
+
move = random.choice(list(board.legal_moves))
|
92 |
+
board.push(move)
|
93 |
+
g["node"] = g["node"].add_variation(move)
|
94 |
+
return _board_state(board) | {"last_move": board.san(move)}
|
95 |
+
|
96 |
+
|
97 |
+
@mcp.tool(description="Return current board state.")
|
98 |
+
def board(session_id: Optional[str] = None) -> Dict:
|
99 |
+
sid = session_id or _get_session_id()
|
100 |
+
g = _ensure_game(sid)
|
101 |
+
return _board_state(g["board"]) | {"session_id": sid}
|
102 |
+
|
103 |
+
|
104 |
+
@mcp.tool(description="List legal moves in SAN notation.")
|
105 |
+
def legal_moves(session_id: Optional[str] = None) -> List[str]:
|
106 |
+
sid = session_id or _get_session_id()
|
107 |
+
g = _ensure_game(sid)
|
108 |
+
b: chess.Board = g["board"]
|
109 |
+
return [b.san(m) for m in b.legal_moves]
|
110 |
+
|
111 |
+
|
112 |
+
@mcp.tool(description="Game status including check, checkmate, stalemate.")
|
113 |
+
def status(session_id: Optional[str] = None) -> Dict:
|
114 |
+
sid = session_id or _get_session_id()
|
115 |
+
g = _ensure_game(sid)
|
116 |
+
b: chess.Board = g["board"]
|
117 |
+
return {
|
118 |
+
"turn": "white" if b.turn else "black",
|
119 |
+
"is_check": b.is_check(),
|
120 |
+
"is_checkmate": b.is_checkmate(),
|
121 |
+
"is_stalemate": b.is_stalemate(),
|
122 |
+
"is_insufficient_material": b.is_insufficient_material(),
|
123 |
+
"is_game_over": b.is_game_over(),
|
124 |
+
"result": b.result() if b.is_game_over() else None,
|
125 |
+
}
|
126 |
+
|
127 |
+
|
128 |
+
@mcp.tool(description="Export game PGN.")
|
129 |
+
def pgn(session_id: Optional[str] = None) -> str:
|
130 |
+
sid = session_id or _get_session_id()
|
131 |
+
g = _ensure_game(sid)
|
132 |
+
game = g["pgn_game"]
|
133 |
+
exporter = chess.pgn.StringExporter(headers=True, variations=False, comments=False)
|
134 |
+
return game.accept(exporter)
|
135 |
+
|
136 |
+
|
chess_game/static/style.css
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
3 |
+
margin: 0; padding: 0; background: #0b1020; color: #f4f4f8;
|
4 |
+
}
|
5 |
+
.container { max-width: 900px; margin: 0 auto; padding: 16px; }
|
6 |
+
a { color: #93c5fd; text-decoration: none; }
|
7 |
+
button { background: #2563eb; color: white; border: 0; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
|
8 |
+
button:hover { background: #1d4ed8; }
|
9 |
+
input, select { padding: 8px; border-radius: 6px; border: 1px solid #334155; background: #0f172a; color: #e5e7eb; }
|
10 |
+
ul { padding-left: 20px; }
|
11 |
+
|
12 |
+
|
chess_game/templates/index.html
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8">
|
5 |
+
<title>Chess Rooms</title>
|
6 |
+
<link rel="stylesheet" href="{{ base_path }}/static/style.css">
|
7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
8 |
+
<style>
|
9 |
+
pre { white-space: pre-wrap; }
|
10 |
+
</style>
|
11 |
+
<link rel="icon" href="data:,">
|
12 |
+
<meta property="og:title" content="Chess MCP" />
|
13 |
+
<meta property="og:description" content="Play chess against an AI via MCP" />
|
14 |
+
<meta property="og:image" content="/static/embed.png" />
|
15 |
+
<meta name="twitter:card" content="summary_large_image" />
|
16 |
+
<meta name="twitter:title" content="Chess MCP" />
|
17 |
+
<meta name="twitter:description" content="Play chess against an AI via MCP" />
|
18 |
+
<meta name="twitter:image" content="/static/embed.png" />
|
19 |
+
<meta name="robots" content="noai" />
|
20 |
+
<meta name="ai-access-control" content="none" />
|
21 |
+
</head>
|
22 |
+
<body>
|
23 |
+
<div class="container">
|
24 |
+
<h1>Chess MCP Rooms</h1>
|
25 |
+
<form method="post" action="{{ base_path }}/create_room">
|
26 |
+
<button type="submit">Create Room</button>
|
27 |
+
</form>
|
28 |
+
|
29 |
+
<h2>Join Room</h2>
|
30 |
+
<form onsubmit="event.preventDefault(); location.href='{{ base_path }}/room/' + document.getElementById('rid').value;">
|
31 |
+
<input id="rid" placeholder="Enter room id" required />
|
32 |
+
<button type="submit">Join</button>
|
33 |
+
</form>
|
34 |
+
|
35 |
+
<h2>Active Rooms</h2>
|
36 |
+
<ul>
|
37 |
+
{% for rid, info in rooms.items() %}
|
38 |
+
<li><a href="{{ base_path }}/room/{{ rid }}">{{ rid }}</a></li>
|
39 |
+
{% else %}
|
40 |
+
<li>No rooms yet. Create one!</li>
|
41 |
+
{% endfor %}
|
42 |
+
</ul>
|
43 |
+
</div>
|
44 |
+
</body>
|
45 |
+
</html>
|
46 |
+
|
47 |
+
|
chess_game/templates/room.html
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8">
|
5 |
+
<title>Chess Room {{ room_id }}</title>
|
6 |
+
<link rel="stylesheet" href="{{ base_path }}/static/style.css">
|
7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
8 |
+
<style>
|
9 |
+
pre.board { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; line-height: 1.2; }
|
10 |
+
.controls { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
|
11 |
+
.status { margin-top: 8px; }
|
12 |
+
</style>
|
13 |
+
<link rel="icon" href="data:,">
|
14 |
+
</head>
|
15 |
+
<body>
|
16 |
+
<div class="container">
|
17 |
+
<a href="{{ base_path }}/">← Back</a>
|
18 |
+
<h1>Room: {{ room_id }}</h1>
|
19 |
+
|
20 |
+
<div class="controls">
|
21 |
+
<select id="color">
|
22 |
+
<option value="white">White</option>
|
23 |
+
<option value="black">Black</option>
|
24 |
+
</select>
|
25 |
+
<button id="startBtn">Start / Reset</button>
|
26 |
+
<input id="moveInput" placeholder="Enter SAN or UCI (e4, Nf3, e7e5)" />
|
27 |
+
<button id="moveBtn">Make Move</button>
|
28 |
+
<button id="aiBtn">AI Move</button>
|
29 |
+
<button id="copyPgnBtn">Copy PGN</button>
|
30 |
+
</div>
|
31 |
+
|
32 |
+
<pre id="board" class="board">{{ state.unicode }}</pre>
|
33 |
+
<div class="status">
|
34 |
+
<div><strong>Turn:</strong> <span id="turn"></span></div>
|
35 |
+
<div><strong>Result:</strong> <span id="result"></span></div>
|
36 |
+
<div><strong>Legal Moves:</strong> <span id="legal"></span></div>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
|
40 |
+
<script>
|
41 |
+
const roomId = {{ room_id | tojson }};
|
42 |
+
const basePath = {{ base_path | tojson }};
|
43 |
+
|
44 |
+
async function getJSON(path, options) {
|
45 |
+
const res = await fetch(path, options);
|
46 |
+
if (!res.ok) {
|
47 |
+
const msg = await res.text();
|
48 |
+
throw new Error(msg || res.statusText);
|
49 |
+
}
|
50 |
+
return res.json();
|
51 |
+
}
|
52 |
+
|
53 |
+
async function refresh() {
|
54 |
+
const state = await getJSON(`${basePath}/room/${roomId}/board`);
|
55 |
+
document.getElementById('board').textContent = state.unicode;
|
56 |
+
const s = await getJSON(`${basePath}/room/${roomId}/status`);
|
57 |
+
document.getElementById('turn').textContent = s.turn + (s.is_check ? ' (check)' : '');
|
58 |
+
document.getElementById('result').textContent = s.is_game_over ? (s.result || 'game over') : '-';
|
59 |
+
const legal = await getJSON(`${basePath}/room/${roomId}/legal_moves`);
|
60 |
+
document.getElementById('legal').textContent = legal.join(', ');
|
61 |
+
}
|
62 |
+
|
63 |
+
document.getElementById('startBtn').addEventListener('click', async () => {
|
64 |
+
const color = document.getElementById('color').value;
|
65 |
+
await getJSON(`${basePath}/room/${roomId}/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_color: color })});
|
66 |
+
await refresh();
|
67 |
+
});
|
68 |
+
|
69 |
+
document.getElementById('moveBtn').addEventListener('click', async () => {
|
70 |
+
const mv = document.getElementById('moveInput').value.trim();
|
71 |
+
if (!mv) return;
|
72 |
+
try {
|
73 |
+
await getJSON(`${basePath}/room/${roomId}/player_move`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ move: mv })});
|
74 |
+
} catch (e) { alert('Move error: ' + e.message); }
|
75 |
+
await refresh();
|
76 |
+
});
|
77 |
+
|
78 |
+
document.getElementById('aiBtn').addEventListener('click', async () => {
|
79 |
+
await getJSON(`${basePath}/room/${roomId}/ai_move`, { method: 'POST' });
|
80 |
+
await refresh();
|
81 |
+
});
|
82 |
+
|
83 |
+
document.getElementById('copyPgnBtn').addEventListener('click', async () => {
|
84 |
+
const data = await getJSON(`${basePath}/room/${roomId}/pgn`);
|
85 |
+
await navigator.clipboard.writeText(data.pgn || '');
|
86 |
+
alert('PGN copied to clipboard');
|
87 |
+
});
|
88 |
+
|
89 |
+
refresh();
|
90 |
+
</script>
|
91 |
+
</body>
|
92 |
+
</html>
|
93 |
+
|
94 |
+
|
chess_game/web_app.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import uuid
|
3 |
+
|
4 |
+
from fastapi import FastAPI, Request, HTTPException
|
5 |
+
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
6 |
+
from fastapi.staticfiles import StaticFiles
|
7 |
+
from fastapi.templating import Jinja2Templates
|
8 |
+
|
9 |
+
from .chess_server import start_game as m_start_game, player_move as m_player_move, ai_move as m_ai_move, board as m_board, legal_moves as m_legal_moves, status as m_status, pgn as m_pgn
|
10 |
+
|
11 |
+
|
12 |
+
BASE_DIR = os.path.dirname(__file__)
|
13 |
+
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
|
14 |
+
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
15 |
+
|
16 |
+
|
17 |
+
app = FastAPI()
|
18 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
19 |
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
20 |
+
|
21 |
+
|
22 |
+
# Simple room registry for UI convenience (MCP state keyed by room_id in chess_server)
|
23 |
+
rooms = {}
|
24 |
+
|
25 |
+
|
26 |
+
def generate_room_id() -> str:
|
27 |
+
return uuid.uuid4().hex[:8]
|
28 |
+
|
29 |
+
|
30 |
+
@app.get("/", response_class=HTMLResponse)
|
31 |
+
def home(request: Request):
|
32 |
+
base_path = request.scope.get('root_path', '')
|
33 |
+
return templates.TemplateResponse("index.html", {"request": request, "rooms": rooms, "base_path": base_path})
|
34 |
+
|
35 |
+
|
36 |
+
@app.post("/create_room")
|
37 |
+
def create_room(request: Request):
|
38 |
+
# Choose a new room id and redirect to room page; player picks color there
|
39 |
+
room_id = generate_room_id()
|
40 |
+
rooms[room_id] = {"id": room_id}
|
41 |
+
return RedirectResponse(url=f"{request.scope.get('root_path', '')}/room/{room_id}", status_code=303)
|
42 |
+
|
43 |
+
|
44 |
+
@app.get("/room/{room_id}", response_class=HTMLResponse)
|
45 |
+
def room_page(room_id: str, request: Request):
|
46 |
+
base_path = request.scope.get('root_path', '')
|
47 |
+
# Fetch board (this also ensures MCP game exists lazily)
|
48 |
+
state = m_board(session_id=room_id)
|
49 |
+
if room_id not in rooms:
|
50 |
+
rooms[room_id] = {"id": room_id}
|
51 |
+
return templates.TemplateResponse("room.html", {"request": request, "room_id": room_id, "state": state, "base_path": base_path})
|
52 |
+
|
53 |
+
|
54 |
+
# --- API: Map to MCP tools ---
|
55 |
+
|
56 |
+
|
57 |
+
@app.post("/room/{room_id}/start")
|
58 |
+
def api_start(room_id: str, payload: dict | None = None):
|
59 |
+
player_color = (payload or {}).get("player_color", "white")
|
60 |
+
state = m_start_game(session_id=room_id, player_color=player_color)
|
61 |
+
return JSONResponse(state)
|
62 |
+
|
63 |
+
|
64 |
+
@app.post("/room/{room_id}/player_move")
|
65 |
+
def api_player_move(room_id: str, payload: dict):
|
66 |
+
if not payload or "move" not in payload:
|
67 |
+
raise HTTPException(status_code=400, detail="Missing 'move' in body")
|
68 |
+
result = m_player_move(move=payload["move"], session_id=room_id)
|
69 |
+
if "error" in result:
|
70 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
71 |
+
return JSONResponse(result)
|
72 |
+
|
73 |
+
|
74 |
+
@app.post("/room/{room_id}/ai_move")
|
75 |
+
def api_ai_move(room_id: str):
|
76 |
+
result = m_ai_move(session_id=room_id)
|
77 |
+
return JSONResponse(result)
|
78 |
+
|
79 |
+
|
80 |
+
@app.get("/room/{room_id}/board")
|
81 |
+
def api_board(room_id: str):
|
82 |
+
return JSONResponse(m_board(session_id=room_id))
|
83 |
+
|
84 |
+
|
85 |
+
@app.get("/room/{room_id}/legal_moves")
|
86 |
+
def api_legal_moves(room_id: str):
|
87 |
+
return JSONResponse(m_legal_moves(session_id=room_id))
|
88 |
+
|
89 |
+
|
90 |
+
@app.get("/room/{room_id}/status")
|
91 |
+
def api_status(room_id: str):
|
92 |
+
return JSONResponse(m_status(session_id=room_id))
|
93 |
+
|
94 |
+
|
95 |
+
@app.get("/room/{room_id}/pgn")
|
96 |
+
def api_pgn(room_id: str):
|
97 |
+
return JSONResponse({"pgn": m_pgn(session_id=room_id)})
|
98 |
+
|
99 |
+
|
requirements.txt
CHANGED
@@ -4,4 +4,5 @@ pydantic>=2.7.0
|
|
4 |
selectolax>=0.3.15
|
5 |
fastapi
|
6 |
jinja2
|
7 |
-
Pillow
|
|
|
|
4 |
selectolax>=0.3.15
|
5 |
fastapi
|
6 |
jinja2
|
7 |
+
Pillow
|
8 |
+
python-chess>=1.999
|
server.py
CHANGED
@@ -5,7 +5,9 @@ from fastapi.staticfiles import StaticFiles
|
|
5 |
from fastapi.templating import Jinja2Templates
|
6 |
from pokemon.pokemon_server import mcp as pokemon_mcp
|
7 |
from geoguessr.geo_server import mcp as geogussr_mcp
|
|
|
8 |
from geoguessr.web_app import app as geoguessr_app
|
|
|
9 |
import os
|
10 |
|
11 |
|
@@ -15,6 +17,7 @@ async def lifespan(app: FastAPI):
|
|
15 |
async with contextlib.AsyncExitStack() as stack:
|
16 |
await stack.enter_async_context(pokemon_mcp.session_manager.run())
|
17 |
await stack.enter_async_context(geogussr_mcp.session_manager.run())
|
|
|
18 |
yield
|
19 |
|
20 |
|
@@ -42,9 +45,11 @@ async def index(request: Request):
|
|
42 |
|
43 |
app.mount("/geoguessr", geogussr_mcp.streamable_http_app())
|
44 |
app.mount("/Pokemon", pokemon_mcp.streamable_http_app())
|
|
|
45 |
|
46 |
# Mount GeoGuessr FastAPI web app (UI + API)
|
47 |
app.mount("/geoguessr_app", geoguessr_app)
|
|
|
48 |
|
49 |
PORT = int(os.environ.get("PORT", "10000"))
|
50 |
|
|
|
5 |
from fastapi.templating import Jinja2Templates
|
6 |
from pokemon.pokemon_server import mcp as pokemon_mcp
|
7 |
from geoguessr.geo_server import mcp as geogussr_mcp
|
8 |
+
from chess_game.chess_server import mcp as chess_mcp
|
9 |
from geoguessr.web_app import app as geoguessr_app
|
10 |
+
from chess_game.web_app import app as chess_app
|
11 |
import os
|
12 |
|
13 |
|
|
|
17 |
async with contextlib.AsyncExitStack() as stack:
|
18 |
await stack.enter_async_context(pokemon_mcp.session_manager.run())
|
19 |
await stack.enter_async_context(geogussr_mcp.session_manager.run())
|
20 |
+
await stack.enter_async_context(chess_mcp.session_manager.run())
|
21 |
yield
|
22 |
|
23 |
|
|
|
45 |
|
46 |
app.mount("/geoguessr", geogussr_mcp.streamable_http_app())
|
47 |
app.mount("/Pokemon", pokemon_mcp.streamable_http_app())
|
48 |
+
app.mount("/chess", chess_mcp.streamable_http_app())
|
49 |
|
50 |
# Mount GeoGuessr FastAPI web app (UI + API)
|
51 |
app.mount("/geoguessr_app", geoguessr_app)
|
52 |
+
app.mount("/chess_app", chess_app)
|
53 |
|
54 |
PORT = int(os.environ.get("PORT", "10000"))
|
55 |
|