@cubis/foundry 0.3.35 → 0.3.37

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 CHANGED
@@ -273,9 +273,14 @@ cbx workflows install --platform codex --bundle agent-environment-setup --postma
273
273
  --mcp-runtime docker \
274
274
  --mcp-fallback local \
275
275
  --mcp-image ghcr.io/cubis/foundry-mcp:0.1.0 \
276
- --mcp-update-policy pinned
276
+ --mcp-update-policy pinned \
277
+ --mcp-build-local # optional: build image locally instead of docker pull
277
278
  ```
278
279
 
280
+ When `--mcp-runtime docker` is selected and Docker is available, install now prepares the image automatically:
281
+ - Pulls the image by default (`docker pull`)
282
+ - Or builds locally when `--mcp-build-local` is set
283
+
279
284
  MCP tool catalog commands:
280
285
 
281
286
  ```bash
@@ -284,6 +289,22 @@ cbx mcp tools list --service postman --scope global
284
289
  cbx mcp tools list --service stitch --scope global
285
290
  ```
286
291
 
292
+ MCP Docker runtime commands:
293
+
294
+ ```bash
295
+ # Inspect runtime/container state
296
+ cbx mcp runtime status --scope global --name cbx-mcp
297
+
298
+ # Start runtime container (pull/build image first as needed)
299
+ cbx mcp runtime up --scope global --name cbx-mcp --port 3310
300
+
301
+ # Recreate existing container
302
+ cbx mcp runtime up --scope global --name cbx-mcp --replace
303
+
304
+ # Stop/remove runtime container
305
+ cbx mcp runtime down --name cbx-mcp
306
+ ```
307
+
287
308
  Docker E2E MCP check (single command):
288
309
 
289
310
  ```bash
package/bin/cubis.js CHANGED
@@ -151,6 +151,7 @@ const POSTMAN_API_KEY_ENV_VAR = "POSTMAN_API_KEY";
151
151
  const POSTMAN_MCP_URL = "https://mcp.postman.com/minimal";
152
152
  const POSTMAN_API_BASE_URL = "https://api.getpostman.com";
153
153
  const POSTMAN_SKILL_ID = "postman";
154
+ const STITCH_SKILL_ID = "stitch";
154
155
  const STITCH_MCP_SERVER_ID = "StitchMCP";
155
156
  const STITCH_API_KEY_ENV_VAR = "STITCH_API_KEY";
156
157
  const STITCH_MCP_URL = "https://stitch.googleapis.com/mcp";
@@ -167,6 +168,9 @@ const DEFAULT_MCP_RUNTIME = "docker";
167
168
  const DEFAULT_MCP_FALLBACK = "local";
168
169
  const DEFAULT_MCP_UPDATE_POLICY = "pinned";
169
170
  const DEFAULT_MCP_DOCKER_IMAGE = "ghcr.io/cubis/foundry-mcp:0.1.0";
171
+ const DEFAULT_MCP_DOCKER_CONTAINER_NAME = "cbx-mcp";
172
+ const DEFAULT_MCP_DOCKER_HOST_PORT = 3310;
173
+ const MCP_DOCKER_CONTAINER_PORT = 3100;
170
174
  const TECH_SCAN_MAX_FILES = 5000;
171
175
  const TECH_SCAN_IGNORED_DIRS = new Set([
172
176
  ".git",
@@ -404,6 +408,139 @@ function normalizeMcpUpdatePolicy(value, fallback = DEFAULT_MCP_UPDATE_POLICY) {
404
408
  return normalized;
405
409
  }
406
410
 
411
+ function normalizePortNumber(value, fallback = DEFAULT_MCP_DOCKER_HOST_PORT) {
412
+ if (value === undefined || value === null || value === "") return fallback;
413
+ const parsed = Number.parseInt(String(value).trim(), 10);
414
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
415
+ throw new Error(
416
+ `Invalid port '${value}'. Use an integer between 1 and 65535.`,
417
+ );
418
+ }
419
+ return parsed;
420
+ }
421
+
422
+ function parseDockerPsRow(line) {
423
+ const [id, image, status, names] = String(line || "").split("\t");
424
+ if (!id || !names) return null;
425
+ return {
426
+ id: id.trim(),
427
+ image: String(image || "").trim(),
428
+ status: String(status || "").trim(),
429
+ name: String(names || "").trim(),
430
+ };
431
+ }
432
+
433
+ async function checkDockerAvailable({ cwd = process.cwd() } = {}) {
434
+ try {
435
+ await execFile("docker", ["ps"], { cwd });
436
+ return true;
437
+ } catch {
438
+ return false;
439
+ }
440
+ }
441
+
442
+ async function checkDockerImageExists({ image, cwd = process.cwd() }) {
443
+ try {
444
+ await execFile("docker", ["image", "inspect", image], { cwd });
445
+ return true;
446
+ } catch {
447
+ return false;
448
+ }
449
+ }
450
+
451
+ async function pullMcpDockerImage({ image, cwd = process.cwd() }) {
452
+ await execFile("docker", ["pull", image], { cwd });
453
+ return {
454
+ action: "pulled",
455
+ image,
456
+ };
457
+ }
458
+
459
+ async function buildMcpDockerImageLocal({ image, cwd = process.cwd() }) {
460
+ const dockerContext = path.join(packageRoot(), "mcp");
461
+ if (!(await pathExists(path.join(dockerContext, "Dockerfile")))) {
462
+ throw new Error(`MCP Dockerfile is missing at ${dockerContext}.`);
463
+ }
464
+ await execFile("docker", ["build", "-t", image, dockerContext], { cwd });
465
+ return {
466
+ action: "built-local",
467
+ image,
468
+ context: dockerContext,
469
+ };
470
+ }
471
+
472
+ async function ensureMcpDockerImage({
473
+ image,
474
+ updatePolicy,
475
+ buildLocal = false,
476
+ cwd = process.cwd(),
477
+ }) {
478
+ if (buildLocal) {
479
+ return buildMcpDockerImageLocal({ image, cwd });
480
+ }
481
+ if (updatePolicy === "pinned") {
482
+ const exists = await checkDockerImageExists({ image, cwd });
483
+ if (exists) {
484
+ return {
485
+ action: "already-present",
486
+ image,
487
+ };
488
+ }
489
+ }
490
+ return pullMcpDockerImage({ image, cwd });
491
+ }
492
+
493
+ async function inspectDockerContainerByName({
494
+ name,
495
+ cwd = process.cwd(),
496
+ }) {
497
+ try {
498
+ const { stdout } = await execFile(
499
+ "docker",
500
+ [
501
+ "ps",
502
+ "-a",
503
+ "--filter",
504
+ `name=^/${name}$`,
505
+ "--format",
506
+ "{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}",
507
+ ],
508
+ { cwd },
509
+ );
510
+ const row = String(stdout || "")
511
+ .trim()
512
+ .split(/\r?\n/)
513
+ .map((line) => parseDockerPsRow(line))
514
+ .find(Boolean);
515
+ if (!row || row.name !== name) return null;
516
+ return row;
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+
522
+ async function resolveDockerContainerHostPort({
523
+ name,
524
+ containerPort = MCP_DOCKER_CONTAINER_PORT,
525
+ cwd = process.cwd(),
526
+ }) {
527
+ try {
528
+ const { stdout } = await execFile(
529
+ "docker",
530
+ ["port", name, `${containerPort}/tcp`],
531
+ { cwd },
532
+ );
533
+ const line = String(stdout || "")
534
+ .trim()
535
+ .split(/\r?\n/)[0];
536
+ if (!line) return null;
537
+ const match = line.match(/:(\d+)\s*$/);
538
+ return match ? Number.parseInt(match[1], 10) : null;
539
+ } catch {
540
+ return null;
541
+ }
542
+ }
543
+
407
544
  function isPathInsideRoot(targetPath, rootPath) {
408
545
  const target = path.resolve(targetPath);
409
546
  const root = path.resolve(rootPath);
@@ -3803,7 +3940,8 @@ async function resolvePostmanInstallSelection({
3803
3940
  );
3804
3941
  }
3805
3942
 
3806
- const enabled = Boolean(options.postman) || hasWorkspaceOption;
3943
+ const stitchRequested = Boolean(options.stitch);
3944
+ const enabled = Boolean(options.postman) || hasWorkspaceOption || stitchRequested;
3807
3945
  if (!enabled) return { enabled: false };
3808
3946
 
3809
3947
  const envApiKey = normalizePostmanApiKey(process.env[POSTMAN_API_KEY_ENV_VAR]);
@@ -3816,7 +3954,7 @@ async function resolvePostmanInstallSelection({
3816
3954
  : null;
3817
3955
  let mcpScope = requestedMcpScope || normalizeMcpScope(scope, "project");
3818
3956
  const warnings = [];
3819
- const stitchEnabled = platform === "antigravity";
3957
+ const stitchEnabled = platform === "antigravity" || stitchRequested;
3820
3958
  const envStitchApiKey = normalizePostmanApiKey(
3821
3959
  process.env[STITCH_API_KEY_ENV_VAR],
3822
3960
  );
@@ -3834,6 +3972,7 @@ async function resolvePostmanInstallSelection({
3834
3972
  options.mcpUpdatePolicy,
3835
3973
  DEFAULT_MCP_UPDATE_POLICY,
3836
3974
  );
3975
+ const mcpBuildLocal = Boolean(options.mcpBuildLocal);
3837
3976
  const mcpToolSync = options.mcpToolSync !== false;
3838
3977
 
3839
3978
  const canPrompt = !options.yes && process.stdin.isTTY;
@@ -3925,14 +4064,9 @@ async function resolvePostmanInstallSelection({
3925
4064
 
3926
4065
  let effectiveRuntime = requestedRuntime;
3927
4066
  let runtimeSkipped = false;
4067
+ let dockerImageAction = "not-requested";
3928
4068
  if (requestedRuntime === "docker") {
3929
- let dockerAvailable = false;
3930
- try {
3931
- await execFile("docker", ["ps"], { cwd });
3932
- dockerAvailable = true;
3933
- } catch {
3934
- dockerAvailable = false;
3935
- }
4069
+ const dockerAvailable = await checkDockerAvailable({ cwd });
3936
4070
  if (!dockerAvailable) {
3937
4071
  if (requestedFallback === "fail") {
3938
4072
  throw new Error(
@@ -3950,6 +4084,35 @@ async function resolvePostmanInstallSelection({
3950
4084
  "Docker runtime unavailable; falling back to local runtime (--mcp-fallback=local).",
3951
4085
  );
3952
4086
  }
4087
+ } else {
4088
+ try {
4089
+ const ensured = await ensureMcpDockerImage({
4090
+ image: requestedImage,
4091
+ updatePolicy: requestedUpdatePolicy,
4092
+ buildLocal: mcpBuildLocal,
4093
+ cwd,
4094
+ });
4095
+ dockerImageAction = ensured.action;
4096
+ } catch (error) {
4097
+ if (requestedFallback === "fail") {
4098
+ throw new Error(
4099
+ `Docker runtime requested but MCP image preparation failed (${error.message}).`,
4100
+ );
4101
+ }
4102
+ if (requestedFallback === "skip") {
4103
+ runtimeSkipped = true;
4104
+ dockerImageAction = "failed";
4105
+ warnings.push(
4106
+ `MCP Docker image preparation failed; skipping MCP runtime patch because --mcp-fallback=skip. (${error.message})`,
4107
+ );
4108
+ } else {
4109
+ effectiveRuntime = "local";
4110
+ dockerImageAction = "failed";
4111
+ warnings.push(
4112
+ `MCP Docker image preparation failed; falling back to local runtime (--mcp-fallback=local). (${error.message})`,
4113
+ );
4114
+ }
4115
+ }
3953
4116
  }
3954
4117
  }
3955
4118
 
@@ -3991,6 +4154,7 @@ async function resolvePostmanInstallSelection({
3991
4154
  docker: {
3992
4155
  image: requestedImage,
3993
4156
  updatePolicy: requestedUpdatePolicy,
4157
+ buildLocal: mcpBuildLocal,
3994
4158
  },
3995
4159
  catalog: {
3996
4160
  toolSync: mcpToolSync,
@@ -4034,6 +4198,8 @@ async function resolvePostmanInstallSelection({
4034
4198
  mcpFallback: requestedFallback,
4035
4199
  mcpImage: requestedImage,
4036
4200
  mcpUpdatePolicy: requestedUpdatePolicy,
4201
+ mcpBuildLocal,
4202
+ dockerImageAction,
4037
4203
  mcpToolSync,
4038
4204
  runtimeSkipped,
4039
4205
  defaultWorkspaceId: defaultWorkspaceId ?? null,
@@ -4299,6 +4465,8 @@ async function configurePostmanInstallArtifacts({
4299
4465
  mcpFallback: postmanSelection.mcpFallback,
4300
4466
  mcpImage: postmanSelection.mcpImage,
4301
4467
  mcpUpdatePolicy: postmanSelection.mcpUpdatePolicy,
4468
+ mcpBuildLocal: postmanSelection.mcpBuildLocal,
4469
+ dockerImageAction: postmanSelection.dockerImageAction,
4302
4470
  mcpToolSync: postmanSelection.mcpToolSync,
4303
4471
  apiKeySource: effectiveApiKeySource,
4304
4472
  stitchApiKeySource: effectiveStitchApiKeySource,
@@ -5252,6 +5420,8 @@ function printPostmanSetupSummary({ postmanSetup }) {
5252
5420
  console.log(`- MCP fallback: ${postmanSetup.mcpFallback}`);
5253
5421
  console.log(`- MCP image: ${postmanSetup.mcpImage}`);
5254
5422
  console.log(`- MCP update policy: ${postmanSetup.mcpUpdatePolicy}`);
5423
+ console.log(`- MCP build local: ${postmanSetup.mcpBuildLocal ? "yes" : "no"}`);
5424
+ console.log(`- MCP image prepare: ${postmanSetup.dockerImageAction}`);
5255
5425
  console.log(`- MCP tool sync: ${postmanSetup.mcpToolSync ? "enabled" : "disabled"}`);
5256
5426
  console.log(`- Postman API key source: ${postmanSetup.apiKeySource}`);
5257
5427
  if (postmanSetup.stitchApiKeySource) {
@@ -5721,6 +5891,10 @@ function withInstallOptions(command) {
5721
5891
  "--postman",
5722
5892
  "optional: install Postman skill and generate cbx_config.json",
5723
5893
  )
5894
+ .option(
5895
+ "--stitch",
5896
+ "optional: include Stitch MCP profile/config alongside Postman",
5897
+ )
5724
5898
  .option(
5725
5899
  "--postman-api-key <key>",
5726
5900
  "deprecated: inline key mode is disabled. Use env vars + profiles.",
@@ -5757,6 +5931,14 @@ function withInstallOptions(command) {
5757
5931
  "MCP image update policy: pinned|latest",
5758
5932
  DEFAULT_MCP_UPDATE_POLICY,
5759
5933
  )
5934
+ .option(
5935
+ "--mcp-build-local",
5936
+ "build MCP Docker image from local package mcp/ directory instead of pulling",
5937
+ )
5938
+ .option(
5939
+ "--mcp-tool-sync",
5940
+ "enable automatic MCP tool catalog sync (default: enabled)",
5941
+ )
5760
5942
  .option("--no-mcp-tool-sync", "disable automatic MCP tool catalog sync")
5761
5943
  .option(
5762
5944
  "--terminal-integration",
@@ -6079,7 +6261,11 @@ async function runWorkflowInstall(options) {
6079
6261
  scope,
6080
6262
  overwrite: Boolean(options.overwrite),
6081
6263
  profilePathsOverride: artifactProfilePaths,
6082
- extraSkillIds: postmanSelection.enabled ? [POSTMAN_SKILL_ID] : [],
6264
+ extraSkillIds: postmanSelection.enabled
6265
+ ? postmanSelection.stitchEnabled
6266
+ ? [POSTMAN_SKILL_ID, STITCH_SKILL_ID]
6267
+ : [POSTMAN_SKILL_ID]
6268
+ : [],
6083
6269
  skillProfile: skillInstallOptions.skillProfile,
6084
6270
  terminalVerifierSelection,
6085
6271
  dryRun,
@@ -7715,6 +7901,220 @@ async function runMcpToolsList(options) {
7715
7901
  }
7716
7902
  }
7717
7903
 
7904
+ function resolveCbxRootPath({ scope, cwd = process.cwd() }) {
7905
+ if (scope === "global") {
7906
+ return path.join(os.homedir(), ".cbx");
7907
+ }
7908
+ const workspaceRoot = findWorkspaceRoot(cwd);
7909
+ return path.join(workspaceRoot, ".cbx");
7910
+ }
7911
+
7912
+ function resolveMcpRuntimeDefaultsFromConfig(configValue) {
7913
+ const mcp =
7914
+ configValue && typeof configValue.mcp === "object" && !Array.isArray(configValue.mcp)
7915
+ ? configValue.mcp
7916
+ : {};
7917
+ const docker =
7918
+ mcp && typeof mcp.docker === "object" && !Array.isArray(mcp.docker)
7919
+ ? mcp.docker
7920
+ : {};
7921
+
7922
+ const runtime = normalizeMcpRuntime(mcp.runtime, DEFAULT_MCP_RUNTIME);
7923
+ const fallback = normalizeMcpFallback(mcp.fallback, DEFAULT_MCP_FALLBACK);
7924
+ const image =
7925
+ normalizePostmanApiKey(docker.image) || DEFAULT_MCP_DOCKER_IMAGE;
7926
+ const updatePolicy = normalizeMcpUpdatePolicy(
7927
+ docker.updatePolicy,
7928
+ DEFAULT_MCP_UPDATE_POLICY,
7929
+ );
7930
+ const buildLocal = Boolean(docker.buildLocal);
7931
+
7932
+ return {
7933
+ runtime,
7934
+ fallback,
7935
+ image,
7936
+ updatePolicy,
7937
+ buildLocal,
7938
+ };
7939
+ }
7940
+
7941
+ async function loadMcpRuntimeDefaults({ scope, cwd = process.cwd() }) {
7942
+ const configPath = resolveCbxConfigPath({ scope, cwd });
7943
+ const existing = await readJsonFileIfExists(configPath);
7944
+ const configValue =
7945
+ existing.value &&
7946
+ typeof existing.value === "object" &&
7947
+ !Array.isArray(existing.value)
7948
+ ? existing.value
7949
+ : {};
7950
+ return {
7951
+ configPath,
7952
+ defaults: resolveMcpRuntimeDefaultsFromConfig(configValue),
7953
+ hasConfig: existing.exists,
7954
+ };
7955
+ }
7956
+
7957
+ async function runMcpRuntimeStatus(options) {
7958
+ try {
7959
+ const opts = resolveActionOptions(options);
7960
+ const cwd = process.cwd();
7961
+ const scope = normalizeMcpScope(opts.scope, "global");
7962
+ const defaults = await loadMcpRuntimeDefaults({ scope, cwd });
7963
+ const containerName =
7964
+ normalizePostmanApiKey(opts.name) || DEFAULT_MCP_DOCKER_CONTAINER_NAME;
7965
+ const dockerAvailable = await checkDockerAvailable({ cwd });
7966
+ const container = dockerAvailable
7967
+ ? await inspectDockerContainerByName({ name: containerName, cwd })
7968
+ : null;
7969
+
7970
+ console.log(`Scope: ${scope}`);
7971
+ console.log(`Config file: ${defaults.configPath}`);
7972
+ console.log(`Config present: ${defaults.hasConfig ? "yes" : "no"}`);
7973
+ console.log(`Configured runtime: ${defaults.defaults.runtime}`);
7974
+ console.log(`Configured fallback: ${defaults.defaults.fallback}`);
7975
+ console.log(`Configured image: ${defaults.defaults.image}`);
7976
+ console.log(`Configured update policy: ${defaults.defaults.updatePolicy}`);
7977
+ console.log(`Configured build local: ${defaults.defaults.buildLocal ? "yes" : "no"}`);
7978
+ console.log(`Docker available: ${dockerAvailable ? "yes" : "no"}`);
7979
+ console.log(`Container name: ${containerName}`);
7980
+ if (!dockerAvailable) {
7981
+ console.log("Container status: unavailable (docker not reachable)");
7982
+ return;
7983
+ }
7984
+ if (!container) {
7985
+ console.log("Container status: not found");
7986
+ return;
7987
+ }
7988
+ const isRunning = container.status.toLowerCase().startsWith("up ");
7989
+ console.log(`Container status: ${container.status}`);
7990
+ console.log(`Container image: ${container.image}`);
7991
+ if (isRunning) {
7992
+ const hostPort =
7993
+ (await resolveDockerContainerHostPort({
7994
+ name: containerName,
7995
+ containerPort: MCP_DOCKER_CONTAINER_PORT,
7996
+ cwd,
7997
+ })) || DEFAULT_MCP_DOCKER_HOST_PORT;
7998
+ console.log(
7999
+ `Endpoint: http://127.0.0.1:${hostPort}/mcp`,
8000
+ );
8001
+ }
8002
+ } catch (error) {
8003
+ console.error(`\nError: ${error.message}`);
8004
+ process.exit(1);
8005
+ }
8006
+ }
8007
+
8008
+ async function runMcpRuntimeUp(options) {
8009
+ try {
8010
+ const opts = resolveActionOptions(options);
8011
+ const cwd = process.cwd();
8012
+ const scope = normalizeMcpScope(opts.scope, "global");
8013
+ const defaults = await loadMcpRuntimeDefaults({ scope, cwd });
8014
+ const containerName =
8015
+ normalizePostmanApiKey(opts.name) || DEFAULT_MCP_DOCKER_CONTAINER_NAME;
8016
+ const image =
8017
+ normalizePostmanApiKey(opts.image) || defaults.defaults.image;
8018
+ const updatePolicy = normalizeMcpUpdatePolicy(
8019
+ opts.updatePolicy,
8020
+ defaults.defaults.updatePolicy,
8021
+ );
8022
+ const buildLocal = hasCliFlag("--build-local")
8023
+ ? true
8024
+ : defaults.defaults.buildLocal;
8025
+ const hostPort = normalizePortNumber(opts.port, DEFAULT_MCP_DOCKER_HOST_PORT);
8026
+ const replace = Boolean(opts.replace);
8027
+ const dockerAvailable = await checkDockerAvailable({ cwd });
8028
+ if (!dockerAvailable) {
8029
+ throw new Error("Docker is unavailable. Start OrbStack/Docker and retry.");
8030
+ }
8031
+
8032
+ const prepared = await ensureMcpDockerImage({
8033
+ image,
8034
+ updatePolicy,
8035
+ buildLocal,
8036
+ cwd,
8037
+ });
8038
+
8039
+ const existing = await inspectDockerContainerByName({
8040
+ name: containerName,
8041
+ cwd,
8042
+ });
8043
+ if (existing && !replace) {
8044
+ throw new Error(
8045
+ `Container '${containerName}' already exists (${existing.status}). Use --replace to recreate it.`,
8046
+ );
8047
+ }
8048
+ if (existing && replace) {
8049
+ await execFile("docker", ["rm", "-f", containerName], { cwd });
8050
+ }
8051
+
8052
+ const cbxRoot = resolveCbxRootPath({ scope, cwd });
8053
+ await mkdir(cbxRoot, { recursive: true });
8054
+ await execFile(
8055
+ "docker",
8056
+ [
8057
+ "run",
8058
+ "-d",
8059
+ "--name",
8060
+ containerName,
8061
+ "-p",
8062
+ `${hostPort}:${MCP_DOCKER_CONTAINER_PORT}`,
8063
+ "-v",
8064
+ `${cbxRoot}:/root/.cbx`,
8065
+ "-e",
8066
+ "CBX_MCP_TRANSPORT=streamable-http",
8067
+ image,
8068
+ ],
8069
+ { cwd },
8070
+ );
8071
+
8072
+ const running = await inspectDockerContainerByName({
8073
+ name: containerName,
8074
+ cwd,
8075
+ });
8076
+ console.log(`Scope: ${scope}`);
8077
+ console.log(`Container: ${containerName}`);
8078
+ console.log(`Image: ${image}`);
8079
+ console.log(`Image prepare: ${prepared.action}`);
8080
+ console.log(`Update policy: ${updatePolicy}`);
8081
+ console.log(`Build local: ${buildLocal ? "yes" : "no"}`);
8082
+ console.log(`Mount: ${cbxRoot} -> /root/.cbx`);
8083
+ console.log(`Port: ${hostPort}:${MCP_DOCKER_CONTAINER_PORT}`);
8084
+ console.log(`Status: ${running ? running.status : "started"}`);
8085
+ console.log(`Endpoint: http://127.0.0.1:${hostPort}/mcp`);
8086
+ } catch (error) {
8087
+ console.error(`\nError: ${error.message}`);
8088
+ process.exit(1);
8089
+ }
8090
+ }
8091
+
8092
+ async function runMcpRuntimeDown(options) {
8093
+ try {
8094
+ const opts = resolveActionOptions(options);
8095
+ const cwd = process.cwd();
8096
+ const containerName =
8097
+ normalizePostmanApiKey(opts.name) || DEFAULT_MCP_DOCKER_CONTAINER_NAME;
8098
+ const dockerAvailable = await checkDockerAvailable({ cwd });
8099
+ if (!dockerAvailable) {
8100
+ throw new Error("Docker is unavailable. Start OrbStack/Docker and retry.");
8101
+ }
8102
+ const existing = await inspectDockerContainerByName({
8103
+ name: containerName,
8104
+ cwd,
8105
+ });
8106
+ if (!existing) {
8107
+ console.log(`Container '${containerName}' is not present.`);
8108
+ return;
8109
+ }
8110
+ await execFile("docker", ["rm", "-f", containerName], { cwd });
8111
+ console.log(`Removed container '${containerName}'.`);
8112
+ } catch (error) {
8113
+ console.error(`\nError: ${error.message}`);
8114
+ process.exit(1);
8115
+ }
8116
+ }
8117
+
7718
8118
  function printRulesInitSummary({
7719
8119
  platform,
7720
8120
  scope,
@@ -8158,6 +8558,62 @@ mcpToolsCommand
8158
8558
  )
8159
8559
  .action(runMcpToolsList);
8160
8560
 
8561
+ const mcpRuntimeCommand = mcpCommand
8562
+ .command("runtime")
8563
+ .description("Manage local Docker runtime container for Cubis MCP gateway");
8564
+
8565
+ mcpRuntimeCommand
8566
+ .command("status")
8567
+ .description("Show Docker runtime status and configured MCP runtime defaults")
8568
+ .option(
8569
+ "--scope <scope>",
8570
+ "config scope: project|workspace|global|user",
8571
+ "global",
8572
+ )
8573
+ .option(
8574
+ "--name <name>",
8575
+ "container name",
8576
+ DEFAULT_MCP_DOCKER_CONTAINER_NAME,
8577
+ )
8578
+ .action(runMcpRuntimeStatus);
8579
+
8580
+ mcpRuntimeCommand
8581
+ .command("up")
8582
+ .description("Start Docker runtime container for Cubis MCP gateway")
8583
+ .option(
8584
+ "--scope <scope>",
8585
+ "config scope: project|workspace|global|user",
8586
+ "global",
8587
+ )
8588
+ .option(
8589
+ "--name <name>",
8590
+ "container name",
8591
+ DEFAULT_MCP_DOCKER_CONTAINER_NAME,
8592
+ )
8593
+ .option("--image <image:tag>", "docker image to run")
8594
+ .option("--update-policy <policy>", "pinned|latest")
8595
+ .option(
8596
+ "--build-local",
8597
+ "build MCP Docker image from local package mcp/ directory instead of pulling",
8598
+ )
8599
+ .option("--port <port>", "host port to map to container :3100")
8600
+ .option("--replace", "remove existing container with same name before start")
8601
+ .action(runMcpRuntimeUp);
8602
+
8603
+ mcpRuntimeCommand
8604
+ .command("down")
8605
+ .description("Stop and remove Docker runtime container")
8606
+ .option(
8607
+ "--name <name>",
8608
+ "container name",
8609
+ DEFAULT_MCP_DOCKER_CONTAINER_NAME,
8610
+ )
8611
+ .action(runMcpRuntimeDown);
8612
+
8613
+ mcpRuntimeCommand.action(() => {
8614
+ mcpRuntimeCommand.help();
8615
+ });
8616
+
8161
8617
  mcpCommand.action(() => {
8162
8618
  mcpCommand.help();
8163
8619
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.35",
3
+ "version": "0.3.37",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: postman
3
+ description: Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.
4
+ ---
5
+
6
+ # Postman MCP
7
+
8
+ Use this skill when you need to work with Postman through MCP tools.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `POSTMAN_API_KEY_<PROFILE>` for authenticated Postman access.
13
+
14
+ ## Notes
15
+
16
+ - Use environment variables for secrets. Do not inline API keys.
17
+ - Prefer tool discovery (`getEnabledTools`) before making assumptions about available tool sets.
@@ -741,6 +741,16 @@
741
741
  "visual testing"
742
742
  ]
743
743
  },
744
+ {
745
+ "id": "postman",
746
+ "name": "postman",
747
+ "canonical_id": "postman",
748
+ "deprecated": false,
749
+ "replaced_by": null,
750
+ "path": ".agents/skills/postman/SKILL.md",
751
+ "description": "Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.",
752
+ "triggers": []
753
+ },
744
754
  {
745
755
  "id": "powershell-windows",
746
756
  "name": "powershell-windows",
@@ -980,6 +990,16 @@
980
990
  "description": "Runs CodeQL-based static security analysis (database build, query pack selection, and SARIF results) for vulnerability discovery and audits. Not for custom QL authoring or CI/CD setup.",
981
991
  "triggers": []
982
992
  },
993
+ {
994
+ "id": "stitch",
995
+ "name": "stitch",
996
+ "canonical_id": "stitch",
997
+ "deprecated": false,
998
+ "replaced_by": null,
999
+ "path": ".agents/skills/stitch/SKILL.md",
1000
+ "description": "Automate Stitch workflows through Stitch MCP using mcp-remote transport.",
1001
+ "triggers": []
1002
+ },
983
1003
  {
984
1004
  "id": "stripe-best-practices",
985
1005
  "name": "stripe-best-practices",
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: stitch
3
+ description: Automate Stitch workflows through Stitch MCP using mcp-remote transport.
4
+ ---
5
+
6
+ # Stitch MCP
7
+
8
+ Use this skill when you need to call Stitch tools through MCP passthrough.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `STITCH_API_KEY_<PROFILE>` for Stitch API-key mode.
13
+
14
+ ## Notes
15
+
16
+ - Stitch transport uses `mcp-remote` to `https://stitch.googleapis.com/mcp`.
17
+ - Inject `X-Goog-Api-Key` from environment at runtime. Do not store raw keys in config files.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: postman
3
+ description: Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.
4
+ ---
5
+
6
+ # Postman MCP
7
+
8
+ Use this skill when you need to work with Postman through MCP tools.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `POSTMAN_API_KEY_<PROFILE>` for authenticated Postman access.
13
+
14
+ ## Notes
15
+
16
+ - Use environment variables for secrets. Do not inline API keys.
17
+ - Prefer tool discovery (`getEnabledTools`) before making assumptions about available tool sets.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: stitch
3
+ description: Automate Stitch workflows through Stitch MCP using mcp-remote transport.
4
+ ---
5
+
6
+ # Stitch MCP
7
+
8
+ Use this skill when you need to call Stitch tools through MCP passthrough.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `STITCH_API_KEY_<PROFILE>` for Stitch API-key mode.
13
+
14
+ ## Notes
15
+
16
+ - Stitch transport uses `mcp-remote` to `https://stitch.googleapis.com/mcp`.
17
+ - Inject `X-Goog-Api-Key` from environment at runtime. Do not store raw keys in config files.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: postman
3
+ description: Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.
4
+ ---
5
+
6
+ # Postman MCP
7
+
8
+ Use this skill when you need to work with Postman through MCP tools.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `POSTMAN_API_KEY_<PROFILE>` for authenticated Postman access.
13
+
14
+ ## Notes
15
+
16
+ - Use environment variables for secrets. Do not inline API keys.
17
+ - Prefer tool discovery (`getEnabledTools`) before making assumptions about available tool sets.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: stitch
3
+ description: Automate Stitch workflows through Stitch MCP using mcp-remote transport.
4
+ ---
5
+
6
+ # Stitch MCP
7
+
8
+ Use this skill when you need to call Stitch tools through MCP passthrough.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `STITCH_API_KEY_<PROFILE>` for Stitch API-key mode.
13
+
14
+ ## Notes
15
+
16
+ - Stitch transport uses `mcp-remote` to `https://stitch.googleapis.com/mcp`.
17
+ - Inject `X-Goog-Api-Key` from environment at runtime. Do not store raw keys in config files.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: postman
3
+ description: Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.
4
+ ---
5
+
6
+ # Postman MCP
7
+
8
+ Use this skill when you need to work with Postman through MCP tools.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `POSTMAN_API_KEY_<PROFILE>` for authenticated Postman access.
13
+
14
+ ## Notes
15
+
16
+ - Use environment variables for secrets. Do not inline API keys.
17
+ - Prefer tool discovery (`getEnabledTools`) before making assumptions about available tool sets.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: stitch
3
+ description: Automate Stitch workflows through Stitch MCP using mcp-remote transport.
4
+ ---
5
+
6
+ # Stitch MCP
7
+
8
+ Use this skill when you need to call Stitch tools through MCP passthrough.
9
+
10
+ ## Required Environment Variables
11
+
12
+ - `STITCH_API_KEY_<PROFILE>` for Stitch API-key mode.
13
+
14
+ ## Notes
15
+
16
+ - Stitch transport uses `mcp-remote` to `https://stitch.googleapis.com/mcp`.
17
+ - Inject `X-Goog-Api-Key` from environment at runtime. Do not store raw keys in config files.