callman-mcp 0.1.0 → 0.2.0

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 (204) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +45 -16
  3. package/dist/client/api.d.ts +64 -3
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +67 -2
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/client/errors.d.ts.map +1 -1
  8. package/dist/client/errors.js +33 -7
  9. package/dist/client/errors.js.map +1 -1
  10. package/dist/client/types.d.ts +91 -2
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/context.d.ts +2 -0
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +7 -2
  15. package/dist/context.js.map +1 -1
  16. package/dist/domain/contracts.d.ts +893 -0
  17. package/dist/domain/contracts.d.ts.map +1 -0
  18. package/dist/domain/contracts.js +90 -0
  19. package/dist/domain/contracts.js.map +1 -0
  20. package/dist/domain/diagram/build.d.ts +520 -18
  21. package/dist/domain/diagram/build.d.ts.map +1 -1
  22. package/dist/domain/diagram/build.js +65 -19
  23. package/dist/domain/diagram/build.js.map +1 -1
  24. package/dist/domain/diagram/patch.d.ts +624 -51
  25. package/dist/domain/diagram/patch.d.ts.map +1 -1
  26. package/dist/domain/diagram/patch.js +180 -40
  27. package/dist/domain/diagram/patch.js.map +1 -1
  28. package/dist/domain/diagram/style.d.ts +315 -0
  29. package/dist/domain/diagram/style.d.ts.map +1 -0
  30. package/dist/domain/diagram/style.js +124 -0
  31. package/dist/domain/diagram/style.js.map +1 -0
  32. package/dist/domain/diagram/summarize.d.ts +8 -0
  33. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  34. package/dist/domain/diagram/summarize.js +28 -13
  35. package/dist/domain/diagram/summarize.js.map +1 -1
  36. package/dist/domain/requestIntegrations.d.ts +719 -0
  37. package/dist/domain/requestIntegrations.d.ts.map +1 -0
  38. package/dist/domain/requestIntegrations.js +222 -0
  39. package/dist/domain/requestIntegrations.js.map +1 -0
  40. package/dist/domain/requestShape.d.ts +1350 -152
  41. package/dist/domain/requestShape.d.ts.map +1 -1
  42. package/dist/domain/requestShape.js +336 -121
  43. package/dist/domain/requestShape.js.map +1 -1
  44. package/dist/domain/resolve.d.ts +2 -1
  45. package/dist/domain/resolve.d.ts.map +1 -1
  46. package/dist/domain/resolve.js +5 -1
  47. package/dist/domain/resolve.js.map +1 -1
  48. package/dist/domain/scenario/build.d.ts +82 -26
  49. package/dist/domain/scenario/build.d.ts.map +1 -1
  50. package/dist/domain/scenario/build.js +196 -65
  51. package/dist/domain/scenario/build.js.map +1 -1
  52. package/dist/domain/scenario/nodeConfig.d.ts +11 -0
  53. package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
  54. package/dist/domain/scenario/nodeConfig.js +251 -0
  55. package/dist/domain/scenario/nodeConfig.js.map +1 -0
  56. package/dist/domain/scenario/summarize.d.ts +145 -25
  57. package/dist/domain/scenario/summarize.d.ts.map +1 -1
  58. package/dist/domain/scenario/summarize.js +84 -29
  59. package/dist/domain/scenario/summarize.js.map +1 -1
  60. package/dist/lib/mask.d.ts +0 -18
  61. package/dist/lib/mask.d.ts.map +1 -1
  62. package/dist/lib/mask.js +2 -44
  63. package/dist/lib/mask.js.map +1 -1
  64. package/dist/lib/pagination.d.ts +3 -2
  65. package/dist/lib/pagination.d.ts.map +1 -1
  66. package/dist/lib/pagination.js +13 -8
  67. package/dist/lib/pagination.js.map +1 -1
  68. package/dist/lib/redact.d.ts +12 -0
  69. package/dist/lib/redact.d.ts.map +1 -0
  70. package/dist/lib/redact.js +50 -0
  71. package/dist/lib/redact.js.map +1 -0
  72. package/dist/lib/strict.d.ts +4 -0
  73. package/dist/lib/strict.d.ts.map +1 -0
  74. package/dist/lib/strict.js +14 -0
  75. package/dist/lib/strict.js.map +1 -0
  76. package/dist/prompts/index.js +7 -7
  77. package/dist/prompts/index.js.map +1 -1
  78. package/dist/resources/docs/contracts.d.ts +2 -0
  79. package/dist/resources/docs/contracts.d.ts.map +1 -0
  80. package/dist/resources/docs/contracts.js +74 -0
  81. package/dist/resources/docs/contracts.js.map +1 -0
  82. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  83. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  84. package/dist/resources/docs/diagramSchema.js +95 -28
  85. package/dist/resources/docs/diagramSchema.js.map +1 -1
  86. package/dist/resources/docs/overview.d.ts +1 -1
  87. package/dist/resources/docs/overview.d.ts.map +1 -1
  88. package/dist/resources/docs/overview.js +29 -7
  89. package/dist/resources/docs/overview.js.map +1 -1
  90. package/dist/resources/docs/requestSchema.d.ts +1 -1
  91. package/dist/resources/docs/requestSchema.d.ts.map +1 -1
  92. package/dist/resources/docs/requestSchema.js +85 -16
  93. package/dist/resources/docs/requestSchema.js.map +1 -1
  94. package/dist/resources/generated/aiPacks.d.ts +1 -1
  95. package/dist/resources/generated/aiPacks.d.ts.map +1 -1
  96. package/dist/resources/generated/aiPacks.js +3 -2
  97. package/dist/resources/generated/aiPacks.js.map +1 -1
  98. package/dist/resources/index.d.ts.map +1 -1
  99. package/dist/resources/index.js +19 -19
  100. package/dist/resources/index.js.map +1 -1
  101. package/dist/run/executeCollection.d.ts +3 -1
  102. package/dist/run/executeCollection.d.ts.map +1 -1
  103. package/dist/run/executeCollection.js +16 -4
  104. package/dist/run/executeCollection.js.map +1 -1
  105. package/dist/run/summarize.d.ts +17 -6
  106. package/dist/run/summarize.d.ts.map +1 -1
  107. package/dist/run/summarize.js +48 -41
  108. package/dist/run/summarize.js.map +1 -1
  109. package/dist/run/types.d.ts +1 -0
  110. package/dist/run/types.d.ts.map +1 -1
  111. package/dist/server.d.ts +1 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +1 -1
  114. package/dist/server.js.map +1 -1
  115. package/dist/tools/catalog.d.ts +1 -1
  116. package/dist/tools/catalog.d.ts.map +1 -1
  117. package/dist/tools/catalog.js +18 -0
  118. package/dist/tools/catalog.js.map +1 -1
  119. package/dist/tools/collections.d.ts.map +1 -1
  120. package/dist/tools/collections.js +53 -41
  121. package/dist/tools/collections.js.map +1 -1
  122. package/dist/tools/contracts.d.ts +4 -0
  123. package/dist/tools/contracts.d.ts.map +1 -0
  124. package/dist/tools/contracts.js +201 -0
  125. package/dist/tools/contracts.js.map +1 -0
  126. package/dist/tools/diagrams.d.ts.map +1 -1
  127. package/dist/tools/diagrams.js +19 -10
  128. package/dist/tools/diagrams.js.map +1 -1
  129. package/dist/tools/environments.d.ts.map +1 -1
  130. package/dist/tools/environments.js +184 -64
  131. package/dist/tools/environments.js.map +1 -1
  132. package/dist/tools/import.d.ts.map +1 -1
  133. package/dist/tools/import.js +111 -20
  134. package/dist/tools/import.js.map +1 -1
  135. package/dist/tools/index.d.ts +2 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +19 -1
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/lookups.d.ts +38 -7
  140. package/dist/tools/lookups.d.ts.map +1 -1
  141. package/dist/tools/lookups.js +113 -10
  142. package/dist/tools/lookups.js.map +1 -1
  143. package/dist/tools/meta.d.ts.map +1 -1
  144. package/dist/tools/meta.js +9 -4
  145. package/dist/tools/meta.js.map +1 -1
  146. package/dist/tools/requests.d.ts.map +1 -1
  147. package/dist/tools/requests.js +138 -50
  148. package/dist/tools/requests.js.map +1 -1
  149. package/dist/tools/run.d.ts.map +1 -1
  150. package/dist/tools/run.js +84 -28
  151. package/dist/tools/run.js.map +1 -1
  152. package/dist/tools/scenarios.d.ts.map +1 -1
  153. package/dist/tools/scenarios.js +238 -75
  154. package/dist/tools/scenarios.js.map +1 -1
  155. package/dist/tools/shared.d.ts +1 -0
  156. package/dist/tools/shared.d.ts.map +1 -1
  157. package/dist/tools/shared.js +9 -1
  158. package/dist/tools/shared.js.map +1 -1
  159. package/dist/tools/workspaces.d.ts.map +1 -1
  160. package/dist/tools/workspaces.js +44 -7
  161. package/dist/tools/workspaces.js.map +1 -1
  162. package/dist-cjs/client/api.js +67 -2
  163. package/dist-cjs/client/errors.js +33 -7
  164. package/dist-cjs/context.js +7 -2
  165. package/dist-cjs/domain/contracts.js +93 -0
  166. package/dist-cjs/domain/diagram/build.js +71 -20
  167. package/dist-cjs/domain/diagram/patch.js +179 -39
  168. package/dist-cjs/domain/diagram/style.js +130 -0
  169. package/dist-cjs/domain/diagram/summarize.js +28 -13
  170. package/dist-cjs/domain/requestIntegrations.js +228 -0
  171. package/dist-cjs/domain/requestShape.js +342 -121
  172. package/dist-cjs/domain/resolve.js +5 -1
  173. package/dist-cjs/domain/scenario/build.js +197 -65
  174. package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
  175. package/dist-cjs/domain/scenario/summarize.js +86 -29
  176. package/dist-cjs/lib/mask.js +3 -48
  177. package/dist-cjs/lib/pagination.js +13 -8
  178. package/dist-cjs/lib/redact.js +53 -0
  179. package/dist-cjs/lib/strict.js +18 -0
  180. package/dist-cjs/prompts/index.js +7 -7
  181. package/dist-cjs/resources/docs/contracts.js +76 -0
  182. package/dist-cjs/resources/docs/diagramSchema.js +95 -28
  183. package/dist-cjs/resources/docs/overview.js +29 -7
  184. package/dist-cjs/resources/docs/requestSchema.js +85 -16
  185. package/dist-cjs/resources/generated/aiPacks.js +3 -2
  186. package/dist-cjs/resources/index.js +17 -17
  187. package/dist-cjs/run/executeCollection.js +16 -4
  188. package/dist-cjs/run/summarize.js +48 -41
  189. package/dist-cjs/server.js +1 -1
  190. package/dist-cjs/tools/catalog.js +18 -0
  191. package/dist-cjs/tools/collections.js +52 -40
  192. package/dist-cjs/tools/contracts.js +204 -0
  193. package/dist-cjs/tools/diagrams.js +19 -10
  194. package/dist-cjs/tools/environments.js +182 -62
  195. package/dist-cjs/tools/import.js +107 -16
  196. package/dist-cjs/tools/index.js +21 -2
  197. package/dist-cjs/tools/lookups.js +123 -11
  198. package/dist-cjs/tools/meta.js +9 -4
  199. package/dist-cjs/tools/requests.js +134 -46
  200. package/dist-cjs/tools/run.js +82 -26
  201. package/dist-cjs/tools/scenarios.js +235 -72
  202. package/dist-cjs/tools/shared.js +11 -2
  203. package/dist-cjs/tools/workspaces.js +43 -6
  204. package/package.json +2 -2
@@ -8,6 +8,7 @@ const requestShape_js_1 = require("../domain/requestShape.js");
8
8
  const summarize_js_2 = require("../domain/scenario/summarize.js");
9
9
  const truncate_js_1 = require("../lib/truncate.js");
10
10
  const lookups_js_1 = require("../tools/lookups.js");
11
+ const contracts_js_1 = require("./docs/contracts.js");
11
12
  const diagramSchema_js_1 = require("./docs/diagramSchema.js");
12
13
  const overview_js_1 = require("./docs/overview.js");
13
14
  const requestSchema_js_1 = require("./docs/requestSchema.js");
@@ -35,13 +36,14 @@ const registerResources = (server, ctx) => {
35
36
  // ── static docs ──────────────────────────────────────────────────────
36
37
  const staticDocs = [
37
38
  { name: "docs-overview", uri: "callman://docs/overview", title: "How to work with Callman", description: "Conventions: whoami first, names as references, dryRun, confirm, secrets, scenarios, diagrams.", body: () => overview_js_1.OVERVIEW_DOC },
38
- { name: "docs-request-schema", uri: "callman://docs/request-schema", title: "Request input schema", description: "The compact request form (body variants, auth, variables, scripts, folder skeleton).", body: () => requestSchema_js_1.REQUEST_SCHEMA_DOC },
39
- { name: "docs-scenario-nodes", uri: "callman://docs/scenario-nodes", title: "Scenario node reference", description: "Every scenario node type with its config fields and defaults, graph rules and examples (from callman-core).", body: () => (0, callman_core_1.buildScenarioSystemPrompt)() },
39
+ { name: "docs-request-schema", uri: "callman://docs/request-schema", title: "Request schema", description: "Every request field as create/update/get use it: url ⇄ query, body modes, auth (incl. oauth2), masked secrets, database / Kafka / Redis steps, DB assertions, scripts, folder skeleton.", body: () => requestSchema_js_1.REQUEST_SCHEMA_DOC },
40
+ { name: "docs-contracts", uri: "callman://docs/contracts", title: "Contract checks", description: "Field rules (path grammar, every rule and format) and data-driven contracts on a request, and how runs evaluate them.", body: () => contracts_js_1.CONTRACTS_DOC },
41
+ { name: "docs-scenario-nodes", uri: "callman://docs/scenario-nodes", title: "Scenario node reference", description: "Every scenario node type: config fields, defaults, what is required, outputs, connections, labels, graph rules, rules/operands, templates and examples (from callman-core, checked against the server's schema).", body: () => (0, callman_core_1.renderScenarioNodeReference)() },
40
42
  { name: "docs-scripting-pm-api", uri: "callman://docs/scripting/pm-api", title: "Scripting: pm.* API", description: "The pm.* API available in pre-request / post-response / test scripts, per stage.", body: () => (0, callman_core_1.renderPmApiReference)() },
41
43
  { name: "docs-scripting-db-queries", uri: "callman://docs/scripting/db-queries", title: "Scripting: database queries", description: "How pm.db.* queries work per database provider.", body: () => (0, callman_core_1.renderDbQueryReference)() },
42
44
  { name: "docs-scripting-templating", uri: "callman://docs/scripting/templating", title: "Templating: {{variables}} and generators", description: "Variable resolution order and dynamic generators ({{$uuid}}, {{$randomInt}} …).", body: () => aiPacks_js_1.AI_PACKS.templating ?? "" },
43
45
  { name: "docs-scripting-sandbox", uri: "callman://docs/scripting/sandbox", title: "Scripting: sandbox and feature policy", description: "Globals available in the script sandbox and the feature policy for DB/Kafka capabilities.", body: () => `${aiPacks_js_1.AI_PACKS["sandbox-globals"] ?? ""}\n\n${aiPacks_js_1.AI_PACKS["feature-policy"] ?? ""}` },
44
- { name: "docs-diagram-schema", uri: "callman://docs/diagram-schema", title: "Diagram schema", description: "The compact diagram input, edge semantics, patch operations and the stored DiagramDocument.", body: () => diagramSchema_js_1.DIAGRAM_SCHEMA_DOC },
46
+ { name: "docs-diagram-schema", uri: "callman://docs/diagram-schema", title: "Diagram schema", description: "Every node and edge field, styling (colors, borders, fonts), containers, edge semantics and colors, patch operations and merge rules, and the stored DiagramDocument.", body: () => diagramSchema_js_1.DIAGRAM_SCHEMA_DOC },
45
47
  ];
46
48
  for (const doc of staticDocs) {
47
49
  server.registerResource(doc.name, doc.uri, { title: doc.title, description: doc.description, mimeType: "text/markdown" }, async (uri) => text(uri, doc.body()));
@@ -56,11 +58,12 @@ const registerResources = (server, ctx) => {
56
58
  type: component.type,
57
59
  label: component.label,
58
60
  ...(component.aliases?.length ? { aliases: component.aliases } : {}),
59
- ...(component.container ? { container: true } : {}),
61
+ ...(component.container || component.swimlane ? { container: true } : {}),
62
+ ...(component.variant === "line" ? { connector: true } : {}),
60
63
  })),
61
64
  }));
62
65
  };
63
- server.registerResource("diagram-components", "callman://diagram/components", { title: "Diagram component catalog", description: "Every component type usable in create_diagram / patch_diagram, grouped by category. Use the `type` values verbatim.", mimeType: "application/json" }, async (uri) => json(uri, { categories: catalog(), note: "Use `type` verbatim as a node's type. Container components take children via `parent`." }));
66
+ server.registerResource("diagram-components", "callman://diagram/components", { title: "Diagram component catalog", description: "Every component type usable in create_diagram / patch_diagram, grouped by category. Use the `type` values verbatim.", mimeType: "application/json" }, async (uri) => json(uri, { categories: catalog(), note: "Use `type` verbatim as a node's type. `container` types (frame, pools) take children via `parent`. `connector` entries are palette lines, not nodes — draw them as edges. Styling and every node/edge field: callman://docs/diagram-schema." }));
64
67
  server.registerResource("diagram-components-category", new mcp_js_1.ResourceTemplate("callman://diagram/components/{category}", {
65
68
  list: undefined,
66
69
  complete: { category: () => [...callman_core_1.DIAGRAM_CATEGORY_ORDER] },
@@ -83,22 +86,19 @@ const registerResources = (server, ctx) => {
83
86
  const { items } = await ctx.api.listEnvironments(ws.id);
84
87
  return json(uri, { workspace: { id: ws.id, name: ws.name }, environments: items.map((environment) => ({ id: environment.id, name: environment.name, variableKeys: environment.variables.map((entry) => entry.key) })) });
85
88
  });
86
- server.registerResource("collection-tree", new mcp_js_1.ResourceTemplate("callman://collection/{collection}/tree", { list: undefined }), { title: "A collection's folders and requests", description: "Folders and requests of a collection (id or name) in the default workspace.", mimeType: "application/json" }, async (uri, variables) => {
87
- const ws = await ctx.resolveWorkspace();
88
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, variable(variables.collection));
89
+ server.registerResource("collection-tree", new mcp_js_1.ResourceTemplate("callman://collection/{collection}/tree", { list: undefined }), { title: "A collection's folders and requests", description: "Folders and requests of a collection (id or name) in any of your workspaces.", mimeType: "application/json" }, async (uri, variables) => {
90
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, null, variable(variables.collection));
89
91
  const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
90
- return json(uri, { collection: { id: resolved.item.id, name: resolved.item.name }, folders: tree.folders, requests: tree.requests.map(requestShape_js_1.requestSummaryView) });
92
+ return json(uri, { collection: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), folders: tree.folders, requests: tree.requests.map(requestShape_js_1.requestSummaryView) });
91
93
  });
92
- server.registerResource("request", new mcp_js_1.ResourceTemplate("callman://request/{request}", { list: undefined }), { title: "A saved request", description: "One request (id or unique name) in the default workspace, secrets masked.", mimeType: "application/json" }, async (uri, variables) => {
93
- const ws = await ctx.resolveWorkspace();
94
- const { request } = await (0, lookups_js_1.resolveRequest)(ctx, ws.id, variable(variables.request));
95
- return json(uri, { request: (0, requestShape_js_1.compactRequestView)(request, { full: true }) });
94
+ server.registerResource("request", new mcp_js_1.ResourceTemplate("callman://request/{request}", { list: undefined }), { title: "A saved request", description: "One request (id or unique name) in any of your workspaces, secrets masked.", mimeType: "application/json" }, async (uri, variables) => {
95
+ const { ws, value } = await (0, lookups_js_1.findRequest)(ctx, null, variable(variables.request));
96
+ return json(uri, { request: (0, requestShape_js_1.compactRequestView)(value.request, { full: true }), workspace: (0, lookups_js_1.workspaceTag)(ws) });
96
97
  });
97
- server.registerResource("environment", new mcp_js_1.ResourceTemplate("callman://environment/{environment}", { list: undefined }), { title: "An environment", description: "One environment (id or name) in the default workspace, values masked by the server.", mimeType: "application/json" }, async (uri, variables) => {
98
- const ws = await ctx.resolveWorkspace();
99
- const resolved = await (0, lookups_js_1.resolveEnvironment)(ctx, ws.id, variable(variables.environment));
98
+ server.registerResource("environment", new mcp_js_1.ResourceTemplate("callman://environment/{environment}", { list: undefined }), { title: "An environment", description: "One environment (id or name) in any of your workspaces, values masked by the server.", mimeType: "application/json" }, async (uri, variables) => {
99
+ const { ws, value: resolved } = await (0, lookups_js_1.findEnvironment)(ctx, null, variable(variables.environment));
100
100
  const fresh = await ctx.api.getEnvironment(ws.id, resolved.item.id);
101
- return json(uri, { environment: { id: fresh.id, name: fresh.name, variables: fresh.variables } });
101
+ return json(uri, { environment: { id: fresh.id, name: fresh.name, variables: fresh.variables }, workspace: (0, lookups_js_1.workspaceTag)(ws) });
102
102
  });
103
103
  server.registerResource("scenario", new mcp_js_1.ResourceTemplate("callman://scenario/{scenario}", { list: undefined }), { title: "A scenario", description: "One scenario (id or name): nodes with configs (clipped, secrets masked) and edges.", mimeType: "application/json" }, async (uri, variables) => {
104
104
  const ws = await ctx.resolveWorkspaceOrNull();
@@ -21,8 +21,17 @@ const diffEnv = (before, after, set, unset) => {
21
21
  if (!(key in after))
22
22
  unset.add(key);
23
23
  };
24
- const executeCollection = async ({ collection, env, connections = {}, bail = false, only = null, deadlineMs, onProgress, }) => {
24
+ const executeCollection = async ({ collection, env, globals = {}, connections = {}, bail = false, only = null, includeDisabled = false, deadlineMs, onProgress, }) => {
25
25
  const envStore = new callman_core_1.EnvStore(env);
26
+ const globalStore = new callman_core_1.EnvStore(globals);
27
+ const seen = new Map();
28
+ const remember = (values) => {
29
+ for (const [key, value] of Object.entries(values))
30
+ if (typeof value === "string" && value)
31
+ seen.set(`${key}\u0000${value}`, key);
32
+ };
33
+ remember(globals);
34
+ remember(env);
26
35
  const results = [];
27
36
  const setKeys = new Set();
28
37
  const unsetKeys = new Set();
@@ -34,7 +43,7 @@ const executeCollection = async ({ collection, env, connections = {}, bail = fal
34
43
  kafka: false,
35
44
  };
36
45
  const wanted = only ? new Set(only) : null;
37
- const runnable = collection.requests.filter((request) => request.isEnabledInRunner !== false && (!wanted || wanted.has(request.id)));
46
+ const runnable = collection.requests.filter((request) => (includeDisabled || request.isEnabledInRunner !== false) && (!wanted || wanted.has(request.id)));
38
47
  for (const [index, request] of runnable.entries()) {
39
48
  if (deadlineMs && Date.now() - startedAt > deadlineMs) {
40
49
  bailed = true;
@@ -43,7 +52,7 @@ const executeCollection = async ({ collection, env, connections = {}, bail = fal
43
52
  const before = envStore.all();
44
53
  const rows = request.dataContract?.rows ?? [];
45
54
  if (rows.length > 0) {
46
- const { iterations } = await (0, callman_core_1.runRequestWithDataset)({ request, env: envStore, previousResponse, connections, featurePolicy }, { requestId: request.id, columns: request.dataContract?.columns ?? [], rows });
55
+ const { iterations } = await (0, callman_core_1.runRequestWithDataset)({ request, env: envStore, globals: globalStore, previousResponse, connections, featurePolicy }, { requestId: request.id, columns: request.dataContract?.columns ?? [], rows });
47
56
  let rowBailed = false;
48
57
  for (const iteration of iterations) {
49
58
  const result = {
@@ -65,6 +74,7 @@ const executeCollection = async ({ collection, env, connections = {}, bail = fal
65
74
  }
66
75
  }
67
76
  diffEnv(before, envStore.all(), setKeys, unsetKeys);
77
+ remember(envStore.all());
68
78
  previousResponse = null;
69
79
  if (rowBailed) {
70
80
  bailed = true;
@@ -72,8 +82,9 @@ const executeCollection = async ({ collection, env, connections = {}, bail = fal
72
82
  }
73
83
  continue;
74
84
  }
75
- const outcome = await (0, callman_core_1.runRequest)({ request, env: envStore, previousResponse, connections, featurePolicy });
85
+ const outcome = await (0, callman_core_1.runRequest)({ request, env: envStore, globals: globalStore, previousResponse, connections, featurePolicy });
76
86
  diffEnv(before, envStore.all(), setKeys, unsetKeys);
87
+ remember(envStore.all());
77
88
  previousResponse = outcome.response;
78
89
  results.push(outcome.result);
79
90
  onProgress?.({ index, total: runnable.length, request, result: outcome.result });
@@ -88,6 +99,7 @@ const executeCollection = async ({ collection, env, connections = {}, bail = fal
88
99
  summary: { total: results.length, passed, failed: results.length - passed },
89
100
  envChanges: { set: [...setKeys].sort(), unset: [...unsetKeys].sort() },
90
101
  bailed,
102
+ knownValues: [...seen].map(([entry, key]) => [key, entry.slice(entry.indexOf("\u0000") + 1)]),
91
103
  };
92
104
  };
93
105
  exports.executeCollection = executeCollection;
@@ -1,44 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.summarizeRun = exports.summarizeResult = void 0;
4
- const mask_js_1 = require("../lib/mask.js");
4
+ const redact_js_1 = require("../lib/redact.js");
5
5
  const truncate_js_1 = require("../lib/truncate.js");
6
6
  const executeCollection_js_1 = require("./executeCollection.js");
7
7
  // What a local run reports back to the model: enough to judge and debug
8
- // (status, timing, tests, a bounded body preview, the request as SENT with
9
- // sensitive headers masked) and nothing that leaks credentials — variable
10
- // VALUES never appear, only the keys scripts changed.
11
- const KEEP_RESPONSE_HEADERS = new Set([
12
- "content-type",
13
- "content-length",
14
- "location",
15
- "cache-control",
16
- "x-request-id",
17
- "x-correlation-id",
18
- "retry-after",
19
- "www-authenticate",
20
- ]);
21
- const responseHeadersOf = (response) => {
8
+ // (status, timing, tests and contracts, the request as SENT, response headers
9
+ // and a bounded body) and no variable VALUES — every value the run held is
10
+ // shown as its {{key}} (lib/redact), credentials that are not variables are
11
+ // masked by name.
12
+ const RESPONSE_HEADERS_MAX = 40;
13
+ const responseHeadersOf = (response, redactor) => {
22
14
  const headers = response?.headers;
23
15
  if (!headers || typeof headers !== "object")
24
- return {};
25
- const out = {};
26
- for (const [key, value] of Object.entries(headers)) {
27
- if (KEEP_RESPONSE_HEADERS.has(key.toLowerCase()))
28
- out[key] = Array.isArray(value) ? value.join(", ") : String(value);
29
- }
30
- return out;
16
+ return { headers: {} };
17
+ const entries = Object.entries(headers);
18
+ return {
19
+ headers: redactor.headers(Object.fromEntries(entries.slice(0, RESPONSE_HEADERS_MAX))),
20
+ ...(entries.length > RESPONSE_HEADERS_MAX ? { headersTruncated: true, headerCount: entries.length } : {}),
21
+ };
31
22
  };
32
- const bodyOf = (response) => {
23
+ // XML (and other parsed) bodies keep their original text in `rawText` —
24
+ // report what the server actually sent.
25
+ const bodyTextOf = (response) => {
33
26
  if (!response || typeof response !== "object")
34
- return null;
27
+ return "";
35
28
  const record = response;
36
- return record.data !== undefined ? record.data : record.body !== undefined ? record.body : null;
29
+ if (typeof record.rawText === "string" && record.rawText)
30
+ return record.rawText;
31
+ const body = record.data !== undefined ? record.data : record.body;
32
+ if (body === null || body === undefined)
33
+ return "";
34
+ return typeof body === "string" ? body : (0, truncate_js_1.safeStringify)(body);
37
35
  };
38
- const summarizeResult = (result, maxBodyBytes) => {
39
- const body = bodyOf(result.response);
40
- const bodyText = body === null || body === undefined ? "" : typeof body === "string" ? body : (0, truncate_js_1.safeStringify)(body);
41
- const clipped = (0, truncate_js_1.clipBytes)(bodyText, maxBodyBytes);
36
+ const testsView = (tests) => tests.map((test) => ({ name: test.name, status: test.status, ...(test.message ? { message: (0, truncate_js_1.clip)(test.message, 300) } : {}) }));
37
+ const summarizeResult = (result, maxBodyBytes, redactor) => {
38
+ const clipped = (0, truncate_js_1.clipBytes)(redactor.text(bodyTextOf(result.response)), maxBodyBytes);
42
39
  const sent = result.request;
43
40
  return {
44
41
  requestId: result.requestId,
@@ -49,25 +46,35 @@ const summarizeResult = (result, maxBodyBytes) => {
49
46
  request: sent
50
47
  ? {
51
48
  method: sent.method,
52
- url: (0, truncate_js_1.clip)(sent.url, 2048),
53
- headers: (0, mask_js_1.maskHeaderRecord)(sent.headers),
54
- ...(sent.body !== undefined && sent.body !== null ? { body: (0, truncate_js_1.clip)(typeof sent.body === "string" ? sent.body : (0, truncate_js_1.safeStringify)(sent.body), truncate_js_1.LIMITS.BODY_PREVIEW) } : {}),
49
+ url: (0, truncate_js_1.clip)(redactor.text(sent.url), 2048),
50
+ ...(sent.query && Object.keys(sent.query).length > 0 ? { query: redactor.query(sent.query) } : {}),
51
+ headers: redactor.headers(sent.headers),
52
+ ...(sent.body !== undefined && sent.body !== null
53
+ ? { body: (0, truncate_js_1.clip)(redactor.text(typeof sent.body === "string" ? sent.body : (0, truncate_js_1.safeStringify)(sent.body)), truncate_js_1.LIMITS.BODY_PREVIEW) }
54
+ : {}),
55
55
  }
56
56
  : null,
57
57
  response: {
58
- headers: responseHeadersOf(result.response),
58
+ ...responseHeadersOf(result.response, redactor),
59
59
  body: clipped.text || null,
60
60
  ...(clipped.truncated ? { bodyTruncated: true, bodyBytes: clipped.totalBytes } : {}),
61
61
  },
62
- tests: result.scriptResults.map((test) => ({ name: test.name, status: test.status, ...(test.message ? { message: (0, truncate_js_1.clip)(test.message, 300) } : {}) })),
63
- contracts: result.contractResults.map((test) => ({ name: test.name, status: test.status, ...(test.message ? { message: (0, truncate_js_1.clip)(test.message, 300) } : {}) })),
62
+ tests: testsView(result.scriptResults),
63
+ contracts: testsView(result.contractResults),
64
+ ...(result.unresolvedVariables && result.unresolvedVariables.length > 0 ? { unresolvedVariables: result.unresolvedVariables } : {}),
65
+ ...(result.logs && result.logs.length > 0
66
+ ? { logs: result.logs.slice(0, 50).map((entry) => (0, truncate_js_1.clip)(redactor.text(`[${entry.stage}${entry.level === "log" ? "" : ` ${entry.level}`}] ${entry.message}`), 500)) }
67
+ : {}),
64
68
  };
65
69
  };
66
70
  exports.summarizeResult = summarizeResult;
67
- const summarizeRun = (outcome, maxBodyBytes, extra = {}) => ({
68
- ...extra,
69
- summary: { ...outcome.summary, bailed: outcome.bailed },
70
- results: outcome.results.map((result) => (0, exports.summarizeResult)(result, maxBodyBytes)),
71
- env: outcome.envChanges,
72
- });
71
+ const summarizeRun = (outcome, maxBodyBytes, extra = {}, redaction = {}) => {
72
+ const redactor = (0, redact_js_1.createRedactor)(outcome.knownValues, redaction);
73
+ return {
74
+ ...extra,
75
+ summary: { ...outcome.summary, bailed: outcome.bailed },
76
+ results: outcome.results.map((result) => (0, exports.summarizeResult)(result, maxBodyBytes, redactor)),
77
+ env: outcome.envChanges,
78
+ };
79
+ };
73
80
  exports.summarizeRun = summarizeRun;
@@ -8,7 +8,7 @@ const index_js_2 = require("./resources/index.js");
8
8
  const index_js_3 = require("./tools/index.js");
9
9
  exports.SERVER_NAME = "callman";
10
10
  exports.SERVER_INSTRUCTIONS = `Callman is an API client and test platform: collections of HTTP requests, environments with variables, multi-step test scenarios, and system-design diagrams. You act as the user whose token is configured, within that token's permissions and the user's workspace role.
11
- Start with whoami. Names work as references (collection, folder, request, environment, scenario, diagram, workspace); ambiguous names return candidates. List before creating — duplicate collection names are refused. Preview large writes with dryRun: true; deletions need confirm: true and the user's agreement. Environment values are masked unless the token holds environments:reveal and you pass reveal: true; never write a masked value back. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating requests, callman://docs/scenario-nodes before scenarios, and callman://diagram/components before diagrams.`;
11
+ Start with whoami. The token works in every workspace the user belongs to: lists and searches cover all of them unless you pass \`workspace\`, names are looked up in the default workspace first and then everywhere, and results say which workspace they come from; creating something needs a target (\`workspace\`, the collection it goes into, or the session default — use_workspace). Names work as references (collection, folder, request, environment, scenario, diagram, workspace); ambiguous names return candidates. List before creating — duplicate collection names are refused. Preview large writes with dryRun: true; deletions need confirm: true and the user's agreement. Environment values are masked unless the token holds environments:reveal and you pass reveal: true; never write a masked value back. Arguments are strict — an unknown key is an error listing the valid ones. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating or editing requests, callman://docs/contracts before contract checks, callman://docs/scenario-nodes before scenarios, and callman://docs/diagram-schema + callman://diagram/components before diagrams.`;
12
12
  const createCallmanMcpServer = (options) => {
13
13
  const version = options.version ?? "0.0.0";
14
14
  const context = (0, context_js_1.createToolContext)({
@@ -8,8 +8,10 @@ exports.toolAvailability = exports.TOOL_CATALOG = void 0;
8
8
  exports.TOOL_CATALOG = [
9
9
  { name: "whoami", group: "meta", scopes: [] },
10
10
  { name: "list_workspaces", group: "workspaces", scopes: ["workspaces:read"] },
11
+ { name: "use_workspace", group: "workspaces", scopes: ["workspaces:read"] },
11
12
  { name: "list_workspace_members", group: "workspaces", scopes: ["workspaces:read"] },
12
13
  { name: "add_workspace_member", group: "workspaces", scopes: ["members:manage"] },
14
+ { name: "update_workspace_member", group: "workspaces", scopes: ["members:manage"] },
13
15
  { name: "remove_workspace_member", group: "workspaces", scopes: ["members:manage"], destructive: true },
14
16
  { name: "list_collections", group: "collections", scopes: ["collections:read"] },
15
17
  { name: "get_collection_tree", group: "collections", scopes: ["collections:read", "requests:read"] },
@@ -31,6 +33,17 @@ exports.TOOL_CATALOG = [
31
33
  { name: "delete_requests", group: "requests", scopes: ["requests:delete"], destructive: true },
32
34
  { name: "run_request", group: "run", scopes: ["requests:run"], localRun: true },
33
35
  { name: "run_collection", group: "run", scopes: ["requests:run"], localRun: true },
36
+ // Contract checks belong to their request (the backend maps them onto the
37
+ // requests:* scopes). add_contract_rules with replace: true also deletes.
38
+ { name: "get_contract_checks", group: "contracts", scopes: ["requests:read"] },
39
+ { name: "add_contract_rules", group: "contracts", scopes: ["requests:write"] },
40
+ { name: "update_contract_rule", group: "contracts", scopes: ["requests:write"] },
41
+ { name: "delete_contract_rules", group: "contracts", scopes: ["requests:delete"], destructive: true },
42
+ { name: "test_contract_rules", group: "contracts", scopes: ["requests:read"] },
43
+ { name: "set_data_contract", group: "contracts", scopes: ["requests:write"] },
44
+ { name: "delete_data_contract", group: "contracts", scopes: ["requests:delete"], destructive: true },
45
+ // The safe connection record (no credentials) — readable with either family.
46
+ { name: "list_connections", group: "connections", scopes: ["requests:read", "scenarios:read"], anyOf: true },
34
47
  // Documents and new collections → import:write; cURL/.http into an existing
35
48
  // collection → requests:write.
36
49
  { name: "import_content", group: "import", scopes: ["import:write", "requests:write"], anyOf: true },
@@ -38,13 +51,18 @@ exports.TOOL_CATALOG = [
38
51
  { name: "list_environments", group: "environments", scopes: ["environments:read"] },
39
52
  { name: "get_environment", group: "environments", scopes: ["environments:read"] },
40
53
  { name: "create_environment", group: "environments", scopes: ["environments:write"] },
54
+ { name: "update_environment", group: "environments", scopes: ["environments:write"] },
55
+ // `unset` additionally needs environments:delete (DELETE …/variables/:key).
41
56
  { name: "set_environment_variables", group: "environments", scopes: ["environments:write"] },
57
+ { name: "set_global_variables", group: "environments", scopes: ["environments:write"] },
42
58
  { name: "delete_environment", group: "environments", scopes: ["environments:delete"], destructive: true },
43
59
  { name: "list_scenarios", group: "scenarios", scopes: ["scenarios:read"] },
44
60
  { name: "get_scenario", group: "scenarios", scopes: ["scenarios:read"] },
45
61
  { name: "get_scenario_report", group: "scenarios", scopes: ["scenarios:read"] },
46
62
  { name: "create_scenario", group: "scenarios", scopes: ["scenarios:write"] },
47
63
  { name: "update_scenario", group: "scenarios", scopes: ["scenarios:write"] },
64
+ { name: "patch_scenario", group: "scenarios", scopes: ["scenarios:write"] },
65
+ { name: "submit_scenario", group: "scenarios", scopes: ["scenarios:write"] },
48
66
  { name: "delete_scenario", group: "scenarios", scopes: ["scenarios:delete"], destructive: true },
49
67
  { name: "run_scenario", group: "scenarios", scopes: ["scenarios:run"] },
50
68
  { name: "list_diagrams", group: "diagrams", scopes: ["diagrams:read"] },
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCollectionTools = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
+ const strict_js_1 = require("../lib/strict.js");
6
7
  const requestShape_js_1 = require("../domain/requestShape.js");
7
8
  const resolve_js_1 = require("../domain/resolve.js");
8
9
  const pagination_js_1 = require("../lib/pagination.js");
@@ -10,23 +11,31 @@ const truncate_js_1 = require("../lib/truncate.js");
10
11
  const lookups_js_1 = require("./lookups.js");
11
12
  const shared_js_1 = require("./shared.js");
12
13
  const EXPORT_MAX_BYTES = 1024 * 1024;
14
+ const runnerSettingsArg = (0, strict_js_1.strictObject)({
15
+ database: zod_1.z.boolean().optional().describe("Run each request's database pre/post steps during collection runs"),
16
+ kafka: zod_1.z.boolean().optional().describe("Run each request's Kafka publish/consume steps during collection runs"),
17
+ }, "runner setting")
18
+ .optional()
19
+ .describe("Collection runner switches (both default to on)");
13
20
  const registerCollectionTools = (server, ctx) => {
14
21
  server.registerTool("list_collections", {
15
22
  title: "List collections",
16
- description: "List the collections in a workspace with request/folder counts. Optional `query` filters by name (substring). Use get_collection_tree to see what is inside one.",
23
+ description: "List collections with request/folder counts — across ALL your workspaces unless `workspace` narrows it (the default workspace comes first; every row says its workspace). Optional `query` filters by name (substring). Use get_collection_tree to see what is inside one.",
17
24
  inputSchema: { workspace: shared_js_1.workspaceArg, query: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
18
25
  annotations: shared_js_1.READ,
19
26
  }, (0, shared_js_1.guarded)(async ({ workspace, query, cursor, limit }) => {
20
- const ws = await ctx.resolveWorkspace(workspace);
21
- const all = await (0, lookups_js_1.loadCollections)(ctx, ws.id);
27
+ const workspaces = await (0, lookups_js_1.workspacesForListing)(ctx, workspace);
22
28
  const needle = query?.trim().toLowerCase();
23
- const filtered = needle ? all.filter((collection) => collection.name.toLowerCase().includes(needle)) : all;
24
- const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit);
29
+ const rows = (await Promise.all(workspaces.map(async (ws) => (await (0, lookups_js_1.loadCollections)(ctx, ws.id)).map((collection) => ({ ws, collection })))))
30
+ .flat()
31
+ .filter(({ collection }) => !needle || collection.name.toLowerCase().includes(needle));
32
+ const page = (0, pagination_js_1.sliceWithCursor)(rows, cursor, limit, `${workspaces.map((ws) => ws.id).join(",")}|${needle ?? ""}`);
25
33
  return (0, shared_js_1.ok)({
26
- workspace: { id: ws.id, name: ws.name },
27
- collections: page.items.map((collection) => ({
34
+ ...(workspaces.length === 1 ? { workspace: (0, lookups_js_1.workspaceTag)(workspaces[0]) } : { workspaces: workspaces.map(lookups_js_1.workspaceTag) }),
35
+ collections: page.items.map(({ ws, collection }) => ({
28
36
  id: collection.id,
29
37
  name: collection.name,
38
+ ...(workspaces.length > 1 ? { workspace: (0, lookups_js_1.workspaceTag)(ws) } : {}),
30
39
  requestCount: collection.requestCount ?? null,
31
40
  folderCount: collection.folderCount ?? null,
32
41
  updatedAt: collection.updatedAt ?? null,
@@ -37,17 +46,17 @@ const registerCollectionTools = (server, ctx) => {
37
46
  }));
38
47
  server.registerTool("get_collection_tree", {
39
48
  title: "Get a collection's folders and requests",
40
- description: "The folders (with paths like \"Auth/Login\") and requests (id, name, method, url, folder) of one collection — the map you need before creating, moving or running requests.",
49
+ description: "The folders (with paths like \"Auth/Login\") and requests (id, name, method, url, folder) of one collection, in whichever workspace it lives — the map you need before creating, moving or running requests.",
41
50
  inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection") },
42
51
  annotations: shared_js_1.READ,
43
52
  }, (0, shared_js_1.guarded)(async ({ workspace, collection }) => {
44
- const ws = await ctx.resolveWorkspace(workspace);
45
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
53
+ const { ws, value: resolved, note } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
46
54
  const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
47
55
  const truncated = tree.requests.length > truncate_js_1.LIMITS.TREE_ITEMS;
48
56
  return (0, shared_js_1.ok)({
49
57
  collection: { id: resolved.item.id, name: resolved.item.name },
50
- resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)],
58
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
59
+ resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(note ? [{ input: collection, note }] : [])],
51
60
  folders: tree.folders.map((folder) => ({ id: folder.id, name: folder.name, parentId: folder.parentId, path: folder.path })),
52
61
  requests: tree.requests.slice(0, truncate_js_1.LIMITS.TREE_ITEMS).map(requestShape_js_1.requestSummaryView),
53
62
  ...(truncated ? { truncated: true, totalRequests: tree.requests.length } : {}),
@@ -59,23 +68,22 @@ const registerCollectionTools = (server, ctx) => {
59
68
  inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), format: zod_1.z.enum(["internal", "postman"]).default("internal") },
60
69
  annotations: shared_js_1.READ,
61
70
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, format }) => {
62
- const ws = await ctx.resolveWorkspace(workspace);
63
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
71
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
64
72
  const document = await ctx.api.exportCollection(ws.id, resolved.item.id, format);
65
73
  const bytes = (0, truncate_js_1.byteLength)(document);
66
74
  if (bytes > EXPORT_MAX_BYTES) {
67
75
  throw new errors_js_1.ToolInputError("EXPORT_TOO_LARGE", `The export is ${bytes} bytes (limit ${EXPORT_MAX_BYTES}). Export it from the desktop app instead.`, { bytes });
68
76
  }
69
- return (0, shared_js_1.ok)({ collection: { id: resolved.item.id, name: resolved.item.name }, format, bytes, document: document });
77
+ return (0, shared_js_1.ok)({ collection: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), format, bytes, document: document });
70
78
  }));
71
79
  server.registerTool("create_collection", {
72
80
  title: "Create a collection",
73
- description: "Create an empty collection. Refuses a name that already exists in the workspace unless allowDuplicateName is true — list_collections first and reuse an existing one when it fits.",
81
+ description: "Create an empty collection in `workspace` (or the session default — use_workspace; with one workspace, that one). Refuses a name that already exists in that workspace unless allowDuplicateName is true — list_collections first and reuse an existing one when it fits.",
74
82
  inputSchema: {
75
83
  workspace: shared_js_1.workspaceArg,
76
84
  name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME),
77
85
  allowDuplicateName: zod_1.z.boolean().optional(),
78
- runnerSettings: zod_1.z.object({ database: zod_1.z.boolean().optional(), kafka: zod_1.z.boolean().optional(), redis: zod_1.z.boolean().optional() }).optional(),
86
+ runnerSettings: runnerSettingsArg,
79
87
  },
80
88
  annotations: shared_js_1.WRITE,
81
89
  }, (0, shared_js_1.guarded)(async ({ workspace, name, allowDuplicateName, runnerSettings }) => {
@@ -88,29 +96,28 @@ const registerCollectionTools = (server, ctx) => {
88
96
  }
89
97
  const created = await ctx.api.createCollection(ws.id, { name: name.trim(), ...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}) });
90
98
  ctx.invalidate(`collections:${ws.id}`);
91
- return (0, shared_js_1.ok)({ collection: { id: created.id, name: created.name }, note: "Created. Add requests with create_request / create_requests_bulk / import_content." });
99
+ return (0, shared_js_1.ok)({ collection: { id: created.id, name: created.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), note: "Created. Add requests with create_request / create_requests_bulk / import_content." });
92
100
  }));
93
101
  server.registerTool("update_collection", {
94
102
  title: "Rename a collection / change runner settings",
95
- description: "Rename a collection or toggle its runner settings (database / kafka / redis steps during runs).",
103
+ description: "Rename a collection or toggle its runner settings: whether collection runs execute the requests' database steps and Kafka steps.",
96
104
  inputSchema: {
97
105
  workspace: shared_js_1.workspaceArg,
98
106
  collection: (0, shared_js_1.refArg)("Collection"),
99
107
  name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME).optional(),
100
- runnerSettings: zod_1.z.object({ database: zod_1.z.boolean().optional(), kafka: zod_1.z.boolean().optional(), redis: zod_1.z.boolean().optional() }).optional(),
108
+ runnerSettings: runnerSettingsArg,
101
109
  },
102
110
  annotations: shared_js_1.IDEMPOTENT_WRITE,
103
111
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, name, runnerSettings }) => {
104
112
  if (name === undefined && runnerSettings === undefined)
105
113
  throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or runnerSettings.");
106
- const ws = await ctx.resolveWorkspace(workspace);
107
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
114
+ const { ws, value: resolved, note } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
108
115
  const updated = await ctx.api.updateCollection(ws.id, resolved.item.id, {
109
116
  ...(name !== undefined ? { name: name.trim() } : {}),
110
117
  ...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}),
111
118
  });
112
119
  ctx.invalidate(`collections:${ws.id}`);
113
- return (0, shared_js_1.ok)({ collection: { id: updated.id, name: updated.name, runnerSettings: updated.runnerSettings ?? null }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
120
+ return (0, shared_js_1.ok)({ collection: { id: updated.id, name: updated.name, runnerSettings: updated.runnerSettings ?? null }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(note ? [{ input: collection, note }] : [])] });
114
121
  }));
115
122
  server.registerTool("create_folder", {
116
123
  title: "Create a folder",
@@ -123,8 +130,7 @@ const registerCollectionTools = (server, ctx) => {
123
130
  },
124
131
  annotations: shared_js_1.WRITE,
125
132
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, name, parent }) => {
126
- const ws = await ctx.resolveWorkspace(workspace);
127
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
133
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
128
134
  const parentFolder = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, parent);
129
135
  const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
130
136
  const sibling = tree.folders.find((folder) => folder.parentId === (parentFolder?.item.id ?? null) && (0, lookups_js_1.sameName)(folder.name, name));
@@ -136,36 +142,44 @@ const registerCollectionTools = (server, ctx) => {
136
142
  ctx.invalidate(`collections:${ws.id}`);
137
143
  return (0, shared_js_1.ok)({
138
144
  folder: { id: folder.id, name: folder.name, parentId: folder.parentId, path: parentFolder ? `${parentFolder.item.path}/${folder.name}` : folder.name },
145
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
139
146
  resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(parentFolder ? [(0, resolve_js_1.resolvedSummary)(parent, parentFolder)] : [])],
140
147
  });
141
148
  }));
142
149
  server.registerTool("update_folder", {
143
150
  title: "Rename or move a folder",
144
- description: "Rename a folder and/or move it under another parent (pass parent: null for top level).",
151
+ description: "Rename a folder and/or move it — under another parent in the same collection (parent: null = top level) or into another collection (targetCollection, with its subfolders and requests).",
145
152
  inputSchema: {
146
153
  workspace: shared_js_1.workspaceArg,
147
154
  collection: (0, shared_js_1.refArg)("Collection"),
148
155
  folder: (0, shared_js_1.refArg)("Folder", "(or path)"),
149
156
  name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME).optional(),
150
- parent: zod_1.z.string().max(300).nullable().optional().describe("New parent folder (id, name or path); null = top level"),
157
+ parent: zod_1.z.string().max(300).nullable().optional().describe("New parent folder (id, name or path) in the target collection; null = top level"),
158
+ targetCollection: zod_1.z.string().max(200).optional().describe("Move the folder into this collection (id or name)"),
151
159
  },
152
160
  annotations: shared_js_1.IDEMPOTENT_WRITE,
153
- }, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, name, parent }) => {
154
- if (name === undefined && parent === undefined)
155
- throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or parent.");
156
- const ws = await ctx.resolveWorkspace(workspace);
157
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
161
+ }, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, name, parent, targetCollection }) => {
162
+ if (name === undefined && parent === undefined && targetCollection === undefined)
163
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name, parent and/or targetCollection.");
164
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
158
165
  const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
159
166
  if (!target)
160
167
  throw new errors_js_1.ToolInputError("REF_REQUIRED", "folder is required.");
161
- const parentFolder = parent ? await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, parent) : null;
168
+ const destination = targetCollection ? await (0, lookups_js_1.resolveCollection)(ctx, ws.id, targetCollection) : resolved;
169
+ const moving = destination.item.id !== resolved.item.id;
170
+ const parentFolder = parent ? await (0, lookups_js_1.resolveFolder)(ctx, ws.id, destination.item.id, parent) : null;
162
171
  if (parentFolder && parentFolder.item.id === target.item.id)
163
172
  throw new errors_js_1.ToolInputError("FOLDER_CYCLE", "A folder cannot be its own parent.");
164
173
  const updated = await ctx.api.updateFolder(ws.id, target.item.id, {
165
174
  ...(name !== undefined ? { name: name.trim() } : {}),
166
- ...(parent !== undefined ? { parentId: parentFolder?.item.id ?? null } : {}),
175
+ ...(parent !== undefined || moving ? { parentId: parentFolder?.item.id ?? null } : {}),
176
+ ...(moving ? { collectionId: destination.item.id } : {}),
167
177
  });
168
178
  ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
179
+ if (moving) {
180
+ ctx.invalidate(`tree:${ws.id}:${destination.item.id}`);
181
+ ctx.invalidate(`collections:${ws.id}`);
182
+ }
169
183
  return (0, shared_js_1.ok)({ folder: { id: updated.id, name: updated.name, parentId: updated.parentId }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), (0, resolve_js_1.resolvedSummary)(folder, target)] });
170
184
  }));
171
185
  server.registerTool("delete_collection", {
@@ -175,12 +189,11 @@ const registerCollectionTools = (server, ctx) => {
175
189
  annotations: shared_js_1.DESTRUCTIVE,
176
190
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, confirm }) => {
177
191
  (0, shared_js_1.requireConfirm)(confirm, "a collection");
178
- const ws = await ctx.resolveWorkspace(workspace);
179
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
192
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection, { exact: true });
180
193
  await ctx.api.deleteCollection(ws.id, resolved.item.id);
181
194
  ctx.invalidate(`collections:${ws.id}`);
182
195
  ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
183
- return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
196
+ return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
184
197
  }));
185
198
  server.registerTool("delete_folder", {
186
199
  title: "Delete a folder",
@@ -189,15 +202,14 @@ const registerCollectionTools = (server, ctx) => {
189
202
  annotations: shared_js_1.DESTRUCTIVE,
190
203
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, confirm }) => {
191
204
  (0, shared_js_1.requireConfirm)(confirm, "a folder");
192
- const ws = await ctx.resolveWorkspace(workspace);
193
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
194
- const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
205
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
206
+ const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder, { exact: true });
195
207
  if (!target)
196
208
  throw new errors_js_1.ToolInputError("REF_REQUIRED", "folder is required.");
197
209
  await ctx.api.deleteFolder(ws.id, target.item.id);
198
210
  ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
199
211
  ctx.invalidate(`collections:${ws.id}`);
200
- return (0, shared_js_1.ok)({ deleted: { id: target.item.id, name: target.item.name, path: target.item.path }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), (0, resolve_js_1.resolvedSummary)(folder, target)] });
212
+ return (0, shared_js_1.ok)({ deleted: { id: target.item.id, name: target.item.name, path: target.item.path }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), (0, resolve_js_1.resolvedSummary)(folder, target)] });
201
213
  }));
202
214
  };
203
215
  exports.registerCollectionTools = registerCollectionTools;