@mcp-b/webmcp-local-relay 2.3.2 → 3.0.0-beta.20260702171253
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 +13 -13
- package/dist/browser/embed.js +2 -1
- package/dist/browser/widget.html +1 -1
- package/dist/browser/widget.js +1 -1
- package/dist/cli.mjs +3 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +21 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{mcpRelayServer-BL8kNwIK.mjs → mcpRelayServer-BT1ct1Cc.mjs} +57 -16
- package/dist/mcpRelayServer-BT1ct1Cc.mjs.map +1 -0
- package/package.json +8 -9
- package/.claude-plugin/marketplace.json +0 -14
- package/.claude-plugin/plugin.json +0 -8
- package/dist/mcpRelayServer-BL8kNwIK.mjs.map +0 -1
- package/skills/webmcp-local-relay/SKILL.md +0 -72
package/README.md
CHANGED
|
@@ -58,14 +58,14 @@ Add one script tag to expose your page's WebMCP tools to the relay:
|
|
|
58
58
|
<script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"></script>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
That's it. If your page already registers tools on `
|
|
61
|
+
That's it. If your page already registers tools on `document.modelContext`, they'll be picked up automatically.
|
|
62
62
|
|
|
63
63
|
New to WebMCP? Here's the full setup:
|
|
64
64
|
|
|
65
65
|
```html
|
|
66
66
|
<script src="https://cdn.jsdelivr.net/npm/@mcp-b/global@latest/dist/index.iife.js"></script>
|
|
67
67
|
<script>
|
|
68
|
-
|
|
68
|
+
document.modelContext.registerTool({
|
|
69
69
|
name: 'get_page_title',
|
|
70
70
|
description: 'Get the current page title',
|
|
71
71
|
inputSchema: { type: 'object', properties: {} },
|
|
@@ -199,14 +199,14 @@ npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com
|
|
|
199
199
|
│ Widget iframe │
|
|
200
200
|
│ embed.js injects widget.html │
|
|
201
201
|
└──────────────────┬───────────────────┘
|
|
202
|
-
│
|
|
202
|
+
│ document.modelContext
|
|
203
203
|
┌──────────────────▼───────────────────┐
|
|
204
204
|
│ Host page │
|
|
205
205
|
│ WebMCP runtime + registered tools │
|
|
206
206
|
└──────────────────────────────────────┘
|
|
207
207
|
```
|
|
208
208
|
|
|
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 `
|
|
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
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.
|
|
211
211
|
|
|
212
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.
|
|
@@ -220,7 +220,7 @@ Supported page runtimes:
|
|
|
220
220
|
|
|
221
221
|
Runtime dispatch behavior in the browser embed/widget layer:
|
|
222
222
|
|
|
223
|
-
- Uses `
|
|
223
|
+
- Uses `document.modelContext.listTools` + `callTool` when present.
|
|
224
224
|
- Falls back to `navigator.modelContextTesting.listTools` + `executeTool`.
|
|
225
225
|
|
|
226
226
|
### WebMCP Standard Status
|
|
@@ -229,7 +229,7 @@ WebMCP is an emerging web platform proposal. This relay works today with polyfil
|
|
|
229
229
|
|
|
230
230
|
- [W3C WebML CG draft](https://webmachinelearning.github.io/webmcp/)
|
|
231
231
|
- [Proposal repository](https://github.com/webmachinelearning/webmcp)
|
|
232
|
-
- [Proposal details (`
|
|
232
|
+
- [Proposal details (`document.modelContext`, `registerTool`, etc.)](https://github.com/webmachinelearning/webmcp/blob/main/docs/proposal.md)
|
|
233
233
|
|
|
234
234
|
For Chromium/Chrome Canary native preview testing:
|
|
235
235
|
|
|
@@ -294,13 +294,13 @@ Produces `webmcp-local-relay-<version>.mcpb` for distribution via Claude Desktop
|
|
|
294
294
|
|
|
295
295
|
### Plugin and Skill Files
|
|
296
296
|
|
|
297
|
-
| File
|
|
298
|
-
|
|
|
299
|
-
| `.claude-plugin/plugin.json`
|
|
300
|
-
| `.claude-plugin/marketplace.json`
|
|
301
|
-
| `.mcp.json`
|
|
302
|
-
|
|
|
303
|
-
| `manifest.json`
|
|
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
304
|
|
|
305
305
|
### References
|
|
306
306
|
|
package/dist/browser/embed.js
CHANGED
|
@@ -1 +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)}
|
|
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)})}})();
|
package/dist/browser/widget.html
CHANGED
|
@@ -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;function
|
|
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>
|
|
9
9
|
</body>
|
|
10
10
|
</html>
|
package/dist/browser/widget.js
CHANGED
|
@@ -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;function
|
|
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()})();
|
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-
|
|
2
|
+
import { n as parseCliOptions, t as LocalRelayMcpServer } from "./mcpRelayServer-BT1ct1Cc.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
let options;
|
|
@@ -17,7 +17,8 @@ const relay = new LocalRelayMcpServer({ bridgeOptions: {
|
|
|
17
17
|
allowedOrigins: options.allowedOrigins,
|
|
18
18
|
...options.label ? { label: options.label } : {},
|
|
19
19
|
...options.relayId ? { relayId: options.relayId } : {},
|
|
20
|
-
...options.workspace ? { workspace: options.workspace } : {}
|
|
20
|
+
...options.workspace ? { workspace: options.workspace } : {},
|
|
21
|
+
...options.maxPayloadBytes ? { maxPayloadBytes: options.maxPayloadBytes } : {}
|
|
21
22
|
} });
|
|
22
23
|
try {
|
|
23
24
|
await relay.start();
|
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};\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;
|
|
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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -459,6 +459,7 @@ declare const RelayServerToClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodOb
|
|
|
459
459
|
uri: z.ZodString;
|
|
460
460
|
description: z.ZodOptional<z.ZodString>;
|
|
461
461
|
mimeType: z.ZodOptional<z.ZodString>;
|
|
462
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
462
463
|
annotations: z.ZodOptional<z.ZodObject<{
|
|
463
464
|
audience: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
464
465
|
user: "user";
|
|
@@ -722,7 +723,7 @@ interface RelayBridgeServerOptions {
|
|
|
722
723
|
* Allowed host page origins reported by browser `hello` messages.
|
|
723
724
|
* Use `["*"]` to allow all origins.
|
|
724
725
|
*
|
|
725
|
-
* Permissive by default for zero-config developer experience
|
|
726
|
+
* Permissive by default for zero-config developer experience: any browser
|
|
726
727
|
* page can connect and register tools without additional setup. For production
|
|
727
728
|
* or shared machines, restrict to trusted host origins via the
|
|
728
729
|
* `--widget-origin` CLI flag or by passing explicit origins here.
|
|
@@ -732,7 +733,7 @@ interface RelayBridgeServerOptions {
|
|
|
732
733
|
allowedOrigins?: string[];
|
|
733
734
|
/**
|
|
734
735
|
* Maximum WebSocket payload size in bytes.
|
|
735
|
-
* @defaultValue `
|
|
736
|
+
* @defaultValue `10000000`
|
|
736
737
|
*/
|
|
737
738
|
maxPayloadBytes?: number;
|
|
738
739
|
/**
|
|
@@ -882,6 +883,9 @@ declare class RelayBridgeServer extends EventEmitter {
|
|
|
882
883
|
private pushToolsToRelayClients;
|
|
883
884
|
/**
|
|
884
885
|
* Handles source disconnection and rejects in-flight calls owned by that source.
|
|
886
|
+
*
|
|
887
|
+
* Guarded against double-call: both 'error' and 'close' events fire on
|
|
888
|
+
* socket failure, but cleanup (and the stateChanged emission) runs only once.
|
|
885
889
|
*/
|
|
886
890
|
private onSocketClose;
|
|
887
891
|
private startHeartbeat;
|
|
@@ -933,6 +937,7 @@ interface CliOptions {
|
|
|
933
937
|
label?: string;
|
|
934
938
|
workspace?: string;
|
|
935
939
|
relayId?: string;
|
|
940
|
+
maxPayloadBytes?: number;
|
|
936
941
|
}
|
|
937
942
|
/**
|
|
938
943
|
* Parses supported CLI flags for relay startup.
|
|
@@ -985,6 +990,7 @@ declare class LocalRelayMcpServer {
|
|
|
985
990
|
private readonly dynamicToolSignature;
|
|
986
991
|
private syncing;
|
|
987
992
|
private syncRequested;
|
|
993
|
+
private syncDebounceTimer;
|
|
988
994
|
private connected;
|
|
989
995
|
/**
|
|
990
996
|
* Creates a local relay MCP server with static and dynamic tool registration.
|
|
@@ -1040,6 +1046,15 @@ declare class LocalRelayMcpServer {
|
|
|
1040
1046
|
* because cmd.exe interprets `&`, `|`, and other metacharacters in URLs.
|
|
1041
1047
|
*/
|
|
1042
1048
|
private openInBrowser;
|
|
1049
|
+
/**
|
|
1050
|
+
* Debounces rapid stateChanged events into a single sync pass.
|
|
1051
|
+
*
|
|
1052
|
+
* Browser reconnections emit multiple stateChanged events in quick
|
|
1053
|
+
* succession (hello, tools/list). Without debouncing, each triggers a
|
|
1054
|
+
* full sync that removes and re-registers tools — sending per-operation
|
|
1055
|
+
* notifications through the stdio transport and overwhelming the agent.
|
|
1056
|
+
*/
|
|
1057
|
+
private debouncedSyncDynamicTools;
|
|
1043
1058
|
/**
|
|
1044
1059
|
* Coalesces concurrent sync requests into a single serialized sync loop.
|
|
1045
1060
|
*/
|
|
@@ -1068,6 +1083,10 @@ declare class LocalRelayMcpServer {
|
|
|
1068
1083
|
private dynamicToolDescription;
|
|
1069
1084
|
/**
|
|
1070
1085
|
* Produces a stable signature for change detection of dynamic tool metadata.
|
|
1086
|
+
*
|
|
1087
|
+
* Deliberately excludes `sourceId` (which is a per-connection UUID that
|
|
1088
|
+
* changes on every browser reconnect) so that a reconnecting tab with the
|
|
1089
|
+
* same tools does not trigger a needless remove+re-register cycle.
|
|
1071
1090
|
*/
|
|
1072
1091
|
private toolSignature;
|
|
1073
1092
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/protocol.ts","../src/schemas.ts","../src/registry.ts","../src/bridgeServer.ts","../src/cli-utils.ts","../src/mcpRelayServer.ts","../src/naming.ts"],"mappings":";;;;;;;;;cAca,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAUpB,iBAAA,EAAiB,CAAA,CAAA,SAAA;;;;;;cAKjB,qBAAA,EAAqB,CAAA,CAAA,WAAA,CAAA,CAAA,CAAA,SAAA,CAAA,CAAA,CAAA,SAAA,EAAA,CAAA,CAAA,UAAA;;;;;cAMrB,yBAAA,EAA2B,MAAA;;;;KAQ5B,SAAA,GAAY,CAAA,CAAE,KAAA,QAAa,oBAAA;;;;KAK3B,oBAAA,GAAuB,iBAAA;;;;KAKvB,mBAAA,GAAsB,gBAAA;;;;KAKtB,eAAA,GAAkB,OAAA,CAAQ,CAAA,CAAE,KAAA,QAAa,qBAAA;;;;;;;iBAQrC,oBAAA,CAAqB,OAAA,EAAS,CAAA,CAAE,KAAA,QAAa,iBAAA,IAAqB,SAAA;;;;;;cCrDrE,yBAAA,EAAyB,CAAA,CAAA,SAAA;;;;;;;;;;;cAwEzB,2BAAA,EAA2B,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAY5B,qBAAA,GAAwB,CAAA,CAAE,KAAA,QAAa,2BAAA;;;;KAKvC,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;;;;cAKpC,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;;;;;;;KAYtB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,qBAAA;;;;cAKhC,wBAAA,EAAwB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;KAUzB,kBAAA,GAAqB,CAAA,CAAE,KAAA,QAAa,wBAAA;AD3FhD;;;AAAA,cCgGa,+BAAA,EAA+B,CAAA,CAAA,SAAA;;;;;;KAOhC,yBAAA,GAA4B,CAAA,CAAE,KAAA,QAAa,+BAAA;;;;cAK1C,+BAAA,EAA+B,CAAA,CAAA,SAAA;;;;;;AD7F5C;;KCsGY,yBAAA,GAA4B,CAAA,CAAE,KAAA,QAAa,+BAAA;;;;cAwC1C,2BAAA,EAA2B,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAa5B,qBAAA,GAAwB,CAAA,CAAE,KAAA,QAAa,2BAAA;;;;;;;cAStC,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;KAetB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,qBAAA;;;;cA6BhC,gCAAA,EAAgC,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;KASjC,0BAAA,GAA6B,CAAA,CAAE,KAAA,QAAa,gCAAA;;;;cAoC3C,gCAAA,EAAgC,CAAA,CAAA,qBAAA,EAAA,CAAA,CAAA,SAAA
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/protocol.ts","../src/schemas.ts","../src/registry.ts","../src/bridgeServer.ts","../src/cli-utils.ts","../src/mcpRelayServer.ts","../src/naming.ts"],"mappings":"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"}
|
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-
|
|
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-BT1ct1Cc.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 };
|
|
@@ -617,6 +617,10 @@ const SUPPORTED_SUBPROTOCOLS = new Set([
|
|
|
617
617
|
RELAY_DISCOVERY_PROTOCOL,
|
|
618
618
|
RELAY_INTERNAL_PROTOCOL
|
|
619
619
|
]);
|
|
620
|
+
function formatPayloadLimit(bytes) {
|
|
621
|
+
if (bytes >= 1e6 && bytes % 1e6 === 0) return `${String(bytes / 1e6)}MB`;
|
|
622
|
+
return `${String(bytes)} bytes`;
|
|
623
|
+
}
|
|
620
624
|
/**
|
|
621
625
|
* WebSocket relay between browser widget frames and MCP server calls.
|
|
622
626
|
*
|
|
@@ -682,7 +686,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
682
686
|
this.portRangeEnd = options.portRangeEnd ?? Math.max(9348, this.preferredPort);
|
|
683
687
|
this.persistPath = options.persistPath ?? defaultRelayPortPersistPath();
|
|
684
688
|
this.allowedOrigins = options.allowedOrigins ?? ["*"];
|
|
685
|
-
this.maxPayloadBytes = options.maxPayloadBytes ??
|
|
689
|
+
this.maxPayloadBytes = options.maxPayloadBytes ?? 1e7;
|
|
686
690
|
this.invokeTimeoutMs = options.invokeTimeoutMs ?? 25e3;
|
|
687
691
|
this.label = options.label;
|
|
688
692
|
this.workspace = options.workspace;
|
|
@@ -690,7 +694,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
690
694
|
this.instanceId = randomUUID();
|
|
691
695
|
if (this.preferredPort !== 0 && (this.preferredPort < 1 || this.preferredPort > 65535)) throw new Error(`Invalid port ${this.preferredPort}. Port must be 0 (auto-assign) or between 1 and 65535.`);
|
|
692
696
|
if (this.portRangeEnd < this.preferredPort && this.preferredPort !== 0) throw new Error(`Invalid port range ${this.preferredPort}-${this.portRangeEnd}. rangeEnd must be greater than or equal to the preferred port.`);
|
|
693
|
-
if (this.maxPayloadBytes <= 0) throw new Error(`Invalid maxPayloadBytes ${this.maxPayloadBytes}. Must be
|
|
697
|
+
if (!Number.isInteger(this.maxPayloadBytes) || this.maxPayloadBytes <= 0) throw new Error(`Invalid maxPayloadBytes ${this.maxPayloadBytes}. Must be a positive integer.`);
|
|
694
698
|
if (this.invokeTimeoutMs <= 0) throw new Error(`Invalid invokeTimeoutMs ${this.invokeTimeoutMs}. Must be greater than 0.`);
|
|
695
699
|
}
|
|
696
700
|
/**
|
|
@@ -889,12 +893,13 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
889
893
|
socket.on("message", (raw) => {
|
|
890
894
|
this.onSocketMessage(connectionId, raw);
|
|
891
895
|
});
|
|
892
|
-
socket.on("close", () => {
|
|
893
|
-
this.onSocketClose(connectionId);
|
|
896
|
+
socket.on("close", (code) => {
|
|
897
|
+
this.onSocketClose(connectionId, code);
|
|
894
898
|
});
|
|
895
899
|
socket.on("error", (err) => {
|
|
896
900
|
process.stderr.write(`[webmcp-local-relay] warn: socket error for connection ${connectionId}: ${err.message}\n`);
|
|
897
|
-
|
|
901
|
+
const code = err.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? 1009 : void 0;
|
|
902
|
+
this.onSocketClose(connectionId, code);
|
|
898
903
|
});
|
|
899
904
|
this.startHeartbeat(connectionId);
|
|
900
905
|
});
|
|
@@ -1049,7 +1054,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1049
1054
|
sendElicitationResponse(connectionId, callId, result) {
|
|
1050
1055
|
const socket = this.socketByConnectionId.get(connectionId);
|
|
1051
1056
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
1052
|
-
process.stderr.write(`[webmcp-local-relay] warn: cannot send elicitation response
|
|
1057
|
+
process.stderr.write(`[webmcp-local-relay] warn: cannot send elicitation response: source ${connectionId} disconnected\n`);
|
|
1053
1058
|
return;
|
|
1054
1059
|
}
|
|
1055
1060
|
const message = {
|
|
@@ -1140,18 +1145,24 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1140
1145
|
}
|
|
1141
1146
|
/**
|
|
1142
1147
|
* Handles source disconnection and rejects in-flight calls owned by that source.
|
|
1148
|
+
*
|
|
1149
|
+
* Guarded against double-call: both 'error' and 'close' events fire on
|
|
1150
|
+
* socket failure, but cleanup (and the stateChanged emission) runs only once.
|
|
1143
1151
|
*/
|
|
1144
|
-
onSocketClose(connectionId) {
|
|
1152
|
+
onSocketClose(connectionId, code) {
|
|
1153
|
+
if (!this.socketByConnectionId.has(connectionId)) return;
|
|
1145
1154
|
this.stopHeartbeat(connectionId);
|
|
1146
1155
|
this.relayClientConnectionIds.delete(connectionId);
|
|
1147
1156
|
this.registry.removeConnection(connectionId);
|
|
1148
1157
|
this.socketByConnectionId.delete(connectionId);
|
|
1149
1158
|
this.emit("stateChanged");
|
|
1159
|
+
const isPayloadExceeded = code === 1009;
|
|
1150
1160
|
for (const [callId, pending] of this.pendingInvocations.entries()) {
|
|
1151
1161
|
if (pending.connectionId !== connectionId) continue;
|
|
1152
1162
|
clearTimeout(pending.timeoutId);
|
|
1153
1163
|
this.pendingInvocations.delete(callId);
|
|
1154
|
-
pending.reject(/* @__PURE__ */ new Error(`Tool
|
|
1164
|
+
if (isPayloadExceeded) pending.reject(/* @__PURE__ */ new Error(`Tool result exceeded maximum payload size (${formatPayloadLimit(this.maxPayloadBytes)}). Use --max-payload to increase the limit.`));
|
|
1165
|
+
else pending.reject(/* @__PURE__ */ new Error(`Tool source ${connectionId} disconnected during invocation`));
|
|
1155
1166
|
}
|
|
1156
1167
|
}
|
|
1157
1168
|
startHeartbeat(connectionId) {
|
|
@@ -1535,6 +1546,14 @@ function parseCliOptions(argv) {
|
|
|
1535
1546
|
i += 1;
|
|
1536
1547
|
continue;
|
|
1537
1548
|
}
|
|
1549
|
+
if (token === "--max-payload") {
|
|
1550
|
+
const raw = readFlagValue(token, i);
|
|
1551
|
+
i += 1;
|
|
1552
|
+
const value = Number(raw);
|
|
1553
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`Invalid max-payload "${raw}". Must be a positive integer (bytes).`);
|
|
1554
|
+
options.maxPayloadBytes = value;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1538
1557
|
if (token === "--help" || token === "-h") {
|
|
1539
1558
|
printHelp();
|
|
1540
1559
|
process.exit(0);
|
|
@@ -1563,6 +1582,7 @@ function printHelp() {
|
|
|
1563
1582
|
" --label Human-readable relay label reported during discovery",
|
|
1564
1583
|
" --workspace Optional workspace name reported during discovery",
|
|
1565
1584
|
" --relay-id Stable relay identifier reported during discovery",
|
|
1585
|
+
" --max-payload Maximum WebSocket payload size in bytes (default: 10000000)",
|
|
1566
1586
|
" --help, -h Show help",
|
|
1567
1587
|
""
|
|
1568
1588
|
].join("\n"));
|
|
@@ -1616,6 +1636,7 @@ var LocalRelayMcpServer = class {
|
|
|
1616
1636
|
dynamicToolSignature = /* @__PURE__ */ new Map();
|
|
1617
1637
|
syncing = false;
|
|
1618
1638
|
syncRequested = false;
|
|
1639
|
+
syncDebounceTimer = null;
|
|
1619
1640
|
connected = false;
|
|
1620
1641
|
/**
|
|
1621
1642
|
* Creates a local relay MCP server with static and dynamic tool registration.
|
|
@@ -1627,9 +1648,7 @@ var LocalRelayMcpServer = class {
|
|
|
1627
1648
|
version: options.serverVersion ?? "0.0.0"
|
|
1628
1649
|
});
|
|
1629
1650
|
this.bridge.on("stateChanged", () => {
|
|
1630
|
-
this.
|
|
1631
|
-
this.logSyncError(err);
|
|
1632
|
-
});
|
|
1651
|
+
this.debouncedSyncDynamicTools();
|
|
1633
1652
|
});
|
|
1634
1653
|
this.bridge.on("elicitationRequest", (request) => {
|
|
1635
1654
|
(async () => {
|
|
@@ -1681,6 +1700,10 @@ var LocalRelayMcpServer = class {
|
|
|
1681
1700
|
*/
|
|
1682
1701
|
async stop() {
|
|
1683
1702
|
this.connected = false;
|
|
1703
|
+
if (this.syncDebounceTimer) {
|
|
1704
|
+
clearTimeout(this.syncDebounceTimer);
|
|
1705
|
+
this.syncDebounceTimer = null;
|
|
1706
|
+
}
|
|
1684
1707
|
let mcpCloseError;
|
|
1685
1708
|
try {
|
|
1686
1709
|
await this.mcpServer.close();
|
|
@@ -1984,6 +2007,23 @@ var LocalRelayMcpServer = class {
|
|
|
1984
2007
|
});
|
|
1985
2008
|
}
|
|
1986
2009
|
/**
|
|
2010
|
+
* Debounces rapid stateChanged events into a single sync pass.
|
|
2011
|
+
*
|
|
2012
|
+
* Browser reconnections emit multiple stateChanged events in quick
|
|
2013
|
+
* succession (hello, tools/list). Without debouncing, each triggers a
|
|
2014
|
+
* full sync that removes and re-registers tools — sending per-operation
|
|
2015
|
+
* notifications through the stdio transport and overwhelming the agent.
|
|
2016
|
+
*/
|
|
2017
|
+
debouncedSyncDynamicTools() {
|
|
2018
|
+
if (this.syncDebounceTimer) return;
|
|
2019
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
2020
|
+
this.syncDebounceTimer = null;
|
|
2021
|
+
this.syncDynamicTools().catch((err) => {
|
|
2022
|
+
this.logSyncError(err);
|
|
2023
|
+
});
|
|
2024
|
+
}, 16);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
1987
2027
|
* Coalesces concurrent sync requests into a single serialized sync loop.
|
|
1988
2028
|
*/
|
|
1989
2029
|
async syncDynamicTools() {
|
|
@@ -2112,6 +2152,10 @@ var LocalRelayMcpServer = class {
|
|
|
2112
2152
|
}
|
|
2113
2153
|
/**
|
|
2114
2154
|
* Produces a stable signature for change detection of dynamic tool metadata.
|
|
2155
|
+
*
|
|
2156
|
+
* Deliberately excludes `sourceId` (which is a per-connection UUID that
|
|
2157
|
+
* changes on every browser reconnect) so that a reconnecting tab with the
|
|
2158
|
+
* same tools does not trigger a needless remove+re-register cycle.
|
|
2115
2159
|
*/
|
|
2116
2160
|
toolSignature(tool) {
|
|
2117
2161
|
return JSON.stringify({
|
|
@@ -2125,10 +2169,7 @@ var LocalRelayMcpServer = class {
|
|
|
2125
2169
|
execution: tool.execution,
|
|
2126
2170
|
_meta: tool._meta,
|
|
2127
2171
|
icons: tool.icons,
|
|
2128
|
-
sources: tool.sources.map((source) => ({
|
|
2129
|
-
sourceId: source.sourceId,
|
|
2130
|
-
tabId: source.tabId
|
|
2131
|
-
}))
|
|
2172
|
+
sources: tool.sources.map((source) => ({ tabId: source.tabId }))
|
|
2132
2173
|
});
|
|
2133
2174
|
}
|
|
2134
2175
|
/**
|
|
@@ -2142,4 +2183,4 @@ var LocalRelayMcpServer = class {
|
|
|
2142
2183
|
|
|
2143
2184
|
//#endregion
|
|
2144
2185
|
export { NormalizedToolSchema as C, normalizeInboundTool as D, ToolSchema as E, InboundToolSchema as S, ToolAnnotationsSchema as T, extractSanitizedDomain as _, BrowserToRelayMessageSchema as a, CallToolResultSchema as b, RelayHelloAcceptedMessageSchema as c, RelaySourceInfoSchema as d, RelayToBrowserMessageSchema as f, buildPublicToolName as g, RelayRegistry as h, RelayBridgeServer as i, RelayHelloRejectedMessageSchema as l, HelloRequiredError as m, parseCliOptions as n, RelayClientToServerMessageSchema as o, ServerHelloMessageSchema as p, printHelp as r, RelayDescriptorSchema as s, LocalRelayMcpServer as t, RelayServerToClientMessageSchema as u, sanitizeName as v, RelayInvokeArgsSchema as w, DEFAULT_TOOL_INPUT_SCHEMA as x, CallToolRequestParamsSchema as y };
|
|
2145
|
-
//# sourceMappingURL=mcpRelayServer-
|
|
2186
|
+
//# sourceMappingURL=mcpRelayServer-BT1ct1Cc.mjs.map
|