machine-bridge-mcp 0.6.2 → 0.7.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Cross-client MCP bridge for canonical local automation, files, Git, SSH resources, managed jobs, and processes over stdio or an OAuth-protected remote relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {
8
- "node": ">=22.0.0"
8
+ "node": ">=26.0.0"
9
9
  },
10
10
  "bin": {
11
11
  "machine-bridge-mcp": "bin/machine-mcp.mjs",
@@ -20,11 +20,15 @@
20
20
  "src/shared",
21
21
  "src/worker/index.ts",
22
22
  "scripts/sync-worker-version.mjs",
23
+ "scripts/privacy-check.mjs",
24
+ "scripts/release-impact-check.mjs",
25
+ "scripts/network-retry.mjs",
23
26
  "wrangler.jsonc",
24
27
  "tsconfig.json",
25
28
  "README.md",
26
29
  "SECURITY.md",
27
30
  "CHANGELOG.md",
31
+ "CONTRIBUTING.md",
28
32
  "docs",
29
33
  "LICENSE"
30
34
  ],
@@ -35,12 +39,12 @@
35
39
  "version:sync": "node scripts/sync-worker-version.mjs",
36
40
  "version:check": "node scripts/sync-worker-version.mjs --check",
37
41
  "version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
38
- "prepack": "npm run version:check",
42
+ "prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
39
43
  "prepublishOnly": "npm run check && npm run version:check && npm run release:check",
40
44
  "worker:types": "wrangler types src/worker/worker-configuration.d.ts",
41
45
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
42
- "syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs",
43
- "check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
46
+ "syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs",
47
+ "check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run network-retry:test && npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
44
48
  "worker:dry-run": "wrangler deploy --dry-run",
45
49
  "worker:integration-test": "node tests/worker-integration-test.mjs",
46
50
  "release:check": "node scripts/github-release.mjs --check",
@@ -51,15 +55,21 @@
51
55
  "managed-jobs:test": "node tests/managed-jobs-test.mjs",
52
56
  "ssh-key:test": "node tests/ssh-key-test.mjs",
53
57
  "full-access:test": "node tests/full-access-test.mjs",
54
- "atomic-fs:test": "node tests/atomic-fs-test.mjs"
58
+ "atomic-fs:test": "node tests/atomic-fs-test.mjs",
59
+ "privacy:check": "node scripts/privacy-check.mjs",
60
+ "package:test": "node tests/package-test.mjs",
61
+ "release-impact:check": "node scripts/release-impact-check.mjs",
62
+ "release-impact:test": "node tests/release-impact-test.mjs",
63
+ "privacy:test": "node tests/privacy-test.mjs",
64
+ "network-retry:test": "node tests/network-retry-test.mjs"
55
65
  },
56
66
  "dependencies": {
57
67
  "wrangler": "4.110.0",
58
68
  "ws": "8.21.0"
59
69
  },
60
70
  "devDependencies": {
61
- "@types/node": "^22.20.1",
62
- "typescript": "^7.0.2"
71
+ "@types/node": "26.1.1",
72
+ "typescript": "7.0.2"
63
73
  },
64
74
  "keywords": [
65
75
  "mcp",
@@ -89,5 +99,18 @@
89
99
  "sharp@0.34.5": true,
90
100
  "workerd@1.20260708.1": true,
91
101
  "fsevents": false
102
+ },
103
+ "packageManager": "npm@12.0.1",
104
+ "devEngines": {
105
+ "runtime": {
106
+ "name": "node",
107
+ "version": ">=26.0.0",
108
+ "onFail": "error"
109
+ },
110
+ "packageManager": {
111
+ "name": "npm",
112
+ "version": ">=12.0.0",
113
+ "onFail": "error"
114
+ }
92
115
  }
93
116
  }
@@ -0,0 +1,47 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
4
+ const TRANSIENT_NETWORK_FAILURE = /(?:SSL_ERROR_SYSCALL|SSL_connect|TLS handshake timeout|Client network socket disconnected before secure TLS connection was established|unexpected EOF|unexpected eof while reading|\bEOF\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ECONNREFUSED|Could not resolve host|Failed to connect|connection (?:was )?reset|remote end hung up|unexpected disconnect|HTTP (?:429|5\d\d)|(?:502|503|504) (?:Bad Gateway|Service Unavailable|Gateway Timeout))/i;
5
+
6
+ export function runNetworkCommand(command, args, options = {}) {
7
+ const attempts = clampInteger(options.attempts, 3, 1, 5);
8
+ const baseDelayMs = clampInteger(options.baseDelayMs, 750, 0, 10_000);
9
+ const actualArgs = networkCommandArgs(command, args);
10
+ let result;
11
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
12
+ result = spawnSync(command, actualArgs, {
13
+ cwd: options.cwd,
14
+ encoding: "utf8",
15
+ stdio: "pipe",
16
+ env: options.env,
17
+ windowsHide: true,
18
+ });
19
+ result.attempts = attempt;
20
+ if (!isTransientNetworkFailure(result) || attempt === attempts) return result;
21
+ sleepSync(Math.min(baseDelayMs * attempt, 5_000));
22
+ }
23
+ return result;
24
+ }
25
+
26
+ export function networkCommandArgs(command, args) {
27
+ return command === "git" ? ["-c", "http.version=HTTP/1.1", ...args] : [...args];
28
+ }
29
+
30
+ export function isTransientNetworkFailure(result) {
31
+ if (result?.error) {
32
+ const code = String(result.error.code || "");
33
+ if (["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH", "ECONNREFUSED"].includes(code)) return true;
34
+ }
35
+ const detail = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}\n${result?.error?.message ?? ""}`;
36
+ return TRANSIENT_NETWORK_FAILURE.test(detail);
37
+ }
38
+
39
+ function sleepSync(milliseconds) {
40
+ if (milliseconds > 0) Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
41
+ }
42
+
43
+ function clampInteger(value, fallback, minimum, maximum) {
44
+ const parsed = Number.parseInt(String(value ?? ""), 10);
45
+ const number = Number.isFinite(parsed) ? parsed : fallback;
46
+ return Math.min(Math.max(number, minimum), maximum);
47
+ }
@@ -0,0 +1,177 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const selfPath = "scripts/privacy-check.mjs";
8
+ const candidates = collectCandidateFiles(root);
9
+ const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
10
+ const findings = [];
11
+
12
+ for (const relativePath of candidates) {
13
+ if (relativePath === ".privacy-denylist") continue;
14
+ scanDenylistPath(relativePath, denylist, findings);
15
+ const fullPath = path.join(root, relativePath);
16
+ let info;
17
+ try { info = lstatSync(fullPath); } catch { continue; }
18
+ if (info.isSymbolicLink()) {
19
+ findings.push({ path: relativePath, line: 1, rule: "symbolic link in publication surface" });
20
+ continue;
21
+ }
22
+ if (!info.isFile()) continue;
23
+ let buffer;
24
+ try { buffer = readFileSync(fullPath); } catch { continue; }
25
+ if (buffer.length > 5 * 1024 * 1024) {
26
+ findings.push({ path: relativePath, line: 1, rule: "file exceeds privacy scanner size limit and requires manual review" });
27
+ continue;
28
+ }
29
+ if (buffer.includes(0)) {
30
+ findings.push({ path: relativePath, line: 1, rule: "binary file in publication surface requires manual review" });
31
+ continue;
32
+ }
33
+ let text;
34
+ try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
35
+ findings.push({ path: relativePath, line: 1, rule: "non-UTF-8 file in publication surface requires manual review" });
36
+ continue;
37
+ }
38
+ if (relativePath !== selfPath) scanBuiltIn(relativePath, text, findings);
39
+ scanDenylist(relativePath, text, denylist, findings);
40
+ }
41
+
42
+ if (findings.length) {
43
+ for (const finding of findings.slice(0, 100)) {
44
+ process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
45
+ }
46
+ if (findings.length > 100) process.stderr.write(`... ${findings.length - 100} additional findings omitted\n`);
47
+ process.stderr.write("Privacy check failed. Replace private identifiers with synthetic examples or review the local denylist.\n");
48
+ process.exit(1);
49
+ }
50
+
51
+ process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries)\n`);
52
+
53
+ function collectCandidateFiles(directory) {
54
+ try {
55
+ return execFileSync("git", ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
56
+ encoding: "buffer",
57
+ maxBuffer: 32 * 1024 * 1024,
58
+ stdio: ["ignore", "pipe", "ignore"],
59
+ }).toString("utf8").split("\0").filter(Boolean).sort();
60
+ } catch {
61
+ const excluded = new Set([".git", ".wrangler", "node_modules"]);
62
+ const files = [];
63
+ const stack = [""];
64
+ while (stack.length) {
65
+ const relative = stack.pop();
66
+ const absolute = path.join(directory, relative);
67
+ let entries;
68
+ try { entries = readdirSync(absolute, { withFileTypes: true }); } catch { continue; }
69
+ for (const entry of entries) {
70
+ if (excluded.has(entry.name) || entry.name === ".privacy-denylist") continue;
71
+ const child = relative ? path.join(relative, entry.name) : entry.name;
72
+ if (entry.isDirectory()) stack.push(child);
73
+ else if (entry.isFile() || entry.isSymbolicLink()) files.push(child.split(path.sep).join("/"));
74
+ }
75
+ }
76
+ return files.sort();
77
+ }
78
+ }
79
+
80
+ function redactReportPath(relativePath, entries) {
81
+ let shown = String(relativePath);
82
+ for (const entry of entries) {
83
+ const expression = new RegExp(escapeRegExp(entry), "gi");
84
+ shown = shown.replace(expression, "<private>");
85
+ }
86
+ return shown;
87
+ }
88
+
89
+ function escapeRegExp(value) {
90
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ }
92
+
93
+ function scanBuiltIn(relativePath, text, out) {
94
+ const rules = [
95
+ ["private key material", /-----BEGIN\s+(?:OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----/g],
96
+ ["AWS access key", /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g],
97
+ ["GitHub access token", /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g],
98
+ ["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
99
+ ["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
100
+ ["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
101
+ ];
102
+ for (const [rule, expression] of rules) {
103
+ for (const match of text.matchAll(expression)) {
104
+ if (rule.includes("home path") && isSyntheticUser(String(match[1] || ""))) continue;
105
+ out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
106
+ }
107
+ }
108
+ const sshLike = /\b[A-Za-z_][A-Za-z0-9._-]*@([A-Za-z0-9.-]+)\b/g;
109
+ for (const match of text.matchAll(sshLike)) {
110
+ if (isReservedExampleHost(match[1]) || !hasNearbySshContext(text, match.index || 0)) continue;
111
+ out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "non-example SSH user@host identifier" });
112
+ }
113
+ }
114
+
115
+ function scanDenylistPath(relativePath, entries, out) {
116
+ const lower = relativePath.toLocaleLowerCase("en-US");
117
+ for (const entry of entries) {
118
+ if (lower.includes(entry)) out.push({ path: relativePath, line: 1, rule: "local private-identifier denylist match in file path" });
119
+ }
120
+ }
121
+
122
+ function scanDenylist(relativePath, text, entries, out) {
123
+ const lower = text.toLocaleLowerCase("en-US");
124
+ for (const entry of entries) {
125
+ let offset = 0;
126
+ while ((offset = lower.indexOf(entry, offset)) !== -1) {
127
+ out.push({ path: relativePath, line: lineNumber(text, offset), rule: "local private-identifier denylist match" });
128
+ offset += Math.max(1, entry.length);
129
+ }
130
+ }
131
+ }
132
+
133
+ function loadDenylist(file) {
134
+ if (!existsSync(file)) return [];
135
+ let text;
136
+ try { text = readFileSync(file, "utf8"); } catch { return []; }
137
+ return [...new Set(text.split(/\r?\n/)
138
+ .map((line) => line.trim())
139
+ .filter((line) => line && !line.startsWith("#") && line.length >= 3)
140
+ .map((line) => line.toLocaleLowerCase("en-US")))];
141
+ }
142
+
143
+ function isSyntheticUser(value) {
144
+ return /^(?:user|username|example|test|name|home|runner|workspace|tmp|private|path|package)$/i.test(value)
145
+ || /^<[^>]+>$/.test(value);
146
+ }
147
+
148
+ function hasNearbySshContext(text, offset) {
149
+ const lineStart = Math.max(0, text.lastIndexOf("\n", offset - 1));
150
+ let start = lineStart;
151
+ for (let count = 0; count < 4 && start > 0; count += 1) start = Math.max(0, text.lastIndexOf("\n", start - 1));
152
+ let end = offset;
153
+ for (let count = 0; count < 4 && end < text.length; count += 1) {
154
+ const next = text.indexOf("\n", end + 1);
155
+ end = next === -1 ? text.length : next;
156
+ }
157
+ return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
158
+ }
159
+
160
+ function isReservedExampleHost(host) {
161
+ const value = String(host || "").toLowerCase().replace(/\.$/, "");
162
+ return value === "localhost"
163
+ || value === "127.0.0.1"
164
+ || value === "::1"
165
+ || value === "example"
166
+ || value.endsWith(".example")
167
+ || value.endsWith(".example.com")
168
+ || value.endsWith(".example.net")
169
+ || value.endsWith(".example.org")
170
+ || value.endsWith(".invalid");
171
+ }
172
+
173
+ function lineNumber(text, offset) {
174
+ let line = 1;
175
+ for (let index = 0; index < offset; index += 1) if (text.charCodeAt(index) === 10) line += 1;
176
+ return line;
177
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ const VERSION_TAG = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
7
+
8
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
+ const current = parseVersion(String(pkg.version || ""));
10
+ if (!current) fail("package.json contains an invalid version");
11
+
12
+ const tags = git(["tag", "--merged", "HEAD", "--sort=-version:refname", "--list", "v[0-9]*"])
13
+ .split("\n")
14
+ .map((value) => value.trim())
15
+ .filter((value) => VERSION_TAG.test(value));
16
+ if (!tags.length) {
17
+ process.stderr.write("release impact check skipped: no reachable version tag\n");
18
+ process.exit(0);
19
+ }
20
+
21
+ const latestTag = tags[0];
22
+ const latest = parseVersion(latestTag.slice(1));
23
+ const changed = new Set([
24
+ ...lines(git(["diff", "--name-only", latestTag, "--"])),
25
+ ...lines(git(["ls-files", "--others", "--exclude-standard"])),
26
+ ]);
27
+ const relevant = [...changed].sort();
28
+
29
+ if (!relevant.length) {
30
+ process.stderr.write(`release impact check ok: no release-relevant changes since ${latestTag}\n`);
31
+ process.exit(0);
32
+ }
33
+ if (compareVersions(current, latest) <= 0) {
34
+ fail(`release-relevant changes exist since ${latestTag}, but package.json is still ${pkg.version}; bump the npm version and add a CHANGELOG section before merging`);
35
+ }
36
+
37
+ const changelog = readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
38
+ const heading = new RegExp(`^## ${escapeRegExp(pkg.version)}(?:\\s+-[^\\n]*)?$`, "m");
39
+ if (!heading.test(changelog)) fail(`CHANGELOG.md has no section for ${pkg.version}`);
40
+
41
+ process.stderr.write(`release impact check ok: ${relevant.length} release-relevant file(s) since ${latestTag}; next npm version ${pkg.version}\n`);
42
+
43
+ function git(args) {
44
+ try {
45
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
46
+ } catch (error) {
47
+ const detail = String(error?.stderr || error?.message || error).trim();
48
+ fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
49
+ }
50
+ }
51
+
52
+ function lines(value) {
53
+ return value ? value.split("\n").map((item) => item.trim()).filter(Boolean) : [];
54
+ }
55
+
56
+ function parseVersion(value) {
57
+ const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
58
+ return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] || "" } : null;
59
+ }
60
+
61
+ function compareVersions(left, right) {
62
+ for (const key of ["major", "minor", "patch"]) {
63
+ if (left[key] !== right[key]) return left[key] - right[key];
64
+ }
65
+ if (left.prerelease === right.prerelease) return 0;
66
+ if (!left.prerelease) return 1;
67
+ if (!right.prerelease) return -1;
68
+ return left.prerelease.localeCompare(right.prerelease);
69
+ }
70
+
71
+ function escapeRegExp(value) {
72
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
+ }
74
+
75
+ function fail(message) {
76
+ process.stderr.write(`release impact check failed: ${message}\n`);
77
+ process.exit(1);
78
+ }
package/src/local/cli.mjs CHANGED
@@ -40,7 +40,7 @@ const BOOLEAN_OPTIONS = new Set([
40
40
  "help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
41
41
  "daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
42
42
  "printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
43
- "yes", "keepWorker", "allowInsecurePermissions",
43
+ "yes", "keepWorker", "allowInsecurePermissions", "showPaths",
44
44
  ]);
45
45
  const VALUE_OPTIONS = new Set([
46
46
  "workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
@@ -92,7 +92,7 @@ const COMMAND_OPTIONS = {
92
92
  workspace: new Set(["workspace", "stateDir"]),
93
93
  service: new Set(["workspace", "stateDir", "quiet"]),
94
94
  autostart: new Set(["workspace", "stateDir", "quiet"]),
95
- resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "json"]),
95
+ resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
96
96
  job: new Set(["workspace", "stateDir", "json", "yes"]),
97
97
  uninstall: new Set(["stateDir", "keepWorker", "yes"]),
98
98
  };
@@ -478,10 +478,19 @@ async function resourceCommand(args) {
478
478
  state.resources ||= {};
479
479
 
480
480
  if (action === "list") {
481
- const resources = publicResourceRegistry(state.resources);
482
- if (args.json) console.log(JSON.stringify({ workspace, resources }, null, 2));
481
+ const includePaths = args.showPaths === true;
482
+ const resources = publicResourceRegistry(state.resources, { includePaths });
483
+ if (args.json) console.log(JSON.stringify({
484
+ workspace: includePaths ? workspace : "<local-workspace>",
485
+ paths_exposed: includePaths,
486
+ resources,
487
+ }, null, 2));
483
488
  else if (!Object.keys(resources).length) console.log("No local resources registered.");
484
- else for (const [name, value] of Object.entries(resources)) console.log(`${name} ${value.path} ${value.mode || "n/a"} ${value.size ?? "n/a"} bytes`);
489
+ else for (const [name, value] of Object.entries(resources)) {
490
+ const fields = [name, value.mode || "n/a", `${value.size ?? "n/a"} bytes`];
491
+ if (includePaths) fields.splice(1, 0, value.path);
492
+ console.log(fields.join("\t"));
493
+ }
485
494
  return;
486
495
  }
487
496
 
@@ -500,11 +509,14 @@ async function resourceCommand(args) {
500
509
  }
501
510
  latest.resources[name] = inspected;
502
511
  saveState(latest);
503
- const result = { name, ...inspected, contents_exposed: false, available_to_new_jobs_immediately: true };
512
+ const result = publicResourceInspection(name, inspected, {
513
+ includePath: args.showPaths === true,
514
+ available_to_new_jobs_immediately: true,
515
+ });
504
516
  if (args.json) console.log(JSON.stringify(result, null, 2));
505
517
  else {
506
518
  console.log(`Registered local resource: ${name}`);
507
- console.log(`Path: ${inspected.path}`);
519
+ if (args.showPaths === true) console.log(`Path: ${inspected.path}`);
508
520
  console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
509
521
  console.log("The resource is available to newly submitted managed jobs immediately.");
510
522
  }
@@ -526,11 +538,10 @@ async function resourceCommand(args) {
526
538
  targetPath: requestedPath,
527
539
  comment: `machine-mcp:${name}`,
528
540
  });
541
+ const includePaths = args.showPaths === true;
529
542
  const result = {
530
543
  name: key.name,
531
544
  created: key.created,
532
- private_key_path: key.privateKeyPath,
533
- public_key_path: key.publicKeyPath,
534
545
  fingerprint: key.fingerprint,
535
546
  key_type: key.keyType,
536
547
  private_mode: key.privateMode,
@@ -538,12 +549,16 @@ async function resourceCommand(args) {
538
549
  private_key_content_exposed: key.privateKeyContentExposed,
539
550
  registered: key.registered,
540
551
  available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
552
+ paths_exposed: includePaths,
553
+ ...(includePaths ? { private_key_path: key.privateKeyPath, public_key_path: key.publicKeyPath } : {}),
541
554
  };
542
555
  if (args.json) console.log(JSON.stringify(result, null, 2));
543
556
  else {
544
557
  console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
545
- console.log(`Private key: ${key.privateKeyPath}`);
546
- console.log(`Public key: ${key.publicKeyPath}`);
558
+ if (includePaths) {
559
+ console.log(`Private key: ${key.privateKeyPath}`);
560
+ console.log(`Public key: ${key.publicKeyPath}`);
561
+ }
547
562
  console.log(`Fingerprint: ${key.fingerprint}`);
548
563
  console.log("Private key content was not printed or sent through MCP.");
549
564
  }
@@ -577,15 +592,33 @@ async function resourceCommand(args) {
577
592
  const resource = state.resources[name];
578
593
  if (!resource) throw new Error(`local resource is not registered: ${name}`);
579
594
  const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
580
- const result = { name, ...inspected, contents_exposed: false };
595
+ const result = publicResourceInspection(name, inspected, { includePath: args.showPaths === true });
581
596
  if (args.json) console.log(JSON.stringify(result, null, 2));
582
- else console.log(`${name}: available (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
597
+ else {
598
+ const pathDetail = args.showPaths === true ? ` at ${inspected.path}` : "";
599
+ console.log(`${name}: available${pathDetail} (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
600
+ }
583
601
  return;
584
602
  }
585
603
 
586
604
  throw new Error(`Unknown resource action: ${action}`);
587
605
  }
588
606
 
607
+ function publicResourceInspection(name, inspected, { includePath = false, ...extra } = {}) {
608
+ return {
609
+ name,
610
+ kind: inspected.kind,
611
+ size: inspected.size ?? null,
612
+ mode: inspected.mode ?? null,
613
+ updated_at: inspected.updatedAt ?? null,
614
+ allow_insecure_permissions: inspected.allowInsecurePermissions === true,
615
+ ...extra,
616
+ paths_exposed: includePath,
617
+ contents_exposed: false,
618
+ ...(includePath ? { path: inspected.path } : {}),
619
+ };
620
+ }
621
+
589
622
  async function jobCommand(args) {
590
623
  const action = String(args._[0] || "list").toLowerCase();
591
624
  const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
@@ -915,7 +948,7 @@ async function statusCommand(args) {
915
948
  async function doctorCommand(args) {
916
949
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
917
950
  const checks = [];
918
- checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 22, detail: process.version });
951
+ checks.push({ name: "node", ok: isSupportedNodeVersion(), detail: process.version });
919
952
  const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true });
920
953
  checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
921
954
  const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
@@ -1218,9 +1251,13 @@ function validateWorkerName(value) {
1218
1251
  return name;
1219
1252
  }
1220
1253
 
1254
+ export function isSupportedNodeVersion(version = process.versions.node) {
1255
+ const major = Number(String(version || "").split(".")[0]);
1256
+ return Number.isInteger(major) && major >= 26;
1257
+ }
1258
+
1221
1259
  function assertNodeVersion() {
1222
- const major = Number(process.versions.node.split(".")[0]);
1223
- if (major < 22) throw new Error(`Node.js >=22 is required; current ${process.version}`);
1260
+ if (!isSupportedNodeVersion()) throw new Error(`Node.js >=26 is required; current ${process.version}`);
1224
1261
  }
1225
1262
 
1226
1263
  function usage() {
@@ -1274,6 +1311,7 @@ Start options:
1274
1311
  --quiet Alias for --log-level error
1275
1312
  --allow-insecure-permissions
1276
1313
  Permit resource registration when a file is group/other-readable
1314
+ --show-paths Include local absolute paths in resource command output
1277
1315
 
1278
1316
  Uninstall options:
1279
1317
  --keep-worker Do not delete deployed Worker(s) during uninstall