simple-webmcp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.agents/skills/webmcp-simple/SKILL.md +27 -12
  2. package/README.md +211 -71
  3. package/dist/dev-polyfill.cjs +68 -0
  4. package/dist/dev-polyfill.cjs.map +1 -0
  5. package/dist/dev-polyfill.d.cts +1 -0
  6. package/dist/dev-polyfill.d.ts +1 -0
  7. package/dist/dev-polyfill.js +62 -0
  8. package/dist/dev-polyfill.js.map +1 -0
  9. package/dist/devtools.cjs +452 -0
  10. package/dist/devtools.cjs.map +1 -0
  11. package/dist/devtools.d.cts +14 -0
  12. package/dist/devtools.d.ts +14 -0
  13. package/dist/devtools.js +449 -0
  14. package/dist/devtools.js.map +1 -0
  15. package/dist/index.cjs +350 -41
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +60 -51
  18. package/dist/index.d.ts +60 -51
  19. package/dist/index.js +344 -42
  20. package/dist/index.js.map +1 -1
  21. package/dist/inspect.cjs +342 -0
  22. package/dist/inspect.cjs.map +1 -0
  23. package/dist/inspect.d.cts +37 -0
  24. package/dist/inspect.d.ts +37 -0
  25. package/dist/inspect.js +334 -0
  26. package/dist/inspect.js.map +1 -0
  27. package/dist/polyfill.cjs.map +1 -1
  28. package/dist/polyfill.d.cts +15 -7
  29. package/dist/polyfill.d.ts +15 -7
  30. package/dist/polyfill.js.map +1 -1
  31. package/dist/react.cjs +1191 -12
  32. package/dist/react.cjs.map +1 -1
  33. package/dist/react.d.cts +28 -10
  34. package/dist/react.d.ts +28 -10
  35. package/dist/react.js +1190 -15
  36. package/dist/react.js.map +1 -1
  37. package/dist/registry-A4DdpmDn.d.cts +38 -0
  38. package/dist/registry-Dscedahn.d.ts +38 -0
  39. package/dist/testing.cjs +68 -0
  40. package/dist/testing.cjs.map +1 -0
  41. package/dist/testing.d.cts +1 -0
  42. package/dist/testing.d.ts +1 -0
  43. package/dist/testing.js +62 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/{types-D-cwSfEU.d.cts → types-CF8kTj5O.d.cts} +61 -2
  46. package/dist/{types-D-cwSfEU.d.ts → types-CF8kTj5O.d.ts} +61 -2
  47. package/dist/zod.d.cts +1 -1
  48. package/dist/zod.d.ts +1 -1
  49. package/package.json +30 -1
@@ -42,18 +42,27 @@ searchTool.unregister();
42
42
  await searchTool({ query: 'alice' });
43
43
  ```
44
44
 
45
- ### 2. React — component lifecycle
45
+ ### 2. React — component lifecycle (1-line optional)
46
46
 
47
47
  ```tsx
48
48
  'use client';
49
- import { webmcp } from 'simple-webmcp';
50
49
  import { useWebMCP, Scope } from 'simple-webmcp/react';
50
+ import { useTool } from 'simple-webmcp/react'; // alias
51
51
 
52
- const addToCartTool = webmcp(addToCart, { description: 'Add product to cart' });
53
-
52
+ // 1-line: wrap + register while mounted recommended
54
53
  export function ProductPage() {
55
- // exposed only while component mounted maps to AbortSignal
56
- useWebMCP(addToCartTool);
54
+ const addToCartTool = useWebMCP(addToCart, { description: 'Add product to cart' });
55
+ // or: const tool = useTool(addToCart, { description: '...' });
56
+ // addToCartTool({productId:'p1', quantity:1}) still callable
57
+ // addToCartTool.registered, addToCartTool.status also available
58
+ return <div>...</div>;
59
+ }
60
+
61
+ // verbose 2-line still works:
62
+ import { webmcp } from 'simple-webmcp';
63
+ const addToCartTool2 = webmcp(addToCart, { description: 'Add product to cart' });
64
+ export function ProductPage2() {
65
+ useWebMCP(addToCartTool2);
57
66
  return <div>...</div>;
58
67
  }
59
68
 
@@ -86,10 +95,16 @@ In all cases `schema` (whole) establishes contract; `fields` patches it (adds de
86
95
 
87
96
  ### 4. Polyfill (non-Chrome)
88
97
 
98
+ For **production cross-browser** (Firefox/Safari), use the dedicated polyfill:
99
+
100
+ ```ts
101
+ import '@mcp-b/webmcp-polyfill'; // ~6k weekly, real transport
102
+ ```
103
+
104
+ For **dev/testing** (Storybook, vitest, local without Chrome), `simple-webmcp` provides a thin in-memory shim (not full MCP):
105
+
89
106
  ```ts
90
- // before importing tools — no-op in Chrome with native WebMCP
91
- import 'simple-webmcp/polyfill';
92
- // or programmatic: import { installPolyfill } from 'simple-webmcp/polyfill'; installPolyfill();
107
+ import 'simple-webmcp/dev-polyfill'; // or 'simple-webmcp/testing' or legacy 'simple-webmcp/polyfill'
93
108
  ```
94
109
 
95
110
  ### 5. Global vs Scoped
@@ -105,11 +120,11 @@ webmcp.global(fn, { description: '…' }); // alias
105
120
 
106
121
  ## Progression Ladder (teach stepwise)
107
122
 
108
- 1. **Beginner:** `webmcp(fn)` done.
109
- 2. **Better desc:** `webmcp(fn, { description: '…' })`
123
+ 1. **Beginner:** `webmcp(fn)` done (vanilla) or `useWebMCP(fn,{description})` / `useTool(fn)` in React — 1 line.
124
+ 2. **Better desc:** `webmcp(fn, { description: '…' })` or `useTool(fn,{description})`
110
125
  3. **Field docs:** `webmcp(fn, { fields: { query: { description: '…' } } })`
111
126
  4. **Full control:** `webmcp(fn, { schema: z.object({…}) })`
112
- 5. **Lifecycle:** `useWebMCP(tool)` / `<Scope tools>`
127
+ 5. **Lifecycle:** `useWebMCP(tool)` / `useTool(fn, opts)` / `<Scope tools>`
113
128
 
114
129
  ## Rules
115
130
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # simple-webmcp
1
+ # Make your existing functions agent-ready
2
2
 
3
- > Turn any JS/TS function into a WebMCP tool `webmcp(fn)` stays callable, auto-registers with React lifecycle or globally. Minimal, typed, framework-agnostic.
3
+ `simple-webmcp` turns ordinary JavaScript and TypeScript functions into WebMCP tools without creating a second tool layer.
4
4
 
5
5
  ```ts
6
6
  import { webmcp } from 'simple-webmcp';
@@ -9,128 +9,268 @@ async function searchCustomers({ query, limit = 20 }: { query: string; limit?: n
9
9
  return customers.filter(c => c.name.includes(query)).slice(0, limit);
10
10
  }
11
11
 
12
- // One line — same function, now also a tool
13
- export const searchTool = webmcp(searchCustomers, {
14
- description: 'Search customers in current account',
15
- fields: { query: { description: 'Name, email, or ID' } },
12
+ const search = webmcp(searchCustomers);
13
+ ```
14
+
15
+ That's it. Your function stays callable:
16
+
17
+ ```ts
18
+ await search({ query: 'alice' });
19
+ ```
20
+
21
+ And can be exposed to WebMCP:
22
+
23
+ ```ts
24
+ import { useWebMCP } from 'simple-webmcp/react';
25
+
26
+ function CustomersPage() {
27
+ const tool = useWebMCP(search, { description: 'Search customers' });
28
+ // visible while mounted — unregisters on unmount (AbortSignal)
29
+ return <CustomersUI />;
30
+ }
31
+ ```
32
+
33
+ **Framework-agnostic core. React adapter included.** Works with vanilla JS, TypeScript, Vite — Next.js support remains experimental until proven.
34
+
35
+ > **One function. Two interfaces.** Human code `search(input)` and agent `search(input)` — same capability.
36
+ >
37
+ > **Write the function once. Expose it to humans and agents.**
38
+
39
+ **Docs:** https://emingure.github.io/simple-webmcp/ · **Live Demo:** [Try the shopping cart demo →](https://emingure.github.io/simple-webmcp/demo/) *(see `examples/demo`)* · **npm:** `simple-webmcp`
40
+
41
+ ---
42
+
43
+ ## Why simple-webmcp?
44
+
45
+ Most WebMCP integrations force a second layer:
46
+
47
+ ```text
48
+ Existing app logic
49
+
50
+ Define tool metadata (name, description, inputSchema)
51
+
52
+ Define execute wrapper
53
+
54
+ Call registerTool()
55
+
56
+ Manage lifecycle (AbortSignal, StrictMode, SSR)
57
+ ```
58
+
59
+ `simple-webmcp` collapses it:
60
+
61
+ ```text
62
+ Existing function
63
+
64
+ webmcp(existingFunction)
65
+
66
+ Done — keep your API, types, and business logic
67
+ ```
68
+
69
+ Use **raw `document.modelContext.registerTool()`** when you want total control over `getTools()` / `executeTool()` / `toolchange` / `exposedTo` — the browser imperative API ([Chrome Docs](https://developer.chrome.com/docs/ai/webmcp/imperative-api)). Use **`simple-webmcp`** when you already have `addToCart`, `searchCustomers`, `updateCustomer` and want them agent-callable without duplicating schema and lifecycle.
70
+
71
+ **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.
72
+
73
+ ## Before / After
74
+
75
+ **Without — raw WebMCP:**
76
+
77
+ ```ts
78
+ document.modelContext.registerTool({
79
+ name: 'add_to_cart',
80
+ description: 'Add a product to the shopping cart',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ productId: { type: 'string', description: 'Product ID' },
85
+ quantity: { type: 'number', minimum: 1 }
86
+ },
87
+ required: ['productId', 'quantity']
88
+ },
89
+ execute: ({ productId, quantity }) => addToCart(productId, quantity)
90
+ }, { signal });
91
+ ```
92
+
93
+ **With — function-first:**
94
+
95
+ ```ts
96
+ import { webmcp } from 'simple-webmcp';
97
+
98
+ const addToCartTool = webmcp(addToCart, {
99
+ description: 'Add a product to the shopping cart'
16
100
  });
17
101
 
18
- await searchTool({ query: 'alice' }); // still just a function
19
- await searchTool.register(); // => document.modelContext.registerTool(...)
20
- // or React: useWebMCP(searchTool) mounted = exposed
102
+ // same function, same app one line
103
+ await addToCartTool({ productId: 'p_123', quantity: 2 }); // human
104
+ // agent calls same tool via WebMCP when <CartPage> is mounted
21
105
  ```
22
106
 
23
- Works with plain JS, TS, React, Vite… (Next.js Server Actions experimental 0.3). Open source, lean (`sideEffects:false`, core no React dep).
107
+ **Same function. Same application. One line.** No duplicated business logic.
24
108
 
25
- **Docs:** https://emingure.github.io/simple-webmcp/ (via GitHub Pages)
109
+ For e-commerce, SaaS dashboards, booking, CRM, forms, internal tools — where `addToCart`, `searchCustomers`, `createInvoice` already exist and suddenly need to be agent-callable.
26
110
 
27
111
  ## Install
28
112
 
29
113
  ```bash
30
114
  npm i simple-webmcp
31
- # React is optional peer (≥18) only if you import simple-webmcp/react
32
- # Repo: https://github.com/emingure/simple-webmcp
115
+ # React is optional peer only if you use simple-webmcp/react
33
116
  ```
34
117
 
35
118
  ## Quick Start
36
119
 
37
- | Need | Code |
38
- |------|------|
39
- | Vanilla — global | `webmcp(fn,{description, global:true})` or `await tool.register()` |
40
- | Vanilla — manual | `tool.register()` / `tool.unregister()` |
41
- | React — page | `useWebMCP(tool)` while component mounted |
42
- | React — layout / route | `<Scope tools={[a,b]}>{children}</Scope>` |
43
- | Rate-limited / disabled | `webmcp(fn,{enabled:false})` |
44
- | Polyfill (Firefox/Safari) | `import 'simple-webmcp/polyfill'` |
45
-
46
- ## Schema
47
-
48
- Hierarchy **corrected per review**: `schema` (whole) → inferred (runtime 0.1 / build 0.2 TS/JSDoc) → `fields` patch → metadata override.
120
+ ### Vanilla manual or global
49
121
 
50
122
  ```ts
51
- // prefer single object param fn({query, limit}) for best inference
52
- webmcp(fn, { description:'…', fields:{ query:{description:'…'}, limit:{type:'integer', maximum:50} } });
123
+ import { webmcp } from 'simple-webmcp';
53
124
 
54
- // whole Zod / StandardSchema — requires `simple-webmcp/zod` side-effect (keeps core lean)
55
- import { z } from 'zod';
56
- import 'simple-webmcp/zod'; // enables Zod JSON Schema in core
57
- webmcp(fn, { description:'…', schema: z.object({ query: z.string().min(1) }) });
125
+ export const tool = webmcp(addToCart, {
126
+ description: 'Add product to shopping cart',
127
+ fields: { productId: { description: 'Product ID' }, quantity: { type: 'integer', minimum: 1 } }
128
+ });
58
129
 
59
- // per-field mix same import enables it
60
- webmcp(fn, { fields:{ query: z.string().describe('…'), limit:{type:'integer'} } });
130
+ await tool({ productId: 'p_1', quantity: 2 }); // human
131
+ await tool.register(); // expose uses document.modelContext (Chrome canary)
132
+ tool.unregister();
61
133
 
62
- // JSON Schema directly
63
- webmcp(fn, { schema:{type:'object', properties:{query:{type:'string'}}, required:['query']} });
134
+ // global (registers on import, client only) — prefer scoped for least privilege
135
+ webmcp.global(addToCart, { description: '...' });
64
136
  ```
65
137
 
66
- `fields` is a **patch** (`Partial<JsonSchema>` or per-field `StandardSchema`) — annotates, does not silently replace core type. Provide `strict:true` to make ambiguous runtime inference throw instead of warn.
67
-
68
- ## React
138
+ ### React 1-line (recommended)
69
139
 
70
140
  ```tsx
71
141
  'use client';
72
- import { webmcp } from 'simple-webmcp';
73
- import { useWebMCP, Scope } from 'simple-webmcp/react';
74
-
75
- const tool = webmcp(fn, { description:'…' });
142
+ import { useWebMCP } from 'simple-webmcp/react'; // alias: useTool
76
143
 
77
- export function Page() {
78
- const { supported, registered, error } = useWebMCP(tool);
79
- return null; // exposed while mounted maps to AbortSignal
144
+ export function ProductPage() {
145
+ const tool = useWebMCP(addToCart, { description: 'Add product to cart' });
146
+ // tool is callable + has tool.registered / tool.status
147
+ return <Product />;
80
148
  }
149
+ ```
150
+
151
+ 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`.
81
152
 
82
- // layout-level route scope (Next.js app/layout.tsx naturally gives route scope)
83
- <Scope tools={[toolA, toolB]}>{children}</Scope>
153
+ `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).
154
+
155
+ ### Hooks — before / after / error / denied (HITL)
156
+
157
+ ```ts
158
+ const tool = webmcp(checkout, {
159
+ description: 'Checkout cart',
160
+ hooks: {
161
+ before: [async ({input}) => {
162
+ const ok = await confirm(`Checkout £${total}?`);
163
+ if (!ok) return { action: 'deny', message: 'User declined' };
164
+ }],
165
+ after: [({output}) => ({ output: redact(output) })],
166
+ error: [({error}) => console.warn(error)],
167
+ denied: [({reason}) => analytics.track('denied', {reason})],
168
+ }
169
+ });
170
+ webmcp.configure({ hooks:{ before:[trackInvocation], after:[trackResult] }});
171
+ <WebMCPProvider hooks={{ before:[addTenant] }}><Scope tools={[tool]}>{children}</Scope></WebMCPProvider>
84
172
  ```
85
173
 
86
- ## Polyfill
174
+ 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](/demo) **Hooks & HITL** card. See [Guide — Hooks](https://emingure.github.io/simple-webmcp/guide/hooks) and [Analytics](https://emingure.github.io/simple-webmcp/guide/analytics) (PostHog, Mixpanel, GA4, Sentry, etc.).
87
175
 
88
- `simple-webmcp/polyfill` is an adapter entry (not hard-coded to one impl). It installs a thin in-memory shim if `document.modelContext` missing, shielding spec churn. For full MCP transport, install `@mcp-b/global` before.
176
+ ## Customize only what you need
89
177
 
90
178
  ```ts
91
- import 'simple-webmcp/polyfill';
92
- import { installPolyfill } from 'simple-webmcp/polyfill'; // programmatic
179
+ const search = webmcp(searchCustomers, {
180
+ description: 'Search customers by name or email',
181
+ fields: {
182
+ query: { description: 'Customer name, email, or ID' },
183
+ limit: { type: 'integer', minimum: 1, maximum: 50 }
184
+ }
185
+ });
93
186
  ```
94
187
 
95
- ## API
188
+ **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.
96
189
 
97
- See `.agents/skills/webmcp-simple/references/api.md` and examples `examples/`.
190
+ ```ts
191
+ // Zod — requires side-effect (keeps core 6.26KB gz lean)
192
+ import { z } from 'zod';
193
+ import 'simple-webmcp/zod';
194
+ webmcp(fn, { schema: z.object({ query: z.string().min(1) }) });
195
+ webmcp(fn, { fields: { query: z.string().describe('Name') } });
196
+ ```
197
+
198
+ ## How inference works
199
+
200
+ **Infer what JavaScript can know at runtime. Get richer TypeScript/JSDoc inference with the optional build plugin.**
201
+
202
+ *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.
203
+
204
+ *Build* — optional `simple-webmcp/unplugin` (Vite/Webpack) reads TypeScript types + JSDoc before erasure. Same `webmcp(fn)` call, richer `inputSchema`, no code change.
205
+
206
+ Progressive:
207
+
208
+ ```
209
+ webmcp(fn)
210
+ ↓ add description
211
+ ↓ add field metadata (fields)
212
+ ↓ provide a schema (Zod)
213
+ ↓ opt into build-time TS/JSDoc inference
214
+ ```
215
+
216
+ Start with one function.
217
+
218
+ ## Comparison
219
+
220
+ | | simple-webmcp | raw WebMCP (`document.modelContext`) | `usewebmcp` | `@mcp-b/react-webmcp` |
221
+ |---|---|---|---|---|
222
+ | Existing function stays callable | ✅ | ❌ | ❌ | ❌ |
223
+ | `webmcp(fn)` — function-first | ✅ | — | — | — |
224
+ | Metadata patching | ✅ | Manual `registerTool` | Manual | Manual |
225
+ | `fields` patch | ✅ | ❌ | ❌ | ❌ |
226
+ | React lifecycle (`AbortSignal`, StrictMode) | ✅ | Manual | ✅ | ✅ |
227
+ | Full MCP ecosystem / `getTools` etc | — (authoring) | ✅ Browser API | — | ✅ |
228
+ | Weekly downloads | new | n/a (browser) | — | ~6k |
229
+ | Focus | Function-first DX | Native API | React hooks | MCP ecosystem |
230
+
231
+ > **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):
232
+ > ```bash
233
+ > npm i @mcp-b/webmcp-polyfill && import '@mcp-b/webmcp-polyfill'
234
+ > ```
235
+ > `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.
236
+
237
+ ## Live Demo
238
+
239
+ **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.
240
+
241
+ **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/.
242
+
243
+ ## API
98
244
 
99
- ### Core
245
+ See `.agents/skills/webmcp-simple/references/api.md`.
100
246
 
101
- - `webmcp(fn, opts)` → `WebMCPTool<F>` (callable + `tool/definition/register/unregister/status`)
102
- - `webmcp.global(fn, opts)` alias
103
- - `isWebMCPSupported()`, `toSnakeCase`, `registry.list()/clear()`
104
- - Errors: `NotSupportedError`, `NotAllowedError` (Permissions Policy), `RegistrationError`, `ValidationError`, `ConfigurationError`
247
+ **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`.
105
248
 
106
- ### React
249
+ **Hooks:** `hooks:{ before:[], after:[], error:[], denied:[] }` on `webmcp(fn,{hooks})`, `webmcp.configure`, and `<WebMCPProvider hooks>`. See [Guide — Hooks](https://emingure.github.io/simple-webmcp/guide/hooks) and [Analytics](https://emingure.github.io/simple-webmcp/guide/analytics) for PostHog/Mixpanel/GA4/Sentry examples. Demo shows live hook log + HITL approval modal.
107
250
 
108
- - `useWebMCP(tool, {enabled})`
109
- - `<Scope tools>`
251
+ **React:** `useWebMCP(fn, opts)` / `useWebMCP(tool)` → `WebMCPTool & status` / `status`, `useTool` alias, `<Scope tools>`, `<WebMCPProvider hooks>`.
110
252
 
111
- ### Errors & Lifecycle
253
+ **Zod:** `import 'simple-webmcp/zod'` then `schema`/`fields` accept Zod/StandardSchema.
112
254
 
113
- `register()` is async `Promise<()=>void>` per current WebMCP types. `status: 'unregistered'|'registering'|'registered'|'unregistering'|'error'`. Use `tool.status` / `tool.isRegistered()`; hook exposes `supported/registered/error`.
255
+ 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.
114
256
 
115
257
  ## Development
116
258
 
117
259
  ```bash
118
- npm run build # tsup ESM+CJS+DTS
119
- npm test # vitest jsdom
260
+ npm run build # tsup ESM+CJS+DTS (core 6.26KB gz, zod 1.40KB separate)
261
+ npm test # vitest jsdom — 47 tests
120
262
  npm run typecheck
263
+ npm run docs:dev # VitePress
121
264
  ```
122
265
 
123
266
  ## Versioning
124
267
 
125
- - **0.1** `webmcp()` + `useWebMCP` + `Scope` + runtime fallback + polyfill adapter + tests (this release)
126
- - **0.2** — `simple-webmcp/unplugin` TS/JSDoc build inference (Vite/Webpack)
127
- - **0.3** — `simple-webmcp/next` `webmcp.server()` — experimental, gated behind working `fixtures/next-app` spike (verifies Server Action reference survival)
128
- - **0.4** — `webmcp.bind()`, DevTools, CLI
268
+ Follows [Semantic Versioning](https://semver.org/). See [`CHANGELOG.md`](./CHANGELOG.md) for the current `0.2.0` notes and [`RELEASING.md`](./RELEASING.md) for the release process. No future roadmap is promised here — track GitHub issues/discussions for what's next.
129
269
 
130
270
  ## Skills (Agent)
131
271
 
132
- This package ships `.agents/skills/webmcp-simple/SKILL.md` for auto-discovery by OpenCode/Claude. No install needed agents discover via `.agents/skills/`.
272
+ 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`.
133
273
 
134
274
  ## License
135
275
 
136
- MIT
276
+ MIT — Copyright © 2026 Muhammed Emin Gure (https://github.com/emingure)
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/polyfill.ts
6
+ var installed = false;
7
+ function installPolyfill(opts) {
8
+ if (installed && !opts?.force) return true;
9
+ if (typeof document === "undefined") return false;
10
+ const docAny = document;
11
+ if (docAny.modelContext && !opts?.force) {
12
+ installed = true;
13
+ return true;
14
+ }
15
+ const tools = /* @__PURE__ */ new Map();
16
+ const polyfill = {
17
+ _isPolyfill: true,
18
+ _tools: tools,
19
+ async registerTool(def, opts2) {
20
+ const name = def?.name;
21
+ if (!name) throw new Error("registerTool: name required");
22
+ if (tools.has(name)) {
23
+ return;
24
+ }
25
+ tools.set(name, { def, signal: opts2?.signal });
26
+ if (opts2?.signal) {
27
+ const onAbort = () => {
28
+ if (tools.get(name)?.signal === opts2.signal) tools.delete(name);
29
+ };
30
+ if (opts2.signal.aborted) {
31
+ tools.delete(name);
32
+ } else {
33
+ opts2.signal.addEventListener("abort", onAbort, { once: true });
34
+ }
35
+ }
36
+ },
37
+ listTools() {
38
+ return Array.from(tools.values()).map((v) => v.def);
39
+ },
40
+ async invokeTool(name, args) {
41
+ const entry = tools.get(name);
42
+ if (!entry) throw new Error(`Tool ${name} not found`);
43
+ return entry.def.execute(args);
44
+ }
45
+ };
46
+ docAny.modelContext = polyfill;
47
+ installed = true;
48
+ return true;
49
+ }
50
+ function isPolyfilled() {
51
+ if (typeof document === "undefined") return false;
52
+ return !!document.modelContext?._isPolyfill;
53
+ }
54
+ if (typeof document !== "undefined") {
55
+ queueMicrotask(() => {
56
+ const docAny = document;
57
+ if (!docAny.modelContext) {
58
+ installPolyfill();
59
+ }
60
+ });
61
+ }
62
+ var polyfill_default = installPolyfill;
63
+
64
+ exports.default = polyfill_default;
65
+ exports.installPolyfill = installPolyfill;
66
+ exports.isPolyfilled = isPolyfilled;
67
+ //# sourceMappingURL=dev-polyfill.cjs.map
68
+ //# sourceMappingURL=dev-polyfill.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/polyfill.ts"],"names":["opts"],"mappings":";;;;;AAwBA,IAAI,SAAA,GAAY,KAAA;AAET,SAAS,gBAAgB,IAAA,EAAiC;AAC/D,EAAA,IAAI,SAAA,IAAa,CAAC,IAAA,EAAM,KAAA,EAAO,OAAO,IAAA;AACtC,EAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,OAAO,KAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,QAAA;AACf,EAAA,IAAI,MAAA,CAAO,YAAA,IAAgB,CAAC,IAAA,EAAM,KAAA,EAAO;AACvC,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,OAAO,IAAA;AAAA,EACT;AAIA,EAAA,MAAM,KAAA,uBAAY,GAAA,EAMhB;AAEF,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,WAAA,EAAa,IAAA;AAAA,IACb,MAAA,EAAQ,KAAA;AAAA,IACR,MAAM,YAAA,CAAa,GAAA,EAAUA,KAAAA,EAAiC;AAC5D,MAAA,MAAM,OAAO,GAAA,EAAK,IAAA;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,6BAA6B,CAAA;AACxD,MAAA,IAAI,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG;AAEnB,QAAA;AAAA,MACF;AACA,MAAA,KAAA,CAAM,IAAI,IAAA,EAAM,EAAE,KAAK,MAAA,EAAQA,KAAAA,EAAM,QAAQ,CAAA;AAC7C,MAAA,IAAIA,OAAM,MAAA,EAAQ;AAChB,QAAA,MAAM,UAAU,MAAM;AACpB,UAAA,IAAI,KAAA,CAAM,IAAI,IAAI,CAAA,EAAG,WAAWA,KAAAA,CAAK,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,IAAI,CAAA;AAAA,QAChE,CAAA;AACA,QAAA,IAAIA,KAAAA,CAAK,OAAO,OAAA,EAAS;AACvB,UAAA,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,QACnB,CAAA,MAAO;AACL,UAAAA,KAAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,QAC/D;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,OAAO,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,EAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG,CAAA;AAAA,IACpD,CAAA;AAAA,IACA,MAAM,UAAA,CAAW,IAAA,EAAc,IAAA,EAAe;AAC5C,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,KAAA,EAAQ,IAAI,CAAA,UAAA,CAAY,CAAA;AACpD,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAAA,IAC/B;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,YAAA,GAAe,QAAA;AACtB,EAAA,SAAA,GAAY,IAAA;AACZ,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,YAAA,GAAwB;AACtC,EAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,OAAO,KAAA;AAC5C,EAAA,OAAO,CAAC,CAAE,QAAA,CAAiB,YAAA,EAAc,WAAA;AAC3C;AAGA,IAAI,OAAO,aAAa,WAAA,EAAa;AAEnC,EAAA,cAAA,CAAe,MAAM;AACnB,IAAA,MAAM,MAAA,GAAS,QAAA;AACf,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,eAAA,EAAgB;AAAA,IAClB;AAAA,EACF,CAAC,CAAA;AACH;AAEA,IAAO,gBAAA,GAAQ","file":"dev-polyfill.cjs","sourcesContent":["/**\n * simple-webmcp/polyfill — dev/testing shim (NOT a WebMCP interoperability polyfill).\n *\n * This is an in-memory registry for tests, Storybook, and local dev without Chrome.\n * It implements `registerTool`/`listTools`/`invokeTool` but not full MCP transport.\n *\n * For real cross-browser WebMCP (Firefox/Safari), use the dedicated\n * `@mcp-b/webmcp-polyfill` (https://www.npmjs.com/package/@mcp-b/webmcp-polyfill)\n * — ~6k weekly downloads, broader MCP ecosystem. Example:\n * `npm i @mcp-b/webmcp-polyfill && import '@mcp-b/webmcp-polyfill'`\n *\n * In production Chrome with native `document.modelContext`, this is a no-op\n * (detected via native presence). Prefer the real polyfill for production\n * cross-browser, and this shim for testing.\n *\n * New aliases (preferred for clarity): `simple-webmcp/dev-polyfill` and\n * `simple-webmcp/testing` — same module, clearer intent. `simple-webmcp/polyfill`\n * is kept for backward compat but will be documented as dev shim.\n */\n\nexport type PolyfillOptions = {\n force?: boolean;\n};\n\nlet installed = false;\n\nexport function installPolyfill(opts?: PolyfillOptions): boolean {\n if (installed && !opts?.force) return true;\n if (typeof document === 'undefined') return false;\n const docAny = document as any;\n if (docAny.modelContext && !opts?.force) {\n installed = true;\n return true;\n }\n\n // Minimal polyfill — tracks tools in memory, invokes via same ABI.\n // Not a full MCP transport; sufficient for registry + tests + devtools.\n const tools = new Map<\n string,\n {\n def: any;\n signal?: AbortSignal;\n }\n >();\n\n const polyfill = {\n _isPolyfill: true,\n _tools: tools,\n async registerTool(def: any, opts?: { signal?: AbortSignal }) {\n const name = def?.name;\n if (!name) throw new Error('registerTool: name required');\n if (tools.has(name)) {\n // Dedup like native — ignore second registration\n return;\n }\n tools.set(name, { def, signal: opts?.signal });\n if (opts?.signal) {\n const onAbort = () => {\n if (tools.get(name)?.signal === opts.signal) tools.delete(name);\n };\n if (opts.signal.aborted) {\n tools.delete(name);\n } else {\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n }\n },\n listTools() {\n return Array.from(tools.values()).map((v) => v.def);\n },\n async invokeTool(name: string, args: unknown) {\n const entry = tools.get(name);\n if (!entry) throw new Error(`Tool ${name} not found`);\n return entry.def.execute(args);\n },\n };\n\n docAny.modelContext = polyfill;\n installed = true;\n return true;\n}\n\nexport function isPolyfilled(): boolean {\n if (typeof document === 'undefined') return false;\n return !!(document as any).modelContext?._isPolyfill;\n}\n\n// Auto-install side-effect import: `import 'simple-webmcp/polyfill'`\nif (typeof document !== 'undefined') {\n // Defer to next tick to allow native detection first\n queueMicrotask(() => {\n const docAny = document as any;\n if (!docAny.modelContext) {\n installPolyfill();\n }\n });\n}\n\nexport default installPolyfill;\n"]}
@@ -0,0 +1 @@
1
+ export { PolyfillOptions, default, default as installPolyfill, isPolyfilled } from './polyfill.cjs';
@@ -0,0 +1 @@
1
+ export { PolyfillOptions, default, default as installPolyfill, isPolyfilled } from './polyfill.js';
@@ -0,0 +1,62 @@
1
+ // src/polyfill.ts
2
+ var installed = false;
3
+ function installPolyfill(opts) {
4
+ if (installed && !opts?.force) return true;
5
+ if (typeof document === "undefined") return false;
6
+ const docAny = document;
7
+ if (docAny.modelContext && !opts?.force) {
8
+ installed = true;
9
+ return true;
10
+ }
11
+ const tools = /* @__PURE__ */ new Map();
12
+ const polyfill = {
13
+ _isPolyfill: true,
14
+ _tools: tools,
15
+ async registerTool(def, opts2) {
16
+ const name = def?.name;
17
+ if (!name) throw new Error("registerTool: name required");
18
+ if (tools.has(name)) {
19
+ return;
20
+ }
21
+ tools.set(name, { def, signal: opts2?.signal });
22
+ if (opts2?.signal) {
23
+ const onAbort = () => {
24
+ if (tools.get(name)?.signal === opts2.signal) tools.delete(name);
25
+ };
26
+ if (opts2.signal.aborted) {
27
+ tools.delete(name);
28
+ } else {
29
+ opts2.signal.addEventListener("abort", onAbort, { once: true });
30
+ }
31
+ }
32
+ },
33
+ listTools() {
34
+ return Array.from(tools.values()).map((v) => v.def);
35
+ },
36
+ async invokeTool(name, args) {
37
+ const entry = tools.get(name);
38
+ if (!entry) throw new Error(`Tool ${name} not found`);
39
+ return entry.def.execute(args);
40
+ }
41
+ };
42
+ docAny.modelContext = polyfill;
43
+ installed = true;
44
+ return true;
45
+ }
46
+ function isPolyfilled() {
47
+ if (typeof document === "undefined") return false;
48
+ return !!document.modelContext?._isPolyfill;
49
+ }
50
+ if (typeof document !== "undefined") {
51
+ queueMicrotask(() => {
52
+ const docAny = document;
53
+ if (!docAny.modelContext) {
54
+ installPolyfill();
55
+ }
56
+ });
57
+ }
58
+ var polyfill_default = installPolyfill;
59
+
60
+ export { polyfill_default as default, installPolyfill, isPolyfilled };
61
+ //# sourceMappingURL=dev-polyfill.js.map
62
+ //# sourceMappingURL=dev-polyfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/polyfill.ts"],"names":["opts"],"mappings":";AAwBA,IAAI,SAAA,GAAY,KAAA;AAET,SAAS,gBAAgB,IAAA,EAAiC;AAC/D,EAAA,IAAI,SAAA,IAAa,CAAC,IAAA,EAAM,KAAA,EAAO,OAAO,IAAA;AACtC,EAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,OAAO,KAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,QAAA;AACf,EAAA,IAAI,MAAA,CAAO,YAAA,IAAgB,CAAC,IAAA,EAAM,KAAA,EAAO;AACvC,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,OAAO,IAAA;AAAA,EACT;AAIA,EAAA,MAAM,KAAA,uBAAY,GAAA,EAMhB;AAEF,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,WAAA,EAAa,IAAA;AAAA,IACb,MAAA,EAAQ,KAAA;AAAA,IACR,MAAM,YAAA,CAAa,GAAA,EAAUA,KAAAA,EAAiC;AAC5D,MAAA,MAAM,OAAO,GAAA,EAAK,IAAA;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,6BAA6B,CAAA;AACxD,MAAA,IAAI,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG;AAEnB,QAAA;AAAA,MACF;AACA,MAAA,KAAA,CAAM,IAAI,IAAA,EAAM,EAAE,KAAK,MAAA,EAAQA,KAAAA,EAAM,QAAQ,CAAA;AAC7C,MAAA,IAAIA,OAAM,MAAA,EAAQ;AAChB,QAAA,MAAM,UAAU,MAAM;AACpB,UAAA,IAAI,KAAA,CAAM,IAAI,IAAI,CAAA,EAAG,WAAWA,KAAAA,CAAK,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,IAAI,CAAA;AAAA,QAChE,CAAA;AACA,QAAA,IAAIA,KAAAA,CAAK,OAAO,OAAA,EAAS;AACvB,UAAA,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,QACnB,CAAA,MAAO;AACL,UAAAA,KAAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,QAC/D;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,OAAO,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,EAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG,CAAA;AAAA,IACpD,CAAA;AAAA,IACA,MAAM,UAAA,CAAW,IAAA,EAAc,IAAA,EAAe;AAC5C,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,KAAA,EAAQ,IAAI,CAAA,UAAA,CAAY,CAAA;AACpD,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAAA,IAC/B;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,YAAA,GAAe,QAAA;AACtB,EAAA,SAAA,GAAY,IAAA;AACZ,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,YAAA,GAAwB;AACtC,EAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,OAAO,KAAA;AAC5C,EAAA,OAAO,CAAC,CAAE,QAAA,CAAiB,YAAA,EAAc,WAAA;AAC3C;AAGA,IAAI,OAAO,aAAa,WAAA,EAAa;AAEnC,EAAA,cAAA,CAAe,MAAM;AACnB,IAAA,MAAM,MAAA,GAAS,QAAA;AACf,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,eAAA,EAAgB;AAAA,IAClB;AAAA,EACF,CAAC,CAAA;AACH;AAEA,IAAO,gBAAA,GAAQ","file":"dev-polyfill.js","sourcesContent":["/**\n * simple-webmcp/polyfill — dev/testing shim (NOT a WebMCP interoperability polyfill).\n *\n * This is an in-memory registry for tests, Storybook, and local dev without Chrome.\n * It implements `registerTool`/`listTools`/`invokeTool` but not full MCP transport.\n *\n * For real cross-browser WebMCP (Firefox/Safari), use the dedicated\n * `@mcp-b/webmcp-polyfill` (https://www.npmjs.com/package/@mcp-b/webmcp-polyfill)\n * — ~6k weekly downloads, broader MCP ecosystem. Example:\n * `npm i @mcp-b/webmcp-polyfill && import '@mcp-b/webmcp-polyfill'`\n *\n * In production Chrome with native `document.modelContext`, this is a no-op\n * (detected via native presence). Prefer the real polyfill for production\n * cross-browser, and this shim for testing.\n *\n * New aliases (preferred for clarity): `simple-webmcp/dev-polyfill` and\n * `simple-webmcp/testing` — same module, clearer intent. `simple-webmcp/polyfill`\n * is kept for backward compat but will be documented as dev shim.\n */\n\nexport type PolyfillOptions = {\n force?: boolean;\n};\n\nlet installed = false;\n\nexport function installPolyfill(opts?: PolyfillOptions): boolean {\n if (installed && !opts?.force) return true;\n if (typeof document === 'undefined') return false;\n const docAny = document as any;\n if (docAny.modelContext && !opts?.force) {\n installed = true;\n return true;\n }\n\n // Minimal polyfill — tracks tools in memory, invokes via same ABI.\n // Not a full MCP transport; sufficient for registry + tests + devtools.\n const tools = new Map<\n string,\n {\n def: any;\n signal?: AbortSignal;\n }\n >();\n\n const polyfill = {\n _isPolyfill: true,\n _tools: tools,\n async registerTool(def: any, opts?: { signal?: AbortSignal }) {\n const name = def?.name;\n if (!name) throw new Error('registerTool: name required');\n if (tools.has(name)) {\n // Dedup like native — ignore second registration\n return;\n }\n tools.set(name, { def, signal: opts?.signal });\n if (opts?.signal) {\n const onAbort = () => {\n if (tools.get(name)?.signal === opts.signal) tools.delete(name);\n };\n if (opts.signal.aborted) {\n tools.delete(name);\n } else {\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n }\n },\n listTools() {\n return Array.from(tools.values()).map((v) => v.def);\n },\n async invokeTool(name: string, args: unknown) {\n const entry = tools.get(name);\n if (!entry) throw new Error(`Tool ${name} not found`);\n return entry.def.execute(args);\n },\n };\n\n docAny.modelContext = polyfill;\n installed = true;\n return true;\n}\n\nexport function isPolyfilled(): boolean {\n if (typeof document === 'undefined') return false;\n return !!(document as any).modelContext?._isPolyfill;\n}\n\n// Auto-install side-effect import: `import 'simple-webmcp/polyfill'`\nif (typeof document !== 'undefined') {\n // Defer to next tick to allow native detection first\n queueMicrotask(() => {\n const docAny = document as any;\n if (!docAny.modelContext) {\n installPolyfill();\n }\n });\n}\n\nexport default installPolyfill;\n"]}