Inspiration

We publish murder mystery scenarios — nine of them, sold on BOOTH, itch.io, The Game Crafter and Etsy. The single biggest thing standing between a customer and a game they have already paid for is not price or quality. It is that they need to find three to seven people who are free on the same evening, plus one more person willing to run the table and never get to play.

The obvious fix is to have an LLM be the game master. We tried it. It does not work, and it fails for a reason that is structural rather than a matter of prompting:

If you put the scenario in the model's context, the model knows who did it — and it then plays every suspect, judges every deduction, and narrates the whole evening while knowing.

You watch a suspect's denial come out a shade too soft. You watch it steer. You watch a character answer something the character had no way of knowing. You cannot instruct your way out of it, because the information is in there.

WebMCP was the first thing we had seen that could actually fix this, so we built the game we had been unable to build.

What it does

Sealed Case is a complete, playable one-player mystery. A man is dead in a bolted room above a second-hand bookshop in Jimbocho at 02:40. Three people are still in the building. The police arrive at 06:00.

An agent connected to the page can read the case file, search the room, question the suspects, keep track of what has been found, reason out loud with you, and put an accusation on the table. What it cannot do — at any point, by any route — is learn the answer, or make the accusation.

The idea: invert where the knowledge lives

WebMCP lets the page hold the case and the agent hold the conversation, meeting at a boundary the page controls.

In engine.js, the solution is captured in a module closure. It is never assigned to window, never written into the DOM, and never included in a value returned to a caller. Every tool reaches the game only through the object createEngine() returns, and that object has no path to the answer.

So the agent can call every tool, in every order, as many times as it likes, and remain in exactly the position the player is in: holding evidence, and guessing.

This is not a prompt instruction a model may or may not respect. It is an absence. The information is not in the agent's context, so there is nothing to leak, soften or let slip.

We did not want to just assert that, so three tests walk every tool-reachable path, collect the combined output of every call, and assert the solution does not appear in it. npm test runs them — 30 tests, no dependencies.

Measured against the real API in Chrome 151: 30 tool calls, 13,857 characters returned to the agent, and not one occurrence of the culprit, the method or the motive.

The tool that does not exist

propose_accusation puts a name, a method and a motive on the table and raises a confirmation card in the page. It resolves nothing. There is no confirm_accusation tool, in any phase, and a test sweeps every phase to prove it.

Confirming is a click, by the person sitting there.

This is the Chrome guidance on sensitive actions taken literally, but it is also the design of the game: a mystery you did not solve yourself is not entertainment. The agent is a very good game master. The accusation has to be yours.

Tools change with the phase

Each phase gets its own AbortController. When the phase moves, the previous controller is aborted and the next set is registered, so the agent's vocabulary is the game's turn structure rather than prose in a prompt that it has to be trusted to follow.

Phase Tools
always read_case_file · list_suspects · get_progress
briefing begin_investigation
investigation search_room · examine · interview · review_notebook · get_accusation_options · propose_accusation · withdraw_accusation
verdict read_epilogue · reopen_case · restart_case
never confirm_accusation

read_epilogue exists in the verdict phase but refuses unless the player confirmed a correct accusation. The gate is the page's, not the caller's word for it.

Two more details that matter at the table:

  • interview returns lines the page wrote. The tool description tells the agent to quote, not invent. Some of those lines are lies — the page knows which; the agent does not.
  • Questions are finite. Eight, across all three suspects. An agent cannot brute-force the testimony space, which is what makes choosing the questions the actual game.

How we built it

Four ES modules and a stylesheet. No framework, no build step, no dependencies, hosted on GitHub Pages.

  • case/lantern-room.js — the scenario; the only place solution exists
  • engine.js — game state, holding the solution in a closure
  • webmcp.js — the WebMCP surface: per-phase registerTool with AbortController, no confirmation tool
  • ui.js — the human interface; the only path to confirmAccusation()
  • test/ — 30 tests, including the containment proofs and a headless playthrough of the whole UI

Every tool has a button, so the page is fully playable with no agent at all and WebMCP is a true progressive enhancement. An agent joining mid-game inherits exactly the state the player built, because both drive the same engine.

Challenges we ran into

Proving a negative. "The agent cannot learn the answer" is easy to claim and worthless unless it is checked. We ended up writing a harvester that calls every tool with every argument in every phase, concatenates the results, and greps for the solution — including for the two words that appear only in the epilogue. That test is the one we would want a judge to read.

Making the containment a feature rather than a limitation. The first version simply refused things, which is annoying rather than interesting. The fix was to show the boundary: the right-hand rail lists the tools the agent can call right now, logs each call as it happens, and displays confirm_accusation struck through, permanently. The restriction becomes the most legible thing on the screen.

Writing a mystery that is fair to a player and opaque to an agent at the same time. Every fact the ending turns on has to be reachable from a tool result, or the game is a cheat — but no tool result may assemble them. There is a test for that too, asserting that the three load-bearing clues are each visible.

Two rough edges in the current implementation. executeTool intermittently rejects with a transient UnknownError, and speechSynthesis stops after about fifteen seconds firing neither end nor error. The walkthrough retries once and then drives the engine directly, and splits narration on sentence boundaries with a keep-alive and a word-count ceiling.

Accomplishments that we're proud of

A game master that is structurally incapable of spoiling the game, demonstrated rather than promised — and a product that is finished rather than a proof of concept: a complete scenario, a real ending, a wrong-answer path with a hint, and a page that stands on its own without an agent.

What we learned

WebMCP is usually described as a way to give an agent more access to a page. The interesting property turned out to be the opposite one. Because the page decides what a tool returns, WebMCP is the first web API that lets a site collaborate with an agent while keeping something from it — and keeping it in a way that does not depend on the model's cooperation.

That generalises well past games: a tutoring site that will not hand over the answer key, an interview tool that will not reveal the rubric, an escape room, a clinical vignette. Any product where the agent should be a capable participant rather than an oracle.

What's next for Sealed Case

Multiple cases behind one shell, and — for our actual catalogue — a mode where the agent runs a scenario for a table of real people, holding the parts of the script that belong to each player and to no one else. That is the same containment property, one step out: today the page keeps a secret from the agent; next it keeps a different secret for each person at the table.

Newly built for this hackathon

All of it. Every file in the repository was written during the submission period; the commit history is the record. Nothing existed beforehand. The scenario is original and written for this project — our commercial scenarios are not included and their solutions do not appear anywhere in the repository, since publishing them would destroy the thing we sell. Every person, publisher and establishment in the scenario is fictional.

Built With

Share this project:

Updates

Submission history