use-webmcp-tool
v0.2.0
Published
React hook for registering WebMCP tools (document.modelContext) with lifecycle-managed registration and MCP result normalization.
Maintainers
Readme
Use-Webmcp-Tool
A hook that registers a WebMCP tool with the browser and ties its lifecycle to a React component.
This is maintained by Chrome, and will be updated with any spec changes. The spec is
🧪experimental, so the hook feature-detects and degrades to a no-op everywhere the API is absent.
Status / accuracy note (2026-06-05): Built against the current WebMCP spec, which exposes the imperative API on document.modelContext (registerTool + an AbortSignal for unregistration).
Install
npm install use-webmcp-toolRequires React 18+ as a peer dependency. Ships as ESM with TypeScript types included — no runtime dependencies.
What it does
WebMCP lets a page expose JavaScript functions as "tools" that an AI agent (browser-built-in, iframe-hosted, or extension) can discover and call. The site author can expose functionality, and the agent uses this instead of scraping the DOM, a11y tree, or using screenshots.
The raw imperative API looks like this:
const controller = new AbortController();
document.modelContext.registerTool({
name: "add-todo",
description: "Add a new item to the user's active todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text content of the todo item" },
},
required: ["text"],
},
async execute({ text }) {
await addTodoItemToCollection(text);
return { content: [{ type: "text", text: `Added todo item: "${text}" successfully.` }] };
},
}, { signal: controller.signal });
// Unregister later:
controller.abort();useWebMCP wraps that imperative, lifecycle-bound API in the declarative, lifecycle-managed model React developers already use for everything else:
import { useWebMCP } from "use-webmcp-tool";
function TodoTools({ addTodo }) {
const { supported, registered } = useWebMCP({
name: "add-todo",
description: "Add a new item to the user's active todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text content of the todo item" },
},
required: ["text"],
},
async execute({ text }) {
addTodo(text);
return `Added todo item: "${text}" successfully.`;
},
});
if (!supported) return null;
return <p>{registered ? "🤖 Agent tools ready" : "…"}</p>;
}The tool is registered when the component mounts and unregistered automatically when it unmounts. This is designed so that the set of tools an agent sees stays in lockstep with what is actually on screen.
API
const { supported, registered, error } = useWebMCP({
name, // string — tool identifier (required)
description, // string — natural-language description for the agent (required)
inputSchema, // JSON Schema object describing args (optional)
annotations, // ToolAnnotations object with readOnlyHint/untrustedContentHint (optional)
execute, // (args) => result | Promise<result> (required)
enabled = true, // boolean — register only while true
formatOutput, // (result, args) => any — optional shaper before MCP normalization
onError, // (error) => void — optional side-effect when execute throws
});Returns
| field | type | meaning |
| ------------ | ---------------- | -------------------------------------------------------------------- |
| supported | boolean | document.modelContext exists in this environment. |
| registered | boolean | The tool is currently registered with the browser. |
| error | Error \| null | Registration error, e.g. NotAllowedError from a tools permissions policy. |
execute return values are normalized:
- a string →
{ content: [{ type: "text", text }] } undefined/null(no return) →{ content: [] }(success, no payload)- a value that is already
{ content: [...] }→ passed through untouched - a thrown value — Error or not (
throw "not signed in",throw { code: 403 }both count) →{ content: [{ type: "text", text }], isError: true }, afteronError. A failure must never read as success to the agent. - a returned
Error→ treated exactly like a throw:onErrorfires, then anisErrorresult - anything else (object/array/number) → JSON-serialized into a text block
Tests
useWebMCP.test.jsx (vitest + jsdom + @testing-library/react, 21 tests) covers the registration lifecycle (mount/unmount, StrictMode, enabled, late injection, registration errors), re-registration identity (execute changes don't churn, content-equal schemas don't churn, name changes do), and the full result/error normalization matrix including thrown non-Errors and returned Errors.
Run with npm install && npm test.
