@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 +59 -52
- package/dist/browser/embed.js +2 -2
- package/dist/browser/widget.html +1 -1
- package/dist/browser/widget.js +1 -1
- package/dist/cli.mjs +37 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +238 -133
- package/dist/index.mjs +1 -1
- package/dist/{mcpRelayServer-BT1ct1Cc.mjs → mcpRelayServer-BxI6zEc2.mjs} +362 -721
- package/dist/mcpRelayServer-BxI6zEc2.mjs.map +1 -0
- package/package.json +9 -9
- package/.mcp.json +0 -8
- package/dist/index.d.mts.map +0 -1
- package/dist/mcpRelayServer-BT1ct1Cc.mjs.map +0 -1
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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`](
|
|
100
|
-
- [WebMCP
|
|
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
|
|
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
|
|
125
|
-
| `webmcp_list_tools` | Lists all relayed tools with source info
|
|
126
|
-
| `
|
|
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
|
|
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
|
|
143
|
-
--ws-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
|
|
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
|
-
-
|
|
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
|
-
│
|
|
205
|
+
│ in process
|
|
193
206
|
┌──────────────────▼───────────────────┐
|
|
194
207
|
│ RelayBridgeServer │
|
|
195
208
|
│ Manages connections, routes calls │
|
|
196
209
|
└──────────────────┬───────────────────┘
|
|
197
|
-
│
|
|
210
|
+
│ WebSocket (ws://127.0.0.1:9333)
|
|
198
211
|
┌──────────────────▼───────────────────┐
|
|
199
212
|
│ Widget iframe │
|
|
200
213
|
│ embed.js injects widget.html │
|
|
201
214
|
└──────────────────┬───────────────────┘
|
|
202
|
-
│
|
|
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
|
|
210
|
-
|
|
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
|
|
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.
|
|
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.
|
|
224
|
-
|
|
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
|
|
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
|
-
- [
|
|
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
|
|
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
|
|
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)
|
package/dist/browser/embed.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let
|
|
2
|
-
`)}function
|
|
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)})}})();
|
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
|
|
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>
|
package/dist/browser/widget.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]);function
|
|
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-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|