@mcp-b/webmcp-local-relay 6.0.0 → 6.0.1

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
@@ -213,16 +213,16 @@ npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com
213
213
  │ WebSocket (ws://127.0.0.1:9333)
214
214
  ┌──────────────────▼───────────────────┐
215
215
  │ Widget iframe │
216
- │ embed.js injects widget.html │
216
+ │ Reads page tools + relay socket │
217
217
  └──────────────────┬───────────────────┘
218
- │ postMessage
218
+ │ same-origin access
219
219
  ┌──────────────────▼───────────────────┐
220
220
  │ Host page │
221
221
  │ WebMCP runtime + registered tools │
222
222
  └──────────────────────────────────────┘
223
223
  ```
224
224
 
225
- **How it connects:** The embed script fetches the sibling `widget.html`, injects configuration, and loads it as a hidden blob iframe that inherits the host page origin. The iframe opens a WebSocket to the relay on `localhost`. Self-hosted copies must serve both `embed.js` and `widget.html`; cross-origin hosts must allow the widget fetch with CORS. The relay fails closed if that fetch fails.
225
+ **How it connects:** The embed script fetches the sibling `widget.html`, injects configuration, and loads it as a hidden blob iframe that inherits the host page origin. The widget reads the host page's `document.modelContext` directly, so native WebMCP, the polyfill, and `@mcp-b/global` all work. `postMessage` is limited to reconnect and reload controls. The widget keeps a WebSocket to the relay on `localhost` for MCP traffic. Self-hosted copies must serve both `embed.js` and `widget.html`; cross-origin hosts must allow the widget fetch with CORS. The relay fails closed if that fetch fails.
226
226
 
227
227
  After a disconnect, the widget retries the last endpoint once after about `500ms`, then rescans the relay range after `10s`, `20s`, and `30s`. If no relay responds, it enters a dormant state and probes the configured or cached endpoint every two minutes; returning to the tab or sending `webmcp.connect` triggers immediate rediscovery.
228
228
 
@@ -233,7 +233,7 @@ After a disconnect, the widget retries the last endpoint once after about `500ms
233
233
  Supported page runtimes:
234
234
 
235
235
  1. `@mcp-b/global` (recommended for the complete MCP-B runtime)
236
- 2. Current native Chrome with `document.modelContext.getTools()` and object-input `executeTool()`, which this relay requires to invoke tools
236
+ 2. Current native Chrome with `document.modelContext.getTools()` and object-input `executeTool()`
237
237
  3. `@mcp-b/webmcp-polyfill`
238
238
 
239
239
  Runtime dispatch behavior in the browser embed/widget layer:
@@ -243,9 +243,9 @@ Runtime dispatch behavior in the browser embed/widget layer:
243
243
  upstream API.
244
244
  - Refreshes the descriptor before every invocation so Chrome never receives a
245
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.
246
+ - Relays the host page's tools plus tools registered by other same-origin
247
+ frames. When names collide, the host page's own tool wins and the widget logs
248
+ one warning per name.
249
249
  - Converts only results that parse to a JSON object into structured content.
250
250
  A JSON string arrives as its content; any other result, such as `10.50`,
251
251
  keeps its original text.
@@ -266,13 +266,13 @@ For Chromium/Chrome Canary native preview testing:
266
266
 
267
267
  ### Troubleshooting
268
268
 
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 |
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's WebMCP runtime. If tools register after load, confirm your runtime emits the `toolchange` event |
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
+ | `Tool execution timed out` | A page tool exceeded the timeout (default 60s). Raise `data-request-timeout` and keep CLI `--invoke-timeout` slightly higher |
276
276
 
277
277
  ---
278
278
 
@@ -291,7 +291,7 @@ src/
291
291
  ├── schemas.ts Browser <-> relay protocol schemas
292
292
  ├── browser/embed.ts Script-tag loader for website owners
293
293
  ├── browser/widget.ts Widget IIFE entry point (calls startWidgetRuntime)
294
- ├── browser/widgetRuntime.ts Hidden iframe bridge runtime
294
+ ├── browser/widgetRuntime.ts Iframe discovery, execution, and relay runtime
295
295
  ├── browser/shared.ts Shared browser-side utilities
296
296
  └── index.ts Public API exports
297
297
  ```
@@ -1,2 +1 @@
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)})}})();
1
+ (function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let t=`[data-webmcp-relay]`,n=`__webmcp_relay_tab_id`,r=null,i=document.currentScript instanceof HTMLScriptElement?document.currentScript:null,a=i?i.hasAttribute(`data-debug`):!1;function o(...e){a&&console.warn(`[webmcp-relay-embed]`,...e)}function s(){try{let e=sessionStorage.getItem(n);if(e)return e}catch(e){o(`sessionStorage read failed, tab ID will not persist:`,e)}let e=crypto.randomUUID();try{sessionStorage.setItem(n,e)}catch(e){o(`sessionStorage write failed:`,e)}return e}function c(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:s(),widgetUrl:new URL(`widget.html`,e.src).href}}async function l({widgetUrl:e,...n}){if(document.querySelector(t))return;let i=new URL(window.location.href);i.search=``,i.hash=``;let a=JSON.stringify({...n,hostOrigin:window.location.origin,hostUrl:i.href,hostTitle:document.title}).replace(/</g,`\\u003c`),o=await fetch(e);if(!o.ok)throw Error(`Widget HTML request failed with status ${String(o.status)}`);let s=await o.text(),c=`<script>window.__WEBMCP_RELAY_CONFIG=${a};<\/script>`,l=URL.createObjectURL(new Blob([s.replace(`</head>`,()=>`${c}</head>`)],{type:`text/html`})),u=document.createElement(`iframe`);u.src=l,u.style.display=`none`,u.setAttribute(`aria-hidden`,`true`),u.setAttribute(`data-webmcp-relay`,`1`),u.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(u),r=u.contentWindow,u.addEventListener(`load`,()=>{r=u.contentWindow,URL.revokeObjectURL(l)}),u.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,u.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(l)})}if(!document.querySelector(t)){let t;try{t=c(i)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,t=>{t.origin!==window.location.origin||!r||t.source!==r||e(t.data)&&t.data.type===`webmcp.reload`&&window.location.reload()});let n=()=>{l(t).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?n():document.addEventListener(`DOMContentLoaded`,n,{once:!0}),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&r&&r.postMessage({type:`webmcp.connect`},window.location.origin)})}})();
@@ -5,6 +5,7 @@
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(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>
8
+ <script>(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([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(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 f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(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 x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||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 w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(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=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] 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}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
9
+ `);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();</script>
9
10
  </body>
10
11
  </html>
@@ -1 +1,2 @@
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()})();
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([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(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 f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(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 x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||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 w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(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=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] 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}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
2
+ `);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();
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-DEgebuO2.mjs";
2
+ import { n as parseCliOptions, r as printHelp, t as LocalRelayMcpServer } from "./mcpRelayServer-DkidP4o7.mjs";
3
3
 
4
4
  //#region src/cli.ts
5
5
  let options;
@@ -16,7 +16,7 @@ if (options.help) {
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
17
  const relay = new LocalRelayMcpServer({
18
18
  bridgeOptions: options,
19
- serverVersion: "6.0.0"
19
+ serverVersion: "6.0.1"
20
20
  });
21
21
  try {
22
22
  await relay.start();
@@ -39,7 +39,7 @@ let shuttingDown = false;
39
39
  * Gracefully shuts down bridge and MCP server for process termination signals.
40
40
  * Force-exits after 5 seconds if graceful shutdown does not complete, preventing hangs.
41
41
  */
42
- const shutdown = async (signal) => {
42
+ const shutdown = async (signal, exitCode = 0) => {
43
43
  if (shuttingDown) return;
44
44
  shuttingDown = true;
45
45
  if (parentCheckInterval) {
@@ -59,7 +59,7 @@ const shutdown = async (signal) => {
59
59
  process.exit(1);
60
60
  }
61
61
  clearTimeout(forceExitTimer);
62
- process.exit(0);
62
+ process.exit(exitCode);
63
63
  };
64
64
  process.stdin.on("end", () => {
65
65
  shutdown("stdin-closed");
@@ -80,12 +80,12 @@ if (relay.bridge.mode === "server") process.stderr.write(`[webmcp-local-relay] s
80
80
  else process.stderr.write(`[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\n`);
81
81
  process.on("uncaughtException", (err) => {
82
82
  process.stderr.write(`[webmcp-local-relay] error: uncaught exception: ${err.stack ?? err.message}\n`);
83
- shutdown("uncaughtException");
83
+ shutdown("uncaughtException", 1);
84
84
  });
85
85
  process.on("unhandledRejection", (reason) => {
86
86
  const message = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
87
87
  process.stderr.write(`[webmcp-local-relay] error: unhandled rejection: ${message}\n`);
88
- shutdown("unhandledRejection");
88
+ shutdown("unhandledRejection", 1);
89
89
  });
90
90
  process.on("SIGINT", () => {
91
91
  shutdown("SIGINT");
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\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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"}
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, exitCode = 0) => {\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(exitCode);\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', 1);\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', 1);\n});\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT');\n});\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM');\n});\n"],"mappings":";;;;AAOA,IAAI;AACJ,IAAI;CACF,UAAU,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;AACjD,SAAS,KAAK;CACZ,QAAQ,OAAO,MACb,+BAA+B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAClF;CACA,QAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,QAAQ,MAAM;CAChB,UAAU;CACV,QAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,QAAQ,eAAe,SAAS,GAAG,GACrC,QAAQ,OAAO,MACb,oHACF;AAGF,MAAM,QAAQ,IAAI,oBAAoB;CACpC,eAAe;CACf;AACF,CAAC;AAED,IAAI;CACF,MAAM,MAAM,MAAM;AACpB,SAAS,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,QAAQ,SAAS,QAAQ,GAC3B,QAAQ,OAAO,MACb,mEAAmE,QAAQ,KAAK,GAAG,QAAQ,KAAK,IAClG;MAEA,QAAQ,OAAO,MAAM,8DAA8D,QAAQ,GAAG;CAEhG,QAAQ,KAAK,CAAC;AAChB;AAEA,IAAI;CACF,MAAM,MAAM,WAAW;AACzB,SAAS,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,QAAQ,OAAO,MAAM,gEAAgE,QAAQ,GAAG;CAChG,QAAQ,KAAK,CAAC;AAChB;AAIA,IAAI;AACJ,IAAI,eAAe;;;;;AAMnB,MAAM,WAAW,OAAO,QAAgB,WAAW,MAAM;CACvD,IAAI,cACF;CAEF,eAAe;CACf,IAAI,qBAAqB;EACvB,cAAc,mBAAmB;EACjC,sBAAsB;CACxB;CAGA,MAAM,iBAAiB,iBAAiB;EACtC,QAAQ,OAAO,MAAM,yEAAyE;EAC9F,QAAQ,KAAK,CAAC;CAChB,GAAG,GAAK;CAER,QAAQ,OAAO,MAAM,iCAAiC,OAAO,kBAAkB;CAE/E,IAAI;EACF,MAAM,MAAM,KAAK;CACnB,SAAS,KAAK;EACZ,QAAQ,OAAO,MACb,+CAA+C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAClG;EACA,aAAa,cAAc;EAC3B,QAAQ,KAAK,CAAC;CAChB;CACA,aAAa,cAAc;CAC3B,QAAQ,KAAK,QAAQ;AACvB;AAEA,QAAQ,MAAM,GAAG,aAAa;CAC5B,AAAK,SAAS,cAAc;AAC9B,CAAC;AACD,QAAQ,MAAM,GAAG,eAAe;CAC9B,AAAK,SAAS,aAAa;AAC7B,CAAC;AACD,QAAQ,OAAO,GAAG,eAAe;CAC/B,AAAK,SAAS,cAAc;AAC9B,CAAC;AAED,IAAI,QAAQ,aAAa,SAAS;CAChC,sBAAsB,kBAAkB;EACtC,IAAI,QAAQ,SAAS,GACnB,AAAK,SAAS,eAAe;CAEjC,GAAG,GAAM;CACT,oBAAoB,MAAM;AAC5B;AAEA,IAAI,MAAM,OAAO,SAAS,UACxB,QAAQ,OAAO,MACb,uDAAuD,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,qBAAqB,QAAQ,eAAe,KAAK,IAAI,EAAE,IAClJ;KAEA,QAAQ,OAAO,MACb,6EAA6E,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,GACjH;AAGF,QAAQ,GAAG,sBAAsB,QAAQ;CACvC,QAAQ,OAAO,MACb,mDAAmD,IAAI,SAAS,IAAI,QAAQ,GAC9E;CACA,AAAK,SAAS,qBAAqB,CAAC;AACtC,CAAC;AACD,QAAQ,GAAG,uBAAuB,WAAW;CAC3C,MAAM,UAAU,kBAAkB,QAAS,OAAO,SAAS,OAAO,UAAW,OAAO,MAAM;CAC1F,QAAQ,OAAO,MAAM,oDAAoD,QAAQ,GAAG;CACpF,AAAK,SAAS,sBAAsB,CAAC;AACvC,CAAC;AAED,QAAQ,GAAG,gBAAgB;CACzB,AAAK,SAAS,QAAQ;AACxB,CAAC;AACD,QAAQ,GAAG,iBAAiB;CAC1B,AAAK,SAAS,SAAS;AACzB,CAAC"}
package/dist/index.d.mts CHANGED
@@ -85,11 +85,15 @@ declare const InboundToolSchema: z.ZodObject<{
85
85
  }, z.core.$strip>>>;
86
86
  title: z.ZodOptional<z.ZodString>;
87
87
  name: z.ZodString;
88
- inputSchema: z.ZodOptional<z.ZodObject<{
88
+ inputSchema: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodObject<{}, z.core.$loose>, z.ZodTransform<{
89
+ type: string;
90
+ }, {
91
+ [x: string]: unknown;
92
+ }>>, z.ZodObject<{
89
93
  type: z.ZodLiteral<"object">;
90
94
  properties: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue, z.core.$ZodTypeInternals<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue>>>>;
91
95
  required: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
- }, z.core.$catchall<z.ZodUnknown>>>;
96
+ }, z.core.$catchall<z.ZodUnknown>>>>;
93
97
  }, z.core.$strip>;
94
98
  /**
95
99
  * Canonical normalized relay tool shape.
@@ -133,43 +137,7 @@ declare const BrowserToRelayMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
133
137
  iconUrl: z.ZodOptional<z.ZodString>;
134
138
  }, z.core.$strip>, z.ZodObject<{
135
139
  type: z.ZodLiteral<"tools/list">;
136
- tools: z.ZodPipe<z.ZodArray<z.ZodObject<{
137
- description: z.ZodOptional<z.ZodString>;
138
- outputSchema: z.ZodOptional<z.ZodObject<{
139
- $schema: z.ZodOptional<z.ZodString>;
140
- }, z.core.$loose>>;
141
- annotations: z.ZodOptional<z.ZodObject<{
142
- title: z.ZodOptional<z.ZodString>;
143
- readOnlyHint: z.ZodOptional<z.ZodBoolean>;
144
- destructiveHint: z.ZodOptional<z.ZodBoolean>;
145
- idempotentHint: z.ZodOptional<z.ZodBoolean>;
146
- openWorldHint: z.ZodOptional<z.ZodBoolean>;
147
- }, z.core.$strip>>;
148
- execution: z.ZodOptional<z.ZodObject<{
149
- taskSupport: z.ZodOptional<z.ZodEnum<{
150
- optional: "optional";
151
- required: "required";
152
- forbidden: "forbidden";
153
- }>>;
154
- }, z.core.$strip>>;
155
- _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
156
- icons: z.ZodOptional<z.ZodArray<z.ZodObject<{
157
- src: z.ZodString;
158
- mimeType: z.ZodOptional<z.ZodString>;
159
- sizes: z.ZodOptional<z.ZodArray<z.ZodString>>;
160
- theme: z.ZodOptional<z.ZodEnum<{
161
- light: "light";
162
- dark: "dark";
163
- }>>;
164
- }, z.core.$strip>>>;
165
- title: z.ZodOptional<z.ZodString>;
166
- name: z.ZodString;
167
- inputSchema: z.ZodOptional<z.ZodObject<{
168
- type: z.ZodLiteral<"object">;
169
- properties: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue, z.core.$ZodTypeInternals<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue>>>>;
170
- required: z.ZodOptional<z.ZodArray<z.ZodString>>;
171
- }, z.core.$catchall<z.ZodUnknown>>>;
172
- }, z.core.$strip>>, z.ZodTransform<{
140
+ tools: z.ZodPipe<z.ZodArray<z.ZodUnknown>, z.ZodTransform<{
173
141
  inputSchema: {
174
142
  [x: string]: unknown;
175
143
  type: "object";
@@ -200,77 +168,10 @@ declare const BrowserToRelayMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
200
168
  theme?: "light" | "dark" | undefined;
201
169
  }[] | undefined;
202
170
  title?: string | undefined;
203
- }[], {
204
- name: string;
205
- description?: string | undefined;
206
- outputSchema?: {
207
- [x: string]: unknown;
208
- $schema?: string | undefined;
209
- } | undefined;
210
- annotations?: {
211
- title?: string | undefined;
212
- readOnlyHint?: boolean | undefined;
213
- destructiveHint?: boolean | undefined;
214
- idempotentHint?: boolean | undefined;
215
- openWorldHint?: boolean | undefined;
216
- } | undefined;
217
- execution?: {
218
- taskSupport?: "optional" | "required" | "forbidden" | undefined;
219
- } | undefined;
220
- _meta?: Record<string, unknown> | undefined;
221
- icons?: {
222
- src: string;
223
- mimeType?: string | undefined;
224
- sizes?: string[] | undefined;
225
- theme?: "light" | "dark" | undefined;
226
- }[] | undefined;
227
- title?: string | undefined;
228
- inputSchema?: {
229
- [x: string]: unknown;
230
- type: "object";
231
- properties?: Record<string, import("@modelcontextprotocol/server").JSONValue> | undefined;
232
- required?: string[] | undefined;
233
- } | undefined;
234
- }[]>>;
171
+ }[], unknown[]>>;
235
172
  }, z.core.$strip>, z.ZodObject<{
236
173
  type: z.ZodLiteral<"tools/changed">;
237
- tools: z.ZodPipe<z.ZodArray<z.ZodObject<{
238
- description: z.ZodOptional<z.ZodString>;
239
- outputSchema: z.ZodOptional<z.ZodObject<{
240
- $schema: z.ZodOptional<z.ZodString>;
241
- }, z.core.$loose>>;
242
- annotations: z.ZodOptional<z.ZodObject<{
243
- title: z.ZodOptional<z.ZodString>;
244
- readOnlyHint: z.ZodOptional<z.ZodBoolean>;
245
- destructiveHint: z.ZodOptional<z.ZodBoolean>;
246
- idempotentHint: z.ZodOptional<z.ZodBoolean>;
247
- openWorldHint: z.ZodOptional<z.ZodBoolean>;
248
- }, z.core.$strip>>;
249
- execution: z.ZodOptional<z.ZodObject<{
250
- taskSupport: z.ZodOptional<z.ZodEnum<{
251
- optional: "optional";
252
- required: "required";
253
- forbidden: "forbidden";
254
- }>>;
255
- }, z.core.$strip>>;
256
- _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
257
- icons: z.ZodOptional<z.ZodArray<z.ZodObject<{
258
- src: z.ZodString;
259
- mimeType: z.ZodOptional<z.ZodString>;
260
- sizes: z.ZodOptional<z.ZodArray<z.ZodString>>;
261
- theme: z.ZodOptional<z.ZodEnum<{
262
- light: "light";
263
- dark: "dark";
264
- }>>;
265
- }, z.core.$strip>>>;
266
- title: z.ZodOptional<z.ZodString>;
267
- name: z.ZodString;
268
- inputSchema: z.ZodOptional<z.ZodObject<{
269
- type: z.ZodLiteral<"object">;
270
- properties: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue, z.core.$ZodTypeInternals<import("@modelcontextprotocol/server").JSONValue, import("@modelcontextprotocol/server").JSONValue>>>>;
271
- required: z.ZodOptional<z.ZodArray<z.ZodString>>;
272
- }, z.core.$catchall<z.ZodUnknown>>>;
273
- }, z.core.$strip>>, z.ZodTransform<{
174
+ tools: z.ZodPipe<z.ZodArray<z.ZodUnknown>, z.ZodTransform<{
274
175
  inputSchema: {
275
176
  [x: string]: unknown;
276
177
  type: "object";
@@ -301,38 +202,7 @@ declare const BrowserToRelayMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
301
202
  theme?: "light" | "dark" | undefined;
302
203
  }[] | undefined;
303
204
  title?: string | undefined;
304
- }[], {
305
- name: string;
306
- description?: string | undefined;
307
- outputSchema?: {
308
- [x: string]: unknown;
309
- $schema?: string | undefined;
310
- } | undefined;
311
- annotations?: {
312
- title?: string | undefined;
313
- readOnlyHint?: boolean | undefined;
314
- destructiveHint?: boolean | undefined;
315
- idempotentHint?: boolean | undefined;
316
- openWorldHint?: boolean | undefined;
317
- } | undefined;
318
- execution?: {
319
- taskSupport?: "optional" | "required" | "forbidden" | undefined;
320
- } | undefined;
321
- _meta?: Record<string, unknown> | undefined;
322
- icons?: {
323
- src: string;
324
- mimeType?: string | undefined;
325
- sizes?: string[] | undefined;
326
- theme?: "light" | "dark" | undefined;
327
- }[] | undefined;
328
- title?: string | undefined;
329
- inputSchema?: {
330
- [x: string]: unknown;
331
- type: "object";
332
- properties?: Record<string, import("@modelcontextprotocol/server").JSONValue> | undefined;
333
- required?: string[] | undefined;
334
- } | undefined;
335
- }[]>>;
205
+ }[], unknown[]>>;
336
206
  }, z.core.$strip>, z.ZodObject<{
337
207
  type: z.ZodLiteral<"result">;
338
208
  callId: z.ZodString;
@@ -427,6 +297,9 @@ declare const RelayToBrowserMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
427
297
  callId: z.ZodString;
428
298
  toolName: z.ZodString;
429
299
  args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
300
+ }, z.core.$strip>, z.ZodObject<{
301
+ type: z.ZodLiteral<"cancel">;
302
+ callId: z.ZodString;
430
303
  }, z.core.$strip>, z.ZodObject<{
431
304
  type: z.ZodLiteral<"ping">;
432
305
  }, z.core.$strip>, z.ZodObject<{
@@ -466,6 +339,9 @@ declare const RelayClientToServerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodOb
466
339
  callId: z.ZodString;
467
340
  toolName: z.ZodString;
468
341
  args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
342
+ }, z.core.$strip>, z.ZodObject<{
343
+ type: z.ZodLiteral<"relay/cancel">;
344
+ callId: z.ZodString;
469
345
  }, z.core.$strip>], "type">;
470
346
  /**
471
347
  * Relay client to server message payload.
@@ -849,8 +725,8 @@ interface RelayBridgeServerOptions {
849
725
  */
850
726
  portRangeEnd?: number;
851
727
  /**
852
- * File path used to cache the last successful relay port.
853
- * @defaultValue `~/.webmcp/relay-port.json`
728
+ * @deprecated Ignored. The relay no longer caches its port, so it always joins a relay
729
+ * that owns the preferred port instead of starting a second one elsewhere.
854
730
  */
855
731
  persistPath?: string;
856
732
  /**
@@ -908,7 +784,6 @@ declare class RelayBridgeServer extends EventEmitter {
908
784
  private desiredPort;
909
785
  private readonly portExplicitlySet;
910
786
  private readonly portRangeEnd;
911
- private readonly persistPath;
912
787
  private readonly allowedOrigins;
913
788
  private readonly maxPayloadBytes;
914
789
  private readonly invokeTimeoutMs;
@@ -922,6 +797,7 @@ declare class RelayBridgeServer extends EventEmitter {
922
797
  private readonly pendingInvocations;
923
798
  private readonly browserClientConnectionIds;
924
799
  private readonly relayClientConnectionIds;
800
+ private readonly relayClientInvocations;
925
801
  private readonly heartbeatIntervalByConnectionId;
926
802
  private readonly lastPongByConnectionId;
927
803
  private readonly onStateChangedPushRelay;
@@ -933,8 +809,6 @@ declare class RelayBridgeServer extends EventEmitter {
933
809
  * Maximum delay for relay-to-relay reconnection backoff in client mode.
934
810
  */
935
811
  private readonly clientMaxReconnectDelay;
936
- private readonly clientMaxReconnectAttempts;
937
- private clientReconnectAttempts;
938
812
  private readonly clientPendingInvocations;
939
813
  private clientTools;
940
814
  private clientSources;
@@ -977,7 +851,6 @@ declare class RelayBridgeServer extends EventEmitter {
977
851
  start(): Promise<void>;
978
852
  private startUsingPortStrategy;
979
853
  private isAddressInUseError;
980
- private persistSelectedPort;
981
854
  private tryAttachToExistingRelay;
982
855
  /**
983
856
  * Stops all relay resources and rejects any pending invocations.
@@ -994,7 +867,8 @@ declare class RelayBridgeServer extends EventEmitter {
994
867
  */
995
868
  invokeTool(toolName: string, args: RelayInvokeArgs, options?: {
996
869
  sourceId?: string;
997
- requestTabId?: string;
870
+ requestTabId?: string; /** Cancels the invocation and forwards cancellation to the browser runtime. */
871
+ signal?: AbortSignal;
998
872
  }): Promise<RelayCallToolResult>;
999
873
  private startAsServer;
1000
874
  private buildServerHello;
@@ -1039,6 +913,7 @@ declare class RelayBridgeServer extends EventEmitter {
1039
913
  */
1040
914
  private reconnectWithModePromotion;
1041
915
  private invokeToolViaRelay;
916
+ private requestTool;
1042
917
  /**
1043
918
  * Builds the complete relay response payload including tools and source metadata.
1044
919
  */
@@ -1123,6 +998,8 @@ declare class LocalRelayMcpServer {
1123
998
  private readonly mcpServer;
1124
999
  private readonly launchBrowser;
1125
1000
  private readonly dynamicTools;
1001
+ /** Page tools that share a name with a relay tool, already reported once. */
1002
+ private readonly shadowedToolNames;
1126
1003
  private syncDebounceTimer;
1127
1004
  private connected;
1128
1005
  /**
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-DEgebuO2.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-DkidP4o7.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 };