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.
- package/CHANGELOG.md +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
package/src/worker/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
53
|
-
const
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
431
|
+
supportedVersions: MCP_LEGACY_PROTOCOL_VERSIONS,
|
|
396
432
|
resumption: this.resumption,
|
|
397
433
|
});
|
|
398
434
|
}
|
|
399
435
|
|
|
400
|
-
const body = await parseJsonRequest(request, this.
|
|
401
|
-
if (isJsonRpcResponse(body))
|
|
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
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
424
|
-
return json(rpcError(
|
|
452
|
+
if (error instanceof McpHttpContractError) {
|
|
453
|
+
return json(rpcError(body.id, error.code, error.message, error.data), error.status);
|
|
425
454
|
}
|
|
426
|
-
|
|
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
|
|
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
|
|
482
|
-
|
|
473
|
+
private async handleLegacyStreamedToolCall(
|
|
474
|
+
body: JsonRpcRequest,
|
|
483
475
|
base: string,
|
|
484
476
|
authorized: AuthorizedToken,
|
|
485
477
|
sessionId: string,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
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(
|
|
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 {
|