npm.io
0.1.0 • Published 2 months ago

@nxisai/webmcp

Licence
MIT
Version
0.1.0
Deps
0
Vulns
0
Weekly
0

@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.modelContext today 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

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's execute also receives a signal it 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. exposedTo and getTools({ fromOrigins }) are accepted for API compatibility but not enforced/implemented; native behavior depends on the tools Permissions 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+) and navigator.modelContext (deprecated alias used by earlier explainer revisions and Chrome 149), so code written against either keeps working. Disable the alias with installWebMCPPolyfill({ installNavigatorAlias: false }).
  • Works in any environment with EventTarget, AbortController, and structuredClone (all modern browsers; Node 17+).

Development

npm install
npm test        # vitest
npm run build   # emits dist/

Keywords