---
description: "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."
---

# `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. Read this to know what the polyfill does and where it differs from Chrome. You normally get it from [`<WebAgentsProvider>`](/docs/reference/react/web-agents-provider) or [`createWebAgents`](/docs/reference/core/create-web-agents), which load and install it where the browser has no WebMCP, or from [`installWebMCPPolyfill`](/docs/reference/polyfill/install-webmcp-polyfill); not by constructing it.

## Import

```ts
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](/docs/reference/core/webmcp-types).

```ts title="src/model-context.ts"
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 a `DOMException` named `AbortError`.
- Fires `toolactivated` (with `.toolName`), then calls `tool.execute(args, { signal })` with a signal of its own.
- The result is normalised: an object with a `content` array is returned as is, `null`/`undefined` becomes `{ content: [] }`, a string becomes one text item, anything else is `JSON.stringify`'d into one.
- A throwing `execute` resolves to `{ content: [{ type: "text", text: message }], isError: true }`. It does not reject.
- Cancelling (aborting the caller's `signal`, or `cancelTool`) aborts the tool's own signal, fires `toolcancel`, and rejects with an `AbortError` `DOMException`. 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:

- `name` from `toolname`; `description` from `tooldescription` (else `""`).
- Input schema from the form's named controls (`submit` and `button` inputs skipped): number and range inputs are `integer` (or `number` when `step` isn't a whole number) with `minimum`/`maximum`; checkboxes are `boolean`; selects are `string` with an `enum` of option values; `email`, `date` and `url` inputs get a `format` (`url` → `uri`); the rest are `string`. `required` controls are listed in `required`.
- Each parameter's description is `toolparamdescription`, else the first `<label>`'s text, else `aria-description`, else the control's `name`.
- Changing or removing `toolname` re-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-active` pseudo-class).
- A click on it (captured, default prevented) resumes the call, which then dispatches the agent submit.
- Cancelling the call meanwhile rejects with an `AbortError` and fires `toolcancel`.
- If the submitter leaves the page before the click (the person navigated away, or the page dropped the form), the call fires `toolcancel` and resolves to a readable error instead of hanging:

```ts
{
  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 Chrome `fill → submit → toolactivated`. `@webmcp-agents/core` handles both.
- `unregisterTool` doesn't cancel the tool's running calls here, while Chrome does. Code that works under the polyfill can still lose calls in Chrome: release through `deferRelease` (the React hooks and registry do).
- Registering a name that is already registered replaces it without an error.
- `cancelTool` tracks one running call per name: the latest.
- A form's description and schema are read when it registers. Later changes to `tooldescription` or to its controls are not picked up until `toolname` changes 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 `toolautosubmit` submits at once, unconfirmed.
- Use core's [`executeTool`](/docs/reference/core/execute-tool) from library UI rather than calling `document.modelContext.executeTool` directly: arg shapes differ by Chrome version.

## See also

- [Polyfill](/docs/more/polyfill), the page that teaches it
- [`installWebMCPPolyfill`](/docs/reference/polyfill/install-webmcp-polyfill)
- [`@webmcp-agents/core/polyfill/auto`](/docs/reference/polyfill/auto)
- [`loadWebMCPPolyfill`](/docs/reference/core/load-webmcp-polyfill)
- [WebMCP types](/docs/reference/core/webmcp-types)
- [JSX attributes](/docs/reference/react/jsx-attributes)
- [`useToolAnswer`](/docs/reference/react/use-tool-answer)
- [Tool error codes](/docs/reference/core/tool-error)
