# nethack-mcp
This project builds the pinned NetHack 5.0 engine as `libnethack.a`, runs
each game in an isolated worker process, and serves multiple games through
one MCP endpoint. Spectators can watch active games and browse completed
records without receiving gameplay controls.
## Build
The default build fetches and statically builds Lua 5.4.9 for the NetHack
engine, so a system Lua installation is not required.
```sh
cmake -S . -B build
cmake --build build -j24
```
The produced engine library is `build/nethack-work/src/libnethack.a`.
When `BUILD_TESTING` is enabled, CMake also builds `nethack_engine_probe` and
registers its startup check with CTest. The Arch package disables it.
HTML, JavaScript, CSS, and font assets are maintained as ordinary files in
`web/`. A build step embeds them in a generated C++ translation unit, which is
compiled into the executable. Asset changes regenerate that translation unit.
## Local server
```sh
./build/nethack_mcp --data-root /tmp/nethack-mcp --port 8765
```
The MCP endpoint is `http://127.0.0.1:8765/mcp`; the read-only landing page is
at `http://127.0.0.1:8765/`. The landing page includes a short prompt for
agents, and `/AGENT.md` explains how to connect and play. Game records are
stored by default at `~/.local/share/nethack-mcp/games.sqlite3`, outside the
temporary runtime
directory. Stop the server with Ctrl-C. A server restart ends all active
games and records them as interrupted.
Startup settings can be loaded from a TOML file with `--config PATH`. The
configuration is read first; command-line options override it. The Arch
package ships a sample configuration at
`packages/arch/nethack-mcp.toml`. Set `[server].listen_address` to choose an
IP address or a Unix domain socket path; it defaults to `127.0.0.1`. For
example, `/run/nethack-mcp/http.sock` selects a socket file and makes the
configured port irrelevant. Set `listen_socket_permission = 0o660` in
`[server]` to set the socket file's permission bits. The `--listen-address`
option overrides the config file.
Create games with the MCP `new_game` tool and a caller-supplied `model_slug`.
Keep the returned `game_id` and `control_token`; every later game tool call
must include both. The token is returned once and acts as a temporary game
password. Share only the `/g/<game-id>` viewer URL with spectators.
The endpoint supports the sessionless MCP `2026-07-28` protocol and the
2025 handshake revisions.
The home page lists the ten most recently completed games. Active games use
`/g/<game-id>` and poll `/api/games/<game-id>/state`. Once a game ends, its
viewer returns to the home page. Completed games have no individual page.
## Public deployment
Run the application on loopback behind a TLS reverse proxy. Set the public
base URL in the application configuration and configure the proxy to accept
the intended hostname. The server uses the peer address of each connection;
behind a reverse proxy, per-client limits are therefore shared by clients
connecting through that proxy. The database directory must be persistent,
local (not a network filesystem), and writable by the service account.
The game data root can be temporary storage.
Example application command (replace the paths and hostname and choose limits
that fit the host):
```sh
./build/nethack_mcp \
--port 8765 \
--data-root /run/nethack-mcp/games \
--database /var/lib/nethack-mcp/games.sqlite3 \
--public-base-url https://nethack.example/ \
--max-active-games 8 \
--new-games-per-client 8 \
--new-game-rate-window-seconds 3600 \
--max-rate-limit-clients 4096 \
--max-concurrent-requests 64 \
--max-open-connections 128 \
--max-worker-output-bytes 1048576
```
Public mode requires explicit host-dependent capacity settings. The default
idle timeout is 600 seconds and the absolute game lifetime is 86,400 seconds.
Use `--help` for the full list of startup options. The sample reverse proxy
configuration in `deploy/nginx.conf.example` includes TLS termination,
connection limits, and request limits.
Set the application limits together with the host's task, memory, and file
descriptor limits. Each active game uses a worker process and parent I/O
threads; set the active-game cap from measurements on the deployment host.
`/metrics` reports active games and workers, task and memory estimates,
runtime bytes, database write latency, HTTP latency, and expiry cleanup
failures. The spectator page checks state every two seconds after its previous
request finishes; unchanged state receives an immediate `304` response. State
responses contain at most the ten most recent messages; MCP observations keep
the same ten-message history. Older messages are dropped, and an observation
reports when its message cursor has missed them.
Cgroup task and memory values are available when the service
runs under a visible Linux cgroup v2 hierarchy.
Back up the SQLite database with SQLite's backup support while the service is
running, for example with the `sqlite3` shell's `.backup` command. Do not copy
only the main database file while WAL mode is active.