@nxisai/webmcp
A TypeScript polyfill for the WebMCP API — the proposed web standard (webmachinelearning/webmcp) that lets web pages expose Model Context Protocol–style tools to browser AI agents via document.modelContext.
WebMCP ships natively behind an origin trial starting in Chrome 149 (chrome://flags/#enable-webmcp-testing). This polyfill provides the same imperative API surface everywhere else, so you can:
- write against
document.modelContexttoday without feature-gating your code, - test tool registration and execution in unit tests / Node / non-Chromium browsers,
- let in-page or extension-based agents discover and call your tools via
getTools()/executeTool().
When a native implementation exists, the polyfill steps aside automatically.
Install
npm install @nxisai/webmcp
Usage
Auto-install (recommended)
import "@nxisai/webmcp/auto";
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 }
);
// Later: unregister by aborting.
controller.abort();
Explicit install
import { installWebMCPPolyfill, isWebMCPSupported } from "@nxisai/webmcp/polyfill";
if (!isWebMCPSupported()) {
installWebMCPPolyfill();
}
Page-level context with provideContext()
Each call replaces the full set of page-level tools (tools added with registerTool() are unaffected) — useful for SPA route changes:
document.modelContext.provideContext({
tools: [searchFlightsTool, filterResultsTool],
});
// Navigating to checkout? Swap the toolset wholesale:
document.modelContext.provideContext({ tools: [checkoutTool] });
Agent side: discovery and execution
const tools = await document.modelContext.getTools();
// [{ name, description, inputSchema, annotations?, origin }]
document.modelContext.addEventListener("toolchange", async () => {
console.log("toolset changed:", await document.modelContext.getTools());
});
// Input may be a JSON string (native API shape) or a plain object.
const result = await document.modelContext.executeTool(
"add-todo",
JSON.stringify({ text: "buy milk" })
);
// => { content: [{ type: "text", text: 'Added todo item: "buy milk" successfully.' }] }
API
| Member | Description |
|---|---|
registerTool(tool, { signal?, exposedTo? }) |
Register one tool. Re-registering a name replaces it; abort the signal to unregister. |
provideContext({ tools? }) |
Set the page-level toolset; each call replaces the previous one. |
getTools({ fromOrigins? }) |
Resolve the list of registered tools (without execute). |
executeTool(name, input?, { signal? }) |
Validate input against the tool's inputSchema and run it. Returns a normalized { content: [...] } response. |
toolchange event / ontoolchange |
Fired (coalesced per microtask) when the toolset changes. |
unregisterTool(name), clearContext() |
Polyfill conveniences, not part of the proposal. |
Error semantics for executeTool():
- unknown tool → rejects with
NotFoundError - malformed JSON or schema-invalid input → rejects with
TypeError - abort → rejects with
AbortError(the tool'sexecutealso receives asignalit can observe) - exception thrown inside the tool → resolves with
{ isError: true, content: [...] }, mirroring MCP tool-result error semantics
Input validation uses a built-in lightweight JSON Schema checker (type, properties/required, items, enum/const, additionalProperties, numeric/string/array bounds, pattern, anyOf/oneOf/allOf). It's also exported standalone as validateAgainstSchema().
What a polyfill can't do
- Connect you to a real agent. Native WebMCP lets the browser's agent (or an assistant like Gemini in Chrome) call your tools. A polyfill can only make the API surface available — something in the page (a script, test harness, or extension content script) still has to call
getTools()/executeTool(). - Cross-origin tool exposure.
exposedToandgetTools({ fromOrigins })are accepted for API compatibility but not enforced/implemented; native behavior depends on thetoolsPermissions Policy and origin isolation (Origin-Agent-Cluster). - The declarative (HTML form annotation) API. Only the imperative API is polyfilled.
Compatibility notes
- Installed at
document.modelContext(current spec, Chrome 150+) andnavigator.modelContext(deprecated alias used by earlier explainer revisions and Chrome 149), so code written against either keeps working. Disable the alias withinstallWebMCPPolyfill({ installNavigatorAlias: false }). - Works in any environment with
EventTarget,AbortController, andstructuredClone(all modern browsers; Node 17+).
Development
npm install
npm test # vitest
npm run build # emits dist/