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.
Maintainers
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 agentsearch(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 logicUse 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 mountedSame 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/reactQuick 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 inferenceStart 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-webmcpauthors 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/polyfillis a dev/testing shim (in-memoryregisterTool/listTools/invokeTool, not full transport) — prefersimple-webmcp/dev-polyfillorsimple-webmcp/testingin tests/Storybook. Nativedocument.modelContext(notnavigator.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 # VitePressVersioning
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)
