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

fastwebmcp

v0.5.0

Published

FastMCP-style ergonomics for WebMCP: typed builders over the Imperative and Declarative browser APIs.

Downloads

400

Readme

fastwebmcp

CI npm GitHub release license

FastMCP-style ergonomics for WebMCP: typed Zod builders over the browser's Imperative and Declarative APIs, with safe no-op + warning degradation when document.modelContext isn't available (WebMCP is still an origin trial as of Chrome 149+ — most visitors won't have it yet).

Install

npm install fastwebmcp

Imperative API

import { z } from 'zod';
import { registerTool } from 'fastwebmcp';

registerTool({
  name: 'add_todo',
  description: 'Add a todo item to the list.',
  inputSchema: z.object({ text: z.string().min(1) }),
  execute: async ({ text }) => {
    // ... your logic, DOM update, etc.
    return `Added: ${text}`;
  },
});

registerTool validates and normalizes the spec with defineTool (deriving the JSON Schema from the Zod schema via z.toJSONSchema, and parsing every call's input before your handler runs), then calls document.modelContext.registerTool(...) if the browser supports it — falling back to a console.warn no-op otherwise, so your page never breaks on an unsupported browser.

defineTool also validates name against the WebMCP spec's own charset (1-128 chars, [A-Za-z0-9_.-]), and warns — never throws — if name/description exceed the length Chrome's tool security guide recommends for reliable agent results. Pass annotations: { readOnlyHint, untrustedContentHint } to flag a tool as side-effect-free or as returning untrusted data — it's forwarded as-is to document.modelContext.registerTool(). Pass title for an optional human-readable label; also forwarded as-is.

Declarative API

import { defineDeclarativeTool, respondToAgentSubmit } from 'fastwebmcp';

const form = document.querySelector('form')!;

defineDeclarativeTool(form, {
  name: 'submit_support_request',
  description: 'Submit a request for support.',
  fields: [{ name: 'topic', description: 'Determines what team this routes to.' }],
});

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const handled = respondToAgentSubmit(event as any, () => ({ status: 'submitted' }));
  if (!handled) {
    // a human submitted the form -- handle it however you normally would
  }
});

defineDeclarativeTool sets the toolname/tooldescription/toolautosubmit/ toolparamdescription attributes the WebMCP Declarative API explainer specifies. The JSON Schema the browser derives from the form's fields is not something this library computes or validates — that algorithm is still unspecified upstream.

Testing your own tools without a real browser

import {
  createWebMcpMock,
  withMockDocument,
  createMockAgentSubmitEvent,
  respondToAgentSubmit,
} from 'fastwebmcp';

const mock = createWebMcpMock();

// Isolated execution without polluting globalThis:
await withMockDocument(mock, async () => {
  registerYourTools();
  const result = await mock.invokeTool('add_todo', { text: 'Buy milk' });
});

// Check registrations and clean up between tests:
mock.hasTool('add_todo'); // true
mock.reset(); // clears all registered tools for the next test

// Unregistration via AbortSignal (WebMCP spec):
const controller = new AbortController();
registerTool(mySpec, { signal: controller.signal });
controller.abort(); // tool is automatically removed from mock

// Testing declarative form submissions:
const { event, waitForResponse } = createMockAgentSubmitEvent();
respondToAgentSubmit(event, () => ({ status: 'processed' }));
const response = await waitForResponse(); // { status: 'processed' }

invokeTool runs the real execute your tool was registered with (Zod parsing included) — not a reimplementation. withMockDocument isolates globalThis.document safely with try...finally, and hasTool(name), getTool(name) and reset() make it easy to assert tool registrations and isolate tests in suites like Jest, Vitest, or node:test.

Optional LSFA integration

Use fastwebmcp/lsfa when a WebMCP tool needs sensitive local capture or explicit human confirmation. The agent-facing schema must contain only non-sensitive intent. A trusted broker supplied by your application owns LSFA policy, risk, secure fields, presentation, confirmation, expiry, binding, single-use consumption and the side effect itself.

import { z } from 'zod';
import { registerLsfaTool, type LsfaBroker } from 'fastwebmcp/lsfa';

declare const trustedBroker: LsfaBroker; // your LSFA host adapter, not FastWebMCP

registerLsfaTool({
  name: 'send_email_securely',
  description: 'Ask the trusted local broker to confirm and send an email.',
  inputSchema: z.object({ recipient: z.string().email(), subject: z.string() }),
  intent: {
    operation: 'send_email',
    purpose: 'Send only after local confirmation.',
    presentation: { mode: 'form', profile: 'confirmation', locale: 'en-US' },
  },
  broker: trustedBroker,
}, { exposedTo: ['https://trusted-agent.example'] });

Schemas containing password, secret, token, credentials, API keys, PIN, OTP or TOTP fields are rejected recursively. The broker result is also strict and sanitized; captured values never return through WebMCP. This route is imperative-only, so it never enables toolautosubmit. Import createLsfaBrokerMock from fastwebmcp/lsfa/testing only in tests or demos: it records calls and returns explicitly queued results, but performs no capture, authorization or execution and provides no production security guarantees.

The included LSFA demo is prominently marked as a simulation. A real broker transport (HTTP loopback, Native Messaging or extension) is intentionally outside this first contract.

The result shape follows LSFA's published 0.2 schema: only status and operation are required; request_id, risk, boolean checks, stored_refs (true, false, present, or absent) and error_code are optional. The presentation validator follows the 0.3 modes, themes and layout limits. A profile-only object is accepted as an adapter hint for convenience; the broker must add/choose the canonical mode and validate the complete LSFA request against its own policy before showing anything.

Framework Integration (React, Next.js, Vue)

WebMCP tools in single-page applications should register when components mount and clean up when they unmount using an AbortController:

React / Next.js

import { useEffect } from 'react';
import { registerTool, type ToolSpec } from 'fastwebmcp';
import type { ZodType } from 'zod';

export function useWebMcpTool<T extends ZodType>(spec: ToolSpec<T>) {
  useEffect(() => {
    const controller = new AbortController();
    registerTool(spec, { signal: controller.signal });
    return () => controller.abort(); // Automatically unregisters on unmount
  }, [spec.name]);
}

Vue 3 (Composition API)

import { onMounted, onUnmounted } from 'vue';
import { registerTool, type ToolSpec } from 'fastwebmcp';
import type { ZodType } from 'zod';

export function useWebMcpTool<T extends ZodType>(spec: ToolSpec<T>) {
  const controller = new AbortController();
  onMounted(() => {
    registerTool(spec, { signal: controller.signal });
  });
  onUnmounted(() => {
    controller.abort();
  });
}

Publishing the same schema to mcpwasm

import { defineTool, toMcpwasmSkillSource } from 'fastwebmcp';

const tool = defineTool({
  name: 'sum_numbers',
  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
});

console.log(
  toMcpwasmSkillSource(tool, {
    handlerBody: 'return args.a + args.b;', // you write this: no DOM in the sandbox
  }),
);

toMcpwasmSkillSource reuses the JSON Schema defineTool already derived from your Zod spec to emit the registerTool({...}) source mcpwasm expects in a tool.js. This is schema-only, not a runtime bridge: mcpwasm's handler runs sandboxed inside QuickJS-wasm with no DOM, no fetch, no window — only registerTool, host.fetchOrigin, and bare ECMAScript — so your execute (which exists specifically to touch the page) can't run there unmodified. What crosses the boundary is name/description/inputSchema; the sandboxed handler body is always yours to write (the function defaults to an explicit TODO stub if you don't supply handlerBody). It doesn't reimplement mcpwasm's own @rckflr/llms-skills CLI, which stays the tool for scaffolding, hash-sealing, and publishing.

Examples

Two runnable demo pages, verified against a real document.modelContext, live in examples/:

npm run build:examples
npx http-server .   # or any static file server
# open examples/ux-page/imperative-demo.html and .../declarative-demo.html

API surface

supportsWebMcp() · defineTool(spec) · registerTool(spec, options?) · createWebMcpMock() · defineDeclarativeTool(form, spec) · respondToAgentSubmit(event, handler) · toMcpwasmSkillSource(tool, options?) · defineLsfaTool(spec) · registerLsfaTool(spec, options?)

Changelog

Every release is documented in CHANGELOG.md, including the RECON findings and known limits behind each one — not just a list of what shipped.

Development / methodology

This repository is built with KDD (Knowledge-Driven Development): every function ships with a frozen test oracle authored before the implementation, and project-level work is tracked as numbered execution contracts under specs/ with verified reports in docs/reports/. See AGENTS.md and knowledge/index.md if you're contributing or want the full methodology reference.

Running the test suite locally (npm test) needs Node.js 23.6+ — it relies on Node's native .ts execution (node --test tests_ts/**/*.test.ts), which is separate from the engines.node: ">=18" this package declares for consumers of the published dist/ (plain compiled JS, no native .ts support needed there).

License

MIT — see LICENSE.