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
@@ -5,196 +5,313 @@ const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
6
  const resolve_js_1 = require("../domain/resolve.js");
7
7
  const build_js_1 = require("../domain/scenario/build.js");
8
+ const nodeConfig_js_1 = require("../domain/scenario/nodeConfig.js");
8
9
  const summarize_js_1 = require("../domain/scenario/summarize.js");
9
- const ids_js_1 = require("../lib/ids.js");
10
10
  const pagination_js_1 = require("../lib/pagination.js");
11
+ const strict_js_1 = require("../lib/strict.js");
11
12
  const lookups_js_1 = require("./lookups.js");
12
13
  const shared_js_1 = require("./shared.js");
13
14
  const TERMINAL_STATUSES = new Set(["success", "failed", "completed_with_failures", "stopped"]);
14
15
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ const groupArg = zod_1.z
17
+ .string()
18
+ .max(200)
19
+ .optional()
20
+ .describe('On-prem: the scenario group that owns it (name or id from list_scenarios `groups`, or "personal"). Needed when you belong to several groups; ignored on cloud');
21
+ // The backend's node schemas strip unknown config keys without an error —
22
+ // compare what we sent with what it stored and say which keys vanished.
23
+ const droppedFieldWarnings = (graph, stored) => {
24
+ const byId = new Map(stored.nodes.map((node) => [node.id, node]));
25
+ const out = [];
26
+ for (const [id, sent] of graph.sent) {
27
+ const node = byId.get(id);
28
+ if (!node)
29
+ continue;
30
+ const dropped = (0, nodeConfig_js_1.droppedPaths)(sent, node.data?.config ?? {});
31
+ if (dropped.length > 0)
32
+ out.push(`${id}: the server ignored config.${dropped.join(", config.")} — not a field of a ${node.type} node (callman://docs/scenario-nodes).`);
33
+ }
34
+ return out;
35
+ };
36
+ const resolveGroup = async (ctx, ws, ref) => {
37
+ if (!ref)
38
+ return undefined;
39
+ if (ref.trim().toLowerCase() === "personal")
40
+ return { id: "personal" };
41
+ const { items } = await ctx.api.listMyScenarioGroups(ws);
42
+ if (items.length === 0)
43
+ return { id: null, note: "Scenario groups exist only on-prem — `group` was ignored." };
44
+ return { id: (0, resolve_js_1.resolveRef)("scenario group", ref, items).item.id };
45
+ };
46
+ const latestScenario = async (ctx, ws, id) => ctx.api.getScenario(ws, id);
15
47
  const registerScenarioTools = (server, ctx) => {
16
48
  server.registerTool("list_scenarios", {
17
49
  title: "List scenarios",
18
- description: "List API test scenarios (multi-step flows) with their folder, review status and version. `query` filters by name. Scenarios are scoped by edition (on-prem: your groups), not strictly by workspace.",
50
+ description: "List API test scenarios (multi-step flows) you can access — regardless of workspace (on-prem: your groups) — with their folder, workspace, review status and version, plus the scenario folders and (on-prem) the groups you can create scenarios in. `query` filters by name.",
19
51
  inputSchema: { workspace: shared_js_1.workspaceArg, query: zod_1.z.string().max(200).optional(), folder: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
20
52
  annotations: shared_js_1.READ,
21
53
  }, (0, shared_js_1.guarded)(async ({ workspace, query, folder, cursor, limit }) => {
22
54
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
23
- const { scenarios, folders } = await (0, lookups_js_1.loadScenarioCatalog)(ctx, ws?.id ?? null);
55
+ const [{ scenarios, folders }, groups, workspaces] = await Promise.all([
56
+ (0, lookups_js_1.loadScenarioCatalog)(ctx, ws?.id ?? null),
57
+ ctx.api.listMyScenarioGroups(ws?.id ?? null).catch(() => ({ items: [] })),
58
+ ctx.listWorkspaces(),
59
+ ]);
60
+ const workspaceName = new Map(workspaces.map((entry) => [entry.id, entry.name]));
24
61
  const folderName = new Map(folders.map((entry) => [entry.id, entry.name]));
25
62
  const scope = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder) : null;
26
63
  const needle = query?.trim().toLowerCase();
27
64
  const filtered = scenarios
28
65
  .filter((scenario) => !scope || scenario.folderId === scope.item.id)
29
66
  .filter((scenario) => !needle || scenario.name.toLowerCase().includes(needle));
30
- const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit);
67
+ const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit, `${scope?.item.id ?? ""}|${needle ?? ""}`);
31
68
  return (0, shared_js_1.ok)({
32
69
  ...(ws ? { workspace: { id: ws.id, name: ws.name } } : {}),
33
70
  folders: folders.map((entry) => ({ id: entry.id, name: entry.name })),
34
- scenarios: page.items.map((scenario) => (0, summarize_js_1.scenarioSummaryView)(scenario, scenario.folderId ? (folderName.get(scenario.folderId) ?? null) : null)),
71
+ ...(groups.items.length > 0 ? { groups: groups.items.map((group) => ({ id: group.id, name: group.name, role: group.role })) } : {}),
72
+ scenarios: page.items.map((scenario) => ({
73
+ ...(0, summarize_js_1.scenarioSummaryView)(scenario, scenario.folderId ? (folderName.get(scenario.folderId) ?? null) : null),
74
+ ...(workspaces.length > 1 && scenario.workspaceId ? { workspace: { id: scenario.workspaceId, name: workspaceName.get(scenario.workspaceId) ?? null } } : {}),
75
+ })),
35
76
  total: page.total,
36
77
  nextCursor: page.nextCursor,
37
78
  });
38
79
  }));
39
80
  server.registerTool("get_scenario", {
40
81
  title: "Get a scenario",
41
- description: "Read a scenario: `summary` (metadata), `graph` (nodes with type/label and the edges — enough to understand the flow) or `full` (node configs, secrets masked, clipped at 2 KB per node). Use `graph` before editing with update_scenario.",
82
+ description: "Read a scenario: `summary` (metadata), `graph` (default — nodes with type/label and the edges), `full` (every node config with secrets masked in place, in the shape the write tools accept; a config over 2 KB comes back as a clipped preview) or one node in full with `node: \"<id or label>\"`. Edit with patch_scenario (one node at a time) or update_scenario.",
42
83
  inputSchema: {
43
84
  workspace: shared_js_1.workspaceArg,
44
85
  scenario: (0, shared_js_1.refArg)("Scenario"),
45
86
  view: zod_1.z.enum(["summary", "graph", "full"]).default("graph"),
46
- reveal: zod_1.z.boolean().optional().describe("full view: return environment values and auth secrets unmasked (needs environments:reveal)"),
87
+ node: zod_1.z.string().max(200).optional().describe("Return this one node (id or label) with its complete config, unclipped"),
88
+ reveal: zod_1.z.boolean().optional().describe("Return environment values and node secrets unmasked (needs environments:reveal)"),
47
89
  },
48
90
  annotations: shared_js_1.READ,
49
- }, (0, shared_js_1.guarded)(async ({ workspace, scenario, view, reveal }) => {
91
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, view, node, reveal }) => {
50
92
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
51
93
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
52
94
  const fresh = resolved.matched === "id" && !reveal ? resolved.item : await ctx.api.getScenario(ws?.id ?? null, resolved.item.id, reveal ?? false);
95
+ if (node) {
96
+ const found = (0, summarize_js_1.scenarioNodeView)(fresh, node, { reveal });
97
+ if (!found) {
98
+ throw new errors_js_1.ToolInputError("SCENARIO_NODE_NOT_FOUND", `No node "${node}" in "${fresh.name}".`, { nodes: fresh.nodes.map((entry) => ({ id: entry.id, type: entry.type, label: entry.data?.label ?? null })) });
99
+ }
100
+ return (0, shared_js_1.ok)({ scenario: { id: fresh.id, name: fresh.name, version: fresh.version }, node: found });
101
+ }
53
102
  const body = view === "summary" ? (0, summarize_js_1.scenarioSummaryView)(fresh) : view === "graph" ? (0, summarize_js_1.scenarioGraphView)(fresh) : (0, summarize_js_1.scenarioFullView)(fresh, { reveal });
54
103
  return (0, shared_js_1.ok)({ scenario: body, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] } : {}) });
55
104
  }));
56
105
  server.registerTool("get_scenario_report", {
57
106
  title: "Get a scenario run report",
58
- description: "The result of a scenario run: status, summary counts and a per-node timeline (status, duration, HTTP status, failed tests) with the first failed node expanded. Defaults to the latest run.",
107
+ description: "The result of a scenario run: status, summary counts, an error outside any node (unexpectedError), a per-node timeline and every failing node expanded (failed tests, assertion checks with expected vs actual, failed contracts, script error with line, the input as resolved at run time, response, logs). `node` returns one node's result in that detail. Defaults to the latest run; `list: true` lists recent runs instead.",
59
108
  inputSchema: {
60
109
  workspace: shared_js_1.workspaceArg,
61
110
  scenario: (0, shared_js_1.refArg)("Scenario"),
62
111
  report: zod_1.z.string().max(64).optional().describe("Report id; omit for the latest run"),
112
+ node: zod_1.z.string().max(200).optional().describe("Return this node's result in detail (id or label)"),
113
+ list: zod_1.z.boolean().optional().describe("List the last 20 runs (id, status, start, failed node) instead of one report"),
63
114
  },
64
115
  annotations: shared_js_1.READ,
65
- }, (0, shared_js_1.guarded)(async ({ workspace, scenario, report }) => {
116
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, report, node, list }) => {
66
117
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
67
118
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
119
+ if (list) {
120
+ const runs = await ctx.api.listScenarioReports(ws?.id ?? null, resolved.item.id, 20);
121
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, runs: runs.items });
122
+ }
68
123
  let reportId = report;
69
124
  if (!reportId) {
70
- const list = await ctx.api.listScenarioReports(ws?.id ?? null, resolved.item.id, 5);
71
- const latest = list.items[0];
125
+ const runs = await ctx.api.listScenarioReports(ws?.id ?? null, resolved.item.id, 5);
126
+ const latest = runs.items[0];
72
127
  if (!latest)
73
128
  return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, report: null, note: "This scenario has not been run on the server yet. Use run_scenario." });
74
129
  reportId = latest.reportId;
75
130
  }
76
131
  const detail = await ctx.api.getScenarioReport(ws?.id ?? null, resolved.item.id, reportId);
132
+ if (node) {
133
+ const result = (detail.nodeResults ?? []).find((entry) => entry.nodeId === node || entry.label === node);
134
+ if (!result)
135
+ throw new errors_js_1.ToolInputError("SCENARIO_NODE_NOT_FOUND", `No result for node "${node}" in this run.`, { nodes: (detail.nodeResults ?? []).map((entry) => ({ nodeId: entry.nodeId, label: entry.label, status: entry.status })) });
136
+ return (0, shared_js_1.ok)({ reportId: detail.id, node: (0, summarize_js_1.nodeDetailView)(result) });
137
+ }
77
138
  return (0, shared_js_1.ok)({ report: (0, summarize_js_1.compactReportView)(detail), ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] } : {}) });
78
139
  }));
79
140
  server.registerTool("create_scenario", {
80
141
  title: "Create a scenario",
81
- description: "Create a multi-step API test scenario from compact nodes and edges. Node configs are merged over the type's defaults, so partial configs are fine (a request node needs at least method + url). Read callman://docs/scenario-nodes for every node type and config field. Positions are laid out automatically. dryRun: true returns the resolved graph without saving. On-prem: new scenarios start as drafts and need approval before scheduled/CI runs.",
142
+ description: "Create a multi-step API test scenario from compact nodes and edges. READ callman://docs/scenario-nodes first: it lists every node type, its config fields, defaults and which are required. Configs are merged over the type's defaults (send what you set; arrays you send must be complete). Request nodes also accept the request tools' shapes (auth { type: \"bearer\", token }, a JSON body) and are converted. Labels are generated when omitted and must be unique. db/redis/kafka/notification/ai nodes need a connectionId (list_connections). Config keys the server does not know come back as warnings. dryRun: true returns the complete resolved nodes without saving. On-prem: new scenarios start as drafts — submit_scenario sends them for approval; personal-token runs use the approved version.",
82
143
  inputSchema: {
83
144
  workspace: shared_js_1.workspaceArg,
84
145
  folder: zod_1.z.string().max(200).optional().describe("Scenario folder id or name"),
146
+ group: groupArg,
85
147
  ...build_js_1.scenarioInputSchema.shape,
86
148
  dryRun: shared_js_1.dryRunArg,
87
149
  },
88
150
  annotations: shared_js_1.WRITE,
89
- }, (0, shared_js_1.guarded)(async ({ workspace, folder, dryRun, ...input }) => {
151
+ }, (0, shared_js_1.guarded)(async ({ workspace, folder, group, dryRun, ...input }) => {
90
152
  const ws = await ctx.resolveWorkspace(workspace);
91
153
  const targetFolder = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws.id, folder) : null;
92
- const body = (0, build_js_1.buildScenarioCreateBody)(input, targetFolder?.item.id ?? null);
154
+ const targetGroup = await resolveGroup(ctx, ws.id, group);
155
+ const { body, graph } = (0, build_js_1.buildScenarioCreateBody)(input, targetFolder?.item.id ?? null, targetGroup?.id === null ? undefined : targetGroup?.id);
93
156
  if (dryRun) {
94
- const nodes = body.nodes;
95
- return (0, shared_js_1.ok)({ dryRun: true, scenario: { name: body.name, nodeCount: nodes.length, edgeCount: body.edges.length, nodes: nodes.map((node) => ({ id: node.id, type: node.type, label: node.data.label ?? null })), edges: body.edges } });
157
+ return (0, shared_js_1.ok)({ dryRun: true, scenario: { name: body.name, nodes: graph.nodes.map((node) => ({ id: node.id, type: node.type, label: node.data.label, config: node.data.config })), edges: graph.edges }, warnings: graph.warnings });
96
158
  }
97
159
  const created = await ctx.api.createScenario(ws.id, body);
98
160
  ctx.invalidate("scenarios:");
161
+ const warnings = [...graph.warnings, ...droppedFieldWarnings(graph, created), ...(created.graph?.warnings ?? []), ...(targetGroup?.note ? [targetGroup.note] : [])];
99
162
  return (0, shared_js_1.ok)({
100
163
  scenario: (0, summarize_js_1.scenarioGraphView)(created),
101
- ...(created.graph?.warnings?.length ? { warnings: created.graph.warnings } : {}),
102
- note: created.reviewStatus === "draft" ? "Created as a draft. Run it with run_scenario; on-prem, submit it for approval in the desktop app before scheduling." : "Created.",
164
+ ...(warnings.length > 0 ? { warnings } : {}),
165
+ note: created.reviewStatus === "draft"
166
+ ? "Created as a draft. On-prem, run_scenario with a personal token runs the APPROVED version — submit_scenario and have it approved first; on cloud it runs as is."
167
+ : "Created.",
103
168
  ...(targetFolder ? { resolved: [(0, resolve_js_1.resolvedSummary)(folder, targetFolder)] } : {}),
104
169
  });
105
170
  }));
171
+ server.registerTool("patch_scenario", {
172
+ title: "Edit a scenario incrementally",
173
+ description: "Apply operations to a scenario without resending it: updateNode (config MERGED into the stored node — only the keys you send change; label; position), addNode, removeNode (drops its edges), addEdge, removeEdge. A masked secret sent back unchanged keeps the stored value. Version-checked like update_scenario.",
174
+ inputSchema: {
175
+ workspace: shared_js_1.workspaceArg,
176
+ scenario: (0, shared_js_1.refArg)("Scenario"),
177
+ expectedVersion: zod_1.z.number().int().nonnegative().optional(),
178
+ ops: zod_1.z
179
+ .array(zod_1.z.discriminatedUnion("op", [
180
+ (0, strict_js_1.strictObject)({ op: zod_1.z.literal("updateNode"), id: zod_1.z.string().min(1).max(64), label: zod_1.z.string().max(200).optional(), config: zod_1.z.record(zod_1.z.unknown()).optional(), position: zod_1.z.object({ x: zod_1.z.number(), y: zod_1.z.number() }).strict().optional() }, "field", "updateNode ops"),
181
+ (0, strict_js_1.strictObject)({ op: zod_1.z.literal("addNode"), node: build_js_1.scenarioNodeInputSchema }, "field", "addNode ops"),
182
+ (0, strict_js_1.strictObject)({ op: zod_1.z.literal("removeNode"), id: zod_1.z.string().min(1).max(64) }, "field", "removeNode ops"),
183
+ (0, strict_js_1.strictObject)({ op: zod_1.z.literal("addEdge"), edge: build_js_1.scenarioEdgeInputSchema }, "field", "addEdge ops"),
184
+ (0, strict_js_1.strictObject)({ op: zod_1.z.literal("removeEdge"), id: zod_1.z.string().min(1).max(64).optional(), source: zod_1.z.string().max(64).optional(), target: zod_1.z.string().max(64).optional() }, "field", "removeEdge ops"),
185
+ ]))
186
+ .min(1)
187
+ .max(200),
188
+ },
189
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
190
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, expectedVersion, ops }) => {
191
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
192
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
193
+ const current = await latestScenario(ctx, ws?.id ?? null, resolved.item.id);
194
+ const nodes = new Map(current.nodes.map((node) => [node.id, { id: node.id, type: node.type }]));
195
+ let edges = current.edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, ...(edge.type ? { type: edge.type } : {}) }));
196
+ for (const op of ops) {
197
+ switch (op.op) {
198
+ case "updateNode": {
199
+ const node = nodes.get(op.id);
200
+ if (!node)
201
+ throw new errors_js_1.ToolInputError("SCENARIO_NODE_NOT_FOUND", `No node "${op.id}".`, { nodes: [...nodes.keys()] });
202
+ nodes.set(op.id, { ...node, ...(op.label !== undefined ? { label: op.label } : {}), ...(op.position ? { position: op.position } : {}), ...(op.config ? { config: { ...(node.config ?? {}), ...op.config } } : {}) });
203
+ break;
204
+ }
205
+ case "addNode": {
206
+ const id = op.node.id;
207
+ if (id && nodes.has(id))
208
+ throw new errors_js_1.ToolInputError("SCENARIO_DUPLICATE_NODE_ID", `A node "${id}" already exists — use updateNode.`);
209
+ nodes.set(id ?? `__new${nodes.size}`, op.node);
210
+ break;
211
+ }
212
+ case "removeNode":
213
+ if (!nodes.delete(op.id))
214
+ throw new errors_js_1.ToolInputError("SCENARIO_NODE_NOT_FOUND", `No node "${op.id}".`);
215
+ edges = edges.filter((edge) => edge.source !== op.id && edge.target !== op.id);
216
+ break;
217
+ case "addEdge":
218
+ edges.push(op.edge);
219
+ break;
220
+ case "removeEdge": {
221
+ const before = edges.length;
222
+ edges = edges.filter((edge) => !(op.id ? edge.id === op.id : edge.source === op.source && edge.target === op.target));
223
+ if (edges.length === before)
224
+ throw new errors_js_1.ToolInputError("SCENARIO_EDGE_NOT_FOUND", "No such edge — give its id, or source + target.");
225
+ break;
226
+ }
227
+ }
228
+ }
229
+ const graph = (0, build_js_1.buildScenarioGraph)([...nodes.values()], edges, { existing: current.nodes });
230
+ return saveGraph(ctx, ws?.id ?? null, current, graph, { expectedVersion, annotation: resolved.annotation ? (0, resolve_js_1.resolvedSummary)(scenario, resolved) : undefined });
231
+ }));
106
232
  server.registerTool("update_scenario", {
107
233
  title: "Update a scenario",
108
- description: "Update a scenario's name/description/priority/folder and/or replace its graph (send nodes AND edges together; node configs merge over defaults like create_scenario). Version-checked: pass the `version` you read (expectedVersion) or it is fetched first; a 409 conflict comes back structured — re-read and retry. On-prem: editing an approved scenario returns it to draft.",
234
+ description: "Update a scenario's name / description / priority / folder / environments / dataset / dbSettings and/or replace its graph (nodes AND edges together). A node whose id already exists MERGES into the stored node (only the config keys you send change, its position is kept); new ids merge over the type's defaults; nodes you leave out are removed. `environments` replaces the whole list (stored ids are kept by name; a masked value sent back unchanged keeps the stored one). For small edits prefer patch_scenario. Version-checked: pass the `version` you read (expectedVersion) or it is fetched first; a conflict comes back structured. On-prem: editing an approved scenario returns it to draft.",
109
235
  inputSchema: {
110
236
  workspace: shared_js_1.workspaceArg,
111
237
  scenario: (0, shared_js_1.refArg)("Scenario"),
112
238
  expectedVersion: zod_1.z.number().int().nonnegative().optional(),
113
239
  force: zod_1.z.boolean().optional().describe("Overwrite even if someone changed it since expectedVersion"),
114
- name: zod_1.z.string().min(1).max(200).optional(),
240
+ name: zod_1.z.string().trim().min(1).max(200).optional(),
115
241
  description: zod_1.z.string().max(5000).optional(),
116
242
  priority: zod_1.z.enum(["critical", "high", "medium", "low"]).optional(),
117
243
  folder: zod_1.z.string().max(200).nullable().optional().describe("Scenario folder id or name; null = no folder"),
118
244
  nodes: zod_1.z.array(build_js_1.scenarioNodeInputSchema).min(1).max(1000).optional(),
119
245
  edges: zod_1.z.array(build_js_1.scenarioEdgeInputSchema).max(2000).optional(),
120
- environments: zod_1.z.array(build_js_1.scenarioEnvironmentInputSchema).max(20).optional(),
246
+ environments: zod_1.z.array(build_js_1.scenarioEnvironmentInputSchema).max(100).optional(),
247
+ dataset: build_js_1.scenarioDatasetSchema.optional(),
248
+ dbSettings: build_js_1.scenarioDbSettingsSchema.optional(),
121
249
  },
122
250
  annotations: shared_js_1.IDEMPOTENT_WRITE,
123
- }, (0, shared_js_1.guarded)(async ({ workspace, scenario, expectedVersion, force, name, description, priority, folder, nodes, edges, environments }) => {
251
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, expectedVersion, force, name, description, priority, folder, nodes, edges, environments, dataset, dbSettings }) => {
124
252
  if ((nodes === undefined) !== (edges === undefined)) {
125
- throw new errors_js_1.ToolInputError("GRAPH_INCOMPLETE", "Send nodes and edges together when replacing the graph.");
253
+ throw new errors_js_1.ToolInputError("GRAPH_INCOMPLETE", "Send nodes and edges together when replacing the graph (or use patch_scenario for single edits).");
126
254
  }
127
255
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
128
256
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
129
- const current = resolved.item;
257
+ const current = await latestScenario(ctx, ws?.id ?? null, resolved.item.id);
130
258
  const targetFolder = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder) : null;
131
- const body = {
132
- ...(name !== undefined ? { name: name.trim() } : {}),
259
+ const graph = nodes && edges ? (0, build_js_1.buildScenarioGraph)(nodes, edges, { existing: current.nodes }) : null;
260
+ const extra = {
261
+ ...(name !== undefined ? { name } : {}),
133
262
  ...(description !== undefined ? { description } : {}),
134
263
  ...(priority !== undefined ? { priority } : {}),
135
264
  ...(folder !== undefined ? { folderId: targetFolder?.item.id ?? null } : {}),
136
- expectedVersion: expectedVersion ?? current.version,
137
- ...(force ? { force: true } : {}),
265
+ ...(environments ? { environments: (0, build_js_1.buildEnvironments)(environments, current.environments) } : {}),
266
+ ...(dataset !== undefined ? { dataset } : {}),
267
+ ...(dbSettings !== undefined ? { dbSettings } : {}),
138
268
  };
139
- if (nodes && edges) {
140
- const graph = (0, build_js_1.buildScenarioGraph)(nodes, edges);
141
- body.nodes = graph.nodes;
142
- body.edges = graph.edges;
143
- }
144
- if (environments) {
145
- const nextEnvId = (0, ids_js_1.sequentialIds)("env", current.environments.map((environment) => environment.id));
146
- body.environments = environments.map((environment) => {
147
- const nextVarId = (0, ids_js_1.sequentialIds)("v");
148
- return {
149
- id: environment.id ?? nextEnvId(),
150
- name: environment.name,
151
- variables: environment.variables.filter((variable) => variable.key.trim()).map((variable) => ({ id: nextVarId(), key: variable.key.trim(), value: variable.value ?? "", enabled: variable.enabled ?? true })),
152
- };
153
- });
154
- }
155
- if (Object.keys(body).filter((key) => key !== "expectedVersion" && key !== "force").length === 0) {
269
+ if (!graph && Object.keys(extra).length === 0)
156
270
  throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass at least one field to change.");
157
- }
158
- try {
159
- const updated = await ctx.api.patchScenario(ws?.id ?? null, current.id, body);
160
- ctx.invalidate("scenarios:");
161
- return (0, shared_js_1.ok)({
162
- scenario: (0, summarize_js_1.scenarioGraphView)(updated),
163
- previousVersion: current.version,
164
- ...(current.reviewStatus === "approved" && updated.reviewStatus !== "approved" ? { note: "The scenario was approved and is now a draft again; schedules keep using the last approved version until it is re-approved." } : {}),
165
- ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] } : {}),
271
+ return saveGraph(ctx, ws?.id ?? null, current, graph, { expectedVersion, force, extra, annotation: resolved.annotation ? (0, resolve_js_1.resolvedSummary)(scenario, resolved) : undefined });
272
+ }));
273
+ server.registerTool("submit_scenario", {
274
+ title: "Submit a scenario for approval",
275
+ description: "Send a draft scenario for review (on-prem approval flow). Personal-token and scheduled runs use the APPROVED version, so a new or edited scenario must be approved before run_scenario runs the change. Optionally name approvers by email (default: every eligible approver is notified). Approval itself happens in the desktop app.",
276
+ inputSchema: {
277
+ workspace: shared_js_1.workspaceArg,
278
+ scenario: (0, shared_js_1.refArg)("Scenario"),
279
+ approvers: zod_1.z.array(zod_1.z.string().max(255)).max(100).optional().describe("Approver emails; omit to notify all eligible approvers"),
280
+ },
281
+ annotations: shared_js_1.WRITE,
282
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, approvers }) => {
283
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
284
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
285
+ let approverIds;
286
+ if (approvers && approvers.length > 0) {
287
+ const { items } = await ctx.api.listApproverCandidates(ws?.id ?? null, resolved.item.id);
288
+ approverIds = approvers.map((email) => {
289
+ const match = items.find((candidate) => candidate.email.toLowerCase() === email.trim().toLowerCase());
290
+ if (!match)
291
+ throw new errors_js_1.ToolInputError("APPROVER_NOT_ELIGIBLE", `"${email}" cannot approve this scenario.`, { candidates: items.map((candidate) => candidate.email) });
292
+ return match.userId;
166
293
  });
167
294
  }
168
- catch (error) {
169
- if (error instanceof errors_js_1.CallmanApiError && error.code === "SCENARIO_VERSION_CONFLICT") {
170
- const details = (error.details ?? {});
171
- return (0, shared_js_1.ok)({
172
- conflict: true,
173
- code: error.code,
174
- message: error.message,
175
- currentVersion: details.currentScenario?.version ?? null,
176
- hint: "Re-read the scenario (get_scenario view:graph), reapply your change and call update_scenario with expectedVersion set to currentVersion — or force: true to overwrite.",
177
- });
178
- }
179
- throw error;
180
- }
295
+ const submitted = await ctx.api.submitScenario(ws?.id ?? null, resolved.item.id, approverIds ? { approverIds } : {});
296
+ ctx.invalidate("scenarios:");
297
+ return (0, shared_js_1.ok)({ scenario: { id: submitted.id ?? resolved.item.id, name: submitted.name ?? resolved.item.name, reviewStatus: submitted.reviewStatus ?? null, version: submitted.version ?? null }, note: "Submitted — an approver approves it in the desktop app." });
181
298
  }));
182
299
  server.registerTool("delete_scenario", {
183
300
  title: "Delete a scenario",
184
- description: "Delete a scenario and its run history. Permanent — confirm with the user first.",
301
+ description: "Delete a scenario and its run history (exact name or id). Permanent — confirm with the user first.",
185
302
  inputSchema: { workspace: shared_js_1.workspaceArg, scenario: (0, shared_js_1.refArg)("Scenario"), confirm: shared_js_1.confirmArg },
186
303
  annotations: shared_js_1.DESTRUCTIVE,
187
304
  }, (0, shared_js_1.guarded)(async ({ workspace, scenario, confirm }) => {
188
305
  (0, shared_js_1.requireConfirm)(confirm, "a scenario");
189
306
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
190
- const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
307
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario, { exact: true });
191
308
  await ctx.api.deleteScenario(ws?.id ?? null, resolved.item.id);
192
309
  ctx.invalidate("scenarios:");
193
310
  return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] });
194
311
  }));
195
312
  server.registerTool("run_scenario", {
196
313
  title: "Run a scenario on the server",
197
- description: "Start a server-side run of a scenario (with one of its inline environments, by name or id) and wait for the result (default up to 5 minutes, max 30). Returns the compact report; if it times out you get the reportId to check later with get_scenario_report.",
314
+ description: "Start a server-side run of a scenario (with one of its inline environments, by name or id; the first one when omitted) and wait for the result (default up to 5 minutes, max 30). Returns the report as in get_scenario_report. A personal token runs the scenario's APPROVED version on-prem — the result says when that differs from the version you edited. If it times out you get the reportId to check later.",
198
315
  inputSchema: {
199
316
  workspace: shared_js_1.workspaceArg,
200
317
  scenario: (0, shared_js_1.refArg)("Scenario"),
@@ -239,7 +356,53 @@ const registerScenarioTools = (server, ctx) => {
239
356
  if (!last || !TERMINAL_STATUSES.has(last.status)) {
240
357
  return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, reportId: trigger.reportId, status: last?.status ?? "running", timedOut: true, note: `Still running after ${timeoutSec}s — call get_scenario_report with this reportId later.` });
241
358
  }
242
- return (0, shared_js_1.ok)({ report: (0, summarize_js_1.compactReportView)(last), ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] } : {}) });
359
+ const warnings = [];
360
+ if (typeof last.scenarioVersion === "number" && last.scenarioVersion !== resolved.item.version) {
361
+ warnings.push(`This run executed version ${last.scenarioVersion} (the approved one); the live scenario is version ${resolved.item.version}. Edits since then run only after submit_scenario + approval.`);
362
+ }
363
+ if (environmentId && last.environmentId !== undefined && last.environmentId !== environmentId) {
364
+ warnings.push(`The server ran environment ${last.environmentId ?? "(first)"} instead of the one you picked — this backend ignores the choice; update the server.`);
365
+ }
366
+ return (0, shared_js_1.ok)({ report: (0, summarize_js_1.compactReportView)(last), ...(warnings.length > 0 ? { warnings } : {}), ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] } : {}) });
243
367
  }));
244
368
  };
245
369
  exports.registerScenarioTools = registerScenarioTools;
370
+ // PATCH with the version check; a conflict comes back structured instead of
371
+ // as an error so the model can re-read and retry.
372
+ const saveGraph = async (ctx, ws, current, graph, options) => {
373
+ const body = {
374
+ ...(options.extra ?? {}),
375
+ ...(graph ? { nodes: graph.nodes, edges: graph.edges } : {}),
376
+ expectedVersion: options.expectedVersion ?? current.version,
377
+ ...(options.force ? { force: true } : {}),
378
+ };
379
+ try {
380
+ const updated = await ctx.api.patchScenario(ws, current.id, body);
381
+ ctx.invalidate("scenarios:");
382
+ const warnings = [...(graph?.warnings ?? []), ...(graph ? droppedFieldWarnings(graph, updated) : []), ...(updated.graph?.warnings ?? [])];
383
+ const removed = graph ? current.nodes.filter((node) => !graph.nodes.some((entry) => entry.id === node.id)).map((node) => node.id) : [];
384
+ return (0, shared_js_1.ok)({
385
+ scenario: (0, summarize_js_1.scenarioGraphView)(updated),
386
+ previousVersion: current.version,
387
+ ...(removed.length > 0 ? { removedNodes: removed } : {}),
388
+ ...(warnings.length > 0 ? { warnings } : {}),
389
+ ...(current.reviewStatus === "approved" && updated.reviewStatus !== "approved"
390
+ ? { note: "The scenario was approved and is now a draft again; runs with a personal token and schedules keep using the last approved version until it is re-approved (submit_scenario)." }
391
+ : {}),
392
+ ...(options.annotation ? { resolved: [options.annotation] } : {}),
393
+ });
394
+ }
395
+ catch (error) {
396
+ if (error instanceof errors_js_1.CallmanApiError && error.code === "SCENARIO_VERSION_CONFLICT") {
397
+ const details = (error.details ?? {});
398
+ return (0, shared_js_1.ok)({
399
+ conflict: true,
400
+ code: error.code,
401
+ message: error.message,
402
+ currentVersion: details.currentScenario?.version ?? null,
403
+ hint: "Re-read the scenario (get_scenario), reapply your change and retry with expectedVersion set to currentVersion — or force: true to overwrite.",
404
+ });
405
+ }
406
+ throw error;
407
+ }
408
+ };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireConfirm = exports.dryRunArg = exports.confirmArg = exports.cursorArgs = exports.refArg = exports.workspaceArg = exports.RUN = exports.DESTRUCTIVE = exports.IDEMPOTENT_WRITE = exports.WRITE = exports.READ = exports.guarded = exports.toolFailure = exports.ok = void 0;
3
+ exports.requireConfirm = exports.dryRunArg = exports.confirmArg = exports.cursorArgs = exports.refArg = exports.workspaceArg = exports.RUN = exports.DESTRUCTIVE = exports.IDEMPOTENT_WRITE = exports.WRITE = exports.READ = exports.guarded = exports.failureOf = exports.toolFailure = exports.ok = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
6
  const truncate_js_1 = require("../lib/truncate.js");
@@ -42,6 +42,15 @@ const toolFailure = (error) => {
42
42
  return { isError: true, content: [{ type: "text", text: (0, truncate_js_1.safeStringify)(structured) }], structuredContent: structured };
43
43
  };
44
44
  exports.toolFailure = toolFailure;
45
+ // One failed item of a batch (bulk create, move, delete, env writes): the
46
+ // same code / hint / details / candidates a single call would return, so a
47
+ // model can fix the item instead of guessing from "Validation failed".
48
+ const failureOf = (error) => {
49
+ const payload = (0, exports.toolFailure)(error).structuredContent.error;
50
+ const { request: _request, status: _status, ...rest } = payload;
51
+ return rest;
52
+ };
53
+ exports.failureOf = failureOf;
45
54
  const guarded = (fn) => async (args) => {
46
55
  try {
47
56
  return await fn(args);
@@ -62,7 +71,7 @@ exports.workspaceArg = zod_1.z
62
71
  .string()
63
72
  .max(200)
64
73
  .optional()
65
- .describe("Workspace id or name. Defaults to CALLMAN_WORKSPACE_ID, or the only workspace you have; call list_workspaces to see them.");
74
+ .describe('Workspace id or name to narrow to. Omit it: lists/searches cover ALL your workspaces and names are found in whichever workspace has them; creating uses the session default (use_workspace). "all" = every workspace.');
66
75
  const refArg = (what, extra = "") => zod_1.z.string().min(1).max(500).describe(`${what} id or name${extra ? ` ${extra}` : ""}`);
67
76
  exports.refArg = refArg;
68
77
  exports.cursorArgs = {
@@ -9,7 +9,7 @@ const shared_js_1 = require("./shared.js");
9
9
  const registerWorkspaceTools = (server, ctx) => {
10
10
  server.registerTool("list_workspaces", {
11
11
  title: "List workspaces",
12
- description: "List the workspaces you belong to, with your role in each (owner / editor / viewer). A token keeps your role: writes are refused where you are a viewer.",
12
+ description: "List the workspaces you belong to, with your role in each (owner / editor / viewer) and which one is the session default. The token works in all of them; a token keeps your role: writes are refused where you are a viewer.",
13
13
  inputSchema: {},
14
14
  annotations: shared_js_1.READ,
15
15
  }, (0, shared_js_1.guarded)(async () => {
@@ -20,10 +20,26 @@ const registerWorkspaceTools = (server, ctx) => {
20
20
  name: workspace.name,
21
21
  role: workspace.role,
22
22
  memberCount: workspace.memberCount ?? null,
23
- isDefault: workspace.id === ctx.config.defaultWorkspaceId,
23
+ isDefault: workspace.id === ctx.getDefaultWorkspaceId(),
24
24
  })),
25
25
  });
26
26
  }));
27
+ server.registerTool("use_workspace", {
28
+ title: "Set the default workspace for this session",
29
+ description: "Pick the workspace new things go to for the rest of this session (collections, environments, scenarios, imports — anything created without `workspace`), and the one that wins when a name exists in several workspaces. It never hides the others: lists and searches still cover every workspace. `workspace: null` clears the default. Lasts until the MCP server restarts; CALLMAN_WORKSPACE_ID sets it at start.",
30
+ inputSchema: { workspace: zod_1.z.string().max(200).nullable().describe("Workspace name or id; null = no default") },
31
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
32
+ }, (0, shared_js_1.guarded)(async ({ workspace }) => {
33
+ const previous = ctx.getDefaultWorkspaceId();
34
+ if (workspace === null || !workspace.trim()) {
35
+ ctx.setDefaultWorkspaceId(null);
36
+ return (0, shared_js_1.ok)({ defaultWorkspace: null, previousDefaultId: previous, note: "No default workspace: creating something now needs `workspace`; reads still cover all workspaces." });
37
+ }
38
+ const target = await ctx.resolveWorkspace(workspace);
39
+ ctx.setDefaultWorkspaceId(target.id);
40
+ ctx.invalidate();
41
+ return (0, shared_js_1.ok)({ defaultWorkspace: { id: target.id, name: target.name, role: target.role }, previousDefaultId: previous });
42
+ }));
27
43
  server.registerTool("list_workspace_members", {
28
44
  title: "List workspace members",
29
45
  description: "List the members of a workspace with their role and status (active / invited).",
@@ -39,7 +55,7 @@ const registerWorkspaceTools = (server, ctx) => {
39
55
  }));
40
56
  server.registerTool("add_workspace_member", {
41
57
  title: "Add a workspace member",
42
- description: "Add a person to a workspace by email with a role (editor can write collections/requests/scenarios; viewer is read-only; owner manages the workspace). Requires the owner role in that workspace. The person must already have a Callman account — the server does not send invitations to unknown emails.",
58
+ description: "Add a person to a workspace by email with a role (editor can write collections/requests/scenarios; viewer is read-only; owner manages the workspace). Requires the owner role in that workspace. The person must already have a Callman account — the server does not send invitations to unknown emails. Change a role later with update_workspace_member.",
43
59
  inputSchema: {
44
60
  workspace: shared_js_1.workspaceArg,
45
61
  email: zod_1.z.string().email().max(255),
@@ -53,12 +69,33 @@ const registerWorkspaceTools = (server, ctx) => {
53
69
  return (0, shared_js_1.ok)({
54
70
  workspace: { id: target.id, name: target.name },
55
71
  member: { id: member.id, email: member.email, role: member.role, status: member.status },
56
- note: member.status === "invited" ? "Pending — the person gets access when they sign in." : "Added.",
72
+ note: "Added — the person has access now.",
57
73
  });
58
74
  }));
75
+ server.registerTool("update_workspace_member", {
76
+ title: "Change a member's role",
77
+ description: "Change a workspace member's role (owner / editor / viewer) by exact email or membership id. Requires the owner role; the last owner cannot be demoted.",
78
+ inputSchema: {
79
+ workspace: shared_js_1.workspaceArg,
80
+ member: zod_1.z.string().min(1).max(255).describe("Member email or membership id"),
81
+ role: zod_1.z.enum(["owner", "editor", "viewer"]),
82
+ },
83
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
84
+ }, (0, shared_js_1.guarded)(async ({ workspace, member, role }) => {
85
+ const target = await ctx.resolveWorkspace(workspace);
86
+ const { items } = await ctx.api.listMembers(target.id);
87
+ const needle = (0, ids_js_1.normalizeName)(member);
88
+ const byEmail = items.filter((entry) => (0, ids_js_1.normalizeName)(entry.email) === needle);
89
+ const resolved = byEmail.length === 1 ? byEmail[0] : (0, resolve_js_1.resolveRef)("member", member, items.map((entry) => ({ ...entry, name: entry.email })), { exact: true }).item;
90
+ if (resolved.role === role)
91
+ return (0, shared_js_1.ok)({ workspace: { id: target.id, name: target.name }, member: { id: resolved.id, email: resolved.email, role }, note: "Already that role — nothing changed." });
92
+ const updated = await ctx.api.updateMemberRole(target.id, resolved.id, role);
93
+ ctx.invalidate(`members:${target.id}`);
94
+ return (0, shared_js_1.ok)({ workspace: { id: target.id, name: target.name }, member: { id: updated.id ?? resolved.id, email: updated.email ?? resolved.email, role: updated.role ?? role, previousRole: resolved.role } });
95
+ }));
59
96
  server.registerTool("remove_workspace_member", {
60
97
  title: "Remove a workspace member",
61
- description: "Remove a member from a workspace (by email or membership id). Requires the owner role. Permanent — confirm with the user first.",
98
+ description: "Remove a member from a workspace (by exact email or membership id). Owners can remove anyone; any member can remove themselves (leave). The last owner cannot be removed. Permanent — confirm with the user first.",
62
99
  inputSchema: {
63
100
  workspace: shared_js_1.workspaceArg,
64
101
  member: zod_1.z.string().min(1).max(255).describe("Member email or membership id"),
@@ -73,7 +110,7 @@ const registerWorkspaceTools = (server, ctx) => {
73
110
  const byEmail = items.filter((entry) => (0, ids_js_1.normalizeName)(entry.email) === needle);
74
111
  const resolved = byEmail.length === 1
75
112
  ? byEmail[0]
76
- : (0, resolve_js_1.resolveRef)("member", member, items.map((entry) => ({ ...entry, name: entry.email }))).item;
113
+ : (0, resolve_js_1.resolveRef)("member", member, items.map((entry) => ({ ...entry, name: entry.email })), { exact: true }).item;
77
114
  if (!resolved)
78
115
  throw new errors_js_1.ToolInputError("REF_NOT_FOUND", `No member matches "${member}".`);
79
116
  await ctx.api.removeMember(target.id, resolved.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callman-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Callman: lets Claude Code, Claude Desktop, Cursor, Codex and Gemini CLI manage collections, requests, environments, test scenarios and system-design diagrams in your Callman workspace — cloud or on-prem.",
5
5
  "keywords": [
6
6
  "callman",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@modelcontextprotocol/sdk": "^1.30.0",
65
- "callman-core": "^1.28.0",
65
+ "callman-core": "^1.28.1",
66
66
  "elkjs": "^0.12.0",
67
67
  "js-yaml": "^4.1.0",
68
68
  "zod": "^3.25.0"