@mcp-b/webmcp-local-relay 2.0.13 → 2.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/dist/browser/embed.js +1 -480
- package/dist/browser/widget.html +1 -335
- package/dist/browser/widget.js +1 -0
- package/dist/cli.js +7 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +124 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{mcpRelayServer-CSrMl6U_.js → mcpRelayServer-rbay_DSr.js} +603 -268
- package/dist/mcpRelayServer-rbay_DSr.js.map +1 -0
- package/package.json +6 -4
- package/dist/mcpRelayServer-CSrMl6U_.js.map +0 -1
package/dist/browser/embed.js
CHANGED
|
@@ -1,480 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Injects a hidden relay widget iframe and bridges widget messages to host tools.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* `<script src=".../embed.js" data-relay-host="127.0.0.1" data-relay-port="9333"></script>`
|
|
6
|
-
*
|
|
7
|
-
* Add `data-debug` to enable diagnostic logging:
|
|
8
|
-
* `<script src=".../embed.js" data-debug></script>`
|
|
9
|
-
*
|
|
10
|
-
* @typedef {{ [key: string]: unknown }} JsonObject
|
|
11
|
-
* @typedef {{ name: string; description?: string; inputSchema?: JsonObject }} ToolDescriptor
|
|
12
|
-
* @typedef {{ isError?: boolean; content?: Array<{ type: string; text: string }>; [key: string]: unknown }} ToolInvokeResult
|
|
13
|
-
* @typedef {{ listTools: () => ToolDescriptor[] | Promise<ToolDescriptor[]>; invoke: (name: string, args: JsonObject) => ToolInvokeResult | Promise<ToolInvokeResult> }} ToolBridge
|
|
14
|
-
* @typedef {{ requestId: string; type: string; toolName?: unknown; args?: unknown }} WidgetRequestMessage
|
|
15
|
-
* @typedef {{ relayHost: string; relayPort: string; tabId: string; widgetUrl: string; widgetOrigin: string }} RelayConfig
|
|
16
|
-
*/
|
|
17
|
-
(() => {
|
|
18
|
-
const RELAY_IFRAME_SELECTOR = '[data-webmcp-relay]';
|
|
19
|
-
const TAB_ID_STORAGE_KEY = '__webmcp_relay_tab_id';
|
|
20
|
-
const FALLBACK_WIDGET_URL =
|
|
21
|
-
'https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay/dist/browser/widget.html';
|
|
22
|
-
|
|
23
|
-
/** @type {Window | null} */
|
|
24
|
-
let widgetWindow = null;
|
|
25
|
-
|
|
26
|
-
/** @returns {HTMLScriptElement | null} */
|
|
27
|
-
function getCurrentScriptElement() {
|
|
28
|
-
return document.currentScript instanceof HTMLScriptElement ? document.currentScript : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const scriptEl = getCurrentScriptElement();
|
|
32
|
-
const DEBUG = scriptEl ? scriptEl.hasAttribute('data-debug') : false;
|
|
33
|
-
|
|
34
|
-
/** @param {unknown[]} args */
|
|
35
|
-
function debugWarn(...args) {
|
|
36
|
-
if (DEBUG) console.warn('[webmcp-relay-embed]', ...args);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {unknown} value
|
|
41
|
-
* @returns {value is JsonObject}
|
|
42
|
-
*/
|
|
43
|
-
function isJsonObject(value) {
|
|
44
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** @returns {string} */
|
|
48
|
-
function createTabId() {
|
|
49
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
50
|
-
return crypto.randomUUID();
|
|
51
|
-
}
|
|
52
|
-
return `${String(Date.now())}_${String(Math.random()).slice(2, 10)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** @returns {string} */
|
|
56
|
-
function readOrCreateTabId() {
|
|
57
|
-
try {
|
|
58
|
-
const storedTabId = sessionStorage.getItem(TAB_ID_STORAGE_KEY);
|
|
59
|
-
if (storedTabId) {
|
|
60
|
-
return storedTabId;
|
|
61
|
-
}
|
|
62
|
-
} catch (err) {
|
|
63
|
-
debugWarn('sessionStorage read failed, tab ID will not persist:', err);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const tabId = createTabId();
|
|
67
|
-
try {
|
|
68
|
-
sessionStorage.setItem(TAB_ID_STORAGE_KEY, tabId);
|
|
69
|
-
} catch (err) {
|
|
70
|
-
debugWarn('sessionStorage write failed:', err);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return tabId;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @param {HTMLScriptElement | null} script
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
function resolveWidgetUrl(script) {
|
|
81
|
-
if (script?.src) {
|
|
82
|
-
try {
|
|
83
|
-
return new URL('widget.html', script.src).href;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
debugWarn('Failed to resolve widget URL from script src, falling back to CDN:', err);
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
debugWarn('Script element has no src attribute, falling back to CDN widget URL.');
|
|
89
|
-
}
|
|
90
|
-
return FALLBACK_WIDGET_URL;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {HTMLScriptElement | null} script
|
|
95
|
-
* @returns {RelayConfig}
|
|
96
|
-
*/
|
|
97
|
-
function buildRelayConfig(script) {
|
|
98
|
-
const widgetUrl = resolveWidgetUrl(script);
|
|
99
|
-
return {
|
|
100
|
-
relayHost: script?.getAttribute('data-relay-host') || '127.0.0.1',
|
|
101
|
-
relayPort: script?.getAttribute('data-relay-port') || '9333',
|
|
102
|
-
tabId: readOrCreateTabId(),
|
|
103
|
-
widgetUrl,
|
|
104
|
-
widgetOrigin: new URL(widgetUrl).origin,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {unknown} rawSchema
|
|
110
|
-
* @returns {JsonObject}
|
|
111
|
-
*/
|
|
112
|
-
function parseTestingSchema(rawSchema) {
|
|
113
|
-
if (typeof rawSchema !== 'string' || rawSchema.length === 0) {
|
|
114
|
-
return { type: 'object', properties: {} };
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const parsed = JSON.parse(rawSchema);
|
|
118
|
-
return isJsonObject(parsed) ? parsed : { type: 'object', properties: {} };
|
|
119
|
-
} catch (err) {
|
|
120
|
-
debugWarn(
|
|
121
|
-
'Tool inputSchema is not valid JSON:',
|
|
122
|
-
typeof rawSchema === 'string' ? rawSchema.slice(0, 200) : rawSchema,
|
|
123
|
-
err
|
|
124
|
-
);
|
|
125
|
-
return { type: 'object', properties: {} };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {unknown} value
|
|
131
|
-
* @returns {JsonObject}
|
|
132
|
-
*/
|
|
133
|
-
function toInvokeArgs(value) {
|
|
134
|
-
if (isJsonObject(value)) return value;
|
|
135
|
-
if (value !== undefined && value !== null) {
|
|
136
|
-
debugWarn('Tool invocation args must be an object, got', typeof value);
|
|
137
|
-
}
|
|
138
|
-
return {};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** @returns {ToolBridge | null} */
|
|
142
|
-
function getToolBridge() {
|
|
143
|
-
const modelContext = navigator.modelContext;
|
|
144
|
-
if (
|
|
145
|
-
modelContext &&
|
|
146
|
-
typeof modelContext.listTools === 'function' &&
|
|
147
|
-
typeof modelContext.callTool === 'function'
|
|
148
|
-
) {
|
|
149
|
-
return {
|
|
150
|
-
listTools() {
|
|
151
|
-
return modelContext.listTools().map((tool) => ({
|
|
152
|
-
name: tool.name,
|
|
153
|
-
description: tool.description,
|
|
154
|
-
inputSchema: isJsonObject(tool.inputSchema)
|
|
155
|
-
? tool.inputSchema
|
|
156
|
-
: { type: 'object', properties: {} },
|
|
157
|
-
}));
|
|
158
|
-
},
|
|
159
|
-
invoke(name, args) {
|
|
160
|
-
return modelContext.callTool({ name, arguments: args });
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const testing = navigator.modelContextTesting;
|
|
166
|
-
if (
|
|
167
|
-
testing &&
|
|
168
|
-
typeof testing.listTools === 'function' &&
|
|
169
|
-
typeof testing.executeTool === 'function'
|
|
170
|
-
) {
|
|
171
|
-
return {
|
|
172
|
-
listTools() {
|
|
173
|
-
return testing.listTools().map((tool) => ({
|
|
174
|
-
name: tool.name,
|
|
175
|
-
description: tool.description,
|
|
176
|
-
inputSchema: parseTestingSchema(tool.inputSchema),
|
|
177
|
-
}));
|
|
178
|
-
},
|
|
179
|
-
async invoke(name, args) {
|
|
180
|
-
const serialized = await testing.executeTool(name, JSON.stringify(args));
|
|
181
|
-
if (serialized === null) {
|
|
182
|
-
return {
|
|
183
|
-
isError: true,
|
|
184
|
-
content: [{ type: 'text', text: 'Tool execution interrupted by navigation' }],
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
let parsed;
|
|
188
|
-
try {
|
|
189
|
-
parsed = JSON.parse(serialized);
|
|
190
|
-
} catch {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`Testing tool returned invalid JSON: ${String(serialized).slice(0, 200)}`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
if (!isJsonObject(parsed)) {
|
|
196
|
-
throw new Error('Testing tool response was not an object');
|
|
197
|
-
}
|
|
198
|
-
return parsed;
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
debugWarn('No WebMCP runtime found (navigator.modelContext or navigator.modelContextTesting).');
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let pushScheduled = false;
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Coalesced handler for tool change events.
|
|
211
|
-
* Uses flag + setTimeout(0) to batch rapid registrations into a single push.
|
|
212
|
-
*/
|
|
213
|
-
function onToolsChanged() {
|
|
214
|
-
if (pushScheduled || !widgetWindow) return;
|
|
215
|
-
pushScheduled = true;
|
|
216
|
-
setTimeout(() => {
|
|
217
|
-
pushScheduled = false;
|
|
218
|
-
if (!widgetWindow) return;
|
|
219
|
-
const bridge = getToolBridge();
|
|
220
|
-
const toolsPromise = bridge ? Promise.resolve(bridge.listTools()) : Promise.resolve([]);
|
|
221
|
-
toolsPromise
|
|
222
|
-
.then((tools) => {
|
|
223
|
-
if (!widgetWindow) return;
|
|
224
|
-
widgetWindow.postMessage(
|
|
225
|
-
{
|
|
226
|
-
type: 'webmcp.tools.changed',
|
|
227
|
-
tools: Array.isArray(tools) ? tools : [],
|
|
228
|
-
},
|
|
229
|
-
config.widgetOrigin
|
|
230
|
-
);
|
|
231
|
-
})
|
|
232
|
-
.catch((err) => {
|
|
233
|
-
debugWarn('Failed to push tool changes:', err);
|
|
234
|
-
});
|
|
235
|
-
}, 0);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Attempts to subscribe to tool change events on the current APIs.
|
|
240
|
-
* @returns {boolean} true if subscription succeeded
|
|
241
|
-
*/
|
|
242
|
-
function trySubscribe() {
|
|
243
|
-
const mc = navigator.modelContext;
|
|
244
|
-
if (mc && typeof mc.addEventListener === 'function') {
|
|
245
|
-
try {
|
|
246
|
-
mc.addEventListener('toolschanged', onToolsChanged);
|
|
247
|
-
return true;
|
|
248
|
-
} catch (error) {
|
|
249
|
-
debugWarn('addEventListener threw:', error);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const testing = navigator.modelContextTesting;
|
|
253
|
-
if (testing && typeof testing.registerToolsChangedCallback === 'function') {
|
|
254
|
-
try {
|
|
255
|
-
testing.registerToolsChangedCallback(onToolsChanged);
|
|
256
|
-
return true;
|
|
257
|
-
} catch (error) {
|
|
258
|
-
debugWarn('Failed to subscribe via registerToolsChangedCallback:', error);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Subscribes to modelContext tool change events.
|
|
266
|
-
* Tries addEventListener first (future native EventTarget support),
|
|
267
|
-
* falls back to registerToolsChangedCallback on modelContextTesting.
|
|
268
|
-
*
|
|
269
|
-
* When embed.js loads as a classic script before the app's module entry
|
|
270
|
-
* point, the WebMCP APIs may not exist yet. In that case we retry on a
|
|
271
|
-
* short interval (up to ~5 s) to catch late initialization.
|
|
272
|
-
*/
|
|
273
|
-
function subscribeToToolChanges() {
|
|
274
|
-
if (trySubscribe()) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let retries = 0;
|
|
279
|
-
const MAX_RETRIES = 50;
|
|
280
|
-
const RETRY_INTERVAL_MS = 100;
|
|
281
|
-
const retryTimer = setInterval(() => {
|
|
282
|
-
retries++;
|
|
283
|
-
if (trySubscribe()) {
|
|
284
|
-
clearInterval(retryTimer);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (retries >= MAX_RETRIES) {
|
|
288
|
-
clearInterval(retryTimer);
|
|
289
|
-
debugWarn(
|
|
290
|
-
'Could not subscribe to tool changes after ' +
|
|
291
|
-
MAX_RETRIES +
|
|
292
|
-
' retries. Dynamic tool updates will not be relayed.'
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}, RETRY_INTERVAL_MS);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* @param {MessageEventSource | null} source
|
|
300
|
-
* @param {string} origin
|
|
301
|
-
* @param {JsonObject} payload
|
|
302
|
-
*/
|
|
303
|
-
function respondToSource(source, origin, payload) {
|
|
304
|
-
if (!source || typeof source !== 'object' || !('postMessage' in source)) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const { postMessage } = source;
|
|
309
|
-
if (typeof postMessage !== 'function') {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
postMessage.call(source, payload, origin);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* @param {unknown} value
|
|
318
|
-
* @returns {WidgetRequestMessage | null}
|
|
319
|
-
*/
|
|
320
|
-
function parseWidgetRequest(value) {
|
|
321
|
-
if (
|
|
322
|
-
!isJsonObject(value) ||
|
|
323
|
-
typeof value.requestId !== 'string' ||
|
|
324
|
-
typeof value.type !== 'string'
|
|
325
|
-
) {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
requestId: value.requestId,
|
|
331
|
-
type: value.type,
|
|
332
|
-
toolName: value.toolName,
|
|
333
|
-
args: value.args,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* @param {WidgetRequestMessage} request
|
|
339
|
-
* @param {MessageEvent} event
|
|
340
|
-
*/
|
|
341
|
-
function handleListRequest(request, event) {
|
|
342
|
-
const bridge = getToolBridge();
|
|
343
|
-
const toolsPromise = bridge ? Promise.resolve(bridge.listTools()) : Promise.resolve([]);
|
|
344
|
-
|
|
345
|
-
toolsPromise
|
|
346
|
-
.then((tools) => {
|
|
347
|
-
respondToSource(event.source, event.origin, {
|
|
348
|
-
type: 'webmcp.tools.list.response',
|
|
349
|
-
requestId: request.requestId,
|
|
350
|
-
tools: Array.isArray(tools) ? tools : [],
|
|
351
|
-
});
|
|
352
|
-
})
|
|
353
|
-
.catch((error) => {
|
|
354
|
-
debugWarn('Failed to list tools:', error);
|
|
355
|
-
respondToSource(event.source, event.origin, {
|
|
356
|
-
type: 'webmcp.tools.list.response',
|
|
357
|
-
requestId: request.requestId,
|
|
358
|
-
tools: [],
|
|
359
|
-
error: `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`,
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* @param {WidgetRequestMessage} request
|
|
366
|
-
* @param {MessageEvent} event
|
|
367
|
-
*/
|
|
368
|
-
function handleInvokeRequest(request, event) {
|
|
369
|
-
const bridge = getToolBridge();
|
|
370
|
-
if (!bridge) {
|
|
371
|
-
respondToSource(event.source, event.origin, {
|
|
372
|
-
type: 'webmcp.tools.invoke.error',
|
|
373
|
-
requestId: request.requestId,
|
|
374
|
-
error: 'No WebMCP runtime found on this page',
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
Promise.resolve(bridge.invoke(String(request.toolName ?? ''), toInvokeArgs(request.args)))
|
|
380
|
-
.then((result) => {
|
|
381
|
-
respondToSource(event.source, event.origin, {
|
|
382
|
-
type: 'webmcp.tools.invoke.response',
|
|
383
|
-
requestId: request.requestId,
|
|
384
|
-
result: isJsonObject(result) ? result : {},
|
|
385
|
-
});
|
|
386
|
-
})
|
|
387
|
-
.catch((error) => {
|
|
388
|
-
respondToSource(event.source, event.origin, {
|
|
389
|
-
type: 'webmcp.tools.invoke.error',
|
|
390
|
-
requestId: request.requestId,
|
|
391
|
-
error: String(error instanceof Error ? error.message : error),
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** @param {RelayConfig} config */
|
|
397
|
-
function injectRelayWidget(config) {
|
|
398
|
-
if (document.querySelector(RELAY_IFRAME_SELECTOR)) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const searchParams = new URLSearchParams();
|
|
403
|
-
searchParams.set('tabId', config.tabId);
|
|
404
|
-
searchParams.set('hostOrigin', window.location.origin);
|
|
405
|
-
const cleanUrl = new URL(window.location.href);
|
|
406
|
-
cleanUrl.search = '';
|
|
407
|
-
cleanUrl.hash = '';
|
|
408
|
-
searchParams.set('hostUrl', cleanUrl.href);
|
|
409
|
-
searchParams.set('hostTitle', document.title || '');
|
|
410
|
-
searchParams.set('relayHost', config.relayHost);
|
|
411
|
-
searchParams.set('relayPort', config.relayPort);
|
|
412
|
-
|
|
413
|
-
const iframe = document.createElement('iframe');
|
|
414
|
-
iframe.src = `${config.widgetUrl}?${searchParams.toString()}`;
|
|
415
|
-
iframe.style.display = 'none';
|
|
416
|
-
iframe.setAttribute('aria-hidden', 'true');
|
|
417
|
-
iframe.setAttribute('data-webmcp-relay', '1');
|
|
418
|
-
document.body.appendChild(iframe);
|
|
419
|
-
widgetWindow = iframe.contentWindow;
|
|
420
|
-
iframe.addEventListener('load', () => {
|
|
421
|
-
widgetWindow = iframe.contentWindow;
|
|
422
|
-
});
|
|
423
|
-
iframe.addEventListener('error', () => {
|
|
424
|
-
console.error(
|
|
425
|
-
'[webmcp-relay-embed] Failed to load relay widget iframe from:',
|
|
426
|
-
iframe.src,
|
|
427
|
-
'-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.'
|
|
428
|
-
);
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (document.querySelector(RELAY_IFRAME_SELECTOR)) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
let config;
|
|
437
|
-
try {
|
|
438
|
-
config = buildRelayConfig(scriptEl);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
console.error('[webmcp-relay-embed] Failed to initialize relay configuration:', err);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
window.addEventListener('message', (event) => {
|
|
445
|
-
if (event.origin !== config.widgetOrigin) {
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (!widgetWindow || event.source !== widgetWindow) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const data = event.data;
|
|
453
|
-
if (isJsonObject(data) && data.type === 'webmcp.reload') {
|
|
454
|
-
window.location.reload();
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const request = parseWidgetRequest(event.data);
|
|
459
|
-
if (!request) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (request.type === 'webmcp.tools.list.request') {
|
|
464
|
-
handleListRequest(request, event);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (request.type === 'webmcp.tools.invoke.request') {
|
|
469
|
-
handleInvokeRequest(request, event);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
if (document.body) {
|
|
474
|
-
injectRelayWidget(config);
|
|
475
|
-
} else {
|
|
476
|
-
document.addEventListener('DOMContentLoaded', () => injectRelayWidget(config), { once: true });
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
subscribeToToolChanges();
|
|
480
|
-
})();
|
|
1
|
+
(function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let t=`[data-webmcp-relay]`,n=`__webmcp_relay_tab_id`,r=null,i;function a(){return document.currentScript instanceof HTMLScriptElement?document.currentScript:null}let o=a(),s=o?o.hasAttribute(`data-debug`):!1;function c(...e){s&&console.warn(`[webmcp-relay-embed]`,...e)}function l(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function u(){try{let e=sessionStorage.getItem(n);if(e)return e}catch(e){c(`sessionStorage read failed, tab ID will not persist:`,e)}let e=l();try{sessionStorage.setItem(n,e)}catch(e){c(`sessionStorage write failed:`,e)}return e}function d(e){if(e?.src)try{return new URL(`widget.html`,e.src).href}catch(e){c(`Failed to resolve widget URL from script src, falling back to CDN:`,e)}else c(`Script element has no src attribute, falling back to CDN widget URL.`);return`https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay/dist/browser/widget.html`}function f(e){let t=d(e),n=e?.getAttribute(`data-relay-id`)||void 0,r=e?.getAttribute(`data-relay-workspace`)||void 0;return{autoConnect:e?.getAttribute(`data-auto-connect`)!==`false`,relayHost:e?.getAttribute(`data-relay-host`)||`127.0.0.1`,relayPort:e?.getAttribute(`data-relay-port`)||`9333`,...n?{relayId:n}:{},...r?{relayWorkspace:r}:{},tabId:u(),widgetUrl:t,widgetOrigin:new URL(t).origin}}function p(t){if(typeof t!=`string`||t.length===0)return{type:`object`,properties:{}};try{let n=JSON.parse(t);return e(n)?n:{type:`object`,properties:{}}}catch(e){return c(`Tool inputSchema is not valid JSON:`,typeof t==`string`?t.slice(0,200):t,e),{type:`object`,properties:{}}}}function m(t){return e(t)?t:(t!=null&&c(`Tool invocation args must be an object, got`,typeof t),{})}function h(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema}}function g(e){return{name:e.name,description:e.description,inputSchema:p(e.inputSchema)}}function _(){let e=navigator.modelContext;if(e&&typeof e.listTools==`function`&&typeof e.callTool==`function`)return e}function v(){let t=_();if(t)return{listTools(){return t.listTools().map(h)},invoke(e,n){return t.callTool({name:e,arguments:n})}};let n=navigator.modelContextTesting;return n&&typeof n.listTools==`function`&&typeof n.executeTool==`function`?{listTools(){return n.listTools().map(g)},async invoke(t,r){let i=await n.executeTool(t,JSON.stringify(r));if(i===null)return{isError:!0,content:[{type:`text`,text:`Tool execution interrupted by navigation`}]};let a;try{a=JSON.parse(i)}catch{throw Error(`Testing tool returned invalid JSON: ${String(i).slice(0,200)}`)}if(!e(a))throw Error(`Testing tool response was not an object`);return a}}:(c(`No WebMCP runtime found (navigator.modelContext or navigator.modelContextTesting).`),null)}let y=!1;function b(){y||!r||(y=!0,setTimeout(()=>{if(y=!1,!r)return;let e=v();(e?Promise.resolve(e.listTools()):Promise.resolve([])).then(e=>{r&&r.postMessage({type:`webmcp.tools.changed`,tools:Array.isArray(e)?e:[]},i.widgetOrigin)}).catch(e=>{c(`Failed to push tool changes:`,e)})},0))}function x(){let e=_();if(e)try{return e.addEventListener(`toolschanged`,b),!0}catch(e){c(`addEventListener threw:`,e)}let t=navigator.modelContextTesting;if(t&&typeof t.registerToolsChangedCallback==`function`)try{return t.registerToolsChangedCallback(b),!0}catch(e){c(`Failed to subscribe via registerToolsChangedCallback:`,e)}return!1}function S(){if(x())return;let e=0,t=setInterval(()=>{if(e++,x()){clearInterval(t);return}e>=50&&(clearInterval(t),c(`Could not subscribe to tool changes after 50 retries. Dynamic tool updates will not be relayed.`))},100)}function C(e,t,n){!e||typeof e!=`object`||!(`postMessage`in e)||e.postMessage(n,t)}function w(t){return!e(t)||typeof t.requestId!=`string`||typeof t.type!=`string`?null:{requestId:t.requestId,type:t.type,toolName:t.toolName,args:t.args}}function T(e,t){let n=v();(n?Promise.resolve(n.listTools()):Promise.resolve([])).then(n=>{C(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:Array.isArray(n)?n:[]})}).catch(n=>{c(`Failed to list tools:`,n),C(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:[],error:`Failed to list tools: ${n instanceof Error?n.message:String(n)}`})})}function E(t,n){let r=v();if(!r){C(n.source,n.origin,{type:`webmcp.tools.invoke.error`,requestId:t.requestId,error:`No WebMCP runtime found on this page`});return}Promise.resolve(r.invoke(String(t.toolName??``),m(t.args))).then(r=>{C(n.source,n.origin,{type:`webmcp.tools.invoke.response`,requestId:t.requestId,result:e(r)?r:{}})}).catch(e=>{C(n.source,n.origin,{type:`webmcp.tools.invoke.error`,requestId:t.requestId,error:String(e instanceof Error?e.message:e)})})}function D(e){if(document.querySelector(t))return;let n=new URLSearchParams;n.set(`tabId`,e.tabId),n.set(`hostOrigin`,window.location.origin);let i=new URL(window.location.href);i.search=``,i.hash=``,n.set(`hostUrl`,i.href),n.set(`hostTitle`,document.title||``),n.set(`relayHost`,e.relayHost),n.set(`relayPort`,e.relayPort),n.set(`autoConnect`,e.autoConnect?`true`:`false`),e.relayId&&n.set(`relayId`,e.relayId),e.relayWorkspace&&n.set(`relayWorkspace`,e.relayWorkspace);let a=document.createElement(`iframe`);a.src=`${e.widgetUrl}?${n.toString()}`,a.style.display=`none`,a.setAttribute(`aria-hidden`,`true`),a.setAttribute(`data-webmcp-relay`,`1`),a.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(a),r=a.contentWindow,a.addEventListener(`load`,()=>{r=a.contentWindow}),a.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,a.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`)})}if(!document.querySelector(t)){try{i=f(o)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,t=>{if(t.origin!==i.widgetOrigin||!r||t.source!==r)return;let n=t.data;if(e(n)&&n.type===`webmcp.reload`){window.location.reload();return}let a=w(t.data);if(a){if(a.type===`webmcp.tools.list.request`){T(a,t);return}a.type===`webmcp.tools.invoke.request`&&E(a,t)}}),document.body?D(i):document.addEventListener(`DOMContentLoaded`,()=>D(i),{once:!0}),S()}})();
|