machine-bridge-mcp 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +8 -0
- package/README.md +5 -5
- package/docs/ARCHITECTURE.md +8 -6
- package/docs/ENGINEERING.md +160 -0
- package/docs/LOGGING.md +65 -28
- package/docs/OPERATIONS.md +7 -1
- package/docs/PRIVACY.md +6 -0
- package/docs/TESTING.md +9 -3
- package/package.json +8 -4
- package/src/local/cli.mjs +339 -280
- package/src/local/full-access-test.mjs +2 -2
- package/src/local/job-runner.mjs +3 -18
- package/src/local/log.mjs +2 -0
- package/src/local/managed-jobs.mjs +57 -77
- package/src/local/relay-connection.mjs +517 -0
- package/src/local/{daemon.mjs → runtime.mjs} +145 -188
- package/src/local/secure-file.mjs +27 -0
- package/src/local/service.mjs +43 -7
- package/src/local/ssh-key.mjs +37 -19
- package/src/local/stdio.mjs +86 -70
- package/src/worker/index.ts +11 -4
package/src/local/stdio.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { LocalRuntime } from "./runtime.mjs";
|
|
4
4
|
import { classifyOperationalError, createLogger } from "./log.mjs";
|
|
5
5
|
import {
|
|
6
6
|
MCP_INSTRUCTIONS,
|
|
@@ -19,7 +19,7 @@ const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.js
|
|
|
19
19
|
|
|
20
20
|
export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
|
|
21
21
|
const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
|
|
22
|
-
const runtime = new
|
|
22
|
+
const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
|
|
23
23
|
const pending = new Map();
|
|
24
24
|
let negotiatedVersion = MCP_PROTOCOL_VERSION;
|
|
25
25
|
let initialized = false;
|
|
@@ -50,100 +50,116 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
|
|
|
50
50
|
runtime.stop();
|
|
51
51
|
|
|
52
52
|
async function handleLine(line) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!isJsonRpcMessage(message)) {
|
|
59
|
-
send(rpcError(null, -32600, "Invalid JSON-RPC message"));
|
|
53
|
+
const message = parseJsonRpcLine(line, send);
|
|
54
|
+
if (!message || typeof message.method !== "string") return;
|
|
55
|
+
if (handleControlMessage(message)) return;
|
|
56
|
+
if (!initialized) {
|
|
57
|
+
send(rpcError(message.id, -32002, "Server is not initialized"));
|
|
60
58
|
return;
|
|
61
59
|
}
|
|
62
|
-
|
|
60
|
+
await handleInitializedMessage(message);
|
|
61
|
+
}
|
|
63
62
|
|
|
63
|
+
function handleControlMessage(message) {
|
|
64
64
|
if (message.method === "initialize") {
|
|
65
65
|
const requested = asObject(message.params).protocolVersion;
|
|
66
66
|
negotiatedVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
67
67
|
? requested
|
|
68
68
|
: MCP_PROTOCOL_VERSION;
|
|
69
69
|
initialized = true;
|
|
70
|
-
send(rpcResult(message.id,
|
|
71
|
-
|
|
72
|
-
capabilities: { tools: { listChanged: false }, logging: {} },
|
|
73
|
-
serverInfo: {
|
|
74
|
-
name: SERVER_NAME,
|
|
75
|
-
title: "Machine Bridge MCP",
|
|
76
|
-
version: PACKAGE_VERSION,
|
|
77
|
-
description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
|
|
78
|
-
},
|
|
79
|
-
instructions: MCP_INSTRUCTIONS,
|
|
80
|
-
}));
|
|
81
|
-
return;
|
|
70
|
+
send(rpcResult(message.id, initializationResult(negotiatedVersion)));
|
|
71
|
+
return true;
|
|
82
72
|
}
|
|
83
|
-
|
|
84
|
-
if (message.method === "notifications/initialized") return;
|
|
73
|
+
if (message.method === "notifications/initialized") return true;
|
|
85
74
|
if (message.method === "notifications/cancelled") {
|
|
86
75
|
const requestId = asObject(message.params).requestId;
|
|
87
|
-
const
|
|
88
|
-
const callId = pending.get(key);
|
|
76
|
+
const callId = pending.get(jsonRpcIdKey(requestId));
|
|
89
77
|
if (callId) runtime.cancelCall(callId, "MCP cancellation notification");
|
|
90
|
-
return;
|
|
78
|
+
return true;
|
|
91
79
|
}
|
|
92
|
-
if (message.method === "logging/setLevel") {
|
|
80
|
+
if (message.method === "logging/setLevel" || message.method === "ping") {
|
|
93
81
|
send(rpcResult(message.id, {}));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleInitializedMessage(message) {
|
|
88
|
+
if (message.method === "tools/list") {
|
|
89
|
+
send(rpcResult(message.id, { tools: toolsForPolicy(policy) }));
|
|
94
90
|
return;
|
|
95
91
|
}
|
|
96
|
-
if (message.method === "
|
|
97
|
-
|
|
92
|
+
if (message.method === "tools/call") {
|
|
93
|
+
await handleToolCall(message);
|
|
98
94
|
return;
|
|
99
95
|
}
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
send(rpcError(message.id, -32601, `Method not found: ${message.method}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleToolCall(message) {
|
|
100
|
+
if (!("id" in message) || message.id === null) {
|
|
101
|
+
send(rpcError(null, -32600, "tools/call requires a non-null request id"));
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const params = asObject(message.params);
|
|
105
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
106
|
+
if (!name) {
|
|
107
|
+
send(rpcError(message.id, -32602, "tools/call requires a tool name"));
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const params = asObject(message.params);
|
|
114
|
-
const name = typeof params.name === "string" ? params.name : "";
|
|
115
|
-
if (!name) {
|
|
116
|
-
send(rpcError(message.id, -32602, "tools/call requires a tool name"));
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const args = asObject(params.arguments);
|
|
120
|
-
const callId = `stdio_${randomBytes(16).toString("hex")}`;
|
|
121
|
-
const key = jsonRpcIdKey(message.id);
|
|
122
|
-
if (key && pending.has(key)) {
|
|
123
|
-
send(rpcError(message.id, -32600, "Duplicate in-flight JSON-RPC request id"));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (key) pending.set(key, callId);
|
|
127
|
-
const started = Date.now();
|
|
128
|
-
logger.debug("tool call started", { call_id: callId.slice(0, 20), tool: name });
|
|
129
|
-
try {
|
|
130
|
-
const result = await runtime.executeTool(name, args, { callId });
|
|
131
|
-
send(rpcResult(message.id, toolResult(result)));
|
|
132
|
-
const durationMs = Date.now() - started;
|
|
133
|
-
logger.debug(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
|
|
134
|
-
} catch (error) {
|
|
135
|
-
const safeError = runtime.safeErrorMessage(error, args);
|
|
136
|
-
send(rpcResult(message.id, toolResult({ error: safeError }, true)));
|
|
137
|
-
const durationMs = Date.now() - started;
|
|
138
|
-
logger.debug("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
|
|
139
|
-
} finally {
|
|
140
|
-
if (key) pending.delete(key);
|
|
141
|
-
runtime.finishCall(callId);
|
|
142
|
-
}
|
|
110
|
+
const args = asObject(params.arguments);
|
|
111
|
+
const callId = `stdio_${randomBytes(16).toString("hex")}`;
|
|
112
|
+
const key = jsonRpcIdKey(message.id);
|
|
113
|
+
if (key && pending.has(key)) {
|
|
114
|
+
send(rpcError(message.id, -32600, "Duplicate in-flight JSON-RPC request id"));
|
|
143
115
|
return;
|
|
144
116
|
}
|
|
145
|
-
|
|
117
|
+
if (key) pending.set(key, callId);
|
|
118
|
+
const started = Date.now();
|
|
119
|
+
logger.debug("tool call started", { call_id: callId.slice(0, 20), tool: name });
|
|
120
|
+
try {
|
|
121
|
+
const result = await runtime.executeTool(name, args, { callId });
|
|
122
|
+
send(rpcResult(message.id, toolResult(result)));
|
|
123
|
+
const durationMs = Date.now() - started;
|
|
124
|
+
logger.debug(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const safeError = runtime.safeErrorMessage(error, args);
|
|
127
|
+
send(rpcResult(message.id, toolResult({ error: safeError }, true)));
|
|
128
|
+
const durationMs = Date.now() - started;
|
|
129
|
+
logger.debug("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
|
|
130
|
+
} finally {
|
|
131
|
+
if (key) pending.delete(key);
|
|
132
|
+
runtime.finishCall(callId);
|
|
133
|
+
}
|
|
146
134
|
}
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseJsonRpcLine(line, send) {
|
|
139
|
+
let message;
|
|
140
|
+
try { message = JSON.parse(line); } catch {
|
|
141
|
+
send(rpcError(null, -32700, "Parse error"));
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (!isJsonRpcMessage(message)) {
|
|
145
|
+
send(rpcError(null, -32600, "Invalid JSON-RPC message"));
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return message;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function initializationResult(protocolVersion) {
|
|
152
|
+
return {
|
|
153
|
+
protocolVersion,
|
|
154
|
+
capabilities: { tools: { listChanged: false }, logging: {} },
|
|
155
|
+
serverInfo: {
|
|
156
|
+
name: SERVER_NAME,
|
|
157
|
+
title: "Machine Bridge MCP",
|
|
158
|
+
version: PACKAGE_VERSION,
|
|
159
|
+
description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
|
|
160
|
+
},
|
|
161
|
+
instructions: MCP_INSTRUCTIONS,
|
|
162
|
+
};
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
function consumeBoundedJsonLines(stream, { maxLineBytes, onLine, onOversize }) {
|
package/src/worker/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
|
|
|
3
3
|
import serverMetadata from "../shared/server-metadata.json";
|
|
4
4
|
|
|
5
5
|
const SERVER_NAME = String(serverMetadata.name);
|
|
6
|
-
const SERVER_VERSION = "0.
|
|
6
|
+
const SERVER_VERSION = "0.8.1";
|
|
7
7
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
8
8
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
9
9
|
const JSONRPC_VERSION = "2.0";
|
|
@@ -293,13 +293,20 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
async webSocketClose(ws: WebSocket): Promise<void> {
|
|
296
|
+
await this.cleanupDaemonSocket(ws, "daemon disconnected");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
|
|
300
|
+
console.error(JSON.stringify({ level: "warn", message: "daemon_websocket_error", error_class: workerErrorClass(error) }));
|
|
301
|
+
await this.cleanupDaemonSocket(ws, "daemon transport error");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async cleanupDaemonSocket(ws: WebSocket, message: string): Promise<void> {
|
|
296
305
|
await this.scheduleCandidateAlarm();
|
|
297
|
-
const attachment = this.daemonAttachment(ws);
|
|
298
|
-
if (!attachment) return;
|
|
299
306
|
for (const [id, pending] of this.pending) {
|
|
300
307
|
if (pending.socket !== ws) continue;
|
|
301
308
|
clearTimeout(pending.timeout);
|
|
302
|
-
pending.reject(new Error(
|
|
309
|
+
pending.reject(new Error(message));
|
|
303
310
|
this.pending.delete(id);
|
|
304
311
|
}
|
|
305
312
|
}
|