Skip to content
WebMCP Agents
Esc
↑↓navigate↵open⌘Jpreview
On this page

Browsers without WebMCP

The provider loads a polyfill of document.modelContext where the browser has none, so tools, the cursor and the chat work in any browser, in development and in production.

Native WebMCP is only in Chrome, behind an origin trial. Everywhere else, a polyfill gives the page the same document.modelContext, so your tools, the agent cursor, the devtools and the chat work in any browser. It ships in @webmcp-agents/core, and you do not load it yourself: <WebAgentsProvider> does, only where the browser needs it.

The provider loads it

When the provider mounts in a browser without document.modelContext, it loads the polyfill with a dynamic import() and installs it. Nothing waits for it but the tools:

  • The page renders at once, on the server too. There is no “ready” state to keep and nothing to load first.
  • Tools registered meanwhile wait. The cursor installs and the catalog and every useWebTool register once document.modelContext exists, a few milliseconds later. A tool released before then never registers.
  • Browsers with WebMCP never download it. The polyfill is its own chunk (about 2.5 KB min+gz), reached only by that dynamic import. With native WebMCP the tools register at once.

createWebAgents, the function the provider wraps, does the same for a page without React (No framework).

Native WebMCP only: polyfill={false}

To leave browsers without WebMCP alone, turn it off. Nothing is loaded, and a tool registered where the browser has no WebMCP is not registered: the first one logs a warning (document.modelContext is unavailable: tools are not registered), and the page works as before.

<WebAgentsProvider tools={tools} polyfill={false}>
  {children}
</WebAgentsProvider>

Without React the option is createWebAgents({ tools, polyfill: false }).

In production

The default loads it in production too, and that is on purpose: a visitor in Firefox or Safari gets the same tools, cursor and chat as one in Chrome. The polyfill is for your own page’s agents, the chat and the devtools; a browser agent that reads native WebMCP only does not see tools a polyfill registered. A site that wants native WebMCP only passes polyfill={false}, as above, or chooses per environment with its own flag.

What it covers

  • Tools. registerTool(tool, { signal }), unregisterTool(name), getTools() and executeTool(nameOrTool, args, { signal }). executeTool accepts both its own (name, object) shape and Chrome’s (tool, JSON string).
  • Lifecycle events on document.modelContext: toolchange, toolactivated and toolcancel.
  • Declarative forms, found with a MutationObserver: the input schema from the controls, toolautosubmit, and the confirmation pause. Tools you write with useWebTool need none of this: they register through registerTool.
  • SubmitEvent.agentInvoked and respondWith(promise).
  • cancelTool(name), a non-standard extra, so the devtools can cancel a call they did not start.

Differences from Chrome

Chrome Polyfill
Form states :tool-form-active, :tool-submit-active [data-tool-form-active], [data-tool-submit-active] (a script cannot define pseudo-classes)
Event order fill, submit, then toolactivated toolactivated, then fill, then submit
Cancelling another caller’s call not possible cancelTool(name)
Unregistering a tool with a call in flight cancels the call the call finishes

@webmcp-agents/core handles both orders, and its cursor shows both kinds of state, so the same code works on either. cursor.css styles the polyfill’s attributes; the native pseudo-classes have their own optional stylesheet, cursor-native.css.

Without the provider

A page that registers tools some other way (declarative forms only, or registerTool by hand) loads it itself, before its tools: await loadWebMCPPolyfill() from @webmcp-agents/core loads it only where it is needed, and import "@webmcp-agents/core/polyfill/auto" installs it from your main bundle. See loadWebMCPPolyfill and the polyfill reference.

@webmcp-agents/polyfill, the package that used to hold it, is deprecated: it re-exports these entries, so existing imports keep working. Uninstall it and delete the import() and the “ready” state that waited for it.

Was this page helpful?