Every build here is drivable by an agent.
Each page registers tools through WebMCP, the W3C proposal that puts a modelContext on the document. An agent in a browser that supports it can list them, call them, and watch the same screen you do. This page is rendered from the same definitions the pages register, so it cannot drift from what is live.
Watch an agent drive it
Recorded September 3, 2026Our OpenAI WebMCP Challenge demo. Codex opens Taskboard, builds a board, moves a card, and is stopped at the confirm bar when it tries to delete one.
How it stays safe
- The screen is the record. Every tool calls the same functions the buttons do, so whatever an agent changes is visible, and an “Agent is driving” pill in the header names the tool while it runs.
- Keys never cross the boundary. Tools can ask whether a key is set, as a boolean, and can open the key panel. Entering, reading or changing a key stays a human action.
- Deletes wait for you. Anything destructive blocks on a confirm bar at the bottom of the page and returns a failure if you cancel or walk away.
- Failures are answers. Tools never throw; they return a structured error with the next step, so an agent can recover instead of guessing.
Try it
- In Chrome 149 or newer, enable
chrome://flags/#enable-webmcp-testingand relaunch. The ChatGPT desktop browser needs no flag. - Open any build, press the WebMCP button in the header, and ask your agent to do what the tool descriptions say. The pill lights up as it works.
- Or, from DevTools on any page:
const ctx = document.modelContext || navigator.modelContext; const tools = await ctx.getTools(); const t = tools.find((x) => x.name === "list_builds"); JSON.parse(await ctx.executeTool(t, "{}"));
Every page
4 toolsRegistered by the Shell, so they exist wherever the agent lands.
list_buildsRead onlyList every build on this site: slug, name, the problem it solves, its approach, tier and URL. Call this first to learn what the site can do, then open_build to reach one.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }open_buildNavigate to a build's page. Each build registers its own tools only while its page is open, so call this before trying to use them.
Parameter Type Description slug ambient | captured | taskboard | this-or-that | policy-diff | skill-gap The build's slug, from list_builds. Input schema as registered
{ "type": "object", "properties": { "slug": { "type": "string", "enum": [ "ambient", "captured", "taskboard", "this-or-that", "policy-diff", "skill-gap" ], "description": "The build's slug, from list_builds." } }, "required": [ "slug" ] }get_setup_statusRead onlyCheck which API keys the user has stored in this browser, as booleans only. Every build needs a Gemini key; Places and Pixabay only improve imagery. Key values are never readable or settable through tools.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }open_key_panelOpen the key panel so the user can type an API key themselves. Use it when get_setup_status shows a missing key. Never ask the user to paste a key into the chat.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }
A kanban board whose cards are the segments of one progress bar.
taskboard_get_stateRead onlyRead the whole board: goal, timeframe, every task with its id, stage, weight, when it last moved and how many days it has sat idle, the effort-weighted progress percent, and the latest triage verdict. Call this before moving or removing anything so you have real ids.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }taskboard_set_goalSet the goal and rough timeframe, then build the board from it: a planner agent breaks the goal into 5 to 9 effort-weighted tasks, all starting in todo. Needs a Gemini key. Fails if a board already exists; reset first or add tasks to it instead.
Parameter Type Description goal string The outcome the user wants, in one or two sentences. Example: Ship the self-serve onboarding flow. horizon optional string Optional rough timeframe in words, like "about six weeks". Leave empty if the user gave none. Input schema as registered
{ "type": "object", "properties": { "goal": { "type": "string", "description": "The outcome the user wants, in one or two sentences. Example: Ship the self-serve onboarding flow." }, "horizon": { "type": "string", "description": "Optional rough timeframe in words, like \"about six weeks\". Leave empty if the user gave none." } }, "required": [ "goal" ] }taskboard_add_taskAdd one card to the board. Use it for work the planner missed or that the user names later. Weight sizes the card's share of the progress bar.
Parameter Type Description title string Two to five words naming the deliverable. detail optional string Optional one sentence saying what done means for this task. stage optional todo | doing | done Column id. "todo" is Not started, "doing" is In progress, "done" is Done. Defaults to todo. weight optional integer Effort from 1 (under an hour) to 5 (several days). Defaults to 3. Input schema as registered
{ "type": "object", "properties": { "title": { "type": "string", "description": "Two to five words naming the deliverable." }, "detail": { "type": "string", "description": "Optional one sentence saying what done means for this task." }, "stage": { "type": "string", "enum": [ "todo", "doing", "done" ], "description": "Column id. \"todo\" is Not started, \"doing\" is In progress, \"done\" is Done. Defaults to todo." }, "weight": { "type": "integer", "minimum": 1, "maximum": 5, "description": "Effort from 1 (under an hour) to 5 (several days). Defaults to 3." } }, "required": [ "title" ] }taskboard_move_taskMove a task to another column. Moving resets its idle clock. Use taskboard_get_state first to get the id.
Parameter Type Description id string The task id exactly as returned by taskboard_get_state (looks like t_abc123). stage todo | doing | done Column id. "todo" is Not started, "doing" is In progress, "done" is Done. Input schema as registered
{ "type": "object", "properties": { "id": { "type": "string", "description": "The task id exactly as returned by taskboard_get_state (looks like t_abc123)." }, "stage": { "type": "string", "enum": [ "todo", "doing", "done" ], "description": "Column id. \"todo\" is Not started, \"doing\" is In progress, \"done\" is Done." } }, "required": [ "id", "stage" ] }taskboard_remove_taskAsks the user firstDelete a task from the board. Destructive: the page shows a confirm bar and this call waits until the user presses Confirm. Tell the user to look for it.
Parameter Type Description id string The task id exactly as returned by taskboard_get_state (looks like t_abc123). Input schema as registered
{ "type": "object", "properties": { "id": { "type": "string", "description": "The task id exactly as returned by taskboard_get_state (looks like t_abc123)." } }, "required": [ "id" ] }taskboard_triageAsk the triage agent what is being neglected. It weighs each task's effort against its idle days and returns a blunt headline, the one task to do next, and a per-task flag (neglected, watch or ok) that the board then shows on the cards. Needs a Gemini key and a non-empty board.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }taskboard_age_boardDemo control: rewind every unfinished card's idle clock by 3 to 20 days so neglect is observable right now. Use it before taskboard_triage when the board was just built.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }taskboard_resetAsks the user firstClear the board and go back to the goal screen. Destructive: waits for the user to press Confirm on the page.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }
Swipe through spots. The overlap across everyone's picks is the answer.
thisorthat_get_stateRead onlyRead where the table is: phase (setup, dealing, handoff, swipe or result), the setup answers, whose turn it is, the card on screen, how many cards that voter has left, every member's yes and no lists, and the result once counted. Call this before voting.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }thisorthat_setupAnswer the whole setup at once and deal the deck: where the group is, how far they will go, any constraints, and who is eating. A deck agent then proposes 8 real restaurants (needs a Gemini key) and the first person's turn begins. Fails if a deck is already in play; reset first.
Parameter Type Description where string City, neighborhood or address, like "Austin, TX" or "Adams Morgan, DC". mode optional walking | driving walking keeps it to about 15 minutes on foot; driving allows about 20 minutes by car. Defaults to driving. tags optional array of cheap | quiet | veg | new | comfort | fast | drinks | outside Preset constraints, any number: cheap (cheap eats), quiet (can hold a conversation), veg (vegetarian-friendly), new (somewhere new), comfort (comfort food), fast (in and out), drinks (a real bar), outside (outdoor seating). craving optional string Anything else in the user's own words, like "no cilantro" or "nothing too loud". Optional. members array of string First names of everyone eating, 2 to 5 people. Each one swipes the deck in this order. If fewer than two are given, seats are filled with stand-in names. Input schema as registered
{ "type": "object", "properties": { "where": { "type": "string", "description": "City, neighborhood or address, like \"Austin, TX\" or \"Adams Morgan, DC\"." }, "mode": { "type": "string", "enum": [ "walking", "driving" ], "description": "walking keeps it to about 15 minutes on foot; driving allows about 20 minutes by car. Defaults to driving." }, "tags": { "type": "array", "items": { "type": "string", "enum": [ "cheap", "quiet", "veg", "new", "comfort", "fast", "drinks", "outside" ] }, "description": "Preset constraints, any number: cheap (cheap eats), quiet (can hold a conversation), veg (vegetarian-friendly), new (somewhere new), comfort (comfort food), fast (in and out), drinks (a real bar), outside (outdoor seating)." }, "craving": { "type": "string", "description": "Anything else in the user's own words, like \"no cilantro\" or \"nothing too loud\". Optional." }, "members": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 5, "description": "First names of everyone eating, 2 to 5 people. Each one swipes the deck in this order. If fewer than two are given, seats are filled with stand-in names." } }, "required": [ "where", "members" ] }thisorthat_next_voterStart the current person's turn from the handoff screen, the same as pressing Start swiping. Returns who is up and their first card. Use it whenever get_state says the phase is handoff.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }thisorthat_voteSwipe the card on screen for the person whose turn it is: yes means they would eat there, no means pass. Returns the next card, or the handoff to the next person, or the counted result after the last swipe. Only ask the user for a preference when they have not already told you.
Parameter Type Description choice yes | no yes to swipe right (would eat here), no to pass. Input schema as registered
{ "type": "object", "properties": { "choice": { "type": "string", "enum": [ "yes", "no" ], "description": "yes to swipe right (would eat here), no to pass." } }, "required": [ "choice" ] }thisorthat_get_resultRead onlyThe table's pick once everyone has swiped: winner with a Maps link, runner-up, whether it was unanimous, the counted reasoning, and the full ranking with who said yes to what. Fails with whose turn it still is if the deck is not finished.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }thisorthat_resetAsks the user firstThrow away the current deck and every vote and go back to setup. Destructive once a deck exists: waits for the user to press Confirm on the page.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }thisorthat_add_memberHTML formAdd one person to the table by name. This is the page's own name form, exposed as a tool; it only exists while the name field is open on the Who's eating step.
Parameter Type Description name string The person's first name, up to 24 characters. Input schema as registered
{ "type": "object", "properties": { "name": { "type": "string", "description": "The person's first name, up to 24 characters." } }, "required": [ "name" ] }
Diff two versions, get the change in plain words, ranked by who it actually lands on.
policydiff_get_stateRead onlyRead what is loaded and how far the run got: character counts for the old and new versions, the current pipeline stage, the deterministic diff stats once computed, how many substantive changes were found, and whether a ranked result is ready.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }policydiff_set_documentsPaste the two versions of a policy into the page. Pass the full text of each; the diff is paragraph-based, so keep paragraph breaks. Either field may be omitted to leave what is already in that box.
Parameter Type Description oldText optional string Full text of the earlier version. newText optional string Full text of the revised version. Input schema as registered
{ "type": "object", "properties": { "oldText": { "type": "string", "description": "Full text of the earlier version." }, "newText": { "type": "string", "description": "Full text of the revised version." } } }policydiff_load_sampleFill both boxes with a built-in sample pair: two versions of a cloud storage terms of service where an arbitration clause and a price rise are buried among renumbering. Use it to demo without documents in hand.
Parameter Type Description name optional nimbus-terms Which sample pair. Only nimbus-terms exists today; defaults to it. Input schema as registered
{ "type": "object", "properties": { "name": { "type": "string", "enum": [ "nimbus-terms" ], "description": "Which sample pair. Only nimbus-terms exists today; defaults to it." } } }policydiff_runRun the pipeline on the loaded documents: a deterministic paragraph diff, then an extractor that drops renumbering noise and merges hunks into logical changes, then an explainer that translates each change into plain language and ranks it by real-world impact. Needs a Gemini key and both documents. Takes 20 to 60 seconds; the stages show on the page as they complete. Returns the stats, the changes with their exact before and after wording, and the ranked verdicts.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }policydiff_get_resultRead onlyThe last run's result: headline, who the revision favors, and every change ranked by impact with severity, plain-language explanation, who it lands on, what to do about it, and the verbatim wording. Fails if no run has finished.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }policydiff_resetClear the result and go back to the two text boxes. The documents themselves stay loaded, so this is safe to call before a new run with edited text.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }
Diff a resume against a posting, get the concrete gap and a two-week plan.
skillgap_get_stateRead onlyRead what is loaded and where the run is: character counts for the posting and the resume, the current stage, the detected role, the fit score, how many requirements are not yet covered, and whether the plan is ready.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }skillgap_set_inputsPaste the job posting and the resume into the page. Pass full text; either field may be omitted to keep what is already in that box.
Parameter Type Description job optional string Full text of the job posting. resume optional string Full text of the candidate's resume. Input schema as registered
{ "type": "object", "properties": { "job": { "type": "string", "description": "Full text of the job posting." }, "resume": { "type": "string", "description": "Full text of the candidate's resume." } } }skillgap_load_sampleFill both boxes with a built-in pair: a Frontend Engineer II posting at a health company and a resume that is close but not quite there. Use it to demo without documents in hand.
Parameter Type Description name optional meridian-frontend Which sample pair. Only meridian-frontend exists today; defaults to it. Input schema as registered
{ "type": "object", "properties": { "name": { "type": "string", "enum": [ "meridian-frontend" ], "description": "Which sample pair. Only meridian-frontend exists today; defaults to it." } } }skillgap_analyzeRun the analysis on the loaded posting and resume: readers extract the requirements and the candidate's claims, a judge marks each requirement have, partial or missing with the evidence, and a planner writes a fourteen-day plan that closes the gaps. Needs a Gemini key and both texts. Takes 30 to 90 seconds; stages show on the page. Returns the role, fit score, verdict, every gap (status other than have) and the plan.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }skillgap_get_resultRead onlyThe last analysis: role, fit score out of 100, verdict, every requirement with its status and evidence, and the day-by-day plan with time per day and suggested resources. Fails if no analysis has finished.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }skillgap_resetClear the result and go back to the two text boxes. The posting and resume stay loaded, so it is safe to call before re-running with edits.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }
Talk. An agent writes the note, names it, and files it in a folder it decides on.
ambient_list_workspaceRead onlyList the workspace: every folder with its id and name, and every note with its id, title, folder and when it was last updated. Ids are what ambient_get_note and ambient_delete_note take.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }ambient_captureFile a braindump the same way a spoken or typed one is filed: a scribe agent reads the text against the current workspace, decides which folders and notes it belongs in, writes each note in full, and files it, showing each step on screen. Pass raw, unstructured text; do not pre-organize it. Needs a Gemini key.
Parameter Type Description text string What the user said or wrote, verbatim. Several topics in one go is the normal case. Input schema as registered
{ "type": "object", "properties": { "text": { "type": "string", "description": "What the user said or wrote, verbatim. Several topics in one go is the normal case." } }, "required": [ "text" ] }ambient_get_noteRead onlyRead one note in full by id (from ambient_list_workspace) and bring it up on screen.
Parameter Type Description id string The note id, which looks like n_abc123. Input schema as registered
{ "type": "object", "properties": { "id": { "type": "string", "description": "The note id, which looks like n_abc123." } }, "required": [ "id" ] }ambient_delete_noteAsks the user firstDelete one note by id. Destructive: the page shows a confirm bar naming the note and this call waits until the user presses Confirm. Tell the user to look for it.
Parameter Type Description id string The note id, which looks like n_abc123. Input schema as registered
{ "type": "object", "properties": { "id": { "type": "string", "description": "The note id, which looks like n_abc123." } }, "required": [ "id" ] }ambient_clear_workspaceAsks the user firstDelete every folder and note. Destructive: waits for the user to press Confirm on the page.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }
Paste a chapter or transcript. Scenes illustrate themselves as you move through them.
captured_get_stateRead onlyRead the reader: whether text is loaded and how long it is, whether the scenes have been found, the title, how many scenes there are, which one is showing, whether autoplay is on, and how many scenes already have an image.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }captured_load_textPaste a chapter or transcript into the page. At least 200 characters; a few pages works best. Replaces whatever is in the box.
Parameter Type Description text string The full passage, verbatim. Input schema as registered
{ "type": "object", "properties": { "text": { "type": "string", "description": "The full passage, verbatim." } }, "required": [ "text" ] }captured_load_sampleFill the box with the built-in sample chapter, a short piece about a late ferry crossing.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }captured_startFind the scenes in the loaded text and open the reader on the first one: a splitter picks the visual beats and quotes each passage verbatim, an art director fixes one style for the whole piece, and images render one scene ahead as you move. Needs a Gemini key; images need billing on that key. Returns the title and the list of scenes.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }captured_go_to_sceneJump the reader to a scene by its 1-based number, the same as clicking a tick on the track.
Parameter Type Description index integer Scene number, starting at 1. Input schema as registered
{ "type": "object", "properties": { "index": { "type": "integer", "minimum": 1, "description": "Scene number, starting at 1." } }, "required": [ "index" ] }captured_get_sceneRead onlyThe scene on screen: its number, beat, passage, quote, and the image status (ready, rendering, or why there is none) with its source and credit.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }captured_set_playingTurn autoplay on or off. On, the reader advances one scene every nine seconds until the end.
Parameter Type Description playing boolean true to play through, false to pause. Input schema as registered
{ "type": "object", "properties": { "playing": { "type": "boolean", "description": "true to play through, false to pause." } }, "required": [ "playing" ] }captured_new_textAsks the user firstLeave the reader and go back to the text box. The rendered images are dropped, so this waits for the user to press Confirm on the page.
No parameters.
Input schema as registered
{ "type": "object", "properties": {} }