This library targets the current WebMCP spec. Both the registration and consumer APIs live on document.modelContext (an EventTarget): registerTool on the registration side, getTools()/executeTool() on the consumer side. navigator.modelContextTesting is a deprecated wrapper over the same consumer engine.
Recommended root wrapper for apps using this library.
| Prop | Type | Description |
|---|---|---|
name |
string |
Your app's name |
version |
string |
Your app's version |
children |
ReactNode |
React children |
On mount, the provider checks for native document.modelContext. If absent, it installs a minimal in-memory polyfill. It cleans up the polyfill when the last provider unmounts (the polyfill is ref-counted across providers).
useMcpTool can still run outside the provider (with a warning), but registration depends on document.modelContext already being present.
Returns the current availability of the WebMCP API.
const { available } = useWebMCPStatus();| Field | Type | Description |
|---|---|---|
available |
boolean |
true once document.modelContext is ready (always false on the server or outside a provider) |
Registers a tool on document.modelContext. Automatically unregisters on unmount (via AbortSignal — there is no unregisterTool).
| Field | Type | Description |
|---|---|---|
name |
string |
Tool name (must be unique, 1–128 chars, matching ^[A-Za-z0-9_.-]+$) |
title |
string |
Optional human-friendly display title |
description |
string |
Human-readable description (required, non-empty) |
input |
z.ZodObject |
Zod schema for inputs. Handler receives typed args |
output |
z.ZodObject |
Optional Zod schema for outputs (library extension; see below) |
annotations |
ToolAnnotations |
Optional behavior hints (readOnlyHint, untrustedContentHint) |
exposedTo |
string[] |
Optional list of trustworthy origins this tool is exposed to across frames |
handler |
(args, ctx) => CallToolResult | Promise<CallToolResult> |
Tool implementation. Receives the parsed input and ctx: { signal: AbortSignal }; the signal aborts when the agent cancels the execution (Chrome 153+; otherwise a never-aborting substitute) |
onSuccess |
(result) => void |
Optional callback on success |
onError |
(error) => void |
Optional callback on error |
The handler receives the validated input object and a second ctx argument containing the execution AbortSignal. Handlers that declare a single parameter keep working.
Same as above, but replace input with inputSchema: InputSchema and (optionally) output with outputSchema: InputSchema. The handler receives Record<string, unknown> instead of typed args.
interface ToolAnnotations {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
}These are the only annotation fields supported. The classic MCP hints (destructiveHint, idempotentHint, openWorldHint) and annotations.title are not part of the current WebMCP spec — use the top-level title field for a display title.
exposedTo?: string[] lets you control cross-frame origin visibility. Each entry must be a parseable, potentially-trustworthy origin (e.g. https://example.com). Changing exposedTo re-registers the tool. Invalid origins cause registration to reject (see below).
const { state, execute, reset } = useMcpTool({ ... });| Field | Type | Description |
|---|---|---|
state.isExecuting |
boolean |
true while the handler is running |
state.lastResult |
CallToolResult | null |
Most recent result |
state.error |
Error | null |
Most recent error |
state.executionCount |
number |
Total successful executions |
execute(input?, { signal }?) |
(input?, options?) => Promise<CallToolResult> |
Manually invoke the tool |
reset() |
() => void |
Reset state to initial values |
execute() (the UI/direct path) throws if validation or handler logic fails. The agent/testing-shim path returns a CallToolResult with isError: true instead. Both paths update the same reactive state and fire the same onSuccess/onError callbacks.
Each execution gets its own AbortSignal, passed to the handler as ctx.signal. On
Chrome 153.0.8007.0+ (and via the polyfill's executeTool) it aborts when the caller
cancels. When an aborted execution's handler rejects, the hook treats it as
cancellation: isExecuting clears, but state.error stays untouched and onError
does not fire. Unregistering a tool (unmount) does not cancel in-flight executions
(Chrome 153.0.8008.0+ behavior). Native Chrome also announces starts and cancellations
through the toolactivated and toolcancel events described under Events.
Handlers always return a CallToolResult with a content array — including error results, which set isError: true. This is a deliberate library convention layered over the spec's looser return type, so results bridge cleanly to desktop MCP clients.
interface CallToolResult {
content: ContentBlock[];
structuredContent?: Record<string, unknown>;
isError?: boolean;
}structuredContent is a library extension for returning structured (machine-readable) output alongside the human-readable content blocks.
When native WebMCP is unavailable, the provider installs a polyfill that exposes:
document.modelContext— the registration API (anEventTarget).registerTool(tool, options?)returns aPromise<undefined>that rejects on invalid input (see below). Unregistration is AbortSignal-only — pass{ signal }and abort it to remove the tool. There is nounregisterTool.document.modelContext.getTools(options?)/executeTool(tool, inputArguments?, options?)— the consumer API.getTools()resolves sorted, freshRegisteredToolobjects whoseinputSchemais a deep-copied object;executeToolaccepts object inputs by reference or parses JSON-string inputs, forwardsoptions.signalinto the tool's execution signal, and — unlike native Chrome — validates input againstinputSchema(OperationError).navigator.modelContextTesting— deprecated wrapper over the same engine (listTools()keeps returning a JSON-stringinputSchema); removed in 2.0.0.
| Chrome | Behavior this library tracks |
|---|---|
| ≤152 | execute(input) — no tool-side signal (the library substitutes one); navigator.modelContextTesting removed in 152.0.7940.0 |
| 153 | execute(input, { signal }); unregistration no longer cancels in-flight executions (153.0.8008.0+) |
| 154 | RegisteredTool.inputSchema is an object (was a JSON string) |
| 155 | executeTool takes object inputs instead of JSON strings (155.0.8052.0+) |
| 156 | toolactivated and toolcancel fire on document.modelContext instead of window (156.0.8076.0+) |
Pass an object to executeTool, using {} for tools without arguments. When options
are omitted or undefined, omitting input or passing undefined defaults to a fresh {}.
When options are supplied (including {} or null), undefined input rejects; pass
{} explicitly, for example executeTool(tool, {}, { signal }). The polyfill uses
UnknownError for invalid inputs; native Chrome uses TypeError for non-object inputs.
This does not change the React hook's execute() convenience, which defaults to {}.
The 1.x polyfill preserves object inputs by reference. It does not JSON-serialize them
or invoke toJSON(): nested objects, Dates, undefined properties, and other values
reach the handler unchanged, subject to input schema validation. Handler mutations can
therefore affect the caller's object. Invalid JSON strings and non-object inputs reject
with UnknownError; schema violations reject with OperationError. An already-aborted
execution rejects with the caller's abort reason before input parsing or validation.
Native Chrome 155 instead JSON-serializes object inputs and passes an independent parsed
copy to the handler. Nested undefined properties are omitted and toJSON() is applied.
Circular references, BigInt values, and serialization returning undefined reject with
TypeError; exceptions from serialization reject with the original exception. Use plain
JSON-compatible objects for portable calls across native Chrome and the polyfill.
For compatibility, the polyfill also accepts JSON-string inputs and preserves their
existing parsing errors (UnknownError). Native Chrome through 154 requires strings;
native Chrome 155.0.8052.0+ requires objects. Polyfill string support remains available in
1.x and is planned for deprecation in 2.x, without removal in 2.x. Prefer objects for new
consumer code. This compatibility policy is separate from the deprecated testing shim.
The native API is detected by reading document.modelContext only; the polyfill marks itself with __isWebMCPPolyfill so native support short-circuits installation.
document.modelContext is an EventTarget. Three events are typed on it:
| Event | Event type | Fires when |
|---|---|---|
toolchange |
Event (no detail) |
the set of registered tools changes (register or unregister). Notifications are microtask-batched. |
toolactivated |
ToolActivatedEvent |
native Chrome begins executing a tool through executeTool |
toolcancel |
ToolCancelEvent |
native Chrome cancels a pending execution, for example when the caller's signal aborts |
ToolActivatedEvent and ToolCancelEvent extend Event with a toolName string. Both listener styles are supported, and ModelContextEventMap maps each event name to its event type:
document.modelContext.addEventListener("toolchange", () => { /* ... */ });
document.modelContext.addEventListener("toolactivated", ({ toolName }) => { /* ... */ });
document.modelContext.ontoolcancel = ({ toolName }) => { /* ... */ };Chrome 156.0.8076.0 moved toolactivated and toolcancel from window to document.modelContext; on Chrome ≤155 they fire on window. Feature-detect with "ontoolactivated" in document.modelContext. Event names outside the map fall back to the plain EventTarget signature.
The polyfill fires only toolchange. It does not dispatch toolactivated or toolcancel.
registerTool rejects (with a DOMException or TypeError) when given:
- an empty/missing name, a name longer than 128 chars, or a name not matching
^[A-Za-z0-9_.-]+$; - a duplicate name already registered;
- an empty/missing description;
- an
executethat is not a function; - a non-serializable
inputSchema; - an
exposedToentry that is not a parseable, potentially-trustworthy origin.
An already-aborted AbortSignal also rejects: registerTool rejects with the signal's abort reason and skips registration, matching the WebMCP spec and native Chrome 152+. Validation errors take precedence — the aborted-signal check runs after the checks above.
The hook routes these rejections into state.error and fires onError, except AbortError (lifecycle teardown), which is ignored.