callman-mcp 0.6.0 → 0.7.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 +36 -59
- package/README.md +18 -24
- package/dist/client/api.d.ts +18 -40
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +7 -54
- package/dist/client/api.js.map +1 -1
- package/dist/client/types.d.ts +19 -44
- package/dist/client/types.d.ts.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/domain/diagram/build.d.ts +17 -17
- package/dist/domain/diagram/build.d.ts.map +1 -1
- package/dist/domain/diagram/build.js +3 -3
- package/dist/domain/diagram/build.js.map +1 -1
- package/dist/domain/diagram/layout.d.ts +1 -3
- package/dist/domain/diagram/layout.d.ts.map +1 -1
- package/dist/domain/diagram/layout.js +3 -9
- package/dist/domain/diagram/layout.js.map +1 -1
- package/dist/domain/diagram/patch.d.ts +22 -723
- package/dist/domain/diagram/patch.d.ts.map +1 -1
- package/dist/domain/diagram/patch.js +0 -53
- package/dist/domain/diagram/patch.js.map +1 -1
- package/dist/domain/diagram/summarize.d.ts +0 -1
- package/dist/domain/diagram/summarize.d.ts.map +1 -1
- package/dist/domain/diagram/summarize.js +0 -1
- package/dist/domain/diagram/summarize.js.map +1 -1
- package/dist/domain/requestIntegrations.d.ts +13 -468
- package/dist/domain/requestIntegrations.d.ts.map +1 -1
- package/dist/domain/requestIntegrations.js +4 -95
- package/dist/domain/requestIntegrations.js.map +1 -1
- package/dist/domain/requestShape.d.ts +37 -679
- package/dist/domain/requestShape.d.ts.map +1 -1
- package/dist/domain/requestShape.js +2 -2
- package/dist/domain/requestShape.js.map +1 -1
- package/dist/domain/scenario/build.d.ts +6 -6
- package/dist/domain/scenario/build.d.ts.map +1 -1
- package/dist/domain/scenario/build.js +17 -68
- package/dist/domain/scenario/build.js.map +1 -1
- package/dist/domain/scenario/nodeConfig.d.ts.map +1 -1
- package/dist/domain/scenario/nodeConfig.js +16 -0
- package/dist/domain/scenario/nodeConfig.js.map +1 -1
- package/dist/domain/scenario/summarize.d.ts +0 -1
- package/dist/domain/scenario/summarize.d.ts.map +1 -1
- package/dist/domain/scenario/summarize.js +6 -1
- package/dist/domain/scenario/summarize.js.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/docs/collectionScripts.d.ts +2 -0
- package/dist/resources/docs/collectionScripts.d.ts.map +1 -0
- package/dist/resources/docs/collectionScripts.js +51 -0
- package/dist/resources/docs/collectionScripts.js.map +1 -0
- package/dist/resources/docs/diagramSchema.d.ts +1 -2
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +4 -63
- 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 +13 -34
- 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 +11 -16
- package/dist/resources/docs/requestSchema.js.map +1 -1
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.js +6 -6
- package/dist/resources/generated/aiPacks.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +5 -3
- package/dist/resources/index.js.map +1 -1
- package/dist/run/executeCollection.d.ts.map +1 -1
- package/dist/run/executeCollection.js +19 -3
- package/dist/run/executeCollection.js.map +1 -1
- package/dist/run/loadBundle.d.ts.map +1 -1
- package/dist/run/loadBundle.js +28 -3
- package/dist/run/loadBundle.js.map +1 -1
- package/dist/run/summarize.d.ts +4 -4
- package/dist/run/types.d.ts +5 -1
- 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 +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +3 -11
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +137 -16
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/contracts.js +2 -2
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +42 -331
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/loadTest.d.ts +4 -0
- package/dist/tools/loadTest.d.ts.map +1 -0
- package/dist/tools/loadTest.js +299 -0
- package/dist/tools/loadTest.js.map +1 -0
- package/dist/tools/lookups.d.ts +1 -5
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +0 -41
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/requests.js +3 -3
- package/dist/tools/requests.js.map +1 -1
- package/dist/tools/run.d.ts +18 -0
- package/dist/tools/run.d.ts.map +1 -1
- package/dist/tools/run.js +14 -5
- package/dist/tools/run.js.map +1 -1
- package/dist/tools/scenarios.d.ts +0 -4
- package/dist/tools/scenarios.d.ts.map +1 -1
- package/dist/tools/scenarios.js +17 -112
- package/dist/tools/scenarios.js.map +1 -1
- package/dist-cjs/client/api.js +7 -54
- package/dist-cjs/config.js +5 -0
- package/dist-cjs/domain/diagram/build.js +3 -3
- package/dist-cjs/domain/diagram/layout.js +3 -10
- package/dist-cjs/domain/diagram/patch.js +1 -54
- package/dist-cjs/domain/diagram/summarize.js +0 -1
- package/dist-cjs/domain/requestIntegrations.js +4 -95
- package/dist-cjs/domain/requestShape.js +2 -2
- package/dist-cjs/domain/scenario/build.js +17 -68
- package/dist-cjs/domain/scenario/nodeConfig.js +16 -0
- package/dist-cjs/domain/scenario/summarize.js +6 -1
- package/dist-cjs/resources/docs/collectionScripts.js +53 -0
- package/dist-cjs/resources/docs/diagramSchema.js +5 -65
- package/dist-cjs/resources/docs/overview.js +13 -34
- package/dist-cjs/resources/docs/requestSchema.js +11 -16
- package/dist-cjs/resources/generated/aiPacks.js +6 -6
- package/dist-cjs/resources/index.js +4 -2
- package/dist-cjs/run/executeCollection.js +18 -2
- package/dist-cjs/run/loadBundle.js +28 -3
- package/dist-cjs/server.js +2 -2
- package/dist-cjs/tools/catalog.js +3 -11
- package/dist-cjs/tools/collections.js +137 -16
- package/dist-cjs/tools/contracts.js +2 -2
- package/dist-cjs/tools/diagrams.js +37 -326
- package/dist-cjs/tools/index.js +4 -3
- package/dist-cjs/tools/loadTest.js +335 -0
- package/dist-cjs/tools/lookups.js +1 -46
- package/dist-cjs/tools/requests.js +3 -3
- package/dist-cjs/tools/run.js +17 -7
- package/dist-cjs/tools/scenarios.js +18 -114
- package/package.json +3 -3
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerLoadTestTool = void 0;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const callman_core_1 = require("callman-core");
|
|
39
|
+
const errors_js_1 = require("../client/errors.js");
|
|
40
|
+
const redact_js_1 = require("../lib/redact.js");
|
|
41
|
+
const resolveTemplate_js_1 = require("../run/resolveTemplate.js");
|
|
42
|
+
const run_js_1 = require("./run.js");
|
|
43
|
+
const lookups_js_1 = require("./lookups.js");
|
|
44
|
+
const shared_js_1 = require("./shared.js");
|
|
45
|
+
// Load / stress testing from an AI tool. Same engine as the desktop's Load test
|
|
46
|
+
// tab and `callme perf run` (callman-core/loadtest), so the numbers agree.
|
|
47
|
+
//
|
|
48
|
+
// Generating load is the one thing here that can hurt a third party, so it is
|
|
49
|
+
// two steps: without `confirm: true` the tool only PREVIEWS (which hosts, which
|
|
50
|
+
// requests, how much traffic, hard limits); the run starts on the second call.
|
|
51
|
+
// Hard caps (users, rate, duration) come from the server config, because a
|
|
52
|
+
// blocking tool call also has to finish inside the client's timeout.
|
|
53
|
+
const PRESETS = ["smoke", "load", "stress", "spike", "breakpoint"];
|
|
54
|
+
const rampable = (executor, rampMs) => {
|
|
55
|
+
if (rampMs <= 0)
|
|
56
|
+
return executor;
|
|
57
|
+
if (executor.type === "constant-vus") {
|
|
58
|
+
const ramp = Math.min(rampMs, Math.max(1000, executor.durationMs - 1000));
|
|
59
|
+
return {
|
|
60
|
+
type: "ramping-vus",
|
|
61
|
+
startVus: 0,
|
|
62
|
+
stages: [
|
|
63
|
+
{ durationMs: ramp, target: executor.vus },
|
|
64
|
+
{ durationMs: executor.durationMs - ramp, target: executor.vus },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (executor.type === "constant-arrival-rate") {
|
|
69
|
+
const ramp = Math.min(rampMs, Math.max(1000, executor.durationMs - 1000));
|
|
70
|
+
return {
|
|
71
|
+
type: "ramping-arrival-rate",
|
|
72
|
+
startRate: 1,
|
|
73
|
+
stages: [
|
|
74
|
+
{ durationMs: ramp, target: executor.rate },
|
|
75
|
+
{ durationMs: executor.durationMs - ramp, target: executor.rate },
|
|
76
|
+
],
|
|
77
|
+
...(executor.maxVus ? { maxVus: executor.maxVus } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return executor;
|
|
81
|
+
};
|
|
82
|
+
const round = (value, digits = 2) => Math.round(value * 10 ** digits) / 10 ** digits;
|
|
83
|
+
const summarize = (result, redact) => {
|
|
84
|
+
const summary = result.summary;
|
|
85
|
+
const failedRate = summary.totalRequests === 0 ? 0 : summary.failedCount / summary.totalRequests;
|
|
86
|
+
const verdict = result.abortedByThreshold
|
|
87
|
+
? "aborted"
|
|
88
|
+
: result.thresholds.some((entry) => !entry.passed)
|
|
89
|
+
? "failed"
|
|
90
|
+
: result.stoppedByUser
|
|
91
|
+
? "stopped"
|
|
92
|
+
: "passed";
|
|
93
|
+
return {
|
|
94
|
+
verdict,
|
|
95
|
+
passed: verdict === "passed",
|
|
96
|
+
durationSeconds: round(summary.durationMs / 1000, 1),
|
|
97
|
+
requests: summary.totalRequests,
|
|
98
|
+
iterations: summary.totalIterations,
|
|
99
|
+
droppedIterations: summary.droppedIterations,
|
|
100
|
+
throughputPerSecond: { average: round(summary.avgRps, 1), peak: round(summary.peakRps, 1) },
|
|
101
|
+
failedRate: round(failedRate, 4),
|
|
102
|
+
latencyMs: {
|
|
103
|
+
average: round(result.duration.avg),
|
|
104
|
+
median: round(result.duration.p50),
|
|
105
|
+
p90: round(result.duration.p90),
|
|
106
|
+
p95: round(result.duration.p95),
|
|
107
|
+
p99: round(result.duration.p99),
|
|
108
|
+
max: round(result.duration.max),
|
|
109
|
+
},
|
|
110
|
+
...(result.correctedDuration
|
|
111
|
+
? { latencyFromIntendedStartMs: { p95: round(result.correctedDuration.p95), p99: round(result.correctedDuration.p99) } }
|
|
112
|
+
: {}),
|
|
113
|
+
peakConcurrency: summary.peakVus,
|
|
114
|
+
checks: summary.checksPassed + summary.checksFailed > 0 ? { passed: summary.checksPassed, failed: summary.checksFailed } : undefined,
|
|
115
|
+
steps: result.steps.map((step) => ({
|
|
116
|
+
name: step.name,
|
|
117
|
+
requests: step.requests,
|
|
118
|
+
failedRate: round(step.failedRate, 4),
|
|
119
|
+
p95Ms: round(step.duration.p95),
|
|
120
|
+
throughputPerSecond: round(step.rps, 1),
|
|
121
|
+
})),
|
|
122
|
+
thresholds: result.thresholds.map((entry) => ({
|
|
123
|
+
expression: entry.threshold.expression,
|
|
124
|
+
passed: entry.passed,
|
|
125
|
+
actual: entry.actual === null ? null : round(entry.actual, 3),
|
|
126
|
+
...(entry.aborted ? { stoppedTheRun: true } : {}),
|
|
127
|
+
})),
|
|
128
|
+
errors: result.errors.slice(0, 5).map((error) => ({
|
|
129
|
+
kind: error.errorClass,
|
|
130
|
+
causedBy: error.blame === "generator" ? "the load generator (this machine)" : error.blame === "server" ? "the target or the network" : error.blame === "http" ? "an HTTP error status" : "the request setup",
|
|
131
|
+
count: error.count,
|
|
132
|
+
example: redact(error.example),
|
|
133
|
+
})),
|
|
134
|
+
loadGenerator: {
|
|
135
|
+
workers: result.generator.workers,
|
|
136
|
+
saturated: result.generator.saturated,
|
|
137
|
+
maxEventLoopLagMs: result.generator.maxLoopLagMs,
|
|
138
|
+
...(result.generator.saturated
|
|
139
|
+
? { warning: "The machine generating the load was saturated, so latencies are likely overstated. Lower the load." }
|
|
140
|
+
: {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const registerLoadTestTool = (server, ctx) => {
|
|
145
|
+
const { loadTestMaxSeconds, loadTestMaxVus, loadTestMaxRate } = ctx.config;
|
|
146
|
+
server.registerTool("run_load_test", {
|
|
147
|
+
title: "Load / stress test a collection",
|
|
148
|
+
description: `Generate load against the requests of a collection from THIS machine and judge it against pass criteria. Same engine as the desktop's Load test and \`callme perf run\`. The requests of the collection (or one folder, or the ones named in \`only\`) run in order as one flow per iteration, with the environment's variables.\n` +
|
|
149
|
+
`Two steps: without \`confirm: true\` it only PREVIEWS (target hosts, requests, planned traffic, limits) — show that to the user, then call again with confirm: true. Only test systems the user owns or may load-test.\n` +
|
|
150
|
+
`Pattern: \`mode: "vus"\` (a closed crowd of virtual users looping the flow; slow servers slow them down) or \`mode: "rate"\` (an open model: iterations START at \`rate\` per second whatever the server does; iterations that cannot start are reported as dropped). \`rampUpSeconds\` ramps to the level first. Or pick a \`preset\` (smoke, load, stress, spike, breakpoint) tuned by vus / rate / durationSeconds.\n` +
|
|
151
|
+
`Pass criteria: \`thresholds\` like "p95<500", "fail<1%", "checks>99%", "rps>100" or "http_req_duration{step:Login}:p(95)<300"; \`abortOn\` takes the same but stops the run when one breaks (use it to find the breaking point).\n` +
|
|
152
|
+
`Returns the verdict, throughput, latency percentiles, failures by cause (and whether the load generator or the target is to blame), per-step numbers and threshold results. Scripts, DB and Redis steps are not run. Limits here: ${loadTestMaxVus} users, ${loadTestMaxRate} iterations/s, ${loadTestMaxSeconds} s per run. Needs requests:run.`,
|
|
153
|
+
inputSchema: {
|
|
154
|
+
workspace: shared_js_1.workspaceArg,
|
|
155
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
156
|
+
folder: zod_1.z.string().max(300).optional().describe("Only this folder (id, name or path), subfolders included"),
|
|
157
|
+
only: zod_1.z.array(zod_1.z.string().min(1).max(500)).max(50).optional().describe("Only these requests (ids or names), in this order"),
|
|
158
|
+
environment: zod_1.z.string().max(200).optional(),
|
|
159
|
+
mode: zod_1.z.enum(["vus", "rate"]).optional().describe('"vus" (default) = virtual users; "rate" = iterations per second'),
|
|
160
|
+
vus: zod_1.z.number().int().min(1).optional().describe(`Virtual users (max ${loadTestMaxVus}); the preset's intensity in vus mode`),
|
|
161
|
+
rate: zod_1.z.number().int().min(1).optional().describe(`Iterations per second (max ${loadTestMaxRate}) for mode "rate"`),
|
|
162
|
+
maxVus: zod_1.z.number().int().min(1).optional().describe("Rate mode: most iterations in flight before new ones are dropped"),
|
|
163
|
+
durationSeconds: zod_1.z.number().int().min(5).optional().describe(`How long (5–${loadTestMaxSeconds}; default 30)`),
|
|
164
|
+
rampUpSeconds: zod_1.z.number().int().min(0).optional().describe("Ramp to the level over this many seconds first"),
|
|
165
|
+
preset: zod_1.z.enum(PRESETS).optional().describe("A ready-made pattern; vus / rate / durationSeconds tune it"),
|
|
166
|
+
thresholds: zod_1.z.array(zod_1.z.string().min(1).max(200)).max(20).optional().describe('Pass criteria, e.g. ["p95<500", "fail<1%"]'),
|
|
167
|
+
abortOn: zod_1.z.array(zod_1.z.string().min(1).max(200)).max(10).optional().describe("Criteria that stop the run when violated"),
|
|
168
|
+
thinkTimeMinMs: zod_1.z.number().int().min(0).max(60_000).optional(),
|
|
169
|
+
thinkTimeMaxMs: zod_1.z.number().int().min(0).max(60_000).optional(),
|
|
170
|
+
confirm: zod_1.z.boolean().optional().describe("true = actually generate the load. Omit for the preview."),
|
|
171
|
+
},
|
|
172
|
+
annotations: shared_js_1.RUN,
|
|
173
|
+
}, (0, shared_js_1.guarded)(async (args) => {
|
|
174
|
+
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, args.workspace, args.collection);
|
|
175
|
+
const prepared = await (0, run_js_1.prepareRun)(ctx, ws.id, resolved.item.id, args.environment);
|
|
176
|
+
// ── Which requests ─────────────────────────────────────────────────
|
|
177
|
+
let folderIds = null;
|
|
178
|
+
if (args.folder) {
|
|
179
|
+
const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, args.folder);
|
|
180
|
+
if (!target)
|
|
181
|
+
throw new errors_js_1.ToolInputError("REF_REQUIRED", "folder is required.");
|
|
182
|
+
folderIds = (0, lookups_js_1.requestIdsInFolder)(await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id), target.item.id);
|
|
183
|
+
}
|
|
184
|
+
let onlyIds = null;
|
|
185
|
+
if (args.only && args.only.length > 0) {
|
|
186
|
+
onlyIds = [];
|
|
187
|
+
for (const ref of args.only)
|
|
188
|
+
onlyIds.push((await (0, lookups_js_1.resolveRequest)(ctx, ws.id, ref, resolved.item.id)).request.id);
|
|
189
|
+
}
|
|
190
|
+
const byId = new Map(prepared.collection.requests.map((request) => [request.id, request]));
|
|
191
|
+
const chosen = (onlyIds
|
|
192
|
+
? onlyIds.filter((id) => !folderIds || folderIds.includes(id)).map((id) => byId.get(id))
|
|
193
|
+
: prepared.collection.requests.filter((request) => request.isEnabledInRunner !== false && (!folderIds || folderIds.includes(request.id)))).filter((request) => Boolean(request));
|
|
194
|
+
if (chosen.length === 0)
|
|
195
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_RUN", "The selection contains no requests.");
|
|
196
|
+
// ── Pattern ────────────────────────────────────────────────────────
|
|
197
|
+
const durationSeconds = args.durationSeconds ?? 30;
|
|
198
|
+
if (durationSeconds > loadTestMaxSeconds) {
|
|
199
|
+
throw new errors_js_1.ToolInputError("LIMIT_EXCEEDED", `durationSeconds ${durationSeconds} exceeds this server's limit of ${loadTestMaxSeconds} s (CALLMAN_LOADTEST_MAX_SECONDS). Use the desktop app or \`callme perf run\` for longer tests.`);
|
|
200
|
+
}
|
|
201
|
+
const mode = args.mode ?? (args.rate && !args.vus ? "rate" : "vus");
|
|
202
|
+
const vus = args.vus ?? (args.preset ? 20 : 10);
|
|
203
|
+
const rate = args.rate ?? 50;
|
|
204
|
+
if (vus > loadTestMaxVus)
|
|
205
|
+
throw new errors_js_1.ToolInputError("LIMIT_EXCEEDED", `vus ${vus} exceeds this server's limit of ${loadTestMaxVus} (CALLMAN_LOADTEST_MAX_VUS).`);
|
|
206
|
+
if (rate > loadTestMaxRate)
|
|
207
|
+
throw new errors_js_1.ToolInputError("LIMIT_EXCEEDED", `rate ${rate} exceeds this server's limit of ${loadTestMaxRate}/s (CALLMAN_LOADTEST_MAX_RATE).`);
|
|
208
|
+
let executor;
|
|
209
|
+
if (args.preset) {
|
|
210
|
+
executor = (0, callman_core_1.buildPresetExecutor)(args.preset, { vus, rate, durationMs: durationSeconds * 1000 });
|
|
211
|
+
}
|
|
212
|
+
else if (mode === "rate") {
|
|
213
|
+
executor = rampable({ type: "constant-arrival-rate", rate, durationMs: durationSeconds * 1000, maxVus: args.maxVus ?? Math.min(Math.max(rate, 50), loadTestMaxVus * 5) }, (args.rampUpSeconds ?? 0) * 1000);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
executor = rampable({ type: "constant-vus", vus, durationMs: durationSeconds * 1000 }, (args.rampUpSeconds ?? 0) * 1000);
|
|
217
|
+
}
|
|
218
|
+
// The preset can climb past the caller's number (stress ×2, spike ×3,
|
|
219
|
+
// breakpoint ×10): hold it to the configured ceiling too.
|
|
220
|
+
const peak = executor.type === "constant-vus" ? executor.vus
|
|
221
|
+
: executor.type === "ramping-vus" ? Math.max(executor.startVus, ...executor.stages.map((stage) => stage.target))
|
|
222
|
+
: executor.type === "constant-arrival-rate" ? executor.rate
|
|
223
|
+
: Math.max(executor.startRate, ...executor.stages.map((stage) => stage.target));
|
|
224
|
+
const peakLimit = executor.type === "constant-vus" || executor.type === "ramping-vus" ? loadTestMaxVus : loadTestMaxRate;
|
|
225
|
+
if (peak > peakLimit) {
|
|
226
|
+
throw new errors_js_1.ToolInputError("LIMIT_EXCEEDED", `This pattern peaks at ${peak}, above this server's limit of ${peakLimit}. Lower ${executor.type.includes("rate") ? "rate" : "vus"}${args.preset ? ` (the ${args.preset} preset climbs above the value you give)` : ""}.`);
|
|
227
|
+
}
|
|
228
|
+
const thresholds = [
|
|
229
|
+
...(args.thresholds ?? []).map((expression, index) => ({ id: `t${index + 1}`, expression })),
|
|
230
|
+
...(args.abortOn ?? []).map((expression, index) => ({ id: `a${index + 1}`, expression, abortOnFail: true, delayAbortEvalMs: 5000 })),
|
|
231
|
+
];
|
|
232
|
+
for (const threshold of thresholds) {
|
|
233
|
+
try {
|
|
234
|
+
(0, callman_core_1.parseThreshold)(threshold.expression);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
throw new errors_js_1.ToolInputError("INVALID_THRESHOLD", error instanceof Error ? error.message : String(error));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const environment = {};
|
|
241
|
+
for (const [key, value] of Object.entries({ ...prepared.globals, ...prepared.env })) {
|
|
242
|
+
if (typeof value === "string")
|
|
243
|
+
environment[key] = value;
|
|
244
|
+
}
|
|
245
|
+
const thinkMin = args.thinkTimeMinMs ?? 0;
|
|
246
|
+
const thinkMax = Math.max(args.thinkTimeMaxMs ?? thinkMin, thinkMin);
|
|
247
|
+
const config = {
|
|
248
|
+
runId: `mcp-${Date.now().toString(36)}`,
|
|
249
|
+
name: resolved.item.name,
|
|
250
|
+
executor,
|
|
251
|
+
steps: chosen.map(callman_core_1.requestToLoadStep),
|
|
252
|
+
environment,
|
|
253
|
+
thresholds,
|
|
254
|
+
...(thinkMax > 0 ? { thinkTimeMs: { min: thinkMin, max: thinkMax } } : {}),
|
|
255
|
+
keepAlive: true,
|
|
256
|
+
verifySsl: true,
|
|
257
|
+
monitor: { enabled: false },
|
|
258
|
+
};
|
|
259
|
+
const issues = (0, callman_core_1.validateLoadTestConfig)(config);
|
|
260
|
+
if ((0, callman_core_1.hasBlockingIssues)(issues)) {
|
|
261
|
+
throw new errors_js_1.ToolInputError("INVALID_LOAD_TEST", issues.filter((issue) => issue.severity === "error").map((issue) => issue.message).join(" "));
|
|
262
|
+
}
|
|
263
|
+
const redactor = (0, redact_js_1.createRedactor)(Object.entries(environment), prepared.redaction);
|
|
264
|
+
const hosts = new Set();
|
|
265
|
+
for (const request of chosen) {
|
|
266
|
+
try {
|
|
267
|
+
hosts.add(new URL(/^[a-z]+:\/\//i.test(request.url) ? (0, resolveTemplate_js_1.resolveTemplate)(request.url, environment) : `http://${(0, resolveTemplate_js_1.resolveTemplate)(request.url, environment)}`).host);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
hosts.add(redactor.text((0, resolveTemplate_js_1.resolveTemplate)(request.url, environment)));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const estimate = (0, callman_core_1.estimateLoad)(config, 250 * chosen.length);
|
|
274
|
+
const notes = chosen
|
|
275
|
+
.map((request) => {
|
|
276
|
+
const features = [];
|
|
277
|
+
if (request.preRequestScript?.trim() || request.postResponseScript?.trim() || request.tests?.trim())
|
|
278
|
+
features.push("scripts and tests");
|
|
279
|
+
if (request.database?.preRequest?.enabled || request.database?.postResponse?.enabled)
|
|
280
|
+
features.push("database steps");
|
|
281
|
+
if (request.redis?.enabled)
|
|
282
|
+
features.push("Redis commands");
|
|
283
|
+
return features.length > 0 ? `${request.name}: ${features.join(", ")} are not run in a load test.` : null;
|
|
284
|
+
})
|
|
285
|
+
.filter((note) => note !== null);
|
|
286
|
+
const warnings = [...prepared.warnings, ...notes, ...issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)];
|
|
287
|
+
if (thresholds.length === 0)
|
|
288
|
+
warnings.push("No pass criteria were given, so the verdict can only be 'passed'. Add thresholds such as [\"p95<500\", \"fail<1%\"].");
|
|
289
|
+
const pattern = {
|
|
290
|
+
type: executor.type,
|
|
291
|
+
...(executor.type === "constant-vus" ? { vus: executor.vus } : {}),
|
|
292
|
+
...(executor.type === "constant-arrival-rate" ? { ratePerSecond: executor.rate } : {}),
|
|
293
|
+
durationSeconds,
|
|
294
|
+
...(args.rampUpSeconds ? { rampUpSeconds: args.rampUpSeconds } : {}),
|
|
295
|
+
...(args.preset ? { preset: args.preset } : {}),
|
|
296
|
+
};
|
|
297
|
+
if (args.confirm !== true) {
|
|
298
|
+
return (0, shared_js_1.ok)({
|
|
299
|
+
preview: true,
|
|
300
|
+
message: "Nothing was sent. Show this to the user and call run_load_test again with confirm: true to generate the load. Only test systems you own or may load-test.",
|
|
301
|
+
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
302
|
+
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
303
|
+
environment: prepared.environment,
|
|
304
|
+
targetHosts: [...hosts],
|
|
305
|
+
flow: chosen.map((request) => ({ name: request.name, method: request.method, url: redactor.text((0, resolveTemplate_js_1.resolveTemplate)(request.url, environment)) })),
|
|
306
|
+
pattern,
|
|
307
|
+
estimate: { requests: estimate.totalRequests, uploadMegabytes: round(estimate.estimatedUploadMb, 1), impactOnThisMachine: estimate.impact },
|
|
308
|
+
limits: { maxVus: loadTestMaxVus, maxRatePerSecond: loadTestMaxRate, maxSeconds: loadTestMaxSeconds },
|
|
309
|
+
thresholds: thresholds.map((threshold) => ({ expression: threshold.expression, ...(threshold.abortOnFail ? { stopsTheRun: true } : {}) })),
|
|
310
|
+
warnings,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const { startLoadTest } = await Promise.resolve().then(() => __importStar(require("callman-core/loadtest")));
|
|
314
|
+
const result = await ctx.withRunLock(async () => {
|
|
315
|
+
const handle = startLoadTest(config, { onEvent: () => undefined, ...(ctx.config.loadWorkerEntry ? { workerEntry: ctx.config.loadWorkerEntry } : {}) });
|
|
316
|
+
const stopTimer = setTimeout(() => handle.stop(), (durationSeconds + 20) * 1000);
|
|
317
|
+
try {
|
|
318
|
+
return await handle.done;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
clearTimeout(stopTimer);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return (0, shared_js_1.ok)({
|
|
325
|
+
...summarize(result, (text) => redactor.text(text)),
|
|
326
|
+
workspace: (0, lookups_js_1.workspaceTag)(ws),
|
|
327
|
+
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
328
|
+
environment: prepared.environment,
|
|
329
|
+
targetHosts: [...hosts],
|
|
330
|
+
pattern,
|
|
331
|
+
warnings,
|
|
332
|
+
});
|
|
333
|
+
}));
|
|
334
|
+
};
|
|
335
|
+
exports.registerLoadTestTool = registerLoadTestTool;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveUiTest = exports.resolveUiTestFolder = exports.uiTestFolderSubtree = exports.loadUiTestFolders = exports.loadUiTests = exports.resolveDiagram = exports.
|
|
3
|
+
exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveUiTest = exports.resolveUiTestFolder = exports.uiTestFolderSubtree = exports.loadUiTestFolders = exports.loadUiTests = exports.resolveDiagram = exports.loadDiagrams = exports.resolveScenarioFolder = exports.resolveScenario = exports.loadScenarioCatalog = exports.resolveEnvironment = exports.loadEnvironments = exports.resolveRequest = exports.searchRequests = exports.requestIdsInFolder = exports.resolveFolder = exports.loadTree = exports.resolveCollection = exports.findEnvironment = exports.findRequest = exports.findCollection = exports.findInWorkspaces = exports.workspaceTag = exports.workspacesForListing = exports.isAllWorkspaces = exports.loadCollections = void 0;
|
|
4
4
|
const errors_js_1 = require("../client/errors.js");
|
|
5
5
|
const requestIntegrations_js_1 = require("../domain/requestIntegrations.js");
|
|
6
6
|
const resolve_js_1 = require("../domain/resolve.js");
|
|
@@ -240,51 +240,6 @@ const resolveScenarioFolder = async (ctx, ws, ref) => (0, resolve_js_1.resolveRe
|
|
|
240
240
|
exports.resolveScenarioFolder = resolveScenarioFolder;
|
|
241
241
|
const loadDiagrams = (ctx) => ctx.cache.getOrSet("diagrams", () => ctx.api.listDiagrams());
|
|
242
242
|
exports.loadDiagrams = loadDiagrams;
|
|
243
|
-
// ── diagram folders (the token owner's private library tree) ──────────
|
|
244
|
-
const loadDiagramFolders = (ctx) => ctx.cache.getOrSet("diagram-folders", () => ctx.api.listDiagramFolders());
|
|
245
|
-
exports.loadDiagramFolders = loadDiagramFolders;
|
|
246
|
-
// The tree as rows with paths ("Architecture/Payments"), depth-first,
|
|
247
|
-
// siblings by name — what a model refers to folders by. An older backend
|
|
248
|
-
// without the route yields no rows (every diagram is then at the top level).
|
|
249
|
-
const diagramFolderRows = async (ctx) => {
|
|
250
|
-
let folders;
|
|
251
|
-
try {
|
|
252
|
-
folders = await (0, exports.loadDiagramFolders)(ctx);
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
if (error instanceof errors_js_1.CallmanApiError && (error.status === 404 || error.status === 403))
|
|
256
|
-
return [];
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
const byName = (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }) || a.id.localeCompare(b.id);
|
|
260
|
-
const out = [];
|
|
261
|
-
const seen = new Set();
|
|
262
|
-
const walk = (parentId, prefix, depth) => {
|
|
263
|
-
for (const folder of folders.filter((entry) => entry.parentId === parentId).sort(byName)) {
|
|
264
|
-
if (seen.has(folder.id))
|
|
265
|
-
continue; // a corrupt cycle must not recurse forever
|
|
266
|
-
seen.add(folder.id);
|
|
267
|
-
const path = prefix ? `${prefix}/${folder.name}` : folder.name;
|
|
268
|
-
out.push({ id: folder.id, name: folder.name, parentId: folder.parentId, path, depth });
|
|
269
|
-
walk(folder.id, path, depth + 1);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
walk(null, "", 0);
|
|
273
|
-
return out;
|
|
274
|
-
};
|
|
275
|
-
exports.diagramFolderRows = diagramFolderRows;
|
|
276
|
-
const diagramFolderPath = (rows, folderId) => folderId ? (rows.find((row) => row.id === folderId)?.path ?? null) : null;
|
|
277
|
-
exports.diagramFolderPath = diagramFolderPath;
|
|
278
|
-
// A folder by id, exact path, exact name (unique) or unique substring.
|
|
279
|
-
const resolveDiagramFolder = async (ctx, ref, options = {}) => {
|
|
280
|
-
const rows = await (0, exports.diagramFolderRows)(ctx);
|
|
281
|
-
return (0, resolve_js_1.resolveRef)("diagram folder", ref, rows, {
|
|
282
|
-
describe: (folder) => ({ path: folder.path }),
|
|
283
|
-
aliases: (folder) => [folder.path],
|
|
284
|
-
exact: options.exact,
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
exports.resolveDiagramFolder = resolveDiagramFolder;
|
|
288
243
|
const resolveDiagram = async (ctx, ref, options = {}) => {
|
|
289
244
|
if ((0, ids_js_1.isObjectId)(ref))
|
|
290
245
|
return { item: await ctx.api.getDiagram(ref), matched: "id" };
|
|
@@ -45,7 +45,7 @@ const registerRequestTools = (server, ctx) => {
|
|
|
45
45
|
}));
|
|
46
46
|
server.registerTool("get_request", {
|
|
47
47
|
title: "Get a request",
|
|
48
|
-
description: "Read one saved request in the SAME shape create_request / update_request accept: url, headers, query, pathVariables, body (a JSON value or { mode, … }), auth ({ type, token | username/password | key/value/location | oauth2 fields }), scripts, isEnabledInRunner, plus which database /
|
|
48
|
+
description: "Read one saved request in the SAME shape create_request / update_request accept: url, headers, query, pathVariables, body (a JSON value or { mode, … }), auth ({ type, token | username/password | key/value/location | oauth2 fields }), scripts, isEnabledInRunner, plus which database / Redis steps it has. Secrets in auth and sensitive headers are masked (••••ab) unless reveal: true — sending a masked value back unchanged keeps the stored secret. A long body comes back as bodyPreview (body: null) unless full: true.",
|
|
49
49
|
inputSchema: {
|
|
50
50
|
workspace: shared_js_1.workspaceArg,
|
|
51
51
|
request: (0, shared_js_1.refArg)("Request", "(name, \"METHOD url\" or id)"),
|
|
@@ -65,7 +65,7 @@ const registerRequestTools = (server, ctx) => {
|
|
|
65
65
|
}));
|
|
66
66
|
server.registerTool("create_request", {
|
|
67
67
|
title: "Create a request",
|
|
68
|
-
description: "Create one saved request in a collection (optionally inside a folder by id, name or path): method, url, headers, query, path variables, body (a JSON value, a raw string, or { mode: raw|formdata|urlencoded|json }), auth (bearer, basic, apiKey, oauth2), pre-request / post-response
|
|
68
|
+
description: "Create one saved request in a collection (optionally inside a folder by id, name or path): method, url, headers, query, path variables, body (a JSON value, a raw string, or { mode: raw|formdata|urlencoded|json }), auth (bearer, basic, apiKey, oauth2), pre-request / post-response scripts (put pm.test assertions in the post-response script; testsScript is a legacy slot), and optional database / Redis steps and DB assertions (connections by id or name — list_connections). Use {{variable}} for hosts and secrets and put the values in an environment. Contract checks are added with add_contract_rules / set_data_contract. For many requests use create_requests_bulk (one call, atomic). Reference: callman://docs/request-schema.",
|
|
69
69
|
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), ...requestShape_js_1.requestInputSchema.omit({ folderKey: true }).shape },
|
|
70
70
|
annotations: shared_js_1.WRITE,
|
|
71
71
|
}, (0, shared_js_1.guarded)(async ({ workspace, collection, ...item }) => {
|
|
@@ -237,7 +237,7 @@ const registerRequestTools = (server, ctx) => {
|
|
|
237
237
|
}));
|
|
238
238
|
server.registerTool("update_request", {
|
|
239
239
|
title: "Update a request",
|
|
240
|
-
description: "Change fields of a saved request — only the fields you pass change, in the same shape as create_request. `headers` / `query` / `pathVariables` / `dbAssertions` replace the whole list; `url` and `query` are kept in sync (a ?query in the url becomes rows). `database`
|
|
240
|
+
description: "Change fields of a saved request — only the fields you pass change, in the same shape as create_request. `headers` / `query` / `pathVariables` / `dbAssertions` replace the whole list; `url` and `query` are kept in sync (a ?query in the url becomes rows). `database` merges: a phase or field you leave out is kept, null turns it off. A masked secret sent back unchanged keeps the stored value. isEnabledInRunner toggles the runner switch. To move it, use move_requests; contract checks: add_contract_rules / set_data_contract.",
|
|
241
241
|
inputSchema: {
|
|
242
242
|
workspace: shared_js_1.workspaceArg,
|
|
243
243
|
request: (0, shared_js_1.refArg)("Request", "(name, \"METHOD url\" or id)"),
|
package/dist-cjs/tools/run.js
CHANGED
|
@@ -33,8 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.registerRunTools = void 0;
|
|
36
|
+
exports.registerRunTools = exports.prepareRun = void 0;
|
|
37
37
|
const zod_1 = require("zod");
|
|
38
|
+
const callman_core_1 = require("callman-core");
|
|
38
39
|
const errors_js_1 = require("../client/errors.js");
|
|
39
40
|
const resolve_js_1 = require("../domain/resolve.js");
|
|
40
41
|
const redact_js_1 = require("../lib/redact.js");
|
|
@@ -51,7 +52,7 @@ const shared_js_1 = require("./shared.js");
|
|
|
51
52
|
// {{key}}). One run at a time.
|
|
52
53
|
// What a local run does NOT do, stated up front so a model does not read a
|
|
53
54
|
// missing check as a pass.
|
|
54
|
-
const LOCAL_RUN_LIMITS = "Local-run limits:
|
|
55
|
+
const LOCAL_RUN_LIMITS = "Local-run limits: DB assertions (dbAssertions) are evaluated only by the desktop app; no cookie jar between requests; no client certificates (mTLS); each request times out after 30 s. Database and Redis steps run when the collection's runnerSettings allow them and the connection is usable from this machine.";
|
|
55
56
|
const maxBodyArg = zod_1.z.number().int().min(256).max(truncate_js_1.LIMITS.RUN_BODY_MAX).optional().describe("Response body preview cap in bytes (default 16 KB)");
|
|
56
57
|
// apiKey auth names its own header / query parameter — mask those too, and
|
|
57
58
|
// note the steps a local run skips.
|
|
@@ -69,8 +70,6 @@ const inspectBundle = (rawCollection) => {
|
|
|
69
70
|
const notes = [];
|
|
70
71
|
if (Array.isArray(request.dbAssertions) && request.dbAssertions.length > 0)
|
|
71
72
|
notes.push("DB assertions (desktop only)");
|
|
72
|
-
if (request.kafka && (request.kafka.enabled || request.kafka.prePublish?.enabled || request.kafka.postPublish?.enabled))
|
|
73
|
-
notes.push("Kafka steps");
|
|
74
73
|
if (typeof request.id === "string" && notes.length > 0)
|
|
75
74
|
skipped.set(request.id, notes);
|
|
76
75
|
}
|
|
@@ -112,6 +111,7 @@ const prepareRun = async (ctx, ws, collectionId, environmentRef) => {
|
|
|
112
111
|
skipped: skippedSteps,
|
|
113
112
|
};
|
|
114
113
|
};
|
|
114
|
+
exports.prepareRun = prepareRun;
|
|
115
115
|
// Warnings for the requests actually run.
|
|
116
116
|
const skippedWarnings = (prepared, ids) => {
|
|
117
117
|
const out = [];
|
|
@@ -141,7 +141,7 @@ const registerRunTools = (server, ctx) => {
|
|
|
141
141
|
const { ws, value, note } = await (0, lookups_js_1.findRequest)(ctx, workspace, request, collection);
|
|
142
142
|
const { request: found } = value;
|
|
143
143
|
const annotation = [value.annotation, note].filter(Boolean).join("; ") || undefined;
|
|
144
|
-
const prepared = await prepareRun(ctx, ws.id, found.collectionId, environment);
|
|
144
|
+
const prepared = await (0, exports.prepareRun)(ctx, ws.id, found.collectionId, environment);
|
|
145
145
|
const runnable = prepared.collection.requests.find((entry) => entry.id === found.id);
|
|
146
146
|
if (!runnable)
|
|
147
147
|
throw new errors_js_1.ToolInputError("REQUEST_NOT_IN_BUNDLE", "The request is not part of the collection bundle (was it just deleted?).");
|
|
@@ -161,7 +161,17 @@ const registerRunTools = (server, ctx) => {
|
|
|
161
161
|
query: redactor.query(Object.fromEntries(Object.entries(runnable.query ?? {}).map(([key, value]) => [key, resolveTemplate(value, env)]))),
|
|
162
162
|
body: redactor.value(runnable.body ?? null),
|
|
163
163
|
hasPreRequestScript: Boolean(runnable.preRequestScript),
|
|
164
|
-
hasTests: Boolean(runnable.tests),
|
|
164
|
+
hasTests: Boolean(runnable.tests || runnable.postResponseScript),
|
|
165
|
+
// Collection / folder scripts that run around this request.
|
|
166
|
+
inheritedScripts: (0, callman_core_1.buildScriptLayers)({
|
|
167
|
+
collection: {
|
|
168
|
+
...(prepared.collection.id ? { id: prepared.collection.id } : {}),
|
|
169
|
+
name: prepared.collection.name,
|
|
170
|
+
scripts: prepared.collection.scripts ?? null,
|
|
171
|
+
},
|
|
172
|
+
folders: prepared.collection.folders ?? [],
|
|
173
|
+
folderId: runnable.folderId ?? null,
|
|
174
|
+
}).map(callman_core_1.describeScriptLayer),
|
|
165
175
|
},
|
|
166
176
|
environment: prepared.environment,
|
|
167
177
|
availableVariableKeys: Object.keys(env),
|
|
@@ -203,7 +213,7 @@ const registerRunTools = (server, ctx) => {
|
|
|
203
213
|
annotations: shared_js_1.RUN,
|
|
204
214
|
}, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, only, environment, bail, maxBodyBytes }) => {
|
|
205
215
|
const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
|
|
206
|
-
const prepared = await prepareRun(ctx, ws.id, resolved.item.id, environment);
|
|
216
|
+
const prepared = await (0, exports.prepareRun)(ctx, ws.id, resolved.item.id, environment);
|
|
207
217
|
let selection = null;
|
|
208
218
|
if (folder) {
|
|
209
219
|
const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
|