machine-bridge-mcp 0.4.2 → 0.5.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.
- package/CHANGELOG.md +24 -0
- package/README.md +17 -9
- package/SECURITY.md +6 -4
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/CLIENTS.md +6 -0
- package/docs/LOGGING.md +80 -0
- package/docs/OPERATIONS.md +25 -19
- package/docs/TESTING.md +7 -4
- package/package.json +4 -4
- package/src/local/cli.mjs +155 -77
- package/src/local/daemon.mjs +58 -41
- package/src/local/log.mjs +56 -15
- package/src/local/service.mjs +40 -21
- package/src/local/state.mjs +45 -20
- package/src/local/stdio.mjs +13 -37
- package/src/local/tools.mjs +28 -13
- package/src/shared/server-metadata.json +17 -0
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/index.ts +13 -11
package/src/local/cli.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import path, { resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import readline from "node:readline/promises";
|
|
6
6
|
import { LocalDaemon } from "./daemon.mjs";
|
|
7
7
|
import { runStdioServer } from "./stdio.mjs";
|
|
8
|
-
import { normalizePolicy } from "./tools.mjs";
|
|
9
|
-
import { createLogger, sanitizeLogText } from "./log.mjs";
|
|
8
|
+
import { DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile } from "./tools.mjs";
|
|
9
|
+
import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
|
|
10
10
|
import { runWrangler } from "./shell.mjs";
|
|
11
11
|
import {
|
|
12
12
|
acquireDaemonLock,
|
|
@@ -37,10 +37,10 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
37
37
|
"help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
38
38
|
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
|
|
39
39
|
"printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
40
|
-
"yes", "keepWorker",
|
|
40
|
+
"yes", "keepWorker",
|
|
41
41
|
]);
|
|
42
42
|
const VALUE_OPTIONS = new Set([
|
|
43
|
-
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "
|
|
43
|
+
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
46
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -51,6 +51,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
51
51
|
if (command === "api") throw removedLocalApiError();
|
|
52
52
|
validateCommandOptions(command, args);
|
|
53
53
|
validatePositionals(command, args);
|
|
54
|
+
validateLoggingOptions(args);
|
|
54
55
|
|
|
55
56
|
switch (command) {
|
|
56
57
|
case "start": return startCommand(args);
|
|
@@ -72,16 +73,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
72
73
|
|
|
73
74
|
const COMMAND_OPTIONS = {
|
|
74
75
|
start: new Set([
|
|
75
|
-
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
76
|
+
"workspace", "stateDir", "workerName", "quiet", "json", "verbose", "logLevel", "rotateSecrets", "forceWorker",
|
|
76
77
|
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials", "printCredentials",
|
|
77
78
|
"profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
78
|
-
"noApi", "api", "apiPort", "apiHost", "apiKey", "rotateApiKey", "apiModel", "port",
|
|
79
79
|
]),
|
|
80
|
-
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose"]),
|
|
80
|
+
stdio: new Set(["workspace", "stateDir", "profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths", "verbose", "quiet", "logLevel"]),
|
|
81
81
|
"client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
|
|
82
82
|
status: new Set(["workspace", "stateDir"]),
|
|
83
83
|
doctor: new Set(["workspace", "stateDir"]),
|
|
84
|
-
"rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "quiet"]),
|
|
84
|
+
"rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
|
|
85
85
|
workspace: new Set(["workspace", "stateDir"]),
|
|
86
86
|
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
87
87
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
@@ -97,6 +97,21 @@ export function validateCommandOptions(command, args) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function validateLoggingOptions(args = {}) {
|
|
101
|
+
if (args.quiet && args.verbose) throw new Error("--quiet and --verbose cannot be used together");
|
|
102
|
+
if (args.logLevel !== undefined && (args.quiet || args.verbose)) {
|
|
103
|
+
throw new Error("--log-level cannot be combined with --quiet or --verbose");
|
|
104
|
+
}
|
|
105
|
+
if (args.logLevel !== undefined) normalizeLogLevel(args.logLevel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function effectiveLogLevel(args = {}) {
|
|
109
|
+
if (args.logLevel !== undefined) return normalizeLogLevel(args.logLevel);
|
|
110
|
+
if (args.quiet) return "error";
|
|
111
|
+
if (args.verbose) return "debug";
|
|
112
|
+
return "info";
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
function toKebab(value) {
|
|
101
116
|
return String(value).replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
102
117
|
}
|
|
@@ -256,8 +271,7 @@ async function confirm(prompt, assumeYes = false) {
|
|
|
256
271
|
|
|
257
272
|
async function startCommand(args) {
|
|
258
273
|
assertNodeVersion();
|
|
259
|
-
|
|
260
|
-
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "cli" });
|
|
274
|
+
const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "cli" });
|
|
261
275
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
262
276
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
263
277
|
const startupLock = acquireStartupLock(state);
|
|
@@ -273,7 +287,7 @@ async function startCommand(args) {
|
|
|
273
287
|
} else {
|
|
274
288
|
// Stop an installed service before acquiring the runtime lock. If a
|
|
275
289
|
// foreground daemon owns the lock, no new policy or secret state is saved.
|
|
276
|
-
await stopAutostartBestEffort(
|
|
290
|
+
await stopAutostartBestEffort(logger);
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
const lock = acquireDaemonLock(state);
|
|
@@ -281,7 +295,7 @@ async function startCommand(args) {
|
|
|
281
295
|
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
282
296
|
logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
|
|
283
297
|
if (args.json) printStartJson(state, {
|
|
284
|
-
|
|
298
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
285
299
|
requestedChangesApplied: false,
|
|
286
300
|
notice: "local daemon already running; requested changes were not applied",
|
|
287
301
|
});
|
|
@@ -289,6 +303,7 @@ async function startCommand(args) {
|
|
|
289
303
|
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
290
304
|
includeCredentials: Boolean(args.printMcpCredentials || args.printCredentials),
|
|
291
305
|
quiet: Boolean(args.quiet),
|
|
306
|
+
verbose: Boolean(args.verbose),
|
|
292
307
|
});
|
|
293
308
|
return;
|
|
294
309
|
}
|
|
@@ -299,7 +314,9 @@ async function startCommand(args) {
|
|
|
299
314
|
const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
|
|
300
315
|
const workerName = validateWorkerName(args.workerName);
|
|
301
316
|
ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName });
|
|
317
|
+
const previousPolicyOrigin = state.policy?.origin;
|
|
302
318
|
state.policy = resolvePolicy(args, state.policy);
|
|
319
|
+
const policyMigrated = !previousPolicyOrigin && state.policy.origin === "migrated";
|
|
303
320
|
state.policy.updatedAt = new Date().toISOString();
|
|
304
321
|
saveState(state);
|
|
305
322
|
|
|
@@ -307,10 +324,10 @@ async function startCommand(args) {
|
|
|
307
324
|
else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
|
|
308
325
|
|
|
309
326
|
const mcpConnectionChanged = previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl;
|
|
310
|
-
const shouldPrintMcpCredentials = Boolean(args.
|
|
327
|
+
const shouldPrintMcpCredentials = Boolean(args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
|
|
311
328
|
|
|
312
329
|
if (!args.daemonOnly && !args.noAutostart) {
|
|
313
|
-
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1] });
|
|
330
|
+
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
|
|
314
331
|
}
|
|
315
332
|
|
|
316
333
|
daemon = new LocalDaemon({
|
|
@@ -318,7 +335,7 @@ async function startCommand(args) {
|
|
|
318
335
|
secret: state.worker.daemonSecret,
|
|
319
336
|
workspace,
|
|
320
337
|
policy: state.policy,
|
|
321
|
-
logger: createLogger({
|
|
338
|
+
logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
|
|
322
339
|
onSuperseded: () => {
|
|
323
340
|
logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
|
|
324
341
|
lock.release();
|
|
@@ -327,13 +344,18 @@ async function startCommand(args) {
|
|
|
327
344
|
});
|
|
328
345
|
|
|
329
346
|
const waitForConnect = daemon.start();
|
|
330
|
-
await waitForConnectWithNotice(waitForConnect, 20_000,
|
|
331
|
-
if (args.json) printStartJson(state, {
|
|
347
|
+
await waitForConnectWithNotice(waitForConnect, 20_000, logger);
|
|
348
|
+
if (args.json) printStartJson(state, {
|
|
349
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
350
|
+
notice: policyMigrated ? "legacy implicit policy migrated to full access" : "",
|
|
351
|
+
});
|
|
332
352
|
else {
|
|
333
353
|
printMcpConnection(state, {
|
|
334
354
|
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
335
355
|
includeCredentials: shouldPrintMcpCredentials,
|
|
336
356
|
quiet: Boolean(args.quiet),
|
|
357
|
+
verbose: Boolean(args.verbose),
|
|
358
|
+
policyMigrated,
|
|
337
359
|
});
|
|
338
360
|
}
|
|
339
361
|
keepProcessAlive({ daemon, lock, logger });
|
|
@@ -347,13 +369,6 @@ async function startCommand(args) {
|
|
|
347
369
|
}
|
|
348
370
|
}
|
|
349
371
|
|
|
350
|
-
const POLICY_PROFILES = Object.freeze({
|
|
351
|
-
review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
352
|
-
edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
353
|
-
agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
354
|
-
full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
|
|
355
|
-
});
|
|
356
|
-
|
|
357
372
|
export function resolvePolicy(args = {}, stored = {}) {
|
|
358
373
|
const hasStored = stored && typeof stored === "object" && (
|
|
359
374
|
typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
|
|
@@ -361,14 +376,15 @@ export function resolvePolicy(args = {}, stored = {}) {
|
|
|
361
376
|
const explicitKeys = ["profile", "execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
|
|
362
377
|
const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
|
|
363
378
|
let base;
|
|
379
|
+
|
|
364
380
|
if (args.profile !== undefined) {
|
|
365
381
|
const profile = String(args.profile).trim().toLowerCase();
|
|
366
382
|
if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
367
|
-
base =
|
|
383
|
+
base = policyProfile(profile, "explicit");
|
|
368
384
|
} else if (hasStored) {
|
|
369
|
-
base =
|
|
385
|
+
base = migrateLegacyPolicy(stored);
|
|
370
386
|
} else {
|
|
371
|
-
base =
|
|
387
|
+
base = policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
372
388
|
}
|
|
373
389
|
|
|
374
390
|
if (!hasExplicit) return normalizePolicy(base);
|
|
@@ -388,16 +404,42 @@ export function resolvePolicy(args = {}, stored = {}) {
|
|
|
388
404
|
if (args.absolutePaths === true) base.exposeAbsolutePaths = true;
|
|
389
405
|
if (args.absolutePaths === false) base.exposeAbsolutePaths = false;
|
|
390
406
|
const overrideKeys = ["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
|
|
391
|
-
if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key)))
|
|
407
|
+
if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
|
|
408
|
+
base.profile = "custom";
|
|
409
|
+
base.origin = "custom";
|
|
410
|
+
base.revision = DEFAULT_POLICY_REVISION;
|
|
411
|
+
}
|
|
392
412
|
return normalizePolicy(base);
|
|
393
413
|
}
|
|
394
414
|
|
|
415
|
+
function migrateLegacyPolicy(stored = {}) {
|
|
416
|
+
if (stored.origin === "default" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
417
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
418
|
+
}
|
|
419
|
+
if (stored.origin === "migrated" && Number(stored.revision || 0) < DEFAULT_POLICY_REVISION) {
|
|
420
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "migrated");
|
|
421
|
+
}
|
|
422
|
+
if (stored.origin) return normalizePolicy(stored);
|
|
423
|
+
const normalized = normalizePolicy(stored);
|
|
424
|
+
const looksLikeLegacyImplicitDefault = (
|
|
425
|
+
normalized.profile === "custom" &&
|
|
426
|
+
normalized.allowWrite === true &&
|
|
427
|
+
normalized.execMode === "shell" &&
|
|
428
|
+
normalized.unrestrictedPaths === false &&
|
|
429
|
+
normalized.minimalEnv === true &&
|
|
430
|
+
normalized.exposeAbsolutePaths === false
|
|
431
|
+
);
|
|
432
|
+
if (looksLikeLegacyImplicitDefault) return policyProfile("full", "migrated");
|
|
433
|
+
return normalizePolicy({ ...normalized, origin: "legacy-preserved", revision: DEFAULT_POLICY_REVISION });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
395
437
|
async function stdioCommand(args) {
|
|
396
438
|
assertNodeVersion();
|
|
397
439
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
398
440
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
399
441
|
const policy = resolvePolicy(args, state.policy);
|
|
400
|
-
await runStdioServer({ workspace, policy,
|
|
442
|
+
await runStdioServer({ workspace, policy, logLevel: effectiveLogLevel(args) });
|
|
401
443
|
}
|
|
402
444
|
|
|
403
445
|
async function clientConfigCommand(args) {
|
|
@@ -426,17 +468,13 @@ async function clientConfigCommand(args) {
|
|
|
426
468
|
}
|
|
427
469
|
}
|
|
428
470
|
|
|
429
|
-
function assertNoRemovedLocalApiOptions(args) {
|
|
430
|
-
const removed = ["api", "noApi", "apiPort", "apiHost", "apiKey", "rotateApiKey", "apiModel", "port"].filter((key) => args[key] !== undefined);
|
|
431
|
-
if (removed.length) throw removedLocalApiError();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
471
|
function removedLocalApiError() {
|
|
435
472
|
return new Error("Local /v1 API support has been removed. Use the printed Remote MCP Server URL/password with an MCP client instead.");
|
|
436
473
|
}
|
|
437
474
|
|
|
475
|
+
|
|
438
476
|
async function ensureWorker(state, args) {
|
|
439
|
-
const logger = createLogger({
|
|
477
|
+
const logger = createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "worker" });
|
|
440
478
|
const desiredHash = workerDeployHash(state);
|
|
441
479
|
const expectedVersion = currentPackageVersion();
|
|
442
480
|
const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
|
|
@@ -487,7 +525,7 @@ async function withSecretsFile(state, callback) {
|
|
|
487
525
|
const dir = state.paths.profileDir;
|
|
488
526
|
ensureOwnerOnlyDir(dir);
|
|
489
527
|
cleanupStaleSecretFiles(dir);
|
|
490
|
-
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}.json`);
|
|
528
|
+
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
|
|
491
529
|
const payload = {
|
|
492
530
|
MCP_OAUTH_PASSWORD: state.worker.oauthPassword,
|
|
493
531
|
DAEMON_SHARED_SECRET: state.worker.daemonSecret,
|
|
@@ -504,10 +542,14 @@ async function withSecretsFile(state, callback) {
|
|
|
504
542
|
|
|
505
543
|
function cleanupStaleSecretFiles(dir) {
|
|
506
544
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
507
|
-
if (!entry.isFile()
|
|
545
|
+
if (!entry.isFile()) continue;
|
|
546
|
+
const match = /^worker-secrets-(\d+)-(\d+)(?:-[a-f0-9]+)?\.json$/.exec(entry.name);
|
|
547
|
+
if (!match) continue;
|
|
508
548
|
const file = resolve(dir, entry.name);
|
|
509
549
|
try {
|
|
510
|
-
|
|
550
|
+
const pid = Number(match[1]);
|
|
551
|
+
const ageMs = Date.now() - statSync(file).mtimeMs;
|
|
552
|
+
if (!isPidAlive(pid) || ageMs > 60 * 60 * 1000) unlinkSync(file);
|
|
511
553
|
} catch {}
|
|
512
554
|
}
|
|
513
555
|
}
|
|
@@ -562,10 +604,18 @@ async function workerHealth(workerUrl, expectedVersion = currentPackageVersion()
|
|
|
562
604
|
if (body?.version !== expectedVersion) return { ok: false, error: `version_mismatch:${body?.version || "unknown"}!=${expectedVersion}` };
|
|
563
605
|
return { ok: true, version: body.version };
|
|
564
606
|
} catch (error) {
|
|
565
|
-
return { ok: false, error: error
|
|
607
|
+
return { ok: false, error: workerHealthError(error) };
|
|
566
608
|
}
|
|
567
609
|
}
|
|
568
610
|
|
|
611
|
+
function workerHealthError(error) {
|
|
612
|
+
const message = String(error?.message || error || "");
|
|
613
|
+
if (/timeout|aborted/i.test(message)) return "timeout";
|
|
614
|
+
if (/certificate|TLS|SSL/i.test(message)) return "tls_error";
|
|
615
|
+
if (/fetch failed|network|ECONN|ENOTFOUND|EAI_AGAIN/i.test(message)) return "network_error";
|
|
616
|
+
return "request_failed";
|
|
617
|
+
}
|
|
618
|
+
|
|
569
619
|
async function retryHealth(workerUrl, expectedVersion, attempts) {
|
|
570
620
|
let last = { ok: false, error: "not_checked" };
|
|
571
621
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -583,23 +633,22 @@ function extractWorkerUrl(text = "") {
|
|
|
583
633
|
return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
|
|
584
634
|
}
|
|
585
635
|
|
|
586
|
-
async function waitForConnectWithNotice(promise, timeoutMs,
|
|
636
|
+
async function waitForConnectWithNotice(promise, timeoutMs, logger) {
|
|
587
637
|
let timeout;
|
|
588
638
|
const timed = new Promise(resolvePromise => {
|
|
589
639
|
timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
|
|
590
640
|
});
|
|
591
641
|
const result = await Promise.race([promise.then(() => "connected"), timed]);
|
|
592
642
|
clearTimeout(timeout);
|
|
593
|
-
if (result === "timeout")
|
|
643
|
+
if (result === "timeout") logger.warn("Still connecting; the process will keep retrying");
|
|
594
644
|
}
|
|
595
645
|
|
|
596
646
|
|
|
597
|
-
function printStartJson(state, {
|
|
598
|
-
const mcpPassword = noPrintCredentials ? previewSecret(state.worker.oauthPassword) : state.worker.oauthPassword;
|
|
647
|
+
function printStartJson(state, { showCredentials = false, requestedChangesApplied = true, notice = "" } = {}) {
|
|
599
648
|
createLogger({ component: "ready" }).json({
|
|
600
649
|
mcp: {
|
|
601
650
|
server_url: state.worker.mcpServerUrl,
|
|
602
|
-
connection_password:
|
|
651
|
+
connection_password: showCredentials ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword),
|
|
603
652
|
worker_url: state.worker.url,
|
|
604
653
|
worker_name: state.worker.name,
|
|
605
654
|
},
|
|
@@ -611,36 +660,45 @@ function printStartJson(state, { noPrintCredentials = false, requestedChangesApp
|
|
|
611
660
|
});
|
|
612
661
|
}
|
|
613
662
|
|
|
614
|
-
function printMcpConnection(state, {
|
|
615
|
-
|
|
663
|
+
function printMcpConnection(state, {
|
|
664
|
+
noPrintCredentials = false,
|
|
665
|
+
includeCredentials = false,
|
|
666
|
+
quiet = false,
|
|
667
|
+
verbose = false,
|
|
668
|
+
policyMigrated = false,
|
|
669
|
+
} = {}) {
|
|
670
|
+
const logger = createLogger({ component: "ready", quiet, level: quiet ? "error" : verbose ? "debug" : "info" });
|
|
616
671
|
const payload = {
|
|
617
672
|
mcp_server_url: state.worker.mcpServerUrl,
|
|
618
673
|
mcp_connection_password: state.worker.oauthPassword,
|
|
619
|
-
worker_url: state.worker.url,
|
|
620
|
-
worker_name: state.worker.name,
|
|
621
674
|
workspace: state.workspace.path,
|
|
622
675
|
state_path: state.paths.statePath,
|
|
623
676
|
policy: state.policy,
|
|
624
677
|
};
|
|
625
|
-
if (json) {
|
|
626
|
-
const safePayload = noPrintCredentials ? { ...payload, mcp_connection_password: previewSecret(payload.mcp_connection_password) } : payload;
|
|
627
|
-
logger.json(safePayload);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
678
|
if (includeCredentials) {
|
|
631
679
|
logger.success("Remote MCP bridge is ready; save these connection details if your ChatGPT app needs to reconnect");
|
|
632
680
|
logger.plain(` MCP Server URL: ${payload.mcp_server_url}`);
|
|
633
681
|
if (!noPrintCredentials) logger.plain(` MCP connection password: ${payload.mcp_connection_password}`);
|
|
634
682
|
else logger.plain(` MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
|
|
635
683
|
} else {
|
|
636
|
-
logger.success("Remote MCP bridge is ready
|
|
637
|
-
logger.plain("
|
|
684
|
+
logger.success("Remote MCP bridge is ready");
|
|
685
|
+
logger.plain(" Connection credentials unchanged; use --print-mcp-credentials only when reconnecting a client.");
|
|
638
686
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
687
|
+
if (policyMigrated) {
|
|
688
|
+
logger.warn("Legacy implicit policy migrated to full access; use --profile agent, edit, or review to narrow it.");
|
|
689
|
+
}
|
|
690
|
+
logger.plain(` Workspace: ${payload.workspace}`);
|
|
691
|
+
logger.plain(` Policy: ${formatPolicySummary(payload.policy)}`);
|
|
692
|
+
if (verbose) logger.plain(` State: ${payload.state_path}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function formatPolicySummary(policy = {}) {
|
|
696
|
+
const scope = policy.unrestrictedPaths ? "all local paths" : "workspace only";
|
|
697
|
+
const environment = policy.minimalEnv ? "isolated env" : "full parent env";
|
|
698
|
+
return `${policy.profile || "custom"} [${policy.origin || "unknown"}; write=${policy.allowWrite ? "on" : "off"}; exec=${policy.execMode || "off"}; ${scope}; ${environment}; absolute_paths=${policy.exposeAbsolutePaths ? "on" : "off"}]`;
|
|
642
699
|
}
|
|
643
700
|
|
|
701
|
+
|
|
644
702
|
function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({ component: "cli" }) } = {}) {
|
|
645
703
|
let stopping = false;
|
|
646
704
|
const stop = async () => {
|
|
@@ -660,8 +718,14 @@ function keepProcessAlive({ daemon = null, lock = null, logger = createLogger({
|
|
|
660
718
|
async function statusCommand(args) {
|
|
661
719
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
662
720
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
721
|
+
const storedPolicyOrigin = state.policy?.origin;
|
|
722
|
+
state.policy = resolvePolicy({}, state.policy);
|
|
663
723
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
664
|
-
const payload = {
|
|
724
|
+
const payload = {
|
|
725
|
+
...redactState(state),
|
|
726
|
+
policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
|
|
727
|
+
workerHealth: health,
|
|
728
|
+
};
|
|
665
729
|
console.log(JSON.stringify(payload, null, 2));
|
|
666
730
|
}
|
|
667
731
|
|
|
@@ -674,9 +738,17 @@ async function doctorCommand(args) {
|
|
|
674
738
|
const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
|
|
675
739
|
checks.push({ name: "cloudflare-login", ok: whoami.code === 0, detail: whoami.code === 0 ? "authenticated" : sanitizeLines(whoami.stderr || whoami.stdout) });
|
|
676
740
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
741
|
+
const storedPolicyOrigin = state.policy?.origin;
|
|
742
|
+
state.policy = resolvePolicy({}, state.policy);
|
|
743
|
+
checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
|
|
677
744
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
678
745
|
checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
|
|
679
|
-
console.log(JSON.stringify({
|
|
746
|
+
console.log(JSON.stringify({
|
|
747
|
+
ok: checks.every(check => check.ok),
|
|
748
|
+
checks,
|
|
749
|
+
policyMigrationPending: !storedPolicyOrigin && state.policy.origin === "migrated",
|
|
750
|
+
state: redactState(state),
|
|
751
|
+
}, null, 2));
|
|
680
752
|
}
|
|
681
753
|
|
|
682
754
|
async function rotateSecretsCommand(args) {
|
|
@@ -688,7 +760,7 @@ async function rotateSecretsCommand(args) {
|
|
|
688
760
|
throw new Error(`another startup/deployment operation is already running for this workspace (${pid})`);
|
|
689
761
|
}
|
|
690
762
|
try {
|
|
691
|
-
await stopAutostartBestEffort(
|
|
763
|
+
await stopAutostartBestEffort(createLogger({ level: args.quiet ? "error" : "warn", component: "service" }));
|
|
692
764
|
await sleep(500);
|
|
693
765
|
const daemonOwner = readDaemonLockOwner(daemonLockPathForState(state));
|
|
694
766
|
if (daemonOwner?.pid && isPidAlive(daemonOwner.pid)) {
|
|
@@ -696,9 +768,10 @@ async function rotateSecretsCommand(args) {
|
|
|
696
768
|
}
|
|
697
769
|
ensureWorkerSecrets(state, { rotateSecrets: true, workerName: validateWorkerName(args.workerName) });
|
|
698
770
|
saveState(state);
|
|
699
|
-
|
|
700
|
-
console.log(`Rotated
|
|
701
|
-
console.log(
|
|
771
|
+
const showMcpPassword = Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials);
|
|
772
|
+
console.log(`Rotated MCP connection password: ${showMcpPassword ? state.worker.oauthPassword : previewSecret(state.worker.oauthPassword)}`);
|
|
773
|
+
console.log(`Rotated daemon secret: ${previewSecret(state.worker.daemonSecret)}`);
|
|
774
|
+
console.log("Run `machine-mcp --print-mcp-credentials` to redeploy and display the new client connection credentials when needed.");
|
|
702
775
|
} finally {
|
|
703
776
|
startupLock.release();
|
|
704
777
|
}
|
|
@@ -742,23 +815,23 @@ async function serviceCommand(args) {
|
|
|
742
815
|
throw new Error(`Unknown service action: ${action}`);
|
|
743
816
|
}
|
|
744
817
|
|
|
745
|
-
async function installAutostartBestEffort({ workspace, stateRoot, entryScript }) {
|
|
818
|
+
async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
|
|
746
819
|
try {
|
|
747
820
|
const { installAutostart } = await import("./service.mjs");
|
|
748
|
-
const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(
|
|
749
|
-
if (result?.ok)
|
|
750
|
-
else
|
|
821
|
+
const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(true) });
|
|
822
|
+
if (result?.ok) logger.info("Autostart installed for future logins", { provider: result.provider });
|
|
823
|
+
else logger.warn("Autostart installation reported a problem; run `machine-mcp service status` for details");
|
|
751
824
|
} catch (error) {
|
|
752
|
-
|
|
825
|
+
logger.warn("Autostart installation skipped", { error_class: classifyOperationalError(error) });
|
|
753
826
|
}
|
|
754
827
|
}
|
|
755
828
|
|
|
756
|
-
async function stopAutostartBestEffort(
|
|
829
|
+
async function stopAutostartBestEffort(logger) {
|
|
757
830
|
try {
|
|
758
831
|
const { stopAutostart } = await import("./service.mjs");
|
|
759
832
|
await stopAutostart({ logger: structuredLogger(true) });
|
|
760
833
|
} catch (error) {
|
|
761
|
-
|
|
834
|
+
logger.warn("Autostart stop skipped", { error_class: classifyOperationalError(error) });
|
|
762
835
|
}
|
|
763
836
|
}
|
|
764
837
|
|
|
@@ -820,6 +893,7 @@ function knownWorkerNames(stateRoot) {
|
|
|
820
893
|
const stateFile = resolve(profiles, entry.name, "state.json");
|
|
821
894
|
if (!existsSync(stateFile)) continue;
|
|
822
895
|
try {
|
|
896
|
+
if (statSync(stateFile).size > 2 * 1024 * 1024) continue;
|
|
823
897
|
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
824
898
|
const name = String(state?.worker?.name || "");
|
|
825
899
|
if (/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) names.add(name);
|
|
@@ -839,7 +913,7 @@ async function removeAutostartBestEffort(stateRoot) {
|
|
|
839
913
|
}
|
|
840
914
|
return true;
|
|
841
915
|
} catch (error) {
|
|
842
|
-
console.warn(`Autostart removal skipped or failed
|
|
916
|
+
console.warn(`Autostart removal skipped or failed (${classifyOperationalError(error)}). Run machine-mcp service status for details.`);
|
|
843
917
|
return false;
|
|
844
918
|
}
|
|
845
919
|
}
|
|
@@ -872,9 +946,10 @@ function isPidAlive(pid) {
|
|
|
872
946
|
}
|
|
873
947
|
|
|
874
948
|
function structuredLogger(quiet) {
|
|
875
|
-
return createLogger({ quiet, component: "
|
|
949
|
+
return createLogger({ quiet, component: "service" });
|
|
876
950
|
}
|
|
877
951
|
|
|
952
|
+
|
|
878
953
|
function sanitizeLines(text) {
|
|
879
954
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
880
955
|
const homePattern = home ? new RegExp(escapeRegExp(home), "g") : null;
|
|
@@ -910,7 +985,7 @@ function usage() {
|
|
|
910
985
|
console.log(`machine-bridge-mcp
|
|
911
986
|
|
|
912
987
|
Usage:
|
|
913
|
-
npm install -g machine-bridge-mcp@latest && machine-mcp
|
|
988
|
+
npm install -g --allow-scripts=esbuild,workerd,sharp machine-bridge-mcp@latest && machine-mcp
|
|
914
989
|
npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
|
|
915
990
|
./mbm # from source checkout
|
|
916
991
|
.\\mbm.cmd # from source checkout on Windows cmd
|
|
@@ -946,9 +1021,12 @@ Start options:
|
|
|
946
1021
|
--no-exec Disable run_process and exec_command
|
|
947
1022
|
--full-env Pass the full parent environment to local commands
|
|
948
1023
|
--unrestricted-paths Allow filesystem tools outside the workspace
|
|
949
|
-
--absolute-paths Return absolute local paths
|
|
1024
|
+
--absolute-paths Return absolute local paths (enabled by the full profile)
|
|
950
1025
|
--state-dir DIR Override state root
|
|
951
|
-
--json Print
|
|
1026
|
+
--json Print connection details as JSON; credentials stay redacted unless explicitly requested
|
|
1027
|
+
--log-level LEVEL error, warn, info (default), or debug
|
|
1028
|
+
--verbose Alias for --log-level debug; includes per-tool success/correlation logs
|
|
1029
|
+
--quiet Alias for --log-level error
|
|
952
1030
|
|
|
953
1031
|
Uninstall options:
|
|
954
1032
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|