npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nxisai/webmcp

v0.2.0

Published

TypeScript polyfill for the WebMCP API (document.modelContext / navigator.modelContext)

Readme

@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 AI agents via document.modelContext.

It is built to the WebMCP spec: each method follows the spec's algorithm, in the spec's order, down to which error a call meets first. npm run conformance checks every behaviour against the spec, and shows beside it what a real Chrome does. Use it to:

  • write against document.modelContext today without feature-gating your code,
  • test tool registration and execution in unit tests / Node / browsers without WebMCP,
  • let in-page or extension-based agents discover and call your tools via getTools() / executeTool().

When the browser has its own document.modelContext, the polyfill steps aside. Browsers are still converging on the spec — see Browsers.

Install

npm install @nxisai/webmcp

Usage

Auto-install (recommended)

import "@nxisai/webmcp/auto";

const controller = new AbortController();

await 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 }, { signal }) {
      await addTodoItemToCollection(text, { signal });
      return { content: [{ type: "text", text: `Added todo item: "${text}".` }] };
    },
  },
  { signal: controller.signal }
);

// Later: unregister by aborting.
controller.abort();

registerTool returns a promise, and every failure arrives through it — none are thrown.

Explicit install

import { installWebMCPPolyfill, isWebMCPSupported } from "@nxisai/webmcp/polyfill";

if (!isWebMCPSupported()) {
  installWebMCPPolyfill();
}

modelContext is a secure-context API: in an insecure context nothing is installed and installWebMCPPolyfill() returns undefined.

Agent side: discovery and execution

const tools = await document.modelContext.getTools();
// [{ annotations?, description, inputSchema?, name, origin, title, window }], sorted by name

document.modelContext.addEventListener("toolchange", async () => {
  console.log("toolset changed:", await document.modelContext.getTools());
});

// Pass the tool from getTools(), and the input as an object.
const addTodo = tools.find((t) => t.name === "add-todo");
const result = await document.modelContext.executeTool(addTodo, { text: "buy milk" });
// => '{"content":[{"type":"text","text":"Added todo item: \"buy milk\"."}]}'  — always JSON
JSON.parse(result);

API

| Member | Description | | --- | --- | | registerTool(tool, { signal?, exposedTo? }) | Register one tool. Resolves once registered and announced; abort the signal to unregister. | | getTools({ fromOrigins? }) | The registered tools, as RegisteredTools, sorted by name. | | executeTool(tool, inputObject, { signal? }) | Run a tool. Resolves with its result as a JSON string. | | toolchange / ontoolchange | The set of tools changed. Once per registration or unregistration. | | toolactivated / ontoolactivated | A tool's execution began. A ToolActivatedEvent, with toolName. | | toolcancel / ontoolcancel | A tool's execution was cancelled. A ToolCancelEvent, with toolName. |

What each does with what it is given, per the spec:

registerTool

  • name, description and execute are required, and inputSchema must be an object: otherwise TypeError.
  • The name must be 1–128 characters of ASCII letters, digits, _, - and ., and not already registered; the description must not be empty: otherwise InvalidStateError.
  • A schema JSON cannot represent (a cycle) rejects with the serializer's error.
  • An already-aborted signal rejects with its reason. Aborting later unregisters the tool, fires toolchange, and rejects the promise if it is still pending.
  • An exposedTo origin that doesn't parse or isn't potentially trustworthy (https, or localhost) is a SecurityError.
  • The promise resolves after toolchange has fired.

getTools

  • Each tool has name, title ("" when none was given), description, origin and window, plus inputSchema and annotations when the tool declared them.
  • inputSchema is a copy through JSON: what JSON can't carry (an undefined default, say) is not there.
  • Annotations are the spec's four hints — readOnlyHint, untrustedContentHint, consequentialHint, debugging — each false unless set. Anything else a tool declares is dropped.

executeTool

  • tool must be a RegisteredTool — as getTools() returns it; a name is a TypeError. An unparseable or opaque origin is a NotSupportedError.
  • inputObject is required and must be an object; omitting it, or passing a string, null or a number, is a TypeError. The tool receives a fresh copy, through JSON — so a Date arrives as a string, and a Date as the whole input is refused with UnknownError, its JSON not being an object.
  • Input is not checked against inputSchema; the agent is responsible for sending what the schema asks for.
  • execute is called as a WebIDL callback: this is undefined, and its second argument is { signal }, aborted if the caller cancels.
  • The result is serialized as JSON — a string result arrives quoted ("done" → '"done"'). A tool that throws, or returns what JSON can't represent (undefined, a function, a cycle), rejects with UnknownError; so does a tool that isn't registered.
  • An already-aborted signal rejects with its reason. Aborting mid-run rejects with the reason, aborts the tool's signal, and fires toolcancel; a result that arrives afterwards is ignored.

validateAgainstSchema() — a lightweight JSON Schema checker — is exported for agents that want to check their input before calling.

What a polyfill can't do

  • Connect you to a real agent. Native WebMCP lets the browser's agent call your tools. A polyfill can only make the API available — something in the page (a script, test harness, or extension content script) still has to call getTools()/executeTool().
  • Other documents' tools. The spec's getTools() gathers tools from every frame in the page, and executeTool() runs them wherever they live. A script can't reach into other documents' registries, so here each document sees its own tools; a tool from another window rejects with UnknownError. exposedTo and fromOrigins are validated but otherwise have nothing to act on.
  • Browser-level checks. "Fully active" is always true for a running script. The tools permissions policy is honoured only where the browser knows the feature. The origin-keyed agent cluster check uses window.originAgentCluster where the browser exposes it.
  • The declarative (HTML form) API. Only the imperative API is polyfilled; the spec hasn't defined the declarative one yet.
  • No document at all. In Node there is no document origin; http://localhost stands in, so tools can run in tests.

Browsers

Chrome ships WebMCP ahead of the spec text in places, and npm run conformance lists where. Measured with this repo's scenarios:

| | Spec (this polyfill) | Chrome 155 (Beta) | Chrome 156 (Canary) | | --- | --- | --- | --- | | A string result | '"plain"' (JSON) | 'plain' | 'plain' | | A tool that returns nothing | rejects UnknownError | 'undefined' | 'undefined' | | executeTool(tool) with no input | rejects TypeError | runs with {} | runs with {} | | debugging annotation | reported | dropped | reported | | toolactivated / toolcancel | fired | not fired | not fired | | ontoolactivated / ontoolcancel | present | absent | absent |

Everything else the suite covers — registration and its errors, name rules, getTools()'s shape and order, input refusals, cancellation — Chrome 155 does as the spec says. Chrome 153 (stable) predates the spec further: it takes the input as a JSON string, refusing an object, and reports inputSchema as a string.

Code that runs against both is straightforward: always pass an input object, return an object (not a bare string or nothing) from execute, and parse executeTool's result with JSON.parse, falling back to the raw string.

Migrating from 0.1

| 0.1 | 0.2 | | --- | --- | | provideContext({ tools }) | Removed. Call registerTool for each tool; abort a signal to remove one. | | unregisterTool(name), clearContext() | Removed. Unregister by aborting the signal passed to registerTool. | | executeTool("name", JSON.stringify(input)) | executeTool(toolFromGetTools, input) — a RegisteredTool and an input object. | | executeTool resolved with { content: [...] } | Resolves with the result as JSON; JSON.parse it. | | Input could be omitted | Required. Pass {} for a tool that takes none. | | A throwing tool resolved with { isError: true } | Rejects with UnknownError. Return { isError: true, … } yourself to report a failure the agent should read. | | A tool returning undefined resolved | Rejects with UnknownError. Return something. | | Unknown tool → NotFoundError | UnknownError. | | Schema-invalid input → TypeError | Not validated; the tool gets the input as sent. | | Re-registering a name replaced the tool | Rejects with InvalidStateError. | | Any tool name | 1–128 of [A-Za-z0-9_.-]. | | registerTool threw synchronously | Returns a promise that rejects. | | execute(input, client) | execute(input, { signal }), called with this undefined. | | navigator.modelContext alias on by default | Off by default; not in the spec. | | ModelContextToolInfo, ModelContextToolAnnotations, ModelContextClient | RegisteredTool, ToolAnnotations, ToolExecuteCallbackOptions (old names kept as deprecated aliases). |

Development

npm install
npm test             # vitest
npm run build        # emits dist/
npm run conformance  # every behaviour against the spec, with Chrome beside it

npm run conformance runs the same scenarios through the polyfill and the browser's native document.modelContext, and compares both with what the spec's algorithms produce. It fails if the polyfill departs from the spec; where Chrome does, it says so without failing. It uses Chrome Beta by default. To test another Chrome, set CHROME_CHANNEL (for example chrome-canary) or CHROME_PATH. To test another implementation, set POLYFILL=/path/to/module.js#ExportName.