@cubis/foundry 0.3.51 → 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 (30) hide show
  1. package/README.md +26 -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 +31 -25
  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/workflows/database.md +1 -1
  14. package/workflows/workflows/agent-environment-setup/platforms/codex/workflows/database.md +1 -1
  15. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +4 -3
  16. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +1 -1
  17. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/SKILL.md +31 -25
  18. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/full-mode-setup.md +36 -0
  19. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/troubleshooting.md +25 -0
  20. package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/references/workspace-policy.md +20 -0
  21. package/workflows/workflows/agent-environment-setup/platforms/copilot/workflows/database.md +1 -1
  22. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/SKILL.md +31 -25
  23. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/full-mode-setup.md +36 -0
  24. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/troubleshooting.md +25 -0
  25. package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/references/workspace-policy.md +20 -0
  26. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/SKILL.md +31 -25
  27. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/full-mode-setup.md +36 -0
  28. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/troubleshooting.md +25 -0
  29. package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/references/workspace-policy.md +20 -0
  30. 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.51",
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,41 +1,53 @@
1
1
  ---
2
2
  name: postman
3
- description: Use Postman MCP tools for workspace, collection, environment, and run workflows with explicit default-workspace handling.
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
+
10
+ References:
11
+ - [Full-Mode Setup](./references/full-mode-setup.md)
12
+ - [Workspace Policy](./references/workspace-policy.md)
13
+ - [Troubleshooting](./references/troubleshooting.md)
9
14
 
10
15
  ## MCP-First Rule
11
16
 
12
- - Prefer Postman MCP tools (`postman.*`) for all Postman operations.
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>`
13
21
  - Do not use Newman/Postman CLI fallback unless the user explicitly asks for fallback.
14
- - If required Postman MCP tools are unavailable, stop and report the MCP discovery issue with remediation steps.
22
+ - If required Postman MCP tools are unavailable, report discovery/remediation steps first.
15
23
 
16
- ## Required Environment Variables
24
+ ## Setup Baseline
17
25
 
18
- - Active profile key alias must be set (typically `POSTMAN_API_KEY_DEFAULT`).
19
- - `POSTMAN_API_KEY_<PROFILE>` aliases are also valid if the active profile uses them.
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`
20
34
 
21
- ## Preflight Checklist
35
+ ## Preflight
22
36
 
23
37
  1. Read Postman status first:
24
- - Call `postman_get_status` (`scope: auto` unless user requires a scope).
25
- 2. Validate connectivity and mode:
26
- - If not configured, report missing env alias/config and stop.
27
- - If mode is not `full`, call `postman_set_mode` with `mode: full`.
38
+ - Call `postman_get_status`.
39
+ 2. Ensure mode is `full`:
40
+ - If not, call `postman_set_mode` with `mode: full`.
28
41
  3. Discover upstream tools:
29
- - Prefer `postman.getEnabledTools` when available.
30
- - Confirm required tool names before proceeding (for example `getWorkspaces`, `getCollections`, `runCollection`).
42
+ - Confirm required tools exist before execution (for example workspaces/collections/runs).
31
43
 
32
- ## Default Workspace ID Policy
44
+ ## Default Workspace Policy
33
45
 
34
46
  Resolve workspace in this order:
35
47
 
36
48
  1. User-provided workspace ID.
37
49
  2. `postman_get_status.defaultWorkspaceId`.
38
- 3. Auto-detect from `postman.getWorkspaces`:
50
+ 3. Auto-detect from workspace listing:
39
51
  - If exactly one workspace exists, use it and state that choice.
40
52
  4. If multiple workspaces and no default:
41
53
  - Ask user to choose one.
@@ -46,17 +58,11 @@ When a Postman tool requires a workspace argument, always pass the resolved work
46
58
 
47
59
  ## Common Workflows
48
60
 
49
- ### List/Inspect
50
-
51
- - `postman.getWorkspaces`
52
- - `postman.getCollections` (with resolved workspace ID)
53
- - `postman.getEnvironments` (with resolved workspace ID)
54
-
55
61
  ### Collection Run
56
62
 
57
63
  1. Resolve workspace ID (policy above).
58
64
  2. Resolve `collectionId` and optional `environmentId`.
59
- 3. Call `postman.runCollection`.
65
+ 3. Call Postman run tool (`postman.runCollection` or alias equivalent).
60
66
  4. Return a concise run summary:
61
67
  - total requests
62
68
  - passed/failed tests
@@ -65,7 +71,7 @@ When a Postman tool requires a workspace argument, always pass the resolved work
65
71
 
66
72
  ## Failure Handling
67
73
 
68
- If dynamic Postman tools are missing (only `postman_get_*` / `postman_set_mode` visible):
74
+ If dynamic Postman tools are missing:
69
75
 
70
76
  1. Verify env alias expected by config is set.
71
77
  2. Resync catalog:
@@ -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
+ ```