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

simple-webmcp

v0.3.0

Published

Turn any JS/TS function into a WebMCP tool — function-first, type-safe, lean. One line: webmcp(fn) still callable, auto-registers with React lifecycle or globally.

Readme

Make your existing functions agent-ready

simple-webmcp turns ordinary JavaScript and TypeScript functions into WebMCP tools without creating a second tool layer.

import { webmcp } from 'simple-webmcp';

async function searchCustomers({ query, limit = 20 }: { query: string; limit?: number }) {
  return customers.filter(c => c.name.includes(query)).slice(0, limit);
}

const search = webmcp(searchCustomers);

That's it. Your function stays callable:

await search({ query: 'alice' });

And can be exposed to WebMCP:

import { useWebMCP } from 'simple-webmcp/react';

function CustomersPage() {
  const tool = useWebMCP(search, { description: 'Search customers' });
  // visible while mounted — unregisters on unmount (AbortSignal)
  return <CustomersUI />;
}

Framework-agnostic core. React adapter included. Works with vanilla JS, TypeScript, Vite — Next.js support remains experimental until proven.

One function. Two interfaces. Human code search(input) and agent search(input) — same capability.

Write the function once. Expose it to humans and agents.

Docs: https://emingure.github.io/simple-webmcp/ · Live Demo: Try the shopping cart demo → (see examples/demo) · npm: simple-webmcp


Why simple-webmcp?

Most WebMCP integrations force a second layer:

Existing app logic
  ↓
Define tool metadata (name, description, inputSchema)
  ↓
Define execute wrapper
  ↓
Call registerTool()
  ↓
Manage lifecycle (AbortSignal, StrictMode, SSR)

simple-webmcp collapses it:

Existing function
  ↓
webmcp(existingFunction)
  ↓
Done — keep your API, types, and business logic

Use raw document.modelContext.registerTool() when you want total control over getTools() / executeTool() / toolchange / exposedTo — the browser imperative API (Chrome Docs). Use simple-webmcp when you already have addToCart, searchCustomers, updateCustomer and want them agent-callable without duplicating schema and lifecycle.

Focused on tool authoring and lifecycle — not a full WebMCP SDK. The browser provides getTools, executeTool, exposedTo etc.; we provide the tiny application layer on top and stay stable while WebMCP evolves underneath.

Before / After

Without — raw WebMCP:

document.modelContext.registerTool({
  name: 'add_to_cart',
  description: 'Add a product to the shopping cart',
  inputSchema: {
    type: 'object',
    properties: {
      productId: { type: 'string', description: 'Product ID' },
      quantity: { type: 'number', minimum: 1 }
    },
    required: ['productId', 'quantity']
  },
  execute: ({ productId, quantity }) => addToCart(productId, quantity)
}, { signal });

With — function-first:

import { webmcp } from 'simple-webmcp';

const addToCartTool = webmcp(addToCart, {
  description: 'Add a product to the shopping cart'
});

// same function, same app — one line
await addToCartTool({ productId: 'p_123', quantity: 2 }); // human
// agent calls same tool via WebMCP when <CartPage> is mounted

Same function. Same application. One line. No duplicated business logic.

For e-commerce, SaaS dashboards, booking, CRM, forms, internal tools — where addToCart, searchCustomers, createInvoice already exist and suddenly need to be agent-callable.

Install

npm i simple-webmcp
# React is optional peer only if you use simple-webmcp/react

Quick Start

Vanilla — manual or global

import { webmcp } from 'simple-webmcp';

export const tool = webmcp(addToCart, {
  description: 'Add product to shopping cart',
  fields: { productId: { description: 'Product ID' }, quantity: { type: 'integer', minimum: 1 } }
});

await tool({ productId: 'p_1', quantity: 2 }); // human
await tool.register(); // expose — uses document.modelContext (Chrome canary)
tool.unregister();

// global (registers on import, client only) — prefer scoped for least privilege
webmcp.global(addToCart, { description: '...' });

React — 1-line (recommended)

'use client';
import { useWebMCP } from 'simple-webmcp/react'; // alias: useTool

export function ProductPage() {
  const tool = useWebMCP(addToCart, { description: 'Add product to cart' });
  // tool is callable + has tool.registered / tool.status
  return <Product />;
}

Verbose 2-line still works: const t = webmcp(fn); useWebMCP(t). Layout-level: <Scope tools={[search, update]}>{children}</Scope> — naturally gives route-level scope in Next.js app/layout.tsx.

register() is async (Promise<() => void>) per current webmcp-types; hook maps to AbortSignal and dedupes StrictMode. status is 'unregistered'|'registering'|'registered'|'unsupported'|'error'supported and registered are mutually exclusive (unsupported never claims registered).

Hooks — before / after / error / denied (HITL)

const tool = webmcp(checkout, {
  description: 'Checkout cart',
  hooks: {
    before: [async ({input}) => {
      const ok = await confirm(`Checkout £${total}?`);
      if (!ok) return { action: 'deny', message: 'User declined' };
    }],
    after:  [({output}) => ({ output: redact(output) })],
    error:  [({error}) => console.warn(error)],
    denied: [({reason}) => analytics.track('denied', {reason})],
  }
});
webmcp.configure({ hooks:{ before:[trackInvocation], after:[trackResult] }});
<WebMCPProvider hooks={{ before:[addTenant] }}><Scope tools={[tool]}>{children}</Scope></WebMCPProvider>

Hooks wrap only the agent execute path — tool({input}) stays pure. Ordering: before global→scoped→tool, after tool→scoped→global. Direct + console + UI logs in the demo Hooks & HITL card. See Guide — Hooks and Analytics (PostHog, Mixpanel, GA4, Sentry, etc.).

Customize only what you need

const search = webmcp(searchCustomers, {
  description: 'Search customers by name or email',
  fields: {
    query: { description: 'Customer name, email, or ID' },
    limit: { type: 'integer', minimum: 1, maximum: 50 }
  }
});

Enhance inferred schemas without rewriting them. fields is a patch over the base schema (Partial<JsonSchema> or per-field StandardSchema). Whole schema establishes the contract; fields decorates it.

// Zod — requires side-effect (keeps core 6.26KB gz lean)
import { z } from 'zod';
import 'simple-webmcp/zod';
webmcp(fn, { schema: z.object({ query: z.string().min(1) }) });
webmcp(fn, { fields: { query: z.string().describe('Name') } });

How inference works

Infer what JavaScript can know at runtime. Get richer TypeScript/JSDoc inference with the optional build plugin.

Runtime — best-effort, confidence:'low': parameter names, defaults, destructured keys ({query, limit=20}query required, limit optional default:20), some primitives from literal defaults. function search(query: string) alone becomes {properties:{query:{}}} — we warn and need fields/schema or strict:true throws.

Build — optional simple-webmcp/unplugin (Vite/Webpack) reads TypeScript types + JSDoc before erasure. Same webmcp(fn) call, richer inputSchema, no code change.

Progressive:

webmcp(fn)
  ↓ add description
  ↓ add field metadata (fields)
  ↓ provide a schema (Zod)
  ↓ opt into build-time TS/JSDoc inference

Start with one function.

Comparison

| | simple-webmcp | raw WebMCP (document.modelContext) | usewebmcp | @mcp-b/react-webmcp | |---|---|---|---|---| | Existing function stays callable | ✅ | ❌ | ❌ | ❌ | | webmcp(fn) — function-first | ✅ | — | — | — | | Metadata patching | ✅ | Manual registerTool | Manual | Manual | | fields patch | ✅ | ❌ | ❌ | ❌ | | React lifecycle (AbortSignal, StrictMode) | ✅ | Manual | ✅ | ✅ | | Full MCP ecosystem / getTools etc | — (authoring) | ✅ Browser API | — | ✅ | | Weekly downloads | new | n/a (browser) | — | ~6k | | Focus | Function-first DX | Native API | React hooks | MCP ecosystem |

simple-webmcp + MCP-B are complementary. simple-webmcp authors capabilities; MCP-B / native WebMCP is the runtime. For real cross-browser WebMCP (Firefox/Safari), use the dedicated @mcp-b/webmcp-polyfill (~6k weekly):

npm i @mcp-b/webmcp-polyfill && import '@mcp-b/webmcp-polyfill'

simple-webmcp/polyfill is a dev/testing shim (in-memory registerTool/listTools/invokeTool, not full transport) — prefer simple-webmcp/dev-polyfill or simple-webmcp/testing in tests/Storybook. Native document.modelContext (not navigator.modelContext) is detected first.

Live Demo

Shopping Cart: MacBook £1,299 / Keyboard £99 / [Checkout] — agent says “Add a keyboard to my cart”add_to_cart via WebMCP, UI updates. Chrome's docs point to demos + inspector extension for this flow.

Admin Dashboard (e-commerce/CRM): search_customers, get_customer, update_customer, create_invoice — ordinary functions const tools = [webmcp(search), webmcp(get), webmcp(update)] exposed from the active component only. See examples/demo/ and https://emingure.github.io/simple-webmcp/demo/.

API

See .agents/skills/webmcp-simple/references/api.md.

Core: webmcp(fn, opts)WebMCPTool (callable + tool/definition/register/status), webmcp.global, webmcp.configure({hooks}), isWebMCPSupported(), registry.list(). Errors: NotSupportedError (unsupported status, mutually exclusive with registered), NotAllowedError, RegistrationError, ConfigurationError.

Hooks: hooks:{ before:[], after:[], error:[], denied:[] } on webmcp(fn,{hooks}), webmcp.configure, and <WebMCPProvider hooks>. See Guide — Hooks and Analytics for PostHog/Mixpanel/GA4/Sentry examples. Demo shows live hook log + HITL approval modal.

React: useWebMCP(fn, opts) / useWebMCP(tool)WebMCPTool & status / status, useTool alias, <Scope tools>, <WebMCPProvider hooks>.

Zod: import 'simple-webmcp/zod' then schema/fields accept Zod/StandardSchema.

WebMCP today is document.modelContext (Chrome canary, origin trial). This package tracks the spec — your app stays on the tiny webmcp(fn) API while we absorb browser changes.

Development

npm run build      # tsup ESM+CJS+DTS (core 6.26KB gz, zod 1.40KB separate)
npm test           # vitest jsdom — 47 tests
npm run typecheck
npm run docs:dev   # VitePress

Versioning

Follows Semantic Versioning. See CHANGELOG.md for the current 0.2.0 notes and RELEASING.md for the release process. No future roadmap is promised here — track GitHub issues/discussions for what's next.

Skills (Agent)

This package ships .agents/skills/webmcp-simple/SKILL.md for auto-discovery by OpenCode/Claude — the agent becomes distribution: “Make this function WebMCP callable”webmcp(fn) instead of raw registerTool.

License

MIT — Copyright © 2026 Muhammed Emin Gure (https://github.com/emingure)