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
useWebToolregister oncedocument.modelContextexists, 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()andexecuteTool(nameOrTool, args, { signal }).executeToolaccepts both its own(name, object)shape and Chrome’s(tool, JSON string). - Lifecycle events on
document.modelContext:toolchange,toolactivatedandtoolcancel. - Declarative forms, found with a MutationObserver: the input schema from the controls,
toolautosubmit, and the confirmation pause. Tools you write withuseWebToolneed none of this: they register throughregisterTool. SubmitEvent.agentInvokedandrespondWith(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.