usewebmcp
v3.0.0
Published
Standalone React hooks for strict core WebMCP tool registration with document.modelContext
Maintainers
Readme
usewebmcp
Standalone React hooks for strict core WebMCP tool registration via document.modelContext.
usewebmcp is intentionally separate from @mcp-b/react-webmcp:
- Use
usewebmcpfor strict core WebMCP workflows. - Use
@mcp-b/react-webmcpfor full MCP-B runtime features (resources, prompts, client/provider flows, etc.).
Type Safety First
usewebmcp is built for schema-driven types:
config.execute/config.handlerinput is inferred frominputSchema- Tool result type is inferred from
outputSchema state.lastResultand returnedexecute(input)are typed from that same inferred output
Package Selection
| Package | Use When |
| ------------------------ | --------------------------------------------------------- |
| usewebmcp | React hooks for strict core document.modelContext tools |
| @mcp-b/react-webmcp | React hooks for full MCP-B runtime surface |
| @mcp-b/webmcp-polyfill | You need a strict core runtime polyfill |
| @mcp-b/global | You need full MCP-B runtime (core + extensions) |
Install
pnpm add usewebmcp react
# or
npm install usewebmcp reactOptional (only if you want Standard Schema authoring like Zod v4 input schemas):
pnpm add zodRuntime Prerequisite
usewebmcp expects window.document.modelContext to exist.
You can provide it via:
- Browser-native WebMCP implementation, or
@mcp-b/webmcp-polyfill, or@mcp-b/global
If document.modelContext is missing, the hook falls back to older preview navigator.modelContext runtimes. If both are missing, the hook logs a warning and skips registration.
Quick Start
import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
import { useWebMCP } from 'usewebmcp';
initializeWebMCPPolyfill();
const COUNTER_INPUT_SCHEMA = {
type: 'object',
properties: {},
} as const;
const COUNTER_OUTPUT_SCHEMA = {
type: 'object',
properties: {
count: { type: 'integer' },
},
required: ['count'],
additionalProperties: false,
} as const;
export function CounterTool() {
const counterTool = useWebMCP({
name: 'counter_get',
description: 'Get current count',
inputSchema: COUNTER_INPUT_SCHEMA,
outputSchema: COUNTER_OUTPUT_SCHEMA,
execute: async () => ({ count: 42 }),
});
return (
<div>
<p>Executions: {counterTool.state.executionCount}</p>
<p>Last count: {counterTool.state.lastResult?.count ?? 'none'}</p>
{counterTool.state.error && <p>Error: {counterTool.state.error.message}</p>}
<button
onClick={async () => {
await counterTool.execute({});
}}
>
Run Tool Locally
</button>
</div>
);
}How useWebMCP Works
- Registers a tool on mount with
document.modelContext.registerTool(tool, { signal })and aborts the controller on unmount. - On Chrome Beta 147 native (which ignores the second arg) cleanup cannot remove the tool. Install
@mcp-b/globalor@mcp-b/webmcp-polyfillfor spec-aligned behavior. - Exposes local execution state:
state.isExecutingstate.lastResultstate.errorstate.executionCount
- Returns
execute(input)for manual in-app invocation andreset()for state reset.
Your tool implementation (config.execute or config.handler) can be synchronous or asynchronous.
config.execute vs returned execute(...)
config.execute: preferred config field for tool logic.config.handler: backward-compatible alias forconfig.execute.- returned
execute(input): hook return function for local manual invocation from your UI/tests.
If both config.execute and config.handler are provided, config.execute is used.
Both paths run the same underlying tool logic and update the hook state.
Type Inference
Input inference
inputSchema supports:
- JSON Schema literals (
as const) viaInferArgsFromInputSchema - Standard Schema v1 input typing (for example Zod v4 / Valibot / ArkType) via
~standard.types.input
const INPUT_SCHEMA = {
type: 'object',
properties: {
query: { type: 'string' },
limit: { type: 'integer' },
},
required: ['query'],
additionalProperties: false,
} as const;
useWebMCP({
name: 'search',
description: 'Search docs',
inputSchema: INPUT_SCHEMA,
execute(input) {
// input is inferred as { query: string; limit?: number }
return { total: 1 };
},
});Output inference
When outputSchema is provided as a literal JSON object schema:
- implementation return type is inferred from
outputSchema state.lastResultis inferred to the same type- MCP response includes
structuredContent
const OUTPUT_SCHEMA = {
type: 'object',
properties: {
total: { type: 'integer' },
},
required: ['total'],
additionalProperties: false,
} as const;
const tool = useWebMCP({
name: 'count_items',
description: 'Count items',
outputSchema: OUTPUT_SCHEMA,
execute: () => ({ total: 3 }),
});
// tool.state.lastResult is inferred as { total: number } | nullManual execute(...) Calls
You can call the returned execute(...) function directly from your component.
function SearchToolPanel() {
const searchTool = useWebMCP({
name: 'search_local',
description: 'Run local search',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
required: ['query'],
additionalProperties: false,
} as const,
execute: async ({ query }) => ({ query, total: query.length }),
});
return (
<button
onClick={async () => {
await searchTool.execute({ query: 'webmcp' });
}}
>
Run Search
</button>
);
}Output Schema Contract
If outputSchema is defined, your tool implementation must return a JSON-serializable object result.
Returning a non-object value (string, null, array, etc.) causes an error response from the registered MCP tool.
Re-Registration and Performance
The tool re-registers when any of these change:
namedescriptioninputSchemareferenceoutputSchemareferenceannotationsreference- values in
deps
The hook avoids re-registration when only callback references change:
executehandleronSuccessonErrorformatOutput
Latest callback versions are still used at execution time.
Recommendation:
- Define schemas/annotations outside render or memoize them.
- Keep
depsprimitive when possible.
API
useWebMCP(config, deps?)
config fields:
name: stringdescription: stringinputSchema?outputSchema?annotations?execute(input)(preferred)handler(input)(backward-compatible alias)formatOutput?(output)(deprecated)onSuccess?(result, input)onError?(error, input)
Return value:
stateexecute(input)reset()
execute(input) is a local direct call to your configured tool implementation for in-app control/testing.
Tool calls coming from MCP clients still go through the page document.modelContext surface.
Important Notes
- This is a client-side hook package (
'use client'). formatOutputis deprecated; preferoutputSchema+ structured output.- When tool output is not a string, default text content is pretty-printed JSON.
License
MIT
