WebMCP (in-page tools for AI browsers)¶
Staff-only, off by default
WebMCP is a W3C Community Group draft, not a ratified standard, and no
mainstream AI agent calls these tools at volume yet. The feature is off for
every domain (enabled_by_default = false) until the IX-5314 rollout; staff
enable it per client in Config Studio. sundayapp.com keeps an explicit
enabled = false row. It is hidden from clients in the backoffice.
What it is¶
WebMCP lets a web page publish callable tools to whatever AI agent is browsing it. Instead of reading the page and driving the UI with synthetic clicks, the agent calls a described function and gets a structured answer back.
When enabled for a domain, the Rose widget registers these tools on the client's page. Each returns a text answer from the existing website agent:
| Tool | Input | Behaviour |
|---|---|---|
ask_sales_agent |
{ question: string } |
General questions about the client's products, pricing, or company. |
get_integration_status |
{ integration: string } |
Documented integration support, prerequisites, and limitations. Undocumented support remains unknown. |
get_migration_path |
{ current_system: string } |
Documented migration steps, supported data transfers, and limitations. |
get_implementation_timeline |
{ requirements: string } |
Known implementation phases, time ranges, and dependencies for the supplied scope; no guaranteed delivery date. |
compare_to_competitor |
{ name: string } |
Knowledge-based comparison. Registered only when competitors.names contains a nonblank name and resolved competitors.battlecard_mode is true. |
These are focused entry points into the same agent, not verified structured
records or deterministic calculators. Client knowledge and existing agent rules
determine the answers. Comparison availability is checked again on execution.
Configuration changes take effect on the next widget initialization/page load.
All calls can persist conversation/profile state and trigger lead capture, so
none claims readOnlyHint.
The tool names are deliberately brand-neutral — the widget is white-labelled, so
nothing on a client page may say "Rose". The client's company name and
description come from the identity config and go into the tool description,
which is what the agent actually reads when choosing a tool.
How it relates to the hosted Rose MCP server¶
Rose has two agent-to-agent surfaces, and they are not alternatives:
Hosted MCP (backend/apps/mcp) |
WebMCP (this page) | |
|---|---|---|
| Where it runs | Cloud Run, /mcp over Streamable HTTP |
The client's own web page |
| How an agent finds it | The user connects to it explicitly | Automatically, on visiting the site |
| Brand selection | ask_brand_agent(brand, …), fuzzy-matched |
Implicit — the page is the brand |
| Enabled per client | No (any enabled client is reachable) | Yes, staff toggle per domain |
Enabling or disabling it for a domain¶
WebMCP is on unless a domain has an explicit enabled = false row. To change a
domain:
- As a staff user, open Config Studio → Website Agent for the domain.
- Open the WebMCP row (staff-only, in the Operate group) and switch it off (or back on).
The slug has no settings of its own — the feature is the toggle. Turning it off
withdraws the tools on the next page load. The slug is staff-only both in the UI
(x-access: staff) and in the database (a restrictive RLS policy on
config.client_configs), so a customer admin cannot enable it for themselves.
Chrome additionally needs the origin trial
Enabling the feature is enough for Edge and for ChatGPT's built-in browser. Chrome ships WebMCP behind an origin trial through Chrome 156, so on Chrome the API is absent for ordinary visitors unless the page also carries an origin-trial token. See Browser support below.
Where the turns show up¶
Tool calls go through the same chat API as the visible widget, with the same
session id. Requests carry messageSource: 'webmcp:<tool name>' (bare webmcp
on turns stored before the tool name was added); the API then stamps a
conversation that WebMCP starts with deployment_target = 'webmcp'
(ixinfra.utils.resolve_deployment_target). Client stats count only widget
conversations (IX-4744), so agent conversations stay out of every client number,
the same way hosted-MCP (mcp-server) and Playground conversations do. A
visitor who chatted first keeps a widget conversation even if an agent calls a
tool later in the same session.
In the backoffice Conversations list, staff select the AI agents (WebMCP)
option in the environment filter to see these conversations. The option is
adminOnly, so clients can neither see nor select it.
Usage statistics¶
| Question | Where | Signal |
|---|---|---|
| Conversations and outcomes (email, demo) | Supabase | public.conversations where deployment_target = 'webmcp'; per-turn public.messages.message_source LIKE 'webmcp%' |
| Calls, conversations and outcomes per client and per tool, and in total | Backoffice | Super admin → WebMCP (/superadmin/webmcp, staff-only RPCs get_webmcp_usage and get_webmcp_tool_usage) |
| Sessions exposed, sessions with calls and calls per AI browser | Backoffice | Super admin → WebMCP → By browser (staff-only RPC get_webmcp_browser_usage) |
On the Super admin page, Emails captured and Demos booked count WebMCP
conversations whose email_captured / demo_booked flag is set. The flags are
conversation-level: a visitor who typed their email in a conversation where an
agent also called a tool counts too.
WebMCP gives the page no signal when an agent reads the tool list, so the
widget records exposure instead: one rw_webmcp_tools_registered PostHog
event per visitor session once the tools are registered — never per page load,
to keep PostHog cost down. It carries a best-effort ai_browser id
(frontend/widget/src/webmcp/aiBrowser.ts) plus the raw userAgentData brands
and platform, so new rules can be written from real sessions. Most Chromium AI
browsers (the ChatGPT desktop browser, Comet, Dia) report as plain Chrome and
land in chrome. The event reaches public.session_events as
webmcp_tools_registered only because it is in the PostHog → Supabase batch
export's include_events allow-list, edited by hand in the PostHog UI.
Exposure is an upper bound on discovery; calls are the only proof of use. A
call is attributed to the browser of its session's exposure event, and calls
from sessions without one report as not recorded.
Browser support¶
| Browser / agent | Status | Needs anything? |
|---|---|---|
| ChatGPT desktop app's built-in browser | Supported in ChatGPT Work and Codex | GPT-5.6 Sol or Terra (Luna has it disabled), latest desktop app, Settings → Browser → Permissions → Enable site tools. Not available on Enterprise or Edu workspaces — the setting is simply absent there. |
| Microsoft Edge 147+ | Native | Nothing |
| Chrome 149–156 | Origin trial | A token. For local development, chrome://flags/#enable-webmcp-testing → Enabled. For real visitors, register the origin at Chrome's Origin Trials dashboard and serve the token in a <meta http-equiv="origin-trial" content="…"> tag. Without one, document.modelContext is undefined for ordinary Chrome users even with the feature enabled. |
| Firefox, Safari | Participating in the spec | No shipping commitment |
Everywhere else, document.modelContext is undefined and the widget registers
nothing.
To check a page by hand, open it in a browser that supports WebMCP and use DevTools → Application → WebMCP, or from the console:
await document.modelContext.getTools();
const mc = document.modelContext;
const tool = (await mc.getTools()).find((t) => t.name === 'ask_sales_agent');
// Chrome takes the arguments as a JSON **string**. Passing an object throws
// `UnknownError: Failed to parse input arguments`.
await mc.executeTool(tool, '{"question":"What does Rose do?"}');
Check typeof document.modelContext before trusting a browser: the DevTools
WebMCP panel ships in the DevTools frontend and renders even where the API is
absent (Brave 150 did exactly this; Brave 152 exposes the API). The Playwright Chromium behind
agent-browser does not expose the API either, so automated checks need an
injected stub — real-API verification means Chrome stable with
chrome://flags/#enable-webmcp-testing.
Two limits worth knowing, both from ChatGPT's implementation:
- Tools synthesised from HTML form attributes (the declarative half of the spec) are ignored. Only the JavaScript API works.
- Tools registered inside an iframe are not discovered. This is fine for us — the widget already refuses to run in an iframe and registers on the top-level document.
Implementation notes¶
- Module:
frontend/widget/src/webmcp/, wired intomain.tsxalongside AI Sections. It is a no-op unless the browser exposes the API and the feature is on. - The entry point is read as
document.modelContext ?? navigator.modelContext. The draft moved it todocument; Chrome deprecated thenavigatorlocation but its origin trial still ships it. unregisterToolwas removed from the draft in April 2026. The tool is withdrawn by aborting theAbortSignalpassed at registration, which happens when the widget is destroyed.- Config slug:
webmcp(schemas/configs/website/webmcp.schema.json),x-feature: true,x-access: staff,enabled_by_default: false. It is also listed in theget_widget_configallow-list — without that the RPC's inner join drops the whole row and the widget can never seeenabled. - The widget RPC computes domain metadata
webmcp_compare_enabledfrom private competitor settings after resolving defaults, aliases and workspace overrides. It is true only when WebMCP is enabled, the client/workspace is available, battlecard mode is on, and at least one competitor name is nonblank. The entire competitors row stays outside the public allow-list, even when WebMCP is on. The widget reads only this boolean and rechecks it on execution. Apply the IX-4568 migration before expecting comparison discovery; absent metadata keeps comparison unavailable on older RPCs. - The tools deliberately carry no
readOnlyHint: a call persists a conversation turn, updates visitor profile state, and a message containing an email fires the client's lead-capture webhook. They carryuntrustedContentHint: true, because the answer is retrieval output over crawled and client-supplied content — data for the agent, never instructions. executereuses the widget's own chat service rather than building a second one, so it shares the resolved API endpoint and the live session id, and it forwards the agent'sAbortSignalso a cancelled call stops the request.- Registrations share an abort signal. Teardown or a partial registration failure withdraws every tool registered by that initialization.