machine-bridge-mcp 0.3.3 → 0.4.2

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.
@@ -1,8 +1,10 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
+ import toolCatalog from "../shared/tool-catalog.json";
2
3
 
3
4
  const SERVER_NAME = "machine-bridge-mcp";
4
- const SERVER_VERSION = "0.3.3";
5
- const MCP_PROTOCOL_VERSION = "2025-06-18";
5
+ const SERVER_VERSION = "0.4.2";
6
+ const MCP_PROTOCOL_VERSION = "2025-11-25";
7
+ const MCP_SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"] as const;
6
8
  const JSONRPC_VERSION = "2.0";
7
9
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
8
10
  const MAX_BODY_BYTES = 16 * 1024 * 1024;
@@ -105,133 +107,33 @@ interface PendingCall {
105
107
  reject: (error: Error) => void;
106
108
  timeout: ReturnType<typeof setTimeout>;
107
109
  socket: WebSocket;
110
+ clientRequestKey?: string;
108
111
  }
109
112
 
110
- const serverInfoTool = {
111
- name: "server_info",
112
- description: "Return bridge metadata, OAuth endpoint details, daemon connection status, and available tools.",
113
- inputSchema: { type: "object", additionalProperties: true },
114
- } as const;
113
+ interface AuthorizedToken {
114
+ tokenKey: string;
115
+ clientId: string;
116
+ }
115
117
 
116
- const workspaceTools = [
117
- {
118
- name: "project_overview",
119
- description: "Summarize the connected local workspace and daemon policy.",
120
- inputSchema: { type: "object", additionalProperties: true },
121
- },
122
- {
123
- name: "list_roots",
124
- description: "List workspace roots exposed by the local daemon.",
125
- inputSchema: { type: "object", additionalProperties: true },
126
- },
127
- {
128
- name: "list_dir",
129
- description: "List direct children of a workspace directory.",
130
- inputSchema: {
131
- type: "object",
132
- properties: { path: { type: "string", default: "." } },
133
- additionalProperties: true,
134
- },
135
- },
136
- {
137
- name: "list_files",
138
- description: "Recursively list files under a workspace path.",
139
- inputSchema: {
140
- type: "object",
141
- properties: {
142
- path: { type: "string", default: "." },
143
- max_files: { type: "integer", minimum: 1, maximum: 10000, default: 1000 },
144
- },
145
- additionalProperties: true,
146
- },
147
- },
148
- {
149
- name: "read_file",
150
- description: "Read a UTF-8 file. Relative paths use the daemon workspace; paths outside the workspace require the daemon to be started with --unrestricted-paths.",
151
- inputSchema: {
152
- type: "object",
153
- properties: {
154
- path: { type: "string" },
155
- max_bytes: { type: "integer", minimum: 1, maximum: 5242880, default: 1048576 },
156
- },
157
- required: ["path"],
158
- additionalProperties: true,
159
- },
160
- },
161
- {
162
- name: "write_file",
163
- description: "Atomically write a UTF-8 file up to 5 MiB. Paths outside the workspace require --unrestricted-paths; symbolic-link destinations are rejected. Enabled by default.",
164
- inputSchema: {
165
- type: "object",
166
- properties: {
167
- path: { type: "string" },
168
- content: { type: "string" },
169
- create_only: { type: "boolean", default: false },
170
- expected_sha256: { type: "string" },
171
- },
172
- required: ["path", "content"],
173
- additionalProperties: true,
174
- },
175
- },
176
- {
177
- name: "search_text",
178
- description: "Search plain text files under a workspace path for a substring.",
179
- inputSchema: {
180
- type: "object",
181
- properties: {
182
- query: { type: "string" },
183
- path: { type: "string", default: "." },
184
- max_matches: { type: "integer", minimum: 1, maximum: 1000, default: 100 },
185
- max_files: { type: "integer", minimum: 1, maximum: 100000, default: 10000 },
186
- },
187
- required: ["query"],
188
- additionalProperties: true,
189
- },
190
- },
191
- {
192
- name: "git_status",
193
- description: "Run git status --short for the repository containing a workspace path.",
194
- inputSchema: {
195
- type: "object",
196
- properties: { path: { type: "string", default: "." } },
197
- additionalProperties: true,
198
- },
199
- },
200
- {
201
- name: "git_diff",
202
- description: "Run git diff for the local workspace and return bounded output.",
203
- inputSchema: {
204
- type: "object",
205
- properties: {
206
- path: { type: "string", default: "." },
207
- max_bytes: { type: "integer", minimum: 1, maximum: 5242880, default: 1048576 },
208
- },
209
- additionalProperties: true,
210
- },
211
- },
212
- {
213
- name: "exec_command",
214
- description: "Execute a shell command with cwd set to the daemon workspace. Enabled by default; environment is intentionally minimal unless the daemon is started with full env.",
215
- inputSchema: {
216
- type: "object",
217
- properties: {
218
- command: { type: "string" },
219
- timeout_seconds: { type: "integer", minimum: 1, maximum: 600, default: 120 },
220
- },
221
- required: ["command"],
222
- additionalProperties: true,
223
- },
224
- },
225
- ] as const;
118
+ type ToolDefinition = Record<string, unknown> & { name: string; availability?: string };
119
+
120
+ const allCatalogTools = toolCatalog as ToolDefinition[];
121
+ const serverInfoTool = publicTool(allCatalogTools.find((tool) => tool.name === "server_info")!);
122
+ const workspaceTools = allCatalogTools.filter((tool) => tool.name !== "server_info").map(publicTool);
226
123
 
227
124
  const MCP_INSTRUCTIONS = [
228
125
  "You are connected to a local workspace through machine-bridge-mcp.",
229
- "The Worker is only a relay. File and command operations run on the user's local daemon.",
230
- "Relative paths use the configured workspace as cwd; filesystem access is workspace-scoped unless the daemon was started with --unrestricted-paths.",
231
- "Writes and shell execution are enabled by default; use --no-write and --no-exec for read-only sessions.",
232
- "Prefer inspecting files before editing, make minimal changes, and report commands you ran.",
126
+ "The Cloudflare Worker authenticates and relays calls; file and command operations execute on the user's local runtime.",
127
+ "Relative paths use the configured workspace. Direct filesystem tools are workspace-scoped unless unrestricted paths are explicitly enabled.",
128
+ "run_process avoids shell parsing but is not an OS sandbox. exec_command is only exposed in shell mode and has the local user's authority.",
129
+ "Prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run.",
233
130
  ].join("\n");
234
131
 
132
+ function publicTool(tool: ToolDefinition): ToolDefinition {
133
+ const { availability: _availability, ...definition } = tool;
134
+ return structuredClone(definition) as ToolDefinition;
135
+ }
136
+
235
137
  export class BridgeRoom extends DurableObject<BridgeEnv> {
236
138
  private readonly pending = new Map<string, PendingCall>();
237
139
  private oauthQueue: Promise<void> = Promise.resolve();
@@ -300,7 +202,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
300
202
  return json({ error: "not_found" }, 404);
301
203
  } catch (error) {
302
204
  if (error instanceof HttpError) return json({ error: error.code, message: error.message }, error.status);
303
- console.error(JSON.stringify({ level: "error", message: "request_failed", path: url.pathname, error: errorMessage(error) }));
205
+ console.error(JSON.stringify({ level: "error", message: "request_failed", path: url.pathname, error_class: workerErrorClass(error) }));
304
206
  return json({ error: "internal_server_error" }, 500);
305
207
  }
306
208
  }
@@ -346,11 +248,12 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
346
248
  return;
347
249
  }
348
250
  }
251
+ const daemonPolicy = sanitizeDaemonPolicy(body.policy);
349
252
  ws.serializeAttachment({
350
253
  role: "daemon",
351
254
  connectedAt: new Date().toISOString(),
352
- policy: sanitizeDaemonPolicy(body.policy),
353
- tools: sanitizeDaemonTools(body.tools),
255
+ policy: daemonPolicy,
256
+ tools: sanitizeDaemonTools(body.tools, daemonPolicy),
354
257
  } satisfies DaemonAttachment);
355
258
  await this.scheduleCandidateAlarm();
356
259
  try {
@@ -428,21 +331,37 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
428
331
  const body = await parseJsonRequest(request, this.bodyLimitBytes());
429
332
  if (isJsonRpcResponse(body)) return new Response(null, { status: 202 });
430
333
  if (!isJsonRpcRequest(body)) return json(rpcError(null, -32600, "Invalid JSON-RPC request"), 400);
431
- const response = await this.dispatchJsonRpc(body, base);
334
+ const protocolError = validateProtocolVersionHeader(request, body);
335
+ if (protocolError) return json(protocolError, 400);
336
+ const response = await this.dispatchJsonRpc(body, base, authorized);
432
337
  if (response === null) return new Response(null, { status: 202 });
433
338
  return json(response);
434
339
  }
435
340
 
436
- private async dispatchJsonRpc(request: JsonRpcRequest, base: string): Promise<Record<string, unknown> | null> {
341
+ private async dispatchJsonRpc(request: JsonRpcRequest, base: string, authorized: AuthorizedToken): Promise<Record<string, unknown> | null> {
437
342
  if (request.method === "initialize") {
343
+ const requested = asObject(request.params).protocolVersion;
344
+ const protocolVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested as typeof MCP_SUPPORTED_PROTOCOL_VERSIONS[number])
345
+ ? requested
346
+ : MCP_PROTOCOL_VERSION;
438
347
  return rpcResult(request.id, {
439
- protocolVersion: MCP_PROTOCOL_VERSION,
440
- capabilities: { tools: { listChanged: false } },
441
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
348
+ protocolVersion,
349
+ capabilities: { tools: { listChanged: false }, logging: {} },
350
+ serverInfo: {
351
+ name: SERVER_NAME,
352
+ title: "Machine Bridge MCP",
353
+ version: SERVER_VERSION,
354
+ description: "Workspace-scoped local coding tools over authenticated remote relay.",
355
+ },
442
356
  instructions: MCP_INSTRUCTIONS,
443
357
  });
444
358
  }
445
359
  if (request.method === "notifications/initialized") return null;
360
+ if (request.method === "notifications/cancelled") {
361
+ this.cancelClientRequest(clientRequestKey(authorized, asObject(request.params).requestId));
362
+ return null;
363
+ }
364
+ if (request.method === "logging/setLevel") return rpcResult(request.id, {});
446
365
  if (request.method === "ping") return rpcResult(request.id, {});
447
366
  if (request.method === "tools/list") return rpcResult(request.id, { tools: this.allTools() });
448
367
  if (request.method === "tools/call") {
@@ -450,7 +369,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
450
369
  const name = requiredString(params, "name");
451
370
  const args = asObject(params.arguments);
452
371
  try {
453
- const result = await this.callTool(name, args, base);
372
+ const result = await this.callTool(name, args, base, clientRequestKey(authorized, request.id));
454
373
  return rpcResult(request.id, textToolResult(result));
455
374
  } catch (error) {
456
375
  return rpcResult(request.id, textToolResult({ error: errorMessage(error) }, true));
@@ -459,7 +378,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
459
378
  return rpcError(request.id, -32601, `Method not found: ${request.method}`);
460
379
  }
461
380
 
462
- private async callTool(name: string, args: Record<string, unknown>, base: string): Promise<unknown> {
381
+ private async callTool(name: string, args: Record<string, unknown>, base: string, requestKey?: string): Promise<unknown> {
463
382
  if (name === "server_info") {
464
383
  return {
465
384
  name: SERVER_NAME,
@@ -472,23 +391,27 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
472
391
  }
473
392
  if (workspaceTools.some((tool) => tool.name === name)) {
474
393
  if (!this.daemonToolEnabled(name)) throw new Error(`tool disabled by local daemon policy: ${name}`);
475
- return this.callDaemonTool(name, args);
394
+ return this.callDaemonTool(name, args, requestKey);
476
395
  }
477
396
  throw new Error(`unknown tool: ${name}`);
478
397
  }
479
398
 
480
- private async callDaemonTool(name: string, args: Record<string, unknown>): Promise<unknown> {
399
+ private async callDaemonTool(name: string, args: Record<string, unknown>, requestKey?: string): Promise<unknown> {
481
400
  const socket = this.daemonSockets()[0];
482
401
  if (!socket) throw new Error("local daemon is not connected; keep the CLI start command running");
483
402
  if (this.pending.size >= MAX_PENDING_CALLS) throw new Error("too many concurrent daemon tool calls");
403
+ if (requestKey && [...this.pending.values()].some((pending) => pending.clientRequestKey === requestKey)) {
404
+ throw new Error("duplicate in-flight JSON-RPC request id for this access token");
405
+ }
484
406
  const id = randomToken("call");
485
407
  const timeoutMs = daemonToolTimeoutMs(name, args);
486
408
  return await new Promise((resolve, reject) => {
487
409
  const timeout = setTimeout(() => {
488
410
  this.pending.delete(id);
411
+ try { socket.send(JSON.stringify({ type: "cancel_call", id })); } catch {}
489
412
  reject(new Error(`daemon tool timed out: ${name}`));
490
413
  }, timeoutMs);
491
- this.pending.set(id, { resolve, reject, timeout, socket });
414
+ this.pending.set(id, { resolve, reject, timeout, socket, clientRequestKey: requestKey });
492
415
  try {
493
416
  socket.send(JSON.stringify({ type: "tool_call", id, tool: name, arguments: args, timeout_ms: timeoutMs }));
494
417
  } catch (error) {
@@ -499,6 +422,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
499
422
  });
500
423
  }
501
424
 
425
+ private cancelClientRequest(requestKey?: string): void {
426
+ if (!requestKey) return;
427
+ for (const [id, pending] of this.pending) {
428
+ if (pending.clientRequestKey !== requestKey) continue;
429
+ clearTimeout(pending.timeout);
430
+ this.pending.delete(id);
431
+ try { pending.socket.send(JSON.stringify({ type: "cancel_call", id })); } catch {}
432
+ pending.reject(new Error("tool call cancelled by client"));
433
+ }
434
+ }
435
+
502
436
  private async acceptDaemonWebSocket(request: Request): Promise<Response> {
503
437
  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return new Response("Expected Upgrade: websocket", { status: 426 });
504
438
  const expected = this.env.DAEMON_SHARED_SECRET ?? "";
@@ -523,20 +457,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
523
457
 
524
458
  private allTools(): Array<Record<string, unknown>> {
525
459
  const advertised = this.daemonAdvertisedTools();
526
- const localTools = advertised
527
- ? workspaceTools.filter((tool) => advertised.has(tool.name))
528
- : workspaceTools;
529
- return [serverInfoTool, ...localTools].map((tool) => ({ ...tool }));
460
+ const localTools = workspaceTools.filter((tool) => advertised.has(tool.name));
461
+ return [serverInfoTool, ...localTools].map((tool) => structuredClone(tool));
530
462
  }
531
463
 
532
464
  private daemonToolEnabled(name: string): boolean {
533
- const advertised = this.daemonAdvertisedTools();
534
- return !advertised || advertised.has(name);
465
+ return this.daemonAdvertisedTools().has(name);
535
466
  }
536
467
 
537
- private daemonAdvertisedTools(): Set<string> | null {
468
+ private daemonAdvertisedTools(): Set<string> {
538
469
  const socket = this.daemonSockets()[0];
539
- if (!socket) return null;
470
+ if (!socket) return new Set();
540
471
  const attachment = this.daemonAttachment(socket);
541
472
  if (!attachment?.tools) return new Set();
542
473
  return new Set(attachment.tools);
@@ -568,11 +499,12 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
568
499
  if (!raw || typeof raw !== "object") return undefined;
569
500
  const candidate = raw as Partial<DaemonAttachment>;
570
501
  if (candidate.role !== "candidate" && candidate.role !== "expired" && candidate.role !== "daemon") return undefined;
502
+ const policy = sanitizeDaemonPolicy(candidate.policy);
571
503
  return {
572
504
  role: candidate.role,
573
505
  connectedAt: sanitizeMetadataText(candidate.connectedAt, 64) ?? "",
574
- policy: sanitizeDaemonPolicy(candidate.policy),
575
- tools: sanitizeDaemonTools(candidate.tools),
506
+ policy,
507
+ tools: sanitizeDaemonTools(candidate.tools, policy),
576
508
  };
577
509
  }
578
510
 
@@ -641,10 +573,9 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
641
573
  name: SERVER_NAME,
642
574
  version: SERVER_VERSION,
643
575
  protocolVersion: MCP_PROTOCOL_VERSION,
576
+ protocolVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS],
644
577
  transport: { type: "streamable-http", url: `${base}/mcp` },
645
- auth: { type: "oauth" },
646
- tools: [serverInfoTool, ...workspaceTools].map((tool) => tool.name),
647
- instructions: MCP_INSTRUCTIONS,
578
+ auth: { type: "oauth", authorization_servers: [base] },
648
579
  };
649
580
  }
650
581
 
@@ -910,22 +841,22 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
910
841
  });
911
842
  }
912
843
 
913
- private async verifyAccessToken(token: string, base: string): Promise<boolean> {
844
+ private async verifyAccessToken(token: string, base: string): Promise<AuthorizedToken | null> {
914
845
  return this.withOAuthLock(async () => {
915
- if (!token) return false;
846
+ if (!token) return null;
916
847
  const store = await this.oauthStore();
917
848
  const key = `sha256:${await sha256Hex(token)}`;
918
849
  const record = store.tokens[key];
919
- if (!record) return false;
850
+ if (!record) return null;
920
851
  if (record.expires_at <= Math.floor(Date.now() / 1000)) {
921
852
  delete store.tokens[key];
922
853
  await this.ctx.storage.put("oauth", store);
923
- return false;
854
+ return null;
924
855
  }
925
856
  const currentVersion = this.env.OAUTH_TOKEN_VERSION ?? "";
926
- if (!record.version || !currentVersion || !(await safeEqual(record.version, currentVersion))) return false;
927
- if (record.resource !== `${base}/mcp`) return false;
928
- return true;
857
+ if (!record.version || !currentVersion || !(await safeEqual(record.version, currentVersion))) return null;
858
+ if (record.resource !== `${base}/mcp`) return null;
859
+ return { tokenKey: key, clientId: record.client_id };
929
860
  });
930
861
  }
931
862
 
@@ -985,12 +916,30 @@ function rpcResult(id: JsonRpcId | undefined, result: unknown): Record<string, u
985
916
  return { jsonrpc: JSONRPC_VERSION, id, result };
986
917
  }
987
918
 
988
- function rpcError(id: JsonRpcId | undefined, code: number, message: string): Record<string, unknown> {
989
- return { jsonrpc: JSONRPC_VERSION, id: id ?? null, error: { code, message } };
919
+ function rpcError(id: JsonRpcId | undefined, code: number, message: string, data?: unknown): Record<string, unknown> {
920
+ const error: Record<string, unknown> = { code, message };
921
+ if (data !== undefined) error.data = data;
922
+ return { jsonrpc: JSONRPC_VERSION, id: id ?? null, error };
990
923
  }
991
924
 
992
925
  function textToolResult(value: unknown, isError = false): Record<string, unknown> {
993
- return { content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }], isError };
926
+ const special = asObject(value).$mcp;
927
+ if (special && typeof special === "object" && !Array.isArray(special)) {
928
+ const specialObject = special as Record<string, unknown>;
929
+ if (Array.isArray(specialObject.content)) {
930
+ const result: Record<string, unknown> = { content: specialObject.content, isError };
931
+ if (specialObject.structuredContent && typeof specialObject.structuredContent === "object" && !Array.isArray(specialObject.structuredContent)) {
932
+ result.structuredContent = specialObject.structuredContent;
933
+ }
934
+ return result;
935
+ }
936
+ }
937
+ const result: Record<string, unknown> = {
938
+ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }],
939
+ isError,
940
+ };
941
+ if (value && typeof value === "object" && !Array.isArray(value)) result.structuredContent = value;
942
+ return result;
994
943
  }
995
944
 
996
945
  function methodNotAllowed(allow: string): Response {
@@ -1031,19 +980,35 @@ function sanitizeMetadataText(value: unknown, maxLength: number): string | undef
1031
980
  }
1032
981
 
1033
982
 
1034
- function sanitizeDaemonTools(value: unknown): string[] {
983
+ function sanitizeDaemonTools(value: unknown, policy: Record<string, unknown>): string[] {
1035
984
  if (!Array.isArray(value)) return [];
1036
- const known = new Set<string>(workspaceTools.map((tool) => tool.name));
1037
- return [...new Set(value.filter((item): item is string => typeof item === "string" && known.has(item)))];
985
+ const definitions = new Map(allCatalogTools.map((tool) => [tool.name, tool]));
986
+ return [...new Set(value.filter((item): item is string => {
987
+ if (typeof item !== "string" || item === "server_info") return false;
988
+ const definition = definitions.get(item);
989
+ return Boolean(definition && daemonPolicyAllows(definition.availability, policy));
990
+ }))];
991
+ }
992
+
993
+ function daemonPolicyAllows(availability: unknown, policy: Record<string, unknown>): boolean {
994
+ if (availability === "always") return true;
995
+ if (availability === "write") return policy.allowWrite === true;
996
+ if (availability === "direct-exec") return policy.execMode === "direct" || policy.execMode === "shell";
997
+ if (availability === "shell-exec") return policy.execMode === "shell";
998
+ return false;
1038
999
  }
1039
1000
 
1040
1001
  function sanitizeDaemonPolicy(value: unknown): Record<string, unknown> {
1041
1002
  const policy = asObject(value);
1003
+ const execMode = policy.execMode === "shell" || policy.execMode === "direct" ? policy.execMode : "off";
1042
1004
  return {
1005
+ profile: sanitizeMetadataText(policy.profile, 32) ?? "custom",
1043
1006
  allowWrite: policy.allowWrite === true,
1044
- allowExec: policy.allowExec === true,
1007
+ allowExec: execMode !== "off",
1008
+ execMode,
1045
1009
  unrestrictedPaths: policy.unrestrictedPaths === true,
1046
1010
  minimalEnv: policy.minimalEnv !== false,
1011
+ exposeAbsolutePaths: policy.exposeAbsolutePaths === true,
1047
1012
  };
1048
1013
  }
1049
1014
 
@@ -1164,11 +1129,27 @@ function clampNumber(value: unknown, fallback: number, min: number, max: number)
1164
1129
  }
1165
1130
 
1166
1131
  function daemonToolTimeoutMs(name: string, args: Record<string, unknown>): number {
1167
- if (name !== "exec_command") return 60_000;
1132
+ if (name !== "exec_command" && name !== "run_process") return 60_000;
1168
1133
  const seconds = clampNumber(args.timeout_seconds, 120, 1, 600);
1169
1134
  return Math.min((seconds + 5) * 1000, 610_000);
1170
1135
  }
1171
1136
 
1137
+ function validateProtocolVersionHeader(request: Request, body: JsonRpcRequest): Record<string, unknown> | null {
1138
+ if (body.method === "initialize") return null;
1139
+ const version = request.headers.get("MCP-Protocol-Version");
1140
+ if (!version) return null;
1141
+ if (MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(version as typeof MCP_SUPPORTED_PROTOCOL_VERSIONS[number])) return null;
1142
+ return rpcError(body.id, -32602, "Unsupported MCP protocol version", {
1143
+ requested: version,
1144
+ supported: [...MCP_SUPPORTED_PROTOCOL_VERSIONS],
1145
+ });
1146
+ }
1147
+
1148
+ function clientRequestKey(authorized: AuthorizedToken, requestId: unknown): string | undefined {
1149
+ if (requestId === null || (typeof requestId !== "string" && typeof requestId !== "number")) return undefined;
1150
+ return `${authorized.tokenKey}:${typeof requestId}:${String(requestId)}`;
1151
+ }
1152
+
1172
1153
  function validateAuthorizationRequest(
1173
1154
  body: Record<string, unknown>,
1174
1155
  base: string,
@@ -1379,6 +1360,14 @@ function escapeHtml(value: string): string {
1379
1360
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
1380
1361
  }
1381
1362
 
1363
+ function workerErrorClass(error: unknown): string {
1364
+ if (error instanceof HttpError) return error.code;
1365
+ if (error instanceof TypeError) return "type_error";
1366
+ if (error instanceof RangeError) return "range_error";
1367
+ if (error instanceof Error) return error.name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase().slice(0, 64) || "error";
1368
+ return "unknown_error";
1369
+ }
1370
+
1382
1371
  function errorMessage(error: unknown): string {
1383
1372
  if (error instanceof Error) return error.message;
1384
1373
  if (typeof error === "string") return error;
package/tsconfig.json CHANGED
@@ -10,10 +10,12 @@
10
10
  "strict": true,
11
11
  "noEmit": true,
12
12
  "skipLibCheck": true,
13
- "forceConsistentCasingInFileNames": true
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true
14
15
  },
15
16
  "include": [
16
17
  "src/worker/**/*.ts",
17
- "src/worker/**/*.d.ts"
18
+ "src/worker/**/*.d.ts",
19
+ "src/shared/**/*.json"
18
20
  ]
19
21
  }