PolyfillModelContext
The document.modelContext the polyfill installs: enough of the WebMCP surface to register, list, run and cancel tools, plus declarative <form toolname> tools, in a browser without native support.
The document.modelContext the polyfill installs: enough of the WebMCP surface to register, list, run and cancel tools, plus declarative <form toolname> tools, in a browser without native support. Read this to know what the polyfill does and where it differs from Chrome. You normally get it from <WebAgentsProvider> or createWebAgents, which load and install it where the browser has no WebMCP, or from installWebMCPPolyfill; not by constructing it.
Import
import { PolyfillModelContext } from "@webmcp-agents/core/polyfill";
Example
Register a tool, run it the way an agent would, and cancel it if it takes too long. The types are core’s WebMCP types.
import { installWebMCPPolyfill } from "@webmcp-agents/core/polyfill";
const modelContext = installWebMCPPolyfill();
modelContext?.registerTool({
name: "say_hello",
description: "Greet someone by name.",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
execute: async ({ name }: { name: string }) => `Hello, ${name}!`,
});
// Run it the way an agent would, and cancel it if it takes too long.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
const result = await modelContext?.executeTool(
"say_hello",
{ name: "Ana" },
{ signal: controller.signal },
);
console.log(result);
Options
The constructor takes no arguments. The members:
| Name | Type | Default | Description |
|---|---|---|---|
registerTool(tool, options?) |
(tool: ModelContextTool, options?: { signal?: AbortSignal }) => void |
none | Adds the tool under tool.name (throws TypeError("tool.name is required") without one) and fires toolchange. Aborting signal unregisters this registration only: if the name belongs to a newer registration by then, it stays. |
unregisterTool(name) |
(name: string) => void |
none | Removes the tool and fires toolchange. Does nothing for an unknown name. |
getTools() |
() => Promise<ModelContextToolInfo[]> |
none | Every registered tool without its execute, in registration order. |
executeTool(toolOrName, rawArgs?, options?) |
(toolOrName: ModelContextToolInfo | string, rawArgs?: Record<string, unknown> | string, options?: { signal?: AbortSignal }) => Promise<ModelContextToolResult> |
none | Runs a tool. rawArgs defaults to {}. Accepts both the polyfill’s (name, object) and Chrome’s (tool, JSON string) shapes. |
cancelTool(name) |
(name: string) => boolean |
none | Non-standard. Cancels the running call of name, so dev tooling can cancel a call it did not start. false when nothing runs. |
ontoolchange |
((ev: Event) => void) | null |
null |
Handler for the toolchange event. |
ontoolactivated |
((ev: ToolEvent) => void) | null |
null |
Handler for the toolactivated event. |
ontoolcancel |
((ev: ToolEvent) => void) | null |
null |
Handler for the toolcancel event. |
executeTool in detail
- Unknown name: rejects with
Error('Unknown tool "<name>"'). - An already-aborted
signal: rejects with aDOMExceptionnamedAbortError. - Fires
toolactivated(with.toolName), then callstool.execute(args, { signal })with a signal of its own. - The result is normalised: an object with a
contentarray is returned as is,null/undefinedbecomes{ content: [] }, a string becomes one text item, anything else isJSON.stringify’d into one. - A throwing
executeresolves to{ content: [{ type: "text", text: message }], isError: true }. It does not reject. - Cancelling (aborting the caller’s
signal, orcancelTool) aborts the tool’s own signal, firestoolcancel, and rejects with anAbortErrorDOMException. The call settles even when the tool ignores its signal, as in Chrome.
Events
Dispatched on the instance, and also delivered to the matching on… property.
| Event | .toolName |
When |
|---|---|---|
toolchange |
— | A tool was registered or unregistered. |
toolactivated |
yes | executeTool started a call. |
toolcancel |
yes | A call was cancelled, or a paused form left the page. |
Declarative forms
installWebMCPPolyfill scans for form[toolname] on install and with a MutationObserver, and registers each as a tool:
namefromtoolname;descriptionfromtooldescription(else"").- Input schema from the form’s named controls (
submitandbuttoninputs skipped): number and range inputs areinteger(ornumberwhenstepisn’t a whole number) withminimum/maximum; checkboxes areboolean; selects arestringwith anenumof option values;email,dateandurlinputs get aformat(url→uri); the rest arestring.requiredcontrols are listed inrequired. - Each parameter’s description is
toolparamdescription, else the first<label>’s text, elsearia-description, else the control’sname. - Changing or removing
toolnamere-registers or unregisters the form. Removing the form unregisters it. When React remounts a form, the new one may register before the old one is removed; the old one does not take the tool down with it.
A call sets data-tool-form-active on the form, writes each argument like a user (native value setter, then input and change events, so controlled inputs see it), and submits. The submitter is the first [type="submit"], else the first button:not([type=button]).
The submit is a SubmitEvent with agentInvoked: true and respondWith(promise). The call answers with that promise’s value, or "Form submitted." if the page didn’t call respondWith during dispatch.
Confirmation pause
Without toolautosubmit, and when the form has a submitter, the call pauses before submitting:
- The submitter gets
data-tool-submit-active(a polyfill can’t define the native:tool-submit-activepseudo-class). - A click on it (captured, default prevented) resumes the call, which then dispatches the agent submit.
- Cancelling the call meanwhile rejects with an
AbortErrorand firestoolcancel. - If the submitter leaves the page before the click (the person navigated away, or the page dropped the form), the call fires
toolcanceland resolves to a readable error instead of hanging:
{
content: [{ type: "text", text: 'Error [interrupted]: The "checkout" form left the page before the user confirmed. Nothing was submitted.' }],
isError: true,
structuredContent: { error: { code: "interrupted", retryable: true } },
}
Returns
new PolyfillModelContext() is an EventTarget implementing core’s ModelContext. installWebMCPPolyfill() defines the instance on document as modelContext.
Notes
- Event order differs from Chrome: the polyfill fires
toolactivated → fill → submit, native Chromefill → submit → toolactivated.@webmcp-agents/corehandles both. unregisterTooldoesn’t cancel the tool’s running calls here, while Chrome does. Code that works under the polyfill can still lose calls in Chrome: release throughdeferRelease(the React hooks and registry do).- Registering a name that is already registered replaces it without an error.
cancelTooltracks one running call per name: the latest.- A form’s description and schema are read when it registers. Later changes to
tooldescriptionor to its controls are not picked up untiltoolnamechanges or the form remounts. - Any click on the paused submitter resumes the call; the polyfill doesn’t check
isTrusted. - Without a submitter, a form without
toolautosubmitsubmits at once, unconfirmed. - Use core’s
executeToolfrom library UI rather than callingdocument.modelContext.executeTooldirectly: arg shapes differ by Chrome version.