The Heist: Building a Co-op Game with WebMCP
September 7, 2026
I built a small heist game to try WebMCP. You watch the map and tell a browser agent where to go. The agent moves the crew, cuts the power, opens the vault, and tries to get the loot back to the exit.
The catch is that its tools don't show it the map. You can see the guards and their sight lines, the laser grid, and the vault code. The agent gets its own position and a description of the cells immediately around it. To play together, you need to tell it what you see.
Where WebMCP fits
WebMCP lets a page expose
JavaScript tools to a browser agent. Each tool has a name, a description, an
input schema, and a function to execute. For this game, that means the agent
can call heist_move_crew with a direction rather than look for a movement
button on the page.
Here is the shape of a move tool. dispatchCommand updates the game's shared
store and returns the result of the move:
const context = document.modelContext ?? navigator.modelContext;
if (context) {
await context.registerTool({
name: "heist_move_crew",
description:
"Move one cell north, south, east, or west. Costs one tick, even if blocked.",
inputSchema: {
type: "object",
properties: {
direction: {
type: "string",
enum: ["north", "south", "east", "west"],
},
},
required: ["direction"],
},
execute: async ({ direction }) => ({
content: [{
type: "text",
text: JSON.stringify(dispatchCommand({ kind: "move", direction })),
}],
}),
});
}
The current Chrome API documentation
uses document.modelContext. The game checks that first and falls back to
navigator.modelContext for older implementations. Browser support is still
experimental, so the page also checks whether the API is available before
showing the game.
Play a round
Use a browser and agent setup that supports WebMCP. Press Start heist, then ask the agent:
Call heist_get_briefing. I'll watch the map and tell you where to move.
Give directions one step at a time until you get a feel for the patrols. When
you reach the vault, read the three-digit code from the plaque. After grabbing
the loot, you still need to reach the exit and call heist_exfiltrate.
Time only passes when the agent takes an action. Each move, wait, or attempted action costs one tick, and the guards move with it. Walking into a wall wastes a tick too. Reading the briefing or checking crew status is free. Malformed tool inputs are rejected without advancing time.
Cutting power gives you six ticks to cross the laser gap. If the crew is still on the laser when it switches back on, the run ends. The locked door near the top of the map offers another way back, but its hack panel is on the vault side.
Three wrong vault codes trigger the alarm and extend the guards' sight range.
You can still enter the correct code afterward. The status field
wrongCodesUntilAlarm counts down to the alarm, not to a lockout.
Replay seed resets the same code and patrol timing so you can try a different route. New seed prepares a fresh run. Seed 4217 is the default if you want to compare attempts with someone else.
What the agent knows
The map is drawn on a canvas. Its guard positions and vault code aren't included in the DOM text or the crew's status response. That gives you something useful to contribute when playing with an agent that relies on text and tool results.
This is a game rule, not a security boundary. An agent with screenshots can read the map too. We used screenshots during testing to cover the lookout's role, then called the WebMCP tools to carry out each move. That checks the game, but it doesn't test how well an agent follows a human partner's directions.
The tool descriptions explain each action's cost and requirements. For example, the door tool tells the agent which side has the panel, and the vault tool tells it to ask for the code. Those details matter when the description is what the agent reads before choosing an action.
How the game is put together
The engine is plain TypeScript. initGame creates the layout and uses the seed
to choose the code and patrol offsets. applyCommand takes a state and a
command, applies the action, advances the guards, and checks whether the crew
was caught.
buildPerception selects the information returned to the agent. Keeping that
in one function makes it straightforward to test that tool results don't
include guard positions or the code.
React and the WebMCP callbacks share one store. A tool call updates that store,
and React subscribes through useSyncExternalStore. The canvas redraws from
the same state. Tools are registered when the game mounts; an AbortController
removes them when you leave the page.
What testing caught
The first browser runs got as far as collecting the loot, then failed on the way out. Crossing the return door at the wrong moment put the crew directly in a guard's sight. A later run escaped in 61 ticks. The successful route took one wait at the door and a different path across the hall.
Testing bad inputs found a less obvious problem. An array containing
"north" could move the crew, while strings such as "constructor" consumed
a tick. The direction check was accepting JavaScript property names and
coercing values instead of requiring a valid direction string. The game now
validates tool arguments before dispatching commands and checks directions
again in the engine.
The tests also cover complete escapes with real patrols across 22 seeds, wrong codes, blocked moves, and actions after a run has ended. In Chrome, we checked that leaving the post removes the tools and returning registers all nine again without duplicates. These checks give me confidence in the game logic and integration; they don't establish compatibility with every browser or agent.
A small experiment in playing together
I wanted a reason to keep talking to the agent while it worked. A patrol about to turn, a short power window, or a code it needs me to read gives us something specific to coordinate. It is easy to lose a run by giving one direction too quickly. Replaying the same seed makes it possible to work out what went wrong and try again.