machine-bridge-mcp 0.4.2 → 0.6.0

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.
@@ -6,6 +6,15 @@ import { ensureOwnerOnlyDir, expandHome } from "./state.mjs";
6
6
 
7
7
  const LABEL = "dev.machine-bridge-mcp.daemon";
8
8
  const WINDOWS_TASK = "MachineBridgeMCP";
9
+ const SERVICE_COMMAND_OUTPUT_BYTES = 64 * 1024;
10
+
11
+ function serviceRun(command, args) {
12
+ return run(command, args, {
13
+ capture: true,
14
+ allowFailure: true,
15
+ maxOutputBytes: SERVICE_COMMAND_OUTPUT_BYTES,
16
+ });
17
+ }
9
18
 
10
19
  export async function installAutostart({ workspace, stateRoot, entryScript, logger = console }) {
11
20
  const spec = serviceSpec({ workspace, stateRoot, entryScript });
@@ -28,14 +37,14 @@ export async function autostartStatus({ logger = console } = {}) {
28
37
 
29
38
  export async function startAutostart({ logger = console } = {}) {
30
39
  if (process.platform === "darwin") return startLaunchd(logger);
31
- if (process.platform === "win32") return run("schtasks", ["/Run", "/TN", WINDOWS_TASK], { capture: true, allowFailure: true });
32
- return run("systemctl", ["--user", "start", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
40
+ if (process.platform === "win32") return serviceRun("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
41
+ return serviceRun("systemctl", ["--user", "start", "machine-bridge-mcp.service"]);
33
42
  }
34
43
 
35
44
  export async function stopAutostart({ logger = console } = {}) {
36
45
  if (process.platform === "darwin") return stopLaunchd(logger);
37
- if (process.platform === "win32") return run("schtasks", ["/End", "/TN", WINDOWS_TASK], { capture: true, allowFailure: true });
38
- return run("systemctl", ["--user", "stop", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
46
+ if (process.platform === "win32") return serviceRun("schtasks", ["/End", "/TN", WINDOWS_TASK]);
47
+ return serviceRun("systemctl", ["--user", "stop", "machine-bridge-mcp.service"]);
39
48
  }
40
49
 
41
50
 
@@ -50,21 +59,31 @@ export function trimAutostartLogs(stateRoot, options = {}) {
50
59
  const info = statSync(file);
51
60
  if (info.size > maxBytes) {
52
61
  const fd = openSync(file, "r");
62
+ let buffer;
53
63
  try {
54
64
  const current = fstatSync(fd);
55
65
  const length = Math.min(keepBytes, current.size);
56
- const buffer = Buffer.alloc(length);
66
+ buffer = Buffer.alloc(length);
57
67
  readSync(fd, buffer, 0, length, Math.max(0, current.size - length));
58
- writeFileSync(file, buffer, { mode: 0o600 });
59
68
  } finally {
60
69
  closeSync(fd);
61
70
  }
71
+ writeFileSync(file, lineSafeTail(buffer), { mode: 0o600 });
62
72
  }
63
73
  chmodSync(file, 0o600);
64
74
  } catch {}
65
75
  }
66
76
  }
67
77
 
78
+ function lineSafeTail(buffer) {
79
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) return Buffer.alloc(0);
80
+ let text = buffer.toString("utf8");
81
+ const firstNewline = text.indexOf("\n");
82
+ if (firstNewline >= 0 && firstNewline < text.length - 1) text = text.slice(firstNewline + 1);
83
+ else text = text.replace(/^\uFFFD+/, "");
84
+ return Buffer.from(text, "utf8");
85
+ }
86
+
68
87
  function serviceSpec({ workspace, stateRoot, entryScript }) {
69
88
  const root = expandHome(stateRoot);
70
89
  const logs = path.join(root, "logs");
@@ -92,7 +111,7 @@ export function daemonArgs(spec) {
92
111
  "--workspace", spec.workspace,
93
112
  "--state-dir", spec.stateRoot,
94
113
  "--no-print-credentials",
95
- "--quiet",
114
+ "--log-level", "warn",
96
115
  ];
97
116
  }
98
117
 
@@ -113,9 +132,9 @@ async function startLaunchd(logger) {
113
132
  const plistPath = launchdPlistPath();
114
133
  const target = `gui/${process.getuid?.() ?? ""}`;
115
134
  if (!existsSync(plistPath)) return { ok: false, error: "launchd plist not installed" };
116
- await run("launchctl", ["bootout", target, plistPath], { capture: true, allowFailure: true });
117
- const boot = await run("launchctl", ["bootstrap", target, plistPath], { capture: true, allowFailure: true });
118
- const kick = await run("launchctl", ["kickstart", "-k", `${target}/${LABEL}`], { capture: true, allowFailure: true });
135
+ await serviceRun("launchctl", ["bootout", target, plistPath]);
136
+ const boot = await serviceRun("launchctl", ["bootstrap", target, plistPath]);
137
+ const kick = await serviceRun("launchctl", ["kickstart", "-k", `${target}/${LABEL}`]);
119
138
  logger.info?.("launchd service started");
120
139
  return { ok: boot.code === 0 || kick.code === 0, bootstrap: boot, kickstart: kick };
121
140
  }
@@ -123,7 +142,7 @@ async function startLaunchd(logger) {
123
142
  async function stopLaunchd(logger) {
124
143
  const plistPath = launchdPlistPath();
125
144
  const target = `gui/${process.getuid?.() ?? ""}`;
126
- const result = await run("launchctl", ["bootout", target, plistPath], { capture: true, allowFailure: true });
145
+ const result = await serviceRun("launchctl", ["bootout", target, plistPath]);
127
146
  logger.info?.("launchd service stopped");
128
147
  return result;
129
148
  }
@@ -139,7 +158,7 @@ async function uninstallLaunchd(logger) {
139
158
  async function statusLaunchd() {
140
159
  const plistPath = launchdPlistPath();
141
160
  const target = `gui/${process.getuid?.() ?? ""}/${LABEL}`;
142
- const result = await run("launchctl", ["print", target], { capture: true, allowFailure: true });
161
+ const result = await serviceRun("launchctl", ["print", target]);
143
162
  return { ok: existsSync(plistPath), provider: "launchd", installed: existsSync(plistPath), path: plistPath, active: result.code === 0, detail: result.stdout || result.stderr };
144
163
  }
145
164
 
@@ -173,25 +192,25 @@ async function installSystemd(spec, logger) {
173
192
  const servicePath = systemdPath();
174
193
  mkdirSync(path.dirname(servicePath), { recursive: true });
175
194
  writeFileSync(servicePath, systemdUnit(spec), { mode: 0o644 });
176
- const reload = await run("systemctl", ["--user", "daemon-reload"], { capture: true, allowFailure: true });
177
- const enable = await run("systemctl", ["--user", "enable", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
178
- const linger = await run("loginctl", ["enable-linger", os.userInfo().username], { capture: true, allowFailure: true });
195
+ const reload = await serviceRun("systemctl", ["--user", "daemon-reload"]);
196
+ const enable = await serviceRun("systemctl", ["--user", "enable", "machine-bridge-mcp.service"]);
197
+ const linger = await serviceRun("loginctl", ["enable-linger", os.userInfo().username]);
179
198
  logger.info?.(`Autostart installed: ${servicePath}`);
180
199
  return { ok: reload.code === 0 && enable.code === 0, provider: "systemd", path: servicePath, reload, enable, linger };
181
200
  }
182
201
 
183
202
  async function uninstallSystemd(logger) {
184
- await run("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
203
+ await serviceRun("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"]);
185
204
  const servicePath = systemdPath();
186
205
  if (existsSync(servicePath)) rmSync(servicePath, { force: true });
187
- await run("systemctl", ["--user", "daemon-reload"], { capture: true, allowFailure: true });
206
+ await serviceRun("systemctl", ["--user", "daemon-reload"]);
188
207
  logger.info?.(`Autostart removed: ${servicePath}`);
189
208
  return { ok: true, provider: "systemd", path: servicePath };
190
209
  }
191
210
 
192
211
  async function statusSystemd() {
193
212
  const servicePath = systemdPath();
194
- const result = await run("systemctl", ["--user", "status", "machine-bridge-mcp.service", "--no-pager"], { capture: true, allowFailure: true });
213
+ const result = await serviceRun("systemctl", ["--user", "status", "machine-bridge-mcp.service", "--no-pager"]);
195
214
  return { ok: existsSync(servicePath), provider: "systemd", installed: existsSync(servicePath), path: servicePath, active: result.code === 0, detail: result.stdout || result.stderr };
196
215
  }
197
216
 
@@ -216,19 +235,19 @@ WantedBy=default.target
216
235
 
217
236
  async function installWindowsTask(spec, logger) {
218
237
  const command = windowsCommand(spec);
219
- const result = await run("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/TR", command, "/F"], { capture: true, allowFailure: true });
238
+ const result = await serviceRun("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/TR", command, "/F"]);
220
239
  logger.info?.("Windows Scheduled Task installed for logon");
221
240
  return { ok: result.code === 0, provider: "schtasks", task: WINDOWS_TASK, result };
222
241
  }
223
242
 
224
243
  async function uninstallWindowsTask(logger) {
225
- const result = await run("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"], { capture: true, allowFailure: true });
244
+ const result = await serviceRun("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
226
245
  logger.info?.("Windows Scheduled Task removed");
227
246
  return { ok: result.code === 0 || /cannot find/i.test(result.stderr), provider: "schtasks", task: WINDOWS_TASK, result };
228
247
  }
229
248
 
230
249
  async function statusWindowsTask() {
231
- const result = await run("schtasks", ["/Query", "/TN", WINDOWS_TASK, "/FO", "LIST"], { capture: true, allowFailure: true });
250
+ const result = await serviceRun("schtasks", ["/Query", "/TN", WINDOWS_TASK, "/FO", "LIST"]);
232
251
  return { ok: result.code === 0, provider: "schtasks", installed: result.code === 0, task: WINDOWS_TASK, active: result.code === 0, detail: result.stdout || result.stderr };
233
252
  }
234
253
 
@@ -1,12 +1,16 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
2
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
6
7
 
7
8
  export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
- export const appName = "machine-bridge-mcp";
9
+ export const appName = String(serverMetadata.name);
9
10
  const STATE_MARKER = ".machine-bridge-mcp-state";
11
+ const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
12
+ const MAX_LOCK_BYTES = 64 * 1024;
13
+ const MAX_MARKER_BYTES = 4096;
10
14
 
11
15
  export function expandHome(input = "") {
12
16
  if (!input || input === "~") return os.homedir();
@@ -16,7 +20,7 @@ export function expandHome(input = "") {
16
20
 
17
21
  export function resolveWorkspace(input = process.cwd()) {
18
22
  const resolved = path.resolve(expandHome(input));
19
- return realpathSync(resolved);
23
+ return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
20
24
  }
21
25
 
22
26
  export function defaultStateRoot() {
@@ -29,7 +33,7 @@ export function defaultStateRoot() {
29
33
  }
30
34
 
31
35
 
32
- export function configPath(stateRoot = defaultStateRoot()) {
36
+ function configPath(stateRoot = defaultStateRoot()) {
33
37
  return path.join(expandHome(stateRoot), "config.json");
34
38
  }
35
39
 
@@ -48,9 +52,10 @@ export function saveGlobalConfig(config, stateRoot = defaultStateRoot()) {
48
52
 
49
53
  export function setSelectedWorkspace(workspace, stateRoot = defaultStateRoot()) {
50
54
  const root = expandHome(stateRoot);
55
+ const canonicalWorkspace = resolveWorkspace(workspace);
51
56
  const config = loadGlobalConfig(root);
52
- config.selectedWorkspace = workspace;
53
- config.selectedWorkspaceHash = workspaceHash(workspace);
57
+ config.selectedWorkspace = canonicalWorkspace;
58
+ config.selectedWorkspaceHash = workspaceHash(canonicalWorkspace);
54
59
  saveGlobalConfig(config, root);
55
60
  return config;
56
61
  }
@@ -75,20 +80,53 @@ export function removeStateRoot(stateRoot = defaultStateRoot()) {
75
80
  }
76
81
 
77
82
  export function workspaceHash(workspace) {
78
- return createHash("sha256").update(String(workspace)).digest("hex").slice(0, 24);
83
+ const canonical = resolveWorkspace(workspace);
84
+ const identity = process.platform === "win32" ? canonical.toLowerCase() : canonical;
85
+ return createHash("sha256").update(identity).digest("hex").slice(0, 24);
79
86
  }
80
87
 
81
- export function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
88
+ function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
82
89
  return path.join(expandHome(stateRoot), "profiles", workspaceHash(workspace));
83
90
  }
84
91
 
85
- export function statePathForWorkspace(workspace, stateRoot = defaultStateRoot()) {
86
- return path.join(profileDirForWorkspace(workspace, stateRoot), "state.json");
92
+
93
+ function matchingLegacyProfileDir(canonicalWorkspace, stateRoot) {
94
+ const profilesRoot = path.join(stateRoot, "profiles");
95
+ if (!existsSync(profilesRoot)) return "";
96
+ const matches = [];
97
+ for (const entry of readdirSync(profilesRoot, { withFileTypes: true })) {
98
+ if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
99
+ const profileDir = path.join(profilesRoot, entry.name);
100
+ const stateFile = path.join(profileDir, "state.json");
101
+ if (!existsSync(stateFile)) continue;
102
+ try {
103
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
104
+ const storedWorkspace = value?.workspace?.path;
105
+ if (typeof storedWorkspace !== "string") continue;
106
+ if (sameWorkspaceIdentity(storedWorkspace, canonicalWorkspace)) matches.push(profileDir);
107
+ } catch {}
108
+ }
109
+ if (matches.length > 1) throw new Error("multiple Machine Bridge profiles refer to the same canonical workspace; remove or merge the duplicate state profiles");
110
+ return matches[0] || "";
111
+ }
112
+
113
+ function sameWorkspaceIdentity(left, right) {
114
+ try {
115
+ const a = resolveWorkspace(left);
116
+ const b = resolveWorkspace(right);
117
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
118
+ } catch {
119
+ return false;
120
+ }
87
121
  }
88
122
 
89
123
  export function loadState(workspace, options = {}) {
124
+ const canonicalWorkspace = resolveWorkspace(workspace);
90
125
  const stateRoot = ensureStateRoot(options.stateDir ? expandHome(options.stateDir) : defaultStateRoot());
91
- const profileDir = profileDirForWorkspace(workspace, stateRoot);
126
+ const canonicalProfileDir = profileDirForWorkspace(canonicalWorkspace, stateRoot);
127
+ const profileDir = existsSync(canonicalProfileDir)
128
+ ? canonicalProfileDir
129
+ : matchingLegacyProfileDir(canonicalWorkspace, stateRoot) || canonicalProfileDir;
92
130
  const statePath = path.join(profileDir, "state.json");
93
131
  ensureOwnerOnlyDir(profileDir);
94
132
  let state = {};
@@ -96,15 +134,16 @@ export function loadState(workspace, options = {}) {
96
134
  ownerOnlyFile(statePath);
97
135
  state = readJsonObjectOrBackup(statePath);
98
136
  }
99
- state.schemaVersion = 3;
137
+ state.schemaVersion = 5;
100
138
  state.workspace = {
101
- path: workspace,
102
- hash: workspaceHash(workspace),
139
+ path: canonicalWorkspace,
140
+ hash: path.basename(profileDir),
103
141
  updatedAt: new Date().toISOString(),
104
142
  };
105
143
  state.paths = { stateRoot, profileDir, statePath };
106
144
  state.worker ||= {};
107
145
  state.policy ||= {};
146
+ state.resources ||= {};
108
147
  delete state.localApi;
109
148
  return state;
110
149
  }
@@ -189,7 +228,7 @@ function releaseProcessLock(lockPath, token) {
189
228
 
190
229
  export function readDaemonLockOwner(lockPath) {
191
230
  try {
192
- const parsed = JSON.parse(readFileSync(lockPath, "utf8"));
231
+ const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
193
232
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
194
233
  } catch {
195
234
  return null;
@@ -207,9 +246,31 @@ function isPidAlive(pid) {
207
246
  }
208
247
  }
209
248
 
249
+ function readBoundedUtf8(filePath, maxBytes, label) {
250
+ const pathInfo = lstatSync(filePath);
251
+ if (pathInfo.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link`);
252
+ const noFollow = Number(fsConstants.O_NOFOLLOW || 0);
253
+ const fd = openSync(filePath, Number(fsConstants.O_RDONLY) | noFollow);
254
+ try {
255
+ const info = fstatSync(fd);
256
+ if (!info.isFile()) throw new Error(`${label} is not a regular file`);
257
+ if (info.size > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
258
+ const buffer = Buffer.alloc(info.size);
259
+ let offset = 0;
260
+ while (offset < buffer.length) {
261
+ const count = readSync(fd, buffer, offset, buffer.length - offset, offset);
262
+ if (count === 0) break;
263
+ offset += count;
264
+ }
265
+ return buffer.subarray(0, offset).toString("utf8");
266
+ } finally {
267
+ closeSync(fd);
268
+ }
269
+ }
270
+
210
271
  function readJsonObjectOrBackup(filePath) {
211
272
  try {
212
- const parsed = JSON.parse(readFileSync(filePath, "utf8"));
273
+ const parsed = JSON.parse(readBoundedUtf8(filePath, MAX_STATE_JSON_BYTES, "state JSON"));
213
274
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("JSON root must be an object");
214
275
  return parsed;
215
276
  } catch {
@@ -286,7 +347,7 @@ function assertSafeStateRootForRemoval(root) {
286
347
  }
287
348
 
288
349
  function assertValidStateMarker(marker, options = {}) {
289
- const content = readFileSync(marker, "utf8");
350
+ const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker");
290
351
  try {
291
352
  const value = JSON.parse(content);
292
353
  if (value?.app === appName && value?.schema === 1) return;
@@ -315,7 +376,7 @@ function stateRootMatchesRecordedWorkspace(root) {
315
376
  if (!entry.isDirectory()) continue;
316
377
  const stateFile = path.join(profiles, entry.name, "state.json");
317
378
  if (!existsSync(stateFile)) continue;
318
- const value = JSON.parse(readFileSync(stateFile, "utf8"));
379
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
319
380
  if (typeof value?.workspace?.path !== "string") continue;
320
381
  try { if (realpathSync(value.workspace.path) === root) return true; } catch {}
321
382
  }
@@ -327,7 +388,7 @@ function isRecognizableLegacyStateRoot(root) {
327
388
  const config = path.join(root, "config.json");
328
389
  try {
329
390
  if (existsSync(config)) {
330
- const value = JSON.parse(readFileSync(config, "utf8"));
391
+ const value = JSON.parse(readBoundedUtf8(config, MAX_STATE_JSON_BYTES, "config JSON"));
331
392
  if (
332
393
  value &&
333
394
  typeof value.selectedWorkspace === "string" &&
@@ -343,7 +404,7 @@ function isRecognizableLegacyStateRoot(root) {
343
404
  if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
344
405
  const stateFile = path.join(profiles, entry.name, "state.json");
345
406
  if (!existsSync(stateFile)) continue;
346
- const value = JSON.parse(readFileSync(stateFile, "utf8"));
407
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
347
408
  if (value?.workspace?.hash === entry.name && typeof value?.workspace?.path === "string") return true;
348
409
  }
349
410
  } catch {}
@@ -354,11 +415,13 @@ function atomicWriteJson(filePath, value) {
354
415
  const dir = path.dirname(filePath);
355
416
  ensureOwnerOnlyDir(dir);
356
417
  cleanupStaleAtomicTemps(dir, path.basename(filePath));
418
+ const serialized = `${JSON.stringify(value, null, 2)}\n`;
419
+ if (Buffer.byteLength(serialized) > MAX_STATE_JSON_BYTES) throw new Error(`state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
357
420
  const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
358
421
  let fd;
359
422
  try {
360
423
  fd = openSync(tempPath, "wx", 0o600);
361
- writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, "utf8");
424
+ writeFileSync(fd, serialized, "utf8");
362
425
  fsyncSync(fd);
363
426
  closeSync(fd);
364
427
  fd = undefined;
@@ -412,11 +475,11 @@ export function ensureWorkerSecrets(state, options = {}) {
412
475
  if (!state.worker.name || options.workerName) state.worker.name = options.workerName || defaultWorkerName(state.workspace.hash);
413
476
  }
414
477
 
415
- export function defaultWorkerName(hash) {
478
+ function defaultWorkerName(hash) {
416
479
  return `mbm-${String(hash || "default").slice(0, 12)}`;
417
480
  }
418
481
 
419
- export function randomToken(prefix) {
482
+ function randomToken(prefix) {
420
483
  return `${prefix}_${randomBytes(32).toString("base64url")}`;
421
484
  }
422
485
 
@@ -424,9 +487,6 @@ export function sha256(value) {
424
487
  return createHash("sha256").update(String(value)).digest("hex");
425
488
  }
426
489
 
427
- export function fileSha256(filePath) {
428
- return createHash("sha256").update(readFileSync(filePath)).digest("hex");
429
- }
430
490
 
431
491
  export function ensureOwnerOnlyDir(dir) {
432
492
  mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -434,6 +494,9 @@ export function ensureOwnerOnlyDir(dir) {
434
494
  }
435
495
 
436
496
  export function ownerOnlyFile(filePath) {
497
+ const info = lstatSync(filePath);
498
+ if (info.isSymbolicLink()) throw new Error(`owner-only file must not be a symbolic link: ${filePath}`);
499
+ if (!info.isFile()) throw new Error(`owner-only path is not a regular file: ${filePath}`);
437
500
  try { chmodSync(filePath, 0o600); } catch {}
438
501
  }
439
502
 
@@ -442,6 +505,11 @@ export function redactState(state) {
442
505
  if (clone.worker?.oauthPassword) clone.worker.oauthPassword = "<redacted>";
443
506
  if (clone.worker?.daemonSecret) clone.worker.daemonSecret = "<redacted>";
444
507
  if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
508
+ if (clone.resources && typeof clone.resources === "object") {
509
+ for (const value of Object.values(clone.resources)) {
510
+ if (value && typeof value === "object" && value.path) value.path = "<local-resource-path>";
511
+ }
512
+ }
445
513
  return clone;
446
514
  }
447
515
 
@@ -2,11 +2,12 @@ import readline from "node:readline";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { LocalDaemon } from "./daemon.mjs";
5
- import { formatFields, sanitizeLogText } from "./log.mjs";
5
+ import { classifyOperationalError, createLogger } from "./log.mjs";
6
6
  import {
7
7
  MCP_INSTRUCTIONS,
8
8
  MCP_PROTOCOL_VERSION,
9
9
  MCP_SUPPORTED_PROTOCOL_VERSIONS,
10
+ SERVER_NAME,
10
11
  rpcError,
11
12
  rpcResult,
12
13
  toolResult,
@@ -14,11 +15,12 @@ import {
14
15
  } from "./tools.mjs";
15
16
 
16
17
  const MAX_LINE_BYTES = 8 * 1024 * 1024;
18
+ const SLOW_TOOL_CALL_MS = 30_000;
17
19
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
18
20
 
19
- export async function runStdioServer({ workspace, policy, verbose = false }) {
20
- const logger = stderrLogger(verbose);
21
- const runtime = new LocalDaemon({ workspace, policy, logger });
21
+ export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
22
+ const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
23
+ const runtime = new LocalDaemon({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
22
24
  const pending = new Map();
23
25
  let negotiatedVersion = MCP_PROTOCOL_VERSION;
24
26
  let initialized = false;
@@ -40,7 +42,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
40
42
  return;
41
43
  }
42
44
  void handleLine(line).catch((error) => {
43
- logger.error("stdio request handler failed", { error: errorMessage(error) });
45
+ logger.error("stdio request handler failed", { error_class: classifyOperationalError(error) });
44
46
  });
45
47
  });
46
48
 
@@ -74,7 +76,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
74
76
  protocolVersion: negotiatedVersion,
75
77
  capabilities: { tools: { listChanged: false }, logging: {} },
76
78
  serverInfo: {
77
- name: "machine-bridge-mcp",
79
+ name: SERVER_NAME,
78
80
  title: "Machine Bridge MCP",
79
81
  version: PACKAGE_VERSION,
80
82
  description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
@@ -128,11 +130,15 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
128
130
  try {
129
131
  const result = await runtime.executeTool(name, args, { callId });
130
132
  send(rpcResult(message.id, toolResult(result)));
131
- logger.info("tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: Date.now() - started, ok: true });
133
+ const durationMs = Date.now() - started;
134
+ if (durationMs >= SLOW_TOOL_CALL_MS) logger.info("slow tool call completed", { tool: name, duration_ms: durationMs });
135
+ else logger.debug("tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
132
136
  } catch (error) {
133
137
  const safeError = runtime.safeErrorMessage(error);
134
138
  send(rpcResult(message.id, toolResult({ error: safeError }, true)));
135
- logger.warn("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: Date.now() - started, ok: false, error_class: classifyError(error) });
139
+ const durationMs = Date.now() - started;
140
+ logger.warn("tool call failed", { tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
141
+ logger.debug("tool call failure correlation", { call_id: callId.slice(0, 20) });
136
142
  } finally {
137
143
  if (key) pending.delete(key);
138
144
  runtime.finishCall(callId);
@@ -162,33 +168,3 @@ function jsonRpcIdKey(value) {
162
168
  function asObject(value) {
163
169
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
164
170
  }
165
-
166
- function classifyError(error) {
167
- const message = error instanceof Error ? error.message : String(error);
168
- if (/cancel/i.test(message)) return "cancelled";
169
- if (/timed out/i.test(message)) return "timeout";
170
- if (/outside the configured workspace/i.test(message)) return "path_boundary";
171
- if (/disabled|requires .* mode/i.test(message)) return "policy_denied";
172
- if (/not found|ENOENT/i.test(message)) return "not_found";
173
- if (/permission|EACCES|EPERM/i.test(message)) return "permission_denied";
174
- if (/maximum|exceeds|max_bytes|too many/i.test(message)) return "limit_exceeded";
175
- if (/invalid|must|requires|ambiguous|mismatch/i.test(message)) return "invalid_request";
176
- return "execution_failed";
177
- }
178
-
179
- function errorMessage(error) {
180
- return sanitizeLogText(error instanceof Error ? error.message : String(error)).slice(0, 4096) || "tool call failed";
181
- }
182
-
183
- function stderrLogger(verbose) {
184
- const write = (level, message, fields) => {
185
- if (level === "debug" && !verbose) return;
186
- process.stderr.write(`[${level}] stdio: ${sanitizeLogText(message)}${formatFields(fields)}\n`);
187
- };
188
- return {
189
- info(message, fields) { write("info", message, fields); },
190
- warn(message, fields) { write("warn", message, fields); },
191
- error(message, fields) { write("error", message, fields); },
192
- debug(message, fields) { write("debug", message, fields); },
193
- };
194
- }
@@ -1,16 +1,30 @@
1
1
  import catalog from "../shared/tool-catalog.json" with { type: "json" };
2
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
3
 
3
- export const MCP_PROTOCOL_VERSION = "2025-11-25";
4
- export const MCP_SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
4
+ export const SERVER_NAME = String(serverMetadata.name);
5
+ export const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
6
+ export const MCP_SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(serverMetadata.supportedProtocolVersions.map((value) => String(value)));
5
7
 
6
- export const MCP_INSTRUCTIONS = [
7
- "You are connected to a local workspace through machine-bridge-mcp.",
8
- "Remote mode uses a Cloudflare relay; stdio mode runs entirely on the local machine.",
9
- "File and command operations execute on the user's local runtime, not in the Worker.",
10
- "Relative paths use the configured workspace. Direct filesystem tools are workspace-scoped unless unrestricted paths are explicitly enabled.",
11
- "run_process avoids shell parsing but is not an OS sandbox. exec_command is only exposed in shell execution mode and has the local user's authority.",
12
- "Inspect before editing, prefer edit_file or apply_patch over whole-file replacement, and report commands that were run.",
13
- ].join("\n");
8
+ export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((value) => String(value))).join("\n");
9
+
10
+
11
+ export const DEFAULT_POLICY_PROFILE = "full";
12
+ export const DEFAULT_POLICY_REVISION = 2;
13
+
14
+ export const POLICY_PROFILES = Object.freeze({
15
+ review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
16
+ edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
17
+ agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
18
+ full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
19
+ });
20
+
21
+ const POLICY_ORIGINS = new Set(["default", "explicit", "custom", "migrated", "legacy-preserved"]);
22
+
23
+ export function policyProfile(name, origin = "explicit") {
24
+ const profile = String(name || "").trim().toLowerCase();
25
+ if (!POLICY_PROFILES[profile]) throw new Error(`unknown policy profile: ${profile}`);
26
+ return normalizePolicy({ ...POLICY_PROFILES[profile], origin, revision: DEFAULT_POLICY_REVISION });
27
+ }
14
28
 
15
29
  export function normalizePolicy(policy = {}) {
16
30
  const execMode = ["off", "direct", "shell"].includes(policy.execMode)
@@ -18,8 +32,12 @@ export function normalizePolicy(policy = {}) {
18
32
  : policy.allowExec === true
19
33
  ? "shell"
20
34
  : "off";
35
+ const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
36
+ const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
21
37
  return {
22
38
  profile: typeof policy.profile === "string" && policy.profile ? policy.profile : "custom",
39
+ origin,
40
+ revision,
23
41
  allowWrite: policy.allowWrite === true,
24
42
  allowExec: execMode !== "off",
25
43
  execMode,
@@ -44,9 +62,6 @@ export function allToolNames() {
44
62
  return catalog.map((tool) => tool.name);
45
63
  }
46
64
 
47
- export function findTool(name) {
48
- return catalog.find((tool) => tool.name === name) || null;
49
- }
50
65
 
51
66
  export function toolResult(value, isError = false) {
52
67
  const special = specialMcpResult(value);
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "machine-bridge-mcp",
3
+ "protocolVersion": "2025-11-25",
4
+ "supportedProtocolVersions": [
5
+ "2025-11-25",
6
+ "2025-06-18",
7
+ "2025-03-26"
8
+ ],
9
+ "instructions": [
10
+ "You are connected to a local workspace through machine-bridge-mcp.",
11
+ "Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
12
+ "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
13
+ "Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
14
+ "Use diagnose_runtime to distinguish a request that reached the daemon from local filesystem, process-spawn, shell, managed-job-storage, and resource failures. A tool call blocked before any response cannot be diagnosed by the server.",
15
+ "Never request or return secret-file contents when a local resource alias can be used. Resources are registered only through the local machine-mcp CLI and may be injected by path, stdin, or environment without entering MCP arguments.",
16
+ "If execution-class tools are unavailable but write access remains, use stage_job to persist a reviewed plan without running it; the operator can launch it locally with machine-mcp job approve JOB_ID.",
17
+ "For multi-step, remote, long-running, or cleanup-sensitive work, prefer one start_job call with argv steps, job-scoped temporary_files, and idempotent finally_steps. The detached runner continues after MCP disconnects; inspect it with read_job or the local machine-mcp job CLI.",
18
+ "Prefer sending remote shell programs through a process stdin rather than creating remote helper files. If temporary files are necessary, use {{temp:name}} or explicit finally_steps.",
19
+ "Managed-job resource redaction is defense in depth, not a guarantee against transformed or partial secret output. Use capture_output=discard for steps that may echo credentials.",
20
+ "Do not use shell encoding, renaming, or alternate tools to bypass host or platform safety policy.",
21
+ "run_process avoids shell parsing but is not an OS sandbox. exec_command is exposed only in shell mode and has the local user's authority.",
22
+ "Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
23
+ ]
24
+ }