@mcp-b/webmcp-local-relay 2.0.12 → 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.
@@ -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 { S as parseCliOptions, t as LocalRelayMcpServer } from "./mcpRelayServer-DAp7h9b1.js";
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
- allowedOrigins: options.allowedOrigins
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}:${options.port}\n`);
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 relay = new LocalRelayMcpServer({\n bridgeOptions: {\n host: options.host,\n port: options.port,\n allowedOrigins: options.allowedOrigins,\n },\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}:${options.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;AAGH,MAAM,QAAQ,IAAI,oBAAoB,EACpC,eAAe;CACb,MAAM,QAAQ;CACd,MAAM,QAAQ;CACd,gBAAgB,QAAQ;CACzB,EACF,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,QAAQ,KAAK,IAC3G;;;;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"}
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"}