supportsWebMcp()
Pure feature detection. Returns true only when
document.modelContext exists and is a non-null object. Every other
function here that touches the browser calls this first — you'll rarely
need to call it yourself unless you're branching your own UI on WebMCP support.
None.
boolean — true if this browser supports WebMCP.
- Never throws, regardless of
document's shape — missing,null, not an object, or anything else.
if (supportsWebMcp()) { console.log('This browser can register WebMCP tools.'); }
defineTool(spec)
Validates and normalizes a tool definition. Derives the JSON Schema from your
Zod inputSchema, and wraps execute so every call is
parsed against that schema before your handler runs. Pure — it never
touches document.modelContext itself.
| Name | Type | Description |
|---|---|---|
spec.name | string | Non-empty, 1-128 characters, letters/numbers/"_"/"-"/"." only (the WebMCP spec's own charset). |
spec.description | string | Non-empty description shown to the agent. |
spec.inputSchema | ZodType | A Zod schema describing the tool's input. |
spec.execute | (input, ctx) => unknown | Your handler. Receives the parsed, typed input and { signal: AbortSignal }. |
spec.annotations? | { readOnlyHint?, untrustedContentHint? } | Optional. Forwarded as-is to document.modelContext.registerTool() — omitted from the result entirely when not passed. |
spec.title? | string | Optional human-readable label. Forwarded as-is — omitted from the result entirely when not passed. No format restriction, unlike name. |
DefinedTool — { name, description, inputSchema, execute, annotations?, title? }, with inputSchema as JSON Schema and execute wrapped and async.
- Synchronously, before doing anything else, if
nameordescriptionis empty or not a string, orexecuteis not a function. - If
namedoesn't match the WebMCP spec's charset/length ([A-Za-z0-9_.-]{1,128}) — fails here with a clear message instead of the browser rejecting it later with a less useful one.
- Via
console.warn(never throws) ifnameexceeds 30 characters ordescriptionexceeds 500 — the limits Chrome's tool security guide recommends for reliable agent results.
const tool = defineTool({ name: 'add_todo', description: 'Add a todo item to the list.', inputSchema: z.object({ text: z.string().min(1) }), execute: async ({ text }) => `Added: ${text}`, annotations: { readOnlyHint: false }, });
registerTool(spec, options?)
The entry point most code calls. Always validates via defineTool
first — a broken spec throws immediately, whether or not the browser
supports WebMCP, because a bug in your spec isn't something the fallback should
hide. Then checks supportsWebMcp(): if unsupported, logs a
console.warn naming the tool and returns false without
touching the browser; if supported, calls
document.modelContext.registerTool(tool, options) and returns
true.
| Name | Type | Description |
|---|---|---|
spec | ToolSpec<TSchema> | Same shape as defineTool's parameter. |
options.signal | AbortSignal? | Lets the browser unregister the tool later. |
options.exposedTo | string[]? | Secure origins allowed cross-origin access. |
boolean — true if actually registered with the browser, false if it no-opped.
- Same as
defineTool— an invalid spec throws regardless of browser support.
const registered = registerTool({ name: 'add_todo', description: 'Add a todo item to the list.', inputSchema: z.object({ text: z.string().min(1) }), execute: async ({ text }) => `Added: ${text}`, }); // registered === false on a browser without WebMCP -- the page still works
defineDeclarativeTool(form, spec)
Sets the four attributes the WebMCP Declarative API explainer defines directly
on a <form>: toolname, tooldescription,
toolautosubmit (presence-only), and toolparamdescription
on the matching field elements.
| Name | Type | Description |
|---|---|---|
form | DeclarativeFormElementLike | A form element (or anything with setAttribute and an elements collection). |
spec.name / description | string | Same rules as defineTool. |
spec.autoSubmit | boolean? | Sets toolautosubmit when true. |
spec.fields | {name,description}[]? | Matched against form.elements by name. |
void — it mutates the form in place.
- Same fail-fast as
defineToolforname/description. - If a field name in
spec.fieldshas no matching control inform.elements.
defineDeclarativeTool(form, {
name: 'submit_support_request',
description: 'Submit a request for support.',
fields: [{ name: 'topic', description: 'Determines what team this routes to.' }],
});
respondToAgentSubmit(event, handler)
The Declarative-side counterpart to defineTool's wrapped
execute. Checks event.agentInvoked; if
false (a human submitted the form), it does nothing and your
normal submit handling runs. If true, it calls your handler and
forwards the result — or a synchronous throw, turned into a rejection
— to event.respondWith().
| Name | Type | Description |
|---|---|---|
event | AgentSubmitEventLike | The SubmitEvent, with agentInvoked/respondWith per the explainer. |
handler | (event) => unknown | Receives the event, runs sync or async. |
boolean — true if the submit was agent-invoked and handled, false otherwise.
form.addEventListener('submit', (event) => { event.preventDefault(); const handled = respondToAgentSubmit(event, () => ({ status: 'submitted' })); if (!handled) { // a human submitted -- handle it however you normally would } });
createWebMcpMock()
A testing harness. Returns a document-shaped object you can assign
to globalThis.document, plus invokeTool() to call a
registered tool the same way a real agent would — running the exact
execute your tool was registered with, not a reimplementation.
None.
WebMcpMock — { document, registeredTools, invokeTool(name, input, context?) }.
invokeToolrejects with a clear error if no tool is registered under that name.- Defaults
context.signalto a freshAbortSignalif you don't provide one.
const mock = createWebMcpMock(); globalThis.document = mock.document; registerYourTools(); const result = await mock.invokeTool('add_todo', { text: 'Buy milk' });
toMcpwasmSkillSource(tool, options?)
Reuses the schema a DefinedTool already carries to emit the
registerTool({...}) source a real
mcpwasm tool.js
expects — verified against the real mcpwasm sandbox, not just checked for
valid syntax. Schema-only: it never tries to translate execute,
which exists to touch a DOM the sandbox doesn't have.
| Name | Type | Description |
|---|---|---|
tool | DefinedTool | The output of defineTool(). |
options.handlerBody | string? | Raw JS text for the sandboxed handler's body; defaults to a TODO stub naming the sandbox's constraints. |
string — the tool.js source text.
The exact code verified end-to-end against the real mcpwasm CLI — see the status section.
const tool = defineTool({ name: 'e2e_sum', description: 'Sum two numbers a and b.', inputSchema: z.object({ a: z.number(), b: z.number() }), execute: async ({ a, b }) => a + b, // browser-only, never auto-translated }); toMcpwasmSkillSource(tool, { handlerBody: 'return Number(args.a) + Number(args.b);', });