Skip to content

useWebMCP

Category
Export Size
761 B
Last Changed
5 days ago

Register a WebMCP tool and tie its lifecycle to the current component.

WebMCP lets a page expose JavaScript functions as "tools" that an AI agent (browser-built-in, iframe-hosted, or extension) can discover and call, instead of scraping the DOM, the accessibility tree, or screenshots. useWebMCP wraps the imperative, AbortSignal-based registration API in a declarative hook: the tool is registered when the component mounts and unregistered automatically when it unmounts, so the set of tools an agent sees stays in lockstep with what is actually on screen.

Experimental

The WebMCP spec is 🧪 experimental and exposes the imperative API on document.modelContext (registerTool + an AbortSignal for unregistration). This hook feature-detects and degrades to a no-op everywhere the API is absent — check isSupported before relying on it.

Demo

Usage

tsx
import { 
useWebMCP
} from '@reause/core'
import {
useState
} from 'react'
const [
todos
,
setTodos
] =
useState
<string[]>([])
const {
isSupported
,
isRegistered
,
error
} =
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
}: {
text
: string }) {
setTodos
(
prev
=> [...
prev
,
text
])
return `Added todo item: "${
text
}" successfully.`
}, })

The raw imperative API this wraps looks like:

ts
const 
controller
= new
AbortController
()
document
.modelContext.registerTool({
name
: 'add-todo',
description
: 'Add a new item to the user\'s active todo list',
inputSchema
: { /* … */ },
async
execute
({
text
}) {
return {
content
: [{
type
: 'text',
text
: `Added todo item: "${
text
}".` }] }
}, }, {
signal
:
controller
.
signal
})
// Unregister later:
controller
.
abort
()

Result normalization

Whatever execute returns is normalized into a valid MCP tool result:

  • 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 valueError or not (throw 'not signed in', throw { code: 403 }) → { content: [{ type: 'text', text }], isError: true }, after onError. A failure must never read as success to the agent.
  • a returned Error → treated exactly like a throw: onError fires, then an isError result
  • anything else (object/array/number) → JSON-serialized into a text block

Reactive & conditional registration

name, description, inputSchema, annotations and enabled are plain React values — upstream takes Vue refs for the same fields. Pass a value derived from state and the tool will be re-registered when that value changes; toggling enabled unregisters and re-registers it. execute, formatOutput and onError are read live at call time, so a changing closure never churns the registration.

tsx
import { 
useWebMCP
} from '@reause/core'
import {
useState
} from 'react'
const [
signedIn
,
setSignedIn
] =
useState
(false)
useWebMCP
({
name
: 'checkout',
description
: 'Complete the checkout for the current cart',
enabled
:
signedIn
, // only exposed to agents while signed in
annotations
: {
readOnlyHint
: false },
execute
() {
// … },
onError
(
err
) {
console
.
error
('checkout tool failed',
err
)
}, })

Registering multiple tools

Call useWebMCP once per tool to register several — each call manages its own registration lifecycle.

tsx
import { 
useWebMCP
} from '@reause/core'
useWebMCP
({
name
: 'add-todo',
description
: 'Add a new item to the todo list',
execute
({
text
}) {
// … }, })
useWebMCP
({
name
: 'clear-todos',
description
: 'Remove every item from the todo list',
annotations
: {
readOnlyHint
: false },
execute
() {
// … }, })

References

Map from (source/vueuse/packages/core/useWebMCP/):

  • index.ts — upstream implementation
  • index.test.ts — upstream tests, mirrored by the co-located index.test.tsx

Type Declarations

Toggle
ts
export interface WebMCPToolContent {
    type: string;
    text?: string;
    [key: string]: unknown;
}

export interface WebMCPToolResponse {
    content: WebMCPToolContent[];
    isError?: boolean;
}

export interface WebMCPToolAnnotations {
    readOnlyHint?: boolean;
    untrustedContentHint?: boolean;
    [key: string]: unknown;
}

export interface WebMCPToolDescriptor {
    name: string;
    description: string;
    inputSchema?: object;
    annotations?: WebMCPToolAnnotations;
    execute: (args: any) => Promise<WebMCPToolResponse> | WebMCPToolResponse;
}

export interface ModelContext {
    registerTool: (tool: WebMCPToolDescriptor, options?: {
        signal?: AbortSignal;
    }) => void;
}

export interface UseWebMCPOptions<Args, Result> {
    name: string;
    description: string;
    inputSchema?: object;
    annotations?: WebMCPToolAnnotations;
    execute: (args: Args) => Result | Promise<Result>;
    enabled?: boolean;
    formatOutput?: (result: Result, args: Args) => unknown;
    onError?: (error: unknown) => void;
    document?: Document;
}

export interface UseWebMCPReturn {
    isSupported: boolean;
    isRegistered: boolean;
    error: Error | null;
}

export function useWebMCP<Args = Record<string, any>, Result = unknown>(options: UseWebMCPOptions<Args, Result>): UseWebMCPReturn

Source

Source · Demo

Contributors

hairyf

Changelog

v0.1.3 on
e07db - feat(core): implement useWebMCP (#867)

Released under the MIT License. v0.1.8