Skip to content

useWebMCP

React adapter for @web-ai-sdk/webmcp. useWebMCP registers tools with component lifecycle cleanup and returns every tool exposed to the current document, refreshing after native toolchange events. For the conceptual overview see WebMCP.

Use the invoke controls below to call a registered tool. The demo reads live tools from useWebMCP() and dispatches the selected RegisteredTool through the SDK’s executeTool() helper.

import { useWebMCP } from "@web-ai-sdk/webmcp/react";
import { z } from "zod"; // Zod 4; other Standard Schema libraries work too
const AddToCartInput = z.object({ sku: z.string().min(1) });
export function AgentTools({ isSignedIn }: { isSignedIn: boolean }) {
const { tools, status } = useWebMCP(
{
name: "add_to_cart",
title: "Add to cart",
description: "Add a SKU to the user's cart",
input: AddToCartInput,
inputSchema: z.toJSONSchema(AddToCartInput, {
io: "input",
target: "draft-2020-12",
}),
execute: async ({ sku }) => ({ ok: true, sku }),
},
{ enabled: isSignedIn },
);
return <p>{status === "ready" ? `${tools.length} tools` : status}</p>;
}

The hook accepts a single plain tool or schema-aware definition, plus readonly arrays containing either shape. It registers on mount, cleans up on unmount, and cleans up whenever enabled changes to false.

Multiple tools can be passed directly:

import type { ToolDefinition } from "@web-ai-sdk/webmcp/react";
const findItemTool = {
name: "find_item",
description: "Find an item by SKU",
input: FindItemInput,
inputSchema: z.toJSONSchema(FindItemInput, {
io: "input",
target: "draft-2020-12",
}),
execute: ({ sku }) => findItem(sku),
} satisfies ToolDefinition<typeof FindItemInput>;
const addToCartTool = {
name: "add_to_cart",
description: "Add an item to the cart",
input: AddToCartInput,
inputSchema: z.toJSONSchema(AddToCartInput, {
io: "input",
target: "draft-2020-12",
}),
execute: ({ sku }) => addToCart(sku),
} satisfies ToolDefinition<typeof AddToCartInput>;
useWebMCP([findItemTool, addToCartTool] as const);

Typing or checking each definition at its definition site preserves its own schema-derived execute input type; the heterogeneous readonly array needs no as unknown as Tool[] cast.

The hook compares discoverable metadata and exposedTo values rather than object identity. Recreating an equivalent tool, tool array, or options object does not tear down its registration. Changing a name, title, description, schema, annotation, exposure value, or enabled state does update the registration.

Memoize tool arrays and large schemas when practical to avoid rebuilding and comparing their metadata on every render. Memoization is a performance optimization here, not a correctness requirement.

The hook return always describes the complete browser-facing tool list, not only definitions passed into that hook call. Call useWebMCP() without definitions whenever a component needs retrieval only:

import {
executeTool,
useWebMCP,
} from "@web-ai-sdk/webmcp/react";
export function ToolInspector() {
const { tools, status, error, refresh } = useWebMCP();
if (status === "unavailable") return <p>WebMCP unavailable</p>;
if (status === "loading") return <p>Discovering tools…</p>;
if (error) return <p>{error.message}</p>;
return (
<>
<ul>
{tools.map((tool) => (
<li key={`${tool.origin}:${tool.name}`}>
<button onClick={() => void executeTool(tool, {})}>
{tool.title || tool.name}
</button>
</li>
))}
</ul>
<button onClick={() => void refresh()}>Refresh</button>
</>
);
}

The status is "idle" | "loading" | "ready" | "unavailable" | "error". refresh() resolves to the fresh list as well as updating hook state.

Pass fromOrigins to request eligible tools exposed by descendant documents at specific secure origins:

const TRUSTED_ORIGINS = ["https://agent.example"] as const;
const discovery = useWebMCP({ fromOrigins: TRUSTED_ORIGINS });

The browser always includes eligible same-origin tools. Cross-origin discovery additionally requires the registering document to include the caller in its exposedTo option. Inline fromOrigins arrays are safe; the hook refreshes discovery only when their values change.

Pass exposedTo through the hook when descendant documents at other origins should discover the tool:

const EXPOSED_TO = ["https://agent.example"] as const;
useWebMCP(addToCartTool, {
enabled: isSignedIn,
exposedTo: EXPOSED_TO,
});

The browser validates the origins. The SDK owns the registration’s AbortSignal; callers cannot replace it.

The registered tool always delegates to the latest committed execute callback, so it can read live props and state without a manual ref bridge:

export function SettingsTool({ pane }: { pane: string }) {
useWebMCP({
name: "open_settings",
description: "Read the currently open settings pane",
execute: async () => ({ pane }),
});
return null;
}

Rerendering SettingsTool with a new pane updates execution immediately without unregistering and re-registering the tool. This avoids duplicate-name races while keeping registration changes tied to the fields an agent host can discover.

Registration cleanup is automatic on unmount via useEffect’s return value. If you need imperative cleanup (e.g. log out flow), call the vanilla registerTool directly per tool and combine the cleanups: const cleanups = tools.map(registerTool).

The hook removes its toolchange subscription on unmount and ignores stale discovery promises. Set enabled: false to suspend registration and retrieval and return to "idle" state.

On browsers without document.modelContext (or the legacy navigator.modelContext), registration is a no-op and the hook reports "unavailable" with an empty list. Render whatever fallback UI you want; nothing breaks.

Native discovery failures, such as invalid cross-origin filters or denied permissions, produce status: "error" and populate error; the hook does not reject during render or effects.

Direct definitions with input reject with ToolValidationError before application code runs when validation fails. Definitions with output reject with ToolOutputValidationError when the resolved result fails validation. Both schemas may transform values and may validate synchronously or asynchronously. The SDK-only input and output fields are never forwarded to WebMCP.

defineTool() remains as a deprecated compatibility wrapper with its historical validate: true input-validation opt-in. New code should pass definitions directly to the hook.

import type {
DefineToolOptions,
GetToolsOptions,
RegisteredTool,
RegisterToolOptions,
StandardSchemaV1,
Tool,
ToolAnnotations,
ToolDefinition,
UseWebMCPOptions,
UseWebMCPReturn,
WebMCPStatus,
} from "@web-ai-sdk/webmcp/react";
interface Tool {
name: string;
title?: string;
description: string;
execute: (input: unknown) => Promise<unknown> | unknown;
readOnly?: boolean; // maps to annotations.readOnlyHint
destructive?: boolean; // compatibility shorthand
annotations?: ToolAnnotations; // raw passthrough; merges on top of shorthand
inputSchema?: object; // JSON Schema
}
interface ToolDefinition<
InputSchema extends StandardSchemaV1 | undefined = undefined,
TOutput = unknown,
OutputSchema extends StandardSchemaV1 | undefined = undefined,
> extends Omit<Tool, "execute"> {
input?: InputSchema;
output?: OutputSchema;
execute: (
input: InputSchema extends StandardSchemaV1
? StandardSchemaV1.InferOutput<InputSchema>
: unknown,
) =>
| Promise<
OutputSchema extends StandardSchemaV1
? StandardSchemaV1.InferInput<OutputSchema>
: TOutput
>
| (OutputSchema extends StandardSchemaV1
? StandardSchemaV1.InferInput<OutputSchema>
: TOutput);
}
interface ToolAnnotations {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
destructiveHint?: boolean; // compatibility
idempotentHint?: boolean; // compatibility
openWorldHint?: boolean; // compatibility
}
interface RegisteredTool {
name: string;
title?: string;
description: string;
inputSchema?: string;
window: Window;
origin: string;
annotations?: ToolAnnotations;
}
interface GetToolsOptions {
fromOrigins?: readonly string[];
}
interface RegisterToolOptions {
exposedTo?: readonly string[];
}
type WebMCPStatus =
| "idle"
| "loading"
| "ready"
| "unavailable"
| "error";
interface UseWebMCPOptions extends GetToolsOptions, RegisterToolOptions {
enabled?: boolean;
}
interface UseWebMCPReturn {
status: WebMCPStatus;
tools: readonly RegisteredTool[];
error: Error | null;
refresh: () => Promise<readonly RegisteredTool[]>;
}
declare function useWebMCP(
toolOrTools:
| Tool
| ToolDefinition
| readonly (Tool | ToolDefinition)[],
options?: UseWebMCPOptions,
): UseWebMCPReturn;
declare function useWebMCP(
options?: UseWebMCPOptions,
): UseWebMCPReturn;
// Deprecated compatibility helper. Direct definitions are the primary API.
/** @deprecated */
declare function defineTool<
InputSchema extends StandardSchemaV1 | undefined = undefined,
TOutput = unknown,
OutputSchema extends StandardSchemaV1 | undefined = undefined,
>(
options: DefineToolOptions<InputSchema, TOutput, OutputSchema>,
): Tool<
InputSchema extends StandardSchemaV1
? StandardSchemaV1.InferInput<InputSchema>
: unknown,
OutputSchema extends StandardSchemaV1
? StandardSchemaV1.InferOutput<OutputSchema>
: TOutput
>;

Source: packages/webmcp/src/react/index.ts.