# WebMCP

Your components' tools, registered with the user's own browser agent: opt a rendered Combobox, Dialog, or Tabs into WebMCP with one keyword, declare a form as a tool with no JavaScript, and try both live on this page.

WebMCP is the browser standard (`document.modelContext`, origin trials in Chrome 149 and Edge 150, supported by ChatGPT Desktop) that lets a page register typed tools for the user's own agent instead of leaving it to scrape the DOM. Poetry components DECLARE their tools in Ruby beside their Stimulus wiring (`tool :set_value, params: { value: { type: "string", required: true } }, executes: :set_value, mutating: true`), the registry projects them (the MCP server's `describe_component` lists them), and a rendered instance opts in with one keyword: `poetry_tabs(webmcp: "sections")`, `poetry_dialog(webmcp: "settings")`, `poetry_combobox(webmcp: "country")`. poetry-agent's registrar controller registers `poetry.{name}.{tool}` on connect and unregisters on disconnect, dispatching each call to the component's own controller action; results and errors come back as descriptive strings. Forms take the declarative path with no JavaScript: `poetry_webmcp_form(tool: { name:, description:, autosubmit: true })` puts `toolname`/`tooldescription` on the `<form>`, the browser synthesizes the parameter schema from the controls and their labels, and `autosubmit` is GET-only by construction - a mutating form keeps the user's Submit. Everything is off until an instance opts in; tools are read-only unless declared `mutating: true`; a per-document budget caps registrations. Setup: `gem "poetry-agent"`, `registerPoetryAgent(application)` beside `registerPoetryControllers`, and `Poetry::Agent.configure { |c| c.origin_trial_tokens = [...] }` for production browsers during the trial (local development uses `chrome://flags/#enable-webmcp-testing`).

Every call is validated in code - a missing, unknown, or mistyped parameter, or a value outside the rendered options, answers with a message naming what the tool takes - and results carry the resulting state (the active tab, the committed value, open or closed); a form's `respondWith` answer is followed by the page catching up (a Turbo visit for a GET, a rendered stream, a POST's redirect target). Declarative form tools carry no annotations (an open spec issue), so a `readOnlyHint` audit cannot tell a search form from a mutation - the GET-only autosubmit rule is the guarantee. Host constraints: never send `Origin-Agent-Cluster: ?0` or set `document.domain`; imperative tools register when the module graph runs, after the parser registered the forms; never parse HTML carrying the tool attributes into an inert document (`DOMParser`, `createHTMLDocument`) - Chrome 151 crashes the renderer on such a form, so the form runtime strips them from a fetched answer before reading it. Tooling: the Model Context Tool Inspector, the nekuda WebMCP Workbench, and Google's `webmcp-evals` (`bin/rails webmcp:smoke` runs this site's committed suite in a real Chrome).
