@nevescloud/pip 3.3.0 → 3.4.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 (2) hide show
  1. package/package.json +4 -1
  2. package/webmcp.esm.js +211 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevescloud/pip",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Floating assistant bubble + panel + chat runtime. ESM, no build.",
5
5
  "type": "module",
6
6
  "main": "pip-core.esm.js",
@@ -8,6 +8,8 @@
8
8
  ".": "./pip-core.esm.js",
9
9
  "./pip-core.esm.js": "./pip-core.esm.js",
10
10
  "./runtime.esm.js": "./runtime.esm.js",
11
+ "./webmcp": "./webmcp.esm.js",
12
+ "./webmcp.esm.js": "./webmcp.esm.js",
11
13
  "./bundle": "./bundle/anthropic.esm.js",
12
14
  "./bundle/anthropic": "./bundle/anthropic.esm.js",
13
15
  "./bundle/anthropic.esm.js": "./bundle/anthropic.esm.js",
@@ -22,6 +24,7 @@
22
24
  "files": [
23
25
  "pip-core.esm.js",
24
26
  "runtime.esm.js",
27
+ "webmcp.esm.js",
25
28
  "bundle/",
26
29
  "providers/",
27
30
  "README.md",
package/webmcp.esm.js ADDED
@@ -0,0 +1,211 @@
1
+ // WebMCP adapter for pip-runtime.
2
+ //
3
+ // Discovers tools registered via the standard `navigator.modelContext`
4
+ // surface (W3C Community Group Report, 23 April 2026) and wires each one
5
+ // into pip's runtime tool registry. Tools registered on the host page
6
+ // become tools the runtime's provider can call.
7
+ //
8
+ // Usage:
9
+ // import { createRuntime } from '@nevescloud/pip/runtime.esm.js';
10
+ // import { attachWebmcp } from '@nevescloud/pip/webmcp.esm.js';
11
+ //
12
+ // const rt = createRuntime({ provider });
13
+ // const webmcp = attachWebmcp(rt);
14
+ //
15
+ // // Page code (somewhere) calls:
16
+ // navigator.modelContext.registerTool({
17
+ // name: 'get_greeting',
18
+ // description: 'Returns a fresh greeting from this tab.',
19
+ // inputSchema: { type: 'object', properties: {}, required: [] },
20
+ // async execute() { return 'Hello!'; },
21
+ // });
22
+ //
23
+ // // The runtime's provider now sees `get_greeting` as a callable tool.
24
+ //
25
+ // // Clean up (reverses the patch, unregisters mirrored tools):
26
+ // webmcp.stop();
27
+ //
28
+ // Discovery strategy:
29
+ //
30
+ // 1. **Snapshot at attach time.** If `navigator.modelContext` exposes an
31
+ // iterable of currently-registered tools (`mc.tools`, `mc.getTools()`,
32
+ // or similar), we read it and mirror each entry into runtime. This
33
+ // covers tools registered before the adapter started.
34
+ // 2. **Intercept future calls.** `registerTool` / `unregisterTool` on
35
+ // `navigator.modelContext` are patched so any later call from the
36
+ // same realm propagates to runtime automatically.
37
+ //
38
+ // Limitations:
39
+ //
40
+ // - Tools registered from a *different* JavaScript realm (e.g. a separate
41
+ // extension's content script with its own `navigator` reference) are
42
+ // invisible to the patch. Snapshot covers them only if `mc` exposes
43
+ // introspection.
44
+ // - The WebMCP draft is still in flux; the field names tools may use
45
+ // (`execute` vs `handler`, `inputSchema` vs `schema`, etc.) are
46
+ // normalized below — both shapes are accepted.
47
+
48
+ const DEFAULT_SCHEMA = { type: 'object', properties: {}, required: [] };
49
+
50
+ // Pull a callable executor off the WebMCP tool def under any of the names
51
+ // the spec/implementations currently use. Returning null lets the caller
52
+ // emit a clearer error than "undefined is not a function".
53
+ function pickExecutor(toolDef) {
54
+ return (
55
+ toolDef.execute ||
56
+ toolDef.handler ||
57
+ toolDef.run ||
58
+ null
59
+ );
60
+ }
61
+
62
+ // Some WebMCP impls hand the input through under `input`, others as a
63
+ // positional argument. The runtime always calls handler(input, ctx), so
64
+ // pass input through positionally and trust the executor's own shape.
65
+ function makeRuntimeHandler(toolDef) {
66
+ const exec = pickExecutor(toolDef);
67
+ if (!exec) {
68
+ return async () => ({
69
+ error: `[webmcp] tool "${toolDef.name}" has no execute()/handler()`,
70
+ });
71
+ }
72
+ return async (input, ctx) => {
73
+ // ctx.signal exists on runtime tools but WebMCP execute() may not
74
+ // expect a signal in its signature — pass through only if the
75
+ // executor declares ≥2 parameters. Conservative: forward signal as
76
+ // a sibling property on the input where possible.
77
+ try {
78
+ return await exec(input, ctx);
79
+ } catch (err) {
80
+ return { error: String(err?.message || err) };
81
+ }
82
+ };
83
+ }
84
+
85
+ function toRuntimeToolDef(toolDef) {
86
+ return {
87
+ name: toolDef.name,
88
+ description: toolDef.description || '',
89
+ schema: toolDef.inputSchema || toolDef.schema || DEFAULT_SCHEMA,
90
+ handler: makeRuntimeHandler(toolDef),
91
+ };
92
+ }
93
+
94
+ // Try to read the current WebMCP tool registry without committing to a
95
+ // single shape — different impls expose this differently.
96
+ function snapshotExisting(mc) {
97
+ if (Array.isArray(mc.tools)) return [...mc.tools];
98
+ if (typeof mc.getTools === 'function') {
99
+ try {
100
+ const t = mc.getTools();
101
+ if (Array.isArray(t)) return t;
102
+ } catch { /* swallow */ }
103
+ }
104
+ // No introspection available — best we can do is empty. Subsequent
105
+ // registerTool calls will be caught by the patch.
106
+ return [];
107
+ }
108
+
109
+ export function attachWebmcp(runtime, options = {}) {
110
+ if (!runtime || typeof runtime.registerTool !== 'function') {
111
+ throw new Error('[webmcp] attachWebmcp(runtime): runtime must expose registerTool/unregisterTool (createRuntime() handle).');
112
+ }
113
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
114
+ throw new Error('[webmcp] navigator.modelContext is not available. Requires a supporting browser (Chrome 146+ with the WebMCP flag, or a polyfill such as hatch).');
115
+ }
116
+
117
+ const {
118
+ autoRegisterExisting = true,
119
+ namePrefix = '',
120
+ onWire = null,
121
+ onUnwire = null,
122
+ } = options;
123
+
124
+ const mc = navigator.modelContext;
125
+ // wired: webmcp tool name (as seen by mc) → runtime tool name (post-prefix).
126
+ const wired = new Map();
127
+ const originalRegister = typeof mc.registerTool === 'function' ? mc.registerTool.bind(mc) : null;
128
+ const originalUnregister = typeof mc.unregisterTool === 'function' ? mc.unregisterTool.bind(mc) : null;
129
+ let patched = false;
130
+ let stopped = false;
131
+
132
+ function runtimeName(webmcpName) {
133
+ return namePrefix ? `${namePrefix}${webmcpName}` : webmcpName;
134
+ }
135
+
136
+ function wireOne(toolDef) {
137
+ if (!toolDef || !toolDef.name) return;
138
+ if (wired.has(toolDef.name)) return; // already mirrored
139
+ const rtDef = toRuntimeToolDef(toolDef);
140
+ rtDef.name = runtimeName(toolDef.name);
141
+ try {
142
+ runtime.registerTool(rtDef);
143
+ wired.set(toolDef.name, rtDef.name);
144
+ onWire?.(rtDef.name, toolDef);
145
+ } catch (err) {
146
+ // Surface but don't throw — one bad tool shouldn't break the rest.
147
+ console.warn(`[webmcp] could not mirror tool "${toolDef.name}":`, err?.message || err);
148
+ }
149
+ }
150
+
151
+ function unwireOne(webmcpName) {
152
+ const rtName = wired.get(webmcpName);
153
+ if (!rtName) return;
154
+ try { runtime.unregisterTool(rtName); } catch {}
155
+ wired.delete(webmcpName);
156
+ onUnwire?.(rtName, webmcpName);
157
+ }
158
+
159
+ function patchModelContext() {
160
+ if (patched || !originalRegister) return;
161
+ mc.registerTool = function patchedRegisterTool(toolDef) {
162
+ const result = originalRegister(toolDef);
163
+ try { wireOne(toolDef); } catch (err) {
164
+ console.warn('[webmcp] wireOne failed:', err?.message || err);
165
+ }
166
+ return result;
167
+ };
168
+ if (originalUnregister) {
169
+ mc.unregisterTool = function patchedUnregisterTool(name) {
170
+ const result = originalUnregister(name);
171
+ try { unwireOne(name); } catch {}
172
+ return result;
173
+ };
174
+ }
175
+ patched = true;
176
+ }
177
+
178
+ function unpatchModelContext() {
179
+ if (!patched) return;
180
+ if (originalRegister) mc.registerTool = originalRegister;
181
+ if (originalUnregister) mc.unregisterTool = originalUnregister;
182
+ patched = false;
183
+ }
184
+
185
+ if (autoRegisterExisting) {
186
+ for (const t of snapshotExisting(mc)) wireOne(t);
187
+ }
188
+ patchModelContext();
189
+
190
+ function stop() {
191
+ if (stopped) return;
192
+ stopped = true;
193
+ unpatchModelContext();
194
+ for (const webmcpName of [...wired.keys()]) unwireOne(webmcpName);
195
+ }
196
+
197
+ return {
198
+ stop,
199
+ /** Re-snapshot — useful if the host added tools through a different
200
+ * realm or before the polyfill installed itself. Idempotent: tools
201
+ * already mirrored are skipped. */
202
+ refresh() {
203
+ if (stopped) return;
204
+ for (const t of snapshotExisting(mc)) wireOne(t);
205
+ },
206
+ /** Names actually mirrored into the runtime (post-prefix). */
207
+ get wired() { return [...wired.values()]; },
208
+ /** Original WebMCP-side names (pre-prefix). */
209
+ get webmcpNames() { return [...wired.keys()]; },
210
+ };
211
+ }