@mcp-b/webmcp-local-relay 4.0.0 → 5.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
@@ -49,6 +49,7 @@ Once connected, your AI client can see and call tools from any open browser tab
49
49
  3. Call any tool directly by name (e.g., `create_issue`, `search_docs`)
50
50
 
51
51
  Tools appear and disappear automatically as you open, reload, and close tabs.
52
+ Tools that require MCP task execution are omitted, and multi-round `input_required` results return an error.
52
53
 
53
54
  ## For Website Owners
54
55
 
@@ -65,12 +66,14 @@ New to WebMCP? Here's the full setup:
65
66
  ```html
66
67
  <script src="https://cdn.jsdelivr.net/npm/@mcp-b/global@latest/dist/index.iife.js"></script>
67
68
  <script>
68
- document.modelContext.registerTool({
69
- name: 'get_page_title',
70
- description: 'Get the current page title',
71
- inputSchema: { type: 'object', properties: {} },
72
- execute: async () => ({ content: [{ type: 'text', text: document.title }] }),
73
- });
69
+ void document.modelContext
70
+ .registerTool({
71
+ name: 'get_page_title',
72
+ description: 'Get the current page title',
73
+ inputSchema: { type: 'object', properties: {} },
74
+ execute: async () => ({ content: [{ type: 'text', text: document.title }] }),
75
+ })
76
+ .catch(console.error);
74
77
  </script>
75
78
  <script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"></script>
76
79
  ```
@@ -94,10 +97,14 @@ several slow API calls and might exceed one minute:
94
97
  ></script>
95
98
  ```
96
99
 
100
+ The relay allows `65000` ms by default. If the page timeout is higher, start the
101
+ relay with a slightly larger limit, for example `--invoke-timeout 125000` for
102
+ the `120000` ms page setting above.
103
+
97
104
  Tool registration references:
98
105
 
99
- - [`@mcp-b/global` quick start and `registerTool`](../global/README.md)
100
- - [WebMCP proposal examples for `registerTool`](https://github.com/webmachinelearning/webmcp/blob/main/docs/proposal.md)
106
+ - [`@mcp-b/global` quick start and `registerTool`](https://docs.mcp-b.ai/packages/global/reference)
107
+ - [WebMCP specification for `registerTool`](https://webmachinelearning.github.io/webmcp/)
101
108
 
102
109
  ---
103
110
 
@@ -107,7 +114,7 @@ Tool registration references:
107
114
 
108
115
  The JSON config above works for most clients. Here are additional options:
109
116
 
110
- **Claude Desktop (MCPB bundle)** — download the `.mcpb` file from [GitHub Releases](https://github.com/WebMCP-org/npm-packages/releases) and double-click to install. No terminal needed.
117
+ **Claude Desktop (MCPB bundle)** — download the `.mcpb` file from [GitHub Releases](https://github.com/WebMCP-org/npm-packages/releases) and double-click to install. It starts with the zero-configuration defaults: loopback, automatic port discovery, and all page origins allowed. No terminal is needed.
111
118
 
112
119
  **Direct CLI** — run the relay standalone:
113
120
 
@@ -117,30 +124,36 @@ npx @mcp-b/webmcp-local-relay
117
124
 
118
125
  ### Exposed Tools
119
126
 
120
- The relay exposes four static management tools that are always available:
127
+ The relay exposes three static management tools that are always available:
121
128
 
122
- | Tool | Description |
123
- | --------------------- | -------------------------------------------------------------------------------------------------------------------- |
124
- | `webmcp_list_sources` | Lists connected browser tabs with metadata (tab ID, origin, URL, title, icon, tool count) |
125
- | `webmcp_list_tools` | Lists all relayed tools with source info |
126
- | `webmcp_call_tool` | Invokes a relayed tool by name with JSON arguments — useful for clients that don't support dynamic tool registration |
127
- | `webmcp_open_page` | Opens a URL in the user's default browser, or refreshes a connected source page by matching origin |
129
+ | Tool | Description |
130
+ | --------------------- | ----------------------------------------------------------------------------------- |
131
+ | `webmcp_list_sources` | Lists connected browser tabs that publish tools, with tab metadata |
132
+ | `webmcp_list_tools` | Lists all relayed tools with source info |
133
+ | `webmcp_open_page` | Opens a URL, or in server mode refreshes a connected source page by matching origin |
128
134
 
129
135
  **Dynamic tools** are registered directly on the MCP server using the original tool name, sanitized to `[a-zA-Z0-9_]`. When tools from different tabs share a name, a short tab-ID suffix is appended for disambiguation:
130
136
 
131
137
  - Single provider: `get_issue`
132
138
  - Multiple providers with the same name: `search_ed93`, `search_a1b2`
133
139
 
140
+ Names are limited to 128 characters. Sanitization, truncation, or tab-prefix collisions receive deterministic `_2`, `_3`, and later suffixes.
141
+
134
142
  ### CLI Options
135
143
 
136
144
  ```text
137
145
  webmcp-local-relay [options]
138
146
 
139
- --host, -H Bind host (default: 127.0.0.1)
140
- --port, -p WebSocket port (default: 9333)
147
+ --host, -H Bind host for local websocket relay (default: 127.0.0.1)
148
+ --port, -p Preferred root port for the local relay cluster (default: 9333)
141
149
  --widget-origin Allowed host page origin(s), comma-separated (default: *)
142
- --allowed-origin Alias for --widget-origin
143
- --ws-origin Alias for --widget-origin
150
+ --allowed-origin Deprecated alias for --widget-origin
151
+ --ws-origin Deprecated alias for --widget-origin
152
+ --label Human-readable relay label reported during discovery
153
+ --workspace Optional workspace name reported during discovery
154
+ --relay-id Stable relay identifier reported during discovery
155
+ --invoke-timeout Browser tool invocation timeout in milliseconds (default: 65000)
156
+ --max-payload Maximum WebSocket payload size in bytes (default: 10000000)
144
157
  --help, -h Show help
145
158
  ```
146
159
 
@@ -161,7 +174,7 @@ npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com
161
174
 
162
175
  - Binds to `127.0.0.1` by default (loopback only, not accessible from your network).
163
176
  - The default `allowedOrigins` is `*`, which permits any browser page to connect and register tools. This is convenient for development but means any website open in your browser can expose tools to the relay.
164
- - `--widget-origin` validates the **host page origin** reported in the browser `hello` message. This is the origin of the page that loaded `embed.js` (e.g., `https://myapp.com`), regardless of whether the widget iframe is served from CDN or self-hosted.
177
+ - `--widget-origin` validates the browser's WebSocket `Origin` header. The injected blob iframe inherits the host page origin, so browser connections cannot override it in `hello`.
165
178
  - **Recommended:** Use `--widget-origin` to restrict which websites can register tools:
166
179
 
167
180
  ```bash
@@ -172,7 +185,8 @@ npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com
172
185
  webmcp-local-relay --widget-origin https://app1.com,https://app2.com
173
186
  ```
174
187
 
175
- - Only the host page origin is checked — any local process can connect regardless of origin restrictions.
188
+ - `--widget-origin` is not local-process authentication. An Origin-less browser-protocol client falls back to its claimed `hello.origin`, while the internal relay-to-relay protocol is outside this browser-origin check. Keep the relay bound to loopback unless you add a separate trusted boundary.
189
+ - [Chrome 147 and later](https://developer.chrome.com/release-notes/147) can ask a public site for Local Network Access permission before it opens the loopback WebSocket. This browser permission is separate from relay configuration.
176
190
 
177
191
  ### Architecture
178
192
 
@@ -186,50 +200,53 @@ npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com
186
200
  │ LocalRelayMcpServer │
187
201
  │ webmcp_list_sources │
188
202
  │ webmcp_list_tools │
189
- │ webmcp_call_tool │
190
203
  │ + dynamic tools from browser │
191
204
  └──────────────────┬───────────────────┘
192
- │ WebSocket (ws://127.0.0.1:9333)
205
+ │ in process
193
206
  ┌──────────────────▼───────────────────┐
194
207
  │ RelayBridgeServer │
195
208
  │ Manages connections, routes calls │
196
209
  └──────────────────┬───────────────────┘
197
- │ postMessage
210
+ │ WebSocket (ws://127.0.0.1:9333)
198
211
  ┌──────────────────▼───────────────────┐
199
212
  │ Widget iframe │
200
213
  │ embed.js injects widget.html │
201
214
  └──────────────────┬───────────────────┘
202
- │ document.modelContext
215
+ │ postMessage
203
216
  ┌──────────────────▼───────────────────┐
204
217
  │ Host page │
205
218
  │ WebMCP runtime + registered tools │
206
219
  └──────────────────────────────────────┘
207
220
  ```
208
221
 
209
- **How it connects:** The embed script injects a hidden iframe into the host page. The iframe opens a WebSocket to the relay on `localhost`. Tools are discovered via `document.modelContext` (or `navigator.modelContextTesting` as fallback) and forwarded to the relay, which registers them as standard MCP tools over stdio.
210
- If the relay is temporarily unavailable, the widget reconnects automatically using exponential backoff (1.5x multiplier) from `500ms` up to `3000ms`, stopping after 100 attempts.
222
+ **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.
223
+
224
+ 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.
211
225
 
212
- **Client mode:** When a second relay instance starts and the port is already in use (`EADDRINUSE`), it automatically falls back to **client mode**. In client mode the relay connects as a WebSocket client to the existing server relay and proxies tool operations through it. If the server relay later stops, the client attempts to promote itself back to server mode. This enables multiple MCP clients to share the same browser connections without manual configuration.
226
+ **Client mode:** When a candidate port is already owned by a compatible WebMCP relay, a second instance joins it in client mode and proxies tool operations through it. A non-relay service is skipped while scanning the default range; an explicitly selected occupied port fails. If the server relay later stops, the client attempts to promote itself back to server mode. This enables multiple MCP clients to share the same browser connections without manual configuration.
213
227
 
214
228
  ### Runtime Compatibility
215
229
 
216
230
  Supported page runtimes:
217
231
 
218
- 1. `@mcp-b/global` (recommended)
219
- 2. `@mcp-b/webmcp-polyfill` with `navigator.modelContextTesting`
232
+ 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
234
+ 3. `@mcp-b/webmcp-polyfill`
220
235
 
221
236
  Runtime dispatch behavior in the browser embed/widget layer:
222
237
 
223
- - Uses `document.modelContext.listTools` + `callTool` when present.
224
- - Falls back to `navigator.modelContextTesting.listTools` + `executeTool`.
238
+ - Uses asynchronous `document.modelContext.getTools()` and the exact returned
239
+ descriptor with feature-detected `executeTool()`.
240
+ - Refreshes the descriptor before every invocation so Chrome never receives a
241
+ stale registration object.
225
242
 
226
243
  ### WebMCP Standard Status
227
244
 
228
- WebMCP is an emerging web platform proposal. This relay works today with polyfills and will support native browser implementations as they mature.
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.
229
246
 
230
247
  - [W3C WebML CG draft](https://webmachinelearning.github.io/webmcp/)
231
248
  - [Proposal repository](https://github.com/webmachinelearning/webmcp)
232
- - [Proposal details (`document.modelContext`, `registerTool`, etc.)](https://github.com/webmachinelearning/webmcp/blob/main/docs/proposal.md)
249
+ - [WebMCP specification (`document.modelContext`, `registerTool`, etc.)](https://webmachinelearning.github.io/webmcp/)
233
250
 
234
251
  For Chromium/Chrome Canary native preview testing:
235
252
 
@@ -239,13 +256,13 @@ For Chromium/Chrome Canary native preview testing:
239
256
 
240
257
  ### Troubleshooting
241
258
 
242
- | Problem | Fix |
243
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
244
- | `No sources connected` | Ensure the page loaded `embed.js` and the relay process is running |
245
- | `No tools listed` | Ensure tools are registered on the page's WebMCP runtime. If tools register after load, confirm your runtime emits tool-change notifications (`toolschanged` or `registerToolsChangedCallback`) |
246
- | `Tool not found` | Tab reloaded or disconnected — call `webmcp_list_tools` again to refresh |
247
- | Connection blocked | Verify `--widget-origin` matches your host page's origin (e.g., `https://myapp.com`), and relay port matches `data-relay-port` |
248
- | `Host response timeout:` | The host page took longer than the per-request timeout (default 60s) to respond. Increase via `data-request-timeout="<ms>"` on the embed script tag |
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 |
249
266
 
250
267
  ---
251
268
 
@@ -292,16 +309,6 @@ pnpm --filter @mcp-b/webmcp-local-relay build:mcpb
292
309
 
293
310
  Produces `webmcp-local-relay-<version>.mcpb` for distribution via Claude Desktop.
294
311
 
295
- ### Plugin and Skill Files
296
-
297
- | File | Purpose |
298
- | -------------------------------------------- | ---------------------------------- |
299
- | `.claude-plugin/plugin.json` | Claude Code plugin definition |
300
- | `.claude-plugin/marketplace.json` | Plugin marketplace metadata |
301
- | `.mcp.json` | MCP server configuration |
302
- | `.agents/skills/webmcp-local-relay/SKILL.md` | Claude Code skill workspace source |
303
- | `manifest.json` | MCPB bundle manifest |
304
-
305
312
  ### References
306
313
 
307
314
  - [MCP Bundle (MCPB) project](https://github.com/modelcontextprotocol/mcpb)
@@ -1,2 +1,2 @@
1
- (function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let t=`[data-webmcp-relay]`,n=`__webmcp_relay_tab_id`,r=null,i;function a(){return document.currentScript instanceof HTMLScriptElement?document.currentScript:null}let o=a(),s=o?o.hasAttribute(`data-debug`):!1;function c(...e){s&&console.warn(`[webmcp-relay-embed]`,...e)}function l(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function u(){try{let e=sessionStorage.getItem(n);if(e)return e}catch(e){c(`sessionStorage read failed, tab ID will not persist:`,e)}let e=l();try{sessionStorage.setItem(n,e)}catch(e){c(`sessionStorage write failed:`,e)}return e}function d(e){if(e?.src)try{return new URL(`widget.html`,e.src).href}catch(e){c(`Failed to resolve widget URL from script src, falling back to CDN:`,e)}else c(`Script element has no src attribute, falling back to CDN widget URL.`);return`https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay/dist/browser/widget.html`}function f(e){let t=d(e),n=e?.getAttribute(`data-relay-id`)||void 0,r=e?.getAttribute(`data-relay-workspace`)||void 0,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:u(),widgetUrl:t,widgetOrigin:new URL(t).origin}}function p(t){if(typeof t!=`string`||t.length===0)return{type:`object`,properties:{}};try{let n=JSON.parse(t);return e(n)?n:{type:`object`,properties:{}}}catch(e){return c(`Tool inputSchema is not valid JSON:`,typeof t==`string`?t.slice(0,200):t,e),{type:`object`,properties:{}}}}function m(t){return e(t)?t:(t!=null&&c(`Tool invocation args must be an object, got`,typeof t),{})}function h(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema}}function g(e){return{name:e.name,description:e.description,inputSchema:p(e.inputSchema)}}function _(){let e=navigator.modelContext;if(e&&typeof e.listTools==`function`&&typeof e.callTool==`function`)return e}function v(){let t=_();if(t)return{listTools(){return t.listTools().map(h)},invoke(e,n){return t.callTool({name:e,arguments:n})}};let n=navigator.modelContextTesting;return n&&typeof n.listTools==`function`&&typeof n.executeTool==`function`?{listTools(){return n.listTools().map(g)},async invoke(t,r){let i=await n.executeTool(t,JSON.stringify(r));if(i===null)return{isError:!0,content:[{type:`text`,text:`Tool execution interrupted by navigation`}]};let a;try{a=JSON.parse(i)}catch{throw Error(`Testing tool returned invalid JSON: ${String(i).slice(0,200)}`)}if(!e(a))throw Error(`Testing tool response was not an object`);return a}}:(c(`No WebMCP runtime found (navigator.modelContext or navigator.modelContextTesting).`),null)}function y(){let e=v();return e?Promise.resolve(e.listTools()).then(e=>Array.isArray(e)?e:[]):Promise.resolve([])}let b=!1,x=null,S=``;function C(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`undefined`;if(Array.isArray(e))return`[${e.map(C).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${C(t[e])}`).join(`,`)}}`}function w(e){return e.map(C).sort().join(`
2
- `)}function T(){b=!1,y().then(e=>{let t=w(e);t===S||!r||(S=t,r.postMessage({type:`webmcp.tools.changed`,tools:e},i.widgetOrigin))}).catch(e=>{c(`Failed to sync tool changes:`,e)})}function E(){b||(b=!0,setTimeout(T,0))}function D(){x||(x=setInterval(E,2e3))}function O(){let e=!1,t=_();if(t)try{t.addEventListener(`toolchange`,E),e=!0}catch(e){c(`addEventListener on modelContext threw:`,e)}let n=navigator.modelContextTesting;if(n){if(typeof n.addEventListener==`function`)try{n.addEventListener(`toolchange`,E),e=!0}catch(e){c(`addEventListener on modelContextTesting threw:`,e)}else if(typeof n.registerToolsChangedCallback==`function`)try{n.registerToolsChangedCallback(E),e=!0}catch(e){c(`Failed to subscribe via registerToolsChangedCallback:`,e)}}return e}function k(){if(D(),E(),O())return;let e=0,t=100,n=()=>{setTimeout(()=>{if(e++,!O()){if(e>=40){c(`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 A(e,t,n){!e||typeof e!=`object`||!(`postMessage`in e)||e.postMessage(n,t)}function j(t){return!e(t)||typeof t.requestId!=`string`||typeof t.type!=`string`?null:{requestId:t.requestId,type:t.type,toolName:t.toolName,args:t.args}}function M(e,t){y().then(n=>{A(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:n})}).catch(n=>{c(`Failed to list tools:`,n),A(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)}`})})}let N=!1;function P(t,n){if(N)return;let r=_();if(!r||typeof r.elicitInput!=`function`){c(`Elicitation bridge not installed: elicitInput not available on modelContext`);return}r.elicitInput=(r,i)=>{let a=I();return new Promise(i=>{let o=()=>{window.removeEventListener(`message`,l),clearTimeout(s)},s=setTimeout(()=>{o(),c(`Elicitation request timed out after 60s`),i({action:`decline`,content:null})},6e4),l=t=>{if(t.origin!==n)return;let r=t.data;!e(r)||r.type!==`webmcp.elicitation.response`||r.callId!==a||(o(),i(e(r.result)?r.result:{action:`decline`,content:null}))};window.addEventListener(`message`,l),A(t,n,{type:`webmcp.elicitation.request`,callId:a,params:r})})},N=!0,c(`Elicitation bridge installed`)}let F=0;function I(){return F+=1,`elicit_${String(Date.now())}_${String(F)}`}function L(t,n){let r=v();if(!r){A(n.source,n.origin,{type:`webmcp.tools.invoke.error`,requestId:t.requestId,error:`No WebMCP runtime found on this page`});return}n.source&&P(n.source,n.origin),Promise.resolve(r.invoke(String(t.toolName??``),m(t.args))).then(r=>{A(n.source,n.origin,{type:`webmcp.tools.invoke.response`,requestId:t.requestId,result:e(r)?r:{}})}).catch(e=>{A(n.source,n.origin,{type:`webmcp.tools.invoke.error`,requestId:t.requestId,error:String(e instanceof Error?e.message:e)})})}async function R(e){if(document.querySelector(t))return;let n=new URLSearchParams;n.set(`tabId`,e.tabId),n.set(`hostOrigin`,window.location.origin);let i=new URL(window.location.href);i.search=``,i.hash=``,n.set(`hostUrl`,i.href),n.set(`hostTitle`,document.title||``),n.set(`relayHost`,e.relayHost),n.set(`relayPort`,e.relayPort),n.set(`autoConnect`,e.autoConnect?`true`:`false`),e.relayId&&n.set(`relayId`,e.relayId),e.relayWorkspace&&n.set(`relayWorkspace`,e.relayWorkspace),e.requestTimeout&&n.set(`requestTimeout`,e.requestTimeout);let a=null;try{let t=await fetch(e.widgetUrl);if(!t.ok)console.warn(`[webmcp-relay-embed] Widget HTML fetch returned ${String(t.status)}; falling back to direct iframe src.`);else{let r=await t.text(),i=`<script>window.__WEBMCP_RELAY_CONFIG=${JSON.stringify(Object.fromEntries(n))};<\/script>`,o=new Blob([r.replace(`</head>`,`${i}</head>`)],{type:`text/html`});a=URL.createObjectURL(o),e.widgetOrigin=window.location.origin}}catch(e){c(`Failed to fetch widget HTML for blob URL:`,e)}let o=document.createElement(`iframe`);o.src=a??`${e.widgetUrl}?${n.toString()}`,o.style.display=`none`,o.setAttribute(`aria-hidden`,`true`),o.setAttribute(`data-webmcp-relay`,`1`),o.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(o),r=o.contentWindow,o.addEventListener(`load`,()=>{r=o.contentWindow,a&&URL.revokeObjectURL(a)}),o.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,o.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),a&&URL.revokeObjectURL(a)})}if(!document.querySelector(t)){try{i=f(o)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,t=>{if(t.origin!==i.widgetOrigin||!r||t.source!==r)return;let n=t.data;if(e(n)&&n.type===`webmcp.reload`){window.location.reload();return}let a=j(t.data);if(a){if(a.type===`webmcp.tools.list.request`){M(a,t);return}a.type===`webmcp.tools.invoke.request`&&L(a,t)}});let t=()=>{R(i).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?t():document.addEventListener(`DOMContentLoaded`,t,{once:!0}),k(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&r&&r.postMessage({type:`webmcp.connect`},i.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(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)})}})();
@@ -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]`]);function t(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function n(t){return e.has(t)}function r(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function i(e){return String(e).replace(/[\r\n]/g,``)}function a(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function o(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let s=3e3,c=[1e4,2e4,3e4],l=c.length;function u(e=window.location.search){let t=new URLSearchParams(e),i=globalThis.__WEBMCP_RELAY_CONFIG;function a(e){return t.get(e)??i?.[e]??null}let o=a(`hostOrigin`);if(!o)return null;let s=a(`hostUrl`)||o,c=a(`hostTitle`)||``,l=a(`tabId`)||r(),u=a(`relayHost`)||`127.0.0.1`,d=a(`relayPort`),f=d&&d.length>0?Number.parseInt(d,10):9333,p=a(`autoConnect`)!==`false`,m=a(`relayId`)||void 0,h=a(`relayWorkspace`)||void 0,g=a(`requestTimeout`),_=g&&g.length>0?Number.parseInt(g,10):6e4;return n(u)?!Number.isInteger(f)||f<1||f>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,d),null):!Number.isInteger(_)||_<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,g),null):{autoConnect:p,hostOrigin:o,hostTitle:c,hostUrl:s,relayHostHint:u,relayPortHint:f,...m?{relayId:m}:{},...h?{relayWorkspace:h}:{},requestTimeoutMs:_,tabId:l}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,u),null)}function d(e){return!t(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function f(){let e=u();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}S(e)}function p(e){return!t(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function m(e){return!t(e)||e.type!==`hello/accepted`?null:{type:`hello/accepted`}}function h(e){return!t(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function g(e){return o({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function _(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(g(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 v(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(g(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function y(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(g(e))}catch{}}function b(e){let t=_(e),n=new Set,r=[],i=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};e.relayPortHint!==void 0&&i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=9333;t<=9348;t+=1)i(e,t);return r}async function x(e){let t=`ws://${e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=p(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 S(e){let n=new Map,o=null,u=null,f=0,g=!1,S=null,C=500,w=null,T=`idle`,E=0,D=null,O=e=>{for(let[t,r]of n)clearTimeout(r.timeoutId),n.delete(t),r.reject(Error(e))};function k(t,i){let a=r();return new Promise((r,o)=>{let s=setTimeout(()=>{n.delete(a),o(Error(`Host response timeout: ${t}`))},e.requestTimeoutMs);n.set(a,{resolve:r,reject:o,timeoutId:s,responseType:`${t}.response`,errorType:`${t}.error`}),window.parent.postMessage({type:`${t}.request`,requestId:a,...i},e.hostOrigin)})}let A=(n,r)=>{let s=[],c=()=>{S&&(clearTimeout(S),S=null)},l=()=>{a(n,JSON.stringify({type:`tools/list`,tools:s}))};w&&(clearTimeout(w),w=null),o=r,u=n,C=500,E=0,g=!1,T=`idle`,n.addEventListener(`message`,s=>{let u;try{u=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let d=p(u);if(d){o={hello:d,host:r.host,port:r.port};return}if(m(u)){c(),g=!0,v(e,r),l();return}let f=h(u);if(f){c(),g=!1,y(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:r.host,port:r.port,message:f.message,reason:f.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,f.reason,f.message);try{n.close(1008,f.message)}catch{}return}if(!(!t(u)||typeof u.type!=`string`)){if(u.type===`ping`){a(n,JSON.stringify({type:`pong`}));return}if(u.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(u.type===`elicitation-response`){window.parent.postMessage({type:`webmcp.elicitation.response`,callId:u.callId,result:t(u.result)?u.result:{action:`decline`,content:null}},e.hostOrigin);return}if(u.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,i(u.type));return}k(`webmcp.tools.invoke`,{toolName:u.toolName,args:t(u.args)?u.args:{}}).then(e=>{a(n,JSON.stringify({type:`result`,callId:u.callId,result:e.result}))}).catch(e=>{a(n,JSON.stringify({type:`result`,callId:u.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}}),n.addEventListener(`close`,()=>{if(u===n){if(g=!1,u=null,c(),O(`WebSocket connection lost`),f+=1,f>=5){f=0,P();return}N()}},{once:!0}),n.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{n.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),k(`webmcp.tools.list`,{}).then(t=>{s=Array.isArray(t.tools)?t.tools:[],a(n,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),S=setTimeout(()=>{S=null,!(u!==n||n.readyState!==WebSocket.OPEN||g)&&(g=!0,v(e,r),l())},250)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{n.close()}catch{}})},j=async t=>{let n=await x({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):(A(n.socket,r),!0)},M=async()=>{T=`discovering`;try{for(let t of b(e))if(await j(t))return f=0,!0;return!1}finally{T===`discovering`&&(T=`idle`)}},N=()=>{if(!o||w)return;let e={host:o.host,port:o.port},t=Math.min(s,Math.round(C*(.85+Math.random()*.3)));C=Math.min(Math.round(C*1.5),s),w=setTimeout(()=>{w=null,j(e).then(e=>{if(e){f=0;return}if(f>=5){f=0,P();return}N()})},t)},P=()=>{if(w)return;if(E>=l){L();return}let e=c[E];w=setTimeout(()=>{w=null,M().then(e=>{e||(E+=1,P())})},e)},F=()=>{document.visibilityState===`visible`&&T===`dormant`&&z()},I=()=>{document.removeEventListener(`visibilitychange`,F),D!==null&&(clearInterval(D),D=null)},L=()=>{T!==`dormant`&&(T=`dormant`,w&&(clearTimeout(w),w=null),document.addEventListener(`visibilitychange`,F),D=setInterval(()=>{R()},12e4))},R=async()=>{if(T!==`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=_(e);if(i&&r(i.host,i.port),n.length!==0){T=`discovering`,I();for(let e of n)if(await j(e)){E=0;return}L()}},z=()=>{I(),C=500,E=0,M().then(e=>{e||L()})};window.addEventListener(`message`,r=>{if(r.origin!==e.hostOrigin)return;let i=r.data;if(t(i)&&i.type===`webmcp.tools.changed`){u&&g&&a(u,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(t(i)&&i.type===`webmcp.connect`){T===`dormant`?z():!u&&T!==`discovering`&&M();return}if(t(i)&&i.type===`webmcp.elicitation.request`){u&&u.readyState===WebSocket.OPEN&&a(u,JSON.stringify({type:`elicitation-request`,callId:i.callId,params:t(i.params)?i.params:{}}));return}let o=d(i);if(!o)return;let s=n.get(o.requestId);if(s){if(o.type===s.responseType){clearTimeout(s.timeoutId),n.delete(o.requestId),s.resolve(o);return}o.type===s.errorType&&(clearTimeout(s.timeoutId),n.delete(o.requestId),s.reject(Error(String(o.error||`Unknown host error`))))}}),e.autoConnect&&M().then(e=>{e||P()})}f()})();</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(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>
9
9
  </body>
10
10
  </html>
@@ -1 +1 @@
1
- (function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]);function t(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function n(t){return e.has(t)}function r(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`${String(Date.now())}_${String(Math.random()).slice(2,10)}`}function i(e){return String(e).replace(/[\r\n]/g,``)}function a(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function o(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let s=3e3,c=[1e4,2e4,3e4],l=c.length;function u(e=window.location.search){let t=new URLSearchParams(e),i=globalThis.__WEBMCP_RELAY_CONFIG;function a(e){return t.get(e)??i?.[e]??null}let o=a(`hostOrigin`);if(!o)return null;let s=a(`hostUrl`)||o,c=a(`hostTitle`)||``,l=a(`tabId`)||r(),u=a(`relayHost`)||`127.0.0.1`,d=a(`relayPort`),f=d&&d.length>0?Number.parseInt(d,10):9333,p=a(`autoConnect`)!==`false`,m=a(`relayId`)||void 0,h=a(`relayWorkspace`)||void 0,g=a(`requestTimeout`),_=g&&g.length>0?Number.parseInt(g,10):6e4;return n(u)?!Number.isInteger(f)||f<1||f>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,d),null):!Number.isInteger(_)||_<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,g),null):{autoConnect:p,hostOrigin:o,hostTitle:c,hostUrl:s,relayHostHint:u,relayPortHint:f,...m?{relayId:m}:{},...h?{relayWorkspace:h}:{},requestTimeoutMs:_,tabId:l}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,u),null)}function d(e){return!t(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function f(){let e=u();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}S(e)}function p(e){return!t(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function m(e){return!t(e)||e.type!==`hello/accepted`?null:{type:`hello/accepted`}}function h(e){return!t(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function g(e){return o({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function _(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(g(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 v(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(g(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function y(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(g(e))}catch{}}function b(e){let t=_(e),n=new Set,r=[],i=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};e.relayPortHint!==void 0&&i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=9333;t<=9348;t+=1)i(e,t);return r}async function x(e){let t=`ws://${e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=p(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 S(e){let n=new Map,o=null,u=null,f=0,g=!1,S=null,C=500,w=null,T=`idle`,E=0,D=null,O=e=>{for(let[t,r]of n)clearTimeout(r.timeoutId),n.delete(t),r.reject(Error(e))};function k(t,i){let a=r();return new Promise((r,o)=>{let s=setTimeout(()=>{n.delete(a),o(Error(`Host response timeout: ${t}`))},e.requestTimeoutMs);n.set(a,{resolve:r,reject:o,timeoutId:s,responseType:`${t}.response`,errorType:`${t}.error`}),window.parent.postMessage({type:`${t}.request`,requestId:a,...i},e.hostOrigin)})}let A=(n,r)=>{let s=[],c=()=>{S&&(clearTimeout(S),S=null)},l=()=>{a(n,JSON.stringify({type:`tools/list`,tools:s}))};w&&(clearTimeout(w),w=null),o=r,u=n,C=500,E=0,g=!1,T=`idle`,n.addEventListener(`message`,s=>{let u;try{u=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let d=p(u);if(d){o={hello:d,host:r.host,port:r.port};return}if(m(u)){c(),g=!0,v(e,r),l();return}let f=h(u);if(f){c(),g=!1,y(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:r.host,port:r.port,message:f.message,reason:f.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,f.reason,f.message);try{n.close(1008,f.message)}catch{}return}if(!(!t(u)||typeof u.type!=`string`)){if(u.type===`ping`){a(n,JSON.stringify({type:`pong`}));return}if(u.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(u.type===`elicitation-response`){window.parent.postMessage({type:`webmcp.elicitation.response`,callId:u.callId,result:t(u.result)?u.result:{action:`decline`,content:null}},e.hostOrigin);return}if(u.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,i(u.type));return}k(`webmcp.tools.invoke`,{toolName:u.toolName,args:t(u.args)?u.args:{}}).then(e=>{a(n,JSON.stringify({type:`result`,callId:u.callId,result:e.result}))}).catch(e=>{a(n,JSON.stringify({type:`result`,callId:u.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}}),n.addEventListener(`close`,()=>{if(u===n){if(g=!1,u=null,c(),O(`WebSocket connection lost`),f+=1,f>=5){f=0,P();return}N()}},{once:!0}),n.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{n.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),k(`webmcp.tools.list`,{}).then(t=>{s=Array.isArray(t.tools)?t.tools:[],a(n,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),S=setTimeout(()=>{S=null,!(u!==n||n.readyState!==WebSocket.OPEN||g)&&(g=!0,v(e,r),l())},250)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{n.close()}catch{}})},j=async t=>{let n=await x({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):(A(n.socket,r),!0)},M=async()=>{T=`discovering`;try{for(let t of b(e))if(await j(t))return f=0,!0;return!1}finally{T===`discovering`&&(T=`idle`)}},N=()=>{if(!o||w)return;let e={host:o.host,port:o.port},t=Math.min(s,Math.round(C*(.85+Math.random()*.3)));C=Math.min(Math.round(C*1.5),s),w=setTimeout(()=>{w=null,j(e).then(e=>{if(e){f=0;return}if(f>=5){f=0,P();return}N()})},t)},P=()=>{if(w)return;if(E>=l){L();return}let e=c[E];w=setTimeout(()=>{w=null,M().then(e=>{e||(E+=1,P())})},e)},F=()=>{document.visibilityState===`visible`&&T===`dormant`&&z()},I=()=>{document.removeEventListener(`visibilitychange`,F),D!==null&&(clearInterval(D),D=null)},L=()=>{T!==`dormant`&&(T=`dormant`,w&&(clearTimeout(w),w=null),document.addEventListener(`visibilitychange`,F),D=setInterval(()=>{R()},12e4))},R=async()=>{if(T!==`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=_(e);if(i&&r(i.host,i.port),n.length!==0){T=`discovering`,I();for(let e of n)if(await j(e)){E=0;return}L()}},z=()=>{I(),C=500,E=0,M().then(e=>{e||L()})};window.addEventListener(`message`,r=>{if(r.origin!==e.hostOrigin)return;let i=r.data;if(t(i)&&i.type===`webmcp.tools.changed`){u&&g&&a(u,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(t(i)&&i.type===`webmcp.connect`){T===`dormant`?z():!u&&T!==`discovering`&&M();return}if(t(i)&&i.type===`webmcp.elicitation.request`){u&&u.readyState===WebSocket.OPEN&&a(u,JSON.stringify({type:`elicitation-request`,callId:i.callId,params:t(i.params)?i.params:{}}));return}let o=d(i);if(!o)return;let s=n.get(o.requestId);if(s){if(o.type===s.responseType){clearTimeout(s.timeoutId),n.delete(o.requestId),s.resolve(o);return}o.type===s.errorType&&(clearTimeout(s.timeoutId),n.delete(o.requestId),s.reject(Error(String(o.error||`Unknown host error`))))}}),e.autoConnect&&M().then(e=>{e||P()})}f()})();
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()})();
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as parseCliOptions, t as LocalRelayMcpServer } from "./mcpRelayServer-BT1ct1Cc.mjs";
2
+ import { n as parseCliOptions, r as printHelp, t as LocalRelayMcpServer } from "./mcpRelayServer-BxI6zEc2.mjs";
3
3
 
4
4
  //#region src/cli.ts
5
5
  let options;
@@ -9,17 +9,12 @@ try {
9
9
  process.stderr.write(`[webmcp-local-relay] error: ${err instanceof Error ? err.message : String(err)}\n`);
10
10
  process.exit(1);
11
11
  }
12
+ if (options.help) {
13
+ printHelp();
14
+ process.exit(0);
15
+ }
12
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");
13
- const relay = new LocalRelayMcpServer({ bridgeOptions: {
14
- host: options.host,
15
- port: options.port,
16
- portExplicitlySet: options.portExplicitlySet,
17
- allowedOrigins: options.allowedOrigins,
18
- ...options.label ? { label: options.label } : {},
19
- ...options.relayId ? { relayId: options.relayId } : {},
20
- ...options.workspace ? { workspace: options.workspace } : {},
21
- ...options.maxPayloadBytes ? { maxPayloadBytes: options.maxPayloadBytes } : {}
22
- } });
17
+ const relay = new LocalRelayMcpServer({ bridgeOptions: options });
23
18
  try {
24
19
  await relay.start();
25
20
  } catch (err) {
@@ -35,24 +30,51 @@ try {
35
30
  process.stderr.write(`[webmcp-local-relay] error: failed to start stdio transport: ${message}\n`);
36
31
  process.exit(1);
37
32
  }
38
- if (relay.bridge.mode === "server") process.stderr.write(`[webmcp-local-relay] server mode: listening on ws://${options.host}:${relay.bridge.port} (allowed origins: ${options.allowedOrigins.join(", ")})\n`);
39
- else process.stderr.write(`[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\n`);
33
+ let parentCheckInterval;
34
+ let shuttingDown = false;
40
35
  /**
41
36
  * Gracefully shuts down bridge and MCP server for process termination signals.
37
+ * Force-exits after 5 seconds if graceful shutdown does not complete, preventing hangs.
42
38
  */
43
39
  const shutdown = async (signal) => {
44
40
  if (shuttingDown) return;
45
41
  shuttingDown = true;
42
+ if (parentCheckInterval) {
43
+ clearInterval(parentCheckInterval);
44
+ parentCheckInterval = void 0;
45
+ }
46
+ const forceExitTimer = setTimeout(() => {
47
+ process.stderr.write("[webmcp-local-relay] warn: graceful shutdown timed out, force exiting\n");
48
+ process.exit(1);
49
+ }, 5e3);
46
50
  process.stderr.write(`[webmcp-local-relay] received ${signal}, shutting down\n`);
47
51
  try {
48
52
  await relay.stop();
49
53
  } catch (err) {
50
- process.stderr.write(`[webmcp-local-relay] error during shutdown: ${err instanceof Error ? err.message : err}\n`);
54
+ process.stderr.write(`[webmcp-local-relay] error during shutdown: ${err instanceof Error ? err.message : String(err)}\n`);
55
+ clearTimeout(forceExitTimer);
51
56
  process.exit(1);
52
57
  }
58
+ clearTimeout(forceExitTimer);
53
59
  process.exit(0);
54
60
  };
55
- let shuttingDown = false;
61
+ process.stdin.on("end", () => {
62
+ shutdown("stdin-closed");
63
+ });
64
+ process.stdin.on("error", () => {
65
+ shutdown("stdin-error");
66
+ });
67
+ process.stdout.on("error", () => {
68
+ shutdown("stdout-error");
69
+ });
70
+ if (process.platform !== "win32") {
71
+ parentCheckInterval = setInterval(() => {
72
+ if (process.ppid === 1) shutdown("parent-exited");
73
+ }, 3e4);
74
+ parentCheckInterval.unref();
75
+ }
76
+ if (relay.bridge.mode === "server") process.stderr.write(`[webmcp-local-relay] server mode: listening on ws://${options.host}:${relay.bridge.port} (allowed origins: ${options.allowedOrigins.join(", ")})\n`);
77
+ else process.stderr.write(`[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\n`);
56
78
  process.on("uncaughtException", (err) => {
57
79
  process.stderr.write(`[webmcp-local-relay] error: uncaught exception: ${err.stack ?? err.message}\n`);
58
80
  shutdown("uncaughtException");
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 } from './cli-utils.js';\nimport { LocalRelayMcpServer } from './mcpRelayServer.js';\n\nlet options: ReturnType<typeof parseCliOptions>;\ntry {\n options = parseCliOptions(process.argv.slice(2));\n} catch (err) {\n process.stderr.write(\n `[webmcp-local-relay] error: ${err instanceof Error ? err.message : String(err)}\\n`\n );\n process.exit(1);\n}\n\nif (options.allowedOrigins.includes('*')) {\n process.stderr.write(\n '[webmcp-local-relay] WARNING: accepting connections from ALL host page origins. Use --widget-origin to restrict.\\n'\n );\n}\n\nconst bridgeOptions = {\n host: options.host,\n port: options.port,\n portExplicitlySet: options.portExplicitlySet,\n allowedOrigins: options.allowedOrigins,\n ...(options.label ? { label: options.label } : {}),\n ...(options.relayId ? { relayId: options.relayId } : {}),\n ...(options.workspace ? { workspace: options.workspace } : {}),\n ...(options.maxPayloadBytes ? { maxPayloadBytes: options.maxPayloadBytes } : {}),\n};\n\nconst relay = new LocalRelayMcpServer({\n bridgeOptions,\n});\n\ntry {\n await relay.start();\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes('EACCES')) {\n process.stderr.write(\n `[webmcp-local-relay] error: insufficient permissions to bind to ${options.host}:${options.port}.\\n`\n );\n } else {\n process.stderr.write(`[webmcp-local-relay] error: failed to start bridge server: ${message}\\n`);\n }\n process.exit(1);\n}\n\ntry {\n await relay.startStdio();\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[webmcp-local-relay] error: failed to start stdio transport: ${message}\\n`);\n process.exit(1);\n}\n\nif (relay.bridge.mode === 'server') {\n process.stderr.write(\n `[webmcp-local-relay] server mode: listening on ws://${options.host}:${relay.bridge.port} (allowed origins: ${options.allowedOrigins.join(', ')})\\n`\n );\n} else {\n process.stderr.write(\n `[webmcp-local-relay] client mode: proxying through existing relay at ws://${options.host}:${relay.bridge.port}\\n`\n );\n}\n\n/**\n * Gracefully shuts down bridge and MCP server for process termination signals.\n */\nconst shutdown = async (signal: string) => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n process.stderr.write(`[webmcp-local-relay] received ${signal}, shutting down\\n`);\n try {\n await relay.stop();\n } catch (err) {\n process.stderr.write(\n `[webmcp-local-relay] error during shutdown: ${err instanceof Error ? err.message : err}\\n`\n );\n process.exit(1);\n }\n process.exit(0);\n};\n\nlet shuttingDown = false;\n\nprocess.on('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":";;;;AAKA,IAAI;AACJ,IAAI;CACF,UAAU,gBAAgB,QAAQ,KAAK,MAAM,EAAE,CAAC;SACzC,KAAK;CACZ,QAAQ,OAAO,MACb,+BAA+B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IACjF;CACD,QAAQ,KAAK,EAAE;;AAGjB,IAAI,QAAQ,eAAe,SAAS,IAAI,EACtC,QAAQ,OAAO,MACb,qHACD;AAcH,MAAM,QAAQ,IAAI,oBAAoB,EACpC;CAXA,MAAM,QAAQ;CACd,MAAM,QAAQ;CACd,mBAAmB,QAAQ;CAC3B,gBAAgB,QAAQ;CACxB,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;CACjD,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;CACvD,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;CAC7D,GAAI,QAAQ,kBAAkB,EAAE,iBAAiB,QAAQ,iBAAiB,GAAG,EAAE;CAIlE,EACd,CAAC;AAEF,IAAI;CACF,MAAM,MAAM,OAAO;SACZ,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;CAChE,IAAI,QAAQ,SAAS,SAAS,EAC5B,QAAQ,OAAO,MACb,mEAAmE,QAAQ,KAAK,GAAG,QAAQ,KAAK,KACjG;MAED,QAAQ,OAAO,MAAM,8DAA8D,QAAQ,IAAI;CAEjG,QAAQ,KAAK,EAAE;;AAGjB,IAAI;CACF,MAAM,MAAM,YAAY;SACjB,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;CAChE,QAAQ,OAAO,MAAM,gEAAgE,QAAQ,IAAI;CACjG,QAAQ,KAAK,EAAE;;AAGjB,IAAI,MAAM,OAAO,SAAS,UACxB,QAAQ,OAAO,MACb,uDAAuD,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,qBAAqB,QAAQ,eAAe,KAAK,KAAK,CAAC,KACjJ;KAED,QAAQ,OAAO,MACb,6EAA6E,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,IAChH;;;;AAMH,MAAM,WAAW,OAAO,WAAmB;CACzC,IAAI,cACF;CAEF,eAAe;CACf,QAAQ,OAAO,MAAM,iCAAiC,OAAO,mBAAmB;CAChF,IAAI;EACF,MAAM,MAAM,MAAM;UACX,KAAK;EACZ,QAAQ,OAAO,MACb,+CAA+C,eAAe,QAAQ,IAAI,UAAU,IAAI,IACzF;EACD,QAAQ,KAAK,EAAE;;CAEjB,QAAQ,KAAK,EAAE;;AAGjB,IAAI,eAAe;AAEnB,QAAQ,GAAG,sBAAsB,QAAQ;CACvC,QAAQ,OAAO,MACb,mDAAmD,IAAI,SAAS,IAAI,QAAQ,IAC7E;CACD,AAAK,SAAS,oBAAoB;EAClC;AACF,QAAQ,GAAG,uBAAuB,WAAW;CAC3C,MAAM,UAAU,kBAAkB,QAAS,OAAO,SAAS,OAAO,UAAW,OAAO,OAAO;CAC3F,QAAQ,OAAO,MAAM,oDAAoD,QAAQ,IAAI;CACrF,AAAK,SAAS,qBAAqB;EACnC;AAEF,QAAQ,GAAG,gBAAgB;CACzB,AAAK,SAAS,SAAS;EACvB;AACF,QAAQ,GAAG,iBAAiB;CAC1B,AAAK,SAAS,UAAU;EACxB"}
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":";;;;AAKA,IAAI;AACJ,IAAI;CACF,UAAU,gBAAgB,QAAQ,KAAK,MAAM,EAAE,CAAC;SACzC,KAAK;CACZ,QAAQ,OAAO,MACb,+BAA+B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IACjF;CACD,QAAQ,KAAK,EAAE;;AAGjB,IAAI,QAAQ,MAAM;CAChB,WAAW;CACX,QAAQ,KAAK,EAAE;;AAGjB,IAAI,QAAQ,eAAe,SAAS,IAAI,EACtC,QAAQ,OAAO,MACb,qHACD;AAGH,MAAM,QAAQ,IAAI,oBAAoB,EACpC,eAAe,SAChB,CAAC;AAEF,IAAI;CACF,MAAM,MAAM,OAAO;SACZ,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;CAChE,IAAI,QAAQ,SAAS,SAAS,EAC5B,QAAQ,OAAO,MACb,mEAAmE,QAAQ,KAAK,GAAG,QAAQ,KAAK,KACjG;MAED,QAAQ,OAAO,MAAM,8DAA8D,QAAQ,IAAI;CAEjG,QAAQ,KAAK,EAAE;;AAGjB,IAAI;CACF,MAAM,MAAM,YAAY;SACjB,KAAK;CACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;CAChE,QAAQ,OAAO,MAAM,gEAAgE,QAAQ,IAAI;CACjG,QAAQ,KAAK,EAAE;;AAKjB,IAAI;AACJ,IAAI,eAAe;;;;;AAMnB,MAAM,WAAW,OAAO,WAAmB;CACzC,IAAI,cACF;CAEF,eAAe;CACf,IAAI,qBAAqB;EACvB,cAAc,oBAAoB;EAClC,sBAAsB;;CAIxB,MAAM,iBAAiB,iBAAiB;EACtC,QAAQ,OAAO,MAAM,0EAA0E;EAC/F,QAAQ,KAAK,EAAE;IACd,IAAM;CAET,QAAQ,OAAO,MAAM,iCAAiC,OAAO,mBAAmB;CAEhF,IAAI;EACF,MAAM,MAAM,MAAM;UACX,KAAK;EACZ,QAAQ,OAAO,MACb,+CAA+C,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IACjG;EACD,aAAa,eAAe;EAC5B,QAAQ,KAAK,EAAE;;CAEjB,aAAa,eAAe;CAC5B,QAAQ,KAAK,EAAE;;AAGjB,QAAQ,MAAM,GAAG,aAAa;CAC5B,AAAK,SAAS,eAAe;EAC7B;AACF,QAAQ,MAAM,GAAG,eAAe;CAC9B,AAAK,SAAS,cAAc;EAC5B;AACF,QAAQ,OAAO,GAAG,eAAe;CAC/B,AAAK,SAAS,eAAe;EAC7B;AAEF,IAAI,QAAQ,aAAa,SAAS;CAChC,sBAAsB,kBAAkB;EACtC,IAAI,QAAQ,SAAS,GACnB,AAAK,SAAS,gBAAgB;IAE/B,IAAO;CACV,oBAAoB,OAAO;;AAG7B,IAAI,MAAM,OAAO,SAAS,UACxB,QAAQ,OAAO,MACb,uDAAuD,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,qBAAqB,QAAQ,eAAe,KAAK,KAAK,CAAC,KACjJ;KAED,QAAQ,OAAO,MACb,6EAA6E,QAAQ,KAAK,GAAG,MAAM,OAAO,KAAK,IAChH;AAGH,QAAQ,GAAG,sBAAsB,QAAQ;CACvC,QAAQ,OAAO,MACb,mDAAmD,IAAI,SAAS,IAAI,QAAQ,IAC7E;CACD,AAAK,SAAS,oBAAoB;EAClC;AACF,QAAQ,GAAG,uBAAuB,WAAW;CAC3C,MAAM,UAAU,kBAAkB,QAAS,OAAO,SAAS,OAAO,UAAW,OAAO,OAAO;CAC3F,QAAQ,OAAO,MAAM,oDAAoD,QAAQ,IAAI;CACrF,AAAK,SAAS,qBAAqB;EACnC;AAEF,QAAQ,GAAG,gBAAgB;CACzB,AAAK,SAAS,SAAS;EACvB;AACF,QAAQ,GAAG,iBAAiB;CAC1B,AAAK,SAAS,UAAU;EACxB"}