BareGit
Commits
Clone:
Name Latest commit Last update
📂 cmake
📂 deploy
📂 designs
📂 include
📂 packages
📂 src
📂 web
📄 .gitignore Implement NetHack MCP server and engine integration 29 hours ago
📄 CMakeLists.txt Gate engine probe behind BUILD_TESTING 2 hours ago
📄 README.md Allow shared database directory permissions 43 minutes ago

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.

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

./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/. 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):

./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.