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

@web-axag/webmcp

v1.1.1

Published

Register AXAG actions as WebMCP tools, tied to the lifetime of the UI they belong to

Readme

@web-axag/webmcp

Registers AXAG actions as WebMCP tools, for as long as the UI they belong to is on screen.

npm install @web-axag/webmcp

Registering the tools your build produced

import { registerManifest } from '@web-axag/webmcp';
import { tools } from 'virtual:axag/tools'; // from @web-axag/compiler

const route = new AbortController();
registerManifest(tools, {
  signal: route.signal,
  handlers: { user_deactivate: ({ user_id }) => api.deactivate(user_id) },
});

// Leaving the route takes the tools with it.
route.abort();

This path is ~1.8 KB gzipped: the tools were built ahead of time, so nothing that reads annotations ships to the browser.

Registering one action

import { defineAction } from '@web-axag/core';
import { registerAction } from '@web-axag/webmcp';

const controller = new AbortController();
registerAction(
  defineAction({
    intent: 'user.deactivate',
    actionType: 'write',
    riskLevel: 'critical',
    requiredParameters: [{ name: 'user_id', type: 'string' }],
    handler: ({ user_id }) => api.deactivate(user_id),
  }),
  { element: button, signal: controller.signal },
);

For components, use the framework bindings instead: @web-axag/react, @web-axag/vue, @web-axag/angular.

Reading the page instead

import { registerDocument } from '@web-axag/webmcp';

registerDocument({ signal: controller.signal });

Every annotated element on the page is registered, with parameters harvested from its form. Costs a DOM scan and ~7.9 KB gzipped, so prefer the build-time path where you have one.

Lifecycle

  • Aborting the signal unregisters. WebMCP has no unregisterTool; the signal is the lifecycle.
  • Operability is watched. An action whose element is disabled, hidden, inert, aria-hidden, aria-disabled or detached is unregistered, and registered again when the element comes back. One MutationObserver serves every registration.
  • No handler needed. The default handler fills the element's form from the tool's arguments — writing through the prototype setter so React and Vue see the change — then presses the control.

Safety enforcers

The middleware high-risk actions need, without writing the glue yourself:

import { createEnforcers, registerManifest } from '@web-axag/webmcp';

const safety = createEnforcers({
  confirm: { from: 'high', endpoint: '/axag/confirm' },
  tenant: { id: () => session.tenantId },
  csrf: {},
  audit: event => analytics.track('agent_action', event),
});

registerManifest(tools, { signal: route.signal, handlers, ...safety });
  • Confirmation — a dialog in a closed shadow root, showing the action and its parameters read-only, for anything at high risk or above (or with confirmation_required). Declining refuses the call with AXAG_CONFIRMATION_MISSING. With endpoint, the answer is exchanged for a single-use token the server can verify.
  • Tenant scope — tenant parameters are removed from the agent-facing schema, so an agent cannot choose a tenant, and the session's tenant is attached instead.
  • CSRF — the token from <meta name="csrf-token"> is attached.
  • Audit — every call is recorded with its outcome and parameter names only.

What they add travels in a reserved _axag entry alongside the agent's parameters. Your handler forwards it:

import { axagHeaders, withoutEnvelope } from '@web-axag/webmcp';

const handler = input =>
  fetch('/api/users/deactivate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...axagHeaders(input) },
    body: JSON.stringify(withoutEnvelope(input)),
  });

Custom middleware still works the same way, and runs in the order given:

registerManifest(tools, { signal: route.signal, middleware: [async (ctx, next) => next()] });

:::warning These run in the page. An agent with the page's credentials can call your API directly, so @web-axag/server has to check the same things. :::

The draft it targets

document.modelContext.registerTool(tool, { signal }), falling back to navigator.modelContext (deprecated in Chromium 150). provideContext/clearContext were removed from the spec in March 2026 and are not used. Everything version-specific lives in src/model-context.ts; when the draft moves again, that file is what changes.

Where the browser has no WebMCP at all, registerTool reports through onError rather than throwing.