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,276 @@
1
+ import {
2
+ MCP_HEADER_MISMATCH,
3
+ MCP_MODERN_PROTOCOL_VERSIONS,
4
+ McpProtocolError,
5
+ requestProtocolVersion,
6
+ validateModernRequestMetadata,
7
+ } from "../shared/mcp-protocol.mjs";
8
+ import type { JsonRpcRequest } from "./mcp-jsonrpc.ts";
9
+
10
+ const HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
11
+ const BASE64_SENTINEL = /^=\?base64\?([A-Za-z0-9+/]*={0,2})\?=$/;
12
+ const MIRRORED_NAME_METHODS = new Map([
13
+ ["tools/call", "name"],
14
+ ["prompts/get", "name"],
15
+ ["resources/read", "uri"],
16
+ ]);
17
+
18
+ type JsonObject = Record<string, unknown>;
19
+ type ToolDefinition = { name: string; inputSchema?: unknown };
20
+ type HeaderBinding = { headerName: string; path: string[]; type: "string" | "integer" | "boolean" };
21
+
22
+ export type McpRequestEra = "modern" | "legacy";
23
+
24
+ export type ModernHttpRequestContext = {
25
+ era: "modern";
26
+ version: string;
27
+ clientCapabilities: JsonObject;
28
+ clientInfo: JsonObject | null;
29
+ progressToken: unknown;
30
+ logLevel: unknown;
31
+ };
32
+
33
+ export class McpHttpContractError extends Error {
34
+ readonly status: number;
35
+ readonly code: number;
36
+ readonly data?: unknown;
37
+ constructor(code: number, message: string, data?: unknown, status = 400) {
38
+ super(message);
39
+ this.name = "McpHttpContractError";
40
+ this.status = status;
41
+ this.code = code;
42
+ this.data = data;
43
+ }
44
+ }
45
+
46
+ export function detectHttpMcpEra(request: Pick<Request, "headers">, body: JsonRpcRequest): McpRequestEra {
47
+ const bodyVersion = requestProtocolVersion(body);
48
+ const headerVersion = request.headers.get("mcp-protocol-version")?.trim() ?? "";
49
+ const legacySession = request.headers.get("mcp-session-id")?.trim() ?? "";
50
+ if (body.method === "server/discover" || bodyVersion || headerVersion === MCP_MODERN_PROTOCOL_VERSIONS[0]) return "modern";
51
+ if (body.method === "initialize" || legacySession) return "legacy";
52
+ return "legacy";
53
+ }
54
+
55
+ export function validateModernHttpRequest(input: {
56
+ request: Pick<Request, "headers">;
57
+ body: JsonRpcRequest;
58
+ tools?: readonly ToolDefinition[];
59
+ supportedVersions?: readonly string[];
60
+ }): ModernHttpRequestContext {
61
+ const supportedVersions = input.supportedVersions ?? MCP_MODERN_PROTOCOL_VERSIONS;
62
+ validateAcceptHeader(input.request.headers);
63
+ const bodyVersion = requestProtocolVersion(input.body);
64
+ if (!bodyVersion) {
65
+ try { validateModernRequestMetadata(input.body, supportedVersions); }
66
+ catch (error) {
67
+ if (error instanceof McpProtocolError) throw new McpHttpContractError(error.code, error.message, error.data);
68
+ throw error;
69
+ }
70
+ }
71
+ const headerVersion = requiredHeader(input.request.headers, "MCP-Protocol-Version");
72
+ if (headerVersion !== bodyVersion) {
73
+ throw headerMismatch("MCP-Protocol-Version does not match request metadata");
74
+ }
75
+ const method = requiredHeader(input.request.headers, "Mcp-Method");
76
+ if (method !== input.body.method) {
77
+ throw headerMismatch("Mcp-Method does not match the JSON-RPC method");
78
+ }
79
+
80
+ const params = asObject(input.body.params);
81
+ const nameField = MIRRORED_NAME_METHODS.get(input.body.method);
82
+ if (nameField) validateMirroredName(input.request.headers, params, nameField);
83
+ if (input.body.method === "tools/call") {
84
+ validateToolParameterHeaders(input.request.headers, params, input.tools ?? []);
85
+ }
86
+
87
+ let metadata;
88
+ try { metadata = validateModernRequestMetadata(input.body, supportedVersions); }
89
+ catch (error) {
90
+ if (error instanceof McpProtocolError) throw new McpHttpContractError(error.code, error.message, error.data);
91
+ throw error;
92
+ }
93
+ return { era: "modern", ...metadata };
94
+ }
95
+
96
+ export function validateToolHeaderSchemas(tools: readonly ToolDefinition[]): void {
97
+ toolParameterHeaderNames(tools);
98
+ }
99
+
100
+ export function toolParameterHeaderNames(tools: readonly ToolDefinition[]): ReadonlySet<string> {
101
+ const names = new Set<string>();
102
+ for (const tool of tools) {
103
+ for (const binding of collectHeaderBindings(tool.inputSchema, tool.name)) names.add(binding.headerName.toLowerCase());
104
+ }
105
+ return names;
106
+ }
107
+
108
+ export function decodeMcpHeaderValue(value: string): string {
109
+ const match = value.match(BASE64_SENTINEL);
110
+ if (!match) {
111
+ if (!isSafePlainHeaderValue(value)) throw headerMismatch("Mirrored MCP header contains an invalid plain value");
112
+ return value;
113
+ }
114
+ const encoded = match[1];
115
+ if (encoded.length % 4 !== 0) throw headerMismatch("Mirrored MCP header contains invalid Base64");
116
+ let bytes: Uint8Array;
117
+ try {
118
+ const binary = atob(encoded);
119
+ bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
120
+ } catch {
121
+ throw headerMismatch("Mirrored MCP header contains invalid Base64");
122
+ }
123
+ const canonical = btoa(String.fromCharCode(...bytes));
124
+ if (canonical !== encoded) throw headerMismatch("Mirrored MCP header contains non-canonical Base64");
125
+ try {
126
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
127
+ } catch {
128
+ throw headerMismatch("Mirrored MCP header contains invalid UTF-8");
129
+ }
130
+ }
131
+
132
+ function validateAcceptHeader(headers: Headers): void {
133
+ const value = headers.get("Accept") ?? "";
134
+ const accepted = new Set<string>();
135
+ for (const entry of value.split(",")) {
136
+ const [rawType, ...parameters] = entry.split(";");
137
+ const mediaType = rawType.trim().toLowerCase();
138
+ if (!mediaType) continue;
139
+ const qualities = parameters
140
+ .map((parameter) => parameter.trim().toLowerCase())
141
+ .filter((parameter) => parameter.startsWith("q="));
142
+ if (qualities.length > 1) continue;
143
+ if (qualities.length === 1 && !positiveHttpQuality(qualities[0].slice(2))) continue;
144
+ accepted.add(mediaType);
145
+ }
146
+ if (!accepted.has("application/json") || !accepted.has("text/event-stream")) {
147
+ throw new McpHttpContractError(-32600, "Accept header must include application/json and text/event-stream");
148
+ }
149
+ }
150
+
151
+ function positiveHttpQuality(value: string): boolean {
152
+ if (!/^(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)$/.test(value)) return false;
153
+ return Number(value) > 0;
154
+ }
155
+
156
+ function validateMirroredName(headers: Headers, params: JsonObject, field: string): void {
157
+ const bodyValue = params[field];
158
+ if (typeof bodyValue !== "string") throw new McpHttpContractError(-32602, `${field} must be a string`);
159
+ const headerValue = decodeMcpHeaderValue(requiredHeader(headers, "Mcp-Name"));
160
+ if (headerValue !== bodyValue) {
161
+ throw headerMismatch(`Mcp-Name does not match params.${field}`);
162
+ }
163
+ }
164
+
165
+ function validateToolParameterHeaders(headers: Headers, params: JsonObject, tools: readonly ToolDefinition[]): void {
166
+ const toolName = params.name;
167
+ if (typeof toolName !== "string") return;
168
+ const tool = tools.find((candidate) => candidate.name === toolName);
169
+ if (!tool) return;
170
+ const bindings = collectHeaderBindings(tool.inputSchema, tool.name);
171
+ const argumentsValue = asObject(params.arguments);
172
+ for (const binding of bindings) {
173
+ const bodyValue = valueAtPath(argumentsValue, binding.path);
174
+ const headerValue = headers.get(binding.headerName);
175
+ if (bodyValue === undefined || bodyValue === null) {
176
+ if (headerValue !== null) throw headerMismatch(`${binding.headerName} is present without a body value`);
177
+ continue;
178
+ }
179
+ if (headerValue === null) throw headerMismatch(`${binding.headerName} is required for this tool call`);
180
+ if (!mirroredValueEquals(decodeMcpHeaderValue(headerValue), bodyValue, binding.type)) {
181
+ throw headerMismatch(`${binding.headerName} does not match the tool argument`);
182
+ }
183
+ }
184
+ }
185
+
186
+ function collectHeaderBindings(schema: unknown, toolName: string): HeaderBinding[] {
187
+ const bindings: HeaderBinding[] = [];
188
+ const names = new Set<string>();
189
+ visitSchema(schema, [], true, toolName, bindings, names);
190
+ return bindings;
191
+ }
192
+
193
+ function visitSchema(
194
+ value: unknown,
195
+ path: string[],
196
+ reachable: boolean,
197
+ toolName: string,
198
+ bindings: HeaderBinding[],
199
+ names: Set<string>,
200
+ ): void {
201
+ if (!isObject(value)) return;
202
+ const annotation = value["x-mcp-header"];
203
+ if (annotation !== undefined) {
204
+ if (!reachable) throw new Error(`tool ${toolName} has x-mcp-header outside a properties-only path`);
205
+ if (typeof annotation !== "string" || !annotation || !HEADER_TOKEN.test(annotation)) {
206
+ throw new Error(`tool ${toolName} has an invalid x-mcp-header name`);
207
+ }
208
+ const type = value.type;
209
+ if (type !== "string" && type !== "integer" && type !== "boolean") {
210
+ throw new Error(`tool ${toolName} uses x-mcp-header on a non-primitive property`);
211
+ }
212
+ const normalized = annotation.toLowerCase();
213
+ if (names.has(normalized)) throw new Error(`tool ${toolName} has duplicate x-mcp-header names`);
214
+ names.add(normalized);
215
+ bindings.push({ headerName: `Mcp-Param-${annotation}`, path, type });
216
+ }
217
+
218
+ const properties = isObject(value.properties) ? value.properties : null;
219
+ if (properties) {
220
+ for (const [key, child] of Object.entries(properties)) {
221
+ visitSchema(child, [...path, key], reachable, toolName, bindings, names);
222
+ }
223
+ }
224
+ for (const [key, child] of Object.entries(value)) {
225
+ if (key === "properties" || key === "x-mcp-header") continue;
226
+ if (Array.isArray(child)) {
227
+ for (const item of child) visitSchema(item, path, false, toolName, bindings, names);
228
+ } else if (isObject(child)) {
229
+ visitSchema(child, path, false, toolName, bindings, names);
230
+ }
231
+ }
232
+ }
233
+
234
+ function mirroredValueEquals(headerValue: string, bodyValue: unknown, type: HeaderBinding["type"]): boolean {
235
+ if (type === "string") return typeof bodyValue === "string" && headerValue === bodyValue;
236
+ if (type === "boolean") return typeof bodyValue === "boolean" && headerValue === String(bodyValue);
237
+ if (typeof bodyValue !== "number" || !Number.isSafeInteger(bodyValue)) return false;
238
+ const parsed = Number(headerValue);
239
+ return Number.isSafeInteger(parsed) && parsed === bodyValue;
240
+ }
241
+
242
+ function valueAtPath(value: JsonObject, path: readonly string[]): unknown {
243
+ let current: unknown = value;
244
+ for (const segment of path) {
245
+ if (!isObject(current) || !Object.hasOwn(current, segment)) return undefined;
246
+ current = current[segment];
247
+ }
248
+ return current;
249
+ }
250
+
251
+ function requiredHeader(headers: Headers, name: string): string {
252
+ const value = headers.get(name);
253
+ if (value === null || !value) throw headerMismatch(`${name} header is required`);
254
+ return value;
255
+ }
256
+
257
+ function headerMismatch(message: string, data?: unknown): McpHttpContractError {
258
+ return new McpHttpContractError(MCP_HEADER_MISMATCH, `Header mismatch: ${message}`, data);
259
+ }
260
+
261
+ function isSafePlainHeaderValue(value: string): boolean {
262
+ if (value !== value.trim() || BASE64_SENTINEL.test(value)) return false;
263
+ for (const character of value) {
264
+ const code = character.charCodeAt(0);
265
+ if (code !== 9 && (code < 32 || code > 126)) return false;
266
+ }
267
+ return true;
268
+ }
269
+
270
+ function asObject(value: unknown): JsonObject {
271
+ return isObject(value) ? value : {};
272
+ }
273
+
274
+ function isObject(value: unknown): value is JsonObject {
275
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
276
+ }
@@ -1,4 +1,5 @@
1
1
  import { projectMcpResult } from "../shared/result-projection.mjs";
2
+ import { MCP_LEGACY_PROTOCOL_VERSION, resultForProtocol } from "../shared/mcp-protocol.mjs";
2
3
  const JSONRPC_VERSION = "2.0";
3
4
  const MAX_SESSION_INSTRUCTION_BYTES = 3 * 1024 * 1024;
4
5
 
@@ -37,16 +38,21 @@ export function rpcError(id: JsonRpcId | undefined, code: number, message: strin
37
38
  return { jsonrpc: JSONRPC_VERSION, id: id ?? null, error };
38
39
  }
39
40
 
40
- export function textToolResult(value: unknown, isError = false): Record<string, unknown> {
41
+ export function textToolResult(
42
+ value: unknown,
43
+ isError = false,
44
+ protocolVersion: string = MCP_LEGACY_PROTOCOL_VERSION,
45
+ serverInfo?: Record<string, unknown>,
46
+ ): Record<string, unknown> {
41
47
  const special = asObject(value).$mcp;
42
48
  if (special && typeof special === "object" && !Array.isArray(special)) {
43
49
  const specialObject = special as Record<string, unknown>;
44
50
  if (Array.isArray(specialObject.content)) {
45
51
  const result: Record<string, unknown> = { content: specialObject.content, isError };
46
- if (specialObject.structuredContent && typeof specialObject.structuredContent === "object" && !Array.isArray(specialObject.structuredContent)) {
47
- result.structuredContent = specialObject.structuredContent;
52
+ if (Object.prototype.hasOwnProperty.call(specialObject, "structuredContent")) {
53
+ result.structuredContent = structuredClone(specialObject.structuredContent);
48
54
  }
49
- return result;
55
+ return resultForProtocol(protocolVersion, result, { serverInfo });
50
56
  }
51
57
  }
52
58
  const projection = projectMcpResult(value);
@@ -54,8 +60,8 @@ export function textToolResult(value: unknown, isError = false): Record<string,
54
60
  content: [{ type: "text", text: projection.text }],
55
61
  isError,
56
62
  };
57
- if (projection.structuredContent) result.structuredContent = projection.structuredContent;
58
- return result;
63
+ if (projection.hasStructuredContent) result.structuredContent = structuredClone(projection.structuredContent);
64
+ return resultForProtocol(protocolVersion, result, { serverInfo });
59
65
  }
60
66
 
61
67
  export function asObject(value: unknown): Record<string, unknown> {
@@ -0,0 +1,104 @@
1
+ import type { AuthorizedToken } from "./oauth-controller.ts";
2
+ import { publicWorkerToolError } from "./errors.ts";
3
+ import { legacyMcpClientRequestKey } from "./mcp-session.ts";
4
+ import { inspectWorkerToolCall } from "./mcp-tool-call-input.ts";
5
+ import {
6
+ asObject, rpcError, rpcResult, sessionInstructionText, textToolResult,
7
+ type JsonRpcRequest,
8
+ } from "./mcp-jsonrpc.ts";
9
+
10
+ type LegacyMcpConfig = Readonly<{
11
+ defaultVersion: string;
12
+ supportedVersions: readonly string[];
13
+ serverInfo: Record<string, unknown>;
14
+ instructions: string;
15
+ daemonToolEnabled: (name: string) => boolean;
16
+ callDaemonTool: (name: string, args: Record<string, unknown>, authorized: AuthorizedToken) => Promise<unknown>;
17
+ recordError: (code: string) => void;
18
+ cancelClientRequest: (requestKey?: string) => Promise<void>;
19
+ tools: (authorized: AuthorizedToken) => Array<Record<string, unknown>>;
20
+ callTool: (input: {
21
+ name: string;
22
+ args: Record<string, unknown>;
23
+ base: string;
24
+ authorized: AuthorizedToken;
25
+ requestKey?: string;
26
+ }) => Promise<unknown>;
27
+ }>;
28
+
29
+ export class LegacyMcpDispatcher {
30
+ private readonly config: LegacyMcpConfig;
31
+
32
+ constructor(config: LegacyMcpConfig) {
33
+ this.config = config;
34
+ }
35
+
36
+ async dispatch(
37
+ request: JsonRpcRequest,
38
+ base: string,
39
+ authorized: AuthorizedToken,
40
+ sessionId: string,
41
+ ): Promise<Record<string, unknown> | null> {
42
+ if (request.method === "initialize") return this.initialize(request, authorized);
43
+ if (request.method === "notifications/initialized") return null;
44
+ if (request.method === "notifications/cancelled") {
45
+ await this.config.cancelClientRequest(legacyMcpClientRequestKey(
46
+ authorized.tokenKey, sessionId, asObject(request.params).requestId,
47
+ ));
48
+ return null;
49
+ }
50
+ if (request.method === "logging/setLevel" || request.method === "ping") return rpcResult(request.id, {});
51
+ if (request.method === "tools/list") return rpcResult(request.id, { tools: this.config.tools(authorized) });
52
+ if (request.method === "tools/call") return this.callTool(request, base, authorized, sessionId);
53
+ return rpcError(request.id, -32601, "Method not found");
54
+ }
55
+
56
+ private async initialize(request: JsonRpcRequest, authorized: AuthorizedToken): Promise<Record<string, unknown> | null> {
57
+ const requested = asObject(request.params).protocolVersion;
58
+ const protocolVersion = typeof requested === "string" && this.config.supportedVersions.includes(requested)
59
+ ? requested
60
+ : this.config.defaultVersion;
61
+ let bootstrap = null;
62
+ if (this.config.daemonToolEnabled("session_bootstrap")) {
63
+ try { bootstrap = await this.config.callDaemonTool("session_bootstrap", { path: "." }, authorized); }
64
+ catch { this.config.recordError("session_bootstrap_failed"); }
65
+ }
66
+ const localInstructions = sessionInstructionText(bootstrap);
67
+ return rpcResult(request.id, {
68
+ protocolVersion,
69
+ capabilities: { tools: { listChanged: false }, logging: {} },
70
+ serverInfo: this.config.serverInfo,
71
+ instructions: localInstructions
72
+ ? `${this.config.instructions}\n\n--- LOCAL SESSION INSTRUCTIONS ---\n${localInstructions}`
73
+ : this.config.instructions,
74
+ });
75
+ }
76
+
77
+ private async callTool(
78
+ request: JsonRpcRequest,
79
+ base: string,
80
+ authorized: AuthorizedToken,
81
+ sessionId: string,
82
+ ): Promise<Record<string, unknown>> {
83
+ if (request.id === undefined || request.id === null) {
84
+ return rpcError(null, -32600, "tools/call requires a non-null request id");
85
+ }
86
+ const inspected = inspectWorkerToolCall(request.params, this.config.tools(authorized));
87
+ if (!inspected.ok) {
88
+ const message = inspected.reason === "missing_name" ? "tools/call requires a tool name"
89
+ : inspected.reason === "unknown_tool" ? "Unknown tool"
90
+ : "Tool arguments do not match the input schema";
91
+ return rpcError(request.id, -32602, message, inspected.issues ? { side_effects_started: false, validation_issues: [...inspected.issues] } : undefined);
92
+ }
93
+ const { name, args } = inspected;
94
+ try {
95
+ const result = await this.config.callTool({
96
+ name, args, base, authorized,
97
+ requestKey: legacyMcpClientRequestKey(authorized.tokenKey, sessionId, request.id),
98
+ });
99
+ return rpcResult(request.id, textToolResult(result))!;
100
+ } catch (error) {
101
+ return rpcResult(request.id, textToolResult({ error: publicWorkerToolError(error) }, true))!;
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,199 @@
1
+ import type { AuthorizedToken } from "./oauth-controller.ts";
2
+ import { publicWorkerToolError } from "./errors.ts";
3
+ import { inspectWorkerToolCall } from "./mcp-tool-call-input.ts";
4
+ import { acceptsEventStream } from "./mcp-stream.ts";
5
+ import { modernMcpStreamRequestKey } from "./mcp-session.ts";
6
+ import { mcpStreamProxyId, type StreamProxyMode } from "./mcp-stream-proxy.ts";
7
+ import { modernJsonRpcResponseStream, modernSubscriptionResponse } from "./mcp-modern-stream.ts";
8
+ import { json } from "./http.ts";
9
+ import {
10
+ acceptedSubscriptionFilter, validateSubscriptionFilter,
11
+ } from "../shared/mcp-subscriptions.mjs";
12
+ import {
13
+ McpProtocolError, modernCacheableResult, modernDiscoverResult,
14
+ } from "../shared/mcp-protocol.mjs";
15
+ import {
16
+ asObject, rpcError, rpcResult, textToolResult, type JsonRpcRequest,
17
+ } from "./mcp-jsonrpc.ts";
18
+
19
+ type ModernMcpConfig = Readonly<{
20
+ capabilities: Record<string, unknown>;
21
+ serverInfo: Record<string, unknown>;
22
+ instructions: string;
23
+ supportedVersions: readonly string[];
24
+ discoveryTtlMs: number;
25
+ toolListTtlMs: number;
26
+ tools: (authorized: AuthorizedToken) => Array<Record<string, unknown>>;
27
+ recordError: (code: string) => void;
28
+ cancelClientRequest: (requestKey?: string) => Promise<void>;
29
+ callTool: (input: {
30
+ name: string;
31
+ args: Record<string, unknown>;
32
+ base: string;
33
+ authorized: AuthorizedToken;
34
+ signal: AbortSignal;
35
+ requestKey?: string;
36
+ }) => Promise<unknown>;
37
+ }>;
38
+
39
+ type ModernDispatchResult = Readonly<{
40
+ message: Record<string, unknown>;
41
+ status: number;
42
+ }>;
43
+
44
+ export class ModernMcpController {
45
+ private readonly config: ModernMcpConfig;
46
+
47
+ constructor(config: ModernMcpConfig) {
48
+ this.config = config;
49
+ }
50
+
51
+ async handleControl(request: Request, mode: StreamProxyMode): Promise<Response | null> {
52
+ if (mode !== "modern-cancel") return null;
53
+ const requestKey = modernMcpStreamRequestKey(mcpStreamProxyId(request));
54
+ await this.config.cancelClientRequest(requestKey);
55
+ return new Response(null, { status: requestKey ? 202 : 400 });
56
+ }
57
+
58
+ async handleRequest(input: {
59
+ request: Request;
60
+ body: JsonRpcRequest;
61
+ base: string;
62
+ authorized: AuthorizedToken;
63
+ protocolVersion: string;
64
+ proxyMode: StreamProxyMode;
65
+ }): Promise<Response> {
66
+ const { request, body } = input;
67
+ if (body.id === undefined) return json(rpcError(undefined, -32601, "Method not found"), 404);
68
+ if (body.id === null) return json(rpcError(null, -32600, "Modern MCP requests require a non-null request id"), 400);
69
+ const requestKey = input.proxyMode === "modern-direct"
70
+ ? modernMcpStreamRequestKey(mcpStreamProxyId(request))
71
+ : undefined;
72
+ if (input.proxyMode === "modern-direct" && !requestKey) {
73
+ return json(rpcError(body.id, -32600, "Invalid modern stream identity"), 400);
74
+ }
75
+ if (body.method === "subscriptions/listen") return this.subscriptionResponse(input);
76
+ if (body.method === "tools/call" && acceptsEventStream(request)) return this.streamedToolResponse({ ...input, requestKey });
77
+ const outcome = await this.dispatch(input.body, input.base, input.authorized, input.protocolVersion, request.signal, requestKey);
78
+ return json(outcome.message, outcome.status);
79
+ }
80
+
81
+ private subscriptionResponse(input: {
82
+ request: Request;
83
+ body: JsonRpcRequest;
84
+ }): Response {
85
+ const { request, body } = input;
86
+ if (!acceptsEventStream(request)) {
87
+ return json(rpcError(body.id, -32602, "subscriptions/listen requires text/event-stream support"), 406);
88
+ }
89
+ try {
90
+ const requested = validateSubscriptionFilter(asObject(body.params).notifications);
91
+ const accepted = acceptedSubscriptionFilter(requested, this.config.capabilities);
92
+ return modernSubscriptionResponse({
93
+ requestId: body.id as string | number,
94
+ notifications: accepted,
95
+ serverInfo: this.config.serverInfo,
96
+ });
97
+ } catch (error) {
98
+ if (error instanceof McpProtocolError) return json(rpcError(body.id, error.code, error.message, error.data), 400);
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ private streamedToolResponse(input: {
104
+ request: Request;
105
+ body: JsonRpcRequest;
106
+ base: string;
107
+ authorized: AuthorizedToken;
108
+ protocolVersion: string;
109
+ requestKey?: string;
110
+ }): Response {
111
+ const controller = new AbortController();
112
+ const cancel = () => controller.abort();
113
+ input.request.signal.addEventListener("abort", cancel, { once: true });
114
+ const result = this.dispatch(input.body, input.base, input.authorized, input.protocolVersion, controller.signal, input.requestKey)
115
+ .then((outcome) => outcome.message)
116
+ .finally(() => input.request.signal.removeEventListener("abort", cancel));
117
+ return modernJsonRpcResponseStream(result, {
118
+ onCancel: cancel,
119
+ onError: () => {
120
+ this.config.recordError("modern_stream_dispatch_failed");
121
+ return rpcError(input.body.id, -32603, "Internal error");
122
+ },
123
+ });
124
+ }
125
+
126
+ private async dispatch(
127
+ request: JsonRpcRequest,
128
+ base: string,
129
+ authorized: AuthorizedToken,
130
+ protocolVersion: string,
131
+ signal: AbortSignal,
132
+ requestKey?: string,
133
+ ): Promise<ModernDispatchResult> {
134
+ if (request.method === "server/discover") return this.discover(request);
135
+ if (request.method === "tools/list") return this.listTools(request, authorized);
136
+ if (request.method === "tools/call") return this.callTool(request, base, authorized, protocolVersion, signal, requestKey);
137
+ return { status: 404, message: rpcError(request.id, -32601, "Method not found") };
138
+ }
139
+
140
+ private discover(request: JsonRpcRequest): ModernDispatchResult {
141
+ return {
142
+ status: 200,
143
+ message: rpcResult(request.id, modernDiscoverResult({
144
+ supportedVersions: this.config.supportedVersions,
145
+ capabilities: this.config.capabilities,
146
+ instructions: this.config.instructions,
147
+ ttlMs: this.config.discoveryTtlMs,
148
+ serverInfo: this.config.serverInfo,
149
+ }))!,
150
+ };
151
+ }
152
+
153
+ private listTools(request: JsonRpcRequest, authorized: AuthorizedToken): ModernDispatchResult {
154
+ return {
155
+ status: 200,
156
+ message: rpcResult(request.id, modernCacheableResult(
157
+ { tools: this.config.tools(authorized) },
158
+ { ttlMs: this.config.toolListTtlMs, cacheScope: "private", serverInfo: this.config.serverInfo },
159
+ ))!,
160
+ };
161
+ }
162
+
163
+ private async callTool(
164
+ request: JsonRpcRequest,
165
+ base: string,
166
+ authorized: AuthorizedToken,
167
+ protocolVersion: string,
168
+ signal: AbortSignal,
169
+ requestKey?: string,
170
+ ): Promise<ModernDispatchResult> {
171
+ const inspected = inspectWorkerToolCall(request.params, this.config.tools(authorized));
172
+ if (!inspected.ok) {
173
+ const message = inspected.reason === "missing_name" ? "tools/call requires a tool name"
174
+ : inspected.reason === "unknown_tool" ? "Unknown tool"
175
+ : "Tool arguments do not match the input schema";
176
+ return {
177
+ status: 200,
178
+ message: rpcError(request.id, -32602, message, inspected.issues ? { side_effects_started: false, validation_issues: [...inspected.issues] } : undefined),
179
+ };
180
+ }
181
+ const { name, args: rawArgs } = inspected;
182
+ try {
183
+ const result = await this.config.callTool({
184
+ name, args: rawArgs as Record<string, unknown>, base, authorized, signal, requestKey,
185
+ });
186
+ return {
187
+ status: 200,
188
+ message: rpcResult(request.id, textToolResult(result, false, protocolVersion, this.config.serverInfo))!,
189
+ };
190
+ } catch (error) {
191
+ return {
192
+ status: 200,
193
+ message: rpcResult(request.id, textToolResult(
194
+ { error: publicWorkerToolError(error) }, true, protocolVersion, this.config.serverInfo,
195
+ ))!,
196
+ };
197
+ }
198
+ }
199
+ }