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.
- package/CHANGELOG.md +82 -0
- package/README.md +45 -16
- package/dist/client/api.d.ts +64 -3
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +67 -2
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +33 -7
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +91 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -2
- package/dist/context.js.map +1 -1
- package/dist/domain/contracts.d.ts +893 -0
- package/dist/domain/contracts.d.ts.map +1 -0
- package/dist/domain/contracts.js +90 -0
- package/dist/domain/contracts.js.map +1 -0
- package/dist/domain/diagram/build.d.ts +520 -18
- package/dist/domain/diagram/build.d.ts.map +1 -1
- package/dist/domain/diagram/build.js +65 -19
- package/dist/domain/diagram/build.js.map +1 -1
- package/dist/domain/diagram/patch.d.ts +624 -51
- package/dist/domain/diagram/patch.d.ts.map +1 -1
- package/dist/domain/diagram/patch.js +180 -40
- package/dist/domain/diagram/patch.js.map +1 -1
- package/dist/domain/diagram/style.d.ts +315 -0
- package/dist/domain/diagram/style.d.ts.map +1 -0
- package/dist/domain/diagram/style.js +124 -0
- package/dist/domain/diagram/style.js.map +1 -0
- package/dist/domain/diagram/summarize.d.ts +8 -0
- package/dist/domain/diagram/summarize.d.ts.map +1 -1
- package/dist/domain/diagram/summarize.js +28 -13
- package/dist/domain/diagram/summarize.js.map +1 -1
- package/dist/domain/requestIntegrations.d.ts +719 -0
- package/dist/domain/requestIntegrations.d.ts.map +1 -0
- package/dist/domain/requestIntegrations.js +222 -0
- package/dist/domain/requestIntegrations.js.map +1 -0
- package/dist/domain/requestShape.d.ts +1350 -152
- package/dist/domain/requestShape.d.ts.map +1 -1
- package/dist/domain/requestShape.js +336 -121
- package/dist/domain/requestShape.js.map +1 -1
- package/dist/domain/resolve.d.ts +2 -1
- package/dist/domain/resolve.d.ts.map +1 -1
- package/dist/domain/resolve.js +5 -1
- package/dist/domain/resolve.js.map +1 -1
- package/dist/domain/scenario/build.d.ts +82 -26
- package/dist/domain/scenario/build.d.ts.map +1 -1
- package/dist/domain/scenario/build.js +196 -65
- package/dist/domain/scenario/build.js.map +1 -1
- package/dist/domain/scenario/nodeConfig.d.ts +11 -0
- package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
- package/dist/domain/scenario/nodeConfig.js +251 -0
- package/dist/domain/scenario/nodeConfig.js.map +1 -0
- package/dist/domain/scenario/summarize.d.ts +145 -25
- package/dist/domain/scenario/summarize.d.ts.map +1 -1
- package/dist/domain/scenario/summarize.js +84 -29
- package/dist/domain/scenario/summarize.js.map +1 -1
- package/dist/lib/mask.d.ts +0 -18
- package/dist/lib/mask.d.ts.map +1 -1
- package/dist/lib/mask.js +2 -44
- package/dist/lib/mask.js.map +1 -1
- package/dist/lib/pagination.d.ts +3 -2
- package/dist/lib/pagination.d.ts.map +1 -1
- package/dist/lib/pagination.js +13 -8
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/redact.d.ts +12 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +50 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/strict.d.ts +4 -0
- package/dist/lib/strict.d.ts.map +1 -0
- package/dist/lib/strict.js +14 -0
- package/dist/lib/strict.js.map +1 -0
- package/dist/prompts/index.js +7 -7
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/docs/contracts.d.ts +2 -0
- package/dist/resources/docs/contracts.d.ts.map +1 -0
- package/dist/resources/docs/contracts.js +74 -0
- package/dist/resources/docs/contracts.js.map +1 -0
- package/dist/resources/docs/diagramSchema.d.ts +1 -1
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +95 -28
- package/dist/resources/docs/diagramSchema.js.map +1 -1
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +29 -7
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/docs/requestSchema.d.ts +1 -1
- package/dist/resources/docs/requestSchema.d.ts.map +1 -1
- package/dist/resources/docs/requestSchema.js +85 -16
- package/dist/resources/docs/requestSchema.js.map +1 -1
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.d.ts.map +1 -1
- package/dist/resources/generated/aiPacks.js +3 -2
- package/dist/resources/generated/aiPacks.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +19 -19
- package/dist/resources/index.js.map +1 -1
- package/dist/run/executeCollection.d.ts +3 -1
- package/dist/run/executeCollection.d.ts.map +1 -1
- package/dist/run/executeCollection.js +16 -4
- package/dist/run/executeCollection.js.map +1 -1
- package/dist/run/summarize.d.ts +17 -6
- package/dist/run/summarize.d.ts.map +1 -1
- package/dist/run/summarize.js +48 -41
- package/dist/run/summarize.js.map +1 -1
- package/dist/run/types.d.ts +1 -0
- package/dist/run/types.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +18 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +53 -41
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/contracts.d.ts +4 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +201 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +19 -10
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/environments.d.ts.map +1 -1
- package/dist/tools/environments.js +184 -64
- package/dist/tools/environments.js.map +1 -1
- package/dist/tools/import.d.ts.map +1 -1
- package/dist/tools/import.js +111 -20
- package/dist/tools/import.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +19 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lookups.d.ts +38 -7
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +113 -10
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/meta.d.ts.map +1 -1
- package/dist/tools/meta.js +9 -4
- package/dist/tools/meta.js.map +1 -1
- package/dist/tools/requests.d.ts.map +1 -1
- package/dist/tools/requests.js +138 -50
- package/dist/tools/requests.js.map +1 -1
- package/dist/tools/run.d.ts.map +1 -1
- package/dist/tools/run.js +84 -28
- package/dist/tools/run.js.map +1 -1
- package/dist/tools/scenarios.d.ts.map +1 -1
- package/dist/tools/scenarios.js +238 -75
- package/dist/tools/scenarios.js.map +1 -1
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +9 -1
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/workspaces.d.ts.map +1 -1
- package/dist/tools/workspaces.js +44 -7
- package/dist/tools/workspaces.js.map +1 -1
- package/dist-cjs/client/api.js +67 -2
- package/dist-cjs/client/errors.js +33 -7
- package/dist-cjs/context.js +7 -2
- package/dist-cjs/domain/contracts.js +93 -0
- package/dist-cjs/domain/diagram/build.js +71 -20
- package/dist-cjs/domain/diagram/patch.js +179 -39
- package/dist-cjs/domain/diagram/style.js +130 -0
- package/dist-cjs/domain/diagram/summarize.js +28 -13
- package/dist-cjs/domain/requestIntegrations.js +228 -0
- package/dist-cjs/domain/requestShape.js +342 -121
- package/dist-cjs/domain/resolve.js +5 -1
- package/dist-cjs/domain/scenario/build.js +197 -65
- package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
- package/dist-cjs/domain/scenario/summarize.js +86 -29
- package/dist-cjs/lib/mask.js +3 -48
- package/dist-cjs/lib/pagination.js +13 -8
- package/dist-cjs/lib/redact.js +53 -0
- package/dist-cjs/lib/strict.js +18 -0
- package/dist-cjs/prompts/index.js +7 -7
- package/dist-cjs/resources/docs/contracts.js +76 -0
- package/dist-cjs/resources/docs/diagramSchema.js +95 -28
- package/dist-cjs/resources/docs/overview.js +29 -7
- package/dist-cjs/resources/docs/requestSchema.js +85 -16
- package/dist-cjs/resources/generated/aiPacks.js +3 -2
- package/dist-cjs/resources/index.js +17 -17
- package/dist-cjs/run/executeCollection.js +16 -4
- package/dist-cjs/run/summarize.js +48 -41
- package/dist-cjs/server.js +1 -1
- package/dist-cjs/tools/catalog.js +18 -0
- package/dist-cjs/tools/collections.js +52 -40
- package/dist-cjs/tools/contracts.js +204 -0
- package/dist-cjs/tools/diagrams.js +19 -10
- package/dist-cjs/tools/environments.js +182 -62
- package/dist-cjs/tools/import.js +107 -16
- package/dist-cjs/tools/index.js +21 -2
- package/dist-cjs/tools/lookups.js +123 -11
- package/dist-cjs/tools/meta.js +9 -4
- package/dist-cjs/tools/requests.js +134 -46
- package/dist-cjs/tools/run.js +82 -26
- package/dist-cjs/tools/scenarios.js +235 -72
- package/dist-cjs/tools/shared.js +11 -2
- package/dist-cjs/tools/workspaces.js +43 -6
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
71
|
-
const latest =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
...(
|
|
102
|
-
note: created.reviewStatus === "draft"
|
|
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 (
|
|
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(
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
...(
|
|
265
|
+
...(environments ? { environments: (0, build_js_1.buildEnvironments)(environments, current.environments) } : {}),
|
|
266
|
+
...(dataset !== undefined ? { dataset } : {}),
|
|
267
|
+
...(dbSettings !== undefined ? { dbSettings } : {}),
|
|
138
268
|
};
|
|
139
|
-
if (
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|
package/dist-cjs/tools/shared.js
CHANGED
|
@@ -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(
|
|
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).
|
|
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.
|
|
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:
|
|
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).
|
|
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.
|
|
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.
|
|
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"
|