@cubis/foundry 0.3.50 → 0.3.52

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 (32) hide show
  1. package/README.md +31 -12
  2. package/bin/cubis.js +662 -80
  3. package/mcp/dist/index.js +108 -46
  4. package/mcp/src/server.ts +51 -39
  5. package/mcp/src/upstream/passthrough.test.ts +30 -0
  6. package/mcp/src/upstream/passthrough.ts +113 -17
  7. package/package.json +1 -1
  8. package/workflows/skills/postman/SKILL.md +76 -7
  9. package/workflows/skills/postman/references/full-mode-setup.md +36 -0
  10. package/workflows/skills/postman/references/troubleshooting.md +25 -0
  11. package/workflows/skills/postman/references/workspace-policy.md +20 -0
  12. package/workflows/workflows/agent-environment-setup/manifest.json +1 -1
  13. package/workflows/workflows/agent-environment-setup/platforms/antigravity/rules/GEMINI.md +10 -1
  14. package/workflows/workflows/agent-environment-setup/platforms/antigravity/workflows/database.md +1 -1
  15. package/workflows/workflows/agent-environment-setup/platforms/codex/rules/AGENTS.md +10 -1
  16. package/workflows/workflows/agent-environment-setup/platforms/codex/workflows/database.md +1 -1
  17. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +14 -4
  18. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +11 -2
  19. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/SKILL.md +76 -7
  20. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/full-mode-setup.md +36 -0
  21. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/troubleshooting.md +25 -0
  22. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/workspace-policy.md +20 -0
  23. package/workflows/workflows/agent-environment-setup/platforms/copilot/workflows/database.md +1 -1
  24. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/SKILL.md +76 -7
  25. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/full-mode-setup.md +36 -0
  26. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/troubleshooting.md +25 -0
  27. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/workspace-policy.md +20 -0
  28. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/SKILL.md +76 -7
  29. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/full-mode-setup.md +36 -0
  30. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/troubleshooting.md +25 -0
  31. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/workspace-policy.md +20 -0
  32. package/workflows/workflows/agent-environment-setup/shared/workflows/database.md +1 -1
package/mcp/dist/index.js CHANGED
@@ -1539,7 +1539,7 @@ var TOOL_REGISTRY = [
1539
1539
  ];
1540
1540
 
1541
1541
  // src/upstream/passthrough.ts
1542
- import { mkdir, writeFile } from "fs/promises";
1542
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
1543
1543
  import path6 from "path";
1544
1544
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1545
1545
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -1550,6 +1550,45 @@ function resolveCatalogDir(configPath) {
1550
1550
  }
1551
1551
  return path6.join(configDir, ".cbx", "mcp", "catalog");
1552
1552
  }
1553
+ function buildPassthroughAliasName(service, toolName) {
1554
+ const normalizedTool = String(toolName || "").trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_").toLowerCase();
1555
+ if (!normalizedTool) return `${service}_tool`;
1556
+ return `${service}_${normalizedTool}`;
1557
+ }
1558
+ function buildUpstreamToolInfo(service, tool) {
1559
+ const name = typeof tool?.name === "string" ? tool.name.trim() : "";
1560
+ if (!name) return null;
1561
+ const namespacedName = `${service}.${name}`;
1562
+ const aliasNames = [buildPassthroughAliasName(service, name)];
1563
+ return {
1564
+ name,
1565
+ namespacedName,
1566
+ aliasNames,
1567
+ description: typeof tool?.description === "string" ? tool.description : void 0,
1568
+ inputSchema: tool?.inputSchema ?? void 0,
1569
+ outputSchema: tool?.outputSchema ?? void 0
1570
+ };
1571
+ }
1572
+ async function loadCachedCatalogTools({
1573
+ service,
1574
+ scope,
1575
+ configPath
1576
+ }) {
1577
+ if (!configPath) return [];
1578
+ const catalogDir = resolveCatalogDir(configPath);
1579
+ const catalogPath = path6.join(catalogDir, `${service}.json`);
1580
+ try {
1581
+ const raw = await readFile2(catalogPath, "utf8");
1582
+ const parsed = JSON.parse(raw);
1583
+ if (scope && typeof parsed.scope === "string" && parsed.scope.trim() && parsed.scope !== scope) {
1584
+ return [];
1585
+ }
1586
+ const tools = Array.isArray(parsed.tools) ? parsed.tools : [];
1587
+ return tools.map((tool) => buildUpstreamToolInfo(service, tool)).filter((tool) => Boolean(tool));
1588
+ } catch {
1589
+ return [];
1590
+ }
1591
+ }
1553
1592
  function getServiceAuth(config, service) {
1554
1593
  if (service === "postman") {
1555
1594
  const state2 = parsePostmanState(config);
@@ -1646,8 +1685,8 @@ async function persistCatalog(catalog) {
1646
1685
  await writeFile(catalogPath, `${JSON.stringify(payload, null, 2)}
1647
1686
  `, "utf8");
1648
1687
  }
1649
- async function discoverUpstreamCatalogs() {
1650
- const effective = readEffectiveConfig("auto");
1688
+ async function discoverUpstreamCatalogs(scope = "auto") {
1689
+ const effective = readEffectiveConfig(scope);
1651
1690
  if (!effective) {
1652
1691
  const missing = {
1653
1692
  service: "postman",
@@ -1686,6 +1725,16 @@ async function discoverUpstreamCatalogs() {
1686
1725
  discoveryError: auth.error
1687
1726
  };
1688
1727
  if (!auth.configured || !auth.mcpUrl || auth.error) {
1728
+ const cachedTools = await loadCachedCatalogTools({
1729
+ service,
1730
+ scope: baseInfo.scope,
1731
+ configPath: baseInfo.configPath
1732
+ });
1733
+ if (cachedTools.length > 0) {
1734
+ catalog.tools = cachedTools;
1735
+ catalog.toolCount = cachedTools.length;
1736
+ catalog.discoveryError = auth.error ? `${auth.error} (using cached tool catalog)` : "Upstream not configured; using cached tool catalog";
1737
+ }
1689
1738
  await persistCatalog(catalog);
1690
1739
  return catalog;
1691
1740
  }
@@ -1696,20 +1745,20 @@ async function discoverUpstreamCatalogs() {
1696
1745
  fn: async (client) => client.listTools()
1697
1746
  });
1698
1747
  const rawTools = Array.isArray(listed.tools) ? listed.tools : [];
1699
- catalog.tools = rawTools.map((tool) => {
1700
- const name = typeof tool?.name === "string" ? tool.name.trim() : "";
1701
- if (!name) return null;
1702
- return {
1703
- name,
1704
- namespacedName: `${service}.${name}`,
1705
- description: typeof tool?.description === "string" ? tool.description : void 0,
1706
- inputSchema: tool?.inputSchema ?? void 0,
1707
- outputSchema: tool?.outputSchema ?? void 0
1708
- };
1709
- }).filter((tool) => Boolean(tool));
1748
+ catalog.tools = rawTools.map((tool) => buildUpstreamToolInfo(service, tool || {})).filter((tool) => Boolean(tool));
1710
1749
  catalog.toolCount = catalog.tools.length;
1711
1750
  } catch (error) {
1712
1751
  catalog.discoveryError = `Tool discovery failed: ${String(error)}`;
1752
+ const cachedTools = await loadCachedCatalogTools({
1753
+ service,
1754
+ scope: baseInfo.scope,
1755
+ configPath: baseInfo.configPath
1756
+ });
1757
+ if (cachedTools.length > 0) {
1758
+ catalog.tools = cachedTools;
1759
+ catalog.toolCount = cachedTools.length;
1760
+ catalog.discoveryError = `${catalog.discoveryError} (using cached tool catalog)`;
1761
+ }
1713
1762
  }
1714
1763
  await persistCatalog(catalog);
1715
1764
  return catalog;
@@ -1722,9 +1771,10 @@ async function discoverUpstreamCatalogs() {
1722
1771
  async function callUpstreamTool({
1723
1772
  service,
1724
1773
  name,
1725
- argumentsValue
1774
+ argumentsValue,
1775
+ scope = "auto"
1726
1776
  }) {
1727
- const effective = readEffectiveConfig("auto");
1777
+ const effective = readEffectiveConfig(scope);
1728
1778
  if (!effective) {
1729
1779
  throw new Error("cbx_config.json not found");
1730
1780
  }
@@ -1799,40 +1849,52 @@ async function createServer({
1799
1849
  logger.debug(
1800
1850
  `Registered ${TOOL_REGISTRY.length} built-in tools from registry`
1801
1851
  );
1802
- const upstreamCatalogs = await discoverUpstreamCatalogs();
1852
+ const upstreamCatalogs = await discoverUpstreamCatalogs(defaultConfigScope);
1803
1853
  const dynamicSchema = z13.object({}).passthrough();
1854
+ const registeredDynamicToolNames = /* @__PURE__ */ new Set();
1804
1855
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
1805
1856
  for (const tool of catalog.tools) {
1806
- const namespaced = tool.namespacedName;
1807
- server.registerTool(
1808
- namespaced,
1809
- {
1810
- description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
1811
- inputSchema: dynamicSchema,
1812
- annotations: {}
1813
- },
1814
- async (args) => {
1815
- try {
1816
- const result = await callUpstreamTool({
1817
- service: catalog.service,
1818
- name: tool.name,
1819
- argumentsValue: args && typeof args === "object" ? args : {}
1820
- });
1821
- return {
1822
- // SDK content is typed broadly; cast to the expected array shape.
1823
- content: result.content ?? [],
1824
- structuredContent: result.structuredContent,
1825
- isError: Boolean(result.isError)
1826
- };
1827
- } catch (error) {
1828
- return toolCallErrorResult({
1829
- service: catalog.service,
1830
- namespacedName: namespaced,
1831
- error
1832
- });
1833
- }
1857
+ const registrationNames = [tool.namespacedName, ...tool.aliasNames || []];
1858
+ const uniqueRegistrationNames = [...new Set(registrationNames)];
1859
+ for (const registrationName of uniqueRegistrationNames) {
1860
+ if (registeredDynamicToolNames.has(registrationName)) {
1861
+ logger.warn(
1862
+ `Skipping duplicate dynamic tool registration name '${registrationName}' from ${catalog.service}.${tool.name}`
1863
+ );
1864
+ continue;
1834
1865
  }
1835
- );
1866
+ registeredDynamicToolNames.add(registrationName);
1867
+ server.registerTool(
1868
+ registrationName,
1869
+ {
1870
+ description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
1871
+ inputSchema: dynamicSchema,
1872
+ annotations: {}
1873
+ },
1874
+ async (args) => {
1875
+ try {
1876
+ const result = await callUpstreamTool({
1877
+ service: catalog.service,
1878
+ name: tool.name,
1879
+ argumentsValue: args && typeof args === "object" ? args : {},
1880
+ scope: defaultConfigScope
1881
+ });
1882
+ return {
1883
+ // SDK content is typed broadly; cast to the expected array shape.
1884
+ content: result.content ?? [],
1885
+ structuredContent: result.structuredContent,
1886
+ isError: Boolean(result.isError)
1887
+ };
1888
+ } catch (error) {
1889
+ return toolCallErrorResult({
1890
+ service: catalog.service,
1891
+ namespacedName: registrationName,
1892
+ error
1893
+ });
1894
+ }
1895
+ }
1896
+ );
1897
+ }
1836
1898
  }
1837
1899
  }
1838
1900
  return server;
package/mcp/src/server.ts CHANGED
@@ -92,49 +92,61 @@ export async function createServer({
92
92
  );
93
93
 
94
94
  // ─── Dynamic upstream passthrough tools ────────────────────
95
- const upstreamCatalogs = await discoverUpstreamCatalogs();
95
+ const upstreamCatalogs = await discoverUpstreamCatalogs(defaultConfigScope);
96
96
  const dynamicSchema = z.object({}).passthrough();
97
+ const registeredDynamicToolNames = new Set<string>();
97
98
 
98
99
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
99
100
  for (const tool of catalog.tools) {
100
- const namespaced = tool.namespacedName;
101
- server.registerTool(
102
- namespaced,
103
- {
104
- description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
105
- inputSchema: dynamicSchema,
106
- annotations: {},
107
- },
108
- async (args: Record<string, unknown>) => {
109
- try {
110
- const result = await callUpstreamTool({
111
- service: catalog.service,
112
- name: tool.name,
113
- argumentsValue:
114
- args && typeof args === "object"
115
- ? args
116
- : ({} as Record<string, unknown>),
117
- });
118
- return {
119
- // SDK content is typed broadly; cast to the expected array shape.
120
- content: (result.content ?? []) as Array<{
121
- type: string;
122
- [k: string]: unknown;
123
- }>,
124
- structuredContent: result.structuredContent as
125
- | Record<string, unknown>
126
- | undefined,
127
- isError: Boolean(result.isError),
128
- };
129
- } catch (error) {
130
- return toolCallErrorResult({
131
- service: catalog.service,
132
- namespacedName: namespaced,
133
- error,
134
- });
135
- }
136
- },
137
- );
101
+ const registrationNames = [tool.namespacedName, ...(tool.aliasNames || [])];
102
+ const uniqueRegistrationNames = [...new Set(registrationNames)];
103
+ for (const registrationName of uniqueRegistrationNames) {
104
+ if (registeredDynamicToolNames.has(registrationName)) {
105
+ logger.warn(
106
+ `Skipping duplicate dynamic tool registration name '${registrationName}' from ${catalog.service}.${tool.name}`,
107
+ );
108
+ continue;
109
+ }
110
+ registeredDynamicToolNames.add(registrationName);
111
+ server.registerTool(
112
+ registrationName,
113
+ {
114
+ description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
115
+ inputSchema: dynamicSchema,
116
+ annotations: {},
117
+ },
118
+ async (args: Record<string, unknown>) => {
119
+ try {
120
+ const result = await callUpstreamTool({
121
+ service: catalog.service,
122
+ name: tool.name,
123
+ argumentsValue:
124
+ args && typeof args === "object"
125
+ ? args
126
+ : ({} as Record<string, unknown>),
127
+ scope: defaultConfigScope,
128
+ });
129
+ return {
130
+ // SDK content is typed broadly; cast to the expected array shape.
131
+ content: (result.content ?? []) as Array<{
132
+ type: string;
133
+ [k: string]: unknown;
134
+ }>,
135
+ structuredContent: result.structuredContent as
136
+ | Record<string, unknown>
137
+ | undefined,
138
+ isError: Boolean(result.isError),
139
+ };
140
+ } catch (error) {
141
+ return toolCallErrorResult({
142
+ service: catalog.service,
143
+ namespacedName: registrationName,
144
+ error,
145
+ });
146
+ }
147
+ },
148
+ );
149
+ }
138
150
  }
139
151
  }
140
152
 
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildPassthroughAliasName,
4
+ buildUpstreamToolInfo,
5
+ } from "./passthrough.js";
6
+
7
+ describe("upstream passthrough aliasing", () => {
8
+ it("builds deterministic underscore aliases for postman tools", () => {
9
+ expect(buildPassthroughAliasName("postman", "runCollection")).toBe(
10
+ "postman_run_collection",
11
+ );
12
+ expect(buildPassthroughAliasName("postman", "get-workspaces")).toBe(
13
+ "postman_get_workspaces",
14
+ );
15
+ });
16
+
17
+ it("maps upstream tools to namespaced + alias registrations", () => {
18
+ const info = buildUpstreamToolInfo("stitch", {
19
+ name: "list_tools",
20
+ description: "List available tools",
21
+ });
22
+
23
+ expect(info).toMatchObject({
24
+ name: "list_tools",
25
+ namespacedName: "stitch.list_tools",
26
+ aliasNames: ["stitch_list_tools"],
27
+ description: "List available tools",
28
+ });
29
+ });
30
+ });
@@ -2,7 +2,7 @@
2
2
  * Cubis Foundry MCP Server – upstream Postman/Stitch passthrough support.
3
3
  */
4
4
 
5
- import { mkdir, writeFile } from "node:fs/promises";
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
8
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -11,13 +11,14 @@ import {
11
11
  parseStitchState,
12
12
  readEffectiveConfig,
13
13
  } from "../cbxConfig/index.js";
14
- import type { CbxConfig } from "../cbxConfig/types.js";
14
+ import type { CbxConfig, ConfigScope } from "../cbxConfig/types.js";
15
15
 
16
16
  type ServiceId = "postman" | "stitch";
17
17
 
18
18
  export interface UpstreamToolInfo {
19
19
  name: string;
20
20
  namespacedName: string;
21
+ aliasNames: string[];
21
22
  description?: string;
22
23
  inputSchema?: unknown;
23
24
  outputSchema?: unknown;
@@ -44,6 +45,86 @@ function resolveCatalogDir(configPath: string): string {
44
45
  return path.join(configDir, ".cbx", "mcp", "catalog");
45
46
  }
46
47
 
48
+ export function buildPassthroughAliasName(
49
+ service: ServiceId,
50
+ toolName: string,
51
+ ): string {
52
+ const normalizedTool = String(toolName || "")
53
+ .trim()
54
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
55
+ .replace(/[^A-Za-z0-9]+/g, "_")
56
+ .replace(/^_+|_+$/g, "")
57
+ .replace(/_+/g, "_")
58
+ .toLowerCase();
59
+ if (!normalizedTool) return `${service}_tool`;
60
+ return `${service}_${normalizedTool}`;
61
+ }
62
+
63
+ export function buildUpstreamToolInfo(
64
+ service: ServiceId,
65
+ tool: {
66
+ name?: unknown;
67
+ description?: unknown;
68
+ inputSchema?: unknown;
69
+ outputSchema?: unknown;
70
+ },
71
+ ): UpstreamToolInfo | null {
72
+ const name = typeof tool?.name === "string" ? tool.name.trim() : "";
73
+ if (!name) return null;
74
+ const namespacedName = `${service}.${name}`;
75
+ const aliasNames = [buildPassthroughAliasName(service, name)];
76
+ return {
77
+ name,
78
+ namespacedName,
79
+ aliasNames,
80
+ description:
81
+ typeof tool?.description === "string" ? tool.description : undefined,
82
+ inputSchema: tool?.inputSchema ?? undefined,
83
+ outputSchema: tool?.outputSchema ?? undefined,
84
+ };
85
+ }
86
+
87
+ async function loadCachedCatalogTools({
88
+ service,
89
+ scope,
90
+ configPath,
91
+ }: {
92
+ service: ServiceId;
93
+ scope: ConfigScope | null;
94
+ configPath: string | null;
95
+ }): Promise<UpstreamToolInfo[]> {
96
+ if (!configPath) return [];
97
+ const catalogDir = resolveCatalogDir(configPath);
98
+ const catalogPath = path.join(catalogDir, `${service}.json`);
99
+ try {
100
+ const raw = await readFile(catalogPath, "utf8");
101
+ const parsed = JSON.parse(raw) as {
102
+ scope?: string;
103
+ tools?: Array<{
104
+ name?: unknown;
105
+ description?: unknown;
106
+ inputSchema?: unknown;
107
+ outputSchema?: unknown;
108
+ }>;
109
+ };
110
+ if (
111
+ scope &&
112
+ typeof parsed.scope === "string" &&
113
+ parsed.scope.trim() &&
114
+ parsed.scope !== scope
115
+ ) {
116
+ // Cross-scope catalogs can be stale for the active config.
117
+ return [];
118
+ }
119
+ const tools = Array.isArray(parsed.tools) ? parsed.tools : [];
120
+ return tools
121
+ .map((tool) => buildUpstreamToolInfo(service, tool))
122
+ .filter((tool): tool is UpstreamToolInfo => Boolean(tool));
123
+ } catch {
124
+ return [];
125
+ }
126
+ }
127
+
47
128
  function getServiceAuth(config: CbxConfig, service: ServiceId): {
48
129
  mcpUrl: string | null;
49
130
  activeProfileName: string | null;
@@ -154,11 +235,13 @@ async function persistCatalog(catalog: UpstreamCatalog): Promise<void> {
154
235
  await writeFile(catalogPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
155
236
  }
156
237
 
157
- export async function discoverUpstreamCatalogs(): Promise<{
238
+ export async function discoverUpstreamCatalogs(
239
+ scope: ConfigScope | "auto" = "auto",
240
+ ): Promise<{
158
241
  postman: UpstreamCatalog;
159
242
  stitch: UpstreamCatalog;
160
243
  }> {
161
- const effective = readEffectiveConfig("auto");
244
+ const effective = readEffectiveConfig(scope);
162
245
  if (!effective) {
163
246
  const missing: UpstreamCatalog = {
164
247
  service: "postman",
@@ -200,6 +283,18 @@ export async function discoverUpstreamCatalogs(): Promise<{
200
283
  };
201
284
 
202
285
  if (!auth.configured || !auth.mcpUrl || auth.error) {
286
+ const cachedTools = await loadCachedCatalogTools({
287
+ service,
288
+ scope: baseInfo.scope,
289
+ configPath: baseInfo.configPath,
290
+ });
291
+ if (cachedTools.length > 0) {
292
+ catalog.tools = cachedTools;
293
+ catalog.toolCount = cachedTools.length;
294
+ catalog.discoveryError = auth.error
295
+ ? `${auth.error} (using cached tool catalog)`
296
+ : "Upstream not configured; using cached tool catalog";
297
+ }
203
298
  await persistCatalog(catalog);
204
299
  return catalog;
205
300
  }
@@ -212,22 +307,21 @@ export async function discoverUpstreamCatalogs(): Promise<{
212
307
  });
213
308
  const rawTools = Array.isArray(listed.tools) ? listed.tools : [];
214
309
  catalog.tools = rawTools
215
- .map((tool) => {
216
- const name = typeof tool?.name === "string" ? tool.name.trim() : "";
217
- if (!name) return null;
218
- return {
219
- name,
220
- namespacedName: `${service}.${name}`,
221
- description:
222
- typeof tool?.description === "string" ? tool.description : undefined,
223
- inputSchema: tool?.inputSchema ?? undefined,
224
- outputSchema: tool?.outputSchema ?? undefined,
225
- } satisfies UpstreamToolInfo;
226
- })
310
+ .map((tool) => buildUpstreamToolInfo(service, tool || {}))
227
311
  .filter((tool): tool is UpstreamToolInfo => Boolean(tool));
228
312
  catalog.toolCount = catalog.tools.length;
229
313
  } catch (error) {
230
314
  catalog.discoveryError = `Tool discovery failed: ${String(error)}`;
315
+ const cachedTools = await loadCachedCatalogTools({
316
+ service,
317
+ scope: baseInfo.scope,
318
+ configPath: baseInfo.configPath,
319
+ });
320
+ if (cachedTools.length > 0) {
321
+ catalog.tools = cachedTools;
322
+ catalog.toolCount = cachedTools.length;
323
+ catalog.discoveryError = `${catalog.discoveryError} (using cached tool catalog)`;
324
+ }
231
325
  }
232
326
 
233
327
  await persistCatalog(catalog);
@@ -244,12 +338,14 @@ export async function callUpstreamTool({
244
338
  service,
245
339
  name,
246
340
  argumentsValue,
341
+ scope = "auto",
247
342
  }: {
248
343
  service: ServiceId;
249
344
  name: string;
250
345
  argumentsValue: Record<string, unknown>;
346
+ scope?: ConfigScope | "auto";
251
347
  }) {
252
- const effective = readEffectiveConfig("auto");
348
+ const effective = readEffectiveConfig(scope);
253
349
  if (!effective) {
254
350
  throw new Error("cbx_config.json not found");
255
351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.50",
3
+ "version": "0.3.52",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,17 +1,86 @@
1
1
  ---
2
2
  name: postman
3
- description: Automate API testing and collection management with Postman MCP. Use for workspace, collection, environment, and mock operations.
3
+ description: Automate API testing and collection management with Postman MCP using full-mode defaults and workspace-aware execution.
4
4
  ---
5
5
 
6
- # Postman MCP
6
+ # Postman MCP Skill
7
7
 
8
- Use this skill when you need to work with Postman through MCP tools.
8
+ Use this skill when you need Postman workspace, collection, environment, or run operations through MCP.
9
9
 
10
- ## Required Environment Variables
10
+ References:
11
+ - [Full-Mode Setup](./references/full-mode-setup.md)
12
+ - [Workspace Policy](./references/workspace-policy.md)
13
+ - [Troubleshooting](./references/troubleshooting.md)
11
14
 
12
- - `POSTMAN_API_KEY_<PROFILE>` for authenticated Postman access.
15
+ ## MCP-First Rule
13
16
 
14
- ## Notes
17
+ - Prefer Postman MCP tools for all Postman operations.
18
+ - Accept both dynamic naming styles from clients:
19
+ - dotted: `postman.<tool>`
20
+ - alias: `postman_<tool>`
21
+ - Do not use Newman/Postman CLI fallback unless the user explicitly asks for fallback.
22
+ - If required Postman MCP tools are unavailable, report discovery/remediation steps first.
23
+
24
+ ## Setup Baseline
25
+
26
+ 1. Install with Postman enabled and explicit full mode:
27
+ - `cbx workflows install --platform <codex|antigravity|copilot> --scope global --bundle agent-environment-setup --postman --postman-mode full --mcp-runtime docker --mcp-fallback local --mcp-tool-sync --yes`
28
+ 2. Persist env aliases once (no per-session re-export):
29
+ - `cbx workflows config keys persist-env --service postman --scope global`
30
+ 3. Verify mode/config:
31
+ - `cbx workflows config --scope global --show`
32
+ - `cbx mcp tools sync --service postman --scope global`
33
+ - `cbx mcp tools list --service postman --scope global`
34
+
35
+ ## Preflight
36
+
37
+ 1. Read Postman status first:
38
+ - Call `postman_get_status`.
39
+ 2. Ensure mode is `full`:
40
+ - If not, call `postman_set_mode` with `mode: full`.
41
+ 3. Discover upstream tools:
42
+ - Confirm required tools exist before execution (for example workspaces/collections/runs).
43
+
44
+ ## Default Workspace Policy
45
+
46
+ Resolve workspace in this order:
47
+
48
+ 1. User-provided workspace ID.
49
+ 2. `postman_get_status.defaultWorkspaceId`.
50
+ 3. Auto-detect from workspace listing:
51
+ - If exactly one workspace exists, use it and state that choice.
52
+ 4. If multiple workspaces and no default:
53
+ - Ask user to choose one.
54
+ - Recommend persisting it with:
55
+ - `cbx workflows config --scope global --workspace-id <workspace-id>`
56
+
57
+ When a Postman tool requires a workspace argument, always pass the resolved workspace ID explicitly.
58
+
59
+ ## Common Workflows
60
+
61
+ ### Collection Run
62
+
63
+ 1. Resolve workspace ID (policy above).
64
+ 2. Resolve `collectionId` and optional `environmentId`.
65
+ 3. Call Postman run tool (`postman.runCollection` or alias equivalent).
66
+ 4. Return a concise run summary:
67
+ - total requests
68
+ - passed/failed tests
69
+ - failing request/test names
70
+ - proposed fix path for failures
71
+
72
+ ## Failure Handling
73
+
74
+ If dynamic Postman tools are missing:
75
+
76
+ 1. Verify env alias expected by config is set.
77
+ 2. Resync catalog:
78
+ - `cbx mcp tools sync --service postman --scope global`
79
+ - `cbx mcp tools list --service postman --scope global`
80
+ 3. Recreate runtime if needed:
81
+ - `cbx mcp runtime up --scope global --name cbx-mcp --replace --port 3310 --skills-root ~/.agents/skills`
82
+
83
+ ## Security Notes
15
84
 
16
85
  - Use environment variables for secrets. Do not inline API keys.
17
- - Prefer tool discovery (`getEnabledTools`) before making assumptions about available tool sets.
86
+ - Never print or persist raw key values in logs, docs, or responses.
@@ -0,0 +1,36 @@
1
+ # Postman Full-Mode Setup
2
+
3
+ Use CLI mode flags instead of manual `jq` edits.
4
+
5
+ ## Install/Upgrade with Full Mode
6
+
7
+ ```bash
8
+ cbx workflows install \
9
+ --platform codex \
10
+ --scope global \
11
+ --bundle agent-environment-setup \
12
+ --postman \
13
+ --postman-mode full \
14
+ --mcp-runtime docker \
15
+ --mcp-fallback local \
16
+ --mcp-tool-sync \
17
+ --overwrite \
18
+ --yes
19
+ ```
20
+
21
+ ## Change Mode Later
22
+
23
+ ```bash
24
+ cbx workflows config --scope global --platform codex --postman-mode full
25
+ ```
26
+
27
+ This updates:
28
+ - `cbx_config.json` Postman URL
29
+ - managed Postman MCP definition under `.cbx/mcp/<platform>/postman.json`
30
+ - platform MCP runtime target patch (when platform is resolvable)
31
+
32
+ ## Persist Env Aliases
33
+
34
+ ```bash
35
+ cbx workflows config keys persist-env --service postman --scope global
36
+ ```