@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
|
@@ -4,12 +4,12 @@ import WebSocket, { WebSocketServer } from "ws";
|
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
|
-
import { CallToolRequestParamsSchema, CallToolResultSchema,
|
|
7
|
+
import { CallToolRequestParamsSchema, CallToolResultSchema, ToolAnnotationsSchema, ToolSchema } from "@modelcontextprotocol/core";
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
9
10
|
import { execFile } from "node:child_process";
|
|
10
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
11
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
12
|
-
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
11
|
+
import { McpServer, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
13
13
|
|
|
14
14
|
//#region src/portStrategy.ts
|
|
15
15
|
const DEFAULT_RELAY_PORT = 9333;
|
|
@@ -34,7 +34,7 @@ async function readPersistedPort(path = defaultRelayPortPersistPath(), options =
|
|
|
34
34
|
if (typeof parsed.port !== "number" || !Number.isInteger(parsed.port) || parsed.port < 1 || parsed.port > 65535) return null;
|
|
35
35
|
if (typeof parsed.host !== "string" || parsed.host.length === 0) return null;
|
|
36
36
|
if (options.expectedHost && parsed.host !== options.expectedHost) return null;
|
|
37
|
-
const maxAgeMs = options.maxAgeMs ??
|
|
37
|
+
const maxAgeMs = options.maxAgeMs ?? RELAY_PORT_CACHE_MAX_AGE_MS;
|
|
38
38
|
const now = options.now ?? Date.now();
|
|
39
39
|
const updatedAtMs = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : NaN;
|
|
40
40
|
if (!Number.isFinite(updatedAtMs) || now - updatedAtMs > maxAgeMs) return null;
|
|
@@ -75,13 +75,6 @@ async function buildPortCandidates(options) {
|
|
|
75
75
|
*/
|
|
76
76
|
const NormalizedToolSchema = ToolSchema.extend({ name: ToolSchema.shape.name.min(1) });
|
|
77
77
|
/**
|
|
78
|
-
* Permissive inbound tool shape from browser/widget payloads.
|
|
79
|
-
*
|
|
80
|
-
* Only enforces a non-empty name at ingest. All other fields are normalized
|
|
81
|
-
* against SDK schemas by {@link normalizeInboundTool}.
|
|
82
|
-
*/
|
|
83
|
-
const InboundToolSchema = z.object({ name: z.string().min(1) }).passthrough();
|
|
84
|
-
/**
|
|
85
78
|
* SDK-derived argument schema for tool invocation payloads.
|
|
86
79
|
*/
|
|
87
80
|
const RelayInvokeArgsSchema = CallToolRequestParamsSchema.shape.arguments;
|
|
@@ -93,30 +86,16 @@ const DEFAULT_TOOL_INPUT_SCHEMA = {
|
|
|
93
86
|
type: "object",
|
|
94
87
|
properties: {}
|
|
95
88
|
};
|
|
89
|
+
/** SDK tool schema with a browser-compatible default input schema. */
|
|
90
|
+
const InboundToolSchema = NormalizedToolSchema.extend({ inputSchema: NormalizedToolSchema.shape.inputSchema.optional() });
|
|
96
91
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* Invalid optional metadata (description, output schema, annotations, etc.)
|
|
100
|
-
* is dropped. Invalid/missing inputSchema falls back to an empty object schema.
|
|
92
|
+
* Applies the browser default input schema and validates the complete SDK tool.
|
|
101
93
|
*/
|
|
102
94
|
function normalizeInboundTool(inbound) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
name: inbound.name,
|
|
108
|
-
inputSchema: inputSchemaParsed.success ? inputSchemaParsed.data : DEFAULT_TOOL_INPUT_SCHEMA
|
|
109
|
-
};
|
|
110
|
-
if (typeof inbound.description === "string") normalizedCandidate.description = inbound.description;
|
|
111
|
-
if (typeof inbound.title === "string") normalizedCandidate.title = inbound.title;
|
|
112
|
-
if (outputSchemaParsed.success && outputSchemaParsed.data !== void 0) normalizedCandidate.outputSchema = outputSchemaParsed.data;
|
|
113
|
-
if (annotationsParsed.success && annotationsParsed.data !== void 0) normalizedCandidate.annotations = annotationsParsed.data;
|
|
114
|
-
const normalizedParsed = NormalizedToolSchema.safeParse(normalizedCandidate);
|
|
115
|
-
if (normalizedParsed.success) return normalizedParsed.data;
|
|
116
|
-
return {
|
|
117
|
-
name: inbound.name,
|
|
118
|
-
inputSchema: DEFAULT_TOOL_INPUT_SCHEMA
|
|
119
|
-
};
|
|
95
|
+
return NormalizedToolSchema.parse({
|
|
96
|
+
...inbound,
|
|
97
|
+
inputSchema: inbound.inputSchema ?? DEFAULT_TOOL_INPUT_SCHEMA
|
|
98
|
+
});
|
|
120
99
|
}
|
|
121
100
|
|
|
122
101
|
//#endregion
|
|
@@ -156,12 +135,10 @@ function extractSanitizedDomain(originOrUrl) {
|
|
|
156
135
|
*/
|
|
157
136
|
function buildPublicToolName(options) {
|
|
158
137
|
const safeName = sanitizeName(options.originalToolName);
|
|
159
|
-
if (!options.disambiguate || !options.tabId) return safeName.slice(0,
|
|
138
|
+
if (!options.disambiguate || !options.tabId) return safeName.slice(0, 128);
|
|
160
139
|
const suffix = `_${sanitizeName(options.tabId).slice(0, TAB_ID_DISAMBIGUATION_LENGTH)}`;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const available = MAX_MCP_TOOL_NAME_LENGTH - suffix.length;
|
|
164
|
-
return `${safeName.slice(0, Math.max(1, available))}${suffix}`;
|
|
140
|
+
const available = 128 - suffix.length;
|
|
141
|
+
return `${safeName.slice(0, available)}${suffix}`;
|
|
165
142
|
}
|
|
166
143
|
|
|
167
144
|
//#endregion
|
|
@@ -217,7 +194,6 @@ var RelayRegistry = class {
|
|
|
217
194
|
const source = this.sourceByConnectionId.get(connectionId);
|
|
218
195
|
if (!source) throw new HelloRequiredError(connectionId);
|
|
219
196
|
this.touchConnection(connectionId);
|
|
220
|
-
this.removeConnectionTools(connectionId);
|
|
221
197
|
const providers = tools.map((tool) => ({
|
|
222
198
|
sourceId: connectionId,
|
|
223
199
|
tabId: source.tabId,
|
|
@@ -231,7 +207,7 @@ var RelayRegistry = class {
|
|
|
231
207
|
* Removes a source and all of its tool registrations.
|
|
232
208
|
*/
|
|
233
209
|
removeConnection(connectionId) {
|
|
234
|
-
this.
|
|
210
|
+
this.toolsByConnectionId.delete(connectionId);
|
|
235
211
|
this.sourceByConnectionId.delete(connectionId);
|
|
236
212
|
this.rebuildPublicNames();
|
|
237
213
|
}
|
|
@@ -283,31 +259,10 @@ var RelayRegistry = class {
|
|
|
283
259
|
const providers = this.providersByPublicToolName.get(options.toolName);
|
|
284
260
|
if (!providers || providers.length === 0) return null;
|
|
285
261
|
const rankedProviders = this.sortProvidersByRecency(providers);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
tool: byConnection.tool,
|
|
291
|
-
publicToolName: byConnection.publicToolName
|
|
292
|
-
};
|
|
293
|
-
const byTabId = rankedProviders.find((provider) => provider.tabId === options.sourceId);
|
|
294
|
-
if (byTabId) return {
|
|
295
|
-
connectionId: byTabId.sourceId,
|
|
296
|
-
tool: byTabId.tool,
|
|
297
|
-
publicToolName: byTabId.publicToolName
|
|
298
|
-
};
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
if (options.requestTabId) {
|
|
302
|
-
const byRequestTab = rankedProviders.find((provider) => provider.tabId === options.requestTabId);
|
|
303
|
-
if (!byRequestTab) return null;
|
|
304
|
-
return {
|
|
305
|
-
connectionId: byRequestTab.sourceId,
|
|
306
|
-
tool: byRequestTab.tool,
|
|
307
|
-
publicToolName: byRequestTab.publicToolName
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
const provider = rankedProviders[0];
|
|
262
|
+
let provider;
|
|
263
|
+
if (options.sourceId) provider = rankedProviders.find((candidate) => candidate.sourceId === options.sourceId) ?? rankedProviders.find((candidate) => candidate.tabId === options.sourceId);
|
|
264
|
+
else if (options.requestTabId) provider = rankedProviders.find((candidate) => candidate.tabId === options.requestTabId);
|
|
265
|
+
else provider = rankedProviders[0];
|
|
311
266
|
if (!provider) return null;
|
|
312
267
|
return {
|
|
313
268
|
connectionId: provider.sourceId,
|
|
@@ -325,12 +280,6 @@ var RelayRegistry = class {
|
|
|
325
280
|
};
|
|
326
281
|
}
|
|
327
282
|
/**
|
|
328
|
-
* Removes all tool providers for a connection.
|
|
329
|
-
*/
|
|
330
|
-
removeConnectionTools(connectionId) {
|
|
331
|
-
this.toolsByConnectionId.delete(connectionId);
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
283
|
* Rebuilds public tool names after source set changes.
|
|
335
284
|
*
|
|
336
285
|
* When the same original tool name appears in multiple tabs, names are
|
|
@@ -345,18 +294,36 @@ var RelayRegistry = class {
|
|
|
345
294
|
byOriginalName.set(key, group);
|
|
346
295
|
}
|
|
347
296
|
this.providersByPublicToolName.clear();
|
|
348
|
-
|
|
297
|
+
const allocations = [];
|
|
298
|
+
for (const [originalName, providers] of byOriginalName) {
|
|
349
299
|
const disambiguate = new Set(providers.map((p) => p.tabId)).size > 1;
|
|
350
|
-
for (const provider of providers) {
|
|
351
|
-
|
|
352
|
-
originalToolName:
|
|
300
|
+
for (const provider of providers) allocations.push({
|
|
301
|
+
candidate: buildPublicToolName({
|
|
302
|
+
originalToolName: originalName,
|
|
353
303
|
tabId: provider.tabId,
|
|
354
304
|
disambiguate
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
305
|
+
}),
|
|
306
|
+
key: disambiguate ? `${originalName}\0${provider.tabId}` : originalName,
|
|
307
|
+
provider
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const nameByKey = /* @__PURE__ */ new Map();
|
|
311
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
312
|
+
for (const { candidate, key, provider } of allocations.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)) {
|
|
313
|
+
let publicToolName = nameByKey.get(key);
|
|
314
|
+
if (!publicToolName) {
|
|
315
|
+
publicToolName = candidate;
|
|
316
|
+
for (let index = 2; usedNames.has(publicToolName); index += 1) {
|
|
317
|
+
const suffix = `_${String(index)}`;
|
|
318
|
+
publicToolName = `${candidate.slice(0, 128 - suffix.length)}${suffix}`;
|
|
319
|
+
}
|
|
320
|
+
nameByKey.set(key, publicToolName);
|
|
321
|
+
usedNames.add(publicToolName);
|
|
359
322
|
}
|
|
323
|
+
provider.publicToolName = publicToolName;
|
|
324
|
+
const existing = this.providersByPublicToolName.get(publicToolName) ?? [];
|
|
325
|
+
existing.push(provider);
|
|
326
|
+
this.providersByPublicToolName.set(publicToolName, existing);
|
|
360
327
|
}
|
|
361
328
|
}
|
|
362
329
|
/**
|
|
@@ -383,9 +350,9 @@ var RelayRegistry = class {
|
|
|
383
350
|
|
|
384
351
|
//#endregion
|
|
385
352
|
//#region src/schemas.ts
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
353
|
+
function normalizeSupportedInboundTools(tools) {
|
|
354
|
+
return tools.map(normalizeInboundTool).filter((tool) => tool.execution?.taskSupport !== "required");
|
|
355
|
+
}
|
|
389
356
|
const BrowserHelloMessageSchema = z.object({
|
|
390
357
|
type: z.literal("hello"),
|
|
391
358
|
tabId: z.string().min(1),
|
|
@@ -394,56 +361,21 @@ const BrowserHelloMessageSchema = z.object({
|
|
|
394
361
|
title: z.string().optional(),
|
|
395
362
|
iconUrl: z.string().optional()
|
|
396
363
|
});
|
|
397
|
-
/**
|
|
398
|
-
* Schema for full tool-list synchronization message.
|
|
399
|
-
*
|
|
400
|
-
* Inbound tools are permissively parsed then normalized to SDK Tool shape.
|
|
401
|
-
*/
|
|
402
364
|
const BrowserToolsListMessageSchema = z.object({
|
|
403
365
|
type: z.literal("tools/list"),
|
|
404
|
-
tools: z.array(InboundToolSchema).transform(
|
|
366
|
+
tools: z.array(InboundToolSchema).transform(normalizeSupportedInboundTools)
|
|
405
367
|
});
|
|
406
|
-
/**
|
|
407
|
-
* Schema for tool-set replacement notification pushed after initial registration.
|
|
408
|
-
*
|
|
409
|
-
* Processing is identical to `tools/list` — the full tool set replaces any
|
|
410
|
-
* previously registered tools — but the distinct type signals a dynamic update
|
|
411
|
-
* rather than an initial handshake.
|
|
412
|
-
*/
|
|
413
368
|
const BrowserToolsChangedMessageSchema = z.object({
|
|
414
369
|
type: z.literal("tools/changed"),
|
|
415
|
-
tools: z.array(InboundToolSchema).transform(
|
|
370
|
+
tools: z.array(InboundToolSchema).transform(normalizeSupportedInboundTools)
|
|
416
371
|
});
|
|
417
|
-
/**
|
|
418
|
-
* Schema for invocation response payloads sent by browser sources.
|
|
419
|
-
*
|
|
420
|
-
* `result` remains permissive and is normalized later to preserve runtime
|
|
421
|
-
* behavior for malformed tool responses.
|
|
422
|
-
*/
|
|
423
372
|
const BrowserToolResultMessageSchema = z.object({
|
|
424
373
|
type: z.literal("result"),
|
|
425
374
|
callId: z.string().min(1),
|
|
426
375
|
result: z.unknown()
|
|
427
376
|
});
|
|
428
|
-
/**
|
|
429
|
-
* Schema for browser heartbeat acknowledgement.
|
|
430
|
-
*/
|
|
431
377
|
const BrowserPongMessageSchema = z.object({ type: z.literal("pong") });
|
|
432
378
|
/**
|
|
433
|
-
* Schema for elicitation request forwarded from a browser tool handler.
|
|
434
|
-
*
|
|
435
|
-
* When a WebMCP tool handler calls `elicitInput()`, the embed script
|
|
436
|
-
* intercepts the call and forwards the parameters to the relay via this
|
|
437
|
-
* message type. The relay then sends `elicitation/create` to the MCP
|
|
438
|
-
* client (e.g. Claude Code) and returns the result via an
|
|
439
|
-
* `elicitation-response` message.
|
|
440
|
-
*/
|
|
441
|
-
const BrowserElicitationRequestSchema = z.object({
|
|
442
|
-
type: z.literal("elicitation-request"),
|
|
443
|
-
callId: z.string().min(1),
|
|
444
|
-
params: z.record(z.string(), z.unknown())
|
|
445
|
-
});
|
|
446
|
-
/**
|
|
447
379
|
* Union schema for all browser-to-relay protocol messages.
|
|
448
380
|
*/
|
|
449
381
|
const BrowserToRelayMessageSchema = z.discriminatedUnion("type", [
|
|
@@ -451,8 +383,7 @@ const BrowserToRelayMessageSchema = z.discriminatedUnion("type", [
|
|
|
451
383
|
BrowserToolsListMessageSchema,
|
|
452
384
|
BrowserToolsChangedMessageSchema,
|
|
453
385
|
BrowserToolResultMessageSchema,
|
|
454
|
-
BrowserPongMessageSchema
|
|
455
|
-
BrowserElicitationRequestSchema
|
|
386
|
+
BrowserPongMessageSchema
|
|
456
387
|
]);
|
|
457
388
|
/**
|
|
458
389
|
* Shared relay identity used for discovery and attach verification.
|
|
@@ -486,34 +417,15 @@ const RelayHelloRejectedMessageSchema = z.object({
|
|
|
486
417
|
reason: z.string().min(1),
|
|
487
418
|
message: z.string().min(1)
|
|
488
419
|
});
|
|
489
|
-
/**
|
|
490
|
-
* Schema for relay invocation messages sent to browser sources.
|
|
491
|
-
*/
|
|
492
420
|
const RelayInvokeMessageSchema = z.object({
|
|
493
421
|
type: z.literal("invoke"),
|
|
494
422
|
callId: z.string().min(1),
|
|
495
423
|
toolName: z.string().min(1),
|
|
496
424
|
args: RelayInvokeArgsSchema
|
|
497
425
|
});
|
|
498
|
-
/**
|
|
499
|
-
* Schema for relay heartbeat messages sent to browser sources.
|
|
500
|
-
*/
|
|
501
426
|
const RelayPingMessageSchema = z.object({ type: z.literal("ping") });
|
|
502
|
-
/**
|
|
503
|
-
* Schema for relay reload messages sent to browser sources.
|
|
504
|
-
*/
|
|
505
427
|
const RelayReloadMessageSchema = z.object({ type: z.literal("reload") });
|
|
506
428
|
/**
|
|
507
|
-
* Schema for elicitation response sent from relay to browser.
|
|
508
|
-
*
|
|
509
|
-
* Contains the MCP client's response to an `elicitation/create` request.
|
|
510
|
-
*/
|
|
511
|
-
const RelayElicitationResponseSchema = z.object({
|
|
512
|
-
type: z.literal("elicitation-response"),
|
|
513
|
-
callId: z.string().min(1),
|
|
514
|
-
result: z.record(z.string(), z.unknown())
|
|
515
|
-
});
|
|
516
|
-
/**
|
|
517
429
|
* Union schema for all relay-to-browser protocol messages.
|
|
518
430
|
*/
|
|
519
431
|
const RelayToBrowserMessageSchema = z.discriminatedUnion("type", [
|
|
@@ -522,13 +434,9 @@ const RelayToBrowserMessageSchema = z.discriminatedUnion("type", [
|
|
|
522
434
|
RelayHelloRejectedMessageSchema,
|
|
523
435
|
RelayInvokeMessageSchema,
|
|
524
436
|
RelayPingMessageSchema,
|
|
525
|
-
RelayReloadMessageSchema
|
|
526
|
-
RelayElicitationResponseSchema
|
|
437
|
+
RelayReloadMessageSchema
|
|
527
438
|
]);
|
|
528
439
|
/**
|
|
529
|
-
* Relay-to-relay protocol (client mode <-> server mode).
|
|
530
|
-
*/
|
|
531
|
-
/**
|
|
532
440
|
* Schema for source metadata transmitted in relay-to-relay messages.
|
|
533
441
|
*/
|
|
534
442
|
const RelaySourceInfoSchema = z.object({
|
|
@@ -542,17 +450,8 @@ const RelaySourceInfoSchema = z.object({
|
|
|
542
450
|
lastSeenAt: z.number(),
|
|
543
451
|
toolCount: z.number()
|
|
544
452
|
});
|
|
545
|
-
/**
|
|
546
|
-
* Schema for relay client identification message.
|
|
547
|
-
*/
|
|
548
453
|
const RelayClientHelloSchema = z.object({ type: z.literal("relay/hello") });
|
|
549
|
-
/**
|
|
550
|
-
* Schema for relay client tool list request.
|
|
551
|
-
*/
|
|
552
454
|
const RelayClientListToolsSchema = z.object({ type: z.literal("relay/list-tools") });
|
|
553
|
-
/**
|
|
554
|
-
* Schema for relay client tool invocation request.
|
|
555
|
-
*/
|
|
556
455
|
const RelayClientInvokeSchema = z.object({
|
|
557
456
|
type: z.literal("relay/invoke"),
|
|
558
457
|
callId: z.string().min(1),
|
|
@@ -569,27 +468,18 @@ const RelayClientToServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
569
468
|
]);
|
|
570
469
|
const RelayToolsPayloadFields = {
|
|
571
470
|
tools: z.array(NormalizedToolSchema),
|
|
572
|
-
sources: z.array(RelaySourceInfoSchema)
|
|
573
|
-
toolSourceMap: z.record(z.string(), z.array(z.string()))
|
|
471
|
+
sources: z.array(RelaySourceInfoSchema),
|
|
472
|
+
toolSourceMap: z.record(z.string(), z.array(z.string()))
|
|
574
473
|
};
|
|
575
|
-
/**
|
|
576
|
-
* Schema for relay server tool list response.
|
|
577
|
-
*/
|
|
578
474
|
const RelayServerToolsSchema = z.object({
|
|
579
475
|
type: z.literal("relay/tools"),
|
|
580
476
|
...RelayToolsPayloadFields
|
|
581
477
|
});
|
|
582
|
-
/**
|
|
583
|
-
* Schema for relay server invocation result response.
|
|
584
|
-
*/
|
|
585
478
|
const RelayServerResultSchema = z.object({
|
|
586
479
|
type: z.literal("relay/result"),
|
|
587
480
|
callId: z.string().min(1),
|
|
588
481
|
result: CallToolResultSchema
|
|
589
482
|
});
|
|
590
|
-
/**
|
|
591
|
-
* Schema for relay server push notification when tools change.
|
|
592
|
-
*/
|
|
593
483
|
const RelayServerToolsChangedSchema = z.object({
|
|
594
484
|
type: z.literal("relay/tools-changed"),
|
|
595
485
|
...RelayToolsPayloadFields
|
|
@@ -651,7 +541,9 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
651
541
|
instanceId;
|
|
652
542
|
wss = null;
|
|
653
543
|
socketByConnectionId = /* @__PURE__ */ new Map();
|
|
544
|
+
requestOriginByConnectionId = /* @__PURE__ */ new Map();
|
|
654
545
|
pendingInvocations = /* @__PURE__ */ new Map();
|
|
546
|
+
browserClientConnectionIds = /* @__PURE__ */ new Set();
|
|
655
547
|
relayClientConnectionIds = /* @__PURE__ */ new Set();
|
|
656
548
|
heartbeatIntervalByConnectionId = /* @__PURE__ */ new Map();
|
|
657
549
|
lastPongByConnectionId = /* @__PURE__ */ new Map();
|
|
@@ -687,15 +579,15 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
687
579
|
this.persistPath = options.persistPath ?? defaultRelayPortPersistPath();
|
|
688
580
|
this.allowedOrigins = options.allowedOrigins ?? ["*"];
|
|
689
581
|
this.maxPayloadBytes = options.maxPayloadBytes ?? 1e7;
|
|
690
|
-
this.invokeTimeoutMs = options.invokeTimeoutMs ??
|
|
582
|
+
this.invokeTimeoutMs = options.invokeTimeoutMs ?? 65e3;
|
|
691
583
|
this.label = options.label;
|
|
692
584
|
this.workspace = options.workspace;
|
|
693
585
|
this.relayId = options.relayId;
|
|
694
586
|
this.instanceId = randomUUID();
|
|
695
|
-
if (this.preferredPort
|
|
696
|
-
if (this.portRangeEnd < this.preferredPort && this.preferredPort !== 0) throw new Error(`Invalid port range ${this.preferredPort}-${this.portRangeEnd}. rangeEnd must be
|
|
587
|
+
if (!Number.isInteger(this.preferredPort) || this.preferredPort < 0 || this.preferredPort > 65535) throw new Error(`Invalid port ${this.preferredPort}. Port must be 0 (auto-assign) or between 1 and 65535.`);
|
|
588
|
+
if (!Number.isInteger(this.portRangeEnd) || this.portRangeEnd < 1 || this.portRangeEnd > 65535 || this.portRangeEnd < this.preferredPort && this.preferredPort !== 0) throw new Error(`Invalid port range ${this.preferredPort}-${this.portRangeEnd}. rangeEnd must be an integer between the preferred port and 65535.`);
|
|
697
589
|
if (!Number.isInteger(this.maxPayloadBytes) || this.maxPayloadBytes <= 0) throw new Error(`Invalid maxPayloadBytes ${this.maxPayloadBytes}. Must be a positive integer.`);
|
|
698
|
-
if (this.invokeTimeoutMs <= 0) throw new Error(`Invalid invokeTimeoutMs ${this.invokeTimeoutMs}. Must be
|
|
590
|
+
if (!Number.isInteger(this.invokeTimeoutMs) || this.invokeTimeoutMs <= 0) throw new Error(`Invalid invokeTimeoutMs ${this.invokeTimeoutMs}. Must be a positive integer.`);
|
|
699
591
|
}
|
|
700
592
|
/**
|
|
701
593
|
* Current operating mode.
|
|
@@ -756,12 +648,12 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
756
648
|
});
|
|
757
649
|
for (const candidate of candidates) try {
|
|
758
650
|
await this.startAsServer(candidate.port);
|
|
759
|
-
await
|
|
651
|
+
await this.persistSelectedPort();
|
|
760
652
|
return;
|
|
761
653
|
} catch (err) {
|
|
762
654
|
if (!this.isAddressInUseError(err)) throw err;
|
|
763
655
|
if (await this.tryAttachToExistingRelay(candidate.port)) {
|
|
764
|
-
await
|
|
656
|
+
await this.persistSelectedPort();
|
|
765
657
|
return;
|
|
766
658
|
}
|
|
767
659
|
if (candidate.wasFixed) throw new Error(`Port ${candidate.port} is already in use by a non-WebMCP service and cannot be shared.`);
|
|
@@ -772,12 +664,20 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
772
664
|
isAddressInUseError(error) {
|
|
773
665
|
return error instanceof Error && ("code" in error ? error.code === "EADDRINUSE" : error.message.includes("EADDRINUSE"));
|
|
774
666
|
}
|
|
667
|
+
async persistSelectedPort() {
|
|
668
|
+
try {
|
|
669
|
+
await persistPort(this.port, this.persistPath, this.host);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
process.stderr.write(`[webmcp-local-relay] warn: could not cache relay port: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
775
674
|
async tryAttachToExistingRelay(port) {
|
|
776
675
|
try {
|
|
777
676
|
await this.startAsClient(port);
|
|
778
677
|
process.stderr.write(`[webmcp-local-relay] info: discovered compatible relay on port ${port}, switching to client mode\n`);
|
|
779
678
|
return true;
|
|
780
|
-
} catch {
|
|
679
|
+
} catch (error) {
|
|
680
|
+
process.stderr.write(`[webmcp-local-relay] info: port ${port} did not complete a relay handshake: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
781
681
|
return false;
|
|
782
682
|
}
|
|
783
683
|
}
|
|
@@ -819,6 +719,8 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
819
719
|
this.heartbeatIntervalByConnectionId.clear();
|
|
820
720
|
this.lastPongByConnectionId.clear();
|
|
821
721
|
this.socketByConnectionId.clear();
|
|
722
|
+
this.requestOriginByConnectionId.clear();
|
|
723
|
+
this.browserClientConnectionIds.clear();
|
|
822
724
|
this.relayClientConnectionIds.clear();
|
|
823
725
|
for (const pending of this.pendingInvocations.values()) {
|
|
824
726
|
clearTimeout(pending.timeoutId);
|
|
@@ -880,16 +782,15 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
880
782
|
server.once("listening", onListening);
|
|
881
783
|
server.once("error", onError);
|
|
882
784
|
});
|
|
883
|
-
wss.on("connection", (socket) => {
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
socket.send(JSON.stringify(this.buildServerHello()));
|
|
888
|
-
} catch (err) {
|
|
889
|
-
process.stderr.write(`[webmcp-local-relay] warn: failed to send server hello to connection ${connectionId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
890
|
-
socket.close(1011, "Failed to send server hello");
|
|
785
|
+
wss.on("connection", (socket, request) => {
|
|
786
|
+
const requestOrigin = request.headers.origin;
|
|
787
|
+
if (requestOrigin && socket.protocol === RELAY_INTERNAL_PROTOCOL) {
|
|
788
|
+
socket.close(1008, "Browser connections cannot use the relay protocol");
|
|
891
789
|
return;
|
|
892
790
|
}
|
|
791
|
+
const connectionId = randomUUID();
|
|
792
|
+
this.socketByConnectionId.set(connectionId, socket);
|
|
793
|
+
if (requestOrigin) this.requestOriginByConnectionId.set(connectionId, requestOrigin);
|
|
893
794
|
socket.on("message", (raw) => {
|
|
894
795
|
this.onSocketMessage(connectionId, raw);
|
|
895
796
|
});
|
|
@@ -898,14 +799,20 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
898
799
|
});
|
|
899
800
|
socket.on("error", (err) => {
|
|
900
801
|
process.stderr.write(`[webmcp-local-relay] warn: socket error for connection ${connectionId}: ${err.message}\n`);
|
|
901
|
-
const code = err.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? 1009 : void 0;
|
|
802
|
+
const code = "code" in err && err.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? 1009 : void 0;
|
|
902
803
|
this.onSocketClose(connectionId, code);
|
|
903
804
|
});
|
|
805
|
+
try {
|
|
806
|
+
socket.send(JSON.stringify(this.buildServerHello()));
|
|
807
|
+
} catch (err) {
|
|
808
|
+
process.stderr.write(`[webmcp-local-relay] warn: failed to send server hello to connection ${connectionId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
809
|
+
socket.close(1011, "Failed to send server hello");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
904
812
|
this.startHeartbeat(connectionId);
|
|
905
813
|
});
|
|
906
814
|
wss.on("error", (err) => {
|
|
907
815
|
process.stderr.write(`[webmcp-local-relay] error: WebSocket server error: ${err.message}\n`);
|
|
908
|
-
this.emit("error", err);
|
|
909
816
|
});
|
|
910
817
|
this.wss = wss;
|
|
911
818
|
this._mode = "server";
|
|
@@ -926,11 +833,18 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
926
833
|
workspace: this.workspace
|
|
927
834
|
};
|
|
928
835
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
836
|
+
/**
|
|
837
|
+
* `async` is load-bearing: {@link onRelayClientMessage} consumes this through
|
|
838
|
+
* `.then(onResult, onError)`, so a synchronous throw would escape the socket
|
|
839
|
+
* message handler as an uncaught exception and leave the relay client waiting
|
|
840
|
+
* for a `relay/result` that never arrives.
|
|
841
|
+
*/
|
|
842
|
+
async invokeToolLocally(toolName, args, options) {
|
|
843
|
+
const resolved = this.registry.resolveInvocation({
|
|
844
|
+
toolName,
|
|
845
|
+
...options.sourceId === void 0 ? {} : { sourceId: options.sourceId },
|
|
846
|
+
...options.requestTabId === void 0 ? {} : { requestTabId: options.requestTabId }
|
|
847
|
+
});
|
|
934
848
|
if (!resolved) throw new Error(`No active browser source provides tool "${toolName}"`);
|
|
935
849
|
const socket = this.socketByConnectionId.get(resolved.connectionId);
|
|
936
850
|
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error(`Tool source ${resolved.connectionId} disconnected before invocation of "${toolName}"`);
|
|
@@ -941,7 +855,6 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
941
855
|
reject(/* @__PURE__ */ new Error(`Invocation for tool "${toolName}" timed out after ${this.invokeTimeoutMs}ms`));
|
|
942
856
|
}, this.invokeTimeoutMs);
|
|
943
857
|
this.pendingInvocations.set(callId, {
|
|
944
|
-
callId,
|
|
945
858
|
connectionId: resolved.connectionId,
|
|
946
859
|
timeoutId,
|
|
947
860
|
resolve,
|
|
@@ -962,12 +875,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
962
875
|
}
|
|
963
876
|
});
|
|
964
877
|
}
|
|
965
|
-
/**
|
|
966
|
-
* Handles a raw WebSocket message from a connected source.
|
|
967
|
-
*
|
|
968
|
-
* Routes relay-protocol messages (`relay/*`) to the relay client handler
|
|
969
|
-
* and browser-protocol messages to the existing browser handler.
|
|
970
|
-
*/
|
|
878
|
+
/** Handles a raw message according to the negotiated WebSocket protocol. */
|
|
971
879
|
onSocketMessage(connectionId, raw) {
|
|
972
880
|
const text = this.rawDataToUtf8(raw);
|
|
973
881
|
let parsedJson;
|
|
@@ -978,26 +886,40 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
978
886
|
process.stderr.write(`[webmcp-local-relay] warn: invalid JSON from connection ${connectionId} (${err instanceof Error ? err.message : "parse error"}): ${preview}\n`);
|
|
979
887
|
return;
|
|
980
888
|
}
|
|
981
|
-
const typeField = typeof parsedJson === "object" && parsedJson !== null ? parsedJson.type : void 0;
|
|
982
|
-
|
|
889
|
+
const typeField = typeof parsedJson === "object" && parsedJson !== null && "type" in parsedJson ? parsedJson.type : void 0;
|
|
890
|
+
const socket = this.socketByConnectionId.get(connectionId);
|
|
891
|
+
if (!socket) return;
|
|
892
|
+
if (socket.protocol === RELAY_INTERNAL_PROTOCOL) {
|
|
983
893
|
const relayMsg = RelayClientToServerMessageSchema.safeParse(parsedJson);
|
|
984
894
|
if (relayMsg.success) this.onRelayClientMessage(connectionId, relayMsg.data);
|
|
985
895
|
else process.stderr.write(`[webmcp-local-relay] warn: invalid relay message from ${connectionId}: ${relayMsg.error.message}\n`);
|
|
986
896
|
return;
|
|
987
897
|
}
|
|
988
|
-
|
|
898
|
+
if (typeof typeField === "string" && typeField.startsWith("relay/")) {
|
|
899
|
+
process.stderr.write(`[webmcp-local-relay] warn: connection ${connectionId} used the relay protocol without negotiating it\n`);
|
|
900
|
+
socket.close(1008, "Relay protocol was not negotiated");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
989
903
|
const parsedMessage = BrowserToRelayMessageSchema.safeParse(parsedJson);
|
|
990
904
|
if (!parsedMessage.success) {
|
|
991
905
|
process.stderr.write(`[webmcp-local-relay] warn: invalid message from connection ${connectionId} (type=${typeField}): ${parsedMessage.error.message}\n`);
|
|
992
906
|
return;
|
|
993
907
|
}
|
|
994
|
-
|
|
908
|
+
this.registry.touchConnection(connectionId);
|
|
909
|
+
this.onBrowserClientMessage(connectionId, parsedMessage.data);
|
|
910
|
+
}
|
|
911
|
+
onBrowserClientMessage(connectionId, message) {
|
|
912
|
+
if (message.type !== "hello" && !this.browserClientConnectionIds.has(connectionId)) {
|
|
913
|
+
process.stderr.write(`[webmcp-local-relay] warn: connection ${connectionId} sent ${message.type} before hello, ignoring\n`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
995
916
|
switch (message.type) {
|
|
996
917
|
case "hello":
|
|
997
918
|
try {
|
|
998
919
|
const socket = this.socketByConnectionId.get(connectionId);
|
|
999
|
-
|
|
1000
|
-
|
|
920
|
+
const origin = this.requestOriginByConnectionId.get(connectionId) ?? message.origin;
|
|
921
|
+
if (!this.isHostOriginAllowed(origin)) {
|
|
922
|
+
process.stderr.write(`[webmcp-local-relay] warn: rejecting source ${connectionId} with disallowed host origin: ${origin ?? "missing"}\n`);
|
|
1001
923
|
if (socket) this.sendHelloRejected(socket, {
|
|
1002
924
|
type: "hello/rejected",
|
|
1003
925
|
reason: "host-origin-not-allowed",
|
|
@@ -1005,7 +927,11 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1005
927
|
}, 1008, "Host origin not allowed");
|
|
1006
928
|
break;
|
|
1007
929
|
}
|
|
1008
|
-
this.registry.upsertSource(connectionId,
|
|
930
|
+
this.registry.upsertSource(connectionId, {
|
|
931
|
+
...message,
|
|
932
|
+
origin
|
|
933
|
+
});
|
|
934
|
+
this.browserClientConnectionIds.add(connectionId);
|
|
1009
935
|
if (socket) this.sendHelloAccepted(socket, { type: "hello/accepted" });
|
|
1010
936
|
this.emit("stateChanged");
|
|
1011
937
|
} catch (err) {
|
|
@@ -1018,11 +944,8 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1018
944
|
this.registry.registerTools(connectionId, message.tools);
|
|
1019
945
|
this.emit("stateChanged");
|
|
1020
946
|
} catch (err) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
process.stderr.write(`[webmcp-local-relay] error: failed to register tools for connection ${connectionId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
1024
|
-
this.socketByConnectionId.get(connectionId)?.close(1011, "Failed to register tools");
|
|
1025
|
-
}
|
|
947
|
+
process.stderr.write(`[webmcp-local-relay] error: failed to register tools for connection ${connectionId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
948
|
+
this.socketByConnectionId.get(connectionId)?.close(1008, "Invalid tool list");
|
|
1026
949
|
}
|
|
1027
950
|
break;
|
|
1028
951
|
case "result": {
|
|
@@ -1031,6 +954,10 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1031
954
|
process.stderr.write(`[webmcp-local-relay] warn: received result for unknown callId ${message.callId}\n`);
|
|
1032
955
|
break;
|
|
1033
956
|
}
|
|
957
|
+
if (pending.connectionId !== connectionId) {
|
|
958
|
+
process.stderr.write(`[webmcp-local-relay] warn: connection ${connectionId} returned result for another source's callId ${message.callId}\n`);
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
1034
961
|
clearTimeout(pending.timeoutId);
|
|
1035
962
|
this.pendingInvocations.delete(message.callId);
|
|
1036
963
|
pending.resolve(this.normalizeCallToolResult(message.result));
|
|
@@ -1039,39 +966,16 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1039
966
|
case "pong":
|
|
1040
967
|
this.lastPongByConnectionId.set(connectionId, Date.now());
|
|
1041
968
|
break;
|
|
1042
|
-
case "elicitation-request":
|
|
1043
|
-
this.emit("elicitationRequest", {
|
|
1044
|
-
callId: message.callId,
|
|
1045
|
-
connectionId,
|
|
1046
|
-
params: message.params
|
|
1047
|
-
});
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Sends an elicitation response back to the browser source that requested it.
|
|
1053
|
-
*/
|
|
1054
|
-
sendElicitationResponse(connectionId, callId, result) {
|
|
1055
|
-
const socket = this.socketByConnectionId.get(connectionId);
|
|
1056
|
-
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
1057
|
-
process.stderr.write(`[webmcp-local-relay] warn: cannot send elicitation response: source ${connectionId} disconnected\n`);
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const message = {
|
|
1061
|
-
type: "elicitation-response",
|
|
1062
|
-
callId,
|
|
1063
|
-
result
|
|
1064
|
-
};
|
|
1065
|
-
try {
|
|
1066
|
-
socket.send(JSON.stringify(message));
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
process.stderr.write(`[webmcp-local-relay] warn: failed to send elicitation response: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1069
969
|
}
|
|
1070
970
|
}
|
|
1071
971
|
/**
|
|
1072
972
|
* Handles relay-protocol messages from relay client connections.
|
|
1073
973
|
*/
|
|
1074
974
|
onRelayClientMessage(connectionId, message) {
|
|
975
|
+
if (message.type !== "relay/hello" && !this.relayClientConnectionIds.has(connectionId)) {
|
|
976
|
+
process.stderr.write(`[webmcp-local-relay] warn: connection ${connectionId} sent ${message.type} before relay/hello, ignoring\n`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
1075
979
|
switch (message.type) {
|
|
1076
980
|
case "relay/hello":
|
|
1077
981
|
this.relayClientConnectionIds.add(connectionId);
|
|
@@ -1088,45 +992,33 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1088
992
|
}
|
|
1089
993
|
case "relay/invoke": {
|
|
1090
994
|
const { callId, toolName, args } = message;
|
|
1091
|
-
(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const socket = this.socketByConnectionId.get(connectionId);
|
|
1099
|
-
if (socket?.readyState === WebSocket.OPEN) try {
|
|
1100
|
-
socket.send(JSON.stringify(response));
|
|
1101
|
-
} catch (sendErr) {
|
|
1102
|
-
process.stderr.write(`[webmcp-local-relay] warn: failed to send relay result to ${connectionId}: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}\n`);
|
|
1103
|
-
}
|
|
1104
|
-
else process.stderr.write(`[webmcp-local-relay] warn: relay client ${connectionId} disconnected before result for callId ${callId} could be delivered\n`);
|
|
1105
|
-
} catch (err) {
|
|
1106
|
-
const response = {
|
|
1107
|
-
type: "relay/result",
|
|
1108
|
-
callId,
|
|
1109
|
-
result: {
|
|
1110
|
-
content: [{
|
|
1111
|
-
type: "text",
|
|
1112
|
-
text: `Relay invocation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1113
|
-
}],
|
|
1114
|
-
isError: true
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
const socket = this.socketByConnectionId.get(connectionId);
|
|
1118
|
-
if (socket?.readyState === WebSocket.OPEN) try {
|
|
1119
|
-
socket.send(JSON.stringify(response));
|
|
1120
|
-
} catch (sendErr) {
|
|
1121
|
-
process.stderr.write(`[webmcp-local-relay] warn: failed to send relay error result to ${connectionId}: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}\n`);
|
|
1122
|
-
}
|
|
1123
|
-
else process.stderr.write(`[webmcp-local-relay] warn: relay client ${connectionId} disconnected before error result for callId ${callId} could be delivered\n`);
|
|
1124
|
-
}
|
|
1125
|
-
})();
|
|
995
|
+
this.invokeToolLocally(toolName, args ?? {}, {}).then((result) => this.sendRelayResult(connectionId, callId, result), (error) => this.sendRelayResult(connectionId, callId, {
|
|
996
|
+
content: [{
|
|
997
|
+
type: "text",
|
|
998
|
+
text: `Relay invocation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
999
|
+
}],
|
|
1000
|
+
isError: true
|
|
1001
|
+
}));
|
|
1126
1002
|
break;
|
|
1127
1003
|
}
|
|
1128
1004
|
}
|
|
1129
1005
|
}
|
|
1006
|
+
sendRelayResult(connectionId, callId, result) {
|
|
1007
|
+
const socket = this.socketByConnectionId.get(connectionId);
|
|
1008
|
+
if (socket?.readyState !== WebSocket.OPEN) {
|
|
1009
|
+
process.stderr.write(`[webmcp-local-relay] warn: relay client ${connectionId} disconnected before result for callId ${callId} could be delivered\n`);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
socket.send(JSON.stringify({
|
|
1014
|
+
type: "relay/result",
|
|
1015
|
+
callId,
|
|
1016
|
+
result
|
|
1017
|
+
}));
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
process.stderr.write(`[webmcp-local-relay] warn: failed to send relay result to ${connectionId}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1130
1022
|
/**
|
|
1131
1023
|
* Pushes current tool state to all connected relay clients.
|
|
1132
1024
|
*/
|
|
@@ -1152,6 +1044,8 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1152
1044
|
onSocketClose(connectionId, code) {
|
|
1153
1045
|
if (!this.socketByConnectionId.has(connectionId)) return;
|
|
1154
1046
|
this.stopHeartbeat(connectionId);
|
|
1047
|
+
this.requestOriginByConnectionId.delete(connectionId);
|
|
1048
|
+
this.browserClientConnectionIds.delete(connectionId);
|
|
1155
1049
|
this.relayClientConnectionIds.delete(connectionId);
|
|
1156
1050
|
this.registry.removeConnection(connectionId);
|
|
1157
1051
|
this.socketByConnectionId.delete(connectionId);
|
|
@@ -1216,10 +1110,9 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1216
1110
|
}
|
|
1217
1111
|
}
|
|
1218
1112
|
async connectToRelayServer(port) {
|
|
1219
|
-
this.stopping = false;
|
|
1220
1113
|
return new Promise((resolve, reject) => {
|
|
1221
1114
|
const wsUrl = `ws://${this.host}:${port}`;
|
|
1222
|
-
const ws = new WebSocket(wsUrl,
|
|
1115
|
+
const ws = new WebSocket(wsUrl, RELAY_INTERNAL_PROTOCOL);
|
|
1223
1116
|
const bufferedMessages = [];
|
|
1224
1117
|
const cleanup = () => {
|
|
1225
1118
|
clearTimeout(timeoutId);
|
|
@@ -1229,8 +1122,9 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1229
1122
|
ws.off("open", onOpen);
|
|
1230
1123
|
};
|
|
1231
1124
|
const rejectWith = (error) => {
|
|
1232
|
-
ws.once("error", () => {});
|
|
1233
1125
|
cleanup();
|
|
1126
|
+
ws.once("error", () => {});
|
|
1127
|
+
ws.terminate();
|
|
1234
1128
|
reject(error);
|
|
1235
1129
|
};
|
|
1236
1130
|
const finish = () => {
|
|
@@ -1319,7 +1213,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1319
1213
|
}
|
|
1320
1214
|
const message = RelayServerToClientMessageSchema.safeParse(parsed);
|
|
1321
1215
|
if (!message.success) {
|
|
1322
|
-
const typeField = typeof parsed === "object" && parsed !== null ? parsed.type : "unknown";
|
|
1216
|
+
const typeField = typeof parsed === "object" && parsed !== null && "type" in parsed ? parsed.type : "unknown";
|
|
1323
1217
|
process.stderr.write(`[webmcp-local-relay] warn: invalid relay server message (type=${typeField}): ${message.error.message}\n`);
|
|
1324
1218
|
return null;
|
|
1325
1219
|
}
|
|
@@ -1360,10 +1254,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1360
1254
|
this.clientReconnectTimer = setTimeout(() => {
|
|
1361
1255
|
this.clientReconnectTimer = null;
|
|
1362
1256
|
if (this.stopping) return;
|
|
1363
|
-
this.reconnectWithModePromotion()
|
|
1364
|
-
process.stderr.write(`[webmcp-local-relay] error: unexpected failure during reconnection: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
1365
|
-
if (!this.stopping) this.scheduleReconnect();
|
|
1366
|
-
});
|
|
1257
|
+
this.reconnectWithModePromotion();
|
|
1367
1258
|
}, delay);
|
|
1368
1259
|
}
|
|
1369
1260
|
/**
|
|
@@ -1384,7 +1275,7 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1384
1275
|
if (!this.stopping) this.scheduleReconnect();
|
|
1385
1276
|
}
|
|
1386
1277
|
}
|
|
1387
|
-
invokeToolViaRelay(toolName, args) {
|
|
1278
|
+
async invokeToolViaRelay(toolName, args) {
|
|
1388
1279
|
if (!this.clientSocket || this.clientSocket.readyState !== WebSocket.OPEN) throw new Error("Not connected to relay server");
|
|
1389
1280
|
const callId = randomUUID();
|
|
1390
1281
|
const socket = this.clientSocket;
|
|
@@ -1394,7 +1285,6 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1394
1285
|
reject(/* @__PURE__ */ new Error(`Proxied invocation for tool "${toolName}" timed out after ${this.invokeTimeoutMs}ms`));
|
|
1395
1286
|
}, this.invokeTimeoutMs);
|
|
1396
1287
|
this.clientPendingInvocations.set(callId, {
|
|
1397
|
-
callId,
|
|
1398
1288
|
connectionId: "relay-server",
|
|
1399
1289
|
timeoutId,
|
|
1400
1290
|
resolve,
|
|
@@ -1425,17 +1315,11 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1425
1315
|
for (const tool of tools) toolSourceMap[tool.name] = tool.sources.map((s) => s.sourceId);
|
|
1426
1316
|
return {
|
|
1427
1317
|
type,
|
|
1428
|
-
tools:
|
|
1318
|
+
tools: tools.map(({ originalName: _originalName, sources: _sources, ...tool }) => tool),
|
|
1429
1319
|
sources,
|
|
1430
1320
|
toolSourceMap
|
|
1431
1321
|
};
|
|
1432
1322
|
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Maps aggregated tools to the wire format used by the relay protocol.
|
|
1435
|
-
*/
|
|
1436
|
-
toWireTools(tools) {
|
|
1437
|
-
return tools.map(({ originalName: _originalName, sources: _sources, ...tool }) => tool);
|
|
1438
|
-
}
|
|
1439
1323
|
isHostOriginAllowed(origin) {
|
|
1440
1324
|
if (this.allowedOrigins.includes("*")) return true;
|
|
1441
1325
|
if (!origin) return false;
|
|
@@ -1482,86 +1366,68 @@ var RelayBridgeServer = class extends EventEmitter {
|
|
|
1482
1366
|
* Converts WebSocket raw data variants to a UTF-8 string payload.
|
|
1483
1367
|
*/
|
|
1484
1368
|
rawDataToUtf8(raw) {
|
|
1485
|
-
if (typeof raw === "string") return raw;
|
|
1486
|
-
if (Buffer.isBuffer(raw)) return raw.toString("utf8");
|
|
1487
|
-
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
|
|
1488
1369
|
if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
|
|
1489
|
-
|
|
1370
|
+
if (Buffer.isBuffer(raw)) return raw.toString("utf8");
|
|
1371
|
+
return Buffer.from(raw).toString("utf8");
|
|
1490
1372
|
}
|
|
1491
1373
|
};
|
|
1492
1374
|
|
|
1493
1375
|
//#endregion
|
|
1494
1376
|
//#region src/cli-utils.ts
|
|
1377
|
+
function parsePositiveInteger(name, value) {
|
|
1378
|
+
if (value === void 0) return void 0;
|
|
1379
|
+
const parsed = Number(value);
|
|
1380
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Invalid ${name} "${value}". Must be a positive integer.`);
|
|
1381
|
+
return parsed;
|
|
1382
|
+
}
|
|
1495
1383
|
/**
|
|
1496
1384
|
* Parses supported CLI flags for relay startup.
|
|
1497
1385
|
*/
|
|
1498
1386
|
function parseCliOptions(argv) {
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1387
|
+
const { values } = parseArgs({
|
|
1388
|
+
args: argv,
|
|
1389
|
+
options: {
|
|
1390
|
+
help: {
|
|
1391
|
+
type: "boolean",
|
|
1392
|
+
short: "h"
|
|
1393
|
+
},
|
|
1394
|
+
host: {
|
|
1395
|
+
type: "string",
|
|
1396
|
+
short: "H"
|
|
1397
|
+
},
|
|
1398
|
+
"allowed-origin": { type: "string" },
|
|
1399
|
+
"invoke-timeout": { type: "string" },
|
|
1400
|
+
label: { type: "string" },
|
|
1401
|
+
"max-payload": { type: "string" },
|
|
1402
|
+
port: {
|
|
1403
|
+
type: "string",
|
|
1404
|
+
short: "p"
|
|
1405
|
+
},
|
|
1406
|
+
"relay-id": { type: "string" },
|
|
1407
|
+
"widget-origin": { type: "string" },
|
|
1408
|
+
workspace: { type: "string" },
|
|
1409
|
+
"ws-origin": { type: "string" }
|
|
1410
|
+
},
|
|
1411
|
+
strict: true
|
|
1412
|
+
});
|
|
1413
|
+
const port = values.port === void 0 ? DEFAULT_RELAY_PORT : Number(values.port);
|
|
1414
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`Invalid port "${values.port}". Port must be an integer between 1 and 65535.`);
|
|
1415
|
+
const invokeTimeoutMs = parsePositiveInteger("invoke-timeout", values["invoke-timeout"]);
|
|
1416
|
+
const maxPayloadBytes = parsePositiveInteger("max-payload", values["max-payload"]);
|
|
1417
|
+
const allowedOrigins = (values["widget-origin"] ?? values["allowed-origin"] ?? values["ws-origin"])?.split(",").map((origin) => origin.trim()).filter(Boolean) ?? ["*"];
|
|
1418
|
+
if (allowedOrigins.length === 0) throw new Error("--widget-origin must include at least one origin.");
|
|
1419
|
+
return {
|
|
1420
|
+
allowedOrigins,
|
|
1421
|
+
help: values.help ?? false,
|
|
1422
|
+
host: values.host ?? "127.0.0.1",
|
|
1423
|
+
port,
|
|
1424
|
+
portExplicitlySet: values.port !== void 0,
|
|
1425
|
+
...values.label === void 0 ? {} : { label: values.label },
|
|
1426
|
+
...invokeTimeoutMs === void 0 ? {} : { invokeTimeoutMs },
|
|
1427
|
+
...maxPayloadBytes === void 0 ? {} : { maxPayloadBytes },
|
|
1428
|
+
...values["relay-id"] === void 0 ? {} : { relayId: values["relay-id"] },
|
|
1429
|
+
...values.workspace === void 0 ? {} : { workspace: values.workspace }
|
|
1509
1430
|
};
|
|
1510
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1511
|
-
const token = argv[i];
|
|
1512
|
-
if (!token) continue;
|
|
1513
|
-
if (token === "--host" || token === "-H") {
|
|
1514
|
-
options.host = readFlagValue(token, i);
|
|
1515
|
-
i += 1;
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
if (token === "--port" || token === "-p") {
|
|
1519
|
-
const raw = readFlagValue(token, i);
|
|
1520
|
-
i += 1;
|
|
1521
|
-
const value = Number.parseInt(raw, 10);
|
|
1522
|
-
if (!Number.isFinite(value) || value <= 0 || value > 65535) throw new Error(`Invalid port "${raw}". Port must be a number between 1 and 65535.`);
|
|
1523
|
-
options.port = value;
|
|
1524
|
-
options.portExplicitlySet = true;
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
if (token === "--widget-origin" || token === "--allowed-origin" || token === "--ws-origin") {
|
|
1528
|
-
const raw = readFlagValue(token, i);
|
|
1529
|
-
i += 1;
|
|
1530
|
-
const split = raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
1531
|
-
if (split.length > 0) options.allowedOrigins = split;
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
if (token === "--label") {
|
|
1535
|
-
options.label = readFlagValue(token, i);
|
|
1536
|
-
i += 1;
|
|
1537
|
-
continue;
|
|
1538
|
-
}
|
|
1539
|
-
if (token === "--workspace") {
|
|
1540
|
-
options.workspace = readFlagValue(token, i);
|
|
1541
|
-
i += 1;
|
|
1542
|
-
continue;
|
|
1543
|
-
}
|
|
1544
|
-
if (token === "--relay-id") {
|
|
1545
|
-
options.relayId = readFlagValue(token, i);
|
|
1546
|
-
i += 1;
|
|
1547
|
-
continue;
|
|
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
|
-
}
|
|
1557
|
-
if (token === "--help" || token === "-h") {
|
|
1558
|
-
printHelp();
|
|
1559
|
-
process.exit(0);
|
|
1560
|
-
}
|
|
1561
|
-
if (token.startsWith("-")) process.stderr.write(`[webmcp-local-relay] warn: unrecognized argument "${token}"\n`);
|
|
1562
|
-
else process.stderr.write(`[webmcp-local-relay] warn: unrecognized argument "${token}" (positional arguments are not supported)\n`);
|
|
1563
|
-
}
|
|
1564
|
-
return options;
|
|
1565
1431
|
}
|
|
1566
1432
|
/**
|
|
1567
1433
|
* Prints CLI usage to stderr.
|
|
@@ -1577,48 +1443,18 @@ function printHelp() {
|
|
|
1577
1443
|
" --host, -H Bind host for local websocket relay (default: 127.0.0.1)",
|
|
1578
1444
|
" --port, -p Preferred root port for the local relay cluster (default: 9333)",
|
|
1579
1445
|
" --widget-origin Allowed host page origin(s), comma-separated (default: *)",
|
|
1580
|
-
" --allowed-origin
|
|
1581
|
-
" --ws-origin
|
|
1446
|
+
" --allowed-origin Deprecated alias for --widget-origin",
|
|
1447
|
+
" --ws-origin Deprecated alias for --widget-origin",
|
|
1582
1448
|
" --label Human-readable relay label reported during discovery",
|
|
1583
1449
|
" --workspace Optional workspace name reported during discovery",
|
|
1584
1450
|
" --relay-id Stable relay identifier reported during discovery",
|
|
1451
|
+
" --invoke-timeout Browser tool invocation timeout in milliseconds (default: 65000)",
|
|
1585
1452
|
" --max-payload Maximum WebSocket payload size in bytes (default: 10000000)",
|
|
1586
1453
|
" --help, -h Show help",
|
|
1587
1454
|
""
|
|
1588
1455
|
].join("\n"));
|
|
1589
1456
|
}
|
|
1590
1457
|
|
|
1591
|
-
//#endregion
|
|
1592
|
-
//#region src/staticToolSchemas.ts
|
|
1593
|
-
const EMPTY_STATIC_TOOL_INPUT_SHAPE = {};
|
|
1594
|
-
const WEBMCP_CALL_TOOL_INPUT_SHAPE = {
|
|
1595
|
-
name: z.string().describe("The tool name to call. Use webmcp_list_tools to see available tool names."),
|
|
1596
|
-
arguments: z.record(z.string(), z.unknown()).optional().describe("Arguments to pass to the tool as a JSON object. Check the tool inputSchema from webmcp_list_tools for expected fields.")
|
|
1597
|
-
};
|
|
1598
|
-
const WEBMCP_OPEN_PAGE_INPUT_SHAPE = {
|
|
1599
|
-
url: z.string().describe("URL to open or match for refresh."),
|
|
1600
|
-
refresh: z.boolean().optional().describe("If true, refresh the connected source matching this URL instead of opening a new tab.")
|
|
1601
|
-
};
|
|
1602
|
-
function isRecord(value) {
|
|
1603
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1604
|
-
}
|
|
1605
|
-
function stripJsonSchemaMetadata(value) {
|
|
1606
|
-
if (Array.isArray(value)) return value.map((item) => stripJsonSchemaMetadata(item));
|
|
1607
|
-
if (!isRecord(value)) return value;
|
|
1608
|
-
const next = {};
|
|
1609
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
1610
|
-
if (key === "$schema") continue;
|
|
1611
|
-
next[key] = stripJsonSchemaMetadata(nestedValue);
|
|
1612
|
-
}
|
|
1613
|
-
return next;
|
|
1614
|
-
}
|
|
1615
|
-
function publicInputSchemaFromZodShape(inputShape) {
|
|
1616
|
-
return stripJsonSchemaMetadata(toJsonSchemaCompat(z.object(inputShape), {
|
|
1617
|
-
strictUnions: true,
|
|
1618
|
-
pipeStrategy: "input"
|
|
1619
|
-
}));
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
1458
|
//#endregion
|
|
1623
1459
|
//#region src/mcpRelayServer.ts
|
|
1624
1460
|
/**
|
|
@@ -1630,12 +1466,7 @@ var LocalRelayMcpServer = class {
|
|
|
1630
1466
|
*/
|
|
1631
1467
|
bridge;
|
|
1632
1468
|
mcpServer;
|
|
1633
|
-
|
|
1634
|
-
dynamicToolHandles = /* @__PURE__ */ new Map();
|
|
1635
|
-
dynamicToolData = /* @__PURE__ */ new Map();
|
|
1636
|
-
dynamicToolSignature = /* @__PURE__ */ new Map();
|
|
1637
|
-
syncing = false;
|
|
1638
|
-
syncRequested = false;
|
|
1469
|
+
dynamicTools = /* @__PURE__ */ new Map();
|
|
1639
1470
|
syncDebounceTimer = null;
|
|
1640
1471
|
connected = false;
|
|
1641
1472
|
/**
|
|
@@ -1650,31 +1481,14 @@ var LocalRelayMcpServer = class {
|
|
|
1650
1481
|
this.bridge.on("stateChanged", () => {
|
|
1651
1482
|
this.debouncedSyncDynamicTools();
|
|
1652
1483
|
});
|
|
1653
|
-
this.bridge.on("elicitationRequest", (request) => {
|
|
1654
|
-
(async () => {
|
|
1655
|
-
try {
|
|
1656
|
-
const result = await this.mcpServer.server.elicitInput(request.params);
|
|
1657
|
-
this.bridge.sendElicitationResponse(request.connectionId, request.callId, result);
|
|
1658
|
-
} catch (err) {
|
|
1659
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1660
|
-
process.stderr.write(`[webmcp-local-relay] warn: elicitation forwarding failed: ${message}\n`);
|
|
1661
|
-
this.bridge.sendElicitationResponse(request.connectionId, request.callId, {
|
|
1662
|
-
action: "decline",
|
|
1663
|
-
content: null,
|
|
1664
|
-
error: message
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
})();
|
|
1668
|
-
});
|
|
1669
1484
|
this.registerStaticTools();
|
|
1670
|
-
this.overrideListToolsHandler();
|
|
1671
1485
|
}
|
|
1672
1486
|
/**
|
|
1673
1487
|
* Starts the browser bridge and synchronizes dynamic MCP tools.
|
|
1674
1488
|
*/
|
|
1675
1489
|
async start() {
|
|
1676
1490
|
await this.bridge.start();
|
|
1677
|
-
|
|
1491
|
+
this.syncDynamicTools();
|
|
1678
1492
|
}
|
|
1679
1493
|
/**
|
|
1680
1494
|
* Connects the MCP server to a transport.
|
|
@@ -1718,289 +1532,147 @@ var LocalRelayMcpServer = class {
|
|
|
1718
1532
|
* Returns dynamic tool names currently registered in MCP.
|
|
1719
1533
|
*/
|
|
1720
1534
|
listDynamicToolNames() {
|
|
1721
|
-
return Array.from(this.
|
|
1722
|
-
}
|
|
1723
|
-
/**
|
|
1724
|
-
* Overrides the SDK's ListTools handler to return real JSON Schema
|
|
1725
|
-
* for dynamic tools instead of the empty `z.object({}).passthrough()`
|
|
1726
|
-
* schema that the SDK would otherwise convert to `{ type: 'object' }`.
|
|
1727
|
-
*
|
|
1728
|
-
* The MCP SDK's McpServer internally runs `toJsonSchemaCompat()` on
|
|
1729
|
-
* registered inputSchema values, which strips plain JSON Schema objects
|
|
1730
|
-
* (non-Zod) to empty schemas. This override bypasses that conversion
|
|
1731
|
-
* for dynamic (relayed) tools while preserving static tools as-is.
|
|
1732
|
-
*/
|
|
1733
|
-
overrideListToolsHandler() {
|
|
1734
|
-
this.mcpServer.server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
1735
|
-
return { tools: [...Array.from(this.staticToolData.values()).map((tool) => ({ ...tool })), ...Array.from(this.dynamicToolData.entries()).map(([name, tool]) => ({
|
|
1736
|
-
name,
|
|
1737
|
-
description: this.dynamicToolDescription(tool),
|
|
1738
|
-
inputSchema: tool.inputSchema,
|
|
1739
|
-
...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
|
|
1740
|
-
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
1741
|
-
}))].sort((left, right) => String(left.name).localeCompare(String(right.name))) };
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
registerStaticTool({ name, description, inputShape, annotations, handler }) {
|
|
1745
|
-
this.mcpServer.registerTool(name, {
|
|
1746
|
-
description,
|
|
1747
|
-
inputSchema: inputShape,
|
|
1748
|
-
...annotations ? { annotations } : {}
|
|
1749
|
-
}, handler);
|
|
1750
|
-
this.staticToolData.set(name, {
|
|
1751
|
-
name,
|
|
1752
|
-
description,
|
|
1753
|
-
inputSchema: publicInputSchemaFromZodShape(inputShape),
|
|
1754
|
-
...annotations ? { annotations } : {}
|
|
1755
|
-
});
|
|
1535
|
+
return Array.from(this.dynamicTools.keys()).sort();
|
|
1756
1536
|
}
|
|
1757
1537
|
/**
|
|
1758
1538
|
* Registers built-in management tools exposed by the relay.
|
|
1759
1539
|
*/
|
|
1760
1540
|
registerStaticTools() {
|
|
1761
|
-
this.
|
|
1762
|
-
name: "webmcp_list_sources",
|
|
1541
|
+
this.mcpServer.registerTool("webmcp_list_sources", {
|
|
1763
1542
|
description: "List connected browser tool sources and their metadata.",
|
|
1764
|
-
|
|
1543
|
+
inputSchema: z.object({}),
|
|
1765
1544
|
annotations: {
|
|
1766
1545
|
readOnlyHint: true,
|
|
1767
1546
|
idempotentHint: true
|
|
1768
|
-
},
|
|
1769
|
-
handler: async () => {
|
|
1770
|
-
if (this.bridge.mode === "client") {
|
|
1771
|
-
const sources = this.bridge.listSourcesFromRelay();
|
|
1772
|
-
const info = {
|
|
1773
|
-
mode: "client",
|
|
1774
|
-
count: sources.length,
|
|
1775
|
-
sources
|
|
1776
|
-
};
|
|
1777
|
-
return {
|
|
1778
|
-
content: [{
|
|
1779
|
-
type: "text",
|
|
1780
|
-
text: JSON.stringify(info, null, 2)
|
|
1781
|
-
}],
|
|
1782
|
-
structuredContent: info
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
const sources = this.bridge.registry.listSources();
|
|
1786
|
-
return {
|
|
1787
|
-
content: [{
|
|
1788
|
-
type: "text",
|
|
1789
|
-
text: JSON.stringify({
|
|
1790
|
-
count: sources.length,
|
|
1791
|
-
sources
|
|
1792
|
-
}, null, 2)
|
|
1793
|
-
}],
|
|
1794
|
-
structuredContent: {
|
|
1795
|
-
count: sources.length,
|
|
1796
|
-
sources
|
|
1797
|
-
}
|
|
1798
|
-
};
|
|
1799
1547
|
}
|
|
1548
|
+
}, async () => {
|
|
1549
|
+
const clientMode = this.bridge.mode === "client";
|
|
1550
|
+
const sources = clientMode ? this.bridge.listSourcesFromRelay() : this.bridge.registry.listSources();
|
|
1551
|
+
const info = {
|
|
1552
|
+
...clientMode ? { mode: "client" } : {},
|
|
1553
|
+
count: sources.length,
|
|
1554
|
+
sources
|
|
1555
|
+
};
|
|
1556
|
+
return {
|
|
1557
|
+
content: [{
|
|
1558
|
+
type: "text",
|
|
1559
|
+
text: JSON.stringify(info, null, 2)
|
|
1560
|
+
}],
|
|
1561
|
+
structuredContent: info
|
|
1562
|
+
};
|
|
1800
1563
|
});
|
|
1801
|
-
this.
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
inputShape: EMPTY_STATIC_TOOL_INPUT_SHAPE,
|
|
1564
|
+
this.mcpServer.registerTool("webmcp_list_tools", {
|
|
1565
|
+
description: "List WebMCP tools available from connected browser sources. Returns tool definitions including name, description, input schema, and source info.",
|
|
1566
|
+
inputSchema: z.object({}),
|
|
1805
1567
|
annotations: {
|
|
1806
1568
|
readOnlyHint: true,
|
|
1807
1569
|
idempotentHint: true
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1570
|
+
}
|
|
1571
|
+
}, async () => {
|
|
1572
|
+
const tools = this.listAggregatedTools();
|
|
1573
|
+
const info = {
|
|
1574
|
+
count: tools.length,
|
|
1575
|
+
tools
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
content: [{
|
|
1579
|
+
type: "text",
|
|
1580
|
+
text: JSON.stringify(info, null, 2)
|
|
1581
|
+
}],
|
|
1582
|
+
structuredContent: info
|
|
1583
|
+
};
|
|
1584
|
+
});
|
|
1585
|
+
this.mcpServer.registerTool("webmcp_open_page", {
|
|
1586
|
+
description: "Open a URL in the user's default browser, or refresh a connected source page. Use to launch WebMCP-enabled pages or reload stale connections.",
|
|
1587
|
+
inputSchema: z.object({
|
|
1588
|
+
url: z.string().describe("URL to open or match for refresh."),
|
|
1589
|
+
refresh: z.boolean().optional().describe("If true, refresh the connected source matching this URL instead of opening a new tab.")
|
|
1590
|
+
}),
|
|
1591
|
+
annotations: { readOnlyHint: false }
|
|
1592
|
+
}, async ({ url, refresh }) => {
|
|
1593
|
+
let parsed;
|
|
1594
|
+
try {
|
|
1595
|
+
parsed = new URL(url);
|
|
1596
|
+
} catch {
|
|
1811
1597
|
return {
|
|
1812
1598
|
content: [{
|
|
1813
1599
|
type: "text",
|
|
1814
|
-
text:
|
|
1815
|
-
count: tools.length,
|
|
1816
|
-
tools
|
|
1817
|
-
}, null, 2)
|
|
1600
|
+
text: `Invalid URL: ${url}`
|
|
1818
1601
|
}],
|
|
1819
|
-
|
|
1820
|
-
count: tools.length,
|
|
1821
|
-
tools
|
|
1822
|
-
}
|
|
1602
|
+
isError: true
|
|
1823
1603
|
};
|
|
1824
1604
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
if (toolSummary) errorLines.push("", "Available tools:", toolSummary);
|
|
1837
|
-
else errorLines.push("", "No tools are currently available. Ensure a browser page with WebMCP is connected.");
|
|
1838
|
-
return {
|
|
1839
|
-
content: [{
|
|
1840
|
-
type: "text",
|
|
1841
|
-
text: errorLines.join("\n")
|
|
1842
|
-
}],
|
|
1843
|
-
isError: true
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
try {
|
|
1847
|
-
const result = await this.bridge.invokeTool(name, args ?? {});
|
|
1848
|
-
const updatedTools = this.bridge.mode === "client" ? this.buildAggregatedToolsFromRelay() : this.bridge.registry.listTools();
|
|
1849
|
-
const updatedSummary = this.buildToolSummary(updatedTools);
|
|
1850
|
-
if (updatedSummary) result.content = [...result.content, {
|
|
1605
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return {
|
|
1606
|
+
content: [{
|
|
1607
|
+
type: "text",
|
|
1608
|
+
text: `Only http: and https: URLs are allowed. Got: ${parsed.protocol}`
|
|
1609
|
+
}],
|
|
1610
|
+
isError: true
|
|
1611
|
+
};
|
|
1612
|
+
const existing = this.bridge.mode === "server" ? this.bridge.registry.listSources().find((source) => source.url && URL.parse(source.url)?.origin === parsed.origin) : void 0;
|
|
1613
|
+
if (refresh) {
|
|
1614
|
+
if (this.bridge.mode === "client") return {
|
|
1615
|
+
content: [{
|
|
1851
1616
|
type: "text",
|
|
1852
|
-
text:
|
|
1853
|
-
}]
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
if (toolSummary) errorLines.push("", "Available tools:", toolSummary);
|
|
1858
|
-
return {
|
|
1859
|
-
content: [{
|
|
1860
|
-
type: "text",
|
|
1861
|
-
text: errorLines.join("\n")
|
|
1862
|
-
}],
|
|
1863
|
-
isError: true
|
|
1864
|
-
};
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
});
|
|
1868
|
-
this.registerStaticTool({
|
|
1869
|
-
name: "webmcp_open_page",
|
|
1870
|
-
description: "Open a URL in the user's default browser, or refresh a connected source page. Use to launch WebMCP-enabled pages or reload stale connections.",
|
|
1871
|
-
inputShape: WEBMCP_OPEN_PAGE_INPUT_SHAPE,
|
|
1872
|
-
annotations: { readOnlyHint: false },
|
|
1873
|
-
handler: async ({ url, refresh }) => {
|
|
1874
|
-
let parsed;
|
|
1875
|
-
try {
|
|
1876
|
-
parsed = new URL(url);
|
|
1877
|
-
} catch {
|
|
1878
|
-
return {
|
|
1879
|
-
content: [{
|
|
1880
|
-
type: "text",
|
|
1881
|
-
text: `Invalid URL: ${url}`
|
|
1882
|
-
}],
|
|
1883
|
-
isError: true
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return {
|
|
1617
|
+
text: "Refresh is not supported in client mode. Only the server relay can reload sources."
|
|
1618
|
+
}],
|
|
1619
|
+
isError: true
|
|
1620
|
+
};
|
|
1621
|
+
if (!existing) return {
|
|
1887
1622
|
content: [{
|
|
1888
1623
|
type: "text",
|
|
1889
|
-
text: `
|
|
1624
|
+
text: `No connected source matches origin ${parsed.origin}. The page may not be open or connected.`
|
|
1890
1625
|
}],
|
|
1891
1626
|
isError: true
|
|
1892
1627
|
};
|
|
1893
|
-
if (refresh) {
|
|
1894
|
-
if (this.bridge.mode === "client") return {
|
|
1895
|
-
content: [{
|
|
1896
|
-
type: "text",
|
|
1897
|
-
text: "Refresh is not supported in client mode. Only the server relay can reload sources."
|
|
1898
|
-
}],
|
|
1899
|
-
isError: true
|
|
1900
|
-
};
|
|
1901
|
-
const matched = this.bridge.registry.listSources().find((s) => {
|
|
1902
|
-
if (!s.url) return false;
|
|
1903
|
-
try {
|
|
1904
|
-
return new URL(s.url).origin === parsed.origin;
|
|
1905
|
-
} catch {
|
|
1906
|
-
process.stderr.write(`[webmcp-local-relay] warn: source ${s.sourceId} has unparseable URL: ${s.url}\n`);
|
|
1907
|
-
return false;
|
|
1908
|
-
}
|
|
1909
|
-
});
|
|
1910
|
-
if (!matched) return {
|
|
1911
|
-
content: [{
|
|
1912
|
-
type: "text",
|
|
1913
|
-
text: `No connected source matches origin ${parsed.origin}. The page may not be open or connected.`
|
|
1914
|
-
}],
|
|
1915
|
-
isError: true
|
|
1916
|
-
};
|
|
1917
|
-
try {
|
|
1918
|
-
this.bridge.reloadSource(matched.sourceId);
|
|
1919
|
-
return { content: [{
|
|
1920
|
-
type: "text",
|
|
1921
|
-
text: `Reload sent to source ${matched.sourceId} (${matched.url ?? matched.origin}).`
|
|
1922
|
-
}] };
|
|
1923
|
-
} catch (err) {
|
|
1924
|
-
return {
|
|
1925
|
-
content: [{
|
|
1926
|
-
type: "text",
|
|
1927
|
-
text: `Failed to reload source: ${err instanceof Error ? err.message : String(err)}`
|
|
1928
|
-
}],
|
|
1929
|
-
isError: true
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
1628
|
try {
|
|
1934
|
-
|
|
1629
|
+
this.bridge.reloadSource(existing.sourceId);
|
|
1630
|
+
return { content: [{
|
|
1631
|
+
type: "text",
|
|
1632
|
+
text: `Reload sent to source ${existing.sourceId} (${existing.url ?? existing.origin}).`
|
|
1633
|
+
}] };
|
|
1935
1634
|
} catch (err) {
|
|
1936
1635
|
return {
|
|
1937
1636
|
content: [{
|
|
1938
1637
|
type: "text",
|
|
1939
|
-
text: `Failed to
|
|
1638
|
+
text: `Failed to reload source: ${err instanceof Error ? err.message : String(err)}`
|
|
1940
1639
|
}],
|
|
1941
1640
|
isError: true
|
|
1942
1641
|
};
|
|
1943
1642
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
process.stderr.write(`[webmcp-local-relay] warn: source ${s.sourceId} has unparseable URL: ${s.url}\n`);
|
|
1951
|
-
return false;
|
|
1952
|
-
}
|
|
1953
|
-
});
|
|
1954
|
-
if (existing) return { content: [{
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
await this.openInBrowser(url);
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
return {
|
|
1648
|
+
content: [{
|
|
1955
1649
|
type: "text",
|
|
1956
|
-
text: `
|
|
1957
|
-
}]
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
type: "text",
|
|
1961
|
-
text: `Opened ${url} in the default browser.`
|
|
1962
|
-
}] };
|
|
1650
|
+
text: `Failed to open browser: ${err instanceof Error ? err.message : String(err)}`
|
|
1651
|
+
}],
|
|
1652
|
+
isError: true
|
|
1653
|
+
};
|
|
1963
1654
|
}
|
|
1655
|
+
if (existing) return { content: [{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: `Opened ${url} in the default browser. Note: a source from ${existing.url ?? existing.origin} is already connected.`
|
|
1658
|
+
}] };
|
|
1659
|
+
return { content: [{
|
|
1660
|
+
type: "text",
|
|
1661
|
+
text: `Opened ${url} in the default browser.`
|
|
1662
|
+
}] };
|
|
1964
1663
|
});
|
|
1965
1664
|
}
|
|
1966
1665
|
/**
|
|
1967
|
-
* Builds a concise plain-text list of available tools.
|
|
1968
|
-
*/
|
|
1969
|
-
buildToolSummary(tools) {
|
|
1970
|
-
if (tools.length === 0) return null;
|
|
1971
|
-
return tools.map((t) => {
|
|
1972
|
-
const desc = t.description ? ` - ${t.description.split("\n")[0]}` : "";
|
|
1973
|
-
return ` ${t.name}${desc}`;
|
|
1974
|
-
}).join("\n");
|
|
1975
|
-
}
|
|
1976
|
-
/**
|
|
1977
1666
|
* Opens a URL in the user's default browser using the platform open command.
|
|
1978
1667
|
*
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
1981
|
-
* because cmd.exe interprets `&`, `|`, and other metacharacters in URLs.
|
|
1668
|
+
* Passes the re-serialized `URL.href` directly to the platform launcher.
|
|
1669
|
+
* No shell or command-language string is involved.
|
|
1982
1670
|
*/
|
|
1983
1671
|
openInBrowser(url) {
|
|
1984
1672
|
const safeUrl = new URL(url).href;
|
|
1673
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
1985
1674
|
return new Promise((resolve, reject) => {
|
|
1986
|
-
|
|
1987
|
-
let command;
|
|
1988
|
-
let args;
|
|
1989
|
-
if (platform === "darwin") {
|
|
1990
|
-
command = "open";
|
|
1991
|
-
args = [safeUrl];
|
|
1992
|
-
} else if (platform === "win32") {
|
|
1993
|
-
command = "powershell.exe";
|
|
1994
|
-
args = [
|
|
1995
|
-
"-NoProfile",
|
|
1996
|
-
"-Command",
|
|
1997
|
-
`Start-Process "${safeUrl}"`
|
|
1998
|
-
];
|
|
1999
|
-
} else {
|
|
2000
|
-
command = "xdg-open";
|
|
2001
|
-
args = [safeUrl];
|
|
2002
|
-
}
|
|
2003
|
-
execFile(command, args, (err) => {
|
|
1675
|
+
execFile(command, [safeUrl], (err) => {
|
|
2004
1676
|
if (err) reject(err);
|
|
2005
1677
|
else resolve();
|
|
2006
1678
|
});
|
|
@@ -2018,36 +1690,15 @@ var LocalRelayMcpServer = class {
|
|
|
2018
1690
|
if (this.syncDebounceTimer) return;
|
|
2019
1691
|
this.syncDebounceTimer = setTimeout(() => {
|
|
2020
1692
|
this.syncDebounceTimer = null;
|
|
2021
|
-
|
|
2022
|
-
this.
|
|
2023
|
-
})
|
|
1693
|
+
try {
|
|
1694
|
+
this.syncDynamicTools();
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
const details = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
1697
|
+
process.stderr.write(`[webmcp-local-relay] error: failed to sync dynamic tools: ${details}\n`);
|
|
1698
|
+
}
|
|
2024
1699
|
}, 16);
|
|
2025
1700
|
}
|
|
2026
1701
|
/**
|
|
2027
|
-
* Coalesces concurrent sync requests into a single serialized sync loop.
|
|
2028
|
-
*/
|
|
2029
|
-
async syncDynamicTools() {
|
|
2030
|
-
if (this.syncing) {
|
|
2031
|
-
this.syncRequested = true;
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
this.syncing = true;
|
|
2035
|
-
try {
|
|
2036
|
-
do {
|
|
2037
|
-
this.syncRequested = false;
|
|
2038
|
-
const tools = this.listAggregatedTools();
|
|
2039
|
-
this.applyDynamicTools(tools);
|
|
2040
|
-
} while (this.syncRequested);
|
|
2041
|
-
} finally {
|
|
2042
|
-
const retryRequested = this.syncRequested;
|
|
2043
|
-
this.syncRequested = false;
|
|
2044
|
-
this.syncing = false;
|
|
2045
|
-
if (retryRequested) this.syncDynamicTools().catch((err) => {
|
|
2046
|
-
this.logSyncError(err);
|
|
2047
|
-
});
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
/**
|
|
2051
1702
|
* Returns the current aggregated tool list, dispatching based on bridge mode.
|
|
2052
1703
|
*/
|
|
2053
1704
|
listAggregatedTools() {
|
|
@@ -2077,52 +1728,51 @@ var LocalRelayMcpServer = class {
|
|
|
2077
1728
|
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
2078
1729
|
}
|
|
2079
1730
|
/**
|
|
2080
|
-
* Applies
|
|
1731
|
+
* Applies current aggregated tool state to MCP dynamic registrations.
|
|
2081
1732
|
*/
|
|
2082
|
-
|
|
1733
|
+
syncDynamicTools() {
|
|
1734
|
+
const tools = this.listAggregatedTools();
|
|
2083
1735
|
const nextNames = new Set(tools.map((tool) => tool.name));
|
|
2084
|
-
|
|
2085
|
-
for (const [name, handle] of this.dynamicToolHandles.entries()) {
|
|
1736
|
+
for (const [name, registration] of this.dynamicTools) {
|
|
2086
1737
|
if (nextNames.has(name)) continue;
|
|
2087
|
-
handle.remove();
|
|
2088
|
-
this.
|
|
2089
|
-
this.dynamicToolData.delete(name);
|
|
2090
|
-
this.dynamicToolSignature.delete(name);
|
|
2091
|
-
changed = true;
|
|
1738
|
+
registration.handle.remove();
|
|
1739
|
+
this.dynamicTools.delete(name);
|
|
2092
1740
|
}
|
|
2093
1741
|
for (const tool of tools) {
|
|
2094
1742
|
const signature = this.toolSignature(tool);
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (
|
|
2098
|
-
|
|
2099
|
-
this.
|
|
1743
|
+
const current = this.dynamicTools.get(tool.name);
|
|
1744
|
+
if (current?.signature === signature) continue;
|
|
1745
|
+
if (current) {
|
|
1746
|
+
current.handle.remove();
|
|
1747
|
+
this.dynamicTools.delete(tool.name);
|
|
1748
|
+
}
|
|
1749
|
+
try {
|
|
1750
|
+
const handle = this.registerDynamicTool(tool);
|
|
1751
|
+
this.dynamicTools.set(tool.name, {
|
|
1752
|
+
handle,
|
|
1753
|
+
signature
|
|
1754
|
+
});
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
const details = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
1757
|
+
process.stderr.write(`[webmcp-local-relay] warn: skipped dynamic tool "${tool.name}" because its schema could not be compiled: ${details}\n`);
|
|
2100
1758
|
}
|
|
2101
|
-
const handle = this.registerDynamicTool(tool);
|
|
2102
|
-
this.dynamicToolHandles.set(tool.name, handle);
|
|
2103
|
-
this.dynamicToolData.set(tool.name, tool);
|
|
2104
|
-
this.dynamicToolSignature.set(tool.name, signature);
|
|
2105
|
-
changed = true;
|
|
2106
|
-
}
|
|
2107
|
-
if (changed && this.connected) try {
|
|
2108
|
-
this.mcpServer.sendToolListChanged();
|
|
2109
|
-
} catch (err) {
|
|
2110
|
-
process.stderr.write(`[webmcp-local-relay] warn: failed to send tool list changed notification: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2111
1759
|
}
|
|
2112
1760
|
}
|
|
2113
1761
|
/**
|
|
2114
1762
|
* Registers a single dynamic tool and returns a removal handle.
|
|
2115
1763
|
*/
|
|
2116
1764
|
registerDynamicTool(tool) {
|
|
2117
|
-
const inputSchema =
|
|
2118
|
-
const
|
|
1765
|
+
const inputSchema = fromJsonSchema(tool.inputSchema);
|
|
1766
|
+
const outputSchema = tool.outputSchema ? fromJsonSchema(tool.outputSchema) : void 0;
|
|
1767
|
+
return this.mcpServer.registerTool(tool.name, {
|
|
1768
|
+
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
2119
1769
|
description: this.dynamicToolDescription(tool),
|
|
2120
|
-
inputSchema
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
...
|
|
2124
|
-
|
|
2125
|
-
}
|
|
1770
|
+
inputSchema,
|
|
1771
|
+
...outputSchema ? { outputSchema } : {},
|
|
1772
|
+
...tool.annotations ? { annotations: tool.annotations } : {},
|
|
1773
|
+
...tool.icons ? { icons: tool.icons } : {},
|
|
1774
|
+
...tool._meta ? { _meta: tool._meta } : {}
|
|
1775
|
+
}, async (args) => {
|
|
2126
1776
|
try {
|
|
2127
1777
|
return await this.bridge.invokeTool(tool.name, args);
|
|
2128
1778
|
} catch (err) {
|
|
@@ -2138,9 +1788,6 @@ var LocalRelayMcpServer = class {
|
|
|
2138
1788
|
};
|
|
2139
1789
|
}
|
|
2140
1790
|
});
|
|
2141
|
-
return { remove: () => {
|
|
2142
|
-
handle.remove();
|
|
2143
|
-
} };
|
|
2144
1791
|
}
|
|
2145
1792
|
/**
|
|
2146
1793
|
* Builds a display description for relayed tools including source context.
|
|
@@ -2159,28 +1806,22 @@ var LocalRelayMcpServer = class {
|
|
|
2159
1806
|
*/
|
|
2160
1807
|
toolSignature(tool) {
|
|
2161
1808
|
return JSON.stringify({
|
|
2162
|
-
name: tool.name,
|
|
2163
1809
|
originalName: tool.originalName,
|
|
2164
1810
|
title: tool.title,
|
|
2165
1811
|
description: tool.description,
|
|
2166
1812
|
inputSchema: tool.inputSchema,
|
|
2167
1813
|
outputSchema: tool.outputSchema,
|
|
2168
1814
|
annotations: tool.annotations,
|
|
2169
|
-
execution: tool.execution,
|
|
2170
1815
|
_meta: tool._meta,
|
|
2171
1816
|
icons: tool.icons,
|
|
2172
|
-
|
|
1817
|
+
source: tool.sources[0] ? {
|
|
1818
|
+
tabId: tool.sources[0].tabId,
|
|
1819
|
+
title: tool.sources[0].title
|
|
1820
|
+
} : void 0
|
|
2173
1821
|
});
|
|
2174
1822
|
}
|
|
2175
|
-
/**
|
|
2176
|
-
* Logs tool sync errors with full detail.
|
|
2177
|
-
*/
|
|
2178
|
-
logSyncError(err) {
|
|
2179
|
-
const details = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
2180
|
-
process.stderr.write(`[webmcp-local-relay] error: failed to sync dynamic tools: ${details}\n`);
|
|
2181
|
-
}
|
|
2182
1823
|
};
|
|
2183
1824
|
|
|
2184
1825
|
//#endregion
|
|
2185
1826
|
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 };
|
|
2186
|
-
//# sourceMappingURL=mcpRelayServer-
|
|
1827
|
+
//# sourceMappingURL=mcpRelayServer-BxI6zEc2.mjs.map
|