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, 2026

Our 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.

44tools across 6 builds
6wait for a human Confirm
1declared on a real HTML form
0ways to read or set an API key

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

  1. In Chrome 149 or newer, enable chrome://flags/#enable-webmcp-testing and relaunch. The ChatGPT desktop browser needs no flag.
  2. 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.
  3. 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 tools

Registered by the Shell, so they exist wherever the agent lands.

  • list_buildsRead only

    List 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_build

    Navigate 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.

    ParameterTypeDescription
    slugambient | captured | taskboard | this-or-that | policy-diff | skill-gapThe 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 only

    Check 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_panel

    Open 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": {}
    }

Taskboard

8 toolsOpen the build

A kanban board whose cards are the segments of one progress bar.

  • taskboard_get_stateRead only

    Read 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_goal

    Set 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.

    ParameterTypeDescription
    goalstringThe outcome the user wants, in one or two sentences. Example: Ship the self-serve onboarding flow.
    horizon optionalstringOptional 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_task

    Add 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.

    ParameterTypeDescription
    titlestringTwo to five words naming the deliverable.
    detail optionalstringOptional one sentence saying what done means for this task.
    stage optionaltodo | doing | doneColumn id. "todo" is Not started, "doing" is In progress, "done" is Done. Defaults to todo.
    weight optionalintegerEffort 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_task

    Move a task to another column. Moving resets its idle clock. Use taskboard_get_state first to get the id.

    ParameterTypeDescription
    idstringThe task id exactly as returned by taskboard_get_state (looks like t_abc123).
    stagetodo | doing | doneColumn 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 first

    Delete 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.

    ParameterTypeDescription
    idstringThe 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_triage

    Ask 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_board

    Demo 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 first

    Clear 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": {}
    }

This or That

7 toolsOpen the build

Swipe through spots. The overlap across everyone's picks is the answer.

  • thisorthat_get_stateRead only

    Read 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_setup

    Answer 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.

    ParameterTypeDescription
    wherestringCity, neighborhood or address, like "Austin, TX" or "Adams Morgan, DC".
    mode optionalwalking | drivingwalking keeps it to about 15 minutes on foot; driving allows about 20 minutes by car. Defaults to driving.
    tags optionalarray of cheap | quiet | veg | new | comfort | fast | drinks | outsidePreset 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 optionalstringAnything else in the user's own words, like "no cilantro" or "nothing too loud". Optional.
    membersarray of stringFirst 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_voter

    Start 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_vote

    Swipe 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.

    ParameterTypeDescription
    choiceyes | noyes 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 only

    The 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 first

    Throw 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 form

    Add 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.

    ParameterTypeDescription
    namestringThe 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"
      ]
    }

Policy Diff

6 toolsOpen the build

Diff two versions, get the change in plain words, ranked by who it actually lands on.

  • policydiff_get_stateRead only

    Read 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_documents

    Paste 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.

    ParameterTypeDescription
    oldText optionalstringFull text of the earlier version.
    newText optionalstringFull 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_sample

    Fill 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.

    ParameterTypeDescription
    name optionalnimbus-termsWhich 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_run

    Run 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 only

    The 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_reset

    Clear 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": {}
    }

Skill Gap

6 toolsOpen the build

Diff a resume against a posting, get the concrete gap and a two-week plan.

  • skillgap_get_stateRead only

    Read 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_inputs

    Paste 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.

    ParameterTypeDescription
    job optionalstringFull text of the job posting.
    resume optionalstringFull 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_sample

    Fill 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.

    ParameterTypeDescription
    name optionalmeridian-frontendWhich 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_analyze

    Run 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 only

    The 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_reset

    Clear 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": {}
    }

Ambient Scribe

5 toolsOpen the build

Talk. An agent writes the note, names it, and files it in a folder it decides on.

  • ambient_list_workspaceRead only

    List 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_capture

    File 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.

    ParameterTypeDescription
    textstringWhat 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 only

    Read one note in full by id (from ambient_list_workspace) and bring it up on screen.

    ParameterTypeDescription
    idstringThe 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 first

    Delete 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.

    ParameterTypeDescription
    idstringThe 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 first

    Delete every folder and note. Destructive: waits for the user to press Confirm on the page.

    No parameters.

    Input schema as registered
    {
      "type": "object",
      "properties": {}
    }

Captured Memory

8 toolsOpen the build

Paste a chapter or transcript. Scenes illustrate themselves as you move through them.

  • captured_get_stateRead only

    Read 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_text

    Paste a chapter or transcript into the page. At least 200 characters; a few pages works best. Replaces whatever is in the box.

    ParameterTypeDescription
    textstringThe full passage, verbatim.
    Input schema as registered
    {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "The full passage, verbatim."
        }
      },
      "required": [
        "text"
      ]
    }
  • captured_load_sample

    Fill 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_start

    Find 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_scene

    Jump the reader to a scene by its 1-based number, the same as clicking a tick on the track.

    ParameterTypeDescription
    indexintegerScene 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 only

    The 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_playing

    Turn autoplay on or off. On, the reader advances one scene every nine seconds until the end.

    ParameterTypeDescription
    playingbooleantrue 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 first

    Leave 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": {}
    }