@testkase/test-management-mcp 2.4.1 → 2.6.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/README.md +101 -88
- package/build/compact.d.ts +5 -0
- package/build/compact.js +106 -0
- package/build/index.js +353 -432
- package/package.json +12 -5
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/test-config.d.ts.map +0 -1
- package/build/test-config.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
|
+
import { compact, flattenFolders, pick, shapeList } from "./compact.js";
|
|
6
7
|
const config = {
|
|
7
8
|
apiBaseUrl: (process.env.TESTKASE_API_BASE_URL || "https://api.testkase.com").replace(/\/+$/, ""),
|
|
8
9
|
patToken: process.env.TESTKASE_PAT_TOKEN,
|
|
@@ -12,8 +13,8 @@ const config = {
|
|
|
12
13
|
: undefined,
|
|
13
14
|
appUrl: process.env.TESTKASE_APP_URL?.replace(/\/+$/, ""),
|
|
14
15
|
};
|
|
15
|
-
const VERSION = "2.
|
|
16
|
-
const REQUEST_TIMEOUT_MS = 60_000;
|
|
16
|
+
const VERSION = "2.5.0";
|
|
17
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.TESTKASE_TIMEOUT_MS) || 60_000;
|
|
17
18
|
// ─── Frontend URL helper ─────────────────────────────────────────────────────
|
|
18
19
|
// Test management lives on its own subdomain
|
|
19
20
|
const KNOWN_APP_HOSTS = {
|
|
@@ -88,13 +89,29 @@ async function callApi(method, path, options = {}) {
|
|
|
88
89
|
}
|
|
89
90
|
const qs = queryParams.toString();
|
|
90
91
|
const url = `${config.apiBaseUrl}/api/v1/${path}${qs ? `?${qs}` : ""}`;
|
|
91
|
-
const
|
|
92
|
+
const send = () => axios({
|
|
92
93
|
method,
|
|
93
94
|
url,
|
|
94
95
|
headers: getApiHeaders(),
|
|
95
96
|
timeout: REQUEST_TIMEOUT_MS,
|
|
96
97
|
...(options.body !== undefined && { data: options.body }),
|
|
97
98
|
});
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await send();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Reads are safe to repeat: retry once on a dropped connection or 502/503/504. Writes and timeouts never retry.
|
|
105
|
+
const status = axios.isAxiosError(error) ? error.response?.status : undefined;
|
|
106
|
+
const transient = axios.isAxiosError(error) &&
|
|
107
|
+
error.code !== "ECONNABORTED" &&
|
|
108
|
+
error.code !== "ETIMEDOUT" &&
|
|
109
|
+
(!error.response || [502, 503, 504].includes(status));
|
|
110
|
+
if (method !== "get" || !transient)
|
|
111
|
+
throw error;
|
|
112
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
113
|
+
response = await send();
|
|
114
|
+
}
|
|
98
115
|
// Some endpoints report "not found" as HTTP 200 with status "error"
|
|
99
116
|
const data = response.data;
|
|
100
117
|
if (data && typeof data === "object" && data.status === "error") {
|
|
@@ -107,6 +124,12 @@ async function callApi(method, path, options = {}) {
|
|
|
107
124
|
function handleApiError(error) {
|
|
108
125
|
if (axios.isAxiosError(error)) {
|
|
109
126
|
const e = error;
|
|
127
|
+
if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
|
|
128
|
+
const isRead = (e.config?.method ?? "get").toLowerCase() === "get";
|
|
129
|
+
return new Error(isRead
|
|
130
|
+
? `Request timed out after ${REQUEST_TIMEOUT_MS} ms. Try a smaller limit or set TESTKASE_TIMEOUT_MS.`
|
|
131
|
+
: `Request timed out after ${REQUEST_TIMEOUT_MS} ms. The change may have been applied: check (search/get) before retrying.`);
|
|
132
|
+
}
|
|
110
133
|
let msg = `API request failed: ${e.message}`;
|
|
111
134
|
if (e.response) {
|
|
112
135
|
msg += `\nStatus: ${e.response.status}`;
|
|
@@ -125,11 +148,35 @@ function handleApiError(error) {
|
|
|
125
148
|
return error;
|
|
126
149
|
}
|
|
127
150
|
// ─── JSON response helper ────────────────────────────────────────────────────
|
|
128
|
-
|
|
151
|
+
// Replies are compact JSON (no indentation, no empty values); verbose returns the raw backend data
|
|
152
|
+
function jsonResponse(data, verbose = false) {
|
|
129
153
|
return {
|
|
130
|
-
content: [{ type: "text", text: JSON.stringify(data
|
|
154
|
+
content: [{ type: "text", text: JSON.stringify(verbose ? data : compact(data)) }],
|
|
131
155
|
};
|
|
132
156
|
}
|
|
157
|
+
const LIST_KEYS = {
|
|
158
|
+
testcases: ["id", "title", "priority", "status", "automated"],
|
|
159
|
+
cycles: ["test_cycle_id", "title", "status", "progress", "execution_summary", "folder_path"],
|
|
160
|
+
plans: ["test_plan_id", "title", "status", "progress", "test_cycle_count", "testcase_count", "execution_summary", "folder_path"],
|
|
161
|
+
linkedCases: ["test_case_id", "title", "priority", "execution_status", "assignee=assignee.assignee_name"],
|
|
162
|
+
projects: ["project_id", "name", "role"],
|
|
163
|
+
};
|
|
164
|
+
// Lists: chosen fields + paging, unless the caller asks for the raw reply
|
|
165
|
+
function listResponse(args, data, keys, urlIdKey) {
|
|
166
|
+
const verbose = !!p(args, "verbose");
|
|
167
|
+
return jsonResponse(verbose ? data : shapeList(data, keys, urlIdKey), verbose);
|
|
168
|
+
}
|
|
169
|
+
function memberRow(m) {
|
|
170
|
+
const name = String(m.name ?? [m.first_name ?? m.firstName, m.last_name ?? m.lastName].filter(Boolean).join(" ")).trim();
|
|
171
|
+
return { ...pick(m, ["user_id", "id"]), ...(name ? { name } : {}), ...pick(m, ["email", "role"]) };
|
|
172
|
+
}
|
|
173
|
+
const JSON_EXAMPLES = {
|
|
174
|
+
test_steps: '[{"description":"Open the login page","expected_result":"Form is shown"}]',
|
|
175
|
+
testcases: '[{"title":"Login works","test_steps":[{"description":"Open the login page"}]}]',
|
|
176
|
+
results: '[{"testcase_id":"TEST-1","execution_status":"pass"}]',
|
|
177
|
+
filters: '{"status":["Active"]}',
|
|
178
|
+
custom_fields: '{"Browser":"Chrome"}',
|
|
179
|
+
};
|
|
133
180
|
function safeJsonParse(raw, fieldName) {
|
|
134
181
|
if (typeof raw !== "string")
|
|
135
182
|
return raw;
|
|
@@ -137,9 +184,37 @@ function safeJsonParse(raw, fieldName) {
|
|
|
137
184
|
return JSON.parse(raw);
|
|
138
185
|
}
|
|
139
186
|
catch {
|
|
140
|
-
|
|
187
|
+
const example = JSON_EXAMPLES[fieldName];
|
|
188
|
+
if (!example)
|
|
189
|
+
throw new Error(`Invalid JSON in ${fieldName}. Ensure the value is valid JSON.`);
|
|
190
|
+
const kind = example.startsWith("[") ? "JSON array" : "JSON object";
|
|
191
|
+
throw new Error(`Invalid JSON in ${fieldName}. Send it as a ${kind} (not inside a string), e.g. ${fieldName}: ${example}`);
|
|
141
192
|
}
|
|
142
193
|
}
|
|
194
|
+
// Small models write "Passed", "FAILED", "not run": map them to the API's values
|
|
195
|
+
const EXECUTION_STATUS = {
|
|
196
|
+
pass: "pass", passed: "pass", ok: "pass", success: "pass",
|
|
197
|
+
fail: "fail", failed: "fail", failure: "fail",
|
|
198
|
+
blocked: "blocked", block: "blocked",
|
|
199
|
+
not_executed: "not_executed", not_run: "not_executed", notrun: "not_executed", untested: "not_executed",
|
|
200
|
+
not_tested: "not_executed", unexecuted: "not_executed", pending: "not_executed",
|
|
201
|
+
};
|
|
202
|
+
const PLAN_STATUS = {
|
|
203
|
+
open: "open", new: "open", not_started: "open", todo: "open",
|
|
204
|
+
in_progress: "in_progress", inprogress: "in_progress", started: "in_progress", active: "in_progress",
|
|
205
|
+
completed: "completed", complete: "completed", done: "completed", closed: "completed", finished: "completed",
|
|
206
|
+
};
|
|
207
|
+
const statusKey = (v) => String(v).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
208
|
+
function normalizeExecutionStatus(v, where) {
|
|
209
|
+
const s = EXECUTION_STATUS[statusKey(v)];
|
|
210
|
+
if (!s)
|
|
211
|
+
throw new Error(`${where}: execution_status "${v}" is not valid. Use one of: pass, fail, blocked, not_executed`);
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
// Unknown values pass through so the API can explain them
|
|
215
|
+
function normalizePlanStatus(v) {
|
|
216
|
+
return v === undefined || v === null || v === "" ? v : PLAN_STATUS[statusKey(v)] ?? v;
|
|
217
|
+
}
|
|
143
218
|
function validateId(id, fieldName) {
|
|
144
219
|
const str = String(id);
|
|
145
220
|
if (!/^[a-zA-Z0-9_\-]+$/.test(str)) {
|
|
@@ -177,6 +252,11 @@ function parseSteps(raw) {
|
|
|
177
252
|
throw new Error("test_steps must be a JSON array");
|
|
178
253
|
return parsed.map(normalizeStep);
|
|
179
254
|
}
|
|
255
|
+
// Step ids + text in their current order
|
|
256
|
+
async function currentSteps(projectId, testcaseId) {
|
|
257
|
+
const detail = await callApi("get", `projects/testcases/get-testcase-detail/${projectId}/${testcaseId}`);
|
|
258
|
+
return (detail?.data?.steps || []).map((st) => pick(st, ["id", "description"]));
|
|
259
|
+
}
|
|
180
260
|
// Steps are added one call each; collect failures instead of aborting
|
|
181
261
|
async function addSteps(projectId, testcaseId, steps) {
|
|
182
262
|
let created = 0;
|
|
@@ -212,6 +292,11 @@ function buildFilters(raw, extra = {}) {
|
|
|
212
292
|
}
|
|
213
293
|
return Object.keys(merged).length ? JSON.stringify(merged) : undefined;
|
|
214
294
|
}
|
|
295
|
+
// create_bulk: these top-level args are defaults for every item
|
|
296
|
+
const BULK_SHARED_FIELDS = [
|
|
297
|
+
"folder_id", "folderId", "priority", "status", "automated", "platform", "labels", "features", "environment",
|
|
298
|
+
"owner_id", "ownerId", "precondition", "custom_fields", "customFields",
|
|
299
|
+
];
|
|
215
300
|
// Builds the create/bulk-create payload from tool args (shared by both actions)
|
|
216
301
|
async function buildTestcasePayload(projectId, tc) {
|
|
217
302
|
const data = { title: tc.title ?? tc.name };
|
|
@@ -256,12 +341,9 @@ async function buildTestcasePayload(projectId, tc) {
|
|
|
256
341
|
return data;
|
|
257
342
|
}
|
|
258
343
|
function jsonResponseWithHints(data, hints) {
|
|
259
|
-
const
|
|
260
|
-
const hintsBlock = hints.length > 0
|
|
261
|
-
? `\n\n---\nHINTS:\n${hints.map((h) => `- ${h}`).join("\n")}`
|
|
262
|
-
: "";
|
|
344
|
+
const hintsBlock = hints.length > 0 ? `\nHINTS: ${hints.join(" | ")}` : "";
|
|
263
345
|
return {
|
|
264
|
-
content: [{ type: "text", text:
|
|
346
|
+
content: [{ type: "text", text: JSON.stringify(compact(data)) + hintsBlock }],
|
|
265
347
|
};
|
|
266
348
|
}
|
|
267
349
|
// ─── Error enrichment ───────────────────────────────────────────────────────
|
|
@@ -339,331 +421,235 @@ function requireArg(args, name, aliases = []) {
|
|
|
339
421
|
return value;
|
|
340
422
|
}
|
|
341
423
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
424
|
+
// REPORT TYPE → ENDPOINT MAPPING
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
426
|
+
const REPORT_TYPE_MAP = {
|
|
427
|
+
// Execution
|
|
428
|
+
execution_summary: "execution/summary",
|
|
429
|
+
execution_by_cycle: "execution/by-cycle",
|
|
430
|
+
execution_by_tester: "execution/by-tester",
|
|
431
|
+
execution_by_priority: "execution/by-priority",
|
|
432
|
+
execution_by_environment: "execution/by-environment",
|
|
433
|
+
execution_by_folder: "execution/by-folder",
|
|
434
|
+
// Coverage
|
|
435
|
+
requirement_coverage: "coverage/requirement-coverage",
|
|
436
|
+
traceability_matrix: "coverage/traceability-matrix",
|
|
437
|
+
failed_requirements: "coverage/failed-requirements",
|
|
438
|
+
uncovered_requirements: "coverage/uncovered-requirements",
|
|
439
|
+
testcase_coverage: "coverage/testcase-coverage",
|
|
440
|
+
unlinked_testcases: "coverage/unlinked-testcases",
|
|
441
|
+
// Defects
|
|
442
|
+
defects_by_folder: "defects/by-folder",
|
|
443
|
+
// Team
|
|
444
|
+
tester_workload: "team/tester-workload",
|
|
445
|
+
testcase_distribution: "team/testcase-distribution",
|
|
446
|
+
// Trends
|
|
447
|
+
execution_trend: "trends/execution-trend",
|
|
448
|
+
execution_burnup: "trends/execution-burnup",
|
|
449
|
+
execution_burndown: "trends/execution-burndown",
|
|
450
|
+
test_creation: "trends/test-creation",
|
|
451
|
+
cycle_comparison: "trends/cycle-comparison",
|
|
452
|
+
scorecard_by_folder: "trends/scorecard-by-folder",
|
|
453
|
+
scorecard_by_tester: "trends/scorecard-by-tester",
|
|
454
|
+
created_vs_executed: "trends/created-vs-executed",
|
|
455
|
+
execution_by_automation: "trends/execution-by-automation",
|
|
456
|
+
defects_by_cycle: "trends/defects-by-cycle",
|
|
457
|
+
defects_by_tester: "trends/defects-by-tester",
|
|
458
|
+
requirement_coverage_trend: "trends/requirement-coverage-trend",
|
|
459
|
+
// Insights (computed from project data, no AI credits)
|
|
460
|
+
flaky_tests: "ai/flaky-tests",
|
|
461
|
+
release_readiness: "ai/release-readiness",
|
|
462
|
+
risk_heatmap_folder: "ai/risk-heatmap-folder",
|
|
463
|
+
risk_heatmap_feature: "ai/risk-heatmap-feature",
|
|
464
|
+
testcase_quality: "ai/testcase-quality",
|
|
465
|
+
predictive_failure: "ai/predictive-failure",
|
|
466
|
+
smart_prioritization: "ai/smart-prioritization",
|
|
467
|
+
tester_effectiveness: "ai/tester-effectiveness",
|
|
468
|
+
stale_tests: "ai/stale-tests",
|
|
469
|
+
defect_hotspots: "ai/defect-hotspots",
|
|
470
|
+
cycle_health: "ai/cycle-health",
|
|
471
|
+
suite_optimization: "ai/suite-optimization",
|
|
472
|
+
requirement_risk_matrix: "ai/requirement-risk-matrix",
|
|
473
|
+
execution_velocity: "ai/execution-velocity",
|
|
474
|
+
project_health: "ai/project-health",
|
|
475
|
+
};
|
|
476
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
342
477
|
// TOOL DEFINITIONS (11 tools)
|
|
478
|
+
// Kept short on purpose: every client sends these on every turn. * = required.
|
|
343
479
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
480
|
+
const S = { type: "string" };
|
|
481
|
+
const N = { type: "number" };
|
|
482
|
+
const VERBOSE = { type: "boolean" };
|
|
483
|
+
const STATUS = { type: "string", enum: ["open", "in_progress", "completed"] };
|
|
484
|
+
const SECTION = { type: "string", enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"] };
|
|
485
|
+
const CSV = S;
|
|
486
|
+
const PROJECT_VALUE = S;
|
|
344
487
|
const LIST_PROJECTS_TOOL = {
|
|
345
488
|
name: "list_projects",
|
|
346
|
-
description: "
|
|
347
|
-
"Use this first to find a projectId, then pass it to other tools.",
|
|
489
|
+
description: "Projects you can access. Start here to get a projectId.",
|
|
348
490
|
annotations: { readOnlyHint: true },
|
|
349
|
-
inputSchema: {
|
|
350
|
-
type: "object",
|
|
351
|
-
properties: {
|
|
352
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
353
|
-
limit: { type: "number", description: "Items per page (default: 12)" },
|
|
354
|
-
},
|
|
355
|
-
required: [],
|
|
356
|
-
},
|
|
491
|
+
inputSchema: { type: "object", properties: { page: N, limit: N, verbose: VERBOSE }, required: [] },
|
|
357
492
|
};
|
|
358
493
|
const GET_PROJECT_STRUCTURE_TOOL = {
|
|
359
494
|
name: "get_project_structure",
|
|
360
|
-
description: "
|
|
361
|
-
"IMPORTANT: Call this before create/update to discover allowed field values, which are project-specific.",
|
|
495
|
+
description: "Folders, labels, members, field_options. Call before create/update: priority, status etc. accept only these values (any case).",
|
|
362
496
|
annotations: { readOnlyHint: true },
|
|
363
497
|
inputSchema: {
|
|
364
498
|
type: "object",
|
|
365
499
|
properties: {
|
|
366
|
-
projectId:
|
|
367
|
-
section:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
description: "Folder section (default: TESTCASE)",
|
|
371
|
-
},
|
|
372
|
-
include: {
|
|
373
|
-
type: "string",
|
|
374
|
-
description: "Comma-separated list of what to include: folders,labels,members,field_options (default: all)",
|
|
375
|
-
},
|
|
500
|
+
projectId: S,
|
|
501
|
+
section: SECTION,
|
|
502
|
+
include: { type: "string", description: "CSV of folders,labels,members,field_options (default all)" },
|
|
503
|
+
verbose: VERBOSE,
|
|
376
504
|
},
|
|
377
505
|
required: ["projectId"],
|
|
378
506
|
},
|
|
379
507
|
};
|
|
380
508
|
const SEARCH_TESTCASES_TOOL = {
|
|
381
509
|
name: "search_testcases",
|
|
382
|
-
description: "Search
|
|
510
|
+
description: "Search/list test cases.",
|
|
383
511
|
annotations: { readOnlyHint: true },
|
|
384
512
|
inputSchema: {
|
|
385
513
|
type: "object",
|
|
386
514
|
properties: {
|
|
387
|
-
projectId:
|
|
388
|
-
search:
|
|
389
|
-
folder_id: {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
'Example: {"status":["Active"],"priority":["High","Critical"]}. Also supports "exclude_status".',
|
|
397
|
-
},
|
|
398
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
399
|
-
limit: { type: "number", description: "Items per page (default: 20, max: 100)" },
|
|
400
|
-
sortBy: { type: "string", description: "Field to sort by (e.g. created_at, title)" },
|
|
401
|
-
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
515
|
+
projectId: S,
|
|
516
|
+
search: S,
|
|
517
|
+
folder_id: { type: "number", description: "includes sub-folders; 0 = no folder" },
|
|
518
|
+
filters: { type: "object", description: 'values are lists: {"status":["Active"]}; also exclude_status' },
|
|
519
|
+
page: N,
|
|
520
|
+
limit: { type: "number", description: "max 100" },
|
|
521
|
+
sortBy: S,
|
|
522
|
+
sortOrder: { type: "string", enum: ["asc", "desc"] },
|
|
523
|
+
verbose: VERBOSE,
|
|
402
524
|
},
|
|
403
525
|
required: ["projectId"],
|
|
404
526
|
},
|
|
405
527
|
};
|
|
406
528
|
const GET_TESTCASE_TOOL = {
|
|
407
529
|
name: "get_testcase",
|
|
408
|
-
description: "
|
|
530
|
+
description: "One test case + steps + attachments (view/download).",
|
|
409
531
|
annotations: { readOnlyHint: true },
|
|
410
|
-
inputSchema: {
|
|
411
|
-
type: "object",
|
|
412
|
-
properties: {
|
|
413
|
-
projectId: { type: "string", description: "Project ID" },
|
|
414
|
-
testcaseId: { type: "string", description: "Test case ID (e.g. TEST-123)" },
|
|
415
|
-
},
|
|
416
|
-
required: ["projectId", "testcaseId"],
|
|
417
|
-
},
|
|
532
|
+
inputSchema: { type: "object", properties: { projectId: S, testcaseId: S, verbose: VERBOSE }, required: ["projectId", "testcaseId"] },
|
|
418
533
|
};
|
|
419
534
|
const TESTCASE_FIELDS = "title, summary, precondition, priority, status, automated, test_data, expected_result, labels, features, platform, environment, custom_fields, owner_id, folder_id";
|
|
420
535
|
const MANAGE_TESTCASE_TOOL = {
|
|
421
536
|
name: "manage_testcase",
|
|
422
|
-
description: "
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
" create - Create a single test case. Params: title (required), test_steps, and any of: " + TESTCASE_FIELDS + "\n" +
|
|
426
|
-
" create_bulk - Create up to 100 test cases in one go. Params: testcases (JSON array; each item takes the same fields as create, including test_steps)\n" +
|
|
427
|
-
" update - Update one field for test case(s). Params: ids, field, value (all required)\n" +
|
|
428
|
-
" delete - PERMANENTLY delete test case(s) with their steps, history and links. Params: ids (required)\n" +
|
|
429
|
-
" add_steps - Append steps to a test case. Params: testcaseId, test_steps\n" +
|
|
430
|
-
" update_step - Change one step. Params: testcaseId, step_id, and description/test_data/expected_result\n" +
|
|
431
|
-
" delete_steps - Delete steps. Params: testcaseId, step_ids (comma-separated)\n" +
|
|
432
|
-
" reorder_steps - Set the step order. Params: testcaseId, step_ids (all step ids, in the new order)\n" +
|
|
433
|
-
"Step ids come from get_testcase.\n\n" +
|
|
434
|
-
"VALID UPDATE FIELDS: " + TESTCASE_FIELDS + "\n\n" +
|
|
435
|
-
"EXAMPLES:\n" +
|
|
436
|
-
" Create: { action: 'create', title: 'Verify login', test_steps: '[{\"description\":\"Open login page\",\"expected_result\":\"Form shown\"}]' }\n" +
|
|
437
|
-
" Update: { action: 'update', ids: 'TEST-1', field: 'priority', value: '<value from get_project_structure>' }\n" +
|
|
438
|
-
" Delete: { action: 'delete', ids: 'TEST-1,TEST-2' }",
|
|
537
|
+
description: "* = required, ids are CSV. create(title*) | create_bulk(testcases*, max 100) | update(ids*, field*, value*): one field | " +
|
|
538
|
+
"delete(ids*): permanent | add_steps(testcaseId*, test_steps*) | update_step(testcaseId*, step_id*) | delete_steps(testcaseId*, step_ids*) | " +
|
|
539
|
+
"reorder_steps(testcaseId*, step_ids*: all, new order)",
|
|
439
540
|
annotations: { destructiveHint: true },
|
|
440
541
|
inputSchema: {
|
|
441
542
|
type: "object",
|
|
442
543
|
properties: {
|
|
443
|
-
projectId:
|
|
444
|
-
action: {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
},
|
|
470
|
-
automated: {
|
|
471
|
-
type: "string",
|
|
472
|
-
description: "[create] Automation status. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
473
|
-
},
|
|
474
|
-
test_data: { type: "string", description: "[create/update_step] Test data" },
|
|
475
|
-
expected_result: { type: "string", description: "[create/update_step] Expected result" },
|
|
476
|
-
platform: {
|
|
477
|
-
type: "string",
|
|
478
|
-
description: "[create] Platform. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
479
|
-
},
|
|
480
|
-
environment: { type: "string", description: "[create] Comma-separated environment values" },
|
|
481
|
-
features: { type: "string", description: "[create] Comma-separated feature/component names" },
|
|
482
|
-
owner_id: { type: "string", description: "[create] Owner user ID (a project member)" },
|
|
483
|
-
custom_fields: {
|
|
484
|
-
type: "string",
|
|
485
|
-
description: '[create] JSON object of custom field values (e.g. {"custom1": "value", "custom2": ["val1","val2"]})',
|
|
486
|
-
},
|
|
487
|
-
// create_bulk params
|
|
488
|
-
testcases: {
|
|
489
|
-
type: "string",
|
|
490
|
-
description: "[create_bulk] JSON array of test cases. Each item takes the create fields, e.g. " +
|
|
491
|
-
'[{"title":"...","folder_id":12,"priority":"High","test_steps":[{"description":"...","expected_result":"..."}]}]',
|
|
492
|
-
},
|
|
493
|
-
// update/delete params
|
|
494
|
-
ids: {
|
|
495
|
-
type: "string",
|
|
496
|
-
description: "[update/delete] Test case IDs, comma-separated (e.g. 'TEST-1' or 'TEST-1,TEST-2')",
|
|
497
|
-
},
|
|
498
|
-
field: {
|
|
499
|
-
type: "string",
|
|
500
|
-
enum: TESTCASE_FIELDS.split(", "),
|
|
501
|
-
description: "[update] Field to update. For priority/status/automated/platform/environment, call get_project_structure(include='field_options') to get allowed values.",
|
|
502
|
-
},
|
|
503
|
-
value: { type: "string", description: "[update] New value. String for most fields. For labels/features/environment: comma-separated. For custom_fields: JSON object (e.g. {\"fieldName\": \"value\"})" },
|
|
504
|
-
// step params
|
|
505
|
-
testcaseId: { type: "string", description: "[add_steps/update_step/delete_steps/reorder_steps] Test case ID (e.g. TEST-123)" },
|
|
506
|
-
step_id: { type: "number", description: "[update_step] Step ID to change" },
|
|
507
|
-
description: { type: "string", description: "[update_step] Step description" },
|
|
508
|
-
step_ids: { type: "string", description: "[delete_steps/reorder_steps] Comma-separated step IDs" },
|
|
544
|
+
projectId: S,
|
|
545
|
+
action: { type: "string", enum: ["create", "create_bulk", "update", "delete", "add_steps", "update_step", "delete_steps", "reorder_steps"] },
|
|
546
|
+
title: S,
|
|
547
|
+
summary: S,
|
|
548
|
+
precondition: S,
|
|
549
|
+
priority: PROJECT_VALUE,
|
|
550
|
+
status: PROJECT_VALUE,
|
|
551
|
+
automated: PROJECT_VALUE,
|
|
552
|
+
platform: PROJECT_VALUE,
|
|
553
|
+
folder_id: N,
|
|
554
|
+
labels: CSV,
|
|
555
|
+
features: CSV,
|
|
556
|
+
environment: CSV,
|
|
557
|
+
test_data: S,
|
|
558
|
+
expected_result: S,
|
|
559
|
+
owner_id: S,
|
|
560
|
+
custom_fields: { type: "object" },
|
|
561
|
+
test_steps: { type: "array", items: { type: "object", properties: { description: S, test_data: S, expected_result: S } } },
|
|
562
|
+
testcases: { type: "array", items: { type: "object" }, description: "top-level fields apply to all" },
|
|
563
|
+
ids: CSV,
|
|
564
|
+
field: { type: "string", enum: TESTCASE_FIELDS.split(", ") },
|
|
565
|
+
value: { type: "string", description: "CSV for list fields, JSON for custom_fields" },
|
|
566
|
+
testcaseId: S,
|
|
567
|
+
step_id: N,
|
|
568
|
+
description: S,
|
|
569
|
+
step_ids: CSV,
|
|
509
570
|
},
|
|
510
571
|
required: ["projectId", "action"],
|
|
511
572
|
},
|
|
512
573
|
};
|
|
513
574
|
const MANAGE_FOLDER_TOOL = {
|
|
514
575
|
name: "manage_folder",
|
|
515
|
-
description: "
|
|
516
|
-
"
|
|
517
|
-
" create - Create a folder. Params: name (required), section, parentId\n" +
|
|
518
|
-
" rename - Rename a folder. Params: folderId (required), name (required)\n" +
|
|
519
|
-
" move - Move a folder. Params: folderId (required), parentId (omit or null for root)\n" +
|
|
520
|
-
" delete - PERMANENTLY delete a folder, all its sub-folders AND every test case / cycle / plan inside them. " +
|
|
521
|
-
"Only owners and project admins can do this. Confirm with the user first. Params: folderId (required)",
|
|
576
|
+
description: "create(name*, parentId) | rename(folderId*, name*) | move(folderId*, parentId: omit = root) | " +
|
|
577
|
+
"delete(folderId*): deletes its contents too; confirm with the user",
|
|
522
578
|
annotations: { destructiveHint: true },
|
|
523
579
|
inputSchema: {
|
|
524
580
|
type: "object",
|
|
525
581
|
properties: {
|
|
526
|
-
projectId:
|
|
527
|
-
action: {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
section: {
|
|
533
|
-
type: "string",
|
|
534
|
-
enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
|
|
535
|
-
description: "Folder section (default: TESTCASE)",
|
|
536
|
-
},
|
|
537
|
-
name: { type: "string", description: "[create/rename] Folder name (max 50 chars)" },
|
|
538
|
-
folderId: {
|
|
539
|
-
type: "number",
|
|
540
|
-
description: "[rename/move/delete] ID of the folder to modify",
|
|
541
|
-
},
|
|
542
|
-
parentId: {
|
|
543
|
-
type: "number",
|
|
544
|
-
description: "[create] Parent folder ID for subfolder. [move] New parent ID (null for root).",
|
|
545
|
-
},
|
|
582
|
+
projectId: S,
|
|
583
|
+
action: { type: "string", enum: ["create", "rename", "move", "delete"] },
|
|
584
|
+
section: SECTION,
|
|
585
|
+
name: { type: "string", description: "max 50 chars" },
|
|
586
|
+
folderId: N,
|
|
587
|
+
parentId: N,
|
|
546
588
|
},
|
|
547
589
|
required: ["projectId", "action"],
|
|
548
590
|
},
|
|
549
591
|
};
|
|
550
592
|
const SEARCH_TEST_CYCLES_TOOL = {
|
|
551
593
|
name: "search_test_cycles",
|
|
552
|
-
description: "
|
|
594
|
+
description: "Search/list test cycles with progress.",
|
|
553
595
|
annotations: { readOnlyHint: true },
|
|
554
596
|
inputSchema: {
|
|
555
597
|
type: "object",
|
|
556
|
-
properties: {
|
|
557
|
-
projectId: { type: "string", description: "Project ID" },
|
|
558
|
-
search: { type: "string", description: "Search by title" },
|
|
559
|
-
folderId: { type: "number", description: "Filter by folder ID" },
|
|
560
|
-
status: { type: "string", enum: ["open", "in_progress", "completed"], description: "Filter by status" },
|
|
561
|
-
page: { type: "number", description: "Page number (default: 1)" },
|
|
562
|
-
limit: { type: "number", description: "Items per page (default: 20, max: 100)" },
|
|
563
|
-
},
|
|
598
|
+
properties: { projectId: S, search: S, folderId: N, status: STATUS, page: N, limit: N, verbose: VERBOSE },
|
|
564
599
|
required: ["projectId"],
|
|
565
600
|
},
|
|
566
601
|
};
|
|
567
602
|
const MANAGE_TEST_CYCLE_TOOL = {
|
|
568
603
|
name: "manage_test_cycle",
|
|
569
|
-
description: "
|
|
570
|
-
"
|
|
571
|
-
" create - Create a cycle. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
572
|
-
" update - Update a cycle. Params: cycleId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
573
|
-
" delete - Delete cycle(s). Params: ids (required, comma-separated)\n" +
|
|
574
|
-
" get_details - Get cycle details. Params: cycleId (required)\n" +
|
|
575
|
-
" get_testcases - List test cases in cycle. Params: cycleId (required), search, page, limit\n" +
|
|
576
|
-
" link_testcases - Link test cases to cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
577
|
-
" unlink_testcases - Unlink test cases from cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
578
|
-
" assign_testcases - Assign test cases to user. Params: cycleId (required), testcase_ids (required), assignee_id (required)\n\n" +
|
|
579
|
-
"VALID STATUSES: open, in_progress, completed",
|
|
604
|
+
description: "create(title*, status, dates, folder_id) | update(cycleId*) | delete(ids*) | get_details(cycleId*) | get_testcases(cycleId*) | " +
|
|
605
|
+
"link_testcases/unlink_testcases(cycleId*, testcase_ids*) | assign_testcases(cycleId*, testcase_ids*, assignee_id*: member user_id). Dates YYYY-MM-DD.",
|
|
580
606
|
annotations: { destructiveHint: true },
|
|
581
607
|
inputSchema: {
|
|
582
608
|
type: "object",
|
|
583
609
|
properties: {
|
|
584
|
-
projectId:
|
|
610
|
+
projectId: S,
|
|
585
611
|
action: {
|
|
586
612
|
type: "string",
|
|
587
|
-
enum: [
|
|
588
|
-
"create",
|
|
589
|
-
"update",
|
|
590
|
-
"delete",
|
|
591
|
-
"get_details",
|
|
592
|
-
"get_testcases",
|
|
593
|
-
"link_testcases",
|
|
594
|
-
"unlink_testcases",
|
|
595
|
-
"assign_testcases",
|
|
596
|
-
],
|
|
597
|
-
description: "Action to perform",
|
|
598
|
-
},
|
|
599
|
-
// Identifiers
|
|
600
|
-
cycleId: {
|
|
601
|
-
type: "string",
|
|
602
|
-
description: "Test cycle ID (e.g. TCYCLE-1). Required for update/delete/get_details/get_testcases/link/unlink/assign.",
|
|
603
|
-
},
|
|
604
|
-
// create/update fields
|
|
605
|
-
title: { type: "string", description: "[create/update] Cycle title" },
|
|
606
|
-
summary: { type: "string", description: "[create/update] Description" },
|
|
607
|
-
status: {
|
|
608
|
-
type: "string",
|
|
609
|
-
enum: ["open", "in_progress", "completed"],
|
|
610
|
-
description: "[create/update] Status",
|
|
611
|
-
},
|
|
612
|
-
planned_start_date: {
|
|
613
|
-
type: "string",
|
|
614
|
-
description: "[create/update] Start date (YYYY-MM-DD)",
|
|
615
|
-
},
|
|
616
|
-
planned_end_date: {
|
|
617
|
-
type: "string",
|
|
618
|
-
description: "[create/update] End date (YYYY-MM-DD)",
|
|
619
|
-
},
|
|
620
|
-
folder_id: {
|
|
621
|
-
type: "number",
|
|
622
|
-
description: "[create/update] Folder ID",
|
|
623
|
-
},
|
|
624
|
-
// delete
|
|
625
|
-
ids: {
|
|
626
|
-
type: "string",
|
|
627
|
-
description: "[delete] Comma-separated cycle IDs to delete",
|
|
613
|
+
enum: ["create", "update", "delete", "get_details", "get_testcases", "link_testcases", "unlink_testcases", "assign_testcases"],
|
|
628
614
|
},
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
search:
|
|
640
|
-
page:
|
|
641
|
-
limit:
|
|
615
|
+
cycleId: S,
|
|
616
|
+
title: S,
|
|
617
|
+
summary: S,
|
|
618
|
+
status: STATUS,
|
|
619
|
+
planned_start_date: S,
|
|
620
|
+
planned_end_date: S,
|
|
621
|
+
folder_id: N,
|
|
622
|
+
ids: CSV,
|
|
623
|
+
testcase_ids: CSV,
|
|
624
|
+
assignee_id: N,
|
|
625
|
+
search: S,
|
|
626
|
+
page: N,
|
|
627
|
+
limit: N,
|
|
628
|
+
verbose: VERBOSE,
|
|
642
629
|
},
|
|
643
630
|
required: ["projectId", "action"],
|
|
644
631
|
},
|
|
645
632
|
};
|
|
646
633
|
const EXECUTE_TESTS_TOOL = {
|
|
647
634
|
name: "execute_tests",
|
|
648
|
-
description: "Record
|
|
649
|
-
"Pass a single result or an array of results. " +
|
|
650
|
-
"Valid execution statuses: pass, fail, blocked, not_executed. " +
|
|
651
|
-
"The test cases must already be linked to the cycle (manage_test_cycle link_testcases).",
|
|
635
|
+
description: "Record results for test cases already linked to the cycle.",
|
|
652
636
|
inputSchema: {
|
|
653
637
|
type: "object",
|
|
654
638
|
properties: {
|
|
655
|
-
projectId:
|
|
656
|
-
cycleId:
|
|
639
|
+
projectId: S,
|
|
640
|
+
cycleId: S,
|
|
657
641
|
results: {
|
|
658
|
-
type: "
|
|
659
|
-
|
|
660
|
-
"
|
|
642
|
+
type: "array",
|
|
643
|
+
items: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: { testcase_id: S, execution_status: { type: "string", enum: ["pass", "fail", "blocked", "not_executed"] }, actual_result: S },
|
|
646
|
+
required: ["testcase_id", "execution_status"],
|
|
647
|
+
},
|
|
661
648
|
},
|
|
662
649
|
mode: {
|
|
663
650
|
type: "string",
|
|
664
651
|
enum: ["manual", "automation"],
|
|
665
|
-
description: "
|
|
666
|
-
"automation: recorded as a CI/automation run, where a failure from any earlier automation run keeps the case failed. Use only for CI results.",
|
|
652
|
+
description: "automation = CI run: an earlier CI fail keeps it failed",
|
|
667
653
|
},
|
|
668
654
|
},
|
|
669
655
|
required: ["projectId", "cycleId", "results"],
|
|
@@ -671,177 +657,56 @@ const EXECUTE_TESTS_TOOL = {
|
|
|
671
657
|
};
|
|
672
658
|
const MANAGE_TEST_PLAN_TOOL = {
|
|
673
659
|
name: "manage_test_plan",
|
|
674
|
-
description: "
|
|
675
|
-
"
|
|
676
|
-
" create - Create a plan. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
677
|
-
" update - Update a plan. Params: planId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
678
|
-
" delete - Delete plan(s). Params: ids (required, comma-separated)\n" +
|
|
679
|
-
" list - List all plans. Params: search, page, limit\n" +
|
|
680
|
-
" get_details - Get plan details. Params: planId (required)\n" +
|
|
681
|
-
" link_cycles - Link cycles to plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
682
|
-
" unlink_cycles - Unlink cycles from plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
683
|
-
" get_cycles - List cycles in plan. Params: planId (required), search, page, limit\n" +
|
|
684
|
-
" get_testcases - List test cases in plan. Params: planId (required), search, page, limit\n\n" +
|
|
685
|
-
"VALID STATUSES: open, in_progress, completed",
|
|
660
|
+
description: "create(title*, status, dates, folder_id) | update(planId*) | delete(ids*) | list | get_details(planId*) | link_cycles/unlink_cycles(planId*, cycle_ids*) | " +
|
|
661
|
+
"get_cycles/get_testcases(planId*). Dates YYYY-MM-DD.",
|
|
686
662
|
annotations: { destructiveHint: true },
|
|
687
663
|
inputSchema: {
|
|
688
664
|
type: "object",
|
|
689
665
|
properties: {
|
|
690
|
-
projectId:
|
|
666
|
+
projectId: S,
|
|
691
667
|
action: {
|
|
692
668
|
type: "string",
|
|
693
|
-
enum: [
|
|
694
|
-
"create",
|
|
695
|
-
"update",
|
|
696
|
-
"delete",
|
|
697
|
-
"list",
|
|
698
|
-
"get_details",
|
|
699
|
-
"link_cycles",
|
|
700
|
-
"unlink_cycles",
|
|
701
|
-
"get_cycles",
|
|
702
|
-
"get_testcases",
|
|
703
|
-
],
|
|
704
|
-
description: "Action to perform",
|
|
705
|
-
},
|
|
706
|
-
// Identifier
|
|
707
|
-
planId: {
|
|
708
|
-
type: "string",
|
|
709
|
-
description: "Test plan ID (e.g. TPLAN-1). Required for update/delete/get_details/link_cycles/unlink_cycles/get_cycles/get_testcases.",
|
|
669
|
+
enum: ["create", "update", "delete", "list", "get_details", "link_cycles", "unlink_cycles", "get_cycles", "get_testcases"],
|
|
710
670
|
},
|
|
711
|
-
|
|
712
|
-
title:
|
|
713
|
-
summary:
|
|
714
|
-
status:
|
|
715
|
-
planned_start_date:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
// delete
|
|
725
|
-
ids: {
|
|
726
|
-
type: "string",
|
|
727
|
-
description: "[delete] Comma-separated plan IDs to delete",
|
|
728
|
-
},
|
|
729
|
-
// link/unlink cycles
|
|
730
|
-
cycle_ids: {
|
|
731
|
-
type: "string",
|
|
732
|
-
description: "[link_cycles/unlink_cycles] Comma-separated test cycle IDs",
|
|
733
|
-
},
|
|
734
|
-
// list/get_cycles/get_testcases pagination
|
|
735
|
-
search: { type: "string", description: "[list/get_cycles/get_testcases] Search text" },
|
|
736
|
-
page: { type: "number", description: "[list/get_cycles/get_testcases] Page number" },
|
|
737
|
-
limit: { type: "number", description: "[list/get_cycles/get_testcases] Items per page" },
|
|
671
|
+
planId: S,
|
|
672
|
+
title: S,
|
|
673
|
+
summary: S,
|
|
674
|
+
status: STATUS,
|
|
675
|
+
planned_start_date: S,
|
|
676
|
+
planned_end_date: S,
|
|
677
|
+
folder_id: N,
|
|
678
|
+
ids: CSV,
|
|
679
|
+
cycle_ids: CSV,
|
|
680
|
+
search: S,
|
|
681
|
+
page: N,
|
|
682
|
+
limit: N,
|
|
683
|
+
verbose: VERBOSE,
|
|
738
684
|
},
|
|
739
685
|
required: ["projectId", "action"],
|
|
740
686
|
},
|
|
741
687
|
};
|
|
742
688
|
const GET_REPORT_TOOL = {
|
|
743
689
|
name: "get_report",
|
|
744
|
-
description: "
|
|
745
|
-
"(the insight reports are computed from your data; they do not use AI credits). " +
|
|
746
|
-
"Available report types: execution_summary, execution_by_cycle, execution_by_tester, execution_by_priority, " +
|
|
747
|
-
"execution_by_environment, execution_by_folder, requirement_coverage, traceability_matrix, " +
|
|
748
|
-
"failed_requirements, uncovered_requirements, testcase_coverage, unlinked_testcases, " +
|
|
749
|
-
"defects_by_folder, tester_workload, testcase_distribution, " +
|
|
750
|
-
"execution_trend, execution_burnup, execution_burndown, test_creation, cycle_comparison, " +
|
|
751
|
-
"scorecard_by_folder, scorecard_by_tester, created_vs_executed, execution_by_automation, " +
|
|
752
|
-
"defects_by_cycle, defects_by_tester, requirement_coverage_trend, " +
|
|
753
|
-
"flaky_tests, release_readiness, risk_heatmap_folder, risk_heatmap_feature, " +
|
|
754
|
-
"testcase_quality, predictive_failure, smart_prioritization, tester_effectiveness, " +
|
|
755
|
-
"stale_tests, defect_hotspots, cycle_health, suite_optimization, " +
|
|
756
|
-
"requirement_risk_matrix, execution_velocity, project_health.",
|
|
690
|
+
description: "Project reports (insights use no AI credits).",
|
|
757
691
|
annotations: { readOnlyHint: true },
|
|
758
692
|
inputSchema: {
|
|
759
693
|
type: "object",
|
|
760
694
|
properties: {
|
|
761
|
-
projectId:
|
|
762
|
-
report_type: {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
},
|
|
770
|
-
|
|
771
|
-
type: "string",
|
|
772
|
-
description: "Only this test plan (one ID, e.g. TPLAN-2)",
|
|
773
|
-
},
|
|
774
|
-
folder_id: { type: "number", description: "Only this folder" },
|
|
775
|
-
startDate: { type: "string", description: "Start date filter (YYYY-MM-DD)" },
|
|
776
|
-
endDate: { type: "string", description: "End date filter (YYYY-MM-DD)" },
|
|
777
|
-
granularity: {
|
|
778
|
-
type: "string",
|
|
779
|
-
enum: ["day", "week", "month"],
|
|
780
|
-
description: "Granularity for trend reports",
|
|
781
|
-
},
|
|
782
|
-
// cycle_comparison specific
|
|
783
|
-
cycle_ids: {
|
|
784
|
-
type: "string",
|
|
785
|
-
description: "[cycle_comparison] Comma-separated cycle IDs to compare",
|
|
786
|
-
},
|
|
695
|
+
projectId: S,
|
|
696
|
+
report_type: { type: "string", enum: Object.keys(REPORT_TYPE_MAP) },
|
|
697
|
+
test_cycle_id: { type: "string", description: "one id" },
|
|
698
|
+
test_plan_id: { type: "string", description: "one id" },
|
|
699
|
+
folder_id: N,
|
|
700
|
+
startDate: { type: "string", description: "YYYY-MM-DD" },
|
|
701
|
+
endDate: { type: "string", description: "YYYY-MM-DD, inclusive" },
|
|
702
|
+
granularity: { type: "string", enum: ["day", "week", "month"] },
|
|
703
|
+
cycle_ids: { type: "string", description: "cycle_comparison" },
|
|
704
|
+
verbose: VERBOSE,
|
|
787
705
|
},
|
|
788
706
|
required: ["projectId", "report_type"],
|
|
789
707
|
},
|
|
790
708
|
};
|
|
791
709
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
-
// REPORT TYPE → ENDPOINT MAPPING
|
|
793
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
794
|
-
const REPORT_TYPE_MAP = {
|
|
795
|
-
// Execution
|
|
796
|
-
execution_summary: "execution/summary",
|
|
797
|
-
execution_by_cycle: "execution/by-cycle",
|
|
798
|
-
execution_by_tester: "execution/by-tester",
|
|
799
|
-
execution_by_priority: "execution/by-priority",
|
|
800
|
-
execution_by_environment: "execution/by-environment",
|
|
801
|
-
execution_by_folder: "execution/by-folder",
|
|
802
|
-
// Coverage
|
|
803
|
-
requirement_coverage: "coverage/requirement-coverage",
|
|
804
|
-
traceability_matrix: "coverage/traceability-matrix",
|
|
805
|
-
failed_requirements: "coverage/failed-requirements",
|
|
806
|
-
uncovered_requirements: "coverage/uncovered-requirements",
|
|
807
|
-
testcase_coverage: "coverage/testcase-coverage",
|
|
808
|
-
unlinked_testcases: "coverage/unlinked-testcases",
|
|
809
|
-
// Defects
|
|
810
|
-
defects_by_folder: "defects/by-folder",
|
|
811
|
-
// Team
|
|
812
|
-
tester_workload: "team/tester-workload",
|
|
813
|
-
testcase_distribution: "team/testcase-distribution",
|
|
814
|
-
// Trends
|
|
815
|
-
execution_trend: "trends/execution-trend",
|
|
816
|
-
execution_burnup: "trends/execution-burnup",
|
|
817
|
-
execution_burndown: "trends/execution-burndown",
|
|
818
|
-
test_creation: "trends/test-creation",
|
|
819
|
-
cycle_comparison: "trends/cycle-comparison",
|
|
820
|
-
scorecard_by_folder: "trends/scorecard-by-folder",
|
|
821
|
-
scorecard_by_tester: "trends/scorecard-by-tester",
|
|
822
|
-
created_vs_executed: "trends/created-vs-executed",
|
|
823
|
-
execution_by_automation: "trends/execution-by-automation",
|
|
824
|
-
defects_by_cycle: "trends/defects-by-cycle",
|
|
825
|
-
defects_by_tester: "trends/defects-by-tester",
|
|
826
|
-
requirement_coverage_trend: "trends/requirement-coverage-trend",
|
|
827
|
-
// AI Insights
|
|
828
|
-
flaky_tests: "ai/flaky-tests",
|
|
829
|
-
release_readiness: "ai/release-readiness",
|
|
830
|
-
risk_heatmap_folder: "ai/risk-heatmap-folder",
|
|
831
|
-
risk_heatmap_feature: "ai/risk-heatmap-feature",
|
|
832
|
-
testcase_quality: "ai/testcase-quality",
|
|
833
|
-
predictive_failure: "ai/predictive-failure",
|
|
834
|
-
smart_prioritization: "ai/smart-prioritization",
|
|
835
|
-
tester_effectiveness: "ai/tester-effectiveness",
|
|
836
|
-
stale_tests: "ai/stale-tests",
|
|
837
|
-
defect_hotspots: "ai/defect-hotspots",
|
|
838
|
-
cycle_health: "ai/cycle-health",
|
|
839
|
-
suite_optimization: "ai/suite-optimization",
|
|
840
|
-
requirement_risk_matrix: "ai/requirement-risk-matrix",
|
|
841
|
-
execution_velocity: "ai/execution-velocity",
|
|
842
|
-
project_health: "ai/project-health",
|
|
843
|
-
};
|
|
844
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
845
710
|
// TOOL HANDLERS
|
|
846
711
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
847
712
|
// 1. list_projects
|
|
@@ -852,9 +717,7 @@ async function handleListProjects(args) {
|
|
|
852
717
|
limit: p(args, "limit"),
|
|
853
718
|
},
|
|
854
719
|
});
|
|
855
|
-
return
|
|
856
|
-
"Next: call get_project_structure(projectId) to see folders, labels, and allowed field values.",
|
|
857
|
-
]);
|
|
720
|
+
return listResponse(args, data, LIST_KEYS.projects);
|
|
858
721
|
}
|
|
859
722
|
// 2. get_project_structure
|
|
860
723
|
async function handleGetProjectStructure(args) {
|
|
@@ -891,9 +754,34 @@ async function handleGetProjectStructure(args) {
|
|
|
891
754
|
}));
|
|
892
755
|
}
|
|
893
756
|
await Promise.all(promises);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
757
|
+
if (p(args, "verbose"))
|
|
758
|
+
return jsonResponse(result, true);
|
|
759
|
+
const unwrap = (d) => (d && typeof d === "object" && !Array.isArray(d) && "data" in d ? d.data : d);
|
|
760
|
+
const shaped = {};
|
|
761
|
+
if (result.folders) {
|
|
762
|
+
const f = unwrap(result.folders);
|
|
763
|
+
shaped.folders = flattenFolders(Array.isArray(f) ? f : f?.folders ?? []);
|
|
764
|
+
}
|
|
765
|
+
if (result.labels) {
|
|
766
|
+
const l = unwrap(result.labels);
|
|
767
|
+
shaped.labels = (Array.isArray(l) ? l : []).map((x) => x?.labels ?? x?.name ?? x);
|
|
768
|
+
}
|
|
769
|
+
if (result.members) {
|
|
770
|
+
const m = unwrap(result.members);
|
|
771
|
+
shaped.members = (Array.isArray(m) ? m : m?.team_members ?? m?.members ?? []).map(memberRow);
|
|
772
|
+
}
|
|
773
|
+
if (result.field_options) {
|
|
774
|
+
shaped.field_options = {};
|
|
775
|
+
for (const f of Array.isArray(result.field_options) ? result.field_options : []) {
|
|
776
|
+
// List fields: allowed values. Custom free-text fields: their type, so the AI knows the custom_fields name.
|
|
777
|
+
if (Array.isArray(f?.values) && f.values.length)
|
|
778
|
+
shaped.field_options[f.fieldName] = f.values;
|
|
779
|
+
else if (f?.isSystem === false)
|
|
780
|
+
shaped.field_options[f.fieldName] = f.fieldType;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Not compacted: an empty section must read as "none" (folders: []), not vanish
|
|
784
|
+
return { content: [{ type: "text", text: JSON.stringify(shaped) }] };
|
|
897
785
|
}
|
|
898
786
|
// 3. search_testcases
|
|
899
787
|
async function handleSearchTestcases(args) {
|
|
@@ -925,7 +813,22 @@ async function handleSearchTestcases(args) {
|
|
|
925
813
|
tc.url = `${baseUrl}/${projectId}/testcases/${tc.id}`;
|
|
926
814
|
});
|
|
927
815
|
}
|
|
928
|
-
return
|
|
816
|
+
return listResponse(args, data, LIST_KEYS.testcases, "id");
|
|
817
|
+
}
|
|
818
|
+
// Keep the useful, shareable fields; drop internal ones (s3_path, uploaded_by, status)
|
|
819
|
+
function shapeAttachments(list) {
|
|
820
|
+
return (Array.isArray(list) ? list : []).map((a) => {
|
|
821
|
+
const s3 = typeof a?.s3_path === "string" ? a.s3_path : "";
|
|
822
|
+
const filename = (s3 ? s3.split("/").pop() : undefined) || a?.filename || a?.name;
|
|
823
|
+
return {
|
|
824
|
+
attachment_id: a?.attachment_id ?? a?.id,
|
|
825
|
+
filename,
|
|
826
|
+
size: a?.size,
|
|
827
|
+
uploaded_at: a?.uploaded_at,
|
|
828
|
+
viewUrl: a?.viewUrl,
|
|
829
|
+
downloadUrl: a?.downloadUrl,
|
|
830
|
+
};
|
|
831
|
+
});
|
|
929
832
|
}
|
|
930
833
|
// 4. get_testcase
|
|
931
834
|
async function handleGetTestcase(args) {
|
|
@@ -940,8 +843,22 @@ async function handleGetTestcase(args) {
|
|
|
940
843
|
const data = await callApi("get", `projects/testcases/get-testcase-detail/${projectId}/${testcaseId}`);
|
|
941
844
|
if (data?.data?.testcase) {
|
|
942
845
|
data.data.testcase.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${testcaseId}`;
|
|
846
|
+
// Attachments live on a separate endpoint; fold them in so the agent gets shareable
|
|
847
|
+
// presigned links (view/download) without the test-case read ever failing on their account.
|
|
848
|
+
try {
|
|
849
|
+
const orgId = await getOrganizationId();
|
|
850
|
+
const att = await callApi("get", `attachments/testcase/${orgId}/${projectId}`, {
|
|
851
|
+
query: { target_id: testcaseId },
|
|
852
|
+
});
|
|
853
|
+
data.data.testcase.attachments = shapeAttachments(att?.attachments ?? att?.data?.attachments);
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
data.data.testcase.attachments = [];
|
|
857
|
+
data.data.testcase.attachments_note =
|
|
858
|
+
"Could not load attachments for this test case (the case itself loaded fine). Re-run get_testcase to retry.";
|
|
859
|
+
}
|
|
943
860
|
}
|
|
944
|
-
return jsonResponse(data);
|
|
861
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
945
862
|
}
|
|
946
863
|
// 5. manage_testcase
|
|
947
864
|
async function handleManageTestcase(args) {
|
|
@@ -963,9 +880,7 @@ async function handleManageTestcase(args) {
|
|
|
963
880
|
const testcaseData = await buildTestcasePayload(projectId, { ...args, title });
|
|
964
881
|
const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
|
|
965
882
|
const tcId = createResult?.testcaseId || createResult?.data?.id;
|
|
966
|
-
const hints = [
|
|
967
|
-
"To update fields, use manage_testcase(action='update', ids='<id>', field='<name>', value='<value>')",
|
|
968
|
-
];
|
|
883
|
+
const hints = [];
|
|
969
884
|
if (tcId) {
|
|
970
885
|
createResult.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${tcId}`;
|
|
971
886
|
if (testSteps?.length) {
|
|
@@ -987,9 +902,14 @@ async function handleManageTestcase(args) {
|
|
|
987
902
|
if (!Array.isArray(testcases) || testcases.length === 0) {
|
|
988
903
|
throw new Error("testcases must be a non-empty JSON array");
|
|
989
904
|
}
|
|
905
|
+
const shared = {};
|
|
906
|
+
for (const f of BULK_SHARED_FIELDS)
|
|
907
|
+
if (args[f] !== undefined && args[f] !== null && args[f] !== "")
|
|
908
|
+
shared[f] = args[f];
|
|
990
909
|
// Validate everything before creating anything
|
|
991
910
|
const items = [];
|
|
992
|
-
for (const [i,
|
|
911
|
+
for (const [i, item] of testcases.entries()) {
|
|
912
|
+
const tc = item && typeof item === "object" ? { ...shared, ...item } : item;
|
|
993
913
|
if (!tc?.title && !tc?.name)
|
|
994
914
|
throw new Error(`testcases[${i}]: title is required`);
|
|
995
915
|
try {
|
|
@@ -1114,7 +1034,7 @@ async function handleManageTestcase(args) {
|
|
|
1114
1034
|
query: { projectId },
|
|
1115
1035
|
body: { stepIds },
|
|
1116
1036
|
});
|
|
1117
|
-
return jsonResponse(data);
|
|
1037
|
+
return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
|
|
1118
1038
|
}
|
|
1119
1039
|
case "reorder_steps": {
|
|
1120
1040
|
const tcId = validateId(requireArg(args, "testcaseId", ["testcase_id", "id"]), "testcaseId");
|
|
@@ -1124,7 +1044,7 @@ async function handleManageTestcase(args) {
|
|
|
1124
1044
|
const data = await callApi("put", `projects/testcases/teststeps/${projectId}/${tcId}/reorder`, {
|
|
1125
1045
|
body: { stepIds },
|
|
1126
1046
|
});
|
|
1127
|
-
return jsonResponse(data);
|
|
1047
|
+
return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
|
|
1128
1048
|
}
|
|
1129
1049
|
default:
|
|
1130
1050
|
throw new Error(`Unknown action: ${action}. Use: create, create_bulk, update, delete, add_steps, update_step, delete_steps, reorder_steps`);
|
|
@@ -1209,7 +1129,7 @@ async function handleSearchTestCycles(args) {
|
|
|
1209
1129
|
validateId(projectId, "projectId");
|
|
1210
1130
|
const folderId = p(args, "folderId", "folder_id");
|
|
1211
1131
|
const filters = buildFilters(undefined, {
|
|
1212
|
-
status: p(args, "status"),
|
|
1132
|
+
status: normalizePlanStatus(p(args, "status")),
|
|
1213
1133
|
folder_id: folderId === undefined || folderId === null || folderId === "" ? undefined : Number(folderId),
|
|
1214
1134
|
});
|
|
1215
1135
|
const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
|
|
@@ -1228,7 +1148,7 @@ async function handleSearchTestCycles(args) {
|
|
|
1228
1148
|
cycle.url = `${baseUrl}/${projectId}/testcycles/${id}`;
|
|
1229
1149
|
});
|
|
1230
1150
|
}
|
|
1231
|
-
return
|
|
1151
|
+
return listResponse(args, data, LIST_KEYS.cycles, "test_cycle_id");
|
|
1232
1152
|
}
|
|
1233
1153
|
// 8. manage_test_cycle
|
|
1234
1154
|
async function handleManageTestCycle(args) {
|
|
@@ -1248,7 +1168,7 @@ async function handleManageTestCycle(args) {
|
|
|
1248
1168
|
if (p(args, "summary"))
|
|
1249
1169
|
body.summary = p(args, "summary");
|
|
1250
1170
|
if (p(args, "status"))
|
|
1251
|
-
body.status = p(args, "status");
|
|
1171
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1252
1172
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1253
1173
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1254
1174
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1268,7 +1188,7 @@ async function handleManageTestCycle(args) {
|
|
|
1268
1188
|
if (p(args, "summary"))
|
|
1269
1189
|
body.summary = p(args, "summary");
|
|
1270
1190
|
if (p(args, "status"))
|
|
1271
|
-
body.status = p(args, "status");
|
|
1191
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1272
1192
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1273
1193
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1274
1194
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1295,7 +1215,7 @@ async function handleManageTestCycle(args) {
|
|
|
1295
1215
|
if (data?.data) {
|
|
1296
1216
|
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testcycles/${cycleId}`;
|
|
1297
1217
|
}
|
|
1298
|
-
return jsonResponse(data);
|
|
1218
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1299
1219
|
}
|
|
1300
1220
|
case "get_testcases": {
|
|
1301
1221
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
@@ -1309,7 +1229,7 @@ async function handleManageTestCycle(args) {
|
|
|
1309
1229
|
limit: p(args, "limit"),
|
|
1310
1230
|
},
|
|
1311
1231
|
});
|
|
1312
|
-
return
|
|
1232
|
+
return listResponse(args, data, LIST_KEYS.linkedCases);
|
|
1313
1233
|
}
|
|
1314
1234
|
case "link_testcases": {
|
|
1315
1235
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
@@ -1367,7 +1287,8 @@ async function handleExecuteTests(args) {
|
|
|
1367
1287
|
throw new Error("results is required");
|
|
1368
1288
|
validateId(projectId, "projectId");
|
|
1369
1289
|
validateId(cycleId, "cycleId");
|
|
1370
|
-
const
|
|
1290
|
+
const parsedResults = safeJsonParse(resultsRaw, "results");
|
|
1291
|
+
const results = parsedResults && !Array.isArray(parsedResults) && typeof parsedResults === "object" ? [parsedResults] : parsedResults;
|
|
1371
1292
|
if (!Array.isArray(results) || results.length === 0) {
|
|
1372
1293
|
throw new Error("results must be a non-empty array");
|
|
1373
1294
|
}
|
|
@@ -1386,7 +1307,7 @@ async function handleExecuteTests(args) {
|
|
|
1386
1307
|
}
|
|
1387
1308
|
const out = {
|
|
1388
1309
|
testcase_id: validateId(testcaseId, "testcase_id"),
|
|
1389
|
-
execution_status: status,
|
|
1310
|
+
execution_status: normalizeExecutionStatus(status, `results[${i}]`),
|
|
1390
1311
|
};
|
|
1391
1312
|
const actual = r.actual_result || r.actualResult;
|
|
1392
1313
|
if (actual)
|
|
@@ -1438,7 +1359,7 @@ async function handleManageTestPlan(args) {
|
|
|
1438
1359
|
if (p(args, "summary"))
|
|
1439
1360
|
body.summary = p(args, "summary");
|
|
1440
1361
|
if (p(args, "status"))
|
|
1441
|
-
body.status = p(args, "status");
|
|
1362
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1442
1363
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1443
1364
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1444
1365
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1458,7 +1379,7 @@ async function handleManageTestPlan(args) {
|
|
|
1458
1379
|
if (p(args, "summary"))
|
|
1459
1380
|
body.summary = p(args, "summary");
|
|
1460
1381
|
if (p(args, "status"))
|
|
1461
|
-
body.status = p(args, "status");
|
|
1382
|
+
body.status = normalizePlanStatus(p(args, "status"));
|
|
1462
1383
|
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1463
1384
|
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1464
1385
|
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
@@ -1492,7 +1413,7 @@ async function handleManageTestPlan(args) {
|
|
|
1492
1413
|
plan.url = `${baseUrl}/${projectId}/testplans/${id}`;
|
|
1493
1414
|
});
|
|
1494
1415
|
}
|
|
1495
|
-
return
|
|
1416
|
+
return listResponse(args, data, LIST_KEYS.plans, "test_plan_id");
|
|
1496
1417
|
}
|
|
1497
1418
|
case "get_details": {
|
|
1498
1419
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1503,7 +1424,7 @@ async function handleManageTestPlan(args) {
|
|
|
1503
1424
|
if (data?.data) {
|
|
1504
1425
|
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testplans/${planId}`;
|
|
1505
1426
|
}
|
|
1506
|
-
return jsonResponse(data);
|
|
1427
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1507
1428
|
}
|
|
1508
1429
|
case "link_cycles": {
|
|
1509
1430
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1541,7 +1462,7 @@ async function handleManageTestPlan(args) {
|
|
|
1541
1462
|
limit: p(args, "limit"),
|
|
1542
1463
|
},
|
|
1543
1464
|
});
|
|
1544
|
-
return
|
|
1465
|
+
return listResponse(args, data, LIST_KEYS.cycles);
|
|
1545
1466
|
}
|
|
1546
1467
|
case "get_testcases": {
|
|
1547
1468
|
const planId = p(args, "planId", "plan_id", "id");
|
|
@@ -1555,7 +1476,7 @@ async function handleManageTestPlan(args) {
|
|
|
1555
1476
|
limit: p(args, "limit"),
|
|
1556
1477
|
},
|
|
1557
1478
|
});
|
|
1558
|
-
return
|
|
1479
|
+
return listResponse(args, data, LIST_KEYS.linkedCases);
|
|
1559
1480
|
}
|
|
1560
1481
|
default:
|
|
1561
1482
|
throw new Error(`Unknown action: ${action}. Use: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases`);
|
|
@@ -1603,7 +1524,7 @@ async function handleGetReport(args) {
|
|
|
1603
1524
|
const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
|
|
1604
1525
|
query,
|
|
1605
1526
|
});
|
|
1606
|
-
return jsonResponse(data);
|
|
1527
|
+
return jsonResponse(data, !!p(args, "verbose"));
|
|
1607
1528
|
}
|
|
1608
1529
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1609
1530
|
// SERVER SETUP
|