machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/GOVERNANCE.md +2 -2
  4. package/README.md +24 -6
  5. package/browser-extension/manifest.json +1 -1
  6. package/docs/AGENT_CONTEXT.md +10 -7
  7. package/docs/ARCHITECTURE.md +35 -22
  8. package/docs/AUDIT.md +85 -1
  9. package/docs/CLIENTS.md +6 -2
  10. package/docs/ENGINEERING.md +31 -9
  11. package/docs/LOCAL_AUTOMATION.md +4 -2
  12. package/docs/LOGGING.md +8 -8
  13. package/docs/OPERATIONS.md +43 -17
  14. package/docs/PRIVACY.md +18 -4
  15. package/docs/PROJECT_STANDARDS.md +2 -2
  16. package/docs/RELEASING.md +35 -11
  17. package/docs/TESTING.md +36 -16
  18. package/docs/THREAT_MODEL.md +20 -5
  19. package/docs/TOOL_REFERENCE.md +18 -12
  20. package/docs/UPGRADING.md +32 -0
  21. package/package.json +15 -6
  22. package/scripts/check-plan.mjs +8 -0
  23. package/scripts/coverage-check.mjs +30 -1
  24. package/scripts/foreground-daemon-recovery.mjs +88 -0
  25. package/scripts/github-release.mjs +22 -16
  26. package/scripts/install-published-prerelease.mjs +7 -7
  27. package/scripts/official-mcp-conformance.mjs +243 -0
  28. package/scripts/persistent-activation-process.mjs +36 -0
  29. package/scripts/release-candidate-manifest.mjs +12 -0
  30. package/scripts/release-publication-guard.mjs +65 -0
  31. package/scripts/release-state.mjs +1 -1
  32. package/scripts/sbom-check.mjs +99 -0
  33. package/scripts/start-release-candidate.mjs +39 -13
  34. package/src/local/agent-context-projection.mjs +26 -7
  35. package/src/local/agent-context.mjs +25 -4
  36. package/src/local/autostart-log-maintenance.mjs +36 -0
  37. package/src/local/capability-observer.mjs +5 -0
  38. package/src/local/child-process-settlement.mjs +103 -0
  39. package/src/local/cli-activate.mjs +42 -5
  40. package/src/local/cli-service.mjs +55 -5
  41. package/src/local/cli.mjs +59 -10
  42. package/src/local/daemon-process.mjs +24 -3
  43. package/src/local/delegated-process-sandbox.mjs +1 -0
  44. package/src/local/execution-routing.mjs +231 -0
  45. package/src/local/git-service.mjs +3 -1
  46. package/src/local/job-runner.mjs +55 -19
  47. package/src/local/macos-trust-broker.mjs +7 -0
  48. package/src/local/managed-job-runner-claim.mjs +54 -0
  49. package/src/local/managed-job-runner.mjs +13 -2
  50. package/src/local/process-execution.mjs +2 -2
  51. package/src/local/process-identity.mjs +11 -0
  52. package/src/local/process-tree-ownership-types.d.ts +37 -0
  53. package/src/local/process-tree-ownership.mjs +49 -41
  54. package/src/local/process-tree.mjs +1 -1
  55. package/src/local/relay-call-recovery.mjs +40 -21
  56. package/src/local/runtime-activation.mjs +357 -38
  57. package/src/local/runtime-capabilities.mjs +22 -6
  58. package/src/local/runtime-diagnostics.mjs +9 -2
  59. package/src/local/runtime.mjs +18 -4
  60. package/src/local/service-convergence.mjs +33 -0
  61. package/src/local/service-owner.mjs +147 -0
  62. package/src/local/service-restart-handoff.mjs +22 -8
  63. package/src/local/service-runtime.mjs +145 -0
  64. package/src/local/service.mjs +143 -25
  65. package/src/local/state.mjs +104 -7
  66. package/src/local/stdio.mjs +139 -45
  67. package/src/local/system-network-route.mjs +76 -0
  68. package/src/local/tool-executor.mjs +24 -6
  69. package/src/local/tools.mjs +6 -5
  70. package/src/local/windows-service-convergence.mjs +49 -0
  71. package/src/local/windows-service.mjs +30 -53
  72. package/src/shared/mcp-protocol.d.mts +27 -0
  73. package/src/shared/mcp-protocol.mjs +256 -0
  74. package/src/shared/mcp-subscriptions.d.mts +4 -0
  75. package/src/shared/mcp-subscriptions.mjs +59 -0
  76. package/src/shared/relay-contract.json +1 -0
  77. package/src/shared/result-projection.d.mts +2 -1
  78. package/src/shared/result-projection.mjs +13 -2
  79. package/src/shared/server-metadata.json +11 -4
  80. package/src/shared/tool-argument-validation.d.mts +17 -0
  81. package/src/shared/tool-argument-validation.mjs +325 -0
  82. package/src/shared/tool-catalog.json +18 -12
  83. package/src/worker/durable-stream-calls.ts +12 -24
  84. package/src/worker/http.ts +36 -2
  85. package/src/worker/index.ts +181 -165
  86. package/src/worker/mcp-http-contract.ts +276 -0
  87. package/src/worker/mcp-jsonrpc.ts +12 -6
  88. package/src/worker/mcp-legacy-dispatch.ts +104 -0
  89. package/src/worker/mcp-modern-controller.ts +199 -0
  90. package/src/worker/mcp-modern-proxy.ts +126 -0
  91. package/src/worker/mcp-modern-stream.ts +71 -0
  92. package/src/worker/mcp-session.ts +12 -3
  93. package/src/worker/mcp-stream-proxy-contract.ts +67 -0
  94. package/src/worker/mcp-stream-proxy.ts +17 -60
  95. package/src/worker/mcp-tool-call-input.ts +23 -0
  96. package/src/worker/tool-catalog.ts +29 -1
  97. package/src/worker/tool-timeout.ts +53 -12
  98. package/src/worker/worker-mcp-config.ts +23 -0
  99. package/src/worker/worker-metadata.ts +10 -1
  100. package/src/worker/worker-runtime-config.ts +19 -0
  101. package/src/worker/worker-static-routes.ts +7 -2
  102. package/tsconfig.local.json +7 -1
@@ -1,5 +1,4 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
- import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
3
2
  import relayContract from "../shared/relay-contract.json" with { type: "json" };
4
3
  import { PendingCallRegistrationError, PendingCallRegistry } from "./pending-calls.ts";
5
4
  import { PendingAdmissionGate } from "./pending-admission.ts";
@@ -7,15 +6,17 @@ import type { PendingCallOutcome } from "./pending-call-contract.ts";
7
6
  import {
8
7
  DAEMON_LIVENESS_TIMEOUT_MS,
9
8
  DAEMON_READY_TIMEOUT_MS,
10
- daemonLastSeenMs,
11
9
  daemonLivenessDeadlineMs,
12
10
  isFreshDaemonCandidate,
13
11
  } from "./daemon-liveness.ts";
14
12
  import { DaemonSocketRegistry } from "./daemon-sockets.ts";
15
13
  import { processRuntimeAlarm, scheduleRuntimeAlarm } from "./runtime-alarm.ts";
16
14
  import { consumeDaemonPreflightNonce, createDaemonChallenge, verifyDaemonAuthentication, verifyDaemonPreflight } from "./daemon-auth.ts";
17
- import { mcpClientRequestKey, resolveMcpSession } from "./mcp-session.ts";
15
+ import { legacyMcpClientRequestKey, resolveMcpSession } from "./mcp-session.ts";
16
+ import { LegacyMcpDispatcher } from "./mcp-legacy-dispatch.ts";
17
+ import { inspectWorkerToolCall } from "./mcp-tool-call-input.ts";
18
18
  import { acceptsEventStream } from "./mcp-stream.ts";
19
+ import { ModernMcpController } from "./mcp-modern-controller.ts";
19
20
  import { authorizeMcpRequest } from "./mcp-access.ts";
20
21
  import { handleMcpResumptionRequest } from "./mcp-resumption-http.ts";
21
22
  import {
@@ -31,45 +32,40 @@ import { WorkerObservability } from "./observability.ts";
31
32
  import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors.ts";
32
33
  import { sanitizeDaemonPolicy, sanitizeDaemonTools } from "./policy.ts";
33
34
  import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from "./access.ts";
34
- import { OAuthController, type AuthorizedToken, type OAuthControllerEnv } from "./oauth-controller.ts";
35
+ import { OAuthController, type AuthorizedToken } from "./oauth-controller.ts";
35
36
  import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority.ts";
36
- import { serverInfoTool, workspaceTools } from "./tool-catalog.ts";
37
+ import { serverInfoTool, validateWorkerToolArguments, workerToolParameterHeaders, workspaceTools } from "./tool-catalog.ts";
38
+ import { detectHttpMcpEra, McpHttpContractError, validateModernHttpRequest } from "./mcp-http-contract.ts";
37
39
  import { randomToken } from "./oauth-state.ts";
38
40
  import {
39
- HttpError, applyCors, baseUrl, corsPreflight, json, methodNotAllowed,
41
+ HttpError, applyCors, baseUrl, corsPreflight, json, mcpOriginRejection, methodNotAllowed,
40
42
  parseJsonRequest, workerErrorClass,
41
43
  } from "./http.ts";
42
44
  import { authorizationServerMetadata } from "./worker-metadata.ts";
45
+ import { workerBodyLimitBytes, type BridgeEnv } from "./worker-runtime-config.ts";
43
46
  import {
44
- asObject, isJsonRpcRequest, isJsonRpcResponse, requiredString, rpcError, rpcResult,
45
- sessionInstructionText, textToolResult, validateProtocolVersionHeader, type JsonRpcRequest,
47
+ MCP_DISCOVERY_TTL_MS, MCP_INSTRUCTIONS, MCP_LEGACY_PROTOCOL_VERSIONS,
48
+ MCP_MODERN_PROTOCOL_VERSIONS, MCP_SERVER_CAPABILITIES, MCP_TOOL_LIST_TTL_MS,
49
+ SERVER_NAME, mcpServerInfo,
50
+ } from "./worker-mcp-config.ts";
51
+ import {
52
+ MCP_LEGACY_PROTOCOL_VERSION, MCP_MODERN_PROTOCOL_VERSION,
53
+ } from "../shared/mcp-protocol.mjs";
54
+ import {
55
+ asObject, isJsonRpcRequest, isJsonRpcResponse, rpcError,
56
+ validateProtocolVersionHeader, type JsonRpcRequest,
46
57
  } from "./mcp-jsonrpc.ts";
47
58
  import {
48
59
  closeWebSocketQuietly, daemonErrorCloseCode, isObjectRecord, rejectDaemonMessage,
49
60
  sendWebSocketQuietly, trySendWebSocket,
50
61
  } from "./websocket-protocol.ts";
51
62
 
52
- const SERVER_NAME = String(serverMetadata.name);
53
- const SERVER_VERSION = "3.0.0-beta.21";
54
- const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
55
- const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
56
- const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
57
- const MAX_BODY_BYTES = 16 * 1024 * 1024;
63
+ const SERVER_VERSION = "3.0.0-beta.26";
64
+ const MCP_SERVER_INFO = mcpServerInfo(SERVER_VERSION);
58
65
  const MAX_PENDING_CALLS = 32;
59
66
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
60
67
  const DAEMON_RECONNECT_GRACE_MS = relayContract.reconnectGraceMs;
61
68
 
62
- interface BridgeEnv extends OAuthControllerEnv {
63
- BRIDGE: DurableObjectNamespace<BridgeRoom>;
64
- DAEMON_DEVICE_PUBLIC_KEY: string;
65
- OAUTH_TOKEN_VERSION: string;
66
- MBM_WORKER_MAX_BODY_BYTES?: string;
67
- MBM_ALLOWED_ORIGINS?: string;
68
- STATEFUL_RATE_LIMITER: RateLimit;
69
- }
70
-
71
- const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
72
-
73
69
  export class BridgeRoom extends DurableObject<BridgeEnv> {
74
70
  private readonly pending = new PendingCallRegistry(MAX_PENDING_CALLS);
75
71
  private readonly observability = new WorkerObservability();
@@ -79,6 +75,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
79
75
  private readonly resumption: McpResumptionStore;
80
76
  private readonly durableCalls: DurableStreamCallCoordinator;
81
77
  private readonly pendingAdmission = new PendingAdmissionGate();
78
+ private readonly modernMcp: ModernMcpController;
79
+ private readonly legacyMcp: LegacyMcpDispatcher;
82
80
 
83
81
  constructor(ctx: DurableObjectState, env: BridgeEnv) {
84
82
  super(ctx, env);
@@ -99,16 +97,42 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
99
97
  this.daemonRegistry,
100
98
  this.observability,
101
99
  MAX_PENDING_CALLS,
102
- (socket, message, closeReason, errorCode) => this.invalidateDaemonSocket(socket, message, closeReason, errorCode),
103
100
  );
101
+ this.modernMcp = new ModernMcpController({
102
+ capabilities: MCP_SERVER_CAPABILITIES,
103
+ serverInfo: MCP_SERVER_INFO,
104
+ instructions: MCP_INSTRUCTIONS,
105
+ supportedVersions: MCP_MODERN_PROTOCOL_VERSIONS,
106
+ discoveryTtlMs: MCP_DISCOVERY_TTL_MS,
107
+ toolListTtlMs: MCP_TOOL_LIST_TTL_MS,
108
+ tools: (authorized) => this.allTools(authorized.role),
109
+ recordError: (code) => this.observability.recordError(code),
110
+ cancelClientRequest: (requestKey) => this.cancelClientRequest(requestKey),
111
+ callTool: ({ name, args, base, authorized, signal, requestKey }) => this.callTool(
112
+ name, args, base, authorized, requestKey, signal,
113
+ ),
114
+ });
115
+ this.legacyMcp = new LegacyMcpDispatcher({
116
+ defaultVersion: MCP_LEGACY_PROTOCOL_VERSION,
117
+ supportedVersions: MCP_LEGACY_PROTOCOL_VERSIONS,
118
+ serverInfo: MCP_SERVER_INFO,
119
+ instructions: MCP_INSTRUCTIONS,
120
+ daemonToolEnabled: (name) => this.daemonToolEnabled(name),
121
+ callDaemonTool: (name, args, authorized) => this.callDaemonTool(name, args, authorized),
122
+ recordError: (code) => this.observability.recordError(code),
123
+ cancelClientRequest: (requestKey) => this.cancelClientRequest(requestKey),
124
+ tools: (authorized) => this.allTools(authorized.role),
125
+ callTool: ({ name, args, base, authorized, requestKey }) => this.callTool(
126
+ name, args, base, authorized, requestKey,
127
+ ),
128
+ });
104
129
  }
105
-
106
130
  async fetch(request: Request): Promise<Response> {
107
131
  await this.expireOverdueCalls();
108
132
  const base = baseUrl(request);
109
133
  const extraOrigins = this.env.MBM_ALLOWED_ORIGINS ?? "";
110
134
  let response: Response;
111
- if (request.method === "OPTIONS" && request.headers.has("Origin")) response = corsPreflight(request, base, extraOrigins);
135
+ if (request.method === "OPTIONS" && request.headers.has("Origin")) response = corsPreflight(request, base, extraOrigins, workerToolParameterHeaders);
112
136
  else response = applyCors(await this.handleRequest(request, base), request, base, extraOrigins);
113
137
  this.observability.requestFinished(response.status);
114
138
  return response;
@@ -343,9 +367,13 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
343
367
  let matched = outcome.ok
344
368
  ? await this.pending.resolve(body.id, ws, outcome.value)
345
369
  : await this.pending.reject(body.id, outcome.error, ws);
370
+ let acknowledge = Boolean(matched);
346
371
  if (!matched && socketAttachment.connectionId) {
347
- matched = await this.durableCalls.settle(body.id, socketAttachment.connectionId, outcome);
372
+ const settlement = await this.durableCalls.settle(body.id, socketAttachment.connectionId, outcome);
373
+ matched = settlement === "committed";
374
+ acknowledge = settlement !== "stale";
348
375
  }
376
+ if (acknowledge) trySendWebSocket(ws, { type: "tool_result_ack", id: body.id });
349
377
  if (!matched) this.observability.unmatchedResult();
350
378
  else await this.scheduleRuntimeAlarm();
351
379
  }
@@ -367,23 +395,31 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
367
395
  await this.scheduleRuntimeAlarm();
368
396
  }
369
397
  private async handleMcp(request: Request, base: string): Promise<Response> {
398
+ const originRejection = mcpOriginRejection(request, base, this.env.MBM_ALLOWED_ORIGINS ?? "");
399
+ if (originRejection) return originRejection;
400
+ const headerVersion = request.headers.get("MCP-Protocol-Version")?.trim() ?? "";
370
401
  if (request.method !== "GET" && request.method !== "POST") {
371
- return new Response(request.method === "HEAD" ? null : JSON.stringify({ error: "mcp endpoint expects GET or POST" }), {
402
+ return new Response(request.method === "HEAD" ? null : JSON.stringify({ error: "mcp endpoint expects POST, or legacy GET" }), {
372
403
  status: 405,
373
404
  headers: { "content-type": "application/json; charset=utf-8", "allow": "GET, POST", "cache-control": "no-store" },
374
405
  });
375
406
  }
407
+ if (request.method === "GET" && headerVersion === MCP_MODERN_PROTOCOL_VERSION) {
408
+ return methodNotAllowed("POST");
409
+ }
376
410
 
377
411
  const proxyMode = mcpStreamProxyMode(request);
378
412
  const subscribed = await handleMcpStreamSubscribeRequest(request, this.streamChannel, this.resumption);
379
413
  if (subscribed) return subscribed;
414
+ const modernControl = await this.modernMcp.handleControl(request, proxyMode);
415
+ if (modernControl) return modernControl;
380
416
 
381
417
  const access = await authorizeMcpRequest({
382
418
  request,
383
419
  base,
384
420
  oauth: this.oauth,
385
421
  storage: this.ctx.storage,
386
- bodyLimitBytes: this.bodyLimitBytes(),
422
+ bodyLimitBytes: workerBodyLimitBytes(this.env.MBM_WORKER_MAX_BODY_BYTES),
387
423
  });
388
424
  if (access.response) return access.response;
389
425
  if (request.method === "GET") {
@@ -392,153 +428,118 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
392
428
  request,
393
429
  authorized: access.authorized,
394
430
  identityKey: this.oauth.identityKey(),
395
- supportedVersions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
431
+ supportedVersions: MCP_LEGACY_PROTOCOL_VERSIONS,
396
432
  resumption: this.resumption,
397
433
  });
398
434
  }
399
435
 
400
- const body = await parseJsonRequest(request, this.bodyLimitBytes());
401
- if (isJsonRpcResponse(body)) return new Response(null, { status: 202 });
436
+ const body = await parseJsonRequest(request, workerBodyLimitBytes(this.env.MBM_WORKER_MAX_BODY_BYTES));
437
+ if (isJsonRpcResponse(body)) {
438
+ return headerVersion === MCP_MODERN_PROTOCOL_VERSION
439
+ ? json(rpcError(null, -32600, "Clients must not send JSON-RPC responses"), 400)
440
+ : new Response(null, { status: 202 });
441
+ }
402
442
  if (!isJsonRpcRequest(body)) return json(rpcError(null, -32600, "Invalid JSON-RPC request"), 400);
403
- const protocolError = validateProtocolVersionHeader(request, body, MCP_SUPPORTED_PROTOCOL_VERSIONS);
404
- if (protocolError) return json(protocolError, 400);
405
-
406
- const session = await resolveMcpSession(request, body.method, this.oauth.identityKey(), access.authorized.tokenKey);
407
- if (session.kind === "invalid") return json(rpcError(body.id, -32001, "MCP session not found"), 404);
408
- const sessionId = session.kind === "active" ? session.sessionId : "";
409
443
 
410
- if (body.method === "tools/call" && acceptsEventStream(request)) {
411
- if (proxyMode !== "prepare") return json(rpcError(body.id, -32603, "MCP stream proxy is unavailable"), 500);
412
- if (body.id === undefined || body.id === null) return json(rpcError(null, -32600, "tools/call requires a non-null request id"), 400);
413
- const requestId = body.id;
414
- const streamId = randomToken("stream");
444
+ if (detectHttpMcpEra(request, body) === "modern") {
415
445
  try {
416
- await this.resumption.begin({
417
- streamId,
418
- tokenKey: access.authorized.tokenKey,
419
- sessionId,
420
- requestId,
446
+ const context = validateModernHttpRequest({ request, body, tools: this.allTools(access.authorized.role) as Array<{ name: string; inputSchema?: unknown }> });
447
+ return await this.modernMcp.handleRequest({
448
+ request, body, base, authorized: access.authorized,
449
+ protocolVersion: context.version, proxyMode,
421
450
  });
422
451
  } catch (error) {
423
- if (error instanceof McpStreamLimitError) {
424
- return json(rpcError(requestId, -32004, error.message), 429);
452
+ if (error instanceof McpHttpContractError) {
453
+ return json(rpcError(body.id, error.code, error.message, error.data), error.status);
425
454
  }
426
- this.observability.event("error", "mcp.stream.begin.failed", { error_class: workerErrorClass(error) });
427
- return json(rpcError(requestId, -32603, "Resumable stream storage is unavailable"), 503);
455
+ throw error;
428
456
  }
429
-
430
- const params = asObject(body.params);
431
- const name = requiredString(params, "name");
432
- const args = asObject(params.arguments);
433
- try {
434
- if (name === "server_info") {
435
- await persistImmediateStreamOutcome({
436
- resumption: this.resumption, observability: this.observability, streamId, requestId,
437
- outcome: { ok: true, value: await this.serverInfoResult(base, access.authorized) },
438
- });
439
- } else {
440
- if (!workspaceTools.some((tool) => tool.name === name)) throw new Error(`unknown tool: ${name}`);
441
- if (!accountRoleAllowsTool(access.authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
442
- this.reclaimStaleDaemonSockets();
443
- const socket = this.daemonRegistry.readySockets()[0];
444
- if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
445
- const attachment = this.daemonRegistry.readyAttachment(socket);
446
- const daemonInstanceId = attachment?.instanceId ?? "";
447
- const connectionId = attachment?.connectionId ?? "";
448
- if (!daemonInstanceId || !connectionId) throw new WorkerToolError("unavailable", "local daemon connection is missing its relay identity", true);
449
- if (!attachment?.tools?.includes(name)) throw new WorkerToolError("authorization_denied", `tool disabled by local daemon policy: ${name}`);
450
- await this.pendingAdmission.run(() => startEventDrivenStreamCall({
451
- resumption: this.resumption, observability: this.observability,
452
- streamId, requestId, clientRequestKey: mcpClientRequestKey(access.authorized.tokenKey, sessionId, requestId),
453
- tool: name, arguments: args, socket, daemonInstanceId, connectionId, timeoutMs: daemonToolTimeoutMs(name, args),
454
- transientActiveCount: this.pending.size, maximumPendingCalls: MAX_PENDING_CALLS,
455
- authorization: {
456
- account_id: access.authorized.accountId, account_version: access.authorized.accountVersion,
457
- client_id: access.authorized.clientId, family_id: access.authorized.familyId, role: access.authorized.role,
458
- },
459
- transform: name === "project_overview" ? {
460
- kind: "project_overview", account_id: access.authorized.accountId,
461
- account_version: access.authorized.accountVersion, role: access.authorized.role,
462
- } : undefined,
463
- onSendFailure: () => this.invalidateDaemonSocket(socket, "failed to send daemon tool call", "daemon send failed"),
464
- }));
465
- await this.scheduleRuntimeAlarm();
466
- }
467
- } catch (error) {
468
- await persistImmediateStreamOutcome({
469
- resumption: this.resumption, observability: this.observability, streamId, requestId,
470
- outcome: { ok: false, error: error instanceof Error ? error : new Error("streamed tool call failed") },
471
- });
472
- }
473
- return mcpStreamDescriptorResponse("initial", streamId);
474
457
  }
475
458
 
476
- const response = await this.dispatchJsonRpc(body, base, access.authorized, sessionId);
459
+ const protocolError = validateProtocolVersionHeader(request, body, MCP_LEGACY_PROTOCOL_VERSIONS);
460
+ if (protocolError) return json(protocolError, 400);
461
+ const session = await resolveMcpSession(request, body.method, this.oauth.identityKey(), access.authorized.tokenKey);
462
+ if (session.kind === "invalid") return json(rpcError(body.id, -32001, "MCP session not found"), 404);
463
+ const sessionId = session.kind === "active" ? session.sessionId : "";
464
+
465
+ if (body.method === "tools/call" && acceptsEventStream(request)) {
466
+ return await this.handleLegacyStreamedToolCall(body, base, access.authorized, sessionId, proxyMode);
467
+ }
468
+ const response = await this.legacyMcp.dispatch(body, base, access.authorized, sessionId);
477
469
  if (response === null) return new Response(null, { status: 202 });
478
470
  return session.kind === "initialize" ? json(response, 200, { "mcp-session-id": session.sessionId }) : json(response);
479
471
  }
480
472
 
481
- private async dispatchJsonRpc(
482
- request: JsonRpcRequest,
473
+ private async handleLegacyStreamedToolCall(
474
+ body: JsonRpcRequest,
483
475
  base: string,
484
476
  authorized: AuthorizedToken,
485
477
  sessionId: string,
486
- ): Promise<Record<string, unknown> | null> {
487
- if (request.method === "initialize") {
488
- const requested = asObject(request.params).protocolVersion;
489
- const protocolVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested as typeof MCP_SUPPORTED_PROTOCOL_VERSIONS[number])
490
- ? requested
491
- : MCP_PROTOCOL_VERSION;
492
- let bootstrap = null;
493
- if (this.daemonToolEnabled("session_bootstrap")) {
494
- try {
495
- bootstrap = await this.callDaemonTool("session_bootstrap", { path: "." }, authorized);
496
- } catch {
497
- // Initialization remains usable without optional local instructions, but the degradation is observable.
498
- this.observability.recordError("session_bootstrap_failed");
499
- }
500
- }
501
- const localInstructions = sessionInstructionText(bootstrap);
502
- return rpcResult(request.id, {
503
- protocolVersion,
504
- capabilities: { tools: { listChanged: false }, logging: {} },
505
- serverInfo: {
506
- name: SERVER_NAME,
507
- title: "Machine Bridge MCP",
508
- version: SERVER_VERSION,
509
- description: "Workspace-scoped local coding tools over authenticated remote relay.",
510
- },
511
- instructions: localInstructions ? `${MCP_INSTRUCTIONS}\n\n--- LOCAL SESSION INSTRUCTIONS ---\n${localInstructions}` : MCP_INSTRUCTIONS,
512
- });
478
+ proxyMode: ReturnType<typeof mcpStreamProxyMode>,
479
+ ): Promise<Response> {
480
+ if (proxyMode !== "prepare") return json(rpcError(body.id, -32603, "MCP stream proxy is unavailable"), 500);
481
+ if (body.id === undefined || body.id === null) return json(rpcError(null, -32600, "tools/call requires a non-null request id"), 400);
482
+ const requestId = body.id;
483
+ const inspected = inspectWorkerToolCall(body.params, this.allTools(authorized.role));
484
+ if (!inspected.ok) {
485
+ const message = inspected.reason === "missing_name" ? "tools/call requires a tool name"
486
+ : inspected.reason === "unknown_tool" ? "Unknown tool"
487
+ : "Tool arguments do not match the input schema";
488
+ return json(rpcError(requestId, -32602, message, inspected.issues ? { side_effects_started: false, validation_issues: [...inspected.issues] } : undefined), 400);
489
+ }
490
+ const { name, args } = inspected;
491
+ const streamId = randomToken("stream");
492
+ try {
493
+ await this.resumption.begin({ streamId, tokenKey: authorized.tokenKey, sessionId, requestId });
494
+ } catch (error) {
495
+ if (error instanceof McpStreamLimitError) return json(rpcError(requestId, -32004, error.message), 429);
496
+ this.observability.event("error", "mcp.stream.begin.failed", { error_class: workerErrorClass(error) });
497
+ return json(rpcError(requestId, -32603, "Resumable stream storage is unavailable"), 503);
513
498
  }
514
- if (request.method === "notifications/initialized") return null;
515
- if (request.method === "notifications/cancelled") {
516
- await this.cancelClientRequest(mcpClientRequestKey(authorized.tokenKey, sessionId, asObject(request.params).requestId));
517
- return null;
518
- }
519
- if (request.method === "logging/setLevel") return rpcResult(request.id, {});
520
- if (request.method === "ping") return rpcResult(request.id, {});
521
- if (request.method === "tools/list") return rpcResult(request.id, { tools: this.allTools(authorized.role) });
522
- if (request.method === "tools/call") {
523
- if (request.id === undefined || request.id === null) return rpcError(null, -32600, "tools/call requires a non-null request id");
524
- const params = asObject(request.params);
525
- const name = requiredString(params, "name");
526
- const args = asObject(params.arguments);
527
- try {
528
- const result = await this.callTool(
529
- name,
530
- args,
531
- base,
532
- authorized,
533
- mcpClientRequestKey(authorized.tokenKey, sessionId, request.id),
534
- );
535
- return rpcResult(request.id, textToolResult(result));
536
- } catch (error) {
537
- return rpcResult(request.id, textToolResult({ error: publicWorkerToolError(error) }, true));
499
+
500
+ try {
501
+ if (name === "server_info") {
502
+ await persistImmediateStreamOutcome({
503
+ resumption: this.resumption, observability: this.observability, streamId, requestId,
504
+ outcome: { ok: true, value: await this.serverInfoResult(base, authorized) },
505
+ });
506
+ } else {
507
+ if (!workspaceTools.some((tool) => tool.name === name)) throw new Error("unknown tool");
508
+ if (!accountRoleAllowsTool(authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
509
+ this.reclaimStaleDaemonSockets();
510
+ const socket = this.daemonRegistry.readySockets()[0];
511
+ if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
512
+ const attachment = this.daemonRegistry.readyAttachment(socket);
513
+ const daemonInstanceId = attachment?.instanceId ?? "";
514
+ const connectionId = attachment?.connectionId ?? "";
515
+ if (!daemonInstanceId || !connectionId) throw new WorkerToolError("unavailable", "local daemon connection is missing its relay identity", true);
516
+ if (!attachment?.tools?.includes(name)) throw new WorkerToolError("authorization_denied", `tool disabled by local daemon policy: ${name}`);
517
+ await this.pendingAdmission.run(() => startEventDrivenStreamCall({
518
+ resumption: this.resumption, observability: this.observability,
519
+ streamId, requestId, clientRequestKey: legacyMcpClientRequestKey(authorized.tokenKey, sessionId, requestId),
520
+ tool: name, arguments: args, socket, daemonInstanceId, connectionId, timeoutMs: daemonToolTimeoutMs(name, args),
521
+ transientActiveCount: this.pending.size, maximumPendingCalls: MAX_PENDING_CALLS,
522
+ authorization: {
523
+ account_id: authorized.accountId, account_version: authorized.accountVersion,
524
+ client_id: authorized.clientId, family_id: authorized.familyId, role: authorized.role,
525
+ },
526
+ transform: name === "project_overview" ? {
527
+ kind: "project_overview", account_id: authorized.accountId,
528
+ account_version: authorized.accountVersion, role: authorized.role,
529
+ } : undefined,
530
+ onSendFailure: () => this.invalidateDaemonSocket(socket, "failed to send daemon tool call", "daemon send failed"),
531
+ }));
532
+ await this.scheduleRuntimeAlarm();
538
533
  }
534
+ } catch (error) {
535
+ await persistImmediateStreamOutcome({
536
+ resumption: this.resumption, observability: this.observability, streamId, requestId,
537
+ outcome: { ok: false, error: error instanceof Error ? error : new Error("streamed tool call failed") },
538
+ });
539
539
  }
540
- return rpcError(request.id, -32601, `Method not found: ${request.method}`);
540
+ return mcpStreamDescriptorResponse("initial", streamId);
541
541
  }
542
+
542
543
  private async serverInfoResult(base: string, authorized: AuthorizedToken): Promise<Record<string, unknown>> {
543
544
  const { daemon, effectiveTools, advertisedTools, authorization } = this.authorityContext(authorized);
544
545
  return buildServerInfoResult({
@@ -548,27 +549,45 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
548
549
  daemonRegistry: this.daemonRegistry, observability: this.observability,
549
550
  });
550
551
  }
552
+ private assertWorkerToolArguments(name: string, args: unknown): void {
553
+ const validation = validateWorkerToolArguments(name, args);
554
+ if (!validation.known) throw new WorkerToolError("not_found", "unknown tool");
555
+ if (!validation.valid) {
556
+ try { daemonToolTimeoutMs(name, asObject(args)); }
557
+ catch (error) { if (error instanceof WorkerToolError) throw error; }
558
+ throw new WorkerToolError(
559
+ "invalid_request",
560
+ "tool arguments do not match the input schema",
561
+ false,
562
+ { tool: name, validation_issues: [...validation.issues] },
563
+ );
564
+ }
565
+ }
566
+
551
567
  private async callTool(
552
568
  name: string,
553
569
  args: Record<string, unknown>,
554
570
  base: string,
555
571
  authorized: AuthorizedToken,
556
572
  requestKey?: string,
573
+ signal?: AbortSignal,
557
574
  ): Promise<unknown> {
575
+ this.assertWorkerToolArguments(name, args);
558
576
  if (name === "server_info") return this.serverInfoResult(base, authorized);
559
577
  if (workspaceTools.some((tool) => tool.name === name)) {
560
578
  if (!accountRoleAllowsTool(authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
561
- const result = await this.callDaemonTool(name, args, authorized, requestKey);
579
+ const result = await this.callDaemonTool(name, args, authorized, requestKey, signal);
562
580
  return name === "project_overview" ? decorateProjectOverview(result, { accountId: authorized.accountId,
563
581
  accountVersion: authorized.accountVersion, role: authorized.role }) : result;
564
582
  }
565
- throw new Error(`unknown tool: ${name}`);
583
+ throw new Error("unknown tool");
566
584
  }
567
585
  private async callDaemonTool(
568
586
  name: string,
569
587
  args: Record<string, unknown>,
570
588
  authorized: AuthorizedToken,
571
589
  requestKey?: string,
590
+ signal?: AbortSignal,
572
591
  ): Promise<unknown> {
573
592
  this.reclaimStaleDaemonSockets();
574
593
  const socket = this.daemonRegistry.readySockets()[0];
@@ -594,6 +613,8 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
594
613
  tool: name,
595
614
  timeoutMs,
596
615
  onTimeout: (record) => this.daemonCallTimeout(record, name),
616
+ signal,
617
+ onAbort: (record) => this.daemonCallCancellation(record),
597
618
  });
598
619
  });
599
620
  } catch (error) {
@@ -630,12 +651,12 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
630
651
  }
631
652
  private daemonCallTimeout(record: import("./pending-call-contract.ts").PendingCallRecord, name: string): Error {
632
653
  if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
633
- const silentForMs = record.socket ? Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(record.socket)) : 0;
634
- if (record.socket && (!Number.isFinite(silentForMs) || silentForMs > 45_000)) {
635
- void this.invalidateDaemonSocket(record.socket, "daemon became unresponsive", "daemon liveness timeout");
636
- }
637
654
  return new WorkerToolError("timeout", `daemon tool timed out: ${name}`, true);
638
655
  }
656
+ private daemonCallCancellation(record: import("./pending-call-contract.ts").PendingCallRecord): Error {
657
+ if (record.socket) sendWebSocketQuietly(record.socket, { type: "cancel_call", id: record.id });
658
+ return new WorkerToolError("cancelled", "tool call cancelled when its HTTP response stream closed");
659
+ }
639
660
 
640
661
  private async cancelClientRequest(requestKey?: string): Promise<void> {
641
662
  if (!requestKey) return;
@@ -815,11 +836,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
815
836
  };
816
837
  }
817
838
 
818
- private bodyLimitBytes(): number {
819
- const parsed = Number.parseInt(this.env.MBM_WORKER_MAX_BODY_BYTES ?? "", 10);
820
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_BODY_BYTES;
821
- return Math.min(parsed, MAX_BODY_BYTES);
822
- }
823
839
  }
824
840
 
825
841
  export default {