@qa-gentic/agents 1.1.2

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.
Files changed (52) hide show
  1. package/README.md +203 -0
  2. package/bin/postinstall.js +75 -0
  3. package/bin/qa-stlc.js +76 -0
  4. package/package.json +48 -0
  5. package/skills/qa-stlc/AGENT-BEHAVIOR.md +373 -0
  6. package/skills/qa-stlc/deduplication-protocol.md +303 -0
  7. package/skills/qa-stlc/generate-gherkin.md +550 -0
  8. package/skills/qa-stlc/generate-playwright-code.md +439 -0
  9. package/skills/qa-stlc/generate-test-cases.md +176 -0
  10. package/skills/qa-stlc/write-helix-files.md +349 -0
  11. package/src/cmd-init.js +84 -0
  12. package/src/cmd-mcp-config.js +177 -0
  13. package/src/cmd-skills.js +124 -0
  14. package/src/cmd-verify.js +129 -0
  15. package/src/qa_stlc_agents/__init__.py +0 -0
  16. package/src/qa_stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
  17. package/src/qa_stlc_agents/agent_gherkin_generator/__init__.py +0 -0
  18. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  19. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
  20. package/src/qa_stlc_agents/agent_gherkin_generator/server.py +502 -0
  21. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__init__.py +0 -0
  22. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  23. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
  24. package/src/qa_stlc_agents/agent_gherkin_generator/tools/ado_gherkin.py +854 -0
  25. package/src/qa_stlc_agents/agent_helix_writer/__init__.py +0 -0
  26. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
  27. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
  28. package/src/qa_stlc_agents/agent_helix_writer/server.py +529 -0
  29. package/src/qa_stlc_agents/agent_helix_writer/tools/__init__.py +0 -0
  30. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  31. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
  32. package/src/qa_stlc_agents/agent_helix_writer/tools/helix_write.py +622 -0
  33. package/src/qa_stlc_agents/agent_playwright_generator/__init__.py +0 -0
  34. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  35. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
  36. package/src/qa_stlc_agents/agent_playwright_generator/server.py +2771 -0
  37. package/src/qa_stlc_agents/agent_playwright_generator/tools/__init__.py +0 -0
  38. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  39. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
  40. package/src/qa_stlc_agents/agent_playwright_generator/tools/ado_attach.py +62 -0
  41. package/src/qa_stlc_agents/agent_test_case_manager/__init__.py +0 -0
  42. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
  43. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
  44. package/src/qa_stlc_agents/agent_test_case_manager/server.py +483 -0
  45. package/src/qa_stlc_agents/agent_test_case_manager/tools/__init__.py +0 -0
  46. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  47. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
  48. package/src/qa_stlc_agents/agent_test_case_manager/tools/ado_workitem.py +302 -0
  49. package/src/qa_stlc_agents/shared/__init__.py +0 -0
  50. package/src/qa_stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/qa_stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
  52. package/src/qa_stlc_agents/shared/auth.py +119 -0
@@ -0,0 +1,62 @@
1
+ """
2
+ ado_attach.py — Attach generated Playwright files to an ADO work item.
3
+
4
+ Used by the Playwright Code Generator agent to persist generated files
5
+ back to ADO as attachments on the Feature or PBI work item.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import requests
11
+
12
+ from qa_stlc_agents.shared.auth import get_auth_headers
13
+
14
+ _API = "7.1"
15
+
16
+
17
+ def attach_file_to_work_item(
18
+ org_url: str,
19
+ project: str,
20
+ work_item_id: int,
21
+ file_name: str,
22
+ file_content: str,
23
+ comment: str = "Generated by QA Playwright Code Generator",
24
+ ) -> dict:
25
+ """Upload a text file and attach it to any ADO work item."""
26
+ org_url = org_url.rstrip("/")
27
+
28
+ safe_name = re.sub(r"[^a-zA-Z0-9._\-]", "_", file_name)
29
+
30
+ upload = requests.post(
31
+ f"{org_url}/{project}/_apis/wit/attachments",
32
+ headers=get_auth_headers("application/octet-stream"),
33
+ params={"fileName": safe_name, "api-version": _API},
34
+ data=file_content.encode("utf-8"),
35
+ timeout=30,
36
+ )
37
+ upload.raise_for_status()
38
+ attachment_url = upload.json()["url"]
39
+
40
+ link = requests.patch(
41
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
42
+ headers=get_auth_headers("application/json-patch+json"),
43
+ params={"api-version": _API},
44
+ json=[{
45
+ "op": "add",
46
+ "path": "/relations/-",
47
+ "value": {
48
+ "rel": "AttachedFile",
49
+ "url": attachment_url,
50
+ "attributes": {"comment": comment},
51
+ },
52
+ }],
53
+ timeout=30,
54
+ )
55
+ link.raise_for_status()
56
+
57
+ return {
58
+ "success": True,
59
+ "work_item_id": work_item_id,
60
+ "file_name": safe_name,
61
+ "attachment_url": attachment_url,
62
+ }
@@ -0,0 +1,483 @@
1
+ """
2
+ Agent 1: QA Test Case Manager — MCP Server
3
+
4
+ Tools:
5
+ fetch_work_item — Fetch a PBI or Bug with acceptance criteria
6
+ create_and_link_test_cases — Create test cases in ADO and link to work item
7
+ get_linked_test_cases — Return existing test cases linked to a work item
8
+
9
+ Authentication:
10
+ Silent from ~/.msal-cache/msal-cache.json (shared with microsoft/azure-devops-mcp).
11
+ If no cached token exists, the browser opens once for interactive login.
12
+ No credentials appear in the UI or in configuration files.
13
+
14
+ Skills: see skills/generate-test-cases.md
15
+ """
16
+ import asyncio
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ from dotenv import load_dotenv
22
+ from mcp.server import Server
23
+ from mcp.server.stdio import stdio_server
24
+ from mcp import types
25
+
26
+ from qa_stlc_agents.shared.auth import get_auth_headers, get_signed_in_user
27
+ from qa_stlc_agents.agent_test_case_manager.tools.ado_workitem import (
28
+ fetch_work_item as _fetch_work_item,
29
+ create_test_case as _create_test_case,
30
+ link_test_cases_to_work_item as _link_test_cases,
31
+ get_linked_test_cases as _get_linked_test_cases,
32
+ )
33
+
34
+ load_dotenv()
35
+
36
+ app = Server("qa-test-case-manager")
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Pre-output validation helpers
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def _validate_test_case_inputs(test_cases: list[dict]) -> dict:
44
+ """Validate test case payloads before sending to ADO.
45
+
46
+ Checks:
47
+ 1. Every test case has a non-empty title.
48
+ 2. Every test case has at least one step.
49
+ 3. Every step has both action and expected_result fields.
50
+ 4. No duplicate titles within the batch.
51
+ 5. Priority is in range 1-4 when specified.
52
+
53
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
54
+ """
55
+ errors: list[str] = []
56
+ warnings: list[str] = []
57
+ seen_titles: set[str] = set()
58
+
59
+ for i, tc in enumerate(test_cases, start=1):
60
+ title = (tc.get("title") or "").strip()
61
+ if not title:
62
+ errors.append(f"Test case #{i}: title is empty or missing.")
63
+ else:
64
+ normalised = title.lower()
65
+ if normalised in seen_titles:
66
+ errors.append(f"Test case #{i}: duplicate title '{title}'.")
67
+ seen_titles.add(normalised)
68
+
69
+ steps = tc.get("steps", [])
70
+ if not steps:
71
+ errors.append(f"Test case #{i} ('{title}'): no steps defined.")
72
+ for j, step in enumerate(steps, start=1):
73
+ action = (step.get("action") or "").strip()
74
+ expected = (step.get("expected_result") or "").strip()
75
+ if not action:
76
+ errors.append(
77
+ f"Test case #{i} ('{title}'), step #{j}: 'action' is empty."
78
+ )
79
+ if not expected:
80
+ errors.append(
81
+ f"Test case #{i} ('{title}'), step #{j}: 'expected_result' is empty."
82
+ )
83
+
84
+ priority = tc.get("priority")
85
+ if priority is not None and priority not in (1, 2, 3, 4):
86
+ errors.append(
87
+ f"Test case #{i} ('{title}'): priority {priority} is out of range (1-4)."
88
+ )
89
+
90
+ if len(test_cases) > 25:
91
+ warnings.append(
92
+ f"Large batch: {len(test_cases)} test cases. Consider splitting "
93
+ f"into smaller batches for easier review."
94
+ )
95
+
96
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
97
+
98
+
99
+ def _validate_fetch_response(result: dict) -> dict:
100
+ """Validate the response from fetch_work_item before returning to user.
101
+
102
+ Checks:
103
+ 1. Work item has a title.
104
+ 2. Acceptance criteria is not empty (warns if missing).
105
+ 3. Work item type is recognised.
106
+
107
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
108
+ """
109
+ errors: list[str] = []
110
+ warnings: list[str] = []
111
+
112
+ if "error" in result:
113
+ return {"valid": True, "errors": [], "warnings": []}
114
+
115
+ wi = result.get("work_item", {})
116
+ if not wi.get("title"):
117
+ errors.append("Work item has no title — ADO data may be incomplete.")
118
+
119
+ if not wi.get("acceptance_criteria"):
120
+ warnings.append(
121
+ "Work item has no acceptance criteria. Test case generation may "
122
+ "produce low-quality results — ask the user for acceptance criteria."
123
+ )
124
+
125
+ wi_type = wi.get("type", "")
126
+ known_types = {"Product Backlog Item", "Bug", "Feature", "User Story", "Task", "Epic"}
127
+ if wi_type and wi_type not in known_types:
128
+ warnings.append(f"Unrecognised work item type: '{wi_type}'.")
129
+
130
+ if result.get("existing_test_cases_count", 0) > 0:
131
+ warnings.append(
132
+ f"Work item already has {result['existing_test_cases_count']} linked test case(s). "
133
+ f"Use get_linked_test_cases to check for duplicates before creating new ones."
134
+ )
135
+
136
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
137
+
138
+
139
+ def _validate_linked_test_cases_response(result: dict) -> dict:
140
+ """Validate the response from get_linked_test_cases before returning to user.
141
+
142
+ Checks:
143
+ 1. Response is not an error.
144
+ 2. Test case list is present.
145
+ 3. Each test case has an ID and title.
146
+ 4. Warns about test cases with no steps.
147
+
148
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
149
+ """
150
+ errors: list[str] = []
151
+ warnings: list[str] = []
152
+
153
+ if "error" in result:
154
+ return {"valid": True, "errors": [], "warnings": []}
155
+
156
+ test_cases = result.get("test_cases", [])
157
+ if not test_cases:
158
+ warnings.append(
159
+ "No linked test cases found. This work item has no TestedBy relations."
160
+ )
161
+ return {"valid": True, "errors": [], "warnings": warnings}
162
+
163
+ for i, tc in enumerate(test_cases, start=1):
164
+ if not tc.get("id"):
165
+ errors.append(f"Test case #{i}: missing ID.")
166
+ if not tc.get("title"):
167
+ warnings.append(f"Test case #{i} (ID={tc.get('id', '?')}): missing title.")
168
+ steps = tc.get("steps", [])
169
+ if not steps:
170
+ warnings.append(
171
+ f"Test case '{tc.get('title', '?')}' (ID={tc.get('id', '?')}): has no steps defined."
172
+ )
173
+
174
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Tool definitions
179
+ # ---------------------------------------------------------------------------
180
+
181
+ @app.list_tools()
182
+ async def list_tools() -> list[types.Tool]:
183
+ return [
184
+ types.Tool(
185
+ name="fetch_work_item",
186
+ description=(
187
+ "Fetch a Product Backlog Item or Bug from Azure DevOps. "
188
+ "Returns work item fields (title, acceptance criteria, description, "
189
+ "story points, priority, area path, iteration path), parent feature, "
190
+ "count of existing linked test cases, and coverage hints derived from "
191
+ "the acceptance criteria text."
192
+ ),
193
+ inputSchema={
194
+ "type": "object",
195
+ "properties": {
196
+ "work_item_id": {"type": "integer", "description": "ADO work item ID (PBI or Bug)"},
197
+ "organization_url": {"type": "string", "description": "e.g. https://dev.azure.com/myorg"},
198
+ "project_name": {"type": "string", "description": "ADO project name"},
199
+ },
200
+ "required": ["work_item_id", "organization_url", "project_name"],
201
+ },
202
+ ),
203
+ types.Tool(
204
+ name="create_and_link_test_cases",
205
+ description=(
206
+ "Create one or more test cases in Azure DevOps and link them to a work item "
207
+ "via a TestedBy-Forward relation. Area path and iteration path are automatically "
208
+ "inherited from the parent work item when not specified. "
209
+ "Priority: 1=Critical, 2=High (default), 3=Medium, 4=Low.\n\n"
210
+ "Feature confirmation gate:\n"
211
+ " When the target work item is a Feature, the first call returns "
212
+ "confirmation_required=true and does NOT create any test cases. "
213
+ "Surface the message to the user and wait for their reply. "
214
+ "Once the user confirms, retry the EXACT same call with confirmed=true added — "
215
+ "do NOT change work_item_id, work_item_type, or any other parameter. "
216
+ "Reply 'no' / 'cancel' → do not retry; abort the operation entirely."
217
+ ),
218
+ inputSchema={
219
+ "type": "object",
220
+ "properties": {
221
+ "work_item_id": {"type": "integer", "description": "Work item to link test cases to"},
222
+ "organization_url": {"type": "string"},
223
+ "project_name": {"type": "string"},
224
+ "test_cases": {
225
+ "type": "array",
226
+ "description": "Test cases to create",
227
+ "items": {
228
+ "type": "object",
229
+ "properties": {
230
+ "title": {"type": "string"},
231
+ "priority": {"type": "integer", "description": "1-4, default 2"},
232
+ "steps": {
233
+ "type": "array",
234
+ "items": {
235
+ "type": "object",
236
+ "properties": {
237
+ "action": {"type": "string"},
238
+ "expected_result": {"type": "string"},
239
+ },
240
+ "required": ["action", "expected_result"],
241
+ },
242
+ },
243
+ },
244
+ "required": ["title", "steps"],
245
+ },
246
+ },
247
+ "confirmed": {
248
+ "type": "boolean",
249
+ "description": (
250
+ "Set to true only when retrying after the user has explicitly confirmed "
251
+ "a Feature-scoped test case upload in response to a confirmation_required "
252
+ "response. Omit (or false) on the first call — the gate will fire "
253
+ "automatically if the work item is a Feature."
254
+ ),
255
+ },
256
+ },
257
+ "required": ["work_item_id", "organization_url", "project_name", "test_cases"],
258
+ },
259
+ ),
260
+ types.Tool(
261
+ name="get_linked_test_cases",
262
+ description=(
263
+ "Return all test cases already linked to a work item via TestedBy. "
264
+ "Use this before creating new test cases to avoid duplicates."
265
+ ),
266
+ inputSchema={
267
+ "type": "object",
268
+ "properties": {
269
+ "work_item_id": {"type": "integer"},
270
+ "organization_url": {"type": "string"},
271
+ "project_name": {"type": "string"},
272
+ },
273
+ "required": ["work_item_id", "organization_url", "project_name"],
274
+ },
275
+ ),
276
+ ]
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Tool dispatcher
281
+ # ---------------------------------------------------------------------------
282
+
283
+ @app.call_tool()
284
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
285
+ try:
286
+ if name == "fetch_work_item":
287
+ result = await asyncio.to_thread(
288
+ _fetch_work_item,
289
+ arguments["organization_url"],
290
+ arguments["project_name"],
291
+ arguments["work_item_id"],
292
+ )
293
+ # ── Pre-output validation ─────────────────────────────────────
294
+ validation = _validate_fetch_response(result)
295
+ result["_validation"] = validation
296
+
297
+ elif name == "create_and_link_test_cases":
298
+ org = arguments["organization_url"]
299
+ project = arguments["project_name"]
300
+ wi_id = arguments["work_item_id"]
301
+ test_cases = arguments["test_cases"]
302
+
303
+ # ── Input validation — reject before hitting ADO ──────────────
304
+ input_validation = _validate_test_case_inputs(test_cases)
305
+ if not input_validation["valid"]:
306
+ result = {
307
+ "error": "input_validation_failed",
308
+ "validation": input_validation,
309
+ "message": (
310
+ "Test case inputs failed validation. Fix the errors "
311
+ "below and retry. No test cases were created in ADO."
312
+ ),
313
+ }
314
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
315
+
316
+ # ── Epic guard — always check work item type before creating TCs ─
317
+ try:
318
+ wi_check = await asyncio.to_thread(_fetch_work_item, org, project, wi_id)
319
+ if wi_check.get("error") == "epic_not_supported":
320
+ result = {
321
+ "error": "epic_not_supported",
322
+ "work_item_id": wi_id,
323
+ "message": (
324
+ f"Work item {wi_id} is an Epic. "
325
+ "Manual test cases cannot be created or linked to an Epic. "
326
+ "ADO test cases are always scoped to PBIs, Bugs, or Features — "
327
+ "never to Epics. Please use fetch_epic_hierarchy (in the "
328
+ "qa-gherkin-generator agent) to enumerate the child Features and "
329
+ "PBIs/Bugs, then call create_and_link_test_cases on each "
330
+ "individual Feature or PBI/Bug ID instead."
331
+ ),
332
+ }
333
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
334
+ except Exception:
335
+ pass # if the peek fails, continue and let the real call surface the error
336
+
337
+ # Inherit area_path / iteration_path from parent work item
338
+ parent_area_path = None
339
+ parent_iteration_path = None
340
+ try:
341
+ wi_data = await asyncio.to_thread(_fetch_work_item, org, project, wi_id)
342
+ parent_area_path = wi_data["work_item"].get("area_path")
343
+ parent_iteration_path = wi_data["work_item"].get("iteration_path")
344
+ except Exception:
345
+ pass
346
+
347
+ # ── Feature confirmation gate ─────────────────────────────────────
348
+ # When the target work item is a Feature, pause and ask the user
349
+ # to confirm before creating and uploading test cases. The
350
+ # server cannot block here (it is async / stateless), so we rely
351
+ # on the skill / agent layer to enforce this. The server signals
352
+ # the need for confirmation by returning a structured response that
353
+ # the LLM skill interprets as a hard stop.
354
+ # Retry protocol: after the user confirms, call this tool again
355
+ # with the EXACT same arguments plus confirmed=true. The gate
356
+ # is then bypassed and creation proceeds normally.
357
+ try:
358
+ wi_type_check = wi_data.get("work_item", {}).get("type", "")
359
+ if wi_type_check == "Feature" and not arguments.get("confirmed", False):
360
+ feature_title = wi_data.get("work_item", {}).get("title", str(wi_id))
361
+ result = {
362
+ "confirmation_required": True,
363
+ "work_item_id": wi_id,
364
+ "work_item_type": "Feature",
365
+ "feature_title": feature_title,
366
+ "proposed_test_case_count": len(test_cases),
367
+ "retry_instructions": (
368
+ "Surface this message to the user. "
369
+ "If they confirm, retry this tool call with the IDENTICAL "
370
+ "arguments and add confirmed=true. "
371
+ "Do NOT change work_item_id or any other parameter. "
372
+ "If they decline, abort — do not call any other tool."
373
+ ),
374
+ "message": (
375
+ f"Work item {wi_id} is a Feature: \"{feature_title}\". "
376
+ f"You are about to create and upload {len(test_cases)} manual "
377
+ f"test case(s) linked to this Feature in ADO. "
378
+ "Please confirm before proceeding. "
379
+ "Reply 'yes' or 'confirm' to proceed, or 'no' / 'cancel' to abort."
380
+ ),
381
+ }
382
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
383
+ except Exception:
384
+ pass # wi_data may not be set if the earlier peek failed; continue normally
385
+
386
+
387
+ failed = []
388
+ created = []
389
+ for tc in test_cases:
390
+ try:
391
+ r = await asyncio.to_thread(
392
+ _create_test_case,
393
+ org, project,
394
+ tc["title"],
395
+ tc.get("steps", []),
396
+ tc.get("priority", 2),
397
+ tc.get("area_path") or parent_area_path,
398
+ tc.get("iteration_path") or parent_iteration_path,
399
+ )
400
+ created.append(r)
401
+ except Exception as e:
402
+ failed.append({"title": tc["title"], "error": str(e)})
403
+
404
+ link_result = {}
405
+ if created:
406
+ tc_ids = [r["test_case_id"] for r in created]
407
+ try:
408
+ link_result = await asyncio.to_thread(
409
+ _link_test_cases, org, project, wi_id, tc_ids
410
+ )
411
+ except Exception as e:
412
+ link_result = {"error": str(e)}
413
+
414
+ result = {
415
+ "summary": {
416
+ "requested": len(test_cases),
417
+ "created": len(created),
418
+ "failed": len(failed),
419
+ "linked_to_work_item": wi_id,
420
+ },
421
+ "created_test_cases": created,
422
+ "failed": failed,
423
+ "link_result": link_result,
424
+ "_validation": {
425
+ "valid": len(failed) == 0 and bool(link_result.get("success", True)),
426
+ "input_validation": input_validation,
427
+ "errors": [f["error"] for f in failed] if failed else [],
428
+ "warnings": input_validation.get("warnings", []),
429
+ "post_creation_check": {
430
+ "all_created": len(created) == len(test_cases),
431
+ "all_linked": link_result.get("success", False) if link_result else False,
432
+ },
433
+ },
434
+ }
435
+
436
+ elif name == "get_linked_test_cases":
437
+ result = await asyncio.to_thread(
438
+ _get_linked_test_cases,
439
+ arguments["organization_url"],
440
+ arguments["project_name"],
441
+ arguments["work_item_id"],
442
+ )
443
+ # ── Pre-output validation ─────────────────────────────────────
444
+ result["_validation"] = _validate_linked_test_cases_response(result)
445
+
446
+ else:
447
+ result = {"error": f"Unknown tool: {name}"}
448
+
449
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
450
+
451
+ except Exception as exc:
452
+ return [types.TextContent(
453
+ type="text",
454
+ text=json.dumps({"error": str(exc), "tool": name}, indent=2),
455
+ )]
456
+
457
+
458
+ # ---------------------------------------------------------------------------
459
+ # Entry point
460
+ # ---------------------------------------------------------------------------
461
+
462
+ async def _run():
463
+ async with stdio_server() as (r, w):
464
+ await app.run(r, w, app.create_initialization_options())
465
+
466
+
467
+ def main():
468
+ # Validate auth on startup — triggers interactive login if needed
469
+ try:
470
+ get_auth_headers()
471
+ except Exception as e:
472
+ print(f"[qa-test-case-manager] Auth error: {e}", file=sys.stderr)
473
+ sys.exit(1)
474
+
475
+ user = get_signed_in_user()
476
+ if user:
477
+ print(f"[qa-test-case-manager] Authenticated as: {user}", file=sys.stderr)
478
+
479
+ asyncio.run(_run())
480
+
481
+
482
+ if __name__ == "__main__":
483
+ main()