@mcp-b/webmcp-local-relay 5.1.0 → 6.0.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/README.md CHANGED
@@ -56,15 +56,18 @@ Tools that require MCP task execution are omitted, and multi-round `input_requir
56
56
  Add one script tag to expose your page's WebMCP tools to the relay:
57
57
 
58
58
  ```html
59
- <script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"></script>
59
+ <script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"></script>
60
60
  ```
61
61
 
62
62
  That's it. If your page already registers tools on `document.modelContext`, they'll be picked up automatically.
63
63
 
64
+ Keep the major version in CDN URLs so the embed and the page runtime upgrade
65
+ together. Pages that still load a 5.x runtime pin `@5` for both scripts.
66
+
64
67
  New to WebMCP? Here's the full setup:
65
68
 
66
69
  ```html
67
- <script src="https://cdn.jsdelivr.net/npm/@mcp-b/global@latest/dist/index.iife.js"></script>
70
+ <script src="https://cdn.jsdelivr.net/npm/@mcp-b/global@6/dist/index.iife.js"></script>
68
71
  <script>
69
72
  void document.modelContext
70
73
  .registerTool({
@@ -75,14 +78,14 @@ New to WebMCP? Here's the full setup:
75
78
  })
76
79
  .catch(console.error);
77
80
  </script>
78
- <script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"></script>
81
+ <script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"></script>
79
82
  ```
80
83
 
81
84
  Custom relay port:
82
85
 
83
86
  ```html
84
87
  <script
85
- src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"
88
+ src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"
86
89
  data-relay-port="9444"
87
90
  ></script>
88
91
  ```
@@ -92,7 +95,7 @@ several slow API calls and might exceed one minute:
92
95
 
93
96
  ```html
94
97
  <script
95
- src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"
98
+ src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"
96
99
  data-request-timeout="120000"
97
100
  ></script>
98
101
  ```
@@ -230,19 +233,26 @@ After a disconnect, the widget retries the last endpoint once after about `500ms
230
233
  Supported page runtimes:
231
234
 
232
235
  1. `@mcp-b/global` (recommended for the complete MCP-B runtime)
233
- 2. Current native Chrome with `document.modelContext.getTools()` and its descriptor-based `executeTool()` extension, which this relay requires to invoke tools
236
+ 2. Current native Chrome with `document.modelContext.getTools()` and object-input `executeTool()`, which this relay requires to invoke tools
234
237
  3. `@mcp-b/webmcp-polyfill`
235
238
 
236
239
  Runtime dispatch behavior in the browser embed/widget layer:
237
240
 
238
241
  - Uses asynchronous `document.modelContext.getTools()` and the exact returned
239
- descriptor with feature-detected `executeTool()`.
242
+ descriptor with `executeTool()` and object input, as defined by the current
243
+ upstream API.
240
244
  - Refreshes the descriptor before every invocation so Chrome never receives a
241
245
  stale registration object.
246
+ - Relays the page's own tools plus tools registered by same-origin descendant
247
+ frames. When names collide, the page's own tool wins and the embed logs one
248
+ warning per name.
249
+ - Converts only results that parse to a JSON object into structured content.
250
+ A JSON string arrives as its content; any other result, such as `10.50`,
251
+ keeps its original text.
242
252
 
243
253
  ### WebMCP Standard Status
244
254
 
245
- WebMCP is an emerging web platform proposal. This relay works with the current native Chrome preview and MCP-B runtimes, but native extension details can still change as implementations mature.
255
+ WebMCP is an emerging web platform proposal. This relay works with the current native Chrome preview and MCP-B runtimes, but the browser API can still change as implementations mature.
246
256
 
247
257
  - [W3C WebML CG draft](https://webmachinelearning.github.io/webmcp/)
248
258
  - [Proposal repository](https://github.com/webmachinelearning/webmcp)
@@ -256,13 +266,13 @@ For Chromium/Chrome Canary native preview testing:
256
266
 
257
267
  ### Troubleshooting
258
268
 
259
- | Problem | Fix |
260
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
261
- | `No sources connected` | Ensure the page loaded `embed.js` and the relay process is running |
262
- | `No tools listed` | Ensure tools are registered on the page's WebMCP runtime. If tools register after load, confirm your runtime emits the `toolchange` event |
263
- | `Tool not found` | Tab reloaded or disconnected — call `webmcp_list_tools` again to refresh |
264
- | Connection blocked | Verify `--widget-origin` matches your host page's origin (e.g., `https://myapp.com`), and relay port matches `data-relay-port` |
265
- | `Host response timeout:` | The host page exceeded its timeout (default 60s). Raise `data-request-timeout` and keep CLI `--invoke-timeout` slightly higher |
269
+ | Problem | Fix |
270
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
271
+ | `No sources connected` | Ensure the page loaded `embed.js` and the relay process is running |
272
+ | `No tools listed` | Ensure tools are registered on the page. The embed listens for changes and polls every two seconds, so load order does not matter. |
273
+ | `Tool not found` | Tab reloaded or disconnected — call `webmcp_list_tools` again to refresh |
274
+ | Connection blocked | Verify `--widget-origin` matches your host page's origin (e.g., `https://myapp.com`), and relay port matches `data-relay-port` |
275
+ | `Host response timeout:` | The host page exceeded its timeout (default 60s). Raise `data-request-timeout` and keep CLI `--invoke-timeout` slightly higher |
266
276
 
267
277
  ---
268
278
 
@@ -1,2 +1,2 @@
1
- (function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(e){return n(e)?e:void 0}function i(t){return e(t)&&Array.isArray(t.content)}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t=r(e);return{content:[{type:`text`,text:a(e)}],...t===void 0?{}:{structuredContent:t},isError:!1}}function s(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function c(){return crypto.randomUUID()}let l=`[data-webmcp-relay]`,u=`__webmcp_relay_tab_id`,d=null,f;function p(){return document.currentScript instanceof HTMLScriptElement?document.currentScript:null}let m=p(),h=m?m.hasAttribute(`data-debug`):!1;function g(...e){h&&console.warn(`[webmcp-relay-embed]`,...e)}function _(){try{let e=sessionStorage.getItem(u);if(e)return e}catch(e){g(`sessionStorage read failed, tab ID will not persist:`,e)}let e=c();try{sessionStorage.setItem(u,e)}catch(e){g(`sessionStorage write failed:`,e)}return e}function v(e){if(!e?.src)throw Error(`The relay embed script must be loaded from a URL`);return new URL(`widget.html`,e.src).href}function y(e){let t=v(e),n=e?.getAttribute(`data-relay-id`)||void 0,r=e?.getAttribute(`data-relay-workspace`)||void 0,i=e?.getAttribute(`data-request-timeout`)||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}:{},...i?{requestTimeout:i}:{},tabId:_(),widgetUrl:t,widgetOrigin:new URL(t).origin}}function b(e){return s(e)?e:(e!=null&&g(`Tool invocation args must be an object, got`,typeof e),{})}function x(e){let t;if(e.inputSchema!==void 0)if(typeof e.inputSchema==`string`)try{t=JSON.parse(e.inputSchema)}catch{return console.warn(`[webmcp-relay-embed] Tool "${e.name}" was not relayed because its input schema is malformed.`),null}else t=e.inputSchema;return{name:e.name,...e.title===void 0?{}:{title:e.title},description:e.description,...t===void 0?{}:{inputSchema:t},...e.annotations===void 0?{}:{annotations:e.annotations}}}function S(e){if(e===null)return{isError:!0,content:[{type:`text`,text:`Tool execution interrupted by navigation`}]};let t;try{t=JSON.parse(e)}catch{t=e}return s(t)&&t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t)}function C(e){return!!(e&&`executeTool`in e&&typeof e.executeTool==`function`)}function w(){let e=document.modelContext;return C(e)?e:void 0}async function T(){let e=w();return e?(await e.getTools()).map(x).filter(e=>e!==null):[]}async function E(e,t){let n=w();if(!n)throw Error(`No executable WebMCP runtime found on this page`);let r=(await n.getTools()).find(t=>t.name===e);if(!r)throw Error(`Tool not found: ${e}`);return S(await n.executeTool(r,JSON.stringify(t)))}let D=!1,O=0,k=null,A=``;function j(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`undefined`;if(Array.isArray(e))return`[${e.map(j).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${j(t[e])}`).join(`,`)}}`}function M(e){return e.map(j).sort().join(`
2
- `)}function N(){D=!1;let e=O;T().then(t=>{if(e!==O)return;let n=M(t);n===A||!d||(A=n,d.postMessage({type:`webmcp.tools.changed`,tools:t},f.widgetOrigin))}).catch(e=>{g(`Failed to sync tool changes:`,e)})}function P(){O++,!D&&(D=!0,setTimeout(N,0))}function F(){k||(k=setInterval(P,2e3))}function I(){try{let e=document.modelContext;return e?(e.addEventListener(`toolchange`,P),!0):!1}catch(e){return g(`addEventListener on modelContext threw:`,e),!1}}function L(){if(F(),P(),I())return;let e=0,t=100,n=()=>{setTimeout(()=>{if(e++,!I()){if(e>=40){g(`Could not subscribe to tool changes after 40 retries. Dynamic tool updates will rely on polling.`);return}t=Math.min(Math.round(t*1.5),1e3),n()}},t)};n()}function R(e,t,n){!e||typeof e!=`object`||!(`postMessage`in e)||e.postMessage(n,t)}function z(e){return!s(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,toolName:e.toolName,args:e.args}}function B(e,t){T().then(n=>{R(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:n})}).catch(n=>{g(`Failed to list tools:`,n),R(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 V(e,t){E(String(e.toolName??``),b(e.args)).then(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.response`,requestId:e.requestId,result:s(n)?n:{}})}).catch(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.error`,requestId:e.requestId,error:String(n instanceof Error?n.message:n)})})}async function H(e){if(document.querySelector(l))return;let t=new URLSearchParams;t.set(`tabId`,e.tabId),t.set(`hostOrigin`,window.location.origin);let n=new URL(window.location.href);n.search=``,n.hash=``,t.set(`hostUrl`,n.href),t.set(`hostTitle`,document.title||``),t.set(`relayHost`,e.relayHost),t.set(`relayPort`,e.relayPort),t.set(`autoConnect`,e.autoConnect?`true`:`false`),e.relayId&&t.set(`relayId`,e.relayId),e.relayWorkspace&&t.set(`relayWorkspace`,e.relayWorkspace),e.requestTimeout&&t.set(`requestTimeout`,e.requestTimeout);let r=await fetch(e.widgetUrl);if(!r.ok)throw Error(`Widget HTML request failed with status ${String(r.status)}`);let i=await r.text(),a=`<script>window.__WEBMCP_RELAY_CONFIG=${JSON.stringify(Object.fromEntries(t))};<\/script>`,o=URL.createObjectURL(new Blob([i.replace(`</head>`,`${a}</head>`)],{type:`text/html`}));e.widgetOrigin=window.location.origin;let s=document.createElement(`iframe`);s.src=o,s.style.display=`none`,s.setAttribute(`aria-hidden`,`true`),s.setAttribute(`data-webmcp-relay`,`1`),s.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(s),d=s.contentWindow,s.addEventListener(`load`,()=>{d=s.contentWindow,URL.revokeObjectURL(o)}),s.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,s.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(o)})}if(!document.querySelector(l)){try{f=y(m)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,e=>{if(e.origin!==f.widgetOrigin||!d||e.source!==d)return;let t=e.data;if(s(t)&&t.type===`webmcp.reload`){window.location.reload();return}let n=z(e.data);if(n){if(n.type===`webmcp.tools.list.request`){B(n,e);return}n.type===`webmcp.tools.invoke.request`&&V(n,e)}});let e=()=>{H(f).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?e():document.addEventListener(`DOMContentLoaded`,e,{once:!0}),L(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&d&&d.postMessage({type:`webmcp.connect`},f.widgetOrigin)})}})();
1
+ (function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(t){if(!e(t))return!1;switch(t.type){case`text`:return typeof t.text==`string`;case`image`:case`audio`:return typeof t.data==`string`&&typeof t.mimeType==`string`;case`resource_link`:return typeof t.uri==`string`&&typeof t.name==`string`;case`resource`:{let n=t.resource;return e(n)&&typeof n.uri==`string`&&(typeof n.text==`string`||typeof n.blob==`string`)}default:return!1}}function i(t){return e(t)&&n(t)&&Array.isArray(t.content)&&t.content.every(r)&&(t.isError===void 0||typeof t.isError==`boolean`)&&(t._meta===void 0||e(t._meta))}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t={content:[{type:`text`,text:a(e)}],isError:!1};return n(e)&&(t.structuredContent=e),t}let s=new Set;function c(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function l(e){return c(e)&&typeof e.requestId==`string`&&typeof e.type==`string`}function u(e){let t;try{t=JSON.parse(e)}catch{}return c(t)?t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t):o(typeof t==`string`?t:e)}function d(e){let t=new Map;for(let n of e){let e=t.get(n.name);e&&(s.has(n.name)||(s.add(n.name),console.warn(`[webmcp-relay-embed] More than one frame registered a tool named "${n.name}". Only one is relayed, preferring this page's own tool.`)),e.window===window||n.window!==window)||t.set(n.name,n)}return[...t.values()]}let f=`[data-webmcp-relay]`,p=`__webmcp_relay_tab_id`,m=null,h=document.currentScript instanceof HTMLScriptElement?document.currentScript:null,g=h?h.hasAttribute(`data-debug`):!1;function _(...e){g&&console.warn(`[webmcp-relay-embed]`,...e)}function v(){try{let e=sessionStorage.getItem(p);if(e)return e}catch(e){_(`sessionStorage read failed, tab ID will not persist:`,e)}let e=crypto.randomUUID();try{sessionStorage.setItem(p,e)}catch(e){_(`sessionStorage write failed:`,e)}return e}function y(e){if(!e?.src)throw Error(`The relay embed script must be loaded from a URL`);return{autoConnect:String(e.getAttribute(`data-auto-connect`)!==`false`),relayHost:e.getAttribute(`data-relay-host`)||`127.0.0.1`,relayPort:e.getAttribute(`data-relay-port`)||`9333`,relayId:e.getAttribute(`data-relay-id`)||void 0,relayWorkspace:e.getAttribute(`data-relay-workspace`)||void 0,requestTimeout:e.getAttribute(`data-request-timeout`)||void 0,tabId:v(),widgetUrl:new URL(`widget.html`,e.src).href}}function b(e){if(e.inputSchema!==void 0&&!c(e.inputSchema))return console.warn(`[webmcp-relay-embed] Tool "${e.name}" was not relayed because its input schema is malformed.`),null;let t={name:e.name,title:e.title,description:e.description};return e.inputSchema!==void 0&&(t.inputSchema=e.inputSchema),e.annotations!==void 0&&(t.annotations=e.annotations),t}async function x(){let e=document.modelContext;return e?d(await e.getTools()).map(b).filter(e=>e!==null):[]}async function S(e,t){let n=document.modelContext;if(!n)throw Error(`No executable WebMCP runtime found on this page`);let r=d(await n.getTools()).find(t=>t.name===e);if(!r)throw Error(`Tool not found: ${e}`);return u(await n.executeTool(r,t))}function C(e){return c(e)?e:(e!=null&&_(`Tool invocation args must be an object; using empty input`),{})}let w=!1,T=0,E=null,D=``;function O(e){return typeof e!=`object`||!e?JSON.stringify(e)??`undefined`:Array.isArray(e)?`[${e.map(O).join(`,`)}]`:`{${Object.entries(e).sort(([e],[t])=>e<t?-1:+(e>t)).map(([e,t])=>`${JSON.stringify(e)}:${O(t)}`).join(`,`)}}`}function k(e){return e.map(O).sort().join(`
2
+ `)}function A(){w=!1;let e=T;x().then(t=>{if(e!==T)return;let n=k(t);n===D||!m||(D=n,m.postMessage({type:`webmcp.tools.changed`,tools:t},window.location.origin))}).catch(e=>{_(`Failed to sync tool changes:`,e)})}function j(){T++,!w&&(w=!0,setTimeout(A,0))}function M(){E||(E=setInterval(j,2e3))}function N(){try{let e=document.modelContext;return e?(e.addEventListener(`toolchange`,j),!0):!1}catch(e){return _(`addEventListener on modelContext threw:`,e),!1}}function P(){if(M(),j(),N())return;let e=0,t=100,n=()=>{setTimeout(()=>{if(e++,!N()){if(e>=40){_(`Could not subscribe to tool changes after 40 retries. Dynamic tool updates will rely on polling.`);return}t=Math.min(Math.round(t*1.5),1e3),n()}},t)};n()}function F(e,t){x().then(n=>{e.postMessage({type:`webmcp.tools.list.response`,requestId:t,tools:n},window.location.origin)}).catch(n=>{_(`Failed to list tools:`,n),e.postMessage({type:`webmcp.tools.list.response`,requestId:t,tools:[],error:`Failed to list tools: ${n instanceof Error?n.message:String(n)}`},window.location.origin)})}function I(e,t){let{requestId:n}=t;S(String(t.toolName??``),C(t.args)).then(t=>{e.postMessage({type:`webmcp.tools.invoke.response`,requestId:n,result:t},window.location.origin)}).catch(t=>{e.postMessage({type:`webmcp.tools.invoke.error`,requestId:n,error:String(t instanceof Error?t.message:t)},window.location.origin)})}async function L({widgetUrl:e,...t}){if(document.querySelector(f))return;let n=new URL(window.location.href);n.search=``,n.hash=``;let r=JSON.stringify({...t,hostOrigin:window.location.origin,hostUrl:n.href,hostTitle:document.title}).replace(/</g,`\\u003c`),i=await fetch(e);if(!i.ok)throw Error(`Widget HTML request failed with status ${String(i.status)}`);let a=await i.text(),o=`<script>window.__WEBMCP_RELAY_CONFIG=${r};<\/script>`,s=URL.createObjectURL(new Blob([a.replace(`</head>`,()=>`${o}</head>`)],{type:`text/html`})),c=document.createElement(`iframe`);c.src=s,c.style.display=`none`,c.setAttribute(`aria-hidden`,`true`),c.setAttribute(`data-webmcp-relay`,`1`),c.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(c),m=c.contentWindow,c.addEventListener(`load`,()=>{m=c.contentWindow,URL.revokeObjectURL(s)}),c.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,c.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(s)})}if(!document.querySelector(f)){let e;try{e=y(h)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,e=>{let t=m;if(e.origin!==window.location.origin||!t||e.source!==t)return;let n=e.data;if(c(n)&&n.type===`webmcp.reload`){window.location.reload();return}l(n)&&(n.type===`webmcp.tools.list.request`?F(t,n.requestId):n.type===`webmcp.tools.invoke.request`&&I(t,n))});let t=()=>{L(e).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?t():document.addEventListener(`DOMContentLoaded`,t,{once:!0}),P(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&m&&m.postMessage({type:`webmcp.connect`},window.location.origin)})}})();
@@ -5,6 +5,6 @@
5
5
  <title>WebMCP Relay Widget</title>
6
6
  </head>
7
7
  <body>
8
- <script>(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(t){return e.has(t)}function i(){return crypto.randomUUID()}function a(e){return String(e).replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),a=globalThis.__WEBMCP_RELAY_CONFIG;function o(e){return n.get(e)??a?.[e]??null}let s=o(`hostOrigin`);if(!s)return null;let c=o(`hostUrl`)||s,l=o(`hostTitle`)||``,u=o(`tabId`)||i(),d=o(`relayHost`)||`127.0.0.1`,f=o(`relayPort`),p=f&&f.length>0?Number(f):t,m=o(`autoConnect`)!==`false`,h=o(`relayId`)||void 0,g=o(`relayWorkspace`)||void 0,_=o(`requestTimeout`),v=_&&_.length>0?Number(_):6e4;return r(d)?!Number.isInteger(p)||p<1||p>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,f),null):!Number.isInteger(v)||v<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,_),null):{autoConnect:m,hostOrigin:s,hostTitle:l,hostUrl:c,relayHostHint:d,relayPortHint:p,...h?{relayId:h}:{},...g?{relayWorkspace:g}:{},requestTimeoutMs:v,tabId:u}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,d),null)}function u(e){return!n(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 d(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}b(e)}function f(e){return!n(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 p(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function m(e){return s({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function h(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(m(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 g(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(m(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function _(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(m(e))}catch{}}function v(e){let n=h(e),r=new Set,i=[],a=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};e.relayPortHint!==void 0&&a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function y(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`: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=f(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 b(e){let t=new Map,r=null,s=null,l=!1,d=null,m=null,b=`idle`,x=0,S=null,C=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function w(n,r){let a=i();return new Promise((i,o)=>{let s=setTimeout(()=>{t.delete(a),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(a,{resolve:i,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:a,...r},e.hostOrigin)})}let T=(t,i)=>{let c=[],u=()=>{d&&(clearTimeout(d),d=null)},h=()=>{o(t,JSON.stringify({type:`tools/list`,tools:c}))};m&&(clearTimeout(m),m=null),r=i,s=t,x=0,l=!1,b=`idle`,t.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 d=f(c);if(d){r={hello:d,host:i.host,port:i.port};return}if(n(c)&&c.type===`hello/accepted`){u(),l=!0,g(e,i),h();return}let m=p(c);if(m){u(),l=!1,_(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:i.host,port:i.port,message:m.message,reason:m.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,m.reason,m.message);try{t.close(1008,m.message)}catch{}return}if(!n(c)||typeof c.type!=`string`)return;let v=c;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(v.type));return}w(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{s===t&&(l=!1,s=null,u(),C(`WebSocket connection lost`),O())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),w(`webmcp.tools.list`,{}).then(n=>{c=Array.isArray(n.tools)?n.tools:[],o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),d=setTimeout(()=>{d=null,!(s!==t||t.readyState!==WebSocket.OPEN||l)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},E=async t=>{let n=await y({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):(T(n.socket,r),!0)},D=async()=>{b=`discovering`;try{for(let t of v(e))if(await E(t))return!0;return!1}finally{b===`discovering`&&(b=`idle`)}},O=()=>{if(!r||m)return;let e={host:r.host,port:r.port},t=Math.round(500*(.85+Math.random()*.3));m=setTimeout(()=>{m=null,E(e).then(e=>{e||k()})},t)},k=()=>{if(m)return;let e=c[x];if(e===void 0){M();return}m=setTimeout(()=>{m=null,D().then(e=>{e||(x+=1,k())})},e)},A=()=>{document.visibilityState===`visible`&&b===`dormant`&&P()},j=()=>{document.removeEventListener(`visibilitychange`,A),S!==null&&(clearInterval(S),S=null)},M=()=>{b!==`dormant`&&(b=`dormant`,m&&(clearTimeout(m),m=null),document.addEventListener(`visibilitychange`,A),S=setInterval(()=>{N()},12e4))},N=async()=>{if(b!==`dormant`)return;let t=new Set,n=[],r=(e,r)=>{let i=`${e}:${String(r)}`;t.has(i)||(t.add(i),n.push({host:e,port:r}))};e.relayPortHint!==void 0&&r(e.relayHostHint,e.relayPortHint);let i=h(e);if(i&&r(i.host,i.port),n.length!==0){b=`discovering`,j();for(let e of n)if(await E(e)){x=0;return}M()}},P=()=>{j(),x=0,D().then(e=>{e||M()})};window.addEventListener(`message`,r=>{if(r.source!==window.parent||r.origin!==e.hostOrigin)return;let i=r.data;if(n(i)&&i.type===`webmcp.tools.changed`){s&&l&&o(s,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(n(i)&&i.type===`webmcp.connect`){b===`dormant`?P():!s&&b!==`discovering`&&D();return}let a=u(i);if(!a)return;let c=t.get(a.requestId);if(c){if(a.type===c.responseType){clearTimeout(c.timeoutId),t.delete(a.requestId),c.resolve(a);return}a.type===c.errorType&&(clearTimeout(c.timeoutId),t.delete(a.requestId),c.reject(Error(String(a.error||`Unknown host error`))))}}),e.autoConnect&&D().then(e=>{e||k()})}d()})();</script>
8
+ <script>(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(e){return n(e)&&typeof e.requestId==`string`&&typeof e.type==`string`}function i(t){return e.has(t)}function a(e){return e.replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),r=globalThis.__WEBMCP_RELAY_CONFIG;function a(e){return n.get(e)??r?.[e]??null}let o=a(`hostOrigin`);if(!o)return null;let s=a(`relayHost`)||`127.0.0.1`;if(!i(s))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,s),null;let c=a(`relayPort`),l=c?Number(c):t;if(!Number.isInteger(l)||l<1||l>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,c),null;let u=a(`requestTimeout`),d=u?Number(u):6e4;return!Number.isInteger(d)||d<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,u),null):{autoConnect:a(`autoConnect`)!==`false`,hostOrigin:o,hostTitle:a(`hostTitle`)||``,hostUrl:a(`hostUrl`)||o,relayHostHint:s,relayId:a(`relayId`)||void 0,relayPortHint:l,relayWorkspace:a(`relayWorkspace`)||void 0,requestTimeoutMs:d,tabId:a(`tabId`)||crypto.randomUUID()}}function u(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}y(e)}function d(e){if(!n(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`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function f(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function p(e){return s({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function m(e){try{let t=sessionStorage.getItem(p(e));if(!t)return null;let r=JSON.parse(t);return!n(r)||typeof r.host!=`string`||r.host.length===0||typeof r.port!=`number`||!Number.isInteger(r.port)||r.port<1||r.port>65535?null:{host:r.host,port:r.port}}catch{return null}}function h(e,t){try{sessionStorage.setItem(p(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function g(e){try{sessionStorage.removeItem(p(e))}catch{}}function _(e){let n=m(e),r=new Set,i=[],a=(e,t)=>{let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function v(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`: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 y(e){let t=new Map,i=[],s=0,l=null,u=null,p=!1,y=null,b=null,x=`idle`,S=0,C=null,w=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function T(n,r){let i=crypto.randomUUID();return new Promise((a,o)=>{let s=setTimeout(()=>{t.delete(i),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(i,{resolve:a,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:i,...r},e.hostOrigin)})}let E=(t,r)=>{let c=s,m=()=>{y&&(clearTimeout(y),y=null)},_=()=>{o(t,JSON.stringify({type:`tools/list`,tools:i}))};b&&(clearTimeout(b),b=null),l=r,u=t,S=0,p=!1,x=`idle`,t.addEventListener(`message`,i=>{let s;try{s=JSON.parse(String(i.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=d(s);if(c){l={hello:c,host:r.host,port:r.port};return}if(n(s)&&s.type===`hello/accepted`){m(),p=!0,h(e,r),_();return}let u=f(s);if(u){m(),p=!1,g(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:r.host,port:r.port,message:u.message,reason:u.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,u.reason,u.message);try{t.close(1008,u.message)}catch{}return}if(!n(s)||typeof s.type!=`string`)return;let v=s;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(s.type));return}T(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{u===t&&(p=!1,u=null,m(),w(`WebSocket connection lost`),k())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),T(`webmcp.tools.list`,{}).then(n=>{s===c&&(i=Array.isArray(n.tools)?n.tools:[]),o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),y=setTimeout(()=>{y=null,!(u!==t||t.readyState!==WebSocket.OPEN||p)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},D=async t=>{let n=await v({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):(E(n.socket,r),!0)},O=async()=>{x=`discovering`;try{for(let t of _(e))if(await D(t))return!0;return!1}finally{x===`discovering`&&(x=`idle`)}},k=()=>{if(!l||b)return;let e={host:l.host,port:l.port},t=Math.round(500*(.85+Math.random()*.3));b=setTimeout(()=>{b=null,D(e).then(e=>{e||A()})},t)},A=()=>{if(b)return;let e=c[S];if(e===void 0){N();return}b=setTimeout(()=>{b=null,O().then(e=>{e||(S+=1,A())})},e)},j=()=>{document.visibilityState===`visible`&&x===`dormant`&&F()},M=()=>{document.removeEventListener(`visibilitychange`,j),C!==null&&(clearInterval(C),C=null)},N=()=>{x!==`dormant`&&(x=`dormant`,b&&(clearTimeout(b),b=null),document.addEventListener(`visibilitychange`,j),C=setInterval(()=>{P()},12e4))},P=async()=>{if(x!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=m(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),x=`discovering`,M();for(let e of t)if(await D(e)){S=0;return}N()},F=()=>{M(),S=0,O().then(e=>{e||N()})};window.addEventListener(`message`,a=>{if(a.source!==window.parent||a.origin!==e.hostOrigin)return;let c=a.data;if(n(c)&&c.type===`webmcp.tools.changed`){i=Array.isArray(c.tools)?c.tools:[],s++,u&&p&&o(u,JSON.stringify({type:`tools/changed`,tools:i}));return}if(n(c)&&c.type===`webmcp.connect`){x===`dormant`?F():!u&&x!==`discovering`&&O();return}if(!r(c))return;let l=t.get(c.requestId);!l||c.type!==l.responseType&&c.type!==l.errorType||(clearTimeout(l.timeoutId),t.delete(c.requestId),c.type===l.responseType?l.resolve(c):l.reject(Error(String(c.error||`Unknown host error`))))}),e.autoConnect&&O().then(e=>{e||A()})}u()})();</script>
9
9
  </body>
10
10
  </html>
@@ -1 +1 @@
1
- (function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(t){return e.has(t)}function i(){return crypto.randomUUID()}function a(e){return String(e).replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),a=globalThis.__WEBMCP_RELAY_CONFIG;function o(e){return n.get(e)??a?.[e]??null}let s=o(`hostOrigin`);if(!s)return null;let c=o(`hostUrl`)||s,l=o(`hostTitle`)||``,u=o(`tabId`)||i(),d=o(`relayHost`)||`127.0.0.1`,f=o(`relayPort`),p=f&&f.length>0?Number(f):t,m=o(`autoConnect`)!==`false`,h=o(`relayId`)||void 0,g=o(`relayWorkspace`)||void 0,_=o(`requestTimeout`),v=_&&_.length>0?Number(_):6e4;return r(d)?!Number.isInteger(p)||p<1||p>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,f),null):!Number.isInteger(v)||v<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,_),null):{autoConnect:m,hostOrigin:s,hostTitle:l,hostUrl:c,relayHostHint:d,relayPortHint:p,...h?{relayId:h}:{},...g?{relayWorkspace:g}:{},requestTimeoutMs:v,tabId:u}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,d),null)}function u(e){return!n(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 d(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}b(e)}function f(e){return!n(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 p(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function m(e){return s({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function h(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(m(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 g(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(m(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function _(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(m(e))}catch{}}function v(e){let n=h(e),r=new Set,i=[],a=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};e.relayPortHint!==void 0&&a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function y(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`: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=f(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 b(e){let t=new Map,r=null,s=null,l=!1,d=null,m=null,b=`idle`,x=0,S=null,C=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function w(n,r){let a=i();return new Promise((i,o)=>{let s=setTimeout(()=>{t.delete(a),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(a,{resolve:i,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:a,...r},e.hostOrigin)})}let T=(t,i)=>{let c=[],u=()=>{d&&(clearTimeout(d),d=null)},h=()=>{o(t,JSON.stringify({type:`tools/list`,tools:c}))};m&&(clearTimeout(m),m=null),r=i,s=t,x=0,l=!1,b=`idle`,t.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 d=f(c);if(d){r={hello:d,host:i.host,port:i.port};return}if(n(c)&&c.type===`hello/accepted`){u(),l=!0,g(e,i),h();return}let m=p(c);if(m){u(),l=!1,_(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:i.host,port:i.port,message:m.message,reason:m.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,m.reason,m.message);try{t.close(1008,m.message)}catch{}return}if(!n(c)||typeof c.type!=`string`)return;let v=c;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(v.type));return}w(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{s===t&&(l=!1,s=null,u(),C(`WebSocket connection lost`),O())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),w(`webmcp.tools.list`,{}).then(n=>{c=Array.isArray(n.tools)?n.tools:[],o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),d=setTimeout(()=>{d=null,!(s!==t||t.readyState!==WebSocket.OPEN||l)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},E=async t=>{let n=await y({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):(T(n.socket,r),!0)},D=async()=>{b=`discovering`;try{for(let t of v(e))if(await E(t))return!0;return!1}finally{b===`discovering`&&(b=`idle`)}},O=()=>{if(!r||m)return;let e={host:r.host,port:r.port},t=Math.round(500*(.85+Math.random()*.3));m=setTimeout(()=>{m=null,E(e).then(e=>{e||k()})},t)},k=()=>{if(m)return;let e=c[x];if(e===void 0){M();return}m=setTimeout(()=>{m=null,D().then(e=>{e||(x+=1,k())})},e)},A=()=>{document.visibilityState===`visible`&&b===`dormant`&&P()},j=()=>{document.removeEventListener(`visibilitychange`,A),S!==null&&(clearInterval(S),S=null)},M=()=>{b!==`dormant`&&(b=`dormant`,m&&(clearTimeout(m),m=null),document.addEventListener(`visibilitychange`,A),S=setInterval(()=>{N()},12e4))},N=async()=>{if(b!==`dormant`)return;let t=new Set,n=[],r=(e,r)=>{let i=`${e}:${String(r)}`;t.has(i)||(t.add(i),n.push({host:e,port:r}))};e.relayPortHint!==void 0&&r(e.relayHostHint,e.relayPortHint);let i=h(e);if(i&&r(i.host,i.port),n.length!==0){b=`discovering`,j();for(let e of n)if(await E(e)){x=0;return}M()}},P=()=>{j(),x=0,D().then(e=>{e||M()})};window.addEventListener(`message`,r=>{if(r.source!==window.parent||r.origin!==e.hostOrigin)return;let i=r.data;if(n(i)&&i.type===`webmcp.tools.changed`){s&&l&&o(s,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(n(i)&&i.type===`webmcp.connect`){b===`dormant`?P():!s&&b!==`discovering`&&D();return}let a=u(i);if(!a)return;let c=t.get(a.requestId);if(c){if(a.type===c.responseType){clearTimeout(c.timeoutId),t.delete(a.requestId),c.resolve(a);return}a.type===c.errorType&&(clearTimeout(c.timeoutId),t.delete(a.requestId),c.reject(Error(String(a.error||`Unknown host error`))))}}),e.autoConnect&&D().then(e=>{e||k()})}d()})();
1
+ (function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(e){return n(e)&&typeof e.requestId==`string`&&typeof e.type==`string`}function i(t){return e.has(t)}function a(e){return e.replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),r=globalThis.__WEBMCP_RELAY_CONFIG;function a(e){return n.get(e)??r?.[e]??null}let o=a(`hostOrigin`);if(!o)return null;let s=a(`relayHost`)||`127.0.0.1`;if(!i(s))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,s),null;let c=a(`relayPort`),l=c?Number(c):t;if(!Number.isInteger(l)||l<1||l>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,c),null;let u=a(`requestTimeout`),d=u?Number(u):6e4;return!Number.isInteger(d)||d<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,u),null):{autoConnect:a(`autoConnect`)!==`false`,hostOrigin:o,hostTitle:a(`hostTitle`)||``,hostUrl:a(`hostUrl`)||o,relayHostHint:s,relayId:a(`relayId`)||void 0,relayPortHint:l,relayWorkspace:a(`relayWorkspace`)||void 0,requestTimeoutMs:d,tabId:a(`tabId`)||crypto.randomUUID()}}function u(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}y(e)}function d(e){if(!n(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`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function f(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function p(e){return s({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function m(e){try{let t=sessionStorage.getItem(p(e));if(!t)return null;let r=JSON.parse(t);return!n(r)||typeof r.host!=`string`||r.host.length===0||typeof r.port!=`number`||!Number.isInteger(r.port)||r.port<1||r.port>65535?null:{host:r.host,port:r.port}}catch{return null}}function h(e,t){try{sessionStorage.setItem(p(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function g(e){try{sessionStorage.removeItem(p(e))}catch{}}function _(e){let n=m(e),r=new Set,i=[],a=(e,t)=>{let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function v(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`: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 y(e){let t=new Map,i=[],s=0,l=null,u=null,p=!1,y=null,b=null,x=`idle`,S=0,C=null,w=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function T(n,r){let i=crypto.randomUUID();return new Promise((a,o)=>{let s=setTimeout(()=>{t.delete(i),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(i,{resolve:a,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:i,...r},e.hostOrigin)})}let E=(t,r)=>{let c=s,m=()=>{y&&(clearTimeout(y),y=null)},_=()=>{o(t,JSON.stringify({type:`tools/list`,tools:i}))};b&&(clearTimeout(b),b=null),l=r,u=t,S=0,p=!1,x=`idle`,t.addEventListener(`message`,i=>{let s;try{s=JSON.parse(String(i.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=d(s);if(c){l={hello:c,host:r.host,port:r.port};return}if(n(s)&&s.type===`hello/accepted`){m(),p=!0,h(e,r),_();return}let u=f(s);if(u){m(),p=!1,g(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:r.host,port:r.port,message:u.message,reason:u.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,u.reason,u.message);try{t.close(1008,u.message)}catch{}return}if(!n(s)||typeof s.type!=`string`)return;let v=s;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(s.type));return}T(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{u===t&&(p=!1,u=null,m(),w(`WebSocket connection lost`),k())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),T(`webmcp.tools.list`,{}).then(n=>{s===c&&(i=Array.isArray(n.tools)?n.tools:[]),o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),y=setTimeout(()=>{y=null,!(u!==t||t.readyState!==WebSocket.OPEN||p)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},D=async t=>{let n=await v({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):(E(n.socket,r),!0)},O=async()=>{x=`discovering`;try{for(let t of _(e))if(await D(t))return!0;return!1}finally{x===`discovering`&&(x=`idle`)}},k=()=>{if(!l||b)return;let e={host:l.host,port:l.port},t=Math.round(500*(.85+Math.random()*.3));b=setTimeout(()=>{b=null,D(e).then(e=>{e||A()})},t)},A=()=>{if(b)return;let e=c[S];if(e===void 0){N();return}b=setTimeout(()=>{b=null,O().then(e=>{e||(S+=1,A())})},e)},j=()=>{document.visibilityState===`visible`&&x===`dormant`&&F()},M=()=>{document.removeEventListener(`visibilitychange`,j),C!==null&&(clearInterval(C),C=null)},N=()=>{x!==`dormant`&&(x=`dormant`,b&&(clearTimeout(b),b=null),document.addEventListener(`visibilitychange`,j),C=setInterval(()=>{P()},12e4))},P=async()=>{if(x!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=m(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),x=`discovering`,M();for(let e of t)if(await D(e)){S=0;return}N()},F=()=>{M(),S=0,O().then(e=>{e||N()})};window.addEventListener(`message`,a=>{if(a.source!==window.parent||a.origin!==e.hostOrigin)return;let c=a.data;if(n(c)&&c.type===`webmcp.tools.changed`){i=Array.isArray(c.tools)?c.tools:[],s++,u&&p&&o(u,JSON.stringify({type:`tools/changed`,tools:i}));return}if(n(c)&&c.type===`webmcp.connect`){x===`dormant`?F():!u&&x!==`discovering`&&O();return}if(!r(c))return;let l=t.get(c.requestId);!l||c.type!==l.responseType&&c.type!==l.errorType||(clearTimeout(l.timeoutId),t.delete(c.requestId),c.type===l.responseType?l.resolve(c):l.reject(Error(String(c.error||`Unknown host error`))))}),e.autoConnect&&O().then(e=>{e||A()})}u()})();
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as parseCliOptions, r as printHelp, t as LocalRelayMcpServer } from "./mcpRelayServer-DfGkigG5.mjs";
2
+ import { n as parseCliOptions, r as printHelp, t as LocalRelayMcpServer } from "./mcpRelayServer-DEgebuO2.mjs";
3
3
 
4
4
  //#region src/cli.ts
5
5
  let options;
@@ -14,7 +14,10 @@ if (options.help) {
14
14
  process.exit(0);
15
15
  }
16
16
  if (options.allowedOrigins.includes("*")) process.stderr.write("[webmcp-local-relay] WARNING: accepting connections from ALL host page origins. Use --widget-origin to restrict.\n");
17
- const relay = new LocalRelayMcpServer({ bridgeOptions: options });
17
+ const relay = new LocalRelayMcpServer({
18
+ bridgeOptions: options,
19
+ serverVersion: "6.0.0"
20
+ });
18
21
  try {
19
22
  await relay.start();
20
23
  } catch (err) {
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { parseCliOptions, printHelp } 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.help) {\n printHelp();\n process.exit(0);\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: options,\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\n// --- Auto-exit when the MCP client disconnects, preventing zombie processes ---\n\nlet parentCheckInterval: NodeJS.Timeout | undefined;\nlet shuttingDown = false;\n\n/**\n * Gracefully shuts down bridge and MCP server for process termination signals.\n * Force-exits after 5 seconds if graceful shutdown does not complete, preventing hangs.\n */\nconst shutdown = async (signal: string) => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n if (parentCheckInterval) {\n clearInterval(parentCheckInterval);\n parentCheckInterval = undefined;\n }\n\n // Safety net: force-exit if graceful shutdown hangs (e.g. SDK writing to a broken stdio pipe)\n const forceExitTimer = setTimeout(() => {\n process.stderr.write('[webmcp-local-relay] warn: graceful shutdown timed out, force exiting\\n');\n process.exit(1);\n }, 5_000);\n\n process.stderr.write(`[webmcp-local-relay] received ${signal}, shutting down\\n`);\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 : String(err)}\\n`\n );\n clearTimeout(forceExitTimer);\n process.exit(1);\n }\n clearTimeout(forceExitTimer);\n process.exit(0);\n};\n\nprocess.stdin.on('end', () => {\n void shutdown('stdin-closed');\n});\nprocess.stdin.on('error', () => {\n void shutdown('stdin-error');\n});\nprocess.stdout.on('error', () => {\n void shutdown('stdout-error');\n});\n\nif (process.platform !== 'win32') {\n parentCheckInterval = setInterval(() => {\n if (process.ppid === 1) {\n void shutdown('parent-exited');\n }\n }, 30_000);\n parentCheckInterval.unref();\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\nprocess.on('uncaughtException', (err) => {\n process.stderr.write(\n `[webmcp-local-relay] error: uncaught exception: ${err.stack ?? err.message}\\n`\n );\n void shutdown('uncaughtException');\n});\nprocess.on('unhandledRejection', (reason) => {\n const message = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);\n process.stderr.write(`[webmcp-local-relay] error: unhandled rejection: ${message}\\n`);\n void shutdown('unhandledRejection');\n});\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT');\n});\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM');\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { parseCliOptions, printHelp } from './cli-utils.js';\nimport { LocalRelayMcpServer } from './mcpRelayServer.js';\n\ndeclare const __RELAY_VERSION__: string;\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.help) {\n printHelp();\n process.exit(0);\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: options,\n serverVersion: __RELAY_VERSION__,\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\n// --- Auto-exit when the MCP client disconnects, preventing zombie processes ---\n\nlet parentCheckInterval: NodeJS.Timeout | undefined;\nlet shuttingDown = false;\n\n/**\n * Gracefully shuts down bridge and MCP server for process termination signals.\n * Force-exits after 5 seconds if graceful shutdown does not complete, preventing hangs.\n */\nconst shutdown = async (signal: string) => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n if (parentCheckInterval) {\n clearInterval(parentCheckInterval);\n parentCheckInterval = undefined;\n }\n\n // Safety net: force-exit if graceful shutdown hangs (e.g. SDK writing to a broken stdio pipe)\n const forceExitTimer = setTimeout(() => {\n process.stderr.write('[webmcp-local-relay] warn: graceful shutdown timed out, force exiting\\n');\n process.exit(1);\n }, 5_000);\n\n process.stderr.write(`[webmcp-local-relay] received ${signal}, shutting down\\n`);\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 : String(err)}\\n`\n );\n clearTimeout(forceExitTimer);\n process.exit(1);\n }\n clearTimeout(forceExitTimer);\n process.exit(0);\n};\n\nprocess.stdin.on('end', () => {\n void shutdown('stdin-closed');\n});\nprocess.stdin.on('error', () => {\n void shutdown('stdin-error');\n});\nprocess.stdout.on('error', () => {\n void shutdown('stdout-error');\n});\n\nif (process.platform !== 'win32') {\n parentCheckInterval = setInterval(() => {\n if (process.ppid === 1) {\n void shutdown('parent-exited');\n }\n }, 30_000);\n parentCheckInterval.unref();\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\nprocess.on('uncaughtException', (err) => {\n process.stderr.write(\n `[webmcp-local-relay] error: uncaught exception: ${err.stack ?? err.message}\\n`\n );\n void shutdown('uncaughtException');\n});\nprocess.on('unhandledRejection', (reason) => {\n const message = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);\n process.stderr.write(`[webmcp-local-relay] error: unhandled rejection: ${message}\\n`);\n void shutdown('unhandledRejection');\n});\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT');\n});\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM');\n});\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
+ import { z } from "zod/v4";
1
2
  import { EventEmitter } from "node:events";
2
3
  import { CallToolRequestParamsSchema, CallToolResultSchema, ToolAnnotationsSchema, ToolSchema } from "@modelcontextprotocol/core";
3
- import { z } from "zod/v4";
4
4
  import { CallToolResult, Tool, ToolAnnotations, Transport } from "@modelcontextprotocol/server";
5
5
 
6
6
  //#region src/protocol.d.ts
@@ -1086,7 +1086,10 @@ declare function printHelp(): void;
1086
1086
  /**
1087
1087
  * Base options shared by all {@link LocalRelayMcpServer} configurations.
1088
1088
  */
1089
+ type BrowserLauncher = (command: string, args: readonly string[], callback: (cause: Error | null) => void) => void;
1089
1090
  interface LocalRelayMcpServerBaseOptions {
1091
+ /** Platform launcher; override to run without opening a browser. */
1092
+ launchBrowser?: BrowserLauncher;
1090
1093
  /**
1091
1094
  * MCP server name reported during initialization.
1092
1095
  */
@@ -1118,6 +1121,7 @@ declare class LocalRelayMcpServer {
1118
1121
  */
1119
1122
  readonly bridge: RelayBridgeServer;
1120
1123
  private readonly mcpServer;
1124
+ private readonly launchBrowser;
1121
1125
  private readonly dynamicTools;
1122
1126
  private syncDebounceTimer;
1123
1127
  private connected;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as NormalizedToolSchema, D as normalizeInboundTool, E as ToolSchema, S as InboundToolSchema, T as ToolAnnotationsSchema, _ as extractSanitizedDomain, a as BrowserToRelayMessageSchema, b as CallToolResultSchema, c as RelayHelloAcceptedMessageSchema, d as RelaySourceInfoSchema, f as RelayToBrowserMessageSchema, g as buildPublicToolName, h as RelayRegistry, i as RelayBridgeServer, l as RelayHelloRejectedMessageSchema, m as HelloRequiredError, n as parseCliOptions, o as RelayClientToServerMessageSchema, p as ServerHelloMessageSchema, r as printHelp, s as RelayDescriptorSchema, t as LocalRelayMcpServer, u as RelayServerToClientMessageSchema, v as sanitizeName, w as RelayInvokeArgsSchema, x as DEFAULT_TOOL_INPUT_SCHEMA, y as CallToolRequestParamsSchema } from "./mcpRelayServer-DfGkigG5.mjs";
1
+ import { C as NormalizedToolSchema, D as normalizeInboundTool, E as ToolSchema, S as InboundToolSchema, T as ToolAnnotationsSchema, _ as extractSanitizedDomain, a as BrowserToRelayMessageSchema, b as CallToolResultSchema, c as RelayHelloAcceptedMessageSchema, d as RelaySourceInfoSchema, f as RelayToBrowserMessageSchema, g as buildPublicToolName, h as RelayRegistry, i as RelayBridgeServer, l as RelayHelloRejectedMessageSchema, m as HelloRequiredError, n as parseCliOptions, o as RelayClientToServerMessageSchema, p as ServerHelloMessageSchema, r as printHelp, s as RelayDescriptorSchema, t as LocalRelayMcpServer, u as RelayServerToClientMessageSchema, v as sanitizeName, w as RelayInvokeArgsSchema, x as DEFAULT_TOOL_INPUT_SCHEMA, y as CallToolRequestParamsSchema } from "./mcpRelayServer-DEgebuO2.mjs";
2
2
 
3
3
  export { BrowserToRelayMessageSchema, CallToolRequestParamsSchema, CallToolResultSchema, DEFAULT_TOOL_INPUT_SCHEMA, HelloRequiredError, InboundToolSchema, LocalRelayMcpServer, NormalizedToolSchema, RelayBridgeServer, RelayClientToServerMessageSchema, RelayDescriptorSchema, RelayHelloAcceptedMessageSchema, RelayHelloRejectedMessageSchema, RelayInvokeArgsSchema, RelayRegistry, RelayServerToClientMessageSchema, RelaySourceInfoSchema, RelayToBrowserMessageSchema, ServerHelloMessageSchema, ToolAnnotationsSchema, ToolSchema, buildPublicToolName, extractSanitizedDomain, normalizeInboundTool, parseCliOptions, printHelp, sanitizeName };