Skip to main content
Glama
prepaser

pure-llm-chess-mcp

by prepaser
README.md
# Pure LLM Chess MCP

An MCP intended to allow a purely LLM-based system to follow chess rules without external information such as Stockfish.

Requires Node.js 24 or newer.

```sh
npx -y pure-llm-chess-mcp
```

Or install the CLI globally:

```sh
npm install -g pure-llm-chess-mcp
pure-llm-chess-mcp
```

## Connect

For a stdio MCP client, use `npx`:

```json
{
  "mcpServers": {
    "chess": {
      "command": "npx",
      "args": ["-y", "pure-llm-chess-mcp"]
    }
  }
}
```

For Streamable HTTP:

```sh
npx -y pure-llm-chess-mcp --transport http --port 3000
```

Connect to `http://127.0.0.1:3000/mcp`. The plain HTTP MCP transport is loopback-only. Games live in memory until the process exits. Export JSON to save them.

Load a local rule plugin:

```sh
npx -y pure-llm-chess-mcp --plugin /absolute/path/to/variant.mjs
```

`--plugin` may be repeated. Plugins are trusted local JavaScript modules loaded at startup, with the same privileges as the server. The server does not execute code supplied through MCP or imported archives. See [the plugin guide](docs/plugins.md) to load the included three-player hex example or write your own rules.

### Manual TLS

Supply a PEM certificate chain and its unencrypted PEM private key:

```sh
npx -y pure-llm-chess-mcp --transport https --port 3000 \
  --tls-cert /path/to/fullchain.pem --tls-key /path/to/privkey.pem
```

The default endpoint is `https://127.0.0.1:3000/mcp`. The certificate must cover the hostname or IP used by the client, and its issuer must be trusted by that client. Certificates are read at startup; restart after replacing them.

To listen outside loopback, specify both the bind address and the certificate's hostname:

```sh
npx -y pure-llm-chess-mcp --transport https --host 0.0.0.0 \
  --domain chess.example.com --port 443 \
  --tls-cert /path/to/fullchain.pem --tls-key /path/to/privkey.pem
```

Connect to `https://chess.example.com/mcp`. When `--domain` is set, only that Host and, if present, Origin hostname is accepted.

### Automatic HTTPS

The server obtains and renews certificates using ACME inside the Node process:

```sh
npx -y pure-llm-chess-mcp --auto-https chess.example.com \
  --acme-email admin@example.com
```

Point the domain's DNS records at the server and make TCP port 80 reachable for HTTP-01 validation. By default the HTTPS endpoint is `https://chess.example.com/mcp` on port 443. The process must be able to bind both ports. Startup waits for a usable certificate; it fails if initial issuance fails.

Account keys and certificates are stored on disk and reused on restart. The server renews certificates after two thirds of their validity period and replaces the TLS context without restarting or losing games. Renewal errors retain the current certificate and retry after five minutes. SIGINT and SIGTERM stop both listeners and cancel any in-progress ACME request.

Storage is isolated by domain and ACME directory, so staging and production certificates cannot be confused. Private files use mode `0600`; a lock prevents concurrent writers. An unclean termination may leave a `.lock` file containing the process ID. If startup reports a stale lock, confirm that process has stopped before removing the exact lock file shown in the error.

| Option | Default | Purpose |
| --- | --- | --- |
| `--auto-https <domain>` | none | Enable automatic HTTPS for one public DNS name |
| `--acme-email <email>` | none | ACME account contact |
| `--https-dir <path>` | `$XDG_DATA_HOME/pure-llm-chess-mcp/https`, or `~/.local/share/pure-llm-chess-mcp/https` | Persistent certificate and account storage |
| `--host <address>` | `0.0.0.0` in automatic mode | Bind address for both listeners |
| `--port <port>` | `443` in automatic mode | HTTPS listener |
| `--http-port <port>` | `80` | HTTP-01 challenge and GET/HEAD redirect listener |
| `--acme-staging` | off | Use Let's Encrypt's staging CA; its certificates are not publicly trusted |
| `--acme-directory <url>` | Let's Encrypt production | Custom ACME directory; cannot be combined with staging |

Preserve the storage directory across restarts. A custom HTTP listener port requires forwarding public TCP port 80 to it for validation. Clients must reach the configured HTTPS port. Only HTTP-01 is supported; wildcard certificates and DNS-01 are not supported. Automatic mode cannot be combined with manual TLS options or `--domain`.

The implementation uses [acme-client](https://github.com/publishlab/node-acme-client) for the ACME protocol. See [Let's Encrypt's HTTP-01 requirements](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) for deployment details. ACME directory URLs must use HTTPS; loopback HTTP URLs are accepted for local testing.

HTTPS encrypts traffic; it does not add client authentication or separate games by user. For a private deployment, restrict network access or put an authenticated gateway in front of the endpoint.

### Codex

Register the npm package as a stdio server:

```sh
codex mcp add chess -- npx -y pure-llm-chess-mcp
codex mcp list
```

Start a new Codex session and use `/mcp` to check the connection. Codex starts the server automatically; Node.js 24 or newer and `npx` must be available on its `PATH`. Try: "Use the chess MCP to create a standard game and show the board with Unicode pieces."

Alternatively, add this to `~/.codex/config.toml`, or `.codex/config.toml` for a trusted project:

```toml
[mcp_servers.chess]
command = "npx"
args = ["-y", "pure-llm-chess-mcp"]
```

For HTTP or HTTPS, start the server in a separate terminal, then register its URL instead of the stdio command:

```sh
codex mcp add chess --url http://127.0.0.1:3000/mcp
```

For HTTPS, use `--url https://chess.example.com/mcp` (including a port if nonstandard). Keep the server running while using Codex. Remove the registration with `codex mcp remove chess`.

See the [official Codex MCP documentation](https://developers.openai.com/codex/mcp) for configuration details.

## Tools

| Tool | Arguments | Result |
| --- | --- | --- |
| `list_variants` | none | Rule IDs, versions, descriptions, schemas, supported formats |
| `new_game` | `variant="standard"`, `options={}` | New game and state |
| `get_state` | `game_id`, `ascii=false`, `unicode=false` | Current state; optional `ascii` string with ASCII or Unicode pieces |
| `play_move` | `game_id`, `move`, `allow_illegal=false` | `legal`, `applied`, `reason`, resulting `state` |
| `edit_board` | `game_id`, `changes`, optional `turn`, `rule_state` | Updated state |
| `import_game` | `format`, `data`, `variant="standard"` for FEN, optional `options` for FEN/PGN | A new game |
| `export_game` | `game_id`, `format` | `{format, data}` |

Built-in variant IDs are `standard`, `chess960`, `antichess`, `atomic`, `crazyhouse`, `horde`, `kingofthehill`, `racingkings`, and `threecheck`. Chess960 accepts `options.index` from 0 to 959 (default 518).

Standard moves use UCI coordinates, such as `e2e4` and `e7e8q`. Inspect `list_variants` for each variant's move schema. The hex example uses objects such as `{"from":"-2,0","to":"-1,0"}`.

An edit maps cells to a piece or `null`:

```json
{
  "game_id": "<game-id>",
  "changes": {
    "e1": null,
    "e4": {"kind": "queen", "owner": "white"}
  },
  "turn": "black"
}
```

State includes cell IDs, a `pieces` map, registered `players`, `turn`, variant-specific `rule_state`, `valid`, `issues`, `check`, `outcome`, `claimable_draws`, and a revision. Normal chess pieces use `pawn`, `knight`, `bishop`, `rook`, `queen`, and `king`; owners are `white` and `black`.

`get_state` with `ascii=true` adds a board like this without replacing structured state:

```text
8  r n b q k b n r
7  p p p p p p p p
6  . . . . . . . .
5  . . . . . . . .
4  . . . . . . . .
3  . . . . . . . .
2  P P P P P P P P
1  R N B Q K B N R

   a b c d e f g h
```

Set `unicode=true` to use Unicode chess pieces (`♔♕♖♗♘♙` and `♚♛♜♝♞♟`). This also requests board output, so `ascii=true` is optional. The rendered text remains in the `ascii` response field; coordinates and empty cells are unchanged. Plugins that do not implement Unicode rendering keep their ASCII output.

```json
{"game_id": "<game-id>", "unicode": true}
```

## Legality and editing

A rejected move leaves the game unchanged. `legal` is `true` or `false` for valid positions and `null` when rules cannot evaluate the position. `applied` independently reports whether the board changed.

With `allow_illegal=true`, legal moves still follow all normal rules. Otherwise the server relocates the source piece, overwrites the destination (including friendly pieces and kings), and advances the current turn. It does not infer illegal castling, en passant, or promotion effects. Empty sources, nonexistent cells, and identical source/destination remain errors. Use `edit_board` for creation, deletion, replacement, and compound edits.

Forced changes reset the repetition segment. Built-in chess variants also clear castling rights, en passant, and the halfmove counter. Edits preserve the turn unless explicitly changed and may explicitly replace `rule_state`. Historical events remain in JSON archives.

Rule-invalid boards, including boards with missing kings, remain editable and printable. Structurally invalid data, such as unknown cells or piece kinds, is rejected. Failed operations do not publish partial state changes.

The reserved object key and cell ID `__proto__` are rejected explicitly, including inside nested JSON data. The same text is allowed as an ordinary string value.

Standard automatic outcomes include checkmate, stalemate, insufficient material, fivefold repetition, and the 75-move rule. Claimable threefold/50-move draws are reported; claim and resignation tools are not included.

## Import and export

- **JSON** is the full-fidelity format: format version, exact variant ID/version, options, metadata, initial state, applied events, and final state. Imports replay events and compare the resulting state. The same plugin version must already be installed. Code is never embedded or loaded from an archive.
- **FEN** exchanges a current position, without history. A syntactically valid but rule-invalid position can be imported. Specify `variant` when importing a nonstandard position.
- **PGN** exchanges a single game's headers, mainline, and result for supported variants. Comments, annotations/NAGs, variations, and multiple games are rejected. Games with edits or actual forced moves require JSON. A PGN `Variant` header selects the rule set.

PGN headers require string values without control characters and valid tag names. JSON metadata that cannot be represented is rejected on PGN export rather than discarded. Position and variant headers are derived from the actual game state. Standard aliases such as `normal`, `classical`, and `wild/0` are accepted; an exact installed plugin ID takes precedence over an alias.

A new game ID is assigned on every import. Input/output is data, not server filesystem paths. No file is read or written through these tools. Unsupported formats fail explicitly instead of dropping information.

For FEN/PGN imports, pass any creation options required by the selected variant in `options`. They are validated, passed to the plugin's importer, and preserved in JSON exports. JSON imports restore their archived options and reject an `options` override.

Accepted FEN promoted-piece markers (`~`) survive unrelated moves and follow the marked piece when it moves; captures remove them. This changes replay semantics outside Crazyhouse, so those built-in adapters now use version `2`. Their version `1` JSON archives are rejected with `VERSION_MISMATCH` rather than silently replayed with different state. Crazyhouse remains version `1`.

PGN declarations such as resignation or an agreed draw are preserved as terminal results, even when the board itself is not terminal. Such declarations are recorded in JSON history. Forced moves and edits clear the declaration and resume from the changed position; a declared result contradicting a rules-based outcome is rejected.

Archive replay rejects another terminal result declaration unless an intervening edit or forced move has cleared the previous one.

## Development

From a source checkout:

```sh
npm ci
npm run check
npm test
npm start
```

`npm test` builds the project before running the tests. TLS tests require `openssl` to generate temporary certificates. Use `npm run build` to build without testing.

`npm pack` builds automatically before creating the package. The package includes the runtime, type declarations, example plugin, documentation, license, and TypeScript source; tests and local workspace files are excluded.

The implementation separates the game service, rule adapters, and MCP transports. See [the plugin contract](docs/plugins.md) and [the hex example](examples/hex.ts) for nonrectangular boards, new pieces, and multiplayer rules.

Dependency versions are pinned in `package-lock.json`.

## License

Licensed under the GNU Affero General Public License v3.0 only (`AGPL-3.0-only`). See [LICENSE](LICENSE).

chessops is licensed separately under [GPL-3.0-or-later](https://github.com/niklasf/chessops/blob/main/LICENSE.txt).

TDQS

A3.6/5.0

Scored across 7 tools

Disambiguation5/5

Each tool targets a distinct action: creating, inspecting, moving, editing, exporting, importing, and listing variants. There is no meaningful overlap between them.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (new_game, get_state, play_move, edit_board, export_game, import_game, list_variants). The naming is uniform and predictable.

Tool Count5/5

Seven tools is well-scoped for a chess MCP server: it covers game lifecycle, state inspection, move execution, board editing, import/export, and variant discovery without unnecessary bloat.

Completeness5/5

The tool surface covers the full chess workflow: create, inspect, move, edit, import/export, and discover variants. No obvious dead ends or missing core operations for the stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues