@cubis/foundry 0.3.42 → 0.3.43

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.
Files changed (3) hide show
  1. package/README.md +21 -2
  2. package/bin/cubis.js +241 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -323,10 +323,10 @@ MCP Docker runtime commands:
323
323
  cbx mcp runtime status --scope global --name cbx-mcp
324
324
 
325
325
  # Start runtime container (pull/build image first as needed)
326
- cbx mcp runtime up --scope global --name cbx-mcp --port 3310
326
+ cbx mcp runtime up --scope global --name cbx-mcp --port 3310 --fallback local
327
327
 
328
328
  # Recreate existing container
329
- cbx mcp runtime up --scope global --name cbx-mcp --replace
329
+ cbx mcp runtime up --scope global --name cbx-mcp --replace --fallback local
330
330
 
331
331
  # Stop/remove runtime container
332
332
  cbx mcp runtime down --name cbx-mcp
@@ -382,6 +382,10 @@ cbx workflows config --scope global --show
382
382
  cbx workflows config --scope global --edit
383
383
  cbx workflows config --scope global --workspace-id "<workspace-id>"
384
384
  cbx workflows config --scope global --clear-workspace-id
385
+
386
+ # Switch MCP runtime preference quickly
387
+ cbx workflows config --scope project --mcp-runtime local
388
+ cbx workflows config --scope project --mcp-runtime docker --mcp-fallback local
385
389
  ```
386
390
 
387
391
  `--show` now includes computed `status`:
@@ -462,6 +466,21 @@ If `~/.agents/skills` is missing, runtime still starts but will warn and skill d
462
466
  - `cbx mcp serve --transport stdio` runs local stdio transport for command-based MCP clients.
463
467
  - Prefer stdio command server entries (`cubis-foundry`) for direct client integrations; use Docker runtime for explicit HTTP endpoint use cases.
464
468
 
469
+ ### Docker endpoint resets at `127.0.0.1:<port>/mcp`
470
+
471
+ If Docker runtime starts but MCP endpoint is unreachable:
472
+
473
+ ```bash
474
+ # Check health and hints
475
+ cbx mcp runtime status --scope project --name cbx-mcp
476
+
477
+ # Switch this project to local runtime
478
+ cbx workflows config --scope project --mcp-runtime local
479
+
480
+ # Use direct local server path
481
+ cbx mcp serve --transport stdio --scope auto
482
+ ```
483
+
465
484
  ### Duplicate skills shown in UI after older installs
466
485
 
467
486
  Installer now auto-cleans nested duplicate skills (for example duplicates under `postman/*`).
package/bin/cubis.js CHANGED
@@ -7112,6 +7112,49 @@ async function writeConfigFile({
7112
7112
  : "created";
7113
7113
  }
7114
7114
 
7115
+ async function persistMcpRuntimePreference({
7116
+ scope,
7117
+ runtime,
7118
+ fallback = null,
7119
+ cwd = process.cwd(),
7120
+ generatedBy = "cbx mcp runtime up",
7121
+ }) {
7122
+ const { configPath, existing, existingValue } = await loadConfigForScope({
7123
+ scope,
7124
+ cwd,
7125
+ });
7126
+ if (!existing.exists || !existingValue) {
7127
+ return {
7128
+ action: "skipped",
7129
+ configPath,
7130
+ reason: "config-missing",
7131
+ };
7132
+ }
7133
+ const next = prepareConfigDocument(existingValue, {
7134
+ scope,
7135
+ generatedBy,
7136
+ });
7137
+ if (!next.mcp || typeof next.mcp !== "object" || Array.isArray(next.mcp)) {
7138
+ next.mcp = {};
7139
+ }
7140
+ next.mcp.runtime = runtime;
7141
+ next.mcp.effectiveRuntime = runtime;
7142
+ if (fallback) {
7143
+ next.mcp.fallback = fallback;
7144
+ }
7145
+ const action = await writeConfigFile({
7146
+ configPath,
7147
+ nextConfig: next,
7148
+ existingExists: existing.exists,
7149
+ dryRun: false,
7150
+ });
7151
+ return {
7152
+ action,
7153
+ configPath,
7154
+ reason: null,
7155
+ };
7156
+ }
7157
+
7115
7158
  function toProfileEnvSuffix(profileName) {
7116
7159
  const normalized =
7117
7160
  normalizeCredentialProfileName(profileName) || DEFAULT_CREDENTIAL_PROFILE_NAME;
@@ -7642,6 +7685,8 @@ async function runWorkflowConfig(options) {
7642
7685
  const hasWorkspaceIdOption = opts.workspaceId !== undefined;
7643
7686
  const wantsClearWorkspaceId = Boolean(opts.clearWorkspaceId);
7644
7687
  const wantsInteractiveEdit = Boolean(opts.edit);
7688
+ const hasMcpRuntimeOption = opts.mcpRuntime !== undefined;
7689
+ const hasMcpFallbackOption = opts.mcpFallback !== undefined;
7645
7690
 
7646
7691
  if (hasWorkspaceIdOption && wantsClearWorkspaceId) {
7647
7692
  throw new Error(
@@ -7650,7 +7695,11 @@ async function runWorkflowConfig(options) {
7650
7695
  }
7651
7696
 
7652
7697
  const wantsMutation =
7653
- hasWorkspaceIdOption || wantsClearWorkspaceId || wantsInteractiveEdit;
7698
+ hasWorkspaceIdOption ||
7699
+ wantsClearWorkspaceId ||
7700
+ wantsInteractiveEdit ||
7701
+ hasMcpRuntimeOption ||
7702
+ hasMcpFallbackOption;
7654
7703
  const showOnly = Boolean(opts.show) || !wantsMutation;
7655
7704
  const { configPath, existing, existingValue } = await loadConfigForScope({
7656
7705
  scope,
@@ -7692,6 +7741,18 @@ async function runWorkflowConfig(options) {
7692
7741
  if (wantsClearWorkspaceId) {
7693
7742
  workspaceId = null;
7694
7743
  }
7744
+ const mcpRuntime = hasMcpRuntimeOption
7745
+ ? normalizeMcpRuntime(
7746
+ opts.mcpRuntime,
7747
+ normalizeMcpRuntime(next.mcp?.runtime, DEFAULT_MCP_RUNTIME),
7748
+ )
7749
+ : null;
7750
+ const mcpFallback = hasMcpFallbackOption
7751
+ ? normalizeMcpFallback(
7752
+ opts.mcpFallback,
7753
+ normalizeMcpFallback(next.mcp?.fallback, DEFAULT_MCP_FALLBACK),
7754
+ )
7755
+ : null;
7695
7756
 
7696
7757
  activeProfile.workspaceId = workspaceId;
7697
7758
  const updatedProfiles = postmanState.profiles.map((profile) =>
@@ -7713,6 +7774,17 @@ async function runWorkflowConfig(options) {
7713
7774
  });
7714
7775
  upsertNormalizedPostmanConfig(next, updatedPostmanState);
7715
7776
 
7777
+ if (!next.mcp || typeof next.mcp !== "object" || Array.isArray(next.mcp)) {
7778
+ next.mcp = {};
7779
+ }
7780
+ if (hasMcpRuntimeOption) {
7781
+ next.mcp.runtime = mcpRuntime;
7782
+ next.mcp.effectiveRuntime = mcpRuntime;
7783
+ }
7784
+ if (hasMcpFallbackOption) {
7785
+ next.mcp.fallback = mcpFallback;
7786
+ }
7787
+
7716
7788
  if (parseStoredStitchConfig(next)) {
7717
7789
  upsertNormalizedStitchConfig(next, parseStoredStitchConfig(next));
7718
7790
  }
@@ -7729,6 +7801,13 @@ async function runWorkflowConfig(options) {
7729
7801
  console.log(
7730
7802
  `postman.defaultWorkspaceId: ${workspaceId === null ? "null" : workspaceId}`,
7731
7803
  );
7804
+ if (hasMcpRuntimeOption) {
7805
+ console.log(`mcp.runtime: ${mcpRuntime}`);
7806
+ console.log(`mcp.effectiveRuntime: ${mcpRuntime}`);
7807
+ }
7808
+ if (hasMcpFallbackOption) {
7809
+ console.log(`mcp.fallback: ${mcpFallback}`);
7810
+ }
7732
7811
  if (Boolean(opts.showAfter)) {
7733
7812
  const payload = buildConfigShowPayload(next);
7734
7813
  console.log(JSON.stringify(payload, null, 2));
@@ -7801,8 +7880,13 @@ async function sendMcpJsonRpcRequest({
7801
7880
  });
7802
7881
  const text = await response.text();
7803
7882
  if (!response.ok) {
7883
+ const parsedError = parseMcpJsonRpcResponse(text);
7884
+ const serverMessage =
7885
+ normalizePostmanApiKey(parsedError?.error?.message) ||
7886
+ normalizePostmanApiKey(parsedError?.message);
7887
+ const detail = serverMessage ? ` (${serverMessage})` : "";
7804
7888
  throw new Error(
7805
- `MCP request failed (${method}): HTTP ${response.status} ${response.statusText}`,
7889
+ `MCP request failed (${method}): HTTP ${response.status} ${response.statusText}${detail}`,
7806
7890
  );
7807
7891
  }
7808
7892
  const parsed = parseMcpJsonRpcResponse(text);
@@ -7815,6 +7899,75 @@ async function sendMcpJsonRpcRequest({
7815
7899
  };
7816
7900
  }
7817
7901
 
7902
+ function sleepMs(ms) {
7903
+ return new Promise((resolve) => setTimeout(resolve, ms));
7904
+ }
7905
+
7906
+ async function waitForMcpEndpointReady({
7907
+ url,
7908
+ headers = {},
7909
+ timeoutMs = 15000,
7910
+ intervalMs = 500,
7911
+ }) {
7912
+ const startedAt = Date.now();
7913
+ let lastError = null;
7914
+
7915
+ while (Date.now() - startedAt < timeoutMs) {
7916
+ let sessionId = null;
7917
+ try {
7918
+ const init = await sendMcpJsonRpcRequest({
7919
+ url,
7920
+ method: "initialize",
7921
+ id: `cbx-runtime-init-${Date.now()}`,
7922
+ params: {
7923
+ protocolVersion: "2025-06-18",
7924
+ capabilities: {},
7925
+ clientInfo: {
7926
+ name: "cbx-runtime",
7927
+ version: CLI_VERSION,
7928
+ },
7929
+ },
7930
+ headers,
7931
+ });
7932
+ sessionId = init.sessionId;
7933
+ await sendMcpJsonRpcRequest({
7934
+ url,
7935
+ method: "notifications/initialized",
7936
+ params: {},
7937
+ headers,
7938
+ sessionId,
7939
+ });
7940
+ return true;
7941
+ } catch (error) {
7942
+ const message = String(error?.message || "").toLowerCase();
7943
+ if (message.includes("already initialized")) {
7944
+ return true;
7945
+ }
7946
+ lastError = error;
7947
+ } finally {
7948
+ if (sessionId) {
7949
+ try {
7950
+ await fetch(url, {
7951
+ method: "DELETE",
7952
+ headers: {
7953
+ ...headers,
7954
+ "mcp-session-id": sessionId,
7955
+ },
7956
+ });
7957
+ } catch {
7958
+ // best effort
7959
+ }
7960
+ }
7961
+ }
7962
+ await sleepMs(intervalMs);
7963
+ }
7964
+
7965
+ const suffix = lastError ? ` (${lastError.message})` : "";
7966
+ throw new Error(
7967
+ `MCP endpoint readiness check timed out after ${timeoutMs}ms${suffix}`,
7968
+ );
7969
+ }
7970
+
7818
7971
  async function discoverUpstreamTools({
7819
7972
  service,
7820
7973
  url,
@@ -8328,9 +8481,23 @@ async function runMcpRuntimeStatus(options) {
8328
8481
  containerPort: MCP_DOCKER_CONTAINER_PORT,
8329
8482
  cwd,
8330
8483
  })) || DEFAULT_MCP_DOCKER_HOST_PORT;
8331
- console.log(
8332
- `Endpoint: http://127.0.0.1:${hostPort}/mcp`,
8333
- );
8484
+ const endpoint = `http://127.0.0.1:${hostPort}/mcp`;
8485
+ console.log(`Endpoint: ${endpoint}`);
8486
+ try {
8487
+ await waitForMcpEndpointReady({
8488
+ url: endpoint,
8489
+ timeoutMs: 5000,
8490
+ intervalMs: 500,
8491
+ });
8492
+ console.log("Endpoint health: ready");
8493
+ } catch (error) {
8494
+ console.log(`Endpoint health: unreachable (${error.message})`);
8495
+ if (defaults.defaults.fallback === "local") {
8496
+ console.log(
8497
+ `Hint: switch config to local runtime: cbx workflows config --scope ${scope} --mcp-runtime local`,
8498
+ );
8499
+ }
8500
+ }
8334
8501
  }
8335
8502
  } catch (error) {
8336
8503
  console.error(`\nError: ${error.message}`);
@@ -8352,6 +8519,10 @@ async function runMcpRuntimeUp(options) {
8352
8519
  opts.updatePolicy,
8353
8520
  defaults.defaults.updatePolicy,
8354
8521
  );
8522
+ const fallback = normalizeMcpFallback(
8523
+ opts.fallback,
8524
+ defaults.defaults.fallback,
8525
+ );
8355
8526
  const buildLocal = hasCliFlag("--build-local")
8356
8527
  ? true
8357
8528
  : defaults.defaults.buildLocal;
@@ -8405,7 +8576,17 @@ async function runMcpRuntimeUp(options) {
8405
8576
  if (skillsRootExists) {
8406
8577
  dockerArgs.push("-v", `${skillsRoot}:/workflows/skills:ro`);
8407
8578
  }
8408
- dockerArgs.push("-e", "CBX_MCP_TRANSPORT=streamable-http", image);
8579
+ dockerArgs.push(
8580
+ image,
8581
+ "--transport",
8582
+ "http",
8583
+ "--host",
8584
+ "0.0.0.0",
8585
+ "--port",
8586
+ String(MCP_DOCKER_CONTAINER_PORT),
8587
+ "--scope",
8588
+ "global",
8589
+ );
8409
8590
  await execFile(
8410
8591
  "docker",
8411
8592
  dockerArgs,
@@ -8421,6 +8602,7 @@ async function runMcpRuntimeUp(options) {
8421
8602
  console.log(`Image: ${image}`);
8422
8603
  console.log(`Image prepare: ${prepared.action}`);
8423
8604
  console.log(`Update policy: ${updatePolicy}`);
8605
+ console.log(`Fallback: ${fallback}`);
8424
8606
  console.log(`Build local: ${buildLocal ? "yes" : "no"}`);
8425
8607
  console.log(`Mount: ${cbxRoot} -> /root/.cbx`);
8426
8608
  if (skillsRootExists) {
@@ -8430,7 +8612,51 @@ async function runMcpRuntimeUp(options) {
8430
8612
  }
8431
8613
  console.log(`Port: ${hostPort}:${MCP_DOCKER_CONTAINER_PORT}`);
8432
8614
  console.log(`Status: ${running ? running.status : "started"}`);
8433
- console.log(`Endpoint: http://127.0.0.1:${hostPort}/mcp`);
8615
+ const endpoint = `http://127.0.0.1:${hostPort}/mcp`;
8616
+ console.log(`Endpoint: ${endpoint}`);
8617
+ try {
8618
+ await waitForMcpEndpointReady({
8619
+ url: endpoint,
8620
+ timeoutMs: 20000,
8621
+ intervalMs: 500,
8622
+ });
8623
+ console.log("Endpoint health: ready");
8624
+ } catch (error) {
8625
+ if (fallback === "skip") {
8626
+ runtimeWarnings.push(
8627
+ `Endpoint health check failed but continuing because --fallback=skip. (${error.message})`,
8628
+ );
8629
+ } else if (fallback === "local") {
8630
+ await execFile("docker", ["rm", "-f", containerName], { cwd }).catch(
8631
+ () => {},
8632
+ );
8633
+ runtimeWarnings.push(
8634
+ `Docker endpoint was unreachable and runtime fell back to local. (${error.message})`,
8635
+ );
8636
+ const persisted = await persistMcpRuntimePreference({
8637
+ scope,
8638
+ runtime: "local",
8639
+ fallback,
8640
+ cwd,
8641
+ generatedBy: "cbx mcp runtime up",
8642
+ });
8643
+ if (persisted.reason === "config-missing") {
8644
+ runtimeWarnings.push(
8645
+ `No cbx config found at ${persisted.configPath}; runtime preference was not persisted.`,
8646
+ );
8647
+ } else {
8648
+ runtimeWarnings.push(
8649
+ `Updated runtime preference in ${persisted.configPath} (${persisted.action}).`,
8650
+ );
8651
+ }
8652
+ console.log("Endpoint health: fallback-to-local");
8653
+ console.log("Local command: cbx mcp serve --transport stdio --scope auto");
8654
+ } else {
8655
+ throw new Error(
8656
+ `MCP endpoint is unreachable at ${endpoint}. ${error.message}`,
8657
+ );
8658
+ }
8659
+ }
8434
8660
  if (runtimeWarnings.length > 0) {
8435
8661
  console.log("Warnings:");
8436
8662
  for (const warning of runtimeWarnings) {
@@ -8768,6 +8994,8 @@ const workflowsConfigCommand = workflowsCommand
8768
8994
  .option("--edit", "edit Postman default workspace ID interactively")
8769
8995
  .option("--workspace-id <id|null>", "set postman.defaultWorkspaceId")
8770
8996
  .option("--clear-workspace-id", "set postman.defaultWorkspaceId to null")
8997
+ .option("--mcp-runtime <runtime>", "set mcp.runtime: docker|local")
8998
+ .option("--mcp-fallback <fallback>", "set mcp.fallback: local|fail|skip")
8771
8999
  .option("--show-after", "print JSON after update")
8772
9000
  .option("--dry-run", "preview changes without writing files")
8773
9001
  .action(runWorkflowConfig);
@@ -8868,6 +9096,8 @@ const skillsConfigCommand = skillsCommand
8868
9096
  .option("--edit", "edit Postman default workspace ID interactively")
8869
9097
  .option("--workspace-id <id|null>", "set postman.defaultWorkspaceId")
8870
9098
  .option("--clear-workspace-id", "set postman.defaultWorkspaceId to null")
9099
+ .option("--mcp-runtime <runtime>", "set mcp.runtime: docker|local")
9100
+ .option("--mcp-fallback <fallback>", "set mcp.fallback: local|fail|skip")
8871
9101
  .option("--show-after", "print JSON after update")
8872
9102
  .option("--dry-run", "preview changes without writing files")
8873
9103
  .action(async (options) => {
@@ -8958,6 +9188,10 @@ mcpRuntimeCommand
8958
9188
  )
8959
9189
  .option("--image <image:tag>", "docker image to run")
8960
9190
  .option("--update-policy <policy>", "pinned|latest")
9191
+ .option(
9192
+ "--fallback <fallback>",
9193
+ "when endpoint is unreachable: local|fail|skip",
9194
+ )
8961
9195
  .option(
8962
9196
  "--build-local",
8963
9197
  "build MCP Docker image from local package mcp/ directory instead of pulling",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.42",
3
+ "version": "0.3.43",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {