simple-webmcp 0.1.0 → 0.2.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.
- package/.agents/skills/webmcp-simple/SKILL.md +27 -12
- package/README.md +194 -72
- package/dist/dev-polyfill.cjs +68 -0
- package/dist/dev-polyfill.cjs.map +1 -0
- package/dist/dev-polyfill.d.cts +1 -0
- package/dist/dev-polyfill.d.ts +1 -0
- package/dist/dev-polyfill.js +62 -0
- package/dist/dev-polyfill.js.map +1 -0
- package/dist/devtools.cjs +452 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +14 -0
- package/dist/devtools.d.ts +14 -0
- package/dist/devtools.js +449 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.cjs +37 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -21
- package/dist/index.d.ts +4 -21
- package/dist/index.js +37 -37
- package/dist/index.js.map +1 -1
- package/dist/inspect.cjs +342 -0
- package/dist/inspect.cjs.map +1 -0
- package/dist/inspect.d.cts +37 -0
- package/dist/inspect.d.ts +37 -0
- package/dist/inspect.js +334 -0
- package/dist/inspect.js.map +1 -0
- package/dist/polyfill.cjs.map +1 -1
- package/dist/polyfill.d.cts +15 -7
- package/dist/polyfill.d.ts +15 -7
- package/dist/polyfill.js.map +1 -1
- package/dist/react.cjs +850 -12
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +7 -10
- package/dist/react.d.ts +7 -10
- package/dist/react.js +851 -14
- package/dist/react.js.map +1 -1
- package/dist/registry-BcPw1qCf.d.cts +38 -0
- package/dist/registry-C6Pz_Rja.d.ts +38 -0
- package/dist/testing.cjs +68 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +62 -0
- package/dist/testing.js.map +1 -0
- package/dist/{types-D-cwSfEU.d.cts → types-DH78HDAE.d.cts} +2 -2
- package/dist/{types-D-cwSfEU.d.ts → types-DH78HDAE.d.ts} +2 -2
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.ts +1 -1
- 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
|
-
|
|
53
|
-
|
|
52
|
+
// 1-line: wrap + register while mounted — recommended
|
|
54
53
|
export function ProductPage() {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# Make your existing functions agent-ready
|
|
2
2
|
|
|
3
|
-
|
|
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,250 @@ 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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. More adapters coming.** Works with vanilla JS, TypeScript, Vite — Next.js `server()` is experimental until the `fixtures/next-app` spike proves the Server Action bridge.
|
|
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
|
-
|
|
19
|
-
await
|
|
20
|
-
//
|
|
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
|
-
|
|
107
|
+
**Same function. Same application. One line.** No duplicated business logic.
|
|
24
108
|
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
webmcp(fn, { description:'…', fields:{ query:{description:'…'}, limit:{type:'integer', maximum:50} } });
|
|
123
|
+
import { webmcp } from 'simple-webmcp';
|
|
53
124
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
//
|
|
63
|
-
webmcp(
|
|
134
|
+
// global (registers on import, client only) — prefer scoped for least privilege
|
|
135
|
+
webmcp.global(addToCart, { description: '...' });
|
|
64
136
|
```
|
|
65
137
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## React
|
|
138
|
+
### React — 1-line (recommended)
|
|
69
139
|
|
|
70
140
|
```tsx
|
|
71
141
|
'use client';
|
|
72
|
-
import {
|
|
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
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
}
|
|
81
|
-
|
|
82
|
-
// layout-level route scope (Next.js app/layout.tsx naturally gives route scope)
|
|
83
|
-
<Scope tools={[toolA, toolB]}>{children}</Scope>
|
|
84
149
|
```
|
|
85
150
|
|
|
86
|
-
|
|
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`.
|
|
152
|
+
|
|
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).
|
|
87
154
|
|
|
88
|
-
|
|
155
|
+
## Customize only what you need
|
|
89
156
|
|
|
90
157
|
```ts
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
const search = webmcp(searchCustomers, {
|
|
159
|
+
description: 'Search customers by name or email',
|
|
160
|
+
fields: {
|
|
161
|
+
query: { description: 'Customer name, email, or ID' },
|
|
162
|
+
limit: { type: 'integer', minimum: 1, maximum: 50 }
|
|
163
|
+
}
|
|
164
|
+
});
|
|
93
165
|
```
|
|
94
166
|
|
|
95
|
-
|
|
167
|
+
**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
168
|
|
|
97
|
-
|
|
169
|
+
```ts
|
|
170
|
+
// Zod — requires side-effect (keeps core 6.26KB gz lean)
|
|
171
|
+
import { z } from 'zod';
|
|
172
|
+
import 'simple-webmcp/zod';
|
|
173
|
+
webmcp(fn, { schema: z.object({ query: z.string().min(1) }) });
|
|
174
|
+
webmcp(fn, { fields: { query: z.string().describe('Name') } });
|
|
175
|
+
```
|
|
98
176
|
|
|
99
|
-
|
|
177
|
+
## How inference works
|
|
100
178
|
|
|
101
|
-
|
|
102
|
-
- `webmcp.global(fn, opts)` alias
|
|
103
|
-
- `isWebMCPSupported()`, `toSnakeCase`, `registry.list()/clear()`
|
|
104
|
-
- Errors: `NotSupportedError`, `NotAllowedError` (Permissions Policy), `RegistrationError`, `ValidationError`, `ConfigurationError`
|
|
179
|
+
**Infer what JavaScript can know at runtime. Get richer TypeScript/JSDoc inference with the optional build plugin.**
|
|
105
180
|
|
|
106
|
-
|
|
181
|
+
*Runtime (0.1)* — 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.
|
|
107
182
|
|
|
108
|
-
- `
|
|
109
|
-
- `<Scope tools>`
|
|
183
|
+
*Build (0.2 `simple-webmcp/unplugin`)* — Vite/Webpack plugin reads TypeScript types + JSDoc before erasure (same approach `webmcp-nexus` validates with `ts-morph`). Same `webmcp(fn)` call, richer `inputSchema`, no code change.
|
|
110
184
|
|
|
111
|
-
|
|
185
|
+
Progressive:
|
|
112
186
|
|
|
113
|
-
|
|
187
|
+
```
|
|
188
|
+
webmcp(fn)
|
|
189
|
+
↓ add description
|
|
190
|
+
↓ add field metadata (fields)
|
|
191
|
+
↓ provide a schema (Zod)
|
|
192
|
+
↓ opt into build-time TS/JSDoc inference
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Start with one function.
|
|
196
|
+
|
|
197
|
+
## Comparison
|
|
198
|
+
|
|
199
|
+
| | simple-webmcp | raw WebMCP (`document.modelContext`) | `usewebmcp` | `@mcp-b/react-webmcp` |
|
|
200
|
+
|---|---|---|---|---|
|
|
201
|
+
| Existing function stays callable | ✅ | ❌ | ❌ | ❌ |
|
|
202
|
+
| `webmcp(fn)` — function-first | ✅ | — | — | — |
|
|
203
|
+
| Metadata patching | ✅ | Manual `registerTool` | Manual | Manual |
|
|
204
|
+
| `fields` patch | ✅ | ❌ | ❌ | ❌ |
|
|
205
|
+
| React lifecycle (`AbortSignal`, StrictMode) | ✅ | Manual | ✅ | ✅ |
|
|
206
|
+
| Full MCP ecosystem / `getTools` etc | — (authoring) | ✅ Browser API | — | ✅ |
|
|
207
|
+
| Weekly downloads | new | n/a (browser) | — | ~6k |
|
|
208
|
+
| Focus | Function-first DX | Native API | React hooks | MCP ecosystem |
|
|
209
|
+
|
|
210
|
+
> **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):
|
|
211
|
+
> ```bash
|
|
212
|
+
> npm i @mcp-b/webmcp-polyfill && import '@mcp-b/webmcp-polyfill'
|
|
213
|
+
> ```
|
|
214
|
+
> `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.
|
|
215
|
+
|
|
216
|
+
## Live Demo
|
|
217
|
+
|
|
218
|
+
**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.
|
|
219
|
+
|
|
220
|
+
**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/.
|
|
221
|
+
|
|
222
|
+
## API
|
|
223
|
+
|
|
224
|
+
See `.agents/skills/webmcp-simple/references/api.md`.
|
|
225
|
+
|
|
226
|
+
**Core:** `webmcp(fn, opts)` → `WebMCPTool` (callable + `tool`/`definition`/`register`/`status`), `webmcp.global`, `isWebMCPSupported()`, `registry.list()`. Errors: `NotSupportedError` (`unsupported` status, mutually exclusive with `registered`), `NotAllowedError`, `RegistrationError`, `ConfigurationError`.
|
|
227
|
+
|
|
228
|
+
**React:** `useWebMCP(fn, opts)` / `useWebMCP(tool)` → `WebMCPTool & status` / `status`, `useTool` alias, `<Scope tools>`.
|
|
229
|
+
|
|
230
|
+
**Zod:** `import 'simple-webmcp/zod'` then `schema`/`fields` accept Zod/StandardSchema.
|
|
231
|
+
|
|
232
|
+
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
233
|
|
|
115
234
|
## Development
|
|
116
235
|
|
|
117
236
|
```bash
|
|
118
|
-
npm run build # tsup ESM+CJS+DTS
|
|
119
|
-
npm test # vitest jsdom
|
|
237
|
+
npm run build # tsup ESM+CJS+DTS (core 6.26KB gz, zod 1.40KB separate)
|
|
238
|
+
npm test # vitest jsdom — 43 tests
|
|
120
239
|
npm run typecheck
|
|
240
|
+
npm run docs:dev # VitePress
|
|
121
241
|
```
|
|
122
242
|
|
|
123
243
|
## Versioning
|
|
124
244
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
245
|
+
* **0.1** — `webmcp()` + `useWebMCP`/`useTool` + `Scope` + runtime best-effort fallback + dev shim + 43 tests (this release)
|
|
246
|
+
* **0.2** — `simple-webmcp/unplugin` TS/JSDoc inference (Vite/Webpack)
|
|
247
|
+
* **0.3** — `simple-webmcp/next` `webmcp.server()` — experimental, gated behind `fixtures/next-app` spike. Next 16 defaults to Turbopack, so no Webpack plugin in 0.1.
|
|
248
|
+
* **0.4** — `webmcp.bind()`, DevTools, CLI — deferred
|
|
249
|
+
|
|
250
|
+
`outputSchema` is kept internally but not marketed in 0.1 — current browser API does not enforce it like `usewebmcp` notes.
|
|
129
251
|
|
|
130
252
|
## Skills (Agent)
|
|
131
253
|
|
|
132
|
-
This package ships `.agents/skills/webmcp-simple/SKILL.md` for auto-discovery by OpenCode/Claude
|
|
254
|
+
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
255
|
|
|
134
256
|
## License
|
|
135
257
|
|
|
136
|
-
MIT
|
|
258
|
+
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"]}
|