machine-bridge-mcp 0.2.0 → 0.2.4
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/README.md +43 -38
- package/package.json +1 -1
- package/src/local/api-server.mjs +206 -94
- package/src/local/cli.mjs +214 -60
- package/src/local/self-test.mjs +168 -20
- package/src/local/service.mjs +2 -0
- package/src/local/state.mjs +8 -0
- package/src/worker/index.ts +259 -16
package/src/local/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
-
import { startLocalApiServer, DEFAULT_API_HOST, DEFAULT_API_PORT, DEFAULT_API_MODEL
|
|
7
|
+
import { startLocalApiServer, DEFAULT_API_HOST, DEFAULT_API_PORT, DEFAULT_API_MODEL } from "./api-server.mjs";
|
|
8
8
|
import { createLogger, redactSecret } from "./log.mjs";
|
|
9
9
|
import { runWrangler } from "./shell.mjs";
|
|
10
10
|
import {
|
|
@@ -159,16 +159,21 @@ async function confirm(prompt, assumeYes = false) {
|
|
|
159
159
|
|
|
160
160
|
async function startCommand(args) {
|
|
161
161
|
assertNodeVersion();
|
|
162
|
-
const logger = createLogger({ quiet: Boolean(args.quiet), verbose: Boolean(args.verbose), component: "cli" });
|
|
162
|
+
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "cli" });
|
|
163
163
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
164
164
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
165
|
+
const previousMcpServerUrl = state.worker?.mcpServerUrl || "";
|
|
166
|
+
const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
|
|
167
|
+
const apiEnabled = args.noApi ? false : true;
|
|
168
|
+
|
|
165
169
|
ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName: args.workerName && String(args.workerName) });
|
|
166
|
-
if (
|
|
170
|
+
if (apiEnabled) configureLocalApiState(state, args);
|
|
167
171
|
state.policy = {
|
|
168
172
|
allowWrite: args.noWrite ? false : true,
|
|
169
173
|
allowExec: args.noExec ? false : true,
|
|
170
174
|
unrestrictedPaths: true,
|
|
171
175
|
minimalEnv: args.fullEnv ? false : true,
|
|
176
|
+
apiEnabled,
|
|
172
177
|
updatedAt: new Date().toISOString(),
|
|
173
178
|
};
|
|
174
179
|
saveState(state);
|
|
@@ -176,6 +181,9 @@ async function startCommand(args) {
|
|
|
176
181
|
if (!args.daemonOnly) await ensureWorker(state, args);
|
|
177
182
|
else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
|
|
178
183
|
|
|
184
|
+
const mcpConnectionChanged = previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl;
|
|
185
|
+
const shouldPrintMcpCredentials = Boolean(args.json || args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
|
|
186
|
+
|
|
179
187
|
if (!args.daemonOnly && !args.noAutostart) {
|
|
180
188
|
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], policy: state.policy });
|
|
181
189
|
}
|
|
@@ -183,38 +191,55 @@ async function startCommand(args) {
|
|
|
183
191
|
if (!args.daemonOnly) await stopAutostartBestEffort(Boolean(args.quiet));
|
|
184
192
|
|
|
185
193
|
const lock = acquireDaemonLock(state);
|
|
194
|
+
let daemon = null;
|
|
186
195
|
let apiServer = null;
|
|
187
196
|
if (!lock.acquired) {
|
|
188
197
|
if (!args.quiet) {
|
|
189
198
|
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
190
199
|
logger.warn(`local daemon already running for this workspace (${pid}); not starting a duplicate`);
|
|
191
|
-
|
|
200
|
+
if (!args.json) printMcpConnection(state, {
|
|
201
|
+
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
202
|
+
includeCredentials: shouldPrintMcpCredentials,
|
|
203
|
+
quiet: Boolean(args.quiet),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (!apiEnabled) {
|
|
207
|
+
if (args.json) printStartJson(state, null, { noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
208
|
+
return;
|
|
192
209
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
196
|
-
keepProcessAlive({ apiServer, logger });
|
|
210
|
+
apiServer = await startOptionalApiServer(state, args, logger);
|
|
211
|
+
if (args.json) printStartJson(state, apiServer, { noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
212
|
+
else if (apiServer) printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials), quiet: Boolean(args.quiet) });
|
|
213
|
+
if (apiServer && !apiServer.alreadyRunning) keepProcessAlive({ apiServer, logger });
|
|
197
214
|
return;
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
try {
|
|
201
|
-
|
|
218
|
+
daemon = new LocalDaemon({
|
|
202
219
|
workerUrl: state.worker.url,
|
|
203
220
|
secret: state.worker.daemonSecret,
|
|
204
221
|
workspace,
|
|
205
222
|
policy: state.policy,
|
|
206
|
-
logger: createLogger({ quiet: Boolean(args.quiet), verbose: Boolean(args.verbose), component: "daemon" }),
|
|
223
|
+
logger: createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "daemon" }),
|
|
207
224
|
});
|
|
208
225
|
|
|
209
226
|
const waitForConnect = daemon.start();
|
|
210
|
-
await waitForConnectWithNotice(waitForConnect, 20_000);
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
await waitForConnectWithNotice(waitForConnect, 20_000, Boolean(args.quiet || args.json));
|
|
228
|
+
if (apiEnabled) apiServer = await startOptionalApiServer(state, args, logger);
|
|
229
|
+
if (args.json) printStartJson(state, apiServer, { noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
230
|
+
else {
|
|
231
|
+
printMcpConnection(state, {
|
|
232
|
+
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
233
|
+
includeCredentials: shouldPrintMcpCredentials,
|
|
234
|
+
quiet: Boolean(args.quiet),
|
|
235
|
+
});
|
|
236
|
+
if (apiServer) printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials), quiet: Boolean(args.quiet) });
|
|
237
|
+
}
|
|
238
|
+
keepProcessAlive({ daemon, lock, apiServer: apiServer?.alreadyRunning ? null : apiServer, logger });
|
|
215
239
|
} catch (error) {
|
|
240
|
+
try { daemon?.stop?.(); } catch {}
|
|
216
241
|
lock.release();
|
|
217
|
-
if (apiServer) await apiServer.close().catch(() => {});
|
|
242
|
+
if (apiServer && !apiServer.alreadyRunning) await apiServer.close().catch(() => {});
|
|
218
243
|
throw error;
|
|
219
244
|
}
|
|
220
245
|
}
|
|
@@ -223,33 +248,95 @@ async function apiCommand(args) {
|
|
|
223
248
|
assertNodeVersion();
|
|
224
249
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
|
|
225
250
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
226
|
-
|
|
251
|
+
configureLocalApiState(state, args);
|
|
227
252
|
saveState(state);
|
|
228
|
-
const logger = createLogger({ quiet: Boolean(args.quiet), verbose: Boolean(args.verbose), component: "api" });
|
|
253
|
+
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "api" });
|
|
229
254
|
const apiServer = await startConfiguredApiServer(state, args, logger);
|
|
230
|
-
|
|
255
|
+
if (args.json) printApiJson(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
256
|
+
else printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials), quiet: Boolean(args.quiet) });
|
|
231
257
|
keepProcessAlive({ apiServer, logger });
|
|
232
258
|
}
|
|
233
259
|
|
|
234
|
-
|
|
260
|
+
|
|
261
|
+
async function startConfiguredApiServer(state, args, logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "api" })) {
|
|
235
262
|
const apiOptions = apiOptionsFromArgs(state, args);
|
|
236
263
|
return startLocalApiServer({ ...apiOptions, logger });
|
|
237
264
|
}
|
|
238
265
|
|
|
266
|
+
async function startOptionalApiServer(state, args, parentLogger) {
|
|
267
|
+
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "api" });
|
|
268
|
+
const apiOptions = apiOptionsFromArgs(state, args);
|
|
269
|
+
try {
|
|
270
|
+
return await startLocalApiServer({ ...apiOptions, logger });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error?.code === "EADDRINUSE") {
|
|
273
|
+
const baseUrl = apiBaseUrl(apiOptions.host, apiOptions.port);
|
|
274
|
+
const health = await probeLocalApiHealth(baseUrl, state.localApi?.apiKey);
|
|
275
|
+
if (health.ok) {
|
|
276
|
+
logger.success("local OpenAI-compatible API already running", { baseUrl });
|
|
277
|
+
return { ...health, baseUrl, host: apiOptions.host, port: Number(apiOptions.port), alreadyRunning: true, close() { return Promise.resolve(); } };
|
|
278
|
+
}
|
|
279
|
+
parentLogger.warn(error.message);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
parentLogger.warn(`Local API skipped: ${error.message}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function apiBaseUrl(host, port) {
|
|
288
|
+
const textHost = String(host || DEFAULT_API_HOST);
|
|
289
|
+
const urlHost = textHost.includes(":") && !textHost.startsWith("[") ? `[${textHost}]` : textHost;
|
|
290
|
+
return `http://${urlHost}:${port || DEFAULT_API_PORT}/v1`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function probeLocalApiHealth(baseUrl, expectedApiKey = "") {
|
|
294
|
+
try {
|
|
295
|
+
const healthUrl = `${String(baseUrl).replace(/\/v1$/, "")}/health`;
|
|
296
|
+
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(750) });
|
|
297
|
+
if (!response.ok) return { ok: false };
|
|
298
|
+
const body = await response.json().catch(() => null);
|
|
299
|
+
if (body?.service !== "machine-bridge-mcp-local-api") return { ok: false };
|
|
300
|
+
if (body.api_key_sha256 && expectedApiKey && body.api_key_sha256 !== sha256String(expectedApiKey)) return { ok: false, reason: "api_key_mismatch" };
|
|
301
|
+
return { ok: true };
|
|
302
|
+
} catch {
|
|
303
|
+
return { ok: false };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
239
307
|
function apiOptionsFromArgs(state, args = {}) {
|
|
240
|
-
const
|
|
241
|
-
const explicitPort = args.apiPort !== undefined && args.apiPort !== true ? args.apiPort : (args.port !== undefined && args.port !== true ? args.port : undefined);
|
|
308
|
+
const explicitPort = explicitArg(args.apiPort) ?? explicitArg(args.port);
|
|
242
309
|
const envPort = valueFromArgsEnv(undefined, "MBM_API_PORT", "PORT");
|
|
243
310
|
return {
|
|
244
|
-
host: valueFromArgsEnv(args.apiHost, "MBM_API_HOST") || DEFAULT_API_HOST,
|
|
245
|
-
port: explicitPort ?? envPort ?? DEFAULT_API_PORT,
|
|
311
|
+
host: valueFromArgsEnv(args.apiHost, "MBM_API_HOST") || state.localApi?.host || DEFAULT_API_HOST,
|
|
312
|
+
port: explicitPort ?? envPort ?? state.localApi?.port ?? DEFAULT_API_PORT,
|
|
246
313
|
apiKey: valueFromArgsEnv(args.apiKey, "MBM_API_KEY") || state.localApi?.apiKey || ensureLocalApiKey(state, { rotateApiKey: Boolean(args.rotateApiKey) }),
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
314
|
+
model: valueFromArgsEnv(args.apiModel, "MBM_API_MODEL") || state.localApi?.model || DEFAULT_API_MODEL,
|
|
315
|
+
workerUrl: state.worker?.url || "",
|
|
316
|
+
daemonSecret: state.worker?.daemonSecret || "",
|
|
250
317
|
};
|
|
251
318
|
}
|
|
252
319
|
|
|
320
|
+
function configureLocalApiState(state, args = {}) {
|
|
321
|
+
state.localApi ||= {};
|
|
322
|
+
ensureLocalApiKey(state, { apiKey: explicitArg(args.apiKey), rotateApiKey: Boolean(args.rotateApiKey) });
|
|
323
|
+
const port = explicitArg(args.apiPort) ?? explicitArg(args.port);
|
|
324
|
+
if (port !== undefined && String(port) !== "0") state.localApi.port = String(port);
|
|
325
|
+
const host = explicitArg(args.apiHost);
|
|
326
|
+
if (host !== undefined) state.localApi.host = String(host);
|
|
327
|
+
const model = explicitArg(args.apiModel);
|
|
328
|
+
if (model !== undefined) state.localApi.model = String(model);
|
|
329
|
+
state.localApi.updatedAt = new Date().toISOString();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function explicitArg(value) {
|
|
333
|
+
return value !== undefined && value !== null && value !== true ? String(value) : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function sha256String(value) {
|
|
337
|
+
return createHash("sha256").update(String(value)).digest("hex");
|
|
338
|
+
}
|
|
339
|
+
|
|
253
340
|
function valueFromArgsEnv(argValue, ...envNames) {
|
|
254
341
|
if (argValue !== undefined && argValue !== null && argValue !== true) return String(argValue);
|
|
255
342
|
for (const name of envNames) {
|
|
@@ -259,7 +346,7 @@ function valueFromArgsEnv(argValue, ...envNames) {
|
|
|
259
346
|
}
|
|
260
347
|
|
|
261
348
|
async function ensureWorker(state, args) {
|
|
262
|
-
const logger = createLogger({ quiet: Boolean(args.quiet), verbose: Boolean(args.verbose), component: "worker" });
|
|
349
|
+
const logger = createLogger({ quiet: Boolean(args.quiet || args.json), verbose: Boolean(args.verbose), component: "worker" });
|
|
263
350
|
const desiredHash = workerDeployHash(state);
|
|
264
351
|
const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
|
|
265
352
|
if (!args.forceWorker && !args.rotateSecrets && complete && state.worker.deployHash === desiredHash) {
|
|
@@ -339,14 +426,22 @@ function workerDeployHash(state) {
|
|
|
339
426
|
hash.update(String(state.worker.oauthTokenVersion || ""));
|
|
340
427
|
for (const file of workerDeployHashFiles()) {
|
|
341
428
|
hash.update(path.relative(packageRoot, file));
|
|
342
|
-
hash.update(
|
|
429
|
+
hash.update(workerHashContent(file));
|
|
343
430
|
}
|
|
344
431
|
return hash.digest("hex");
|
|
345
432
|
}
|
|
346
433
|
|
|
434
|
+
function workerHashContent(file) {
|
|
435
|
+
const content = readFileSync(file, "utf8");
|
|
436
|
+
if (path.relative(packageRoot, file).replaceAll("\\", "/") === "src/worker/index.ts") {
|
|
437
|
+
return content.replace(/const SERVER_VERSION = "[^"]+";/, 'const SERVER_VERSION = "<ignored-for-deploy-hash>";');
|
|
438
|
+
}
|
|
439
|
+
return content;
|
|
440
|
+
}
|
|
441
|
+
|
|
347
442
|
function workerDeployHashFiles() {
|
|
348
443
|
const files = [];
|
|
349
|
-
for (const item of ["src/worker", "wrangler.jsonc", "tsconfig.json"
|
|
444
|
+
for (const item of ["src/worker", "wrangler.jsonc", "tsconfig.json"]) {
|
|
350
445
|
collectHashFiles(resolve(packageRoot, item), files);
|
|
351
446
|
}
|
|
352
447
|
return files.sort();
|
|
@@ -396,18 +491,61 @@ function extractWorkerUrl(text = "") {
|
|
|
396
491
|
return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
|
|
397
492
|
}
|
|
398
493
|
|
|
399
|
-
async function waitForConnectWithNotice(promise, timeoutMs) {
|
|
494
|
+
async function waitForConnectWithNotice(promise, timeoutMs, quiet = false) {
|
|
400
495
|
let timeout;
|
|
401
496
|
const timed = new Promise(resolvePromise => {
|
|
402
497
|
timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
|
|
403
498
|
});
|
|
404
499
|
const result = await Promise.race([promise.then(() => "connected"), timed]);
|
|
405
500
|
clearTimeout(timeout);
|
|
406
|
-
if (result === "timeout") createLogger({ component: "daemon" }).warn("Still connecting;
|
|
501
|
+
if (result === "timeout") createLogger({ component: "daemon", quiet }).warn("Still connecting; the process will keep retrying");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
function printStartJson(state, apiServer, { noPrintCredentials = false } = {}) {
|
|
506
|
+
const mcpPassword = noPrintCredentials ? previewSecret(state.worker.oauthPassword) : state.worker.oauthPassword;
|
|
507
|
+
const runtimeApiKey = apiServer?.apiKey || state.localApi?.apiKey || "";
|
|
508
|
+
const apiKey = runtimeApiKey ? (noPrintCredentials ? previewSecret(runtimeApiKey) : runtimeApiKey) : null;
|
|
509
|
+
createLogger({ component: "ready" }).json({
|
|
510
|
+
mcp: {
|
|
511
|
+
server_url: state.worker.mcpServerUrl,
|
|
512
|
+
connection_password: mcpPassword,
|
|
513
|
+
worker_url: state.worker.url,
|
|
514
|
+
worker_name: state.worker.name,
|
|
515
|
+
},
|
|
516
|
+
local_api: apiServer ? {
|
|
517
|
+
base_url: apiServer.baseUrl,
|
|
518
|
+
api_key: apiKey,
|
|
519
|
+
already_running: Boolean(apiServer.alreadyRunning),
|
|
520
|
+
backend: "chatgpt-mcp",
|
|
521
|
+
model: state.localApi?.model || DEFAULT_API_MODEL,
|
|
522
|
+
mcp_bridge_configured: Boolean(state.worker?.url && state.worker?.daemonSecret),
|
|
523
|
+
client_type: "OpenAI-compatible",
|
|
524
|
+
} : null,
|
|
525
|
+
workspace: state.workspace.path,
|
|
526
|
+
state_path: state.paths.statePath,
|
|
527
|
+
policy: state.policy,
|
|
528
|
+
});
|
|
407
529
|
}
|
|
408
530
|
|
|
409
|
-
function
|
|
410
|
-
const
|
|
531
|
+
function printApiJson(apiServer, state, { noPrintCredentials = false } = {}) {
|
|
532
|
+
const runtimeApiKey = apiServer?.apiKey || state.localApi?.apiKey || "";
|
|
533
|
+
createLogger({ component: "ready" }).json({
|
|
534
|
+
local_api: {
|
|
535
|
+
base_url: apiServer.baseUrl,
|
|
536
|
+
api_key: noPrintCredentials ? previewSecret(runtimeApiKey) : runtimeApiKey,
|
|
537
|
+
backend: "chatgpt-mcp",
|
|
538
|
+
model: apiServer?.model || state.localApi?.model || DEFAULT_API_MODEL,
|
|
539
|
+
mcp_bridge_configured: Boolean(state.worker?.url && state.worker?.daemonSecret),
|
|
540
|
+
client_type: "OpenAI-compatible",
|
|
541
|
+
},
|
|
542
|
+
workspace: state.workspace.path,
|
|
543
|
+
state_path: state.paths.statePath,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function printMcpConnection(state, { json = false, noPrintCredentials = false, includeCredentials = false, quiet = false } = {}) {
|
|
548
|
+
const logger = createLogger({ component: "ready", quiet });
|
|
411
549
|
const payload = {
|
|
412
550
|
mcp_server_url: state.worker.mcpServerUrl,
|
|
413
551
|
mcp_connection_password: state.worker.oauthPassword,
|
|
@@ -422,23 +560,34 @@ function printConnection(state, { json = false, noPrintCredentials = false } = {
|
|
|
422
560
|
logger.json(safePayload);
|
|
423
561
|
return;
|
|
424
562
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
563
|
+
if (includeCredentials) {
|
|
564
|
+
logger.success("Remote MCP bridge is ready; save these connection details if your ChatGPT app needs to reconnect");
|
|
565
|
+
logger.plain(` MCP Server URL: ${payload.mcp_server_url}`);
|
|
566
|
+
if (!noPrintCredentials) logger.plain(` MCP connection password: ${payload.mcp_connection_password}`);
|
|
567
|
+
else logger.plain(` MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
|
|
568
|
+
} else {
|
|
569
|
+
logger.success("Remote MCP bridge is ready; MCP connection details unchanged");
|
|
570
|
+
logger.plain(" Use --print-mcp-credentials only when a ChatGPT app needs to reconnect.");
|
|
571
|
+
}
|
|
429
572
|
logger.plain(` Workspace cwd: ${payload.workspace}`);
|
|
430
|
-
logger.plain(` Policy: write=${payload.policy.allowWrite ? "on" : "off"}, exec=${payload.policy.allowExec ? "on" : "off"}, unrestricted_paths=${payload.policy.unrestrictedPaths ? "on" : "off"}`);
|
|
573
|
+
logger.plain(` Policy: write=${payload.policy.allowWrite ? "on" : "off"}, exec=${payload.policy.allowExec ? "on" : "off"}, local_api=${payload.policy.apiEnabled === false ? "off" : "on"}, unrestricted_paths=${payload.policy.unrestrictedPaths ? "on" : "off"}`);
|
|
431
574
|
logger.plain(` State: ${payload.state_path}`);
|
|
432
575
|
}
|
|
433
576
|
|
|
434
|
-
function printApiConnection(apiServer, state, { noPrintCredentials = false } = {}) {
|
|
435
|
-
const logger = createLogger({ component: "ready" });
|
|
436
|
-
|
|
577
|
+
function printApiConnection(apiServer, state, { noPrintCredentials = false, quiet = false } = {}) {
|
|
578
|
+
const logger = createLogger({ component: "ready", quiet });
|
|
579
|
+
const runtimeApiKey = apiServer?.apiKey || state.localApi?.apiKey || "";
|
|
580
|
+
logger.success("Local ChatGPT MCP-backed OpenAI-compatible API is running");
|
|
437
581
|
logger.plain(` API Base URL: ${apiServer.baseUrl}`);
|
|
438
|
-
logger.plain(` API key: ${noPrintCredentials ? `${redactSecret(
|
|
582
|
+
logger.plain(` API key: ${noPrintCredentials ? `${redactSecret(runtimeApiKey)} (redacted)` : runtimeApiKey}`);
|
|
439
583
|
logger.plain(" Client type: OpenAI-compatible");
|
|
584
|
+
logger.plain(` Model: ${apiServer?.model || state.localApi?.model || DEFAULT_API_MODEL}`);
|
|
585
|
+
logger.plain(" Backend: ChatGPT MCP sampling via the connected ChatGPT app");
|
|
586
|
+
if (state.worker?.mcpServerUrl) logger.plain(` MCP Server URL: ${state.worker.mcpServerUrl}`);
|
|
587
|
+
else logger.plain(" MCP bridge: not deployed yet; run `machine-mcp` before using generation routes.");
|
|
440
588
|
}
|
|
441
589
|
|
|
590
|
+
|
|
442
591
|
function keepProcessAlive({ daemon = null, lock = null, apiServer = null, logger = createLogger({ component: "cli" }) } = {}) {
|
|
443
592
|
let stopping = false;
|
|
444
593
|
const stop = async () => {
|
|
@@ -640,8 +789,8 @@ Usage:
|
|
|
640
789
|
.\\mbm.cmd # from source checkout on Windows cmd
|
|
641
790
|
|
|
642
791
|
Commands:
|
|
643
|
-
start Deploy/update Worker, install autostart, start local
|
|
644
|
-
api Start local OpenAI-compatible API
|
|
792
|
+
start Deploy/update Worker, install autostart, start daemon and local API
|
|
793
|
+
api Start local ChatGPT MCP-backed OpenAI-compatible API only
|
|
645
794
|
workspace show Show remembered workspace
|
|
646
795
|
workspace set Re-select workspace; prompts with current/default path
|
|
647
796
|
service status Show autostart status
|
|
@@ -661,21 +810,21 @@ Start options:
|
|
|
661
810
|
--rotate-secrets Rotate secrets before deploying
|
|
662
811
|
--daemon-only Skip deploy and only connect daemon from existing state
|
|
663
812
|
--no-autostart Do not install login autostart during start
|
|
664
|
-
--no-print-credentials Redact
|
|
813
|
+
--no-print-credentials Redact credentials in console output
|
|
814
|
+
--print-mcp-credentials Print MCP URL/password again for reconnecting ChatGPT apps
|
|
665
815
|
--no-write Disable write_file (default: write enabled)
|
|
666
816
|
--no-exec Disable exec_command (default: exec enabled)
|
|
667
817
|
--full-env Pass full parent environment to exec_command (default: minimal env)
|
|
668
818
|
--state-dir DIR Override state root
|
|
669
|
-
--json Print connection details as JSON
|
|
670
|
-
--api
|
|
819
|
+
--json Print MCP and local API connection details as JSON
|
|
820
|
+
--no-api Do not start the local OpenAI-compatible API
|
|
821
|
+
--api Deprecated no-op; local API starts by default
|
|
671
822
|
--api-port PORT Local API port (default: 8765)
|
|
672
823
|
--port PORT Alias for --api-port on the api command
|
|
673
824
|
--api-host HOST Local API host (default: 127.0.0.1)
|
|
674
825
|
--api-key KEY Set or replace local API key in state
|
|
675
826
|
--rotate-api-key Rotate local API key
|
|
676
|
-
--api-
|
|
677
|
-
--api-upstream-key KEY Upstream provider key; env OPENAI_API_KEY also works
|
|
678
|
-
--api-model MODEL Model id advertised by /v1/models
|
|
827
|
+
--api-model MODEL Local model id advertised by /v1/models (default: chatgpt-mcp)
|
|
679
828
|
|
|
680
829
|
Uninstall options:
|
|
681
830
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|
|
@@ -684,17 +833,19 @@ Uninstall options:
|
|
|
684
833
|
}
|
|
685
834
|
|
|
686
835
|
function apiUsage() {
|
|
687
|
-
console.log(`machine-bridge-mcp local API
|
|
836
|
+
console.log(`machine-bridge-mcp local ChatGPT MCP-backed API
|
|
688
837
|
|
|
689
838
|
Usage:
|
|
690
|
-
machine-mcp
|
|
839
|
+
machine-mcp
|
|
840
|
+
machine-mcp --api-port 8766
|
|
841
|
+
machine-mcp api # local API only
|
|
691
842
|
machine-mcp api --api-port 8766
|
|
692
|
-
machine-mcp start --api --api-port 8766
|
|
693
843
|
|
|
694
844
|
Client settings:
|
|
695
845
|
API Base URL: http://127.0.0.1:<port>/v1
|
|
696
846
|
API key: printed on startup unless --no-print-credentials is set
|
|
697
|
-
|
|
847
|
+
Model: chatgpt-mcp by default
|
|
848
|
+
Backend: connected ChatGPT app through the Remote MCP bridge
|
|
698
849
|
|
|
699
850
|
Options:
|
|
700
851
|
--workspace PATH Use and remember this workspace path
|
|
@@ -703,18 +854,21 @@ Options:
|
|
|
703
854
|
--port PORT Alias for --api-port
|
|
704
855
|
--api-key KEY Set or replace local API key in state
|
|
705
856
|
--rotate-api-key Rotate local API key
|
|
706
|
-
--api-
|
|
707
|
-
--api-upstream-key KEY Upstream provider key; env OPENAI_API_KEY also works
|
|
708
|
-
--api-model MODEL Model id advertised by /v1/models
|
|
857
|
+
--api-model MODEL Local model id advertised by /v1/models
|
|
709
858
|
--no-print-credentials Redact the local API key in console output
|
|
859
|
+
--no-api Only valid for start; disables the default local API
|
|
710
860
|
--state-dir DIR Override state root
|
|
711
861
|
|
|
862
|
+
Important:
|
|
863
|
+
Generation routes require a ChatGPT app connected to the printed MCP Server URL.
|
|
864
|
+
The local API sends sampling/createMessage requests through that MCP connection.
|
|
865
|
+
|
|
712
866
|
Environment:
|
|
713
|
-
MBM_API_HOST, MBM_API_PORT, MBM_API_KEY,
|
|
714
|
-
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_API_BASE, OPENAI_MODEL
|
|
867
|
+
MBM_API_HOST, MBM_API_PORT, MBM_API_KEY, MBM_API_MODEL
|
|
715
868
|
`);
|
|
716
869
|
}
|
|
717
870
|
|
|
871
|
+
|
|
718
872
|
function version() {
|
|
719
873
|
const pkg = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
|
|
720
874
|
console.log(`${pkg.name} ${pkg.version}`);
|