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
@@ -0,0 +1,126 @@
1
+ import relayContract from "../shared/relay-contract.json" with { type: "json" };
2
+ import { MCP_MODERN_PROTOCOL_VERSION } from "../shared/mcp-protocol.mjs";
3
+ import { randomToken } from "./oauth-state.ts";
4
+ import { acceptsEventStream } from "./mcp-stream.ts";
5
+ import {
6
+ withProxyHeaders, type BridgeFetcher, type StreamExecutionContext,
7
+ } from "./mcp-stream-proxy-contract.ts";
8
+
9
+ const HEARTBEAT_MS = Number(relayContract.streamHeartbeatMs);
10
+ const HEARTBEAT = new TextEncoder().encode(": heartbeat\n\n");
11
+
12
+ export async function proxyModernMcpStream(input: {
13
+ request: Request;
14
+ bridge: BridgeFetcher;
15
+ ctx: StreamExecutionContext;
16
+ }): Promise<Response | null> {
17
+ const version = input.request.headers.get("MCP-Protocol-Version")?.trim() ?? "";
18
+ if (version !== MCP_MODERN_PROTOCOL_VERSION
19
+ || input.request.method !== "POST"
20
+ || !acceptsEventStream(input.request)) return null;
21
+
22
+ const streamId = randomToken("stream");
23
+ const upstreamController = new AbortController();
24
+ let earlyAbortReason: unknown;
25
+ const earlyAbort = () => {
26
+ earlyAbortReason = input.request.signal.reason ?? "client request aborted";
27
+ upstreamController.abort(earlyAbortReason);
28
+ };
29
+ input.request.signal.addEventListener("abort", earlyAbort, { once: true });
30
+ if (input.request.signal.aborted) earlyAbort();
31
+
32
+ let upstream: Response;
33
+ try {
34
+ upstream = await input.bridge.fetch(withProxyHeaders(
35
+ input.request, "modern-direct", streamId, upstreamController.signal,
36
+ ));
37
+ } catch (error) {
38
+ input.request.signal.removeEventListener("abort", earlyAbort);
39
+ if (earlyAbortReason !== undefined) input.ctx.waitUntil(cancelModernCall(input.bridge, input.request, streamId));
40
+ throw error;
41
+ }
42
+ if (!upstream.body || !upstream.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream")) {
43
+ input.request.signal.removeEventListener("abort", earlyAbort);
44
+ if (earlyAbortReason !== undefined) input.ctx.waitUntil(cancelModernCall(input.bridge, input.request, streamId));
45
+ return upstream;
46
+ }
47
+
48
+ const reader = upstream.body.getReader();
49
+ let closed = false;
50
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
51
+ const finish = () => {
52
+ if (closed) return false;
53
+ closed = true;
54
+ if (heartbeat) clearInterval(heartbeat);
55
+ input.request.signal.removeEventListener("abort", abort);
56
+ return true;
57
+ };
58
+ const cancel = (reason: unknown): boolean => {
59
+ if (!finish()) return false;
60
+ upstreamController.abort(reason);
61
+ void reader.cancel(reason).catch(() => {}).finally(() => releaseReaderQuietly(reader));
62
+ input.ctx.waitUntil(cancelModernCall(input.bridge, input.request, streamId));
63
+ return true;
64
+ };
65
+ const abort = () => cancel(input.request.signal.reason ?? "client request aborted");
66
+ input.request.signal.removeEventListener("abort", earlyAbort);
67
+ input.request.signal.addEventListener("abort", abort, { once: true });
68
+ if (earlyAbortReason !== undefined) cancel(earlyAbortReason);
69
+
70
+ const body = new ReadableStream<Uint8Array>({
71
+ start(target) {
72
+ if (closed) {
73
+ target.close();
74
+ return;
75
+ }
76
+ heartbeat = setInterval(() => {
77
+ if (closed) return;
78
+ try { target.enqueue(HEARTBEAT); }
79
+ catch { cancel("public response stream closed"); }
80
+ }, HEARTBEAT_MS);
81
+ void pumpUpstream(reader, target, finish, cancel);
82
+ },
83
+ cancel(reason) { cancel(reason); },
84
+ });
85
+ return new Response(body, {
86
+ status: upstream.status,
87
+ statusText: upstream.statusText,
88
+ headers: new Headers(upstream.headers),
89
+ });
90
+ }
91
+
92
+ async function pumpUpstream(
93
+ reader: ReadableStreamDefaultReader<Uint8Array>,
94
+ target: ReadableStreamDefaultController<Uint8Array>,
95
+ finish: () => boolean,
96
+ cancel: (reason: unknown) => boolean,
97
+ ): Promise<void> {
98
+ try {
99
+ for (;;) {
100
+ const chunk = await reader.read();
101
+ if (chunk.done) {
102
+ if (finish()) target.close();
103
+ return;
104
+ }
105
+ target.enqueue(chunk.value);
106
+ }
107
+ } catch (error) {
108
+ if (!cancel(error)) return;
109
+ try { target.error(error); } catch { /* The public response already closed. */ }
110
+ } finally {
111
+ releaseReaderQuietly(reader);
112
+ }
113
+ }
114
+
115
+ function releaseReaderQuietly(reader: ReadableStreamDefaultReader<Uint8Array>): void {
116
+ try { reader.releaseLock(); } catch { /* The reader may already be released by cancellation cleanup. */ }
117
+ }
118
+
119
+ async function cancelModernCall(bridge: BridgeFetcher, request: Request, streamId: string): Promise<void> {
120
+ try {
121
+ const control = new Request(request.url, { method: "POST" });
122
+ await bridge.fetch(withProxyHeaders(control, "modern-cancel", streamId));
123
+ } catch {
124
+ // The daemon call retains its bounded operation timeout if cancellation delivery fails.
125
+ }
126
+ }
@@ -0,0 +1,71 @@
1
+ import { subscriptionAcknowledgedNotification, subscriptionCompleteResult } from "../shared/mcp-subscriptions.mjs";
2
+ import { rpcResult, type JsonRpcId } from "./mcp-jsonrpc.ts";
3
+
4
+ export function modernSubscriptionResponse(input: {
5
+ requestId: Exclude<JsonRpcId, null>;
6
+ notifications: Record<string, unknown>;
7
+ serverInfo: Record<string, unknown>;
8
+ }): Response {
9
+ const encoder = new TextEncoder();
10
+ const acknowledged = subscriptionAcknowledgedNotification(input.requestId, input.notifications);
11
+ const complete = rpcResult(input.requestId, subscriptionCompleteResult(input.requestId, input.serverInfo));
12
+ const body = new ReadableStream<Uint8Array>({
13
+ start(controller) {
14
+ controller.enqueue(encoder.encode(sseMessage(acknowledged)));
15
+ controller.enqueue(encoder.encode(sseMessage(complete)));
16
+ controller.close();
17
+ },
18
+ });
19
+ return modernEventStreamResponse(body);
20
+ }
21
+
22
+ export function modernJsonRpcResponseStream(
23
+ result: Promise<Record<string, unknown>>,
24
+ options: { onCancel: () => void; onError: (error: unknown) => Record<string, unknown> },
25
+ ): Response {
26
+ const encoder = new TextEncoder();
27
+ let writable = true;
28
+ const body = new ReadableStream<Uint8Array>({
29
+ start(controller) {
30
+ controller.enqueue(encoder.encode(": connected\n\n"));
31
+ void result.then(
32
+ (message) => {
33
+ if (!writable) return;
34
+ try { controller.enqueue(encoder.encode(sseMessage(message))); } catch { writable = false; }
35
+ if (!writable) return;
36
+ writable = false;
37
+ try { controller.close(); } catch { /* Client already closed the response stream. */ }
38
+ },
39
+ (error) => {
40
+ if (!writable) return;
41
+ try { controller.enqueue(encoder.encode(sseMessage(options.onError(error)))); } catch { writable = false; }
42
+ if (!writable) return;
43
+ writable = false;
44
+ try { controller.close(); } catch { /* Client already closed the response stream. */ }
45
+ },
46
+ );
47
+ },
48
+ cancel() {
49
+ if (!writable) return;
50
+ writable = false;
51
+ options.onCancel();
52
+ },
53
+ });
54
+ return modernEventStreamResponse(body);
55
+ }
56
+
57
+ function modernEventStreamResponse(body: ReadableStream<Uint8Array>): Response {
58
+ return new Response(body, {
59
+ status: 200,
60
+ headers: {
61
+ "content-type": "text/event-stream; charset=utf-8",
62
+ "cache-control": "no-store, no-transform",
63
+ "x-accel-buffering": "no",
64
+ "x-content-type-options": "nosniff",
65
+ },
66
+ });
67
+ }
68
+
69
+ function sseMessage(message: unknown): string {
70
+ return `event: message\ndata: ${JSON.stringify(message)}\n\n`;
71
+ }
@@ -66,7 +66,16 @@ export async function resolveMcpSession(
66
66
  return { kind: "active", sessionId };
67
67
  }
68
68
 
69
- export function mcpClientRequestKey(tokenKey: string, sessionId: string, requestId: unknown): string | undefined {
70
- if (!sessionId || requestId === null || (typeof requestId !== "string" && typeof requestId !== "number")) return undefined;
71
- return `${tokenKey}:${sessionId}:${typeof requestId}:${String(requestId)}`;
69
+ export function legacyMcpClientRequestKey(tokenKey: string, sessionId: string, requestId: unknown): string | undefined {
70
+ if (!sessionId || !isRequestId(requestId)) return undefined;
71
+ return `${tokenKey}:legacy:${sessionId}:${typeof requestId}:${String(requestId)}`;
72
+ }
73
+
74
+ export function modernMcpStreamRequestKey(streamId: string): string | undefined {
75
+ if (!/^stream_[A-Za-z0-9_-]{43}$/.test(streamId)) return undefined;
76
+ return `modern:${streamId}`;
77
+ }
78
+
79
+ function isRequestId(value: unknown): value is string | number {
80
+ return typeof value === "string" || (typeof value === "number" && Number.isFinite(value));
72
81
  }
@@ -0,0 +1,67 @@
1
+ import { json } from "./http.ts";
2
+
3
+ export const MCP_STREAM_PROXY_MODE_HEADER = "x-machine-bridge-internal-mcp-stream-mode";
4
+ export const MCP_STREAM_PROXY_ID_HEADER = "x-machine-bridge-internal-mcp-stream-id";
5
+ export const MCP_STREAM_DESCRIPTOR_HEADER = "x-machine-bridge-mcp-stream-descriptor";
6
+ const STREAM_ID_PATTERN = /^stream_[A-Za-z0-9_-]{43}$/;
7
+
8
+ export type StreamProxyMode = "prepare" | "subscribe" | "modern-direct" | "modern-cancel" | "";
9
+ export type StreamDescriptorKind = "initial" | "resume" | "complete";
10
+ export type StreamDescriptor = { kind: StreamDescriptorKind; stream_id: string };
11
+ export type BridgeFetcher = { fetch(request: Request): Promise<Response> };
12
+ export type StreamExecutionContext = { waitUntil(promise: Promise<unknown>): void };
13
+
14
+ export function sanitizeBridgeRequest(request: Request): Request {
15
+ const headers = new Headers(request.headers);
16
+ headers.delete(MCP_STREAM_PROXY_MODE_HEADER);
17
+ headers.delete(MCP_STREAM_PROXY_ID_HEADER);
18
+ return new Request(request, { headers });
19
+ }
20
+
21
+ export function mcpStreamProxyMode(request: Pick<Request, "headers">): StreamProxyMode {
22
+ const value = request.headers.get(MCP_STREAM_PROXY_MODE_HEADER)?.trim().toLowerCase() ?? "";
23
+ return value === "prepare" || value === "subscribe" || value === "modern-direct" || value === "modern-cancel" ? value : "";
24
+ }
25
+
26
+ export function mcpStreamProxyId(request: Pick<Request, "headers">): string {
27
+ const value = request.headers.get(MCP_STREAM_PROXY_ID_HEADER)?.trim() ?? "";
28
+ return STREAM_ID_PATTERN.test(value) ? value : "";
29
+ }
30
+
31
+ export function mcpStreamDescriptorResponse(kind: StreamDescriptorKind, streamId: string): Response {
32
+ if (!STREAM_ID_PATTERN.test(streamId)) throw new Error("invalid MCP stream descriptor id");
33
+ return json({ kind, stream_id: streamId }, 200, { [MCP_STREAM_DESCRIPTOR_HEADER]: "1" });
34
+ }
35
+
36
+ export function withProxyHeaders(
37
+ request: Request,
38
+ mode: Exclude<StreamProxyMode, "">,
39
+ streamId = "",
40
+ signal?: AbortSignal,
41
+ ): Request {
42
+ const sanitized = sanitizeBridgeRequest(request);
43
+ const headers = new Headers(sanitized.headers);
44
+ headers.set(MCP_STREAM_PROXY_MODE_HEADER, mode);
45
+ if (streamId) headers.set(MCP_STREAM_PROXY_ID_HEADER, streamId);
46
+ if (mode === "subscribe") headers.set("Upgrade", "websocket");
47
+ const method = mode === "subscribe" ? "GET" : mode === "modern-cancel" ? "POST" : sanitized.method;
48
+ return new Request(sanitized, { method, headers, signal: signal ?? sanitized.signal });
49
+ }
50
+
51
+ export async function readDescriptor(response: Response): Promise<StreamDescriptor> {
52
+ const value: unknown = await response.json();
53
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("MCP stream descriptor is invalid");
54
+ const kind = (value as { kind?: unknown }).kind;
55
+ const streamId = (value as { stream_id?: unknown }).stream_id;
56
+ if ((kind !== "initial" && kind !== "resume" && kind !== "complete")
57
+ || typeof streamId !== "string" || !STREAM_ID_PATTERN.test(streamId)) {
58
+ throw new Error("MCP stream descriptor is invalid");
59
+ }
60
+ return { kind, stream_id: streamId };
61
+ }
62
+
63
+ export function stripInternalResponseHeaders(response: Response): Response {
64
+ const headers = new Headers(response.headers);
65
+ headers.delete(MCP_STREAM_DESCRIPTOR_HEADER);
66
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
67
+ }
@@ -1,18 +1,20 @@
1
+ import { MCP_MODERN_PROTOCOL_VERSION } from "../shared/mcp-protocol.mjs";
1
2
  import { applyCors, baseUrl, json } from "./http.ts";
2
3
  import { acceptsEventStream, resumeJsonRpcResponse, streamJsonRpcResponse } from "./mcp-stream.ts";
3
4
  import type { McpStreamChannel } from "./mcp-stream-channel.ts";
5
+ import { proxyModernMcpStream } from "./mcp-modern-proxy.ts";
4
6
  import { DEFAULT_SUBSCRIBE_RETRY_DELAYS_MS, subscribeTerminalMessage } from "./mcp-stream-subscription.ts";
7
+ import {
8
+ MCP_STREAM_DESCRIPTOR_HEADER, mcpStreamProxyId, mcpStreamProxyMode, readDescriptor,
9
+ stripInternalResponseHeaders, withProxyHeaders,
10
+ type BridgeFetcher, type StreamExecutionContext,
11
+ } from "./mcp-stream-proxy-contract.ts";
5
12
 
6
- export const MCP_STREAM_PROXY_MODE_HEADER = "x-machine-bridge-internal-mcp-stream-mode";
7
- export const MCP_STREAM_PROXY_ID_HEADER = "x-machine-bridge-internal-mcp-stream-id";
8
- const MCP_STREAM_DESCRIPTOR_HEADER = "x-machine-bridge-mcp-stream-descriptor";
9
- const STREAM_ID_PATTERN = /^stream_[A-Za-z0-9_-]{43}$/;
10
-
11
- type StreamProxyMode = "prepare" | "subscribe" | "";
12
- type StreamDescriptorKind = "initial" | "resume" | "complete";
13
- type StreamDescriptor = { kind: StreamDescriptorKind; stream_id: string };
14
- type BridgeFetcher = { fetch(request: Request): Promise<Response> };
15
- type StreamExecutionContext = { waitUntil(promise: Promise<unknown>): void };
13
+ export {
14
+ MCP_STREAM_PROXY_ID_HEADER, MCP_STREAM_PROXY_MODE_HEADER,
15
+ mcpStreamDescriptorResponse, mcpStreamProxyId, mcpStreamProxyMode, sanitizeBridgeRequest,
16
+ } from "./mcp-stream-proxy-contract.ts";
17
+ export type { StreamProxyMode } from "./mcp-stream-proxy-contract.ts";
16
18
 
17
19
  export async function proxyMcpEventStream(input: {
18
20
  request: Request;
@@ -22,10 +24,13 @@ export async function proxyMcpEventStream(input: {
22
24
  subscribeRetryDelaysMs?: readonly number[];
23
25
  }): Promise<Response | null> {
24
26
  const url = new URL(input.request.url);
27
+ if (url.pathname !== "/mcp") return null;
28
+ const protocolVersion = input.request.headers.get("MCP-Protocol-Version")?.trim() ?? "";
29
+ if (protocolVersion === MCP_MODERN_PROTOCOL_VERSION) return proxyModernMcpStream(input);
30
+
25
31
  const eligible = input.request.method === "GET"
26
32
  || (input.request.method === "POST" && acceptsEventStream(input.request));
27
- if (url.pathname !== "/mcp" || !eligible) return null;
28
-
33
+ if (!eligible) return null;
29
34
  const prepared = await input.bridge.fetch(withProxyHeaders(input.request, "prepare"));
30
35
  if (prepared.headers.get(MCP_STREAM_DESCRIPTOR_HEADER) !== "1") return stripInternalResponseHeaders(prepared);
31
36
  const descriptor = await readDescriptor(prepared);
@@ -64,51 +69,3 @@ export async function handleMcpStreamSubscribeRequest(
64
69
  if (!streamId) return json({ error: "invalid_internal_stream_id" }, 400);
65
70
  return await channel.subscribe(request, streamId, resumption);
66
71
  }
67
-
68
- export function sanitizeBridgeRequest(request: Request): Request {
69
- const headers = new Headers(request.headers);
70
- headers.delete(MCP_STREAM_PROXY_MODE_HEADER);
71
- headers.delete(MCP_STREAM_PROXY_ID_HEADER);
72
- return new Request(request, { headers });
73
- }
74
-
75
- export function mcpStreamProxyMode(request: Request): StreamProxyMode {
76
- const value = request.headers.get(MCP_STREAM_PROXY_MODE_HEADER)?.trim().toLowerCase() ?? "";
77
- return value === "prepare" || value === "subscribe" ? value : "";
78
- }
79
-
80
- export function mcpStreamProxyId(request: Request): string {
81
- const value = request.headers.get(MCP_STREAM_PROXY_ID_HEADER)?.trim() ?? "";
82
- return STREAM_ID_PATTERN.test(value) ? value : "";
83
- }
84
-
85
- export function mcpStreamDescriptorResponse(kind: StreamDescriptorKind, streamId: string): Response {
86
- if (!STREAM_ID_PATTERN.test(streamId)) throw new Error("invalid MCP stream descriptor id");
87
- return json({ kind, stream_id: streamId }, 200, { [MCP_STREAM_DESCRIPTOR_HEADER]: "1" });
88
- }
89
-
90
- function withProxyHeaders(request: Request, mode: Exclude<StreamProxyMode, "">, streamId = ""): Request {
91
- const sanitized = sanitizeBridgeRequest(request);
92
- const headers = new Headers(sanitized.headers);
93
- headers.set(MCP_STREAM_PROXY_MODE_HEADER, mode);
94
- if (streamId) headers.set(MCP_STREAM_PROXY_ID_HEADER, streamId);
95
- if (mode === "subscribe") headers.set("Upgrade", "websocket");
96
- return new Request(sanitized, { method: mode === "subscribe" ? "GET" : sanitized.method, headers });
97
- }
98
-
99
- async function readDescriptor(response: Response): Promise<StreamDescriptor> {
100
- const value: unknown = await response.json();
101
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("MCP stream descriptor is invalid");
102
- const kind = (value as { kind?: unknown }).kind;
103
- const streamId = (value as { stream_id?: unknown }).stream_id;
104
- if ((kind !== "initial" && kind !== "resume" && kind !== "complete") || typeof streamId !== "string" || !STREAM_ID_PATTERN.test(streamId)) {
105
- throw new Error("MCP stream descriptor is invalid");
106
- }
107
- return { kind, stream_id: streamId };
108
- }
109
-
110
- function stripInternalResponseHeaders(response: Response): Response {
111
- const headers = new Headers(response.headers);
112
- headers.delete(MCP_STREAM_DESCRIPTOR_HEADER);
113
- return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
114
- }
@@ -0,0 +1,23 @@
1
+ import { validateWorkerToolArguments } from "./tool-catalog.ts";
2
+ import { asObject } from "./mcp-jsonrpc.ts";
3
+
4
+ type ToolDefinition = Readonly<{ name?: unknown }>;
5
+ type ValidationIssue = Readonly<{ instancePath: string; keyword: string; message: string }>;
6
+
7
+ export type WorkerToolCallInput =
8
+ | Readonly<{ ok: true; name: string; args: Record<string, unknown> }>
9
+ | Readonly<{ ok: false; reason: "missing_name" | "unknown_tool" | "invalid_arguments"; issues?: readonly ValidationIssue[] }>;
10
+
11
+ export function inspectWorkerToolCall(paramsValue: unknown, advertisedTools: readonly ToolDefinition[]): WorkerToolCallInput {
12
+ const params = asObject(paramsValue);
13
+ const name = typeof params.name === "string" && params.name.trim() ? params.name.trim() : "";
14
+ if (!name) return Object.freeze({ ok: false, reason: "missing_name" });
15
+ if (!advertisedTools.some((tool) => tool.name === name)) return Object.freeze({ ok: false, reason: "unknown_tool" });
16
+ const rawArgs = params.arguments === undefined ? {} : params.arguments;
17
+ const validation = validateWorkerToolArguments(name, rawArgs);
18
+ if (!validation.known) return Object.freeze({ ok: false, reason: "unknown_tool" });
19
+ if (!validation.valid) {
20
+ return Object.freeze({ ok: false, reason: "invalid_arguments", issues: validation.issues });
21
+ }
22
+ return Object.freeze({ ok: true, name, args: rawArgs as Record<string, unknown> });
23
+ }
@@ -1,11 +1,39 @@
1
1
  import toolCatalog from "../shared/tool-catalog.json" with { type: "json" };
2
+ import { compileToolArgumentValidators } from "../shared/tool-argument-validation.mjs";
3
+ import { toolParameterHeaderNames } from "./mcp-http-contract.ts";
4
+ import {
5
+ isConfigurableForegroundTool,
6
+ remoteForegroundDefaultSeconds,
7
+ REMOTE_FOREGROUND_TIMEOUT_SECONDS,
8
+ } from "./tool-timeout.ts";
2
9
 
3
10
  export type WorkerToolDefinition = Record<string, unknown> & { name: string; availability?: string };
11
+ type JsonSchema = Record<string, unknown> & { properties: Record<string, Record<string, unknown>> };
4
12
 
5
13
  const allTools = toolCatalog as WorkerToolDefinition[];
6
14
 
7
15
  export const serverInfoTool = publicTool(allTools.find((tool) => tool.name === "server_info")!);
8
- export const workspaceTools = Object.freeze(allTools.filter((tool) => tool.name !== "server_info").map(publicTool));
16
+ export const workspaceTools = Object.freeze(allTools.filter((tool) => tool.name !== "server_info").map(remotePublicTool));
17
+
18
+ const publicTools = [serverInfoTool, ...workspaceTools];
19
+ const workerToolArguments = compileToolArgumentValidators(publicTools);
20
+ export const workerToolParameterHeaders = toolParameterHeaderNames(publicTools);
21
+
22
+ export function validateWorkerToolArguments(name: unknown, value: unknown) {
23
+ return workerToolArguments.validate(name, value);
24
+ }
25
+
26
+ function remotePublicTool(tool: WorkerToolDefinition): WorkerToolDefinition {
27
+ const definition = publicTool(tool);
28
+ if (!isConfigurableForegroundTool(definition.name)) return definition;
29
+
30
+ const schema = definition.inputSchema as JsonSchema;
31
+ const timeout = schema.properties.timeout_seconds;
32
+ timeout.maximum = REMOTE_FOREGROUND_TIMEOUT_SECONDS;
33
+ timeout.default = remoteForegroundDefaultSeconds(definition.name);
34
+ definition.description = `${String(definition.description)} Remote foreground execution is limited to ${REMOTE_FOREGROUND_TIMEOUT_SECONDS} seconds; use process sessions or managed jobs for longer work.`;
35
+ return definition;
36
+ }
9
37
 
10
38
  function publicTool(tool: WorkerToolDefinition): WorkerToolDefinition {
11
39
  const { availability: _availability, ...definition } = tool;
@@ -1,21 +1,62 @@
1
1
  import relayContract from "../shared/relay-contract.json" with { type: "json" };
2
+ import { WorkerToolError } from "./errors.ts";
3
+
4
+ const CONFIGURABLE_FOREGROUND_TOOLS = new Set([
5
+ "exec_command", "run_process", "run_local_command", "open_local_application",
6
+ "inspect_local_application", "operate_local_application", "browser_list_tabs",
7
+ "browser_manage_tabs", "browser_wait", "browser_get_source", "browser_inspect_page",
8
+ "browser_action", "browser_fill_form", "browser_screenshot", "browser_upload_files",
9
+ ]);
10
+
11
+ export const REMOTE_FOREGROUND_TIMEOUT_SECONDS = Math.floor(
12
+ relayContract.maximumInteractiveExecutionTimeoutMs / 1000,
13
+ );
14
+ const THIRTY_SECOND_FOREGROUND_TOOLS = new Set([
15
+ "open_local_application", "inspect_local_application", "operate_local_application",
16
+ "browser_list_tabs", "browser_manage_tabs", "browser_wait", "browser_get_source",
17
+ "browser_inspect_page", "browser_action", "browser_screenshot",
18
+ ]);
19
+
20
+ export function isConfigurableForegroundTool(name: string): boolean {
21
+ return CONFIGURABLE_FOREGROUND_TOOLS.has(name);
22
+ }
23
+
24
+ export function remoteForegroundDefaultSeconds(name: string): number {
25
+ return THIRTY_SECOND_FOREGROUND_TOOLS.has(name) ? 30 : 60;
26
+ }
2
27
 
3
28
  export function daemonToolTimeoutMs(name: string, args: Record<string, unknown>): number {
4
29
  if (name === "session_bootstrap") return 10_000;
5
- const configurable = new Set([
6
- "exec_command", "run_process", "run_local_command", "open_local_application",
7
- "inspect_local_application", "operate_local_application", "browser_list_tabs",
8
- "browser_manage_tabs", "browser_wait", "browser_get_source", "browser_inspect_page",
9
- "browser_action", "browser_fill_form", "browser_screenshot", "browser_upload_files",
10
- ]);
11
- if (!configurable.has(name)) return 60_000;
12
- const seconds = clampNumber(args.timeout_seconds, name === "browser_fill_form" ? 60 : 120, 1, 600);
30
+ if (!isConfigurableForegroundTool(name)) return 60_000;
31
+
32
+ const seconds = remoteForegroundSeconds(name, args.timeout_seconds);
13
33
  const executionMs = Math.min(seconds * 1000, relayContract.maximumExecutionTimeoutMs);
14
34
  return Math.min(executionMs + relayContract.toolCallOverheadMs, relayContract.maximumRelayToolTimeoutMs);
15
35
  }
16
36
 
17
- function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
18
- const number = typeof value === "number" && Number.isFinite(value) ? value : Number.parseInt(String(value ?? ""), 10);
19
- const safe = Number.isFinite(number) ? number : fallback;
20
- return Math.min(Math.max(Math.floor(safe), min), max);
37
+ function remoteForegroundSeconds(name: string, value: unknown): number {
38
+ if (value === undefined) return remoteForegroundDefaultSeconds(name);
39
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
40
+ throw foregroundTimeoutError("remote foreground timeout must be an integer of at least 1 second");
41
+ }
42
+ if (value > REMOTE_FOREGROUND_TIMEOUT_SECONDS) {
43
+ throw foregroundTimeoutError(
44
+ `remote foreground timeout exceeds ${REMOTE_FOREGROUND_TIMEOUT_SECONDS} seconds; split mutations from verification and use start_process/read_process or start_job/read_job for longer work`,
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function foregroundTimeoutError(message: string): WorkerToolError {
51
+ return new WorkerToolError(
52
+ "invalid_request",
53
+ message,
54
+ false,
55
+ {
56
+ side_effects_started: false,
57
+ minimum_foreground_timeout_seconds: 1,
58
+ maximum_foreground_timeout_seconds: REMOTE_FOREGROUND_TIMEOUT_SECONDS,
59
+ recommended_tools: ["start_process", "read_process", "start_job", "read_job"],
60
+ },
61
+ );
21
62
  }
@@ -0,0 +1,23 @@
1
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
+ import { serverImplementation } from "../shared/mcp-protocol.mjs";
3
+
4
+ export const SERVER_NAME = String(serverMetadata.name);
5
+ export const MCP_MODERN_PROTOCOL_VERSIONS = Object.freeze(
6
+ serverMetadata.modernProtocolVersions.map((value) => String(value)),
7
+ );
8
+ export const MCP_LEGACY_PROTOCOL_VERSIONS = Object.freeze(
9
+ serverMetadata.legacyProtocolVersions.map((value) => String(value)),
10
+ );
11
+ export const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
12
+ export const MCP_SERVER_CAPABILITIES = Object.freeze({ tools: Object.freeze({ listChanged: false }) });
13
+ export const MCP_DISCOVERY_TTL_MS = 300_000;
14
+ export const MCP_TOOL_LIST_TTL_MS = 300_000;
15
+
16
+ export function mcpServerInfo(version: string): Readonly<Record<string, unknown>> {
17
+ return Object.freeze(serverImplementation({
18
+ name: SERVER_NAME,
19
+ title: "Machine Bridge MCP",
20
+ version,
21
+ description: "Workspace-scoped local coding tools over authenticated remote relay.",
22
+ }));
23
+ }
@@ -8,7 +8,16 @@ export function mcpMetadata(base: string, identity: WorkerIdentity): Record<stri
8
8
  version: identity.version,
9
9
  protocolVersion: String(serverMetadata.protocolVersion),
10
10
  protocolVersions: serverMetadata.supportedProtocolVersions.map((value) => String(value)),
11
- transport: { type: "streamable-http", url: `${base}/mcp` },
11
+ protocolEras: {
12
+ modern: serverMetadata.modernProtocolVersions.map((value) => String(value)),
13
+ legacy: serverMetadata.legacyProtocolVersions.map((value) => String(value)),
14
+ },
15
+ transport: {
16
+ type: "streamable-http",
17
+ url: `${base}/mcp`,
18
+ modern: { methods: ["POST"], protocolSessions: false, resumableSse: false },
19
+ legacy: { methods: ["GET", "POST"], protocolSessions: true, resumableSse: true },
20
+ },
12
21
  auth: { type: "oauth", authorization_servers: [base] },
13
22
  };
14
23
  }
@@ -0,0 +1,19 @@
1
+ import type { OAuthControllerEnv } from "./oauth-controller.ts";
2
+
3
+ const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
4
+ const MAX_BODY_BYTES = 16 * 1024 * 1024;
5
+
6
+ export interface BridgeEnv extends OAuthControllerEnv {
7
+ BRIDGE: DurableObjectNamespace;
8
+ DAEMON_DEVICE_PUBLIC_KEY: string;
9
+ OAUTH_TOKEN_VERSION: string;
10
+ MBM_WORKER_MAX_BODY_BYTES?: string;
11
+ MBM_ALLOWED_ORIGINS?: string;
12
+ STATEFUL_RATE_LIMITER: RateLimit;
13
+ }
14
+
15
+ export function workerBodyLimitBytes(configured: string | undefined): number {
16
+ const parsed = Number.parseInt(configured ?? "", 10);
17
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_BODY_BYTES;
18
+ return Math.min(parsed, MAX_BODY_BYTES);
19
+ }
@@ -1,4 +1,5 @@
1
- import { applyCors, baseUrl, corsPreflight, json, methodNotAllowed } from "./http.ts";
1
+ import { applyCors, baseUrl, corsPreflight, json, mcpOriginRejection, methodNotAllowed } from "./http.ts";
2
+ import { workerToolParameterHeaders } from "./tool-catalog.ts";
2
3
  import {
3
4
  authorizationServerMetadata, mcpMetadata, protectedResourceMetadata, type WorkerIdentity,
4
5
  } from "./worker-metadata.ts";
@@ -25,7 +26,11 @@ export function respondWithoutDurableObject(request: Request, identity: WorkerId
25
26
  const url = new URL(request.url);
26
27
  const base = baseUrl(request);
27
28
  const path = url.pathname;
28
- if (request.method === "OPTIONS" && request.headers.has("Origin")) return corsPreflight(request, base, extraOrigins);
29
+ if (request.method === "OPTIONS" && request.headers.has("Origin")) return corsPreflight(request, base, extraOrigins, workerToolParameterHeaders);
30
+ if (path === "/mcp") {
31
+ const originRejection = mcpOriginRejection(request, base, extraOrigins);
32
+ if (originRejection) return originRejection;
33
+ }
29
34
  const statefulMethods = STATEFUL_METHODS.get(path);
30
35
  if (statefulMethods) {
31
36
  const allowed = new Set(statefulMethods.split(",").map((method) => method.trim()));
@@ -28,6 +28,12 @@
28
28
  "src/local/agent-context-projection.mjs",
29
29
  "src/local/agent-skill-discovery.mjs",
30
30
  "src/local/agent-text-file.mjs",
31
- "src/local/managed-job-projection.mjs"
31
+ "src/local/managed-job-projection.mjs",
32
+ "src/local/child-process-settlement.mjs",
33
+ "src/local/system-network-route.mjs",
34
+ "src/local/process-tree-ownership.mjs",
35
+ "src/local/service-owner.mjs",
36
+ "src/local/service-runtime.mjs",
37
+ "src/local/windows-service-convergence.mjs"
32
38
  ]
33
39
  }