simple-webmcp 0.1.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.
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: simple-webmcp
3
+ description: Expose existing JS/TS functions as WebMCP agent tools with minimal boilerplate — function-first, works with vanilla JS, TS, React, Vite, Next.js. Use when the user wants to make functions callable by LLM agents via WebMCP (document.modelContext).
4
+ ---
5
+
6
+ # simple-webmcp Skill
7
+
8
+ ## When to use
9
+
10
+ Trigger this skill when the user wants to:
11
+ - Expose a JS/TS function to LLM / agentic tool calling via WebMCP
12
+ - Make `addToCart`, `searchCustomers`, `updateCustomer`, etc. available as `document.modelContext` tools
13
+ - Support plain JS, TypeScript, React (scoped lifecycle), or Next.js (server actions) without heavy boilerplate
14
+
15
+ Do **not** use for consuming external APIs — this skill is for *exposing* your own functions as tools.
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Vanilla / Vite — 1 line
20
+
21
+ ```ts
22
+ import { webmcp } from 'simple-webmcp';
23
+
24
+ async function searchCustomers({ query, limit = 20 }: { query: string; limit?: number }) {
25
+ return customers.filter(c => c.name.includes(query)).slice(0, limit);
26
+ }
27
+
28
+ // still callable as before, but also a WebMCP tool descriptor
29
+ export const searchTool = webmcp(searchCustomers, {
30
+ description: 'Search customers in current account',
31
+ fields: {
32
+ query: { description: 'Name, email, or ID' },
33
+ limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Max results' },
34
+ },
35
+ });
36
+
37
+ // vanilla global register (outside React)
38
+ await searchTool.register(); // => document.modelContext.registerTool(...)
39
+ searchTool.unregister();
40
+
41
+ // still callable like original
42
+ await searchTool({ query: 'alice' });
43
+ ```
44
+
45
+ ### 2. React — component lifecycle
46
+
47
+ ```tsx
48
+ 'use client';
49
+ import { webmcp } from 'simple-webmcp';
50
+ import { useWebMCP, Scope } from 'simple-webmcp/react';
51
+
52
+ const addToCartTool = webmcp(addToCart, { description: 'Add product to cart' });
53
+
54
+ export function ProductPage() {
55
+ // exposed only while component mounted — maps to AbortSignal
56
+ useWebMCP(addToCartTool);
57
+ return <div>...</div>;
58
+ }
59
+
60
+ // Or route/component subtree (also works for Next.js layouts)
61
+ export function CustomersLayout({ children }: { children: React.ReactNode }) {
62
+ return <Scope tools={[searchTool]}>{children}</Scope>;
63
+ }
64
+ ```
65
+
66
+ ### 3. Zod / StandardSchema — per-field or whole
67
+
68
+ ```ts
69
+ import { z } from 'zod';
70
+ import { webmcp } from 'simple-webmcp';
71
+
72
+ // whole schema
73
+ webmcp(fn, { description: '…', schema: z.object({ query: z.string().min(1), limit: z.number().max(50).optional() }) });
74
+
75
+ // per-field mix — Lean + Zod
76
+ webmcp(searchCustomers, {
77
+ description: 'Search',
78
+ fields: {
79
+ query: z.string().describe('Name or email'), // StandardSchema
80
+ limit: { type: 'integer', minimum: 1, maximum: 50 }, // FieldDef (Partial<JsonSchema>)
81
+ },
82
+ });
83
+ ```
84
+
85
+ In all cases `schema` (whole) establishes contract; `fields` patches it (adds descriptions, min/max) but does not silently change core type.
86
+
87
+ ### 4. Polyfill (non-Chrome)
88
+
89
+ ```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();
93
+ ```
94
+
95
+ ### 5. Global vs Scoped
96
+
97
+ ```ts
98
+ import { webmcp } from 'simple-webmcp';
99
+ // scoped (default) — inert until useWebMCP or .register()
100
+ webmcp(fn, { description: '…' });
101
+ // global — registers on import (client only)
102
+ webmcp(fn, { description: '…', global: true });
103
+ webmcp.global(fn, { description: '…' }); // alias
104
+ ```
105
+
106
+ ## Progression Ladder (teach stepwise)
107
+
108
+ 1. **Beginner:** `webmcp(fn)` done.
109
+ 2. **Better desc:** `webmcp(fn, { description: '…' })`
110
+ 3. **Field docs:** `webmcp(fn, { fields: { query: { description: '…' } } })`
111
+ 4. **Full control:** `webmcp(fn, { schema: z.object({…}) })`
112
+ 5. **Lifecycle:** `useWebMCP(tool)` / `<Scope tools>`
113
+
114
+ ## Rules
115
+
116
+ - Always provide `description` (or JSDoc); warning if missing.
117
+ - Prefer single object param `fn({query, limit})` for tools — best inference. Multi-arg legacy deferred to `webmcp.bind` (0.4).
118
+ - Use `readOnlyHint`/`destructiveHint` in `annotations` for agent hints; annotations are extensible `Record<string,unknown>`.
119
+ - Registration is async: `await tool.register()` → `() => void` unregister. React hook handles AbortSignal.
120
+ - `strict:true` makes ambiguous runtime inference a build error instead of warn.
121
+
122
+ ## Inference
123
+
124
+ - **Runtime** (0.1): param names + defaults → low-confidence `{type:'object'}` placeholder.
125
+ - **Build** (0.2 `simple-webmcp/unplugin` for Vite/Webpack): TS types + JSDoc before erasure. Same API.
126
+ - Never pretend `function search(query)` alone is fully known — warn if no type/schema/fields.
127
+
128
+ ## Next.js Server Actions — experimental (0.3, requires spike)
129
+
130
+ Do not assume transparent bridge works yet. The spike must verify `webmcp.server(action)` reference survives server/client boundary. Until then, keep server tools as described in docs but behind experimental flag.
131
+
132
+ ## References
133
+
134
+ - `references/api.md` — full API surface
135
+ - `references/recipes.md` — vanilla, React, Vite, Next patterns
136
+ - `references/build.md` — unplugin inference
@@ -0,0 +1,77 @@
1
+ # API Reference — simple-webmcp 0.1
2
+
3
+ ## Core
4
+
5
+ ```ts
6
+ import { webmcp } from 'simple-webmcp';
7
+
8
+ function webmcp<F extends (...args:any)=>any>(fn: F, options?: WebMCPOptions<F>): WebMCPTool<F>
9
+ namespace webmcp { function global<F>(fn:F, opts?: Omit<WebMCPOptions<F>,'global'>): WebMCPTool<F> }
10
+
11
+ type WebMCPTool<F> = F & {
12
+ __webmcpBrand: true;
13
+ tool: ToolContract; definition: ToolContract;
14
+ register(opts?: {signal?:AbortSignal}): Promise<()=>void>;
15
+ unregister(): void;
16
+ status: 'unregistered'|'registering'|'registered'|'unregistering'|'error';
17
+ registration: Promise<void>|null;
18
+ isRegistered(): boolean;
19
+ }
20
+
21
+ type WebMCPOptions<F> = {
22
+ name?: string; description?: string;
23
+ schema?: JsonSchema | StandardSchemaV1; // whole contract (Zod, Valibot etc or JSON)
24
+ outputSchema?: JsonSchema | StandardSchemaV1;
25
+ fields?: Record<string, FieldDef|StandardSchemaV1>; // FieldDef = Partial<JsonSchema>
26
+ annotations?: Record<string,unknown> & {readOnlyHint?, destructiveHint?, openWorldHint?, title?}
27
+ scope?: 'global'|'scoped'|'manual'; global?: boolean; enabled?: boolean; strict?: boolean;
28
+ }
29
+
30
+ type ToolContract = { name:string; description:string; inputSchema:JsonSchema; outputSchema?:JsonSchema; annotations?:Record<string,unknown> }
31
+ ```
32
+
33
+ Hierarchy: `schema` (whole) → inferred (runtime 0.1 / build 0.2) → `fields` patch → metadata.
34
+
35
+ `fields` example:
36
+
37
+ ```ts
38
+ fields: {
39
+ query: { description:'Name or email' },
40
+ limit: { type:'integer', minimum:1, maximum:50 }
41
+ // or per-field Zod: query: z.string().describe('...')
42
+ }
43
+ ```
44
+
45
+ Errors: `SimpleWebMCPError` → `NotSupportedError`, `NotAllowedError` (Permissions Policy), `RegistrationError`, `ValidationError`, `ConfigurationError` — each `code` + `cause`.
46
+
47
+ Utils:
48
+
49
+ ```ts
50
+ import { isWebMCPSupported, getModelContext, toSnakeCase, registry } from 'simple-webmcp';
51
+ ```
52
+
53
+ ## React
54
+
55
+ ```ts
56
+ import { useWebMCP, Scope } from 'simple-webmcp/react'; // 'use client'
57
+
58
+ function useWebMCP<F>(tool: WebMCPTool<F>, opts?:{enabled?:boolean}): {supported:boolean; registered:boolean; error:Error|null; isPolyfilled:boolean; status:string}
59
+ function Scope({tools, enabled, children}: {tools:WebMCPTool<any>[]; enabled?:boolean; children?:React.ReactNode}): JSX.Element
60
+ ```
61
+
62
+ ## Polyfill
63
+
64
+ ```ts
65
+ import 'simple-webmcp/polyfill'; // auto
66
+ import { installPolyfill, isPolyfilled } from 'simple-webmcp/polyfill';
67
+ ```
68
+
69
+ ## Next (experimental, 0.3)
70
+
71
+ ```ts
72
+ // after spike passes
73
+ import { webmcp } from 'simple-webmcp/next';
74
+ const tool = webmcp.server(action, opts); // action is 'use server' fn
75
+ ```
76
+
77
+ Not in 0.1 — do not depend yet.
@@ -0,0 +1,113 @@
1
+ # Recipes
2
+
3
+ ## Vanilla JS (Vite, plain TS)
4
+
5
+ ```ts
6
+ import { webmcp } from 'simple-webmcp';
7
+ import 'simple-webmcp/polyfill'; // dev only
8
+
9
+ export async function addToCart({ productId, quantity }: { productId: string; quantity: number }) {
10
+ cart.push({ productId, quantity });
11
+ return { ok: true };
12
+ }
13
+
14
+ export const addToCartTool = webmcp(addToCart, {
15
+ description: 'Add product to shopping cart',
16
+ fields: {
17
+ productId: { description: 'Product ID' },
18
+ quantity: { type: 'integer', minimum: 1 },
19
+ },
20
+ annotations: { readOnlyHint: false },
21
+ });
22
+
23
+ // outside React — register programmatically
24
+ await addToCartTool.register();
25
+ // still call directly
26
+ await addToCartTool({ productId: 'p_123', quantity: 2 });
27
+ ```
28
+
29
+ ## TypeScript + Zod (per-field)
30
+
31
+ ```ts
32
+ import { z } from 'zod';
33
+ import { webmcp } from 'simple-webmcp';
34
+
35
+ async function searchCustomers({ query, limit }: { query: string; limit?: number }) {
36
+ // ...
37
+ }
38
+
39
+ export const searchTool = webmcp(searchCustomers, {
40
+ description: 'Search customers',
41
+ fields: {
42
+ query: z.string().min(1).describe('Name, email, or ID'),
43
+ limit: z.number().int().min(1).max(50).optional().describe('Max results'),
44
+ },
45
+ });
46
+ ```
47
+
48
+ ## Zod whole schema
49
+
50
+ ```ts
51
+ webmcp(fn, { description: '…', schema: z.object({ query: z.string(), limit: z.number().optional() }) });
52
+ ```
53
+
54
+ ## React — page level
55
+
56
+ ```tsx
57
+ 'use client';
58
+ import { webmcp } from 'simple-webmcp';
59
+ import { useWebMCP } from 'simple-webmcp/react';
60
+
61
+ const searchTool = webmcp(searchCustomers, { description: 'Search' });
62
+
63
+ export function SearchPage() {
64
+ useWebMCP(searchTool);
65
+ return <SearchUI />;
66
+ }
67
+ ```
68
+
69
+ ## React — layout/route scope
70
+
71
+ ```tsx
72
+ 'use client';
73
+ import { Scope } from 'simple-webmcp/react';
74
+ import { searchTool, updateTool } from '@/lib/tools';
75
+
76
+ export function CustomersLayout({ children }: { children: React.ReactNode }) {
77
+ // Tools exposed while layout mounted → natural route scope in Next.js layouts
78
+ return <Scope tools={[searchTool, updateTool]}>{children}</Scope>;
79
+ }
80
+ ```
81
+
82
+ ## Global (entire app)
83
+
84
+ ```ts
85
+ import { webmcp } from 'simple-webmcp';
86
+ webmcp.global(searchCustomers, { description: 'Global search' });
87
+ // or
88
+ webmcp(searchCustomers, { description: 'Search', global: true });
89
+ ```
90
+
91
+ ## Strict mode (warn vs error)
92
+
93
+ ```ts
94
+ // default: dev warn if no type/schema/fields
95
+ webmcp(fnWithoutTypes, { description: '…' }); // warn
96
+
97
+ // strict: throw
98
+ webmcp(fnWithoutTypes, { description: '…', strict: true }); // throws ConfigurationError
99
+ ```
100
+
101
+ ## Inference tips
102
+
103
+ - Best: `async function fn({query, limit}:{query:string, limit?:number})` — runtime sees `query` required, `limit` optional default
104
+ - Better with build (0.2 `simple-webmcp/unplugin`): TS type + JSDoc `@param query ...` → full schema, no manual `fields`
105
+ - Fallback for plain JS: add `fields` or `schema` explicitly
106
+
107
+ ## Global registry for tests
108
+
109
+ ```ts
110
+ import { registry } from 'simple-webmcp';
111
+ registry.list(); // [{name, status}]
112
+ registry.clear();
113
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammed Emin Gure (https://github.com/emingure)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # simple-webmcp
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.
4
+
5
+ ```ts
6
+ import { webmcp } from 'simple-webmcp';
7
+
8
+ async function searchCustomers({ query, limit = 20 }: { query: string; limit?: number }) {
9
+ return customers.filter(c => c.name.includes(query)).slice(0, limit);
10
+ }
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' } },
16
+ });
17
+
18
+ await searchTool({ query: 'alice' }); // still just a function
19
+ await searchTool.register(); // => document.modelContext.registerTool(...)
20
+ // or React: useWebMCP(searchTool) → mounted = exposed
21
+ ```
22
+
23
+ Works with plain JS, TS, React, Vite… (Next.js Server Actions experimental 0.3). Open source, lean (`sideEffects:false`, core no React dep).
24
+
25
+ **Docs:** https://emingure.github.io/simple-webmcp/ (via GitHub Pages)
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ 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
33
+ ```
34
+
35
+ ## Quick Start
36
+
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.
49
+
50
+ ```ts
51
+ // prefer single object param fn({query, limit}) for best inference
52
+ webmcp(fn, { description:'…', fields:{ query:{description:'…'}, limit:{type:'integer', maximum:50} } });
53
+
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) }) });
58
+
59
+ // per-field mix — same import enables it
60
+ webmcp(fn, { fields:{ query: z.string().describe('…'), limit:{type:'integer'} } });
61
+
62
+ // JSON Schema directly
63
+ webmcp(fn, { schema:{type:'object', properties:{query:{type:'string'}}, required:['query']} });
64
+ ```
65
+
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
69
+
70
+ ```tsx
71
+ 'use client';
72
+ import { webmcp } from 'simple-webmcp';
73
+ import { useWebMCP, Scope } from 'simple-webmcp/react';
74
+
75
+ const tool = webmcp(fn, { description:'…' });
76
+
77
+ export function Page() {
78
+ const { supported, registered, error } = useWebMCP(tool);
79
+ return null; // exposed while mounted — maps to AbortSignal
80
+ }
81
+
82
+ // layout-level route scope (Next.js app/layout.tsx naturally gives route scope)
83
+ <Scope tools={[toolA, toolB]}>{children}</Scope>
84
+ ```
85
+
86
+ ## Polyfill
87
+
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.
89
+
90
+ ```ts
91
+ import 'simple-webmcp/polyfill';
92
+ import { installPolyfill } from 'simple-webmcp/polyfill'; // programmatic
93
+ ```
94
+
95
+ ## API
96
+
97
+ See `.agents/skills/webmcp-simple/references/api.md` and examples `examples/`.
98
+
99
+ ### Core
100
+
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`
105
+
106
+ ### React
107
+
108
+ - `useWebMCP(tool, {enabled})`
109
+ - `<Scope tools>`
110
+
111
+ ### Errors & Lifecycle
112
+
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`.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ npm run build # tsup ESM+CJS+DTS
119
+ npm test # vitest jsdom
120
+ npm run typecheck
121
+ ```
122
+
123
+ ## Versioning
124
+
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
129
+
130
+ ## Skills (Agent)
131
+
132
+ This package ships `.agents/skills/webmcp-simple/SKILL.md` for auto-discovery by OpenCode/Claude. No install needed — agents discover via `.agents/skills/`.
133
+
134
+ ## License
135
+
136
+ MIT