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/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, DEFAULT_UPSTREAM_URL } from "./api-server.mjs";
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 (args.api) ensureLocalApiKey(state, { apiKey: valueFromArgsEnv(args.apiKey, "MBM_API_KEY"), rotateApiKey: Boolean(args.rotateApiKey) });
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
- printConnection(state, { json: Boolean(args.json), noPrintCredentials: Boolean(args.noPrintCredentials) });
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
- if (!args.api) return;
194
- apiServer = await startConfiguredApiServer(state, args);
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
- const daemon = new LocalDaemon({
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 (args.api) apiServer = await startConfiguredApiServer(state, args);
212
- printConnection(state, { json: Boolean(args.json), noPrintCredentials: Boolean(args.noPrintCredentials) });
213
- if (apiServer) printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials) });
214
- keepProcessAlive({ daemon, lock, apiServer, logger });
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
- ensureLocalApiKey(state, { apiKey: valueFromArgsEnv(args.apiKey, "MBM_API_KEY"), rotateApiKey: Boolean(args.rotateApiKey) });
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
- printApiConnection(apiServer, state, { noPrintCredentials: Boolean(args.noPrintCredentials) });
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
- async function startConfiguredApiServer(state, args, logger = createLogger({ quiet: Boolean(args.quiet), verbose: Boolean(args.verbose), component: "api" })) {
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 upstreamKey = valueFromArgsEnv(args.apiUpstreamKey, "MBM_API_UPSTREAM_KEY", "OPENAI_API_KEY");
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
- upstreamUrl: valueFromArgsEnv(args.apiUpstreamUrl, "MBM_API_UPSTREAM_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE") || DEFAULT_UPSTREAM_URL,
248
- upstreamKey: upstreamKey || "",
249
- model: valueFromArgsEnv(args.apiModel, "MBM_API_MODEL", "OPENAI_MODEL") || DEFAULT_API_MODEL,
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(readFileSync(file));
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", "package.json", "package-lock.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; credentials are printed now and the process will keep retrying");
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 printConnection(state, { json = false, noPrintCredentials = false } = {}) {
410
- const logger = createLogger({ component: "ready" });
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
- logger.success("Remote MCP bridge is ready; keep this process running");
426
- logger.plain(` MCP Server URL: ${payload.mcp_server_url}`);
427
- if (!noPrintCredentials) logger.plain(` MCP connection password: ${payload.mcp_connection_password}`);
428
- else logger.plain(` MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
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
- logger.success("Local model API provider is ready");
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(state.localApi.apiKey)} (redacted)` : state.localApi.apiKey}`);
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 daemon
644
- api Start local OpenAI-compatible API provider only
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 the MCP password in console output
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 Also start local OpenAI-compatible API provider
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-upstream-url URL Upstream OpenAI-compatible base URL (default: https://api.openai.com/v1)
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 provider
836
+ console.log(`machine-bridge-mcp local ChatGPT MCP-backed API
688
837
 
689
838
  Usage:
690
- machine-mcp api
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
- Client type: OpenAI-compatible
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-upstream-url URL Upstream OpenAI-compatible base URL
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, MBM_API_UPSTREAM_URL, MBM_API_UPSTREAM_KEY, MBM_API_MODEL
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}`);