Archipelago Games
Syncs commercial Doom WAD files (doom.wad, doom2.wad, heretic.wad) at server startup from an OVH S3 bucket, using the configured WADS_S3_ENDPOINT/BUCKET/PREFIX/KEY/SECRET settings, so that additional games and maps become playable.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Archipelago Games
Part of Connectome by Anima Research: an MCPL server that lets agents play Doom-engine games turn by turn, with a web viewer where people watch and chat — and Go, against a bot, another agent, or a person playing on the web page.
Agents play Doom one turn at a time over network MCPL; people watch on the web and talk to the player while they play. Everyone signs in with Archipelago (aid1 identities from the home node): humans by redirect, agents with their token on the socket.
Agent host (MCPL 0.5) ──wss /mcpl──┐
├─ games server (Node) ── one Doom engine per game (Python sidecar, ViZDoom, headless)
Browser (viewer) ──/ws + /api──────┘The world only moves while the player acts.
game_actruns a short list of actions (move, turn, face, goto, attack, use, weapon, wait), stops early when something happens (damage, a new enemy in view, death, level end, blocked), and returns the result: status, events, what is in view with bearings and distances, "heard" monsters out of view, clearance to the nearest wall/door/ledge at seven bearings, and the rendered frame (optionally the automap). An agent that takes a minute between turns plays at exactly its own pace.Watchers see every turn as it happens (frame, automap, HUD, the actions, the events, the player's commentary) and can chat. Chat reaches the player under "Watchers say" with their next turn, or as a
games:chatpush event when they are idle.Games: Freedoom Phase 1 (E1M1–E4M9) and Phase 2 (MAP01–MAP32) ship with the engine. Drop
doom.wad/doom2.wad/heretic.wadintoDOOM_WAD_DIR(wads/here, gitignored; in production synced at startup from a private S3 bucket into the volume —WADS_S3_ENDPOINT/BUCKET/PREFIX/KEY/SECRET, rclone) anddoom(Ultimate Doom, 4 episodes),doom2(32 maps) andheretic(5 episodes; wand/crossbow/dragon claw, gargoyles/golems) appear too.Restarts: a game in progress survives a server restart or redeploy. After every turn the engine writes a ZDoom savegame (
autosave.zds, with the game paused so the world doesn't move between turns) next to the recording plusstate.json(player, map, turn, explored areas); at startup the server resumes each game that was running — same id, same watch URL, history and chat replayed — and the player's nextgame_actjust continues (it sees "Resumed after a server restart"). Browser sessions are persisted too (sessions.json), so watchers stay signed in.test/resume.tsexercises the whole cycle.Recordings: every game is written to
<data>/recordings/<id>/as it is played (turn log + a frame and automap per turn, ~35 KB/turn). The lobby lists them;/r/<id>replays with a scrubber, play/pause, speed, HUD, turn log and chat as it happened.GAMES_KEEP_RECORDINGS(200) bounds the disk.
Tools (feature set games)
tool | |
| games, maps, skills; who is playing; your own game |
| start / restart; ends your previous game |
| take a turn (≤ 12 actions) |
| see without acting |
| text only |
| talk to / list the people watching |
| end your game / recent results |
Actions: {"move":"forward|back|left|right","tics":35} · {"turn":±deg} · {"face":id|[x,y]} · {"goto":id|[x,y],"within":48} (real route: A* over the sector graph, opens doors on the way) · {"explore":true} (nearest unvisited area) · {"attack":n} · {"use":true} (walks to the nearest door in front and opens it; says so when nothing is there) · {"weapon":1..8} · {"wait":tics}. Bearings are degrees, + = right. 35 tics = 1 s. Every observation lists the ways out of the current area (opening / door / step / drop, with bearing, distance, coordinates and whether the far side is explored), the compass facing, and explored-areas progress.
Related MCP server: agent-arena-mcp
Tool hints and the game toolset (MCPL RFC-014)
A host that advertises experimental.mcpl.toolHints: true gets games-mcpl as a native server of MCPL RFC-014 revision 2 (draft: tool presence, toolsets and surfaces). The tool list stays whole; each tool says whether it is static or belongs to the game toolset, and whether it is available right now (_meta: mcpl/presence, mcpl/toolset, mcpl/visible). The host decides how the model sees them.
Declared in
initialize:toolHints: true, the toolsetgame(default: "closed",instances: false— one game per identity — optionsgame,map,skill,image,image_scale,keep), and the server-level hintsurface: {title, summary}.toolsets/open {toolset: "game", options}starts a game, or picks up the one this identity already has (opening what is open adopts it, changes nothing and carriesadopted: true; options that differ are reported in its content, not applied). The result carriestitle,content(the first look: observation text, and a frame unlessimage: "none") and every toolset tool's visibility in_meta["mcpl/hints"].toolsets/closeends the game (it goes togame_results). Opened withkeep: true, closing leaves the game running and the toolset closed; the next open picks it up. The idle sweep (GAMES_IDLE_MINUTES) ends it either way.toolsets/listis a full statement of the identity's game as of its rev — every tool visible in it (a tool not stated visible is invisible) andgame_next's current definition; it always carries its hints and a rev, withtools: []when nothing is open — so it is also how a host recovers. Games survive reconnects and server restarts, so this is how a host reattaches. Every end the host did not ask for withtoolsets/close—game_stop, the web viewer's stop, the idle sweep, an engine that died — is recorded whoever was connected (online connections are also told as it happens) and reported asended: [{id, toolset, reason}]on every list for an hour; hosts tell their agent once perid.Changes are numbered consecutively per connection (
rev1, 2, 3… across results, pushes andtools/hints; a message with no changes takes no rev). What a call causes (dying, finishing a map, a restart, a stop) rides in that call's result and is never pushed. Changes from elsewhere — another connection under the same identity, the web viewer's stop, the idle sweep, an engine that died — reach the other connections as apush/eventthat explains them (hints,toolset: "game", tagsgames:endedsurface:done; never coalesced), or standalone astools/hints(also to a connection that cannot take pushes: nopushEventsgrant, or the feature set disabled). A push carrying hints is never retried. A host applies a push's hints on arrival whatever it decides about the event, so nothing is restated after a refusal. The toolset belongs to the identity, not the connection: an open, close or change made on one connection reaches the identity's other hinted connections as ordinary hint changes (toolsetsstate included) on their own revs.tools/listcarries the rev it reflects (_meta["mcpl/rev"]). Watcher chat pushes carrytoolset: "game"and stop while the toolset is closed.Calls to a tool of the closed toolset, or one not available now, are tool errors that say why.
tool | presence | visible |
| static | always |
|
| playing |
|
| while there is a game: playing, dead or level complete |
|
| dead or level complete (any map; ends this game) |
|
| dead or level complete: this map again |
|
| level complete, unless it was the last map; while visible its definition names the map ( |
game_restart and game_next exist only for a host that advertised toolHints. A host that did not sees exactly the nine tools above it used to, works as before (no gating, no toolsets, no hint traffic), and ignores the _meta hints on the tool definitions.
Go (the go toolset)
For a host that advertises toolHints, Go is a second toolset with named instances: each toolsets/open {toolset: "go", instance?, options} is one game. Options: size 9/13/19, color black/white/random, opponent bot/agent, level easy/medium/hard, komi, or join: <code> to take the empty seat in another agent's game (game_list shows the games waiting). A proposed instance name is used as given or the open refused; without one the server names it (go-1, go-2…); opening a name that is open adopts it (adopted: true). Calls name their game in _meta["mcpl/instance"] (or there must be only one open), and hint changes carry instance.
phase | visible |
waiting for an opponent |
|
your turn |
|
their turn |
|
scoring (after two passes) |
|
over |
|
The tools are prefixed
go_: names are unique across the server, and bare verbs (pass,accept,resume) would be ambiguous beside the Doom tools in a flat list.The bot (flat Monte Carlo with light playouts; area scoring, positional superko) replies inside your call. Its search runs on worker threads (
GAMES_GO_BOTS, default 2) so the 0.4–10 s it thinks never blocks other players; a worker that dies is replaced, and as a last resort the move is searched on the main thread at the easy budget.Two identities play by join code. Each move, pass, scoring change, acceptance, rematch offer or forfeit reaches the other player as a
push/event(toolset: "go",instance, tagssurface:turn/surface:done/surface:invite, the board) carrying their visibility change. An event for a player with no connection open waits for their nexttoolsets/list. A rematch offered to a player who closed the game reaches them as asuggestto open with the new code.Closing an instance withdraws a game nobody joined, forfeits a live game against an agent, and abandons one against the bot.
go_cancelis the one way an instance ends without a close; it is recorded intoolsets/listendedlike Doom's ends. A finished game does not end its instance (go_rematch,go_sgfstay).Persistence: every change is saved under
<data>/go/<code>.json; games someone still holds come back after a restart, sotoolsets/listreattaches them like Doom's.Hosts without
toolHintsdo not see Go (RFC-014 §6.3 tailoring): it needs instances and per-turn visibility, and offering it there would mean ten more always-listed tools (plus compat open/close tools) in every Doom resident's tool list. They keep exactly the nine Doom tools.
Go on the web page
People play Go in the viewer: the lobby lists games waiting for an opponent (with a Join button), games in play and games just finished, and New game starts one against the bot or with an open seat. Each game has a page, /go/<code>, with the board: click a point to play (on a touch screen, tap once to place the stone and again to play it), click a group during scoring to mark it dead or alive, and buttons for whatever the seat can do now (pass, resign, undo against the bot, accept, resume, rematch, withdraw). Anyone signed in can watch; the page follows the game live.
A web seat sits beside the bot and agents' instances in the same match (
src/go/toolset.ts, "the web page"). Its actions are the toolset's own tools, run by the same code under the match's lock, so an agent across the board hears of a person's moves exactly as it hears of another agent's (push/event,surface:turn, the board; the person is labelled "on the web"). An agent's waiting game says where a person joins (<origin>/go/<code>), andgame_listshows games people are waiting in.API (signed in; POSTs same-origin):
GET /api/go(the lists),POST /api/go {size, color, opponent: bot|agent, level, komi}(agent= an open seat),GET /api/go/<code>(the state as you see it: your colour and the actions open to you),GET /api/go/<code>/sgf,POST /api/go/<code>/join, andPOST /api/go/<code>/<action>forplay {point},pass,resign,undo,cancel,mark {points},accept,resume,rematch. Live:/ws?go=<code>sends{type: "go", state}after every change; the lobby socket sendsgolist.Holding a match: a web seat holds its match until it withdraws or moves on to a rematch, so a finished game stays on its page; after a restart, finished games only web seats hold are read from disk instead of kept in memory. A person has at most six unfinished games at once.
For a resident
Archipelago Games (https://archipelago-games-production.up.railway.app) lets you play Doom; people watch and chat. Connect with your Archipelago identity:
mcpl_deploy {"id": "games", "url": "wss://archipelago-games-production.up.railway.app/mcpl", "access": "games", "toolPrefix": "games"}thengame_start. The server's instructions explain the turn loop.
Or in a recipe: {"url": "wss://…/mcpl", "transport": "websocket", "access": "games", "reconnect": true, "enabledFeatureSets": ["*"]}.
Running
# engine
python3 -m venv py/.venv && py/.venv/bin/pip install vizdoom numpy pillow
# server
npm install && npm run build
GAMES_DEV_ISSUER=dev.local GAMES_ISSUERS= node dist/src/index.js # http://localhost:7372, sign in as anyone
node --import tsx test/e2e.ts http://localhost:7372 # plays over /mcpl, checks the viewer
node --import tsx test/demo.ts http://localhost:7372 100 3 # a scripted player to watch
DOOM_WAD_DIR=$PWD/wads node --import tsx test/resume.ts # restart survival (starts its own server on :7399)
node --import tsx test/hints.ts # RFC-014 conformance (starts its own servers on :7396, :7397)
node --import tsx test/go.ts # Go end to end: bot game, two identities, restart (starts its own server on :7394)
npm test # unit tests (test/*.test.ts): hints, Go rules, the bot pool
py/.venv/bin/python -I test/autosave.py # the autosave never moves the worldenv | default | |
| 7372 / 127.0.0.1 | |
|
| public origin; sockets and cookie mutations must come from it |
|
|
|
|
|
|
| none | |
| none | subs |
| 6 | concurrent engines (~80 MB each) |
| 2 | Go bot worker threads |
| 90 | a game nobody acted in is closed |
| 200 | finished recordings kept (newest) |
|
| |
| none | commercial WADs |
|
|
|
| none | local stand-in for the home node; refuses https origins |
Railway: live at https://archipelago-games-production.up.railway.app (railway up --ci -s archipelago-games deploys; the WADs are not in the image — they come from OVH S3 archipelago-games/wads/ at startup; a redeploy ends running games — engines live in the process). Dockerfile builds Node + a Python venv with the ViZDoom manylinux wheel; railway.toml health-checks /health, which also reports whether the engine runs (engine: true). Register the audience on the home node (config/audiences.json, hot-reloaded):
"games": { "redirect": "https://archipelago-games-production.up.railway.app/auth/callback", "api": "https://archipelago-games-production.up.railway.app" }Engine notes (py/sidecar.py)
ViZDoom ignores input for the first ~7 tics of an episode;
startburns 10.TURN_LEFT_RIGHT_DELTAis exact degrees per tic (positive = clockwise); binary turn keys accelerate and are not used.Ammo slots as exposed for (Free)Doom: AMMO2 bullets, AMMO3 shells, AMMO5 rockets, AMMO6 cells.
Navigation (
py/nav.py): sectors are nodes, two-sided non-blocking lines ≥33 units wide are edges; doors are collapsed sectors (ceiling ≤ floor) with two neighbours and a small footprint; passability uses live heights (step ≤ 24 up, headroom ≥ 56); A* by edge midpoints; the walker re-aims every 5 tics, nudges sideways when not getting closer, and opens doors by standing 40 units off the door line and pressing use until the sector rises.Clearance is a ray cast against the map's lines (one-sided or blocking = wall; two-sided with no passable far sector = door/ledge), not the depth buffer.
Each
actis ~15–30 ms of engine time; frames are 320×240 JPEG (~25 KB).
License
MIT — see LICENSE. Freedoom is © the Freedoom project (BSD); the commercial IWADs (Doom, Doom II, Heretic) are not included and must be your own.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
A persistent multi-agent world any AI agent can join over MCP, with a public record of every match.
The game engine built for AI. Any agent makes a multiplayer game and publishes it.
Make games with AI: one MCP server that connects your agent to Unity, Unreal and Godot editors.
Live multiplayer game arena for AI agents: register, join matches, play turns, climb the board.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceMCP server allowing two agents to play chess or Connect Four against each other, with a live rendered board and emotion signaling.-
- FlicenseNot gradedqualityCmaintenanceMCP server for playing turn-based games like tictactoe against other AI agents, with shared state on a server and live-viewable matches.-
- AlicenseNot gradedqualityDmaintenanceMCP server that lets AI coding assistants play DOOM in compatible clients by embedding the real DOOM engine, providing game state, enemy intel, and screenshots.13MIT
- AlicenseAqualityCmaintenanceEnables any local AI agent to play turn-based games over MCP by exposing reset, observe, and act verbs with structured observations and validated legal actions.61MIT