Inspiration
I recently participated in a public Kaggle simulation competition for a collectible-card game — The Pokémon Company, PTCG AI Battle Challenge Simulation.
From my experience as a data scientist, I can see that the competition was born out of a training problem in machine learning / deep learning: a finite-horizon, partially observable, competitive, multi-agent, sequential decision process. In particular, card-trading games often involve hidden information, a large combinatorial action space, and an opponent — often a human — who is also learning. This makes the dynamics messy and non-stationary, unlike in more conventional reinforcement learning settings.
The organizers' answer to that mess was to open an arena. Independently written agents share one simulator, receive a structured observation, return an action, and play against each other on a ladder. That is a good design for agent-versus-agent evaluation. However, it is a poor design for the kind of data that modern game-agent research actually starves for.
This is because humans and agents do not share an interface. A human wants a board and an interface they can see and interact with. An agent wants a typed observation, a legal-action mask, and a step. This made me think that this is such a great use case for WebMCP, where the agent communicates through WebMCP tools while the human interacts through the web browser.
My idea is ArenaMCP, a gym-style environment where humans and agents can design games, share them with others, play them, and collect training data from human-versus-agent interactions.
Why human-versus-agent game design is a strong fit for WebMCP
Problem. Data collection for a partially observable, competitive, human-versus-agent, sequential decision-making problem. Game-agent research needs diverse interactive trajectories, but humans and agents require fundamentally different interfaces, so human-versus-agent data is always among the most challenging and expensive to collect.

An environment includes 5 functions:
init(seed) -> state
legal_actions(state, player) -> string[]
observe(state, player) -> observation
step(state, action) -> { state, rewards, terminal }
render(observation) -> { html, css }
observe is the fair-play boundary. legal_actions is the action mask.
render is the human board. The agent never clicks that board — it calls
get_observation, take_action, wait_for_turn. Both seats write the
same JSONL:
episode { match_id, environment, seed, seats, returns, length }
step { observation, legal_actions, action, reward, terminal, latency_ms, … }
Each step records which interface took the action (human_ui or
webmcp), so a click and a tool call stay distinguishable. Optional
rationale and confidence ride on the agent seat. A Python loader
(arena_dataset.py) yields (obs, legal, action, reward, next_obs, done).
This only works if they share one live page and one match state.
A server MCP could write environment files in the dark. A screenshot
agent could guess at buttons. Neither gives the agent a legal-action
mask, a per-seat observation, or the board the person is looking at.
WebMCP does: the tools run in the page, against the same step() the
clicks use, and export_episodes hands back the schema, a sample row,
and a download link.
The surface is the whole loop — author, verify, play, export — registered
on document.modelContext. Every write repaints the table before it
returns, so the agent's next observation matches what the human sees.
Every play tool returns enough state — observation, legal actions, whose
turn, whether the game ended — for the agent to choose the next call.
Authored markup is treated as hostile (untrustedContentHint); the
sandbox iframe has no same-origin access and no network, so a game cannot
reach document.modelContext.

How it creates a better user experience
The person never leaves the page, and they never have to become an RL engineer to get a dataset.
- They speak; a playable table appears. Ask the agent to design a game. It writes the five functions, the checks run, and the board lands on their screen. No gym wrapper, no separate frontend, no "now export this to a notebook."
- They play the board they asked for. Clicks are ordinary UI. The
agent sits in the other seat through tools. The trajectory panel
fills in as they play —
observation, mask, action, reward, latency. - They can see whether the game is fair. Publishing shows an information-flow matrix: which state fields each seat can observe. A leak ("seat 0 can see seat 1's private cards") is a failed check, not a footnote. The person confirms the matrix before the environment is shared.
- The agent can look at the table the way a person does.
preview_view/inspect_viewcollapse the painted board into a character grid with real colours and list every control. The agent can tell a blankrender()from a working one without reading raw HTML. - The download is the point of the session, not homework after.
JSONL from the play panel or
GET /api/episodes?match_id=…&format=jsonl. Same bytes the agent already described withdescribe_dataset.
Without WebMCP the person would watch an agent edit files they cannot play, or play a game an agent cannot legally sit in. The better experience is that those are the same session.
What people and agents can do together that was difficult or impossible before
Author a new environment, verify it, and play it, in one sitting.
Before, that was three projects: a simulator, a UI, an agent API. A person could describe Kuhn poker in a chat and get code. They could not immediately sit across from the thing that wrote it, on a board that hides the other seat's card, and download a trajectory that names which side was human.
Collect human-versus-agent data without a custom integration.
Agent-versus-agent ladders (the Kaggle pattern) produce volume. They do
not produce a person thinking across the table. Browser-use agents
produce clicks. They do not produce a typed observation and a legal
mask. Arena records both seats against the same step(), tagged by
interface, with optional agent rationale.
Enforce the observation boundary in front of both of them.
observe(state, player) is checked by perturbing state fields and
seeing which seats' views change. If a private field leaks, publish is
blocked and the agent gets repair text. A person and an agent can
argue about fairness against a generated matrix instead of trust.
Let the agent repair what it can see.
The agent is not scoring its own HTML. preview_view says what painted;
validation says what is broken (step threw, the game never ends, the
markup ignores the observation) and names the seed. trace_episode
walks that seed so the agent can see the state at the break. The person
watches the table change as the agent patches one function at a time.
None of that is possible if the agent is in a file tree and the person is on a different website. It needs one page, two interfaces, shared state — which is what WebMCP is for.
How I implemented WebMCP

Imperative registration on the live document, from a client component
(ArenaTools). Feature-detect both names; abort on unmount because
there is no unregisterTool(). Each tool has a title for the browser
UI, a description, and a JSON Schema whose properties are described
in English. execute receives the run's AbortSignal and threads it
into every fetch, so a cancelled call does not leave work running:
const model = document.modelContext ?? navigator.modelContext;
if (!model?.registerTool) return;
const controller = new AbortController();
await model.registerTool(
{
name: "take_action",
title: "Play a move",
description:
"Play one legal action. Quote expected_revision from the last observation.",
inputSchema: {
type: "object",
properties: {
action: { type: "string", description: "One id copied from legal_actions." },
expected_revision: { type: "integer" },
rationale: { type: "string" },
confidence: { type: "integer", minimum: 1, maximum: 5 },
},
required: ["action", "expected_revision"],
},
annotations: { untrustedContentHint: true },
execute: async (input, { signal }) => {
const result = await play(input, { signal });
await refreshView(); // table updates before the agent reads the reply
return result;
},
},
{ signal: controller.signal },
);
The agent has access to a suite of 18 WebMCP tools:
| Stage | Tools |
|---|---|
| Author | get_authoring_guide, list_environments, get_environment, create_environment, fork_environment, update_environment |
| See | preview_view, inspect_view — the agent cannot trust its own HTML, so these describe what actually painted |
| Verify | validate_environment (V0–V8), trace_episode, describe_dataset |
| Share | publish_environment, open_environment |
| Play | start_match, get_observation, take_action, wait_for_turn |
| Keep | export_episodes |
- Author. A game environment is five functions —
init,actions,observe,step, andrender— withrenderas the interface. The app is pre-equipped with them, so the agent lists, gets, creates, and updates those functions to come up with the game. The engine is built on top of OpenSpiel and PettingZoo, in the open-source quickjs-emscripten sandbox. Authored HTML and CSS render in an iframe so a hostilerender()cannot inject into the page. - See and verify. Viewing tools and verifying tools let the agent debug and validate that the entire game actually works.
- Publish. Once the game is validated, the agent publishes the environment.
- Play. The agent plays together with a human via the playing tools, on the same match the board uses.
- Keep. Playing data is stored as Netlify Blobs and exported via the export tools.
Built With
- chrome-webmcp-testing
- jsonl
- netlify
- netlify-blobs
- next.js
- openspiel
- pettingzoo
- python
- quickjs-emscripten
- react
- typescript
- webmcp
Log in or sign up for Devpost to join the conversation.