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
@@ -27,6 +27,7 @@ const MAX_MARKER_BYTES = 4096;
27
27
  const STARTUP_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
28
28
  const MALFORMED_LOCK_GRACE_MS = 60_000;
29
29
  const MAINTENANCE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
30
+ const MACHINE_SERVICE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
30
31
 
31
32
  export function expandHome(input = "") {
32
33
  if (!input || input === "~") return os.homedir();
@@ -34,6 +35,12 @@ export function expandHome(input = "") {
34
35
  return input;
35
36
  }
36
37
 
38
+ function expandHomeFrom(input, home) {
39
+ if (!input || input === "~") return home;
40
+ if (input.startsWith("~/")) return path.join(home, input.slice(2));
41
+ return input;
42
+ }
43
+
37
44
  export function resolveWorkspace(input = process.cwd()) {
38
45
  const resolved = path.resolve(expandHome(input));
39
46
  return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
@@ -49,13 +56,16 @@ export function ensureWorkspaceDirectory(input) {
49
56
  return resolveWorkspace(requested);
50
57
  }
51
58
 
52
- export function defaultStateRoot() {
53
- if (process.platform === "win32") {
54
- const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
59
+ export function defaultStateRoot(options = {}) {
60
+ const platform = String(options.platform || process.platform);
61
+ const home = path.resolve(String(options.home || os.homedir()));
62
+ const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
63
+ if (platform === "win32") {
64
+ const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
55
65
  return path.join(base, appName);
56
66
  }
57
- if (process.env.XDG_STATE_HOME) return path.join(expandHome(process.env.XDG_STATE_HOME), appName);
58
- return path.join(os.homedir(), ".local", "state", appName);
67
+ if (environment.XDG_STATE_HOME) return path.join(expandHomeFrom(String(environment.XDG_STATE_HOME), home), appName);
68
+ return path.join(home, ".local", "state", appName);
59
69
  }
60
70
 
61
71
 
@@ -272,6 +282,57 @@ function lockPathForState(state, name) {
272
282
  return path.join(profileDir, name);
273
283
  }
274
284
 
285
+ export function machineServiceControlRoot(options = {}) {
286
+ const home = path.resolve(String(options.home || os.homedir()));
287
+ if (options.controlRoot) return path.resolve(expandHomeFrom(String(options.controlRoot), home));
288
+ if (String(options.platform || process.platform) === "win32") {
289
+ const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
290
+ const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
291
+ return path.join(base, `${appName}-control`);
292
+ }
293
+ return path.join(home, ".local", "state", `${appName}-control`);
294
+ }
295
+
296
+ export function machineServiceLockPath(options = {}) {
297
+ return path.join(machineServiceControlRoot(options), "service-operation.lock");
298
+ }
299
+
300
+ export function acquireMachineServiceLock(metadata = {}, options = {}) {
301
+ const root = machineServiceControlRoot(options);
302
+ ensureOwnerOnlyDir(root);
303
+ assertNoForeignMaintenance(root);
304
+ const operation = typeof metadata.operation === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(metadata.operation)
305
+ ? metadata.operation
306
+ : "service-operation";
307
+ return acquireProcessLock(machineServiceLockPath({ controlRoot: root }), {
308
+ workspace: { path: "" },
309
+ paths: { profileDir: root, stateRoot: root },
310
+ }, "machine-service", { operation }, { maxAgeMs: MACHINE_SERVICE_LOCK_MAX_AGE_MS });
311
+ }
312
+
313
+ export async function acquireMachineServiceLockWithWait(options = {}) {
314
+ const timeoutMs = boundedPositiveInteger(options.timeoutMs, 30_000);
315
+ const pollMs = boundedPositiveInteger(options.pollMs, 100);
316
+ const logger = options.logger || { info() {} };
317
+ const metadata = { operation: options.operation };
318
+ let lock = acquireMachineServiceLock(metadata, options);
319
+ if (lock.acquired) return lock;
320
+ const ownerPid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "another process";
321
+ logger.info?.(`waiting for ${ownerPid} to finish the current machine-service operation`);
322
+ const deadline = createMonotonicDeadline(timeoutMs);
323
+ while (!deadline.expired()) {
324
+ await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
325
+ lock = acquireMachineServiceLock(metadata, options);
326
+ if (lock.acquired) {
327
+ logger.info?.("the previous machine-service operation finished; continuing");
328
+ return lock;
329
+ }
330
+ }
331
+ const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
332
+ const operation = typeof lock.owner?.operation === "string" ? ` (${lock.owner.operation})` : "";
333
+ throw new Error(`another machine-service operation did not finish within ${Math.ceil(timeoutMs / 1000)} seconds (${pid}${operation}); inspect the process before retrying`);
334
+ }
335
+
275
336
  export function acquireMaintenanceLock(stateRoot, metadata = {}) {
276
337
  const root = path.resolve(expandHome(stateRoot));
277
338
  if (!existsSync(root)) throw new Error("cannot acquire maintenance lock for a missing state root");
@@ -316,7 +377,7 @@ function assertNoForeignMaintenance(stateRoot) {
316
377
 
317
378
  export function acquireDaemonLock(state, metadata = {}) {
318
379
  assertNoForeignMaintenance(state?.paths?.stateRoot);
319
- const details = {};
380
+ const details = { startupReady: false, startupReadyAt: null };
320
381
  if (metadata?.mode === "foreground" || metadata?.mode === "service") details.mode = metadata.mode;
321
382
  if (typeof metadata?.version === "string" && /^[0-9A-Za-z.+_-]{1,64}$/.test(metadata.version)) details.version = metadata.version;
322
383
  return acquireProcessLock(daemonLockPathForState(state), state, "daemon", details, { maxAgeMs: Number.POSITIVE_INFINITY });
@@ -371,12 +432,17 @@ function acquireProcessLock(lockPath, state, purpose, details = {}, options = {}
371
432
  createExclusiveFileSync(lockPath, `${JSON.stringify(payload, null, 2)}
372
433
  `, { mode: 0o600 });
373
434
  ownerOnlyFile(lockPath);
374
- return {
435
+ const lock = {
375
436
  acquired: true,
376
437
  path: lockPath,
377
438
  owner: payload,
439
+ update(patch) {
440
+ lock.owner = updateProcessLock(lockPath, token, patch);
441
+ return lock.owner;
442
+ },
378
443
  release() { releaseProcessLock(lockPath, token); },
379
444
  };
445
+ return lock;
380
446
  } catch (error) {
381
447
  if (error?.code !== "EEXIST") throw error;
382
448
  const snapshot = readProcessLockSnapshot(lockPath);
@@ -452,6 +518,37 @@ function sameLockIdentity(left, right) {
452
518
  return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
453
519
  }
454
520
 
521
+ function updateProcessLock(lockPath, token, patch) {
522
+ if (!isPlainRecord(patch)) throw new TypeError("process lock update must be a plain record");
523
+ const allowed = new Set(["startupReady", "startupReadyAt"]);
524
+ for (const key of Object.keys(patch)) {
525
+ if (!allowed.has(key)) throw new Error(`process lock field is immutable: ${key}`);
526
+ }
527
+ if (patch.startupReady !== true) {
528
+ throw new TypeError("process lock startup readiness can only be published as true");
529
+ }
530
+ if (typeof patch.startupReadyAt !== "string" || !Number.isFinite(Date.parse(patch.startupReadyAt))) {
531
+ throw new TypeError("process lock startupReadyAt update must be an ISO timestamp");
532
+ }
533
+ const snapshot = readProcessLockSnapshot(lockPath);
534
+ if (!snapshot?.owner || snapshot.owner.token !== token || snapshot.owner.pid !== process.pid) {
535
+ throw new Error("process lock ownership changed before update");
536
+ }
537
+ if (snapshot.owner.purpose !== "daemon") {
538
+ throw new Error("only daemon locks can publish startup readiness");
539
+ }
540
+ if (snapshot.owner.startupReady === true || snapshot.owner.startupReadyAt !== null) {
541
+ throw new Error("process lock startup readiness was already published");
542
+ }
543
+ const updated = { ...snapshot.owner, startupReady: true, startupReadyAt: patch.startupReadyAt };
544
+ const content = `${JSON.stringify(updated, null, 2)}
545
+ `;
546
+ if (Buffer.byteLength(content) > MAX_LOCK_BYTES) throw new Error("process lock update exceeds the size limit");
547
+ replaceFileAtomicallySync(lockPath, content, { mode: 0o600 });
548
+ ownerOnlyFile(lockPath);
549
+ return updated;
550
+ }
551
+
455
552
  function releaseProcessLock(lockPath, token) {
456
553
  let snapshot;
457
554
  try { snapshot = readProcessLockSnapshot(lockPath); } catch { return; }
@@ -1,34 +1,49 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { LocalRuntime } from "./runtime.mjs";
4
+ import { validateToolArguments } from "./tool-executor.mjs";
4
5
  import { classifyOperationalError, createLogger } from "./log.mjs";
5
6
  import { publicError } from "./errors.mjs";
6
7
  import {
7
8
  MCP_INSTRUCTIONS,
8
- MCP_PROTOCOL_VERSION,
9
- MCP_SUPPORTED_PROTOCOL_VERSIONS,
10
9
  SERVER_NAME,
11
10
  rpcError,
12
11
  rpcResult,
13
12
  toolResult,
14
13
  toolsForPolicy,
15
14
  } from "./tools.mjs";
15
+ import {
16
+ MCP_LEGACY_PROTOCOL_VERSION, MCP_LEGACY_PROTOCOL_VERSIONS, MCP_MODERN_PROTOCOL_VERSION,
17
+ MCP_MODERN_PROTOCOL_VERSIONS, McpProtocolError, isModernMcpRequest, modernCacheableResult,
18
+ modernDiscoverResult, serverImplementation, validateModernRequestMetadata,
19
+ } from "../shared/mcp-protocol.mjs";
20
+ import {
21
+ acceptedSubscriptionFilter, subscriptionAcknowledgedNotification, subscriptionCompleteResult, validateSubscriptionFilter,
22
+ } from "../shared/mcp-subscriptions.mjs";
16
23
 
17
24
  const MAX_LINE_BYTES = 8 * 1024 * 1024;
18
25
  const MAX_PENDING_TOOL_CALLS = 32;
19
26
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
27
+ const MCP_SERVER_INFO = Object.freeze(serverImplementation({
28
+ name: SERVER_NAME, title: "Machine Bridge MCP", version: PACKAGE_VERSION,
29
+ description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
30
+ }));
31
+ const MCP_SERVER_CAPABILITIES = Object.freeze({ tools: Object.freeze({ listChanged: false }) });
32
+ const MCP_DISCOVERY_TTL_MS = 300_000;
33
+ const MCP_TOOL_LIST_TTL_MS = 300_000;
34
+ const MODERN_LIMIT_EXCEEDED = -31900;
20
35
 
21
36
  export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "" }) {
22
37
  const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
23
38
  const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath, browserStateRoot });
24
39
  const pending = new Map();
25
- let negotiatedVersion = MCP_PROTOCOL_VERSION;
26
- let initialized = false;
27
-
40
+ let legacyInitialized = false;
41
+ let legacyVersion = MCP_LEGACY_PROTOCOL_VERSION;
28
42
  const writes = new Set();
29
43
  const send = (message) => {
30
44
  if (message === null || message === undefined) return;
31
- const line = `${JSON.stringify(message)}\n`;
45
+ const line = `${JSON.stringify(message)}
46
+ `;
32
47
  const promise = new Promise((resolvePromise, rejectPromise) => {
33
48
  process.stdout.write(line, (error) => error ? rejectPromise(error) : resolvePromise());
34
49
  }).catch((error) => {
@@ -52,52 +67,113 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
52
67
 
53
68
  async function handleLine(line) {
54
69
  const message = parseJsonRpcLine(line, send);
55
- if (!message || typeof message.method !== "string") return;
56
- if (await handleControlMessage(message)) return;
57
- if (!initialized) {
58
- send(rpcError(message.id, -32002, "Server is not initialized"));
70
+ if (!message) return;
71
+ if (typeof message.method !== "string") {
72
+ send(rpcError(message.id, -32600, "Clients must not send JSON-RPC responses"));
73
+ return;
74
+ }
75
+ if (message.method === "notifications/cancelled") {
76
+ cancelRequest(message);
77
+ return;
78
+ }
79
+ if (isModernMcpRequest(message)) {
80
+ let context;
81
+ try {
82
+ context = validateModernRequestMetadata(message, MCP_MODERN_PROTOCOL_VERSIONS);
83
+ } catch (error) {
84
+ if (error instanceof McpProtocolError) {
85
+ send(rpcError(message.id, error.code, error.message, error.data));
86
+ return;
87
+ }
88
+ throw error;
89
+ }
90
+ await handleModernMessage(message, context.version);
59
91
  return;
60
92
  }
61
- await handleInitializedMessage(message);
62
- }
63
93
 
64
- async function handleControlMessage(message) {
65
94
  if (message.method === "initialize") {
66
- const requested = asObject(message.params).protocolVersion;
67
- negotiatedVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
68
- ? requested
69
- : MCP_PROTOCOL_VERSION;
70
- initialized = true;
71
- send(rpcResult(message.id, await initializationResult(negotiatedVersion, runtime)));
72
- return true;
73
- }
74
- if (message.method === "notifications/initialized") return true;
75
- if (message.method === "notifications/cancelled") {
76
- const requestId = asObject(message.params).requestId;
77
- const callId = pending.get(jsonRpcIdKey(requestId));
78
- if (callId) runtime.cancelCall(callId, "MCP cancellation notification");
79
- return true;
95
+ await initializeLegacy(message);
96
+ return;
80
97
  }
98
+ if (message.method === "notifications/initialized") return;
81
99
  if (message.method === "logging/setLevel" || message.method === "ping") {
82
100
  send(rpcResult(message.id, {}));
83
- return true;
101
+ return;
102
+ }
103
+ if (!legacyInitialized) {
104
+ send(rpcError(message.id, -32002, "Server is not initialized"));
105
+ return;
106
+ }
107
+ await handleLegacyMessage(message);
108
+ }
109
+
110
+ async function initializeLegacy(message) {
111
+ const requested = asObject(message.params).protocolVersion;
112
+ legacyVersion = typeof requested === "string" && MCP_LEGACY_PROTOCOL_VERSIONS.includes(requested)
113
+ ? requested
114
+ : MCP_LEGACY_PROTOCOL_VERSION;
115
+ legacyInitialized = true;
116
+ send(rpcResult(message.id, await legacyInitializationResult(legacyVersion, runtime)));
117
+ }
118
+
119
+ function cancelRequest(message) {
120
+ const requestId = asObject(message.params).requestId;
121
+ const callId = pending.get(jsonRpcIdKey(requestId));
122
+ if (callId) runtime.cancelCall(callId, "MCP cancellation notification");
123
+ }
124
+
125
+ async function handleModernMessage(message, protocolVersion) {
126
+ if (!("id" in message) || message.id === null) {
127
+ if (!message.method.startsWith("notifications/")) {
128
+ send(rpcError(null, -32600, "Modern MCP requests require a non-null request id"));
129
+ }
130
+ return;
131
+ }
132
+ if (message.method === "server/discover") {
133
+ send(rpcResult(message.id, modernDiscoverResult({
134
+ supportedVersions: MCP_MODERN_PROTOCOL_VERSIONS,
135
+ capabilities: MCP_SERVER_CAPABILITIES,
136
+ instructions: MCP_INSTRUCTIONS,
137
+ ttlMs: MCP_DISCOVERY_TTL_MS,
138
+ serverInfo: MCP_SERVER_INFO,
139
+ })));
140
+ return;
141
+ }
142
+ if (message.method === "tools/list") {
143
+ send(rpcResult(message.id, modernCacheableResult(
144
+ { tools: toolsForPolicy(policy) },
145
+ { ttlMs: MCP_TOOL_LIST_TTL_MS, cacheScope: "private", serverInfo: MCP_SERVER_INFO },
146
+ )));
147
+ return;
148
+ }
149
+ if (message.method === "subscriptions/listen") {
150
+ const requested = subscriptionFilterOrError(asObject(message.params).notifications, message.id, send);
151
+ if (!requested) return;
152
+ const accepted = acceptedSubscriptionFilter(requested, MCP_SERVER_CAPABILITIES);
153
+ send(subscriptionAcknowledgedNotification(message.id, accepted));
154
+ send(rpcResult(message.id, subscriptionCompleteResult(message.id, MCP_SERVER_INFO)));
155
+ return;
156
+ }
157
+ if (message.method === "tools/call") {
158
+ await handleToolCall(message, protocolVersion);
159
+ return;
84
160
  }
85
- return false;
161
+ send(rpcError(message.id, -32601, "Method not found"));
86
162
  }
87
163
 
88
- async function handleInitializedMessage(message) {
164
+ async function handleLegacyMessage(message) {
89
165
  if (message.method === "tools/list") {
90
166
  send(rpcResult(message.id, { tools: toolsForPolicy(policy) }));
91
167
  return;
92
168
  }
93
169
  if (message.method === "tools/call") {
94
- await handleToolCall(message);
170
+ await handleToolCall(message, legacyVersion);
95
171
  return;
96
172
  }
97
- send(rpcError(message.id, -32601, `Method not found: ${message.method}`));
173
+ send(rpcError(message.id, -32601, "Method not found"));
98
174
  }
99
175
 
100
- async function handleToolCall(message) {
176
+ async function handleToolCall(message, protocolVersion) {
101
177
  if (!("id" in message) || message.id === null) {
102
178
  send(rpcError(null, -32600, "tools/call requires a non-null request id"));
103
179
  return;
@@ -108,28 +184,51 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
108
184
  send(rpcError(message.id, -32602, "tools/call requires a tool name"));
109
185
  return;
110
186
  }
111
- const args = asObject(params.arguments);
187
+ if (!toolsForPolicy(policy).some((tool) => tool.name === name)) {
188
+ send(rpcError(message.id, -32602, "Unknown or unavailable tool"));
189
+ return;
190
+ }
191
+ const rawArgs = params.arguments === undefined ? {} : params.arguments;
192
+ const validation = validateToolArguments(name, rawArgs);
193
+ if (!validation.known || !validation.valid) {
194
+ send(rpcError(message.id, -32602, !validation.known
195
+ ? "Unknown tool"
196
+ : "Tool arguments do not match the input schema", validation.valid ? undefined : {
197
+ validation_issues: validation.issues,
198
+ }));
199
+ return;
200
+ }
201
+ const args = rawArgs;
202
+ const modern = protocolVersion === MCP_MODERN_PROTOCOL_VERSION;
112
203
  if (pending.size >= MAX_PENDING_TOOL_CALLS) {
113
- send(rpcError(message.id, -32000, "Too many concurrent tool calls"));
204
+ send(rpcError(message.id, modern ? MODERN_LIMIT_EXCEEDED : -32000, "Too many concurrent tool calls"));
114
205
  return;
115
206
  }
116
- const callId = `stdio_${randomBytes(16).toString("hex")}`;
117
207
  const key = jsonRpcIdKey(message.id);
118
208
  if (key && pending.has(key)) {
119
209
  send(rpcError(message.id, -32600, "Duplicate in-flight JSON-RPC request id"));
120
210
  return;
121
211
  }
212
+ const callId = `stdio_${randomBytes(16).toString("hex")}`;
122
213
  if (key) pending.set(key, callId);
123
214
  try {
124
215
  const result = await runtime.executeTool(name, args, { callId, origin: "stdio" });
125
- send(rpcResult(message.id, toolResult(result)));
216
+ send(rpcResult(message.id, toolResult(result, false, protocolVersion, MCP_SERVER_INFO)));
126
217
  } catch (error) {
127
- send(rpcResult(message.id, toolResult({ error: publicError(error) }, true)));
218
+ send(rpcResult(message.id, toolResult({ error: publicError(error) }, true, protocolVersion, MCP_SERVER_INFO)));
128
219
  } finally {
129
220
  if (key) pending.delete(key);
130
221
  }
131
222
  }
223
+ }
132
224
 
225
+ function subscriptionFilterOrError(value, requestId, send) {
226
+ try { return validateSubscriptionFilter(value); }
227
+ catch (error) {
228
+ if (!(error instanceof McpProtocolError)) throw error;
229
+ send(rpcError(requestId, error.code, error.message, error.data));
230
+ return null;
231
+ }
133
232
  }
134
233
 
135
234
  function parseJsonRpcLine(line, send) {
@@ -145,7 +244,7 @@ function parseJsonRpcLine(line, send) {
145
244
  return message;
146
245
  }
147
246
 
148
- async function initializationResult(protocolVersion, runtime) {
247
+ async function legacyInitializationResult(protocolVersion, runtime) {
149
248
  const bootstrap = await runtime.sessionBootstrap({ path: "." }).catch((error) => {
150
249
  runtime.logger?.debug?.("session bootstrap unavailable during initialize", { error_class: classifyOperationalError(error) });
151
250
  return null;
@@ -154,12 +253,7 @@ async function initializationResult(protocolVersion, runtime) {
154
253
  return {
155
254
  protocolVersion,
156
255
  capabilities: { tools: { listChanged: false }, logging: {} },
157
- serverInfo: {
158
- name: SERVER_NAME,
159
- title: "Machine Bridge MCP",
160
- version: PACKAGE_VERSION,
161
- description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
162
- },
256
+ serverInfo: MCP_SERVER_INFO,
163
257
  instructions: `${MCP_INSTRUCTIONS}${localInstructions}`,
164
258
  };
165
259
  }
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+
3
+ const TUNNEL_INTERFACE = /^(?:utun|tun|tap|ppp|ipsec|wg|gif|stf)/i;
4
+ const LOOPBACK_INTERFACE = /^lo\d*$/i;
5
+ const PHYSICAL_INTERFACE = /^(?:en|eth|wlan|wl|wifi|wi-fi)\d*$/i;
6
+
7
+ /** @typedef {{code?: number, stdout?: string}} FixedProcessResult */
8
+ /** @typedef {(command: string, args: string[], timeoutMs: number, capture: boolean, maxOutputBytes: number, context: unknown) => Promise<FixedProcessResult>} FixedInternalRunner */
9
+ /**
10
+ * @typedef {{
11
+ * platform?: string,
12
+ * runFixedInternal?: FixedInternalRunner,
13
+ * classifyError?: (error: unknown) => string,
14
+ * context?: unknown,
15
+ * }} SystemNetworkRouteOptions
16
+ */
17
+
18
+ /** @param {SystemNetworkRouteOptions} [options] */
19
+ export async function systemNetworkRouteCheck({
20
+ platform = process.platform,
21
+ runFixedInternal,
22
+ classifyError = () => "unavailable",
23
+ context = {},
24
+ } = {}) {
25
+ try {
26
+ const route = await inspectSystemNetworkRoute({ platform, runFixedInternal, context });
27
+ return route.supported && route.available ? {
28
+ layer: "system-network-route",
29
+ ok: true,
30
+ route_class: route.route_class,
31
+ operating_system_interception: route.operating_system_interception === true,
32
+ } : {
33
+ layer: "system-network-route",
34
+ ok: false,
35
+ skipped: true,
36
+ error_class: route.supported ? "unavailable" : "unsupported_platform",
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ layer: "system-network-route",
41
+ ok: false,
42
+ skipped: true,
43
+ error_class: classifyError(error),
44
+ };
45
+ }
46
+ }
47
+
48
+ /** @param {SystemNetworkRouteOptions} [options] */
49
+ export async function inspectSystemNetworkRoute({
50
+ platform = process.platform,
51
+ runFixedInternal,
52
+ context = {},
53
+ } = {}) {
54
+ if (platform !== "darwin" || typeof runFixedInternal !== "function") return { supported: false };
55
+ const result = await runFixedInternal("/sbin/route", ["-n", "get", "default"], 5_000, true, 16 * 1024, context);
56
+ if (result?.code !== 0) return { supported: true, available: false };
57
+ const match = String(result.stdout || "").match(/^\s*interface:\s*(\S+)\s*$/mi);
58
+ if (!match) return { supported: true, available: false };
59
+ const routeClass = classifySystemRouteInterface(match[1]);
60
+ return {
61
+ supported: true,
62
+ available: true,
63
+ route_class: routeClass,
64
+ operating_system_interception: routeClass === "tunnel-or-vpn",
65
+ };
66
+ }
67
+
68
+ /** @param {unknown} value */
69
+ export function classifySystemRouteInterface(value) {
70
+ const name = String(value || "").trim();
71
+ if (!name) return "unknown";
72
+ if (TUNNEL_INTERFACE.test(name)) return "tunnel-or-vpn";
73
+ if (LOOPBACK_INTERFACE.test(name)) return "loopback";
74
+ if (PHYSICAL_INTERFACE.test(name)) return "physical-or-other";
75
+ return "other";
76
+ }
@@ -1,7 +1,15 @@
1
1
  import { performance } from "node:perf_hooks";
2
- import { errorCode, normalizeBridgeError } from "./errors.mjs";
2
+ import catalog from "../shared/tool-catalog.json" with { type: "json" };
3
+ import { compileToolArgumentValidators } from "../shared/tool-argument-validation.mjs";
4
+ import { BridgeError, errorCode, normalizeBridgeError } from "./errors.mjs";
3
5
  import { normalizeToolResult } from "./tool-result-boundary.mjs";
4
6
 
7
+ const TOOL_ARGUMENTS = compileToolArgumentValidators(catalog);
8
+
9
+ export function validateToolArguments(tool, value) {
10
+ return TOOL_ARGUMENTS.validate(tool, value);
11
+ }
12
+
5
13
  export class ToolExecutor {
6
14
  constructor(options = {}) {
7
15
  this.handlers = options.handlers || {};
@@ -18,11 +26,12 @@ export class ToolExecutor {
18
26
  lifecycleMiddleware(this.callRegistry),
19
27
  observabilityMiddleware(this.observability, this.securityAudit, this.logger, this.safeMessage, this.slowMs),
20
28
  authorizeMiddleware(this.policyGate, this.accountAccessGate, this.operationAuthorizer),
29
+ validateArgumentsMiddleware(),
21
30
  ], invokeHandler(this.handlers));
22
31
  }
23
32
 
24
33
  execute(tool, args = {}, request = {}) {
25
- return this.pipeline({ tool: String(tool || ""), args: isRecord(args) ? args : {}, request });
34
+ return this.pipeline({ tool: String(tool || ""), args, request });
26
35
  }
27
36
  }
28
37
 
@@ -48,6 +57,19 @@ function authorizeMiddleware(policyGate, accountAccessGate, operationAuthorizer)
48
57
  };
49
58
  }
50
59
 
60
+ function validateArgumentsMiddleware() {
61
+ return async (operation, next) => {
62
+ const result = TOOL_ARGUMENTS.validate(operation.tool, operation.args);
63
+ if (!result.known) throw new BridgeError("not_found", `unknown tool: ${operation.tool}`);
64
+ if (!result.valid) {
65
+ throw new BridgeError("invalid_request", `tool arguments do not match the input schema: ${operation.tool}`, {
66
+ details: { tool: operation.tool, validation_issues: result.issues },
67
+ });
68
+ }
69
+ return next(operation);
70
+ };
71
+ }
72
+
51
73
  function lifecycleMiddleware(callRegistry) {
52
74
  return async (operation, next) => {
53
75
  const context = callRegistry.open({
@@ -138,7 +160,3 @@ function safeByteLength(value) {
138
160
  function shortCallId(value) {
139
161
  return String(value || "").slice(0, 20);
140
162
  }
141
-
142
- function isRecord(value) {
143
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
144
- }
@@ -1,5 +1,6 @@
1
1
  import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
2
  import { projectMcpResult } from "../shared/result-projection.mjs";
3
+ import { MCP_LEGACY_PROTOCOL_VERSION, resultForProtocol } from "../shared/mcp-protocol.mjs";
3
4
  export {
4
5
  DEFAULT_POLICY_PROFILE,
5
6
  DEFAULT_POLICY_REVISION,
@@ -27,13 +28,13 @@ export const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
27
28
  export const MCP_SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(serverMetadata.supportedProtocolVersions.map((value) => String(value)));
28
29
  export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((value) => String(value))).join("\n");
29
30
 
30
- export function toolResult(value, isError = false) {
31
+ export function toolResult(value, isError = false, protocolVersion = MCP_LEGACY_PROTOCOL_VERSION, serverInfo) {
31
32
  const special = specialMcpResult(value);
32
- if (special) return { ...special, isError };
33
+ if (special) return resultForProtocol(protocolVersion, { ...special, isError }, { serverInfo });
33
34
  const projection = projectMcpResult(value);
34
35
  const result = { content: [{ type: "text", text: projection.text }], isError };
35
- if (projection.structuredContent) result.structuredContent = projection.structuredContent;
36
- return result;
36
+ if (projection.hasStructuredContent) result.structuredContent = structuredClone(projection.structuredContent);
37
+ return resultForProtocol(protocolVersion, result, { serverInfo });
37
38
  }
38
39
 
39
40
  export function rpcResult(id, result) {
@@ -51,7 +52,7 @@ function specialMcpResult(value) {
51
52
  const special = value && typeof value === "object" && !Array.isArray(value) ? value.$mcp : null;
52
53
  if (!special || typeof special !== "object" || !Array.isArray(special.content)) return null;
53
54
  const result = { content: structuredClone(special.content) };
54
- if (special.structuredContent && typeof special.structuredContent === "object" && !Array.isArray(special.structuredContent)) {
55
+ if (Object.prototype.hasOwnProperty.call(special, "structuredContent")) {
55
56
  result.structuredContent = structuredClone(special.structuredContent);
56
57
  }
57
58
  return result;
@@ -0,0 +1,49 @@
1
+ // @ts-check
2
+
3
+ import { waitForStableActiveStatus } from "./service-convergence.mjs";
4
+
5
+ /** @typedef {{ statusAttempts?: unknown, statusDelayMs?: unknown, stableSamples?: unknown, sleep?: (milliseconds: number) => Promise<void> }} WindowsConvergenceOptions */
6
+ /** @typedef {Record<string, unknown> & { active?: boolean }} WindowsStatus */
7
+
8
+ /** @param {() => WindowsStatus | Promise<WindowsStatus>} readStatus @param {WindowsConvergenceOptions} [options] */
9
+ export async function stableWindowsStatus(readStatus, options = {}) {
10
+ if (typeof readStatus !== "function") throw new TypeError("Windows status reader is required");
11
+ return waitForStableActiveStatus(readStatus, {
12
+ attempts: boundedPositiveInteger(options.statusAttempts, 10),
13
+ delayMs: boundedPositiveInteger(options.statusDelayMs, 100),
14
+ stableSamples: boundedPositiveInteger(options.stableSamples, 5),
15
+ sleep: typeof options.sleep === "function" ? options.sleep : undefined,
16
+ });
17
+ }
18
+
19
+ /** @param {() => WindowsStatus | Promise<WindowsStatus>} readStatus @param {(status: WindowsStatus) => boolean} predicate @param {WindowsConvergenceOptions} [options] */
20
+ export async function waitForWindowsStatus(readStatus, predicate, options = {}) {
21
+ if (typeof readStatus !== "function") throw new TypeError("Windows status reader is required");
22
+ if (typeof predicate !== "function") throw new TypeError("Windows status predicate is required");
23
+ const attempts = boundedPositiveInteger(options.statusAttempts, 10);
24
+ const delayMs = boundedPositiveInteger(options.statusDelayMs, 100);
25
+ const sleep = typeof options.sleep === "function"
26
+ ? options.sleep
27
+ : (/** @type {number} */ milliseconds) => new Promise(resolve => { setTimeout(resolve, milliseconds); });
28
+ let status = await readStatus();
29
+ for (let index = 1; index < attempts && !predicate(status); index += 1) {
30
+ await sleep(delayMs);
31
+ status = await readStatus();
32
+ }
33
+ return status;
34
+ }
35
+
36
+ /** @param {WindowsConvergenceOptions} [options] */
37
+ export function windowsStatusWaitOptions(options = {}) {
38
+ return {
39
+ attempts: boundedPositiveInteger(options.statusAttempts, 10),
40
+ delayMs: boundedPositiveInteger(options.statusDelayMs, 100),
41
+ sleep: typeof options.sleep === "function" ? options.sleep : undefined,
42
+ };
43
+ }
44
+
45
+ /** @param {unknown} value @param {number} fallback */
46
+ export function boundedPositiveInteger(value, fallback) {
47
+ const number = Number(value);
48
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
49
+ }