@mcp-b/webmcp-local-relay 2.0.13 → 2.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.
- 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/widget.html
CHANGED
|
@@ -5,340 +5,6 @@
|
|
|
5
5
|
<title>WebMCP Relay Widget</title>
|
|
6
6
|
</head>
|
|
7
7
|
<body>
|
|
8
|
-
<script>
|
|
9
|
-
/**
|
|
10
|
-
* Relay widget runtime. Runs inside a hidden iframe and proxies tool messages
|
|
11
|
-
* between the host page (`postMessage`) and the local relay socket.
|
|
12
|
-
*
|
|
13
|
-
* Security: The iframe boundary provides origin isolation. All postMessage
|
|
14
|
-
* exchanges validate `event.origin` against the host page's origin to prevent
|
|
15
|
-
* cross-origin message injection.
|
|
16
|
-
*
|
|
17
|
-
* @typedef {{ [key: string]: unknown }} JsonObject
|
|
18
|
-
* @typedef {{ requestId: string; type: string; tools?: unknown; result?: unknown; error?: unknown }} HostMessage
|
|
19
|
-
* @typedef {{ resolve: (value: HostMessage) => void; reject: (reason: Error) => void; timeoutId: number; responseType: string; errorType: string }} PendingRequest
|
|
20
|
-
* @typedef {{ hostOrigin: string; hostUrl: string; hostTitle: string; tabId: string; wsUrl: string }} WidgetConfig
|
|
21
|
-
*/
|
|
22
|
-
(() => {
|
|
23
|
-
'use strict';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Browser-to-relay reconnect policy.
|
|
27
|
-
* Uses exponential backoff from 500ms up to a 3000ms cap so recovery
|
|
28
|
-
* remains responsive without creating tight reconnect loops.
|
|
29
|
-
*/
|
|
30
|
-
const RECONNECT_INITIAL_DELAY_MS = 500;
|
|
31
|
-
const RECONNECT_MAX_DELAY_MS = 3000;
|
|
32
|
-
const RECONNECT_MAX_ATTEMPTS = 100;
|
|
33
|
-
const REQUEST_TIMEOUT_MS = 10000;
|
|
34
|
-
const params = new URLSearchParams(window.location.search);
|
|
35
|
-
|
|
36
|
-
/** @returns {string} */
|
|
37
|
-
function createRequestId() {
|
|
38
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
39
|
-
return crypto.randomUUID();
|
|
40
|
-
}
|
|
41
|
-
return String(Date.now() + Math.random());
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {unknown} value
|
|
46
|
-
* @returns {value is JsonObject}
|
|
47
|
-
*/
|
|
48
|
-
function isJsonObject(value) {
|
|
49
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {string} host
|
|
54
|
-
* @returns {boolean}
|
|
55
|
-
*/
|
|
56
|
-
function isLoopback(host) {
|
|
57
|
-
return host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** @returns {WidgetConfig | null} */
|
|
61
|
-
function parseConfig() {
|
|
62
|
-
const hostOrigin = params.get('hostOrigin');
|
|
63
|
-
if (!hostOrigin) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const hostUrl = params.get('hostUrl') || hostOrigin;
|
|
68
|
-
const hostTitle = params.get('hostTitle') || '';
|
|
69
|
-
const tabId = params.get('tabId') || createRequestId();
|
|
70
|
-
const relayHost = params.get('relayHost') || '127.0.0.1';
|
|
71
|
-
const relayPort = params.get('relayPort') || '9333';
|
|
72
|
-
|
|
73
|
-
if (!isLoopback(relayHost)) {
|
|
74
|
-
console.error('[webmcp-relay-widget] relayHost must be a loopback address, got:', relayHost);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { hostOrigin, hostUrl, hostTitle, tabId, wsUrl: `ws://${relayHost}:${relayPort}` };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @param {unknown} value
|
|
83
|
-
* @returns {HostMessage | null}
|
|
84
|
-
*/
|
|
85
|
-
function parseHostMessage(value) {
|
|
86
|
-
if (!isJsonObject(value) || typeof value.requestId !== 'string' || typeof value.type !== 'string') {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
requestId: value.requestId,
|
|
91
|
-
type: value.type,
|
|
92
|
-
tools: value.tools,
|
|
93
|
-
result: value.result,
|
|
94
|
-
error: value.error,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Sends data through a WebSocket, catching errors from closed/closing states.
|
|
100
|
-
* @param {WebSocket} ws
|
|
101
|
-
* @param {string} data
|
|
102
|
-
*/
|
|
103
|
-
function safeSend(ws, data) {
|
|
104
|
-
try {
|
|
105
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
106
|
-
ws.send(data);
|
|
107
|
-
}
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.warn('[webmcp-relay-widget] Failed to send message:', err);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const config = parseConfig();
|
|
114
|
-
if (!config) {
|
|
115
|
-
console.warn('[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** @type {Map<string, PendingRequest>} */
|
|
120
|
-
const pendingRequests = new Map();
|
|
121
|
-
let reconnectDelayMs = RECONNECT_INITIAL_DELAY_MS;
|
|
122
|
-
let reconnectAttempts = 0;
|
|
123
|
-
/** @type {WebSocket | null} */
|
|
124
|
-
let activeSocket = null;
|
|
125
|
-
let helloSent = false;
|
|
126
|
-
|
|
127
|
-
window.addEventListener('message', (event) => {
|
|
128
|
-
if (event.origin !== config.hostOrigin) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const data = event.data;
|
|
133
|
-
if (isJsonObject(data) && data.type === 'webmcp.tools.changed') {
|
|
134
|
-
if (activeSocket && helloSent) {
|
|
135
|
-
safeSend(
|
|
136
|
-
activeSocket,
|
|
137
|
-
JSON.stringify({
|
|
138
|
-
type: 'tools/changed',
|
|
139
|
-
tools: Array.isArray(data.tools) ? data.tools : [],
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const message = parseHostMessage(data);
|
|
147
|
-
if (!message) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const pending = pendingRequests.get(message.requestId);
|
|
152
|
-
if (!pending) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (message.type === pending.responseType) {
|
|
157
|
-
clearTimeout(pending.timeoutId);
|
|
158
|
-
pendingRequests.delete(message.requestId);
|
|
159
|
-
pending.resolve(message);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (message.type === pending.errorType) {
|
|
164
|
-
clearTimeout(pending.timeoutId);
|
|
165
|
-
pendingRequests.delete(message.requestId);
|
|
166
|
-
pending.reject(new Error(String(message.error || 'Unknown host error')));
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* @param {string} baseType
|
|
172
|
-
* @param {JsonObject} payload
|
|
173
|
-
* @returns {Promise<HostMessage>}
|
|
174
|
-
*/
|
|
175
|
-
function requestHost(baseType, payload) {
|
|
176
|
-
const requestId = createRequestId();
|
|
177
|
-
|
|
178
|
-
return new Promise((resolve, reject) => {
|
|
179
|
-
const timeoutId = setTimeout(() => {
|
|
180
|
-
pendingRequests.delete(requestId);
|
|
181
|
-
reject(new Error(`Host response timeout: ${baseType}`));
|
|
182
|
-
}, REQUEST_TIMEOUT_MS);
|
|
183
|
-
|
|
184
|
-
pendingRequests.set(requestId, {
|
|
185
|
-
resolve,
|
|
186
|
-
reject,
|
|
187
|
-
timeoutId,
|
|
188
|
-
responseType: `${baseType}.response`,
|
|
189
|
-
errorType: `${baseType}.error`,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
window.parent.postMessage(
|
|
193
|
-
{ type: `${baseType}.request`, requestId, ...payload },
|
|
194
|
-
config.hostOrigin
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** @param {WebSocket} socket */
|
|
200
|
-
function sendHelloAndTools(socket) {
|
|
201
|
-
requestHost('webmcp.tools.list', {})
|
|
202
|
-
.then((message) => {
|
|
203
|
-
const tools = Array.isArray(message.tools) ? message.tools : [];
|
|
204
|
-
safeSend(
|
|
205
|
-
socket,
|
|
206
|
-
JSON.stringify({
|
|
207
|
-
type: 'hello',
|
|
208
|
-
tabId: config.tabId,
|
|
209
|
-
origin: config.hostOrigin,
|
|
210
|
-
url: config.hostUrl,
|
|
211
|
-
title: config.hostTitle || document.referrer || 'Unknown page',
|
|
212
|
-
})
|
|
213
|
-
);
|
|
214
|
-
safeSend(socket, JSON.stringify({ type: 'tools/list', tools }));
|
|
215
|
-
helloSent = true;
|
|
216
|
-
})
|
|
217
|
-
.catch((error) => {
|
|
218
|
-
console.warn('[webmcp-relay-widget] Hello handshake failed:', error);
|
|
219
|
-
socket.close();
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* @param {WebSocket} socket
|
|
225
|
-
* @param {unknown} rawMessage
|
|
226
|
-
*/
|
|
227
|
-
function handleRelayMessage(socket, rawMessage) {
|
|
228
|
-
let relayMessage;
|
|
229
|
-
try {
|
|
230
|
-
relayMessage = JSON.parse(String(rawMessage));
|
|
231
|
-
} catch (parseError) {
|
|
232
|
-
console.warn('[webmcp-relay-widget] Failed to parse relay message:', parseError);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!isJsonObject(relayMessage) || typeof relayMessage.type !== 'string') {
|
|
237
|
-
console.debug('[webmcp-relay-widget] Ignoring non-object or untyped relay message');
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (relayMessage.type === 'ping') {
|
|
242
|
-
safeSend(socket, JSON.stringify({ type: 'pong' }));
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (relayMessage.type === 'reload') {
|
|
247
|
-
window.parent.postMessage({ type: 'webmcp.reload' }, config.hostOrigin);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (relayMessage.type !== 'invoke') {
|
|
252
|
-
console.debug('[webmcp-relay-widget] Ignoring unrecognized message type:', relayMessage.type);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
requestHost('webmcp.tools.invoke', {
|
|
257
|
-
toolName: relayMessage.toolName,
|
|
258
|
-
args: isJsonObject(relayMessage.args) ? relayMessage.args : {},
|
|
259
|
-
})
|
|
260
|
-
.then((hostResponse) => {
|
|
261
|
-
safeSend(
|
|
262
|
-
socket,
|
|
263
|
-
JSON.stringify({
|
|
264
|
-
type: 'result',
|
|
265
|
-
callId: relayMessage.callId,
|
|
266
|
-
result: hostResponse.result,
|
|
267
|
-
})
|
|
268
|
-
);
|
|
269
|
-
})
|
|
270
|
-
.catch((error) => {
|
|
271
|
-
safeSend(
|
|
272
|
-
socket,
|
|
273
|
-
JSON.stringify({
|
|
274
|
-
type: 'result',
|
|
275
|
-
callId: relayMessage.callId,
|
|
276
|
-
result: {
|
|
277
|
-
isError: true,
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: 'text',
|
|
281
|
-
text: String(error instanceof Error ? error.message : error),
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
},
|
|
285
|
-
})
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function connect() {
|
|
291
|
-
const socket = new WebSocket(config.wsUrl);
|
|
292
|
-
activeSocket = socket;
|
|
293
|
-
|
|
294
|
-
socket.addEventListener('open', () => {
|
|
295
|
-
reconnectDelayMs = RECONNECT_INITIAL_DELAY_MS;
|
|
296
|
-
reconnectAttempts = 0;
|
|
297
|
-
sendHelloAndTools(socket);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
socket.addEventListener('message', (event) => {
|
|
301
|
-
handleRelayMessage(socket, event.data);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
socket.addEventListener('close', () => {
|
|
305
|
-
helloSent = false;
|
|
306
|
-
activeSocket = null;
|
|
307
|
-
|
|
308
|
-
for (const [requestId, pending] of pendingRequests) {
|
|
309
|
-
clearTimeout(pending.timeoutId);
|
|
310
|
-
pendingRequests.delete(requestId);
|
|
311
|
-
pending.reject(new Error('WebSocket connection lost'));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
reconnectAttempts++;
|
|
315
|
-
if (reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
|
|
316
|
-
console.error(
|
|
317
|
-
`[webmcp-relay-widget] Giving up reconnection after ${reconnectAttempts} attempts. Is the relay server running at ${config.wsUrl}?`
|
|
318
|
-
);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (reconnectAttempts % 10 === 0) {
|
|
322
|
-
console.warn(
|
|
323
|
-
`[webmcp-relay-widget] Still reconnecting after ${reconnectAttempts} attempts (delay: ${reconnectDelayMs}ms)`
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
setTimeout(connect, reconnectDelayMs);
|
|
327
|
-
reconnectDelayMs = Math.min(reconnectDelayMs * 1.5, RECONNECT_MAX_DELAY_MS);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
socket.addEventListener('error', (event) => {
|
|
331
|
-
console.warn('[webmcp-relay-widget] WebSocket error:', event);
|
|
332
|
-
try {
|
|
333
|
-
socket.close();
|
|
334
|
-
} catch (closeErr) {
|
|
335
|
-
console.warn('[webmcp-relay-widget] Error closing socket after error:', closeErr);
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
connect();
|
|
341
|
-
})();
|
|
342
|
-
</script>
|
|
8
|
+
<script>(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]);function t(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function n(t){return e.has(t)}function r(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function i(e){return String(e).replace(/[\r\n]/g,``)}function a(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function o(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let s=3e3;function c(e=window.location.search){let t=new URLSearchParams(e),i=t.get(`hostOrigin`);if(!i)return null;let a=t.get(`hostUrl`)||i,o=t.get(`hostTitle`)||``,s=t.get(`tabId`)||r(),c=t.get(`relayHost`)||`127.0.0.1`,l=t.get(`relayPort`),u=l&&l.length>0?Number.parseInt(l,10):9333,d=t.get(`autoConnect`)!==`false`,f=t.get(`relayId`)||void 0,p=t.get(`relayWorkspace`)||void 0;return n(c)?!Number.isInteger(u)||u<1||u>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,l),null):{autoConnect:d,hostOrigin:i,hostTitle:o,hostUrl:a,relayHostHint:c,relayPortHint:u,...f?{relayId:f}:{},...p?{relayWorkspace:p}:{},tabId:s}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,c),null)}function l(e){return!t(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function u(){let e=c();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}_(e)}function d(e){return!t(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function f(e){return o({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function p(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(f(e));if(!t)return null;let n=JSON.parse(t);return typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function m(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(f(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function h(e){let t=p(e);[e.relayHostHint,`127.0.0.1`,`[::1]`].filter((e,t,n)=>n.indexOf(e)===t);let n=new Set,r=[],i=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};e.relayPortHint!==void 0&&i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=9333;t<=9348;t+=1)i(e,t);return r}async function g(e){let t=`ws://${e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=d(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1200);i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function _(e){let n=new Map,o=null,c=null,u=0,f=!1,p=500,_=null,v=`idle`,y=e=>{for(let[t,r]of n)clearTimeout(r.timeoutId),n.delete(t),r.reject(Error(e))},b=e=>{v=e};function x(t,i){let a=r();return new Promise((r,o)=>{let s=setTimeout(()=>{n.delete(a),o(Error(`Host response timeout: ${t}`))},1e4);n.set(a,{resolve:r,reject:o,timeoutId:s,responseType:`${t}.response`,errorType:`${t}.error`}),window.parent.postMessage({type:`${t}.request`,requestId:a,...i},e.hostOrigin)})}let S=(n,r)=>{_&&(clearTimeout(_),_=null),o=r,c=n,p=500,f=!1,b(`connected`),m(e,r),n.addEventListener(`message`,s=>{let c;try{c=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let l=d(c);if(l){o={hello:l,host:r.host,port:r.port};return}if(!t(c)||typeof c.type!=`string`){console.debug(`[webmcp-relay-widget] Ignoring non-object or untyped relay message`);return}if(c.type===`ping`){a(n,JSON.stringify({type:`pong`}));return}if(c.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(c.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,i(c.type));return}x(`webmcp.tools.invoke`,{toolName:c.toolName,args:t(c.args)?c.args:{}}).then(e=>{a(n,JSON.stringify({type:`result`,callId:c.callId,result:e.result}))}).catch(e=>{a(n,JSON.stringify({type:`result`,callId:c.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),n.addEventListener(`close`,()=>{if(c===n){if(f=!1,c=null,y(`WebSocket connection lost`),u+=1,u>=5){u=0,E();return}T()}},{once:!0}),n.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{n.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),x(`webmcp.tools.list`,{}).then(t=>{let r=Array.isArray(t.tools)?t.tools:[];a(n,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),a(n,JSON.stringify({type:`tools/list`,tools:r})),f=!0}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{n.close()}catch{}})},C=async t=>{let n=await g({host:t.host,port:t.port});if(!n)return!1;let r=n.endpoint;return e.relayId&&r.hello.relayId!==e.relayId||e.relayWorkspace&&r.hello.workspace!==e.relayWorkspace?(n.socket.close(),!1):(S(n.socket,r),!0)},w=async()=>{b(`discovering`);for(let t of h(e))if(await C(t))return u=0,!0;return!1},T=()=>{if(!o||_)return;b(`retry-same-endpoint`);let e={host:o.host,port:o.port},t=Math.min(s,Math.round(p*(.85+Math.random()*.3)));p=Math.min(Math.round(p*1.5),s),_=setTimeout(()=>{_=null,C(e).then(e=>{if(e){u=0;return}if(u>=5){u=0,E();return}T()})},t)},E=()=>{if(_)return;b(`rediscover`);let e=Math.min(s,Math.round(p*(.85+Math.random()*.3)));p=Math.min(Math.round(p*1.5),s),_=setTimeout(()=>{_=null,w().then(e=>{e||E()})},e)};window.addEventListener(`message`,r=>{if(r.origin!==e.hostOrigin)return;let i=r.data;if(t(i)&&i.type===`webmcp.tools.changed`){c&&f&&a(c,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(t(i)&&i.type===`webmcp.connect`){!c&&v!==`discovering`&&w();return}let o=l(i);if(!o)return;let s=n.get(o.requestId);if(s){if(o.type===s.responseType){clearTimeout(s.timeoutId),n.delete(o.requestId),s.resolve(o);return}o.type===s.errorType&&(clearTimeout(s.timeoutId),n.delete(o.requestId),s.reject(Error(String(o.error||`Unknown host error`))))}}),e.autoConnect?w().then(e=>{e||E()}):b(`idle`)}u()})();</script>
|
|
343
9
|
</body>
|
|
344
10
|
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]);function t(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function n(t){return e.has(t)}function r(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function i(e){return String(e).replace(/[\r\n]/g,``)}function a(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function o(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let s=3e3;function c(e=window.location.search){let t=new URLSearchParams(e),i=t.get(`hostOrigin`);if(!i)return null;let a=t.get(`hostUrl`)||i,o=t.get(`hostTitle`)||``,s=t.get(`tabId`)||r(),c=t.get(`relayHost`)||`127.0.0.1`,l=t.get(`relayPort`),u=l&&l.length>0?Number.parseInt(l,10):9333,d=t.get(`autoConnect`)!==`false`,f=t.get(`relayId`)||void 0,p=t.get(`relayWorkspace`)||void 0;return n(c)?!Number.isInteger(u)||u<1||u>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,l),null):{autoConnect:d,hostOrigin:i,hostTitle:o,hostUrl:a,relayHostHint:c,relayPortHint:u,...f?{relayId:f}:{},...p?{relayWorkspace:p}:{},tabId:s}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,c),null)}function l(e){return!t(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function u(){let e=c();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}_(e)}function d(e){return!t(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function f(e){return o({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function p(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(f(e));if(!t)return null;let n=JSON.parse(t);return typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function m(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(f(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function h(e){let t=p(e);[e.relayHostHint,`127.0.0.1`,`[::1]`].filter((e,t,n)=>n.indexOf(e)===t);let n=new Set,r=[],i=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};e.relayPortHint!==void 0&&i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=9333;t<=9348;t+=1)i(e,t);return r}async function g(e){let t=`ws://${e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=d(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1200);i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function _(e){let n=new Map,o=null,c=null,u=0,f=!1,p=500,_=null,v=`idle`,y=e=>{for(let[t,r]of n)clearTimeout(r.timeoutId),n.delete(t),r.reject(Error(e))},b=e=>{v=e};function x(t,i){let a=r();return new Promise((r,o)=>{let s=setTimeout(()=>{n.delete(a),o(Error(`Host response timeout: ${t}`))},1e4);n.set(a,{resolve:r,reject:o,timeoutId:s,responseType:`${t}.response`,errorType:`${t}.error`}),window.parent.postMessage({type:`${t}.request`,requestId:a,...i},e.hostOrigin)})}let S=(n,r)=>{_&&(clearTimeout(_),_=null),o=r,c=n,p=500,f=!1,b(`connected`),m(e,r),n.addEventListener(`message`,s=>{let c;try{c=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let l=d(c);if(l){o={hello:l,host:r.host,port:r.port};return}if(!t(c)||typeof c.type!=`string`){console.debug(`[webmcp-relay-widget] Ignoring non-object or untyped relay message`);return}if(c.type===`ping`){a(n,JSON.stringify({type:`pong`}));return}if(c.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(c.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,i(c.type));return}x(`webmcp.tools.invoke`,{toolName:c.toolName,args:t(c.args)?c.args:{}}).then(e=>{a(n,JSON.stringify({type:`result`,callId:c.callId,result:e.result}))}).catch(e=>{a(n,JSON.stringify({type:`result`,callId:c.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),n.addEventListener(`close`,()=>{if(c===n){if(f=!1,c=null,y(`WebSocket connection lost`),u+=1,u>=5){u=0,E();return}T()}},{once:!0}),n.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{n.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),x(`webmcp.tools.list`,{}).then(t=>{let r=Array.isArray(t.tools)?t.tools:[];a(n,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),a(n,JSON.stringify({type:`tools/list`,tools:r})),f=!0}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{n.close()}catch{}})},C=async t=>{let n=await g({host:t.host,port:t.port});if(!n)return!1;let r=n.endpoint;return e.relayId&&r.hello.relayId!==e.relayId||e.relayWorkspace&&r.hello.workspace!==e.relayWorkspace?(n.socket.close(),!1):(S(n.socket,r),!0)},w=async()=>{b(`discovering`);for(let t of h(e))if(await C(t))return u=0,!0;return!1},T=()=>{if(!o||_)return;b(`retry-same-endpoint`);let e={host:o.host,port:o.port},t=Math.min(s,Math.round(p*(.85+Math.random()*.3)));p=Math.min(Math.round(p*1.5),s),_=setTimeout(()=>{_=null,C(e).then(e=>{if(e){u=0;return}if(u>=5){u=0,E();return}T()})},t)},E=()=>{if(_)return;b(`rediscover`);let e=Math.min(s,Math.round(p*(.85+Math.random()*.3)));p=Math.min(Math.round(p*1.5),s),_=setTimeout(()=>{_=null,w().then(e=>{e||E()})},e)};window.addEventListener(`message`,r=>{if(r.origin!==e.hostOrigin)return;let i=r.data;if(t(i)&&i.type===`webmcp.tools.changed`){c&&f&&a(c,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(t(i)&&i.type===`webmcp.connect`){!c&&v!==`discovering`&&w();return}let o=l(i);if(!o)return;let s=n.get(o.requestId);if(s){if(o.type===s.responseType){clearTimeout(s.timeoutId),n.delete(o.requestId),s.resolve(o);return}o.type===s.errorType&&(clearTimeout(s.timeoutId),n.delete(o.requestId),s.reject(Error(String(o.error||`Unknown host error`))))}}),e.autoConnect?w().then(e=>{e||E()}):b(`idle`)}u()})();
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { t as LocalRelayMcpServer, w as parseCliOptions } from "./mcpRelayServer-rbay_DSr.js";
|
|
3
3
|
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
let options;
|
|
@@ -13,7 +13,11 @@ if (options.allowedOrigins.includes("*")) process.stderr.write("[webmcp-local-re
|
|
|
13
13
|
const relay = new LocalRelayMcpServer({ bridgeOptions: {
|
|
14
14
|
host: options.host,
|
|
15
15
|
port: options.port,
|
|
16
|
-
|
|
16
|
+
portExplicitlySet: options.portExplicitlySet,
|
|
17
|
+
allowedOrigins: options.allowedOrigins,
|
|
18
|
+
...options.label ? { label: options.label } : {},
|
|
19
|
+
...options.relayId ? { relayId: options.relayId } : {},
|
|
20
|
+
...options.workspace ? { workspace: options.workspace } : {}
|
|
17
21
|
} });
|
|
18
22
|
try {
|
|
19
23
|
await relay.start();
|
|
@@ -31,7 +35,7 @@ try {
|
|
|
31
35
|
process.exit(1);
|
|
32
36
|
}
|
|
33
37
|
if (relay.bridge.mode === "server") process.stderr.write(`[webmcp-local-relay] server mode: listening on ws://${options.host}:${relay.bridge.port} (allowed origins: ${options.allowedOrigins.join(", ")})\n`);
|
|
34
|
-
else process.stderr.write(`[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${
|
|
38
|
+
else process.stderr.write(`[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\n`);
|
|
35
39
|
/**
|
|
36
40
|
* Gracefully shuts down bridge and MCP server for process termination signals.
|
|
37
41
|
*/
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":["options: ReturnType<typeof parseCliOptions>"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { parseCliOptions } from './cli-utils.js';\nimport { LocalRelayMcpServer } from './mcpRelayServer.js';\n\nlet options: ReturnType<typeof parseCliOptions>;\ntry {\n options = parseCliOptions(process.argv.slice(2));\n} catch (err) {\n process.stderr.write(\n `[webmcp-local-relay] error: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.exit(1);\n}\n\nif (options.allowedOrigins.includes('*')) {\n process.stderr.write(\n '[webmcp-local-relay] WARNING: accepting connections from ALL host page origins. Use --widget-origin to restrict.\\n'\n );\n}\n\nconst
|
|
1
|
+
{"version":3,"file":"cli.js","names":["options: ReturnType<typeof parseCliOptions>"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { parseCliOptions } from './cli-utils.js';\nimport { LocalRelayMcpServer } from './mcpRelayServer.js';\n\nlet options: ReturnType<typeof parseCliOptions>;\ntry {\n options = parseCliOptions(process.argv.slice(2));\n} catch (err) {\n process.stderr.write(\n `[webmcp-local-relay] error: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.exit(1);\n}\n\nif (options.allowedOrigins.includes('*')) {\n process.stderr.write(\n '[webmcp-local-relay] WARNING: accepting connections from ALL host page origins. Use --widget-origin to restrict.\\n'\n );\n}\n\nconst bridgeOptions = {\n host: options.host,\n port: options.port,\n portExplicitlySet: options.portExplicitlySet,\n allowedOrigins: options.allowedOrigins,\n ...(options.label ? { label: options.label } : {}),\n ...(options.relayId ? { relayId: options.relayId } : {}),\n ...(options.workspace ? { workspace: options.workspace } : {}),\n};\n\nconst relay = new LocalRelayMcpServer({\n bridgeOptions,\n});\n\ntry {\n await relay.start();\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes('EACCES')) {\n process.stderr.write(\n `[webmcp-local-relay] error: insufficient permissions to bind to ${options.host}:${options.port}.\\n`\n );\n } else {\n process.stderr.write(`[webmcp-local-relay] error: failed to start bridge server: ${message}\\n`);\n }\n process.exit(1);\n}\n\ntry {\n await relay.startStdio();\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[webmcp-local-relay] error: failed to start stdio transport: ${message}\\n`);\n process.exit(1);\n}\n\nif (relay.bridge.mode === 'server') {\n process.stderr.write(\n `[webmcp-local-relay] server mode: listening on ws://${options.host}:${relay.bridge.port} (allowed origins: ${options.allowedOrigins.join(', ')})\\n`\n );\n} else {\n process.stderr.write(\n `[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\\n`\n );\n}\n\n/**\n * Gracefully shuts down bridge and MCP server for process termination signals.\n */\nconst shutdown = async (signal: string) => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n process.stderr.write(`[webmcp-local-relay] received ${signal}, shutting down\\n`);\n try {\n await relay.stop();\n } catch (err) {\n process.stderr.write(\n `[webmcp-local-relay] error during shutdown: ${err instanceof Error ? err.message : err}\\n`\n );\n process.exit(1);\n }\n process.exit(0);\n};\n\nlet shuttingDown = false;\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT');\n});\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM');\n});\n"],"mappings":";;;;AAKA,IAAIA;AACJ,IAAI;AACF,WAAU,gBAAgB,QAAQ,KAAK,MAAM,EAAE,CAAC;SACzC,KAAK;AACZ,SAAQ,OAAO,MACb,+BAA+B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IACjF;AACD,SAAQ,KAAK,EAAE;;AAGjB,IAAI,QAAQ,eAAe,SAAS,IAAI,CACtC,SAAQ,OAAO,MACb,qHACD;AAaH,MAAM,QAAQ,IAAI,oBAAoB,EACpC,eAXoB;CACpB,MAAM,QAAQ;CACd,MAAM,QAAQ;CACd,mBAAmB,QAAQ;CAC3B,gBAAgB,QAAQ;CACxB,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;CACjD,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;CACvD,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;CAC9D,EAIA,CAAC;AAEF,IAAI;AACF,OAAM,MAAM,OAAO;SACZ,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,KAAI,QAAQ,SAAS,SAAS,CAC5B,SAAQ,OAAO,MACb,mEAAmE,QAAQ,KAAK,GAAG,QAAQ,KAAK,KACjG;KAED,SAAQ,OAAO,MAAM,8DAA8D,QAAQ,IAAI;AAEjG,SAAQ,KAAK,EAAE;;AAGjB,IAAI;AACF,OAAM,MAAM,YAAY;SACjB,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAQ,OAAO,MAAM,gEAAgE,QAAQ,IAAI;AACjG,SAAQ,KAAK,EAAE;;AAGjB,IAAI,MAAM,OAAO,SAAS,SACxB,SAAQ,OAAO,MACb,uDAAuD,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,qBAAqB,QAAQ,eAAe,KAAK,KAAK,CAAC,KACjJ;IAED,SAAQ,OAAO,MACb,6EAA6E,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,IAChH;;;;AAMH,MAAM,WAAW,OAAO,WAAmB;AACzC,KAAI,aACF;AAEF,gBAAe;AACf,SAAQ,OAAO,MAAM,iCAAiC,OAAO,mBAAmB;AAChF,KAAI;AACF,QAAM,MAAM,MAAM;UACX,KAAK;AACZ,UAAQ,OAAO,MACb,+CAA+C,eAAe,QAAQ,IAAI,UAAU,IAAI,IACzF;AACD,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,KAAK,EAAE;;AAGjB,IAAI,eAAe;AAEnB,QAAQ,GAAG,gBAAgB;AACzB,CAAK,SAAS,SAAS;EACvB;AACF,QAAQ,GAAG,iBAAiB;AAC1B,CAAK,SAAS,UAAU;EACxB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { CallToolRequestParamsSchema, CallToolResult, CallToolResult as CallToolResult$1, CallToolResultSchema, Tool, Tool as Tool$1, ToolAnnotations, ToolAnnotations as ToolAnnotations$1, ToolAnnotationsSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1
2
|
import { z } from "zod/v4";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { CallToolRequestParamsSchema, CallToolResult, CallToolResult as CallToolResult$1, CallToolResultSchema, Tool, Tool as Tool$1, ToolAnnotations, ToolAnnotations as ToolAnnotations$1, ToolAnnotationsSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
5
|
|
|
6
6
|
//#region src/protocol.d.ts
|
|
@@ -207,10 +207,53 @@ type BrowserToRelayMessage = z.infer<typeof BrowserToRelayMessageSchema>;
|
|
|
207
207
|
* Browser source bootstrap message.
|
|
208
208
|
*/
|
|
209
209
|
type BrowserHelloMessage = z.infer<typeof BrowserHelloMessageSchema>;
|
|
210
|
+
/**
|
|
211
|
+
* Shared relay identity used for discovery and attach verification.
|
|
212
|
+
*/
|
|
213
|
+
declare const RelayDescriptorSchema: z.ZodObject<{
|
|
214
|
+
host: z.ZodString;
|
|
215
|
+
instanceId: z.ZodString;
|
|
216
|
+
label: z.ZodOptional<z.ZodString>;
|
|
217
|
+
port: z.ZodNumber;
|
|
218
|
+
relayId: z.ZodOptional<z.ZodString>;
|
|
219
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
220
|
+
}, z.core.$strip>;
|
|
221
|
+
/**
|
|
222
|
+
* Relay identity used for discovery and attach verification.
|
|
223
|
+
*/
|
|
224
|
+
type RelayDescriptor = z.infer<typeof RelayDescriptorSchema>;
|
|
225
|
+
/**
|
|
226
|
+
* Schema for server hello messages sent immediately after WebSocket connect.
|
|
227
|
+
*/
|
|
228
|
+
declare const ServerHelloMessageSchema: z.ZodObject<{
|
|
229
|
+
host: z.ZodString;
|
|
230
|
+
instanceId: z.ZodString;
|
|
231
|
+
label: z.ZodOptional<z.ZodString>;
|
|
232
|
+
port: z.ZodNumber;
|
|
233
|
+
relayId: z.ZodOptional<z.ZodString>;
|
|
234
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
235
|
+
type: z.ZodLiteral<"server-hello">;
|
|
236
|
+
service: z.ZodLiteral<"webmcp-local-relay">;
|
|
237
|
+
version: z.ZodLiteral<1>;
|
|
238
|
+
}, z.core.$strip>;
|
|
239
|
+
/**
|
|
240
|
+
* Server hello payload.
|
|
241
|
+
*/
|
|
242
|
+
type ServerHelloMessage = z.infer<typeof ServerHelloMessageSchema>;
|
|
210
243
|
/**
|
|
211
244
|
* Union schema for all relay-to-browser protocol messages.
|
|
212
245
|
*/
|
|
213
246
|
declare const RelayToBrowserMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
247
|
+
host: z.ZodString;
|
|
248
|
+
instanceId: z.ZodString;
|
|
249
|
+
label: z.ZodOptional<z.ZodString>;
|
|
250
|
+
port: z.ZodNumber;
|
|
251
|
+
relayId: z.ZodOptional<z.ZodString>;
|
|
252
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
253
|
+
type: z.ZodLiteral<"server-hello">;
|
|
254
|
+
service: z.ZodLiteral<"webmcp-local-relay">;
|
|
255
|
+
version: z.ZodLiteral<1>;
|
|
256
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
214
257
|
type: z.ZodLiteral<"invoke">;
|
|
215
258
|
callId: z.ZodString;
|
|
216
259
|
toolName: z.ZodString;
|
|
@@ -266,6 +309,16 @@ type RelayClientToServerMessage = z.infer<typeof RelayClientToServerMessageSchem
|
|
|
266
309
|
* Union schema for all relay-server-to-client messages.
|
|
267
310
|
*/
|
|
268
311
|
declare const RelayServerToClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
312
|
+
host: z.ZodString;
|
|
313
|
+
instanceId: z.ZodString;
|
|
314
|
+
label: z.ZodOptional<z.ZodString>;
|
|
315
|
+
port: z.ZodNumber;
|
|
316
|
+
relayId: z.ZodOptional<z.ZodString>;
|
|
317
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
318
|
+
type: z.ZodLiteral<"server-hello">;
|
|
319
|
+
service: z.ZodLiteral<"webmcp-local-relay">;
|
|
320
|
+
version: z.ZodLiteral<1>;
|
|
321
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
269
322
|
tools: z.ZodArray<z.ZodObject<{
|
|
270
323
|
description: z.ZodOptional<z.ZodString>;
|
|
271
324
|
inputSchema: z.ZodObject<{
|
|
@@ -613,6 +666,22 @@ interface RelayBridgeServerOptions {
|
|
|
613
666
|
* @defaultValue `9333`
|
|
614
667
|
*/
|
|
615
668
|
port?: number;
|
|
669
|
+
/**
|
|
670
|
+
* Whether the preferred port came from an explicit CLI/API override.
|
|
671
|
+
* Explicit ports fail if occupied by a non-relay process.
|
|
672
|
+
* @defaultValue `false`
|
|
673
|
+
*/
|
|
674
|
+
portExplicitlySet?: boolean;
|
|
675
|
+
/**
|
|
676
|
+
* Inclusive upper bound for automatic port discovery.
|
|
677
|
+
* @defaultValue `9348`
|
|
678
|
+
*/
|
|
679
|
+
portRangeEnd?: number;
|
|
680
|
+
/**
|
|
681
|
+
* File path used to cache the last successful relay port.
|
|
682
|
+
* @defaultValue `~/.webmcp/relay-port.json`
|
|
683
|
+
*/
|
|
684
|
+
persistPath?: string;
|
|
616
685
|
/**
|
|
617
686
|
* Allowed host page origins reported by browser `hello` messages.
|
|
618
687
|
* Use `["*"]` to allow all origins.
|
|
@@ -635,6 +704,18 @@ interface RelayBridgeServerOptions {
|
|
|
635
704
|
* @defaultValue `25000`
|
|
636
705
|
*/
|
|
637
706
|
invokeTimeoutMs?: number;
|
|
707
|
+
/**
|
|
708
|
+
* Human-readable relay label reported in discovery handshakes.
|
|
709
|
+
*/
|
|
710
|
+
label?: string;
|
|
711
|
+
/**
|
|
712
|
+
* Optional workspace name reported in discovery handshakes.
|
|
713
|
+
*/
|
|
714
|
+
workspace?: string;
|
|
715
|
+
/**
|
|
716
|
+
* Stable relay identifier used to select between multiple relays.
|
|
717
|
+
*/
|
|
718
|
+
relayId?: string;
|
|
638
719
|
}
|
|
639
720
|
/**
|
|
640
721
|
* WebSocket relay between browser widget frames and MCP server calls.
|
|
@@ -652,14 +733,24 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
652
733
|
*/
|
|
653
734
|
readonly registry: RelayRegistry;
|
|
654
735
|
private readonly host;
|
|
736
|
+
private readonly preferredPort;
|
|
655
737
|
private desiredPort;
|
|
738
|
+
private readonly portExplicitlySet;
|
|
739
|
+
private readonly portRangeEnd;
|
|
740
|
+
private readonly persistPath;
|
|
656
741
|
private readonly allowedOrigins;
|
|
657
742
|
private readonly maxPayloadBytes;
|
|
658
743
|
private readonly invokeTimeoutMs;
|
|
744
|
+
private readonly label;
|
|
745
|
+
private readonly workspace;
|
|
746
|
+
private readonly relayId;
|
|
747
|
+
private readonly instanceId;
|
|
659
748
|
private wss;
|
|
660
749
|
private readonly socketByConnectionId;
|
|
661
750
|
private readonly pendingInvocations;
|
|
662
751
|
private readonly relayClientConnectionIds;
|
|
752
|
+
private readonly heartbeatIntervalByConnectionId;
|
|
753
|
+
private readonly lastPongByConnectionId;
|
|
663
754
|
private readonly onStateChangedPushRelay;
|
|
664
755
|
private _mode;
|
|
665
756
|
private clientSocket;
|
|
@@ -707,10 +798,13 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
707
798
|
getToolSourceMapFromRelay(): Record<string, string[]>;
|
|
708
799
|
/**
|
|
709
800
|
* Starts the bridge. Attempts to bind a WebSocket server (server mode).
|
|
710
|
-
* If
|
|
711
|
-
*
|
|
801
|
+
* If a compatible relay already owns a candidate port, joins it in client mode.
|
|
802
|
+
* If a non-relay process owns the port, continues searching the reserved range.
|
|
712
803
|
*/
|
|
713
804
|
start(): Promise<void>;
|
|
805
|
+
private startUsingPortStrategy;
|
|
806
|
+
private isAddressInUseError;
|
|
807
|
+
private tryAttachToExistingRelay;
|
|
714
808
|
/**
|
|
715
809
|
* Stops all relay resources and rejects any pending invocations.
|
|
716
810
|
*/
|
|
@@ -729,6 +823,7 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
729
823
|
requestTabId?: string;
|
|
730
824
|
}): Promise<RelayCallToolResult>;
|
|
731
825
|
private startAsServer;
|
|
826
|
+
private buildServerHello;
|
|
732
827
|
private invokeToolLocally;
|
|
733
828
|
/**
|
|
734
829
|
* Handles a raw WebSocket message from a connected source.
|
|
@@ -749,18 +844,20 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
749
844
|
* Handles source disconnection and rejects in-flight calls owned by that source.
|
|
750
845
|
*/
|
|
751
846
|
private onSocketClose;
|
|
847
|
+
private startHeartbeat;
|
|
848
|
+
private stopHeartbeat;
|
|
752
849
|
private startAsClient;
|
|
850
|
+
private connectToRelayServer;
|
|
753
851
|
private setupClientHandlers;
|
|
852
|
+
private sendRelayClientHandshake;
|
|
853
|
+
private parseRelayServerMessage;
|
|
854
|
+
private processRelayServerMessage;
|
|
754
855
|
private scheduleReconnect;
|
|
755
856
|
/**
|
|
756
857
|
* Attempts to promote from client to server mode when reconnecting.
|
|
757
|
-
*
|
|
858
|
+
* Re-runs the same attach-or-bind strategy used during startup.
|
|
758
859
|
*/
|
|
759
860
|
private reconnectWithModePromotion;
|
|
760
|
-
/**
|
|
761
|
-
* Reconnects as a client to an existing relay server.
|
|
762
|
-
*/
|
|
763
|
-
private reconnectAsClient;
|
|
764
861
|
private invokeToolViaRelay;
|
|
765
862
|
/**
|
|
766
863
|
* Builds the complete relay response payload including tools and source metadata.
|
|
@@ -789,7 +886,11 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
789
886
|
interface CliOptions {
|
|
790
887
|
host: string;
|
|
791
888
|
port: number;
|
|
889
|
+
portExplicitlySet: boolean;
|
|
792
890
|
allowedOrigins: string[];
|
|
891
|
+
label?: string;
|
|
892
|
+
workspace?: string;
|
|
893
|
+
relayId?: string;
|
|
793
894
|
}
|
|
794
895
|
/**
|
|
795
896
|
* Parses supported CLI flags for relay startup.
|
|
@@ -836,7 +937,9 @@ declare class LocalRelayMcpServer {
|
|
|
836
937
|
*/
|
|
837
938
|
readonly bridge: RelayBridgeServer;
|
|
838
939
|
private readonly mcpServer;
|
|
940
|
+
private readonly staticToolData;
|
|
839
941
|
private readonly dynamicToolHandles;
|
|
942
|
+
private readonly dynamicToolData;
|
|
840
943
|
private readonly dynamicToolSignature;
|
|
841
944
|
private syncing;
|
|
842
945
|
private syncRequested;
|
|
@@ -867,6 +970,18 @@ declare class LocalRelayMcpServer {
|
|
|
867
970
|
* Returns dynamic tool names currently registered in MCP.
|
|
868
971
|
*/
|
|
869
972
|
listDynamicToolNames(): string[];
|
|
973
|
+
/**
|
|
974
|
+
* Overrides the SDK's ListTools handler to return real JSON Schema
|
|
975
|
+
* for dynamic tools instead of the empty `z.object({}).passthrough()`
|
|
976
|
+
* schema that the SDK would otherwise convert to `{ type: 'object' }`.
|
|
977
|
+
*
|
|
978
|
+
* The MCP SDK's McpServer internally runs `toJsonSchemaCompat()` on
|
|
979
|
+
* registered inputSchema values, which strips plain JSON Schema objects
|
|
980
|
+
* (non-Zod) to empty schemas. This override bypasses that conversion
|
|
981
|
+
* for dynamic (relayed) tools while preserving static tools as-is.
|
|
982
|
+
*/
|
|
983
|
+
private overrideListToolsHandler;
|
|
984
|
+
private registerStaticTool;
|
|
870
985
|
/**
|
|
871
986
|
* Registers built-in management tools exposed by the relay.
|
|
872
987
|
*/
|
|
@@ -940,5 +1055,5 @@ declare function buildPublicToolName(options: {
|
|
|
940
1055
|
disambiguate?: boolean;
|
|
941
1056
|
}): string;
|
|
942
1057
|
//#endregion
|
|
943
|
-
export { type AggregatedTool, type BrowserToRelayMessage, BrowserToRelayMessageSchema, CallToolRequestParamsSchema, type CallToolResult, CallToolResultSchema, type CliOptions, DEFAULT_TOOL_INPUT_SCHEMA, HelloRequiredError, InboundToolSchema, LocalRelayMcpServer, type LocalRelayMcpServerOptions, NormalizedToolSchema, RelayBridgeServer, type RelayBridgeServerOptions, type RelayCallToolResult, type RelayClientToServerMessage, RelayClientToServerMessageSchema, type RelayInvokeArgs, RelayInvokeArgsSchema, RelayRegistry, type RelayServerToClientMessage, RelayServerToClientMessageSchema, type RelaySourceInfo, RelaySourceInfoSchema, type RelayToBrowserMessage, RelayToBrowserMessageSchema, type RelayTool, type RelayToolAnnotations, type ResolvedInvocation, type SourceInfo, type Tool, type ToolAnnotations, ToolAnnotationsSchema, ToolSchema, buildPublicToolName, extractSanitizedDomain, normalizeInboundTool, parseCliOptions, printHelp, sanitizeName };
|
|
1058
|
+
export { type AggregatedTool, type BrowserToRelayMessage, BrowserToRelayMessageSchema, CallToolRequestParamsSchema, type CallToolResult, CallToolResultSchema, type CliOptions, DEFAULT_TOOL_INPUT_SCHEMA, HelloRequiredError, InboundToolSchema, LocalRelayMcpServer, type LocalRelayMcpServerOptions, NormalizedToolSchema, RelayBridgeServer, type RelayBridgeServerOptions, type RelayCallToolResult, type RelayClientToServerMessage, RelayClientToServerMessageSchema, type RelayDescriptor, RelayDescriptorSchema, type RelayInvokeArgs, RelayInvokeArgsSchema, RelayRegistry, type RelayServerToClientMessage, RelayServerToClientMessageSchema, type RelaySourceInfo, RelaySourceInfoSchema, type RelayToBrowserMessage, RelayToBrowserMessageSchema, type RelayTool, type RelayToolAnnotations, type ResolvedInvocation, type ServerHelloMessage, ServerHelloMessageSchema, type SourceInfo, type Tool, type ToolAnnotations, ToolAnnotationsSchema, ToolSchema, buildPublicToolName, extractSanitizedDomain, normalizeInboundTool, parseCliOptions, printHelp, sanitizeName };
|
|
944
1059
|
//# sourceMappingURL=index.d.ts.map
|