callman-mcp 0.10.2 → 0.11.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 +25 -0
- package/README.md +14 -3
- package/dist/client/api.d.ts +26 -3
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +11 -0
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +33 -1
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +59 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/domain/pipeline.d.ts +80 -0
- package/dist/domain/pipeline.d.ts.map +1 -0
- package/dist/domain/pipeline.js +139 -0
- package/dist/domain/pipeline.js.map +1 -0
- package/dist/domain/scenario/build.d.ts +6 -6
- 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 +20 -0
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.js +2 -2
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +4 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +249 -17
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/mockSchemas.d.ts +48 -48
- package/dist/tools/mockShared.d.ts +64 -64
- package/dist/tools/run.d.ts +2 -0
- package/dist/tools/run.d.ts.map +1 -1
- package/dist/tools/run.js +34 -5
- package/dist/tools/run.js.map +1 -1
- package/dist-cjs/client/api.js +11 -0
- package/dist-cjs/client/errors.js +33 -1
- package/dist-cjs/domain/pipeline.js +144 -0
- package/dist-cjs/resources/docs/overview.js +20 -0
- package/dist-cjs/resources/generated/aiPacks.js +2 -2
- package/dist-cjs/tools/catalog.js +4 -0
- package/dist-cjs/tools/collections.js +247 -15
- package/dist-cjs/tools/run.js +33 -4
- package/package.json +2 -2
|
@@ -4,6 +4,8 @@ exports.registerCollectionTools = void 0;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const errors_js_1 = require("../client/errors.js");
|
|
6
6
|
const strict_js_1 = require("../lib/strict.js");
|
|
7
|
+
const ids_js_1 = require("../lib/ids.js");
|
|
8
|
+
const pipeline_js_1 = require("../domain/pipeline.js");
|
|
7
9
|
const requestShape_js_1 = require("../domain/requestShape.js");
|
|
8
10
|
const resolve_js_1 = require("../domain/resolve.js");
|
|
9
11
|
const pagination_js_1 = require("../lib/pagination.js");
|
|
@@ -43,6 +45,99 @@ const variableWrites = (variables) => {
|
|
|
43
45
|
// collection variables (older backend).
|
|
44
46
|
const variablesView = (variables) => Array.isArray(variables) ? variables.map((variable) => ({ key: variable.key, value: variable.value, enabled: variable.enabled !== false })) : null;
|
|
45
47
|
const UNSUPPORTED_VARIABLES_NOTE = "This server does not support collection variables yet (update the Callman backend); use an environment instead.";
|
|
48
|
+
// ── Pipeline collections (on-prem) ────────────────────────────────────────
|
|
49
|
+
// Changes go through review like scenarios, approved versions are kept, CI
|
|
50
|
+
// runs only the active approved version and its stored pass criteria decide
|
|
51
|
+
// the verdict. Approving is a person's decision in the app — tokens may
|
|
52
|
+
// submit and request, never approve.
|
|
53
|
+
const kindArg = zod_1.z
|
|
54
|
+
.enum(["pipeline"])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('On-prem: "pipeline" makes it a pipeline collection — reviewed like scenarios, CI runs only its approved version and its pipelineConfig decides pass/fail. Needs `group`. The way back to standard goes through review (desktop app)');
|
|
57
|
+
const groupArg = zod_1.z
|
|
58
|
+
.string()
|
|
59
|
+
.max(200)
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("On-prem pipeline collections: the group whose approvers review it (name or id from the groups you belong to, as list_scenarios shows them). Optional when you belong to one group; fixed once a version is approved or a review is open");
|
|
62
|
+
const PIPELINE_DRAFT_NOTE = "Pipeline collection: edits change the DRAFT. CI (callme run, run_collection) keeps running the approved version until the change is submitted (submit_collection) and approved by a person in the Callman app.";
|
|
63
|
+
// The reviewing group: by id or name among the groups the user belongs to.
|
|
64
|
+
// Omitted → the only group the user has, when there is exactly one.
|
|
65
|
+
const resolveCollectionGroup = async (ctx, ws, ref) => {
|
|
66
|
+
let items;
|
|
67
|
+
try {
|
|
68
|
+
items = (await ctx.api.listMyScenarioGroups(ws)).items;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// The group list needs scenarios:read; an id can still be passed through.
|
|
72
|
+
if (ref && (0, ids_js_1.isObjectId)(ref.trim()) && error instanceof errors_js_1.CallmanApiError && error.code === "PAT_SCOPE_MISSING")
|
|
73
|
+
return { id: ref.trim() };
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (ref) {
|
|
77
|
+
if (items.length === 0)
|
|
78
|
+
throw new errors_js_1.ToolInputError("NO_REVIEW_GROUPS", "You belong to no group that can review collections. Pipeline collections exist on Callman on-prem only, and their group is an on-prem group.");
|
|
79
|
+
return { id: (0, resolve_js_1.resolveRef)("group", ref, items).item.id };
|
|
80
|
+
}
|
|
81
|
+
if (items.length === 1)
|
|
82
|
+
return { id: items[0].id, note: `Reviewed by the group "${items[0].name}" (your only group).` };
|
|
83
|
+
if (items.length > 1) {
|
|
84
|
+
throw new errors_js_1.ToolInputError("COLLECTION_GROUP_REQUIRED", "Pass `group`: the group whose approvers review this pipeline collection.", {
|
|
85
|
+
candidates: items.map((group) => ({ id: group.id, name: group.name })),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
// Critical request refs (ids or names) → ids of requests in this collection.
|
|
91
|
+
const criticalRequestIds = async (ctx, ws, collectionId, refs) => {
|
|
92
|
+
if (refs.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const tree = await (0, lookups_js_1.loadTree)(ctx, ws, collectionId);
|
|
95
|
+
return [...new Set(refs.map((ref) => (0, resolve_js_1.resolveRef)("request", ref, tree.requests, { aliases: (request) => [`${request.method} ${request.url}`] }).item.id))];
|
|
96
|
+
};
|
|
97
|
+
const reviewStateView = (collection) => collection.kind === "pipeline" && collection.review ? { review: collection.review } : {};
|
|
98
|
+
// What a collection is, for every read: kind always (older servers: standard),
|
|
99
|
+
// group / draft pass criteria / review state for pipeline collections.
|
|
100
|
+
const pipelineView = (collection) => ({
|
|
101
|
+
kind: collection.kind ?? "standard",
|
|
102
|
+
...(collection.kind === "pipeline"
|
|
103
|
+
? {
|
|
104
|
+
groupId: collection.groupId ?? null,
|
|
105
|
+
pipelineConfig: collection.pipelineConfig ?? null,
|
|
106
|
+
...reviewStateView(collection),
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
});
|
|
110
|
+
const reviewSummaryView = (summary) => ({
|
|
111
|
+
status: summary.status,
|
|
112
|
+
activeVersionNumber: summary.activeVersionNumber ?? null,
|
|
113
|
+
approvedAt: summary.approvedAt ?? null,
|
|
114
|
+
approvedByLabel: summary.approvedByLabel ?? null,
|
|
115
|
+
hasUnapprovedChanges: summary.hasUnapprovedChanges,
|
|
116
|
+
group: summary.groupId ? { id: summary.groupId, name: summary.groupName ?? null } : null,
|
|
117
|
+
});
|
|
118
|
+
const reviewRequestView = (request) => ({
|
|
119
|
+
id: request.id,
|
|
120
|
+
action: request.action,
|
|
121
|
+
status: request.status,
|
|
122
|
+
baseVersionNumber: request.baseVersionNumber ?? null,
|
|
123
|
+
note: request.note ?? null,
|
|
124
|
+
assigneeIds: request.assigneeIds ?? [],
|
|
125
|
+
submittedAt: request.submittedAt,
|
|
126
|
+
});
|
|
127
|
+
const reviewNoteArg = zod_1.z.string().max(2000).optional().describe("Note for the reviewers (what changed, why)");
|
|
128
|
+
const approversArg = zod_1.z.array(zod_1.z.string().max(255)).max(100).optional().describe("Approver emails; omit to notify every eligible approver of the group");
|
|
129
|
+
// Approver emails → user ids among the server's candidates for the action.
|
|
130
|
+
const approverIdsFor = async (ctx, ws, collectionId, action, emails) => {
|
|
131
|
+
if (!emails || emails.length === 0)
|
|
132
|
+
return undefined;
|
|
133
|
+
const candidates = await ctx.api.listCollectionApproverCandidates(ws, collectionId, action);
|
|
134
|
+
return emails.map((email) => {
|
|
135
|
+
const match = candidates.find((candidate) => candidate.email.toLowerCase() === email.trim().toLowerCase());
|
|
136
|
+
if (!match)
|
|
137
|
+
throw new errors_js_1.ToolInputError("APPROVER_NOT_ELIGIBLE", `"${email}" cannot approve this collection.`, { candidates: candidates.map((candidate) => candidate.email) });
|
|
138
|
+
return match.userId;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
46
141
|
// ── Collection / folder detail: docs + scripts ─────────────────────────────
|
|
47
142
|
const DOCS_MAX_CHARS = 500_000;
|
|
48
143
|
const SCRIPT_MAX_CHARS = 100_000;
|
|
@@ -93,7 +188,7 @@ const detailView = (item) => ({
|
|
|
93
188
|
const registerCollectionTools = (server, ctx) => {
|
|
94
189
|
server.registerTool("get_collection", {
|
|
95
190
|
title: "Get a collection's documentation, scripts and variables",
|
|
96
|
-
description: "A collection's details: its documentation (Markdown), its collection-level pre-request / post-response / tests scripts (they run around every request in it) and its collection variables ({ key, value, enabled }). Variable values are masked (••••ab) unless reveal: true AND the token holds environments:reveal. Use get_collection_tree for its folders and requests.",
|
|
191
|
+
description: "A collection's details: its documentation (Markdown), its collection-level pre-request / post-response / tests scripts (they run around every request in it) and its collection variables ({ key, value, enabled }). Variable values are masked (••••ab) unless reveal: true AND the token holds environments:reveal. Also its `kind`; an on-prem pipeline collection adds its group, its DRAFT pipelineConfig (pass criteria) and `review` (status, the active approved version, pending submission). Use get_collection_tree for its folders and requests.",
|
|
97
192
|
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), reveal: zod_1.z.boolean().optional().describe("Return variable values unmasked (needs environments:reveal)") },
|
|
98
193
|
annotations: shared_js_1.READ,
|
|
99
194
|
}, (0, shared_js_1.guarded)(async ({ workspace, collection, reveal }) => {
|
|
@@ -107,8 +202,10 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
107
202
|
requestCount: detail.requestCount ?? null,
|
|
108
203
|
folderCount: detail.folderCount ?? null,
|
|
109
204
|
runnerSettings: detail.runnerSettings ?? null,
|
|
205
|
+
...pipelineView(detail),
|
|
110
206
|
},
|
|
111
207
|
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
208
|
+
...(detail.kind === "pipeline" ? { pipelineNote: PIPELINE_DRAFT_NOTE } : {}),
|
|
112
209
|
...detailView(detail),
|
|
113
210
|
variables: variablesView(detail.variables),
|
|
114
211
|
...(detail.variables === undefined ? { variablesNote: UNSUPPORTED_VARIABLES_NOTE } : {}),
|
|
@@ -137,7 +234,7 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
137
234
|
}));
|
|
138
235
|
server.registerTool("list_collections", {
|
|
139
236
|
title: "List collections",
|
|
140
|
-
description: "List collections with request/folder counts — across ALL your workspaces unless `workspace` narrows it (the default workspace comes first; every row says its workspace). Optional `query` filters by name (substring). Use get_collection_tree to see what is inside one.",
|
|
237
|
+
description: "List collections with request/folder counts — across ALL your workspaces unless `workspace` narrows it (the default workspace comes first; every row says its workspace). Optional `query` filters by name (substring). Each row says its `kind`; on-prem pipeline collections add their review state. Use get_collection_tree to see what is inside one.",
|
|
141
238
|
inputSchema: { workspace: shared_js_1.workspaceArg, query: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
|
|
142
239
|
annotations: shared_js_1.READ,
|
|
143
240
|
}, (0, shared_js_1.guarded)(async ({ workspace, query, cursor, limit }) => {
|
|
@@ -156,6 +253,17 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
156
253
|
requestCount: collection.requestCount ?? null,
|
|
157
254
|
folderCount: collection.folderCount ?? null,
|
|
158
255
|
updatedAt: collection.updatedAt ?? null,
|
|
256
|
+
kind: collection.kind ?? "standard",
|
|
257
|
+
...(collection.kind === "pipeline" && collection.review
|
|
258
|
+
? {
|
|
259
|
+
review: {
|
|
260
|
+
status: collection.review.status,
|
|
261
|
+
activeVersionNumber: collection.review.activeVersionNumber ?? null,
|
|
262
|
+
pendingAction: collection.review.pendingAction ?? null,
|
|
263
|
+
hasUnapprovedChanges: collection.review.hasUnapprovedChanges,
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
: {}),
|
|
159
267
|
})),
|
|
160
268
|
total: page.total,
|
|
161
269
|
nextCursor: page.nextCursor,
|
|
@@ -195,7 +303,7 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
195
303
|
}));
|
|
196
304
|
server.registerTool("create_collection", {
|
|
197
305
|
title: "Create a collection",
|
|
198
|
-
description: "Create an empty collection in `workspace` (or the session default — use_workspace; with one workspace, that one), optionally with its collection variables, docs and scripts. Refuses a name that already exists in that workspace unless allowDuplicateName is true — list_collections first and reuse an existing one when it fits.",
|
|
306
|
+
description: "Create an empty collection in `workspace` (or the session default — use_workspace; with one workspace, that one), optionally with its collection variables, docs and scripts. On-prem, kind: \"pipeline\" (+ group, pipelineConfig) creates a pipeline collection: it starts as a draft, and CI runs it only after submit_collection and a person's approval. Refuses a name that already exists in that workspace unless allowDuplicateName is true — list_collections first and reuse an existing one when it fits.",
|
|
199
307
|
inputSchema: {
|
|
200
308
|
workspace: shared_js_1.workspaceArg,
|
|
201
309
|
name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME),
|
|
@@ -204,9 +312,18 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
204
312
|
variables: collectionVariablesArg,
|
|
205
313
|
docs: docsArg,
|
|
206
314
|
scripts: scriptsArg,
|
|
315
|
+
kind: kindArg,
|
|
316
|
+
group: groupArg,
|
|
317
|
+
pipelineConfig: pipeline_js_1.pipelineConfigArg,
|
|
207
318
|
},
|
|
208
319
|
annotations: shared_js_1.WRITE,
|
|
209
|
-
}, (0, shared_js_1.guarded)(async ({ workspace, name, allowDuplicateName, runnerSettings, variables, docs, scripts }) => {
|
|
320
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, name, allowDuplicateName, runnerSettings, variables, docs, scripts, kind, group, pipelineConfig }) => {
|
|
321
|
+
if (kind !== "pipeline" && (group !== undefined || pipelineConfig !== undefined)) {
|
|
322
|
+
throw new errors_js_1.ToolInputError("KIND_REQUIRED", 'group and pipelineConfig belong to pipeline collections — pass kind: "pipeline" as well.');
|
|
323
|
+
}
|
|
324
|
+
if ((pipelineConfig?.passCriteria?.requiredRequests?.length ?? 0) > 0) {
|
|
325
|
+
throw new errors_js_1.ToolInputError("CRITICAL_REQUESTS_LATER", "A new collection has no requests yet. Create it, add the requests, then set passCriteria.requiredRequests with update_collection.");
|
|
326
|
+
}
|
|
210
327
|
const writes = variables ? variableWrites(variables) : undefined;
|
|
211
328
|
const ws = await ctx.resolveWorkspace(workspace);
|
|
212
329
|
const existing = (await (0, lookups_js_1.loadCollections)(ctx, ws.id)).find((collection) => (0, lookups_js_1.sameName)(collection.name, name));
|
|
@@ -215,24 +332,39 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
215
332
|
existing: { id: existing.id, name: existing.name },
|
|
216
333
|
});
|
|
217
334
|
}
|
|
335
|
+
const reviewGroup = kind === "pipeline" ? await resolveCollectionGroup(ctx, ws.id, group) : undefined;
|
|
218
336
|
const created = await ctx.api.createCollection(ws.id, {
|
|
219
337
|
name: name.trim(),
|
|
220
338
|
...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}),
|
|
221
339
|
...(writes && writes.length > 0 ? { variables: writes } : {}),
|
|
222
340
|
...containerWrite({ docs, scripts }),
|
|
341
|
+
...(kind === "pipeline"
|
|
342
|
+
? {
|
|
343
|
+
kind,
|
|
344
|
+
...(reviewGroup ? { groupId: reviewGroup.id } : {}),
|
|
345
|
+
...(pipelineConfig ? { pipelineConfig: await (0, pipeline_js_1.mergePipelineConfig)(null, pipelineConfig, async () => []) } : {}),
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
223
348
|
});
|
|
224
349
|
ctx.invalidate(`collections:${ws.id}`);
|
|
225
350
|
const variablesLost = writes !== undefined && writes.length > 0 && !Array.isArray(created.variables);
|
|
226
351
|
return (0, shared_js_1.ok)({
|
|
227
|
-
collection: {
|
|
352
|
+
collection: {
|
|
353
|
+
id: created.id,
|
|
354
|
+
name: created.name,
|
|
355
|
+
...(writes && writes.length > 0 && !variablesLost ? { variableKeys: writes.map((variable) => variable.key) } : {}),
|
|
356
|
+
...pipelineView(created),
|
|
357
|
+
},
|
|
228
358
|
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
229
|
-
note:
|
|
359
|
+
note: created.kind === "pipeline"
|
|
360
|
+
? `Created as a pipeline collection (draft). Add requests with create_request / create_requests_bulk / import_content, then submit_collection — CI runs it only once a person approves it.${reviewGroup?.note ? ` ${reviewGroup.note}` : ""}`
|
|
361
|
+
: "Created. Add requests with create_request / create_requests_bulk / import_content.",
|
|
230
362
|
...(variablesLost ? { warnings: [`The variables were not saved: ${UNSUPPORTED_VARIABLES_NOTE}`] } : {}),
|
|
231
363
|
});
|
|
232
364
|
}));
|
|
233
365
|
server.registerTool("update_collection", {
|
|
234
366
|
title: "Update a collection (name, runner settings, variables, docs, scripts)",
|
|
235
|
-
description: "Rename a collection, toggle its runner settings (whether collection runs execute the requests' database steps), replace its collection variables (the whole list — read it with get_collection, change it, send it back), replace its documentation (Markdown — pass expectedDocsVersion from get_collection) and/or set its collection-level scripts.",
|
|
367
|
+
description: "Rename a collection, toggle its runner settings (whether collection runs execute the requests' database steps), replace its collection variables (the whole list — read it with get_collection, change it, send it back), replace its documentation (Markdown — pass expectedDocsVersion from get_collection) and/or set its collection-level scripts. On-prem: kind: \"pipeline\" (+ group) turns a standard collection into a pipeline collection, and pipelineConfig changes its pass criteria (omitted keys keep their value). A pipeline collection's edits change its draft; CI keeps running the approved version until submit_collection + approval.",
|
|
236
368
|
inputSchema: {
|
|
237
369
|
workspace: shared_js_1.workspaceArg,
|
|
238
370
|
collection: (0, shared_js_1.refArg)("Collection"),
|
|
@@ -242,26 +374,60 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
242
374
|
docs: docsArg,
|
|
243
375
|
expectedDocsVersion: expectedDocsVersionArg,
|
|
244
376
|
scripts: scriptsArg,
|
|
377
|
+
kind: kindArg,
|
|
378
|
+
group: groupArg,
|
|
379
|
+
pipelineConfig: pipeline_js_1.pipelineConfigArg,
|
|
245
380
|
},
|
|
246
381
|
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
247
|
-
}, (0, shared_js_1.guarded)(async ({ workspace, collection, name, runnerSettings, variables, docs, expectedDocsVersion, scripts }) => {
|
|
248
|
-
if (name === undefined &&
|
|
249
|
-
|
|
382
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, name, runnerSettings, variables, docs, expectedDocsVersion, scripts, kind, group, pipelineConfig }) => {
|
|
383
|
+
if (name === undefined &&
|
|
384
|
+
runnerSettings === undefined &&
|
|
385
|
+
variables === undefined &&
|
|
386
|
+
docs === undefined &&
|
|
387
|
+
scripts === undefined &&
|
|
388
|
+
kind === undefined &&
|
|
389
|
+
group === undefined &&
|
|
390
|
+
pipelineConfig === undefined) {
|
|
391
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name, runnerSettings, variables, docs, scripts, kind, group and/or pipelineConfig.");
|
|
250
392
|
}
|
|
251
393
|
requireDocsVersion(docs, expectedDocsVersion);
|
|
252
394
|
const writes = variables ? variableWrites(variables) : undefined;
|
|
253
395
|
const { ws, value: resolved, note } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
|
|
254
|
-
|
|
396
|
+
// Kind / group / pass criteria: the stored config is replaced whole, so
|
|
397
|
+
// the input is laid over the current draft.
|
|
398
|
+
let pipelineWrite = {};
|
|
399
|
+
let groupNote;
|
|
400
|
+
if (kind !== undefined || group !== undefined || pipelineConfig !== undefined) {
|
|
401
|
+
const current = await ctx.api.getCollection(ws.id, resolved.item.id);
|
|
402
|
+
const converting = kind === "pipeline" && current.kind !== "pipeline";
|
|
403
|
+
if (!converting && current.kind !== "pipeline" && (group !== undefined || pipelineConfig !== undefined)) {
|
|
404
|
+
throw new errors_js_1.ToolInputError("NOT_A_PIPELINE_COLLECTION", 'This is a standard collection — pass kind: "pipeline" to turn it into a pipeline collection (with group and pipelineConfig).');
|
|
405
|
+
}
|
|
406
|
+
const reviewGroup = converting || group !== undefined ? await resolveCollectionGroup(ctx, ws.id, group) : undefined;
|
|
407
|
+
groupNote = reviewGroup?.note;
|
|
408
|
+
pipelineWrite = {
|
|
409
|
+
...(converting ? { kind } : {}),
|
|
410
|
+
...(reviewGroup && (converting || group !== undefined) ? { groupId: reviewGroup.id } : {}),
|
|
411
|
+
...(pipelineConfig
|
|
412
|
+
? { pipelineConfig: await (0, pipeline_js_1.mergePipelineConfig)(current.pipelineConfig ?? null, pipelineConfig, (refs) => criticalRequestIds(ctx, ws.id, resolved.item.id, refs)) }
|
|
413
|
+
: {}),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const body = {
|
|
255
417
|
...(name !== undefined ? { name: name.trim() } : {}),
|
|
256
418
|
...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}),
|
|
257
419
|
...(writes !== undefined ? { variables: writes } : {}),
|
|
258
420
|
...containerWrite({ docs, expectedDocsVersion, scripts }),
|
|
259
|
-
|
|
421
|
+
...pipelineWrite,
|
|
422
|
+
};
|
|
423
|
+
if (Object.keys(body).length === 0)
|
|
424
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Nothing changes: it is already a pipeline collection.");
|
|
425
|
+
const updated = await ctx.api.updateCollection(ws.id, resolved.item.id, body);
|
|
260
426
|
ctx.invalidate(`collections:${ws.id}`);
|
|
261
427
|
// An older server drops the unknown field without an error: say so
|
|
262
428
|
// instead of reporting the variables as saved.
|
|
263
429
|
const variablesLost = writes !== undefined && !Array.isArray(updated.variables);
|
|
264
|
-
if (variablesLost && name === undefined && runnerSettings === undefined && docs === undefined && scripts === undefined) {
|
|
430
|
+
if (variablesLost && name === undefined && runnerSettings === undefined && docs === undefined && scripts === undefined && Object.keys(pipelineWrite).length === 0) {
|
|
265
431
|
throw new errors_js_1.ToolInputError("COLLECTION_VARIABLES_UNSUPPORTED", UNSUPPORTED_VARIABLES_NOTE);
|
|
266
432
|
}
|
|
267
433
|
return (0, shared_js_1.ok)({
|
|
@@ -277,8 +443,10 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
277
443
|
: {}),
|
|
278
444
|
...(docs !== undefined ? { docsVersion: updated.docsVersion ?? null } : {}),
|
|
279
445
|
...(scripts !== undefined ? { hasScripts: updated.hasScripts ?? null } : {}),
|
|
446
|
+
...pipelineView(updated),
|
|
280
447
|
},
|
|
281
448
|
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
449
|
+
...(updated.kind === "pipeline" ? { pipelineNote: `${PIPELINE_DRAFT_NOTE}${groupNote ? ` ${groupNote}` : ""}` } : {}),
|
|
282
450
|
resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(note ? [{ input: collection, note }] : [])],
|
|
283
451
|
...(variablesLost ? { warnings: [`The variables were not saved: ${UNSUPPORTED_VARIABLES_NOTE}`] } : {}),
|
|
284
452
|
});
|
|
@@ -361,17 +529,81 @@ const registerCollectionTools = (server, ctx) => {
|
|
|
361
529
|
}));
|
|
362
530
|
server.registerTool("delete_collection", {
|
|
363
531
|
title: "Delete a collection",
|
|
364
|
-
description: "Delete a collection with all its folders and requests. Permanent — confirm with the user first.",
|
|
532
|
+
description: "Delete a collection with all its folders and requests. Permanent — confirm with the user first. An on-prem pipeline collection cannot be deleted directly: use request_collection_deletion (an approver decides).",
|
|
365
533
|
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), confirm: shared_js_1.confirmArg },
|
|
366
534
|
annotations: shared_js_1.DESTRUCTIVE,
|
|
367
535
|
}, (0, shared_js_1.guarded)(async ({ workspace, collection, confirm }) => {
|
|
368
536
|
(0, shared_js_1.requireConfirm)(confirm, "a collection");
|
|
369
537
|
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection, { exact: true });
|
|
370
|
-
|
|
538
|
+
try {
|
|
539
|
+
await ctx.api.deleteCollection(ws.id, resolved.item.id);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error instanceof errors_js_1.CallmanApiError && error.code === "COLLECTION_DELETE_REQUIRES_REVIEW") {
|
|
543
|
+
throw new errors_js_1.ToolInputError("COLLECTION_DELETE_REQUIRES_REVIEW", `"${resolved.item.name}" is a pipeline collection: it is deleted through review. Call request_collection_deletion { collection: "${resolved.item.id}", confirm: true } (optionally with a note and approvers); it is deleted once an approver approves the request in the Callman app.`, { collection: { id: resolved.item.id, name: resolved.item.name }, next: "request_collection_deletion" });
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
371
547
|
ctx.invalidate(`collections:${ws.id}`);
|
|
372
548
|
ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
|
|
373
549
|
return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
|
|
374
550
|
}));
|
|
551
|
+
server.registerTool("submit_collection", {
|
|
552
|
+
title: "Submit a pipeline collection for review",
|
|
553
|
+
description: "On-prem pipeline collections: send the current draft (requests, folders, scripts, pass criteria) for review. CI runs only the approved version, so a new or edited pipeline collection must be approved before callme run / run_collection use the change. Optionally name approvers by email (default: every eligible approver of the collection's group is notified); replace: true swaps your own open submission for the current draft. Approval happens in the Callman app — AI tools cannot approve.",
|
|
554
|
+
inputSchema: {
|
|
555
|
+
workspace: shared_js_1.workspaceArg,
|
|
556
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
557
|
+
note: reviewNoteArg,
|
|
558
|
+
approvers: approversArg,
|
|
559
|
+
replace: zod_1.z.boolean().optional().describe("Replace your own pending submission with the current draft"),
|
|
560
|
+
},
|
|
561
|
+
annotations: shared_js_1.WRITE,
|
|
562
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, note, approvers, replace }) => {
|
|
563
|
+
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
|
|
564
|
+
const assigneeIds = await approverIdsFor(ctx, ws.id, resolved.item.id, "update", approvers);
|
|
565
|
+
const { summary, request } = await ctx.api.submitCollectionReview(ws.id, resolved.item.id, {
|
|
566
|
+
...(note !== undefined ? { note } : {}),
|
|
567
|
+
...(assigneeIds ? { assigneeIds } : {}),
|
|
568
|
+
...(replace ? { replace: true } : {}),
|
|
569
|
+
});
|
|
570
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
571
|
+
return (0, shared_js_1.ok)({
|
|
572
|
+
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
573
|
+
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
574
|
+
review: reviewSummaryView(summary),
|
|
575
|
+
request: reviewRequestView(request),
|
|
576
|
+
note: "Submitted — an approver approves it in the Callman app. Until then CI keeps running the previous approved version (none yet: CI refuses the collection).",
|
|
577
|
+
});
|
|
578
|
+
}));
|
|
579
|
+
server.registerTool("request_collection_deletion", {
|
|
580
|
+
title: "Request deletion of a pipeline collection",
|
|
581
|
+
description: "On-prem pipeline collections are not deleted directly (delete_collection refuses them): this sends a deletion request to the group's approvers, and the collection is deleted when one approves it in the Callman app. Confirm with the user first. Optionally a note and approver emails.",
|
|
582
|
+
inputSchema: {
|
|
583
|
+
workspace: shared_js_1.workspaceArg,
|
|
584
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
585
|
+
note: reviewNoteArg,
|
|
586
|
+
approvers: approversArg,
|
|
587
|
+
confirm: shared_js_1.confirmArg,
|
|
588
|
+
},
|
|
589
|
+
annotations: { ...shared_js_1.WRITE, destructiveHint: true },
|
|
590
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, note, approvers, confirm }) => {
|
|
591
|
+
(0, shared_js_1.requireConfirm)(confirm, "a pipeline collection (once approved)");
|
|
592
|
+
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection, { exact: true });
|
|
593
|
+
const assigneeIds = await approverIdsFor(ctx, ws.id, resolved.item.id, "delete", approvers);
|
|
594
|
+
const { summary, request } = await ctx.api.requestCollectionDeletion(ws.id, resolved.item.id, {
|
|
595
|
+
...(note !== undefined ? { note } : {}),
|
|
596
|
+
...(assigneeIds ? { assigneeIds } : {}),
|
|
597
|
+
});
|
|
598
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
599
|
+
return (0, shared_js_1.ok)({
|
|
600
|
+
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
601
|
+
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
602
|
+
review: reviewSummaryView(summary),
|
|
603
|
+
request: reviewRequestView(request),
|
|
604
|
+
note: "Deletion requested — the collection is deleted when an approver approves the request in the Callman app. CI keeps running it until then.",
|
|
605
|
+
});
|
|
606
|
+
}));
|
|
375
607
|
server.registerTool("delete_folder", {
|
|
376
608
|
title: "Delete a folder",
|
|
377
609
|
description: "Delete a folder and everything inside it (subfolders and requests). Permanent — confirm with the user first.",
|
package/dist-cjs/tools/run.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.registerRunTools = exports.prepareRun = void 0;
|
|
|
37
37
|
const zod_1 = require("zod");
|
|
38
38
|
const callman_core_1 = require("callman-core");
|
|
39
39
|
const errors_js_1 = require("../client/errors.js");
|
|
40
|
+
const pipeline_js_1 = require("../domain/pipeline.js");
|
|
40
41
|
const resolve_js_1 = require("../domain/resolve.js");
|
|
41
42
|
const redact_js_1 = require("../lib/redact.js");
|
|
42
43
|
const truncate_js_1 = require("../lib/truncate.js");
|
|
@@ -103,6 +104,7 @@ const prepareRun = async (ctx, ws, collectionId, environmentRef) => {
|
|
|
103
104
|
environment: bundle.environment ? { id: bundle.environment.id, name: bundle.environment.name } : null,
|
|
104
105
|
warnings,
|
|
105
106
|
redaction,
|
|
107
|
+
pipeline: bundle.pipeline ?? null,
|
|
106
108
|
};
|
|
107
109
|
};
|
|
108
110
|
exports.prepareRun = prepareRun;
|
|
@@ -184,7 +186,7 @@ const registerRunTools = (server, ctx) => {
|
|
|
184
186
|
}));
|
|
185
187
|
server.registerTool("run_collection", {
|
|
186
188
|
title: "Run a collection locally",
|
|
187
|
-
description: `Run a collection (or one folder of it, or specific requests) sequentially from this machine with an environment and the collection's variables, sharing variables between requests like the collection runner. Requests switched off for the runner are skipped unless named in \`only\`; data-driven contracts run once per row. Returns per-request results (as in run_request, values shown as {{key}}, with \`failure\` / \`diagnosis\` where something broke), a pass/fail summary
|
|
189
|
+
description: `Run a collection (or one folder of it, or specific requests) sequentially from this machine with an environment and the collection's variables, sharing variables between requests like the collection runner. Requests switched off for the runner are skipped unless named in \`only\`; data-driven contracts run once per row. Returns per-request results (as in run_request, values shown as {{key}}, with \`failure\` / \`diagnosis\` where something broke), a pass/fail summary, the variable names scripts changed and the \`verdict\` — the pass criteria callme run applies in CI: every executed request must pass, or \`passThreshold\` percent of them, and every \`criticalRequests\` entry. An on-prem pipeline collection runs its APPROVED version (not the draft) with its approved criteria (the inputs are then ignored, \`version\` says which version ran) and may refuse folder / partial runs. \`bail\` stops at the first failure (not with passThreshold). ${LOCAL_RUN_LIMITS} Needs requests:run.`,
|
|
188
190
|
inputSchema: {
|
|
189
191
|
workspace: shared_js_1.workspaceArg,
|
|
190
192
|
collection: (0, shared_js_1.refArg)("Collection"),
|
|
@@ -192,12 +194,25 @@ const registerRunTools = (server, ctx) => {
|
|
|
192
194
|
only: zod_1.z.array(zod_1.z.string().min(1).max(500)).max(200).optional().describe("Run only these requests (ids or names)"),
|
|
193
195
|
environment: zod_1.z.string().max(200).optional(),
|
|
194
196
|
bail: zod_1.z.boolean().optional(),
|
|
197
|
+
passThreshold: zod_1.z.number().int().min(1).max(100).optional().describe("The run passes when at least this percent of the executed requests passed (default: all must). Ignored for pipeline collections"),
|
|
198
|
+
criticalRequests: zod_1.z.array(zod_1.z.string().min(1).max(500)).max(200).optional().describe("Requests (ids or names) that must pass whatever the percentage. Ignored for pipeline collections"),
|
|
195
199
|
maxBodyBytes: maxBodyArg,
|
|
196
200
|
},
|
|
197
201
|
annotations: shared_js_1.RUN,
|
|
198
|
-
}, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, only, environment, bail, maxBodyBytes }) => {
|
|
202
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, only, environment, bail, passThreshold, criticalRequests, maxBodyBytes }) => {
|
|
199
203
|
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
|
|
200
204
|
const prepared = await (0, exports.prepareRun)(ctx, ws.id, resolved.item.id, environment);
|
|
205
|
+
// What decides the verdict: a pipeline collection's approved config,
|
|
206
|
+
// else the inputs. Critical requests are looked up in what runs.
|
|
207
|
+
const isPipeline = prepared.pipeline?.kind === "pipeline";
|
|
208
|
+
const criticalRequestIds = !isPipeline && criticalRequests && criticalRequests.length > 0
|
|
209
|
+
? [...new Set(criticalRequests.map((ref) => (0, resolve_js_1.resolveRef)("request", ref, prepared.collection.requests, { aliases: (request) => [`${request.method} ${request.url}`] }).item.id))]
|
|
210
|
+
: [];
|
|
211
|
+
const runCriteria = (0, pipeline_js_1.resolveRunCriteria)(prepared.pipeline, {
|
|
212
|
+
passThreshold,
|
|
213
|
+
criticalRequestIds: isPipeline ? (criticalRequests ?? []) : criticalRequestIds,
|
|
214
|
+
bail,
|
|
215
|
+
});
|
|
201
216
|
let selection = null;
|
|
202
217
|
if (folder) {
|
|
203
218
|
const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
|
|
@@ -213,6 +228,9 @@ const registerRunTools = (server, ctx) => {
|
|
|
213
228
|
}
|
|
214
229
|
if (selection && selection.length === 0)
|
|
215
230
|
throw new errors_js_1.ToolInputError("NOTHING_TO_RUN", "The selection contains no requests.");
|
|
231
|
+
if (selection && !runCriteria.allowFolderRuns) {
|
|
232
|
+
throw new errors_js_1.ToolInputError("PIPELINE_FOLDER_RUN_NOT_ALLOWED", `This pipeline collection's approved config${runCriteria.version ? ` v${runCriteria.version.number}` : ""} runs the whole collection only: drop folder / only (run_request runs a single request), or allow folder runs in its pipelineConfig and get the change approved.`);
|
|
233
|
+
}
|
|
216
234
|
const outcome = await ctx.withRunLock(() => (0, executeCollection_js_1.executeCollection)({
|
|
217
235
|
collection: prepared.collection,
|
|
218
236
|
env: prepared.env,
|
|
@@ -220,13 +238,24 @@ const registerRunTools = (server, ctx) => {
|
|
|
220
238
|
connections: prepared.connections,
|
|
221
239
|
only: selection,
|
|
222
240
|
includeDisabled: Boolean(only && only.length > 0),
|
|
223
|
-
bail: bail
|
|
241
|
+
bail: runCriteria.bail,
|
|
224
242
|
deadlineMs: ctx.config.runTimeoutMs,
|
|
225
243
|
}));
|
|
244
|
+
const verdict = (0, pipeline_js_1.evaluateRunVerdict)({
|
|
245
|
+
results: outcome.results,
|
|
246
|
+
criteria: runCriteria.criteria,
|
|
247
|
+
collectionName: prepared.collection.name,
|
|
248
|
+
version: runCriteria.version?.number ?? null,
|
|
249
|
+
requests: prepared.collection.requests,
|
|
250
|
+
selection,
|
|
251
|
+
});
|
|
226
252
|
return (0, shared_js_1.ok)((0, summarize_js_1.summarizeRun)(outcome, maxBodyBytes ?? ctx.config.maxBodyBytes, {
|
|
227
|
-
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
253
|
+
collection: { id: resolved.item.id, name: resolved.item.name, kind: runCriteria.kind },
|
|
254
|
+
...(runCriteria.version ? { version: runCriteria.version } : {}),
|
|
228
255
|
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
229
256
|
environment: prepared.environment,
|
|
257
|
+
verdict,
|
|
258
|
+
...(runCriteria.notes.length > 0 ? { notes: runCriteria.notes } : {}),
|
|
230
259
|
warnings: prepared.warnings,
|
|
231
260
|
resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)],
|
|
232
261
|
}, prepared.redaction));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callman-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "MCP server for Callman: lets Claude Code, Claude Desktop, Cursor, Codex and Gemini CLI manage collections, requests, environments, test scenarios, UI tests (web, Android, iOS) 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.
|
|
65
|
+
"callman-core": "^1.43.0",
|
|
66
66
|
"elkjs": "^0.12.0",
|
|
67
67
|
"js-yaml": "^4.1.0",
|
|
68
68
|
"zod": "^3.25.0"
|