mcp-chess
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-chessExplain why a6 is a mistake"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-chess
An MCP (Model Context Protocol) server that exposes Stockfish (via
python-chess) to LLM chat clients. Built for analyzing your own games and
understanding why the engine's move is better than yours.
"Why is X the best move and not the one I played? It looks just as good to me."
Runs on the host as a systemd user service, serving both MCP transports (Streamable HTTP + SSE) on a single port. Tested with Open WebUI (podman container) and opencode.
Table of contents
What it does
The server wraps Stockfish via the python-chess library and exposes four MCP
tools that an LLM can call during chat. The tools are designed around the kinds
of questions a chess player asks when reviewing their games:
"What's the best move here?" →
analyze_position"I played X, was Y better?" →
compare_moves"Why is the engine's move better than mine?" →
explain_choice"Review this whole game" →
analyze_game
All scores are from the perspective of the side to move (positive = good for the mover). Centipawn loss (CPL) is clamped to 0 to suppress negative search noise between multipv and single-pv evals.
Tools reference
analyze_position
Get Stockfish's top N candidate moves for a position.
Parameters:
Name | Type | Default | Description |
| string | — | FEN, PGN, or space-separated SAN |
| int | 22 | Stockfish search depth |
| int | 5 | Number of candidate lines to return |
Returns: The position FEN, side to move, legal move count, check status,
and a list of the top multipv lines — each with SAN, UCI, score (cp or mate),
depth, and the principal variation in SAN.
Example call:
await analyze_position(fen_or_moves="1.e4 e5 2.Nf3 Nc6 3.Bb5", depth=18, multipv=3)Example output (abbreviated):
{
"fen": "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3",
"turn": "black",
"legal_move_count": 29,
"in_check": false,
"lines": [
{
"move_san": "Bc5",
"move_uci": "f8c5",
"score_cp": -22,
"score_mate": null,
"depth": 18,
"pv_san": ["Bc5", "O-O", "Nf6", "d3", "a6", "c3", ...]
},
...
]
}compare_moves
Evaluate a set of candidate moves at the same position. For each move, plays it and evaluates the resulting position, then reports centipawn loss vs the engine's best.
Parameters:
Name | Type | Default | Description |
| string | — | FEN, PGN, or SAN |
| string[] | — | Candidate moves (SAN or UCI) |
| int | 22 | Stockfish depth |
Returns: The engine's best move (SAN/UCI/eval/PV) plus per-move results: SAN, UCI, resulting FEN, eval (cp/mate) from mover's POV, centipawn loss vs best, and a short PV.
Example:
await compare_moves(
fen_or_moves="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
moves=["e4", "d4", "Nf3", "a3"],
depth=15
)explain_choice
The primary tool for "why is X best and not the move I played?". Returns the engine's best move(s) with eval and PV, plus the player's move with its eval after being played, centipawn loss vs best, and side-by-side PV prefixes so the LLM can describe the resulting plans.
Parameters:
Name | Type | Default | Description |
| string | — | FEN, PGN, or SAN |
| string | — | The move played (SAN or UCI) |
| int | 22 | Stockfish depth |
| int | 4 | Top engine alternatives to surface |
Returns: Best move + alternatives + player move eval + CPL + PVs.
Example:
await explain_choice(
fen_or_moves="1.e4 e5 2.Nf3 Nc6 3.Bb5",
player_move="a6",
depth=18
)analyze_game
Review a full chess game move-by-move with Stockfish. Designed for "analyze this game" requests. Plays through every move, evaluates each position before and after, computes centipawn loss for the played move vs the engine's best, and classifies each move.
Only surfaces the critical moves (CPL ≥ threshold) in full detail, plus a compact summary of the whole game. This keeps the response small enough for the chat model to reason over while still flagging the moments that decided the game.
Parameters:
Name | Type | Default | Description |
| string | — | Full PGN string (with or without headers) |
| int | 16 | Stockfish depth per move |
| int | 75 | Only moves with CPL ≥ this appear in |
Move classifications (Lichess-style thresholds):
Label | CPL range |
| 0 |
| 1–24 |
| 25–74 |
| 75–149 |
| ≥ 150 |
Returns:
headers— PGN headers (White, Black, Result, Opening, etc.) if presentresult— Game result string ("1-0","0-1","1/2-1/2")move_count— Total plies in the gamesummary— Per-side:{blunders, mistakes, inaccuracies, good_moves, best_moves, total_cpl, avg_cpl, moves}critical_moves— Moves where CPL ≥ threshold, each with full detail (best PV, played PV, FEN after, eval before/after)all_moves— Compact list of every move (number, side, SAN, best SAN, CPL, classification)
Example:
await analyze_game(
pgn='[Event "rated rapid game"] [White "khalednafea"] [Black "dvip"] [Result "0-1"] 1. e4 f5 2. exf5 Nf6 3. Bc4 d5 4. Bb3 h6 ... 0-1',
depth=16,
classification_threshold=75
)Example output (abbreviated):
{
"headers": {"White": "khalednafea", "Black": "dvip", "Result": "0-1", "Opening": "Duras Gambit"},
"result": "0-1",
"move_count": 50,
"summary": {
"white": {"blunders": 4, "mistakes": 4, "inaccuracies": 7, "avg_cpl": 96.1, "moves": 25},
"black": {"blunders": 4, "mistakes": 5, "inaccuracies": 3, "avg_cpl": 69.8, "moves": 25}
},
"critical_moves": [
{
"move_number": 1,
"side": "black",
"played_san": "f5",
"best_san": "e6",
"centipawn_loss": 147,
"classification": "mistake",
"best_pv_san": ["e6", "d4", "d5", "Nc3", "Nf6"],
"played_pv_san": ["f5", "exf5", "Nf6", "Be2", "Rg8"]
},
...
],
"all_moves": [
{"move_number": 1, "side": "white", "played_san": "e4", "best_san": "e4", "centipawn_loss": 0, "classification": "best"},
{"move_number": 1, "side": "black", "played_san": "f5", "best_san": "e6", "centipawn_loss": 147, "classification": "mistake"},
...
]
}Architecture
┌──────────────────────────────────────────────────────────────┐
│ Host (Fedora) │
│ │
│ systemd user service (mcp-chess.service) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ uv run mcp-chess │ │
│ │ ┌─────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ FastMCP app │───▶│ python-chess → Stockfish 18 │ │ │
│ │ │ (uvicorn) │ │ (asyncio.Lock serialized) │ │ │
│ │ └──────┬──────┘ └──────────────────────────────┘ │ │
│ │ │ │ │
│ │ SSE: /sse Streamable HTTP: /mcp │ │
│ │ 127.0.0.1:8765 │ │
│ └─────────┬───────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────┴──────────┐ ┌──────────────────────────────┐ │
│ │ Open WebUI │ │ opencode │ │
│ │ (podman, net=host) │ │ (native, reads opencode.json)│ │
│ │ POST /mcp │ │ GET /sse │ │
│ └────────────────────┘ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘Key design points:
Both transports on one process. The server mounts the SSE app's routes (
/sse,/messages) into the Streamable HTTP app (/mcp) so a single uvicorn process serves both. This matters because Open WebUI and opencode prefer different transports.Engine access is serialized with an
asyncio.Lock. Concurrent tool calls (the LLM often fires several in parallel) would otherwise interleaveposition/goUCI commands on the single Stockfish process, corrupting its state and crashing it (NNUE assertion failures).Threads=1in Stockfish. Multipv search uses SF's internal threading; settingThreads=1avoids resource contention and the crash above.Lingering enabled (
loginctl enable-linger), so the service starts at boot and survives logout.
Installation
Prerequisites
Stockfish installed system-wide:
sudo dnf install stockfish # Fedora # Verify: stockfish # should print "Stockfish 18 by the Stockfish developers"uv (Python package manager):
curl -LsSf https://astral.sh/uv/install.sh | sh
Steps
The project is already set up at /home/dvip/Documents/chess_stuff/mcp-chess/.
To recreate from scratch:
mkdir -p mcp-chess/src/mcp_chess
cd mcp-chess
uv init --no-readme --no-workspace --bareCreate pyproject.toml:
[project]
name = "mcp-chess"
version = "0.1.0"
description = "MCP server exposing Stockfish analysis to Open WebUI"
requires-python = ">=3.11"
dependencies = [
"mcp[cli]>=1.2.0",
"python-chess>=1.999",
"uvicorn>=0.30",
]
[project.scripts]
mcp-chess = "mcp_chess.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_chess"]
[tool.uv]
package = trueCreate src/mcp_chess/__init__.py and src/mcp_chess/server.py (the latter is
the full server implementation — see the existing file).
Install dependencies:
uv syncRunning it
As a systemd user service (recommended)
Create ~/.config/systemd/user/mcp-chess.service:
[Unit]
Description=Chess MCP server (Stockfish via python-chess) for Open WebUI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/dvip/Documents/chess_stuff/mcp-chess
Environment=STOCKFISH_PATH=/usr/bin/stockfish
Environment=STOCKFISH_THREADS=1
Environment=STOCKFISH_HASH_MB=128
Environment=STOCKFISH_DEFAULT_DEPTH=22
Environment=STOCKFISH_DEFAULT_MULTI_PV=5
Environment=MCP_HOST=127.0.0.1
Environment=MCP_PORT=8765
Environment=LOGLEVEL=INFO
ExecStart=/home/dvip/.local/bin/uv run mcp-chess
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.targetEnable and start it:
systemctl --user daemon-reload
systemctl --user enable --now mcp-chess.service
loginctl enable-linger $USER # survive logout, start at bootCheck status:
systemctl --user status mcp-chess
journalctl --user -u mcp-chess -fManually (for development)
cd /home/dvip/Documents/chess_stuff/mcp-chess
uv run mcp-chess
# Or with custom env:
STOCKFISH_DEFAULT_DEPTH=18 MCP_PORT=9000 uv run mcp-chessIntegrating with Open WebUI
The Open WebUI container runs with --network=host, so 127.0.0.1 is
reachable from inside it.
Step 1: Add the tool server
Open http://localhost:8080 as admin.
Go to Admin Panel → Workspace → External Tool Servers → + New.
Fill in:
Name:
chessTransport / Connection Type:
Streamable HTTPURL:
http://127.0.0.1:8765/mcp
Save. After a few seconds the status dot should turn green.
If your Open WebUI only offers
SSEas a transport, use URLhttp://127.0.0.1:8765/sseinstead.
Step 2: Fix the auth type (important!)
Open WebUI's build_tool_server_headers always emits
Authorization: Bearer <key> when auth_type == "bearer", even if key is
empty — producing Bearer (trailing space), which httpx rejects as an
illegal header value. This causes "Failed to connect to MCP server" errors at
chat time, even though the verify step succeeds.
Fix: Set the connection's Auth Type to session (not bearer), or put
any non-empty string in the Key field if you must use bearer.
If you need to fix this in the DB directly:
podman exec open-webui python -c "
import sqlite3, json
db = sqlite3.connect('/app/backend/data/webui.db')
rows = db.execute(\"SELECT key, value FROM config WHERE key='tool_server.connections'\").fetchall()
k, v = rows[0]
conns = json.loads(v)
for c in conns:
if c.get('info', {}).get('id') == 'chess':
c['auth_type'] = 'session'
db.execute('UPDATE config SET value=? WHERE key=?', (json.dumps(conns), k))
db.commit()
print('done')
"
podman restart open-webuiStep 3: Enable tools in chat
Start a new conversation.
Click the tools button (next to the model selector).
Enable the
chessserver.
Step 4: Verify
Watch the logs while sending a message:
journalctl --user -u mcp-chess -fYou should see POST /mcp HTTP/1.1 200 OK and Processing request of type CallToolRequest. If you see 405 Method Not Allowed, you're using the wrong
URL/transport combination (see Troubleshooting).
Integrating with opencode
opencode reads MCP server config from ~/.config/opencode/opencode.json.
Create or edit that file:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"chess": {
"type": "remote",
"url": "http://127.0.0.1:8765/sse",
"enabled": true
}
}
}opencode's
remotetype uses SSE transport, so point at/sse. If you prefer Streamable HTTP, check the opencode docs for the currenttypevalue.
Verify:
opencode mcp list
# Should show: chess ✓ connectedThe tools are now available to all opencode agents. No restart needed — opencode loads config on each invocation.
Config reference: https://opencode.ai/config.json
Verifying it works
Quick curl checks
# SSE handshake (should return an endpoint event)
curl -sN http://127.0.0.1:8765/sse | head -2
# Streamable HTTP initialize (should return MCP init response)
curl -s -X POST http://127.0.0.1:8765/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'End-to-end MCP client test
cd /home/dvip/Documents/chess_stuff/mcp-chess
uv run python - <<'PY'
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("http://127.0.0.1:8765/mcp") as (r, w, _):
async with ClientSession(r, w) as s:
await s.initialize()
tools = await s.list_tools()
print("TOOLS:", [t.name for t in tools.tools])
# Try a real call:
result = await s.call_tool("explain_choice", {
"fen_or_moves": "1.e4 e5 2.Nf3 Nc6 3.Bb5",
"player_move": "a6",
"depth": 14
})
for c in result.content:
print(c.text[:300])
asyncio.run(main())
PYExpected output:
TOOLS: ['analyze_position', 'compare_moves', 'explain_choice', 'analyze_game']
{"fen": "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3", ...From inside the Open WebUI container
podman exec open-webui curl -s -X POST http://127.0.0.1:8765/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'Configuration
All configuration is via environment variables in the systemd service file
(~/.config/systemd/user/mcp-chess.service). Edit and restart with
systemctl --user restart mcp-chess.
Variable | Default | Description |
|
| Path to the Stockfish binary |
|
| Stockfish |
|
| Stockfish |
|
| Default depth for |
|
| Default multipv for |
|
| Bind address |
|
| Listen port |
|
| Python log level |
Tuning depth vs. speed
Depth | Use case | Speed |
12–14 | Quick checks, full-game review | Fast (~0.5s/position) |
16–18 | Default for | Moderate (~1s/position) |
20–22 | Default for single-position tools | Slower (~2–4s/position) |
26+ | Deep analysis | Slow (~10s+/position) |
For analyze_game on a 40-move game at depth 16, expect ~60–90 seconds total.
Troubleshooting
"405 Method Not Allowed" in logs
HTTP Request: POST http://127.0.0.1:8765/sse "HTTP/1.1 405 Method Not Allowed"Cause: Transport/URL mismatch. The client is POSTing to /sse (SSE only
accepts GET), or GETing /mcp (Streamable HTTP expects POST).
Fix: Use /mcp with Streamable HTTP transport, /sse with SSE transport.
"Failed to connect to MCP server" in Open WebUI chat
The verify step succeeds (logs show POST /mcp 200 OK), but chat-time
connection fails.
Cause: Open WebUI sends Authorization: Bearer (empty token) when
auth_type == "bearer" with empty key. httpx rejects the malformed header.
Fix: Set the connection's auth type to session, or put a non-empty key.
See Integrating with Open WebUI → Step 2.
Stockfish crashes (coredump, NNUE assertion)
Assertion '__n < this->size()' failed.
Process (stockfish) dumped core.Cause: Concurrent analyse() calls interleave UCI commands on the single
engine, corrupting its state.
Fix: Already fixed — the server serializes all engine access with an
asyncio.Lock and uses Threads=1. If you still see this, check that you
haven't set STOCKFISH_THREADS higher than 1, and that the service is running
the latest code:
systemctl --user restart mcp-chess"invalid san: '1.e4'" or PGN parse errors
Cause: Older versions of the PGN parser didn't handle move numbers glued to
moves (1.e4) or PGN header tags ([Event "..."]).
Fix: Already fixed — the parser now strips [...] headers,
\d+\.+ move numbers, {...} comments, and $N NAGs. Verify:
cd /home/dvip/Documents/chess_stuff/mcp-chess
uv run python -c "
from mcp_chess.server import _normalize_fen_or_moves
b = _normalize_fen_or_moves('1.e4 e5 2.Nf3 Nc6 3.Bb5')
print(b.fen())
"Service won't start
# Check logs
journalctl --user -u mcp-chess -n 30 --no-pager
# Common issues:
# - uv not found: ensure /home/dvip/.local/bin/uv exists
# - stockfish not found: ensure /usr/bin/stockfish exists
# - port in use: change MCP_PORTExample prompts
In Open WebUI or opencode
Single position:
Analyze
r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3and tell me the top 3 moves.
Why not my move:
In the Italian opening after 1.e4 e5 2.Nf3 Nc6 3.Bb5, I played 3...a6 — was that the best move? If not, why is the engine's choice better?
Compare candidates:
Compare
Nf3,d4, anda3from the starting position at depth 20.
Full game review:
[Paste full PGN with headers] Can you analyze this game? Tell me where I went wrong and what I should have played instead.
Specific game moment:
In this game: 1.e4 f5 2.exf5 Nf6 3.Bc4 d5 4.Bb3 h6 5.Qf3 Nc6 6.c3 d4 7.cxd4 Nxd4 — was 7.cxd4 a blunder? What should white have played?
About Maia
The maia-1500.pb.gz file in the parent directory (/home/dvip/Documents/chess_stuff/)
is a Leela/Lc0 weights file for human-like move prediction at a ~1500 Elo level.
It is not wired into this server because running it requires lc0 (Leela
Chess Zero), not Stockfish. If you want human-likely-move suggestions added as a
tool:
Install Lc0:
sudo dnf install lczeroA
maia_suggest(fen, rating)tool can be added that shells out tolc0with the appropriate Maia weights file. The rating parameter selects from Maia's 1100–1900 rating models.
Stockfish tells you what's objectively best. Maia tells you what a human at a given rating would most likely play — useful for understanding practical chances and preparing opening repertoire against typical opponents.
File layout
/home/dvip/Documents/chess_stuff/
├── maia-1500.pb.gz # Leela weights (not used by this server)
└── mcp-chess/
├── pyproject.toml # uv project definition
├── uv.lock # pinned dependencies
├── README.md # this file
└── src/mcp_chess/
├── __init__.py
└── server.py # the MCP server (FastMCP + python-chess)
~/.config/systemd/user/
└── mcp-chess.service # systemd user service
~/.config/opencode/
└── opencode.json # opencode MCP config (remote → /sse)Tech stack
Stockfish 18 — the engine (system package)
python-chess — Python chess library (board, move parsing, UCI client)
MCP Python SDK (
mcp[cli]) — FastMCP server frameworkuvicorn — ASGI server
uv — Python package manager
systemd — user service management
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/dvip1/chess_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server