@testkase/test-management-mcp 2.4.0 → 2.5.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/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.4.0";
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 response = await axios({
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
- function jsonResponse(data) {
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, null, 2) }],
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
- throw new Error(`Invalid JSON in ${fieldName}. Ensure the value is valid JSON.`);
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 json = JSON.stringify(data, null, 2);
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: json + hintsBlock }],
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: "List all projects the authenticated user has access to. " +
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: "Get the structure of a project: folders, labels, team members, and field options (allowed values for priority, status, etc.). " +
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: { type: "string", description: "Project ID (e.g. PRJ-1001)" },
367
- section: {
368
- type: "string",
369
- enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
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 and list test cases in a project. Supports text search, folder filter, filters, sorting, and pagination.",
510
+ description: "Search/list test cases.",
383
511
  annotations: { readOnlyHint: true },
384
512
  inputSchema: {
385
513
  type: "object",
386
514
  properties: {
387
- projectId: { type: "string", description: "Project ID" },
388
- search: { type: "string", description: "Search text (ID, title, summary, field values)" },
389
- folder_id: {
390
- type: "number",
391
- description: "Only test cases in this folder (sub-folders included). Use 0 for test cases with no folder.",
392
- },
393
- filters: {
394
- type: "string",
395
- description: 'JSON object of filters; every value is a list. Keys are field names (use the field_options names). ' +
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: "Get full details of a single test case including test steps (with step ids) and attachments.",
530
+ description: "One test case with steps (step ids) and attachments.",
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: "IMPORTANT: Before creating/updating, call get_project_structure(projectId) to discover folder IDs and allowed field values.\n\n" +
423
- "Create, update or delete test cases and their test steps.\n\n" +
424
- "ACTIONS:\n" +
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: { type: "string", description: "Project ID" },
444
- action: {
445
- type: "string",
446
- enum: ["create", "create_bulk", "update", "delete", "add_steps", "update_step", "delete_steps", "reorder_steps"],
447
- description: "Action to perform",
448
- },
449
- // create params
450
- title: { type: "string", description: "[create] Test case title" },
451
- summary: { type: "string", description: "[create] Description/summary" },
452
- priority: {
453
- type: "string",
454
- description: "[create] Priority. Values are project-specific — call get_project_structure(projectId, include='field_options') first to discover allowed values.",
455
- },
456
- folder_id: { type: "number", description: "[create] Folder ID to place the test case in" },
457
- labels: {
458
- type: "string",
459
- description: "[create] Comma-separated label names",
460
- },
461
- test_steps: {
462
- type: "string",
463
- description: '[create/add_steps] JSON array of test steps: [{"description":"...","test_data":"...","expected_result":"..."}]. Every step needs a description.',
464
- },
465
- precondition: { type: "string", description: "[create] Preconditions for the test case" },
466
- status: {
467
- type: "string",
468
- description: "[create] Status. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
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: "Create, rename, move, or delete folders. Works across all sections (TESTCASE, TEST_CYCLE, TEST_PLAN).\n\n" +
516
- "ACTIONS:\n" +
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: { type: "string", description: "Project ID" },
527
- action: {
528
- type: "string",
529
- enum: ["create", "rename", "move", "delete"],
530
- description: "Action to perform",
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: "List and search test cycles in a project with execution progress.",
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: "Manage test cycles: create, update, delete, get details, list/link/unlink/assign test cases.\n\n" +
570
- "ACTIONS:\n" +
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: { type: "string", description: "Project ID" },
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)",
613
+ enum: ["create", "update", "delete", "get_details", "get_testcases", "link_testcases", "unlink_testcases", "assign_testcases"],
615
614
  },
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",
628
- },
629
- // link/unlink/assign
630
- testcase_ids: {
631
- type: "string",
632
- description: "[link_testcases/unlink_testcases/assign_testcases] Comma-separated test case IDs",
633
- },
634
- assignee_id: {
635
- type: "number",
636
- description: "[assign_testcases] User ID to assign",
637
- },
638
- // get_testcases params
639
- search: { type: "string", description: "[get_testcases] Search text" },
640
- page: { type: "number", description: "[get_testcases] Page number" },
641
- limit: { type: "number", description: "[get_testcases] Items per page" },
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 test execution results for test cases in a test cycle. " +
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: { type: "string", description: "Project ID" },
656
- cycleId: { type: "string", description: "Test cycle ID (e.g. TCYCLE-1)" },
639
+ projectId: S,
640
+ cycleId: S,
657
641
  results: {
658
- type: "string",
659
- description: 'JSON array of results: [{"testcase_id":"TEST-1","execution_status":"pass","actual_result":"Worked","environment":"staging"}]. ' +
660
- "Each object must have testcase_id and execution_status. actual_result and environment are optional.",
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: "manual (default): each result is recorded like a tester setting it in the app — the new status replaces the old one. " +
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: "Manage test plans: create, update, delete, list, get details, link/unlink cycles, get cycles, get test cases.\n\n" +
675
- "ACTIONS:\n" +
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: { type: "string", description: "Project ID" },
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.",
710
- },
711
- // create/update fields
712
- title: { type: "string", description: "[create/update] Plan title" },
713
- summary: { type: "string", description: "[create/update] Description" },
714
- status: { type: "string", enum: ["open", "in_progress", "completed"], description: "[create/update] Status" },
715
- planned_start_date: {
716
- type: "string",
717
- description: "[create/update] Start date (YYYY-MM-DD)",
669
+ enum: ["create", "update", "delete", "list", "get_details", "link_cycles", "unlink_cycles", "get_cycles", "get_testcases"],
718
670
  },
719
- planned_end_date: {
720
- type: "string",
721
- description: "[create/update] End date (YYYY-MM-DD)",
722
- },
723
- folder_id: { type: "number", description: "[create/update] Folder ID" },
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: "Get reports and analytics for a project. Covers execution, coverage, trends, team, defects, and insights " +
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: { type: "string", description: "Project ID" },
762
- report_type: {
763
- type: "string",
764
- description: "Report type (see description for full list)",
765
- },
766
- test_cycle_id: {
767
- type: "string",
768
- description: "Only this test cycle (one ID, e.g. TCYCLE-3)",
769
- },
770
- test_plan_id: {
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 jsonResponseWithHints(data, [
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
- return jsonResponseWithHints(result, [
895
- "Use the field_options values above when creating/updating test cases.",
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,7 @@ async function handleSearchTestcases(args) {
925
813
  tc.url = `${baseUrl}/${projectId}/testcases/${tc.id}`;
926
814
  });
927
815
  }
928
- return jsonResponse(data);
816
+ return listResponse(args, data, LIST_KEYS.testcases, "id");
929
817
  }
930
818
  // 4. get_testcase
931
819
  async function handleGetTestcase(args) {
@@ -941,7 +829,7 @@ async function handleGetTestcase(args) {
941
829
  if (data?.data?.testcase) {
942
830
  data.data.testcase.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${testcaseId}`;
943
831
  }
944
- return jsonResponse(data);
832
+ return jsonResponse(data, !!p(args, "verbose"));
945
833
  }
946
834
  // 5. manage_testcase
947
835
  async function handleManageTestcase(args) {
@@ -963,9 +851,7 @@ async function handleManageTestcase(args) {
963
851
  const testcaseData = await buildTestcasePayload(projectId, { ...args, title });
964
852
  const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
965
853
  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
- ];
854
+ const hints = [];
969
855
  if (tcId) {
970
856
  createResult.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${tcId}`;
971
857
  if (testSteps?.length) {
@@ -987,9 +873,14 @@ async function handleManageTestcase(args) {
987
873
  if (!Array.isArray(testcases) || testcases.length === 0) {
988
874
  throw new Error("testcases must be a non-empty JSON array");
989
875
  }
876
+ const shared = {};
877
+ for (const f of BULK_SHARED_FIELDS)
878
+ if (args[f] !== undefined && args[f] !== null && args[f] !== "")
879
+ shared[f] = args[f];
990
880
  // Validate everything before creating anything
991
881
  const items = [];
992
- for (const [i, tc] of testcases.entries()) {
882
+ for (const [i, item] of testcases.entries()) {
883
+ const tc = item && typeof item === "object" ? { ...shared, ...item } : item;
993
884
  if (!tc?.title && !tc?.name)
994
885
  throw new Error(`testcases[${i}]: title is required`);
995
886
  try {
@@ -1114,7 +1005,7 @@ async function handleManageTestcase(args) {
1114
1005
  query: { projectId },
1115
1006
  body: { stepIds },
1116
1007
  });
1117
- return jsonResponse(data);
1008
+ return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
1118
1009
  }
1119
1010
  case "reorder_steps": {
1120
1011
  const tcId = validateId(requireArg(args, "testcaseId", ["testcase_id", "id"]), "testcaseId");
@@ -1124,7 +1015,7 @@ async function handleManageTestcase(args) {
1124
1015
  const data = await callApi("put", `projects/testcases/teststeps/${projectId}/${tcId}/reorder`, {
1125
1016
  body: { stepIds },
1126
1017
  });
1127
- return jsonResponse(data);
1018
+ return jsonResponse({ ...compact(data), steps: await currentSteps(projectId, tcId) });
1128
1019
  }
1129
1020
  default:
1130
1021
  throw new Error(`Unknown action: ${action}. Use: create, create_bulk, update, delete, add_steps, update_step, delete_steps, reorder_steps`);
@@ -1209,7 +1100,7 @@ async function handleSearchTestCycles(args) {
1209
1100
  validateId(projectId, "projectId");
1210
1101
  const folderId = p(args, "folderId", "folder_id");
1211
1102
  const filters = buildFilters(undefined, {
1212
- status: p(args, "status"),
1103
+ status: normalizePlanStatus(p(args, "status")),
1213
1104
  folder_id: folderId === undefined || folderId === null || folderId === "" ? undefined : Number(folderId),
1214
1105
  });
1215
1106
  const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
@@ -1228,7 +1119,7 @@ async function handleSearchTestCycles(args) {
1228
1119
  cycle.url = `${baseUrl}/${projectId}/testcycles/${id}`;
1229
1120
  });
1230
1121
  }
1231
- return jsonResponse(data);
1122
+ return listResponse(args, data, LIST_KEYS.cycles, "test_cycle_id");
1232
1123
  }
1233
1124
  // 8. manage_test_cycle
1234
1125
  async function handleManageTestCycle(args) {
@@ -1248,7 +1139,7 @@ async function handleManageTestCycle(args) {
1248
1139
  if (p(args, "summary"))
1249
1140
  body.summary = p(args, "summary");
1250
1141
  if (p(args, "status"))
1251
- body.status = p(args, "status");
1142
+ body.status = normalizePlanStatus(p(args, "status"));
1252
1143
  if (p(args, "planned_start_date", "plannedStartDate"))
1253
1144
  body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
1254
1145
  if (p(args, "planned_end_date", "plannedEndDate"))
@@ -1268,7 +1159,7 @@ async function handleManageTestCycle(args) {
1268
1159
  if (p(args, "summary"))
1269
1160
  body.summary = p(args, "summary");
1270
1161
  if (p(args, "status"))
1271
- body.status = p(args, "status");
1162
+ body.status = normalizePlanStatus(p(args, "status"));
1272
1163
  if (p(args, "planned_start_date", "plannedStartDate"))
1273
1164
  body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
1274
1165
  if (p(args, "planned_end_date", "plannedEndDate"))
@@ -1295,7 +1186,7 @@ async function handleManageTestCycle(args) {
1295
1186
  if (data?.data) {
1296
1187
  data.data.url = `${getFrontendBaseUrl()}/${projectId}/testcycles/${cycleId}`;
1297
1188
  }
1298
- return jsonResponse(data);
1189
+ return jsonResponse(data, !!p(args, "verbose"));
1299
1190
  }
1300
1191
  case "get_testcases": {
1301
1192
  const cycleId = p(args, "cycleId", "cycle_id", "id");
@@ -1309,7 +1200,7 @@ async function handleManageTestCycle(args) {
1309
1200
  limit: p(args, "limit"),
1310
1201
  },
1311
1202
  });
1312
- return jsonResponse(data);
1203
+ return listResponse(args, data, LIST_KEYS.linkedCases);
1313
1204
  }
1314
1205
  case "link_testcases": {
1315
1206
  const cycleId = p(args, "cycleId", "cycle_id", "id");
@@ -1367,7 +1258,8 @@ async function handleExecuteTests(args) {
1367
1258
  throw new Error("results is required");
1368
1259
  validateId(projectId, "projectId");
1369
1260
  validateId(cycleId, "cycleId");
1370
- const results = safeJsonParse(resultsRaw, "results");
1261
+ const parsedResults = safeJsonParse(resultsRaw, "results");
1262
+ const results = parsedResults && !Array.isArray(parsedResults) && typeof parsedResults === "object" ? [parsedResults] : parsedResults;
1371
1263
  if (!Array.isArray(results) || results.length === 0) {
1372
1264
  throw new Error("results must be a non-empty array");
1373
1265
  }
@@ -1386,7 +1278,7 @@ async function handleExecuteTests(args) {
1386
1278
  }
1387
1279
  const out = {
1388
1280
  testcase_id: validateId(testcaseId, "testcase_id"),
1389
- execution_status: status,
1281
+ execution_status: normalizeExecutionStatus(status, `results[${i}]`),
1390
1282
  };
1391
1283
  const actual = r.actual_result || r.actualResult;
1392
1284
  if (actual)
@@ -1438,7 +1330,7 @@ async function handleManageTestPlan(args) {
1438
1330
  if (p(args, "summary"))
1439
1331
  body.summary = p(args, "summary");
1440
1332
  if (p(args, "status"))
1441
- body.status = p(args, "status");
1333
+ body.status = normalizePlanStatus(p(args, "status"));
1442
1334
  if (p(args, "planned_start_date", "plannedStartDate"))
1443
1335
  body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
1444
1336
  if (p(args, "planned_end_date", "plannedEndDate"))
@@ -1458,7 +1350,7 @@ async function handleManageTestPlan(args) {
1458
1350
  if (p(args, "summary"))
1459
1351
  body.summary = p(args, "summary");
1460
1352
  if (p(args, "status"))
1461
- body.status = p(args, "status");
1353
+ body.status = normalizePlanStatus(p(args, "status"));
1462
1354
  if (p(args, "planned_start_date", "plannedStartDate"))
1463
1355
  body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
1464
1356
  if (p(args, "planned_end_date", "plannedEndDate"))
@@ -1492,7 +1384,7 @@ async function handleManageTestPlan(args) {
1492
1384
  plan.url = `${baseUrl}/${projectId}/testplans/${id}`;
1493
1385
  });
1494
1386
  }
1495
- return jsonResponse(data);
1387
+ return listResponse(args, data, LIST_KEYS.plans, "test_plan_id");
1496
1388
  }
1497
1389
  case "get_details": {
1498
1390
  const planId = p(args, "planId", "plan_id", "id");
@@ -1503,7 +1395,7 @@ async function handleManageTestPlan(args) {
1503
1395
  if (data?.data) {
1504
1396
  data.data.url = `${getFrontendBaseUrl()}/${projectId}/testplans/${planId}`;
1505
1397
  }
1506
- return jsonResponse(data);
1398
+ return jsonResponse(data, !!p(args, "verbose"));
1507
1399
  }
1508
1400
  case "link_cycles": {
1509
1401
  const planId = p(args, "planId", "plan_id", "id");
@@ -1541,7 +1433,7 @@ async function handleManageTestPlan(args) {
1541
1433
  limit: p(args, "limit"),
1542
1434
  },
1543
1435
  });
1544
- return jsonResponse(data);
1436
+ return listResponse(args, data, LIST_KEYS.cycles);
1545
1437
  }
1546
1438
  case "get_testcases": {
1547
1439
  const planId = p(args, "planId", "plan_id", "id");
@@ -1555,7 +1447,7 @@ async function handleManageTestPlan(args) {
1555
1447
  limit: p(args, "limit"),
1556
1448
  },
1557
1449
  });
1558
- return jsonResponse(data);
1450
+ return listResponse(args, data, LIST_KEYS.linkedCases);
1559
1451
  }
1560
1452
  default:
1561
1453
  throw new Error(`Unknown action: ${action}. Use: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases`);
@@ -1603,7 +1495,7 @@ async function handleGetReport(args) {
1603
1495
  const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
1604
1496
  query,
1605
1497
  });
1606
- return jsonResponse(data);
1498
+ return jsonResponse(data, !!p(args, "verbose"));
1607
1499
  }
1608
1500
  // ═══════════════════════════════════════════════════════════════════════════════
1609
1501
  // SERVER SETUP