foundry-mcp 0.3.3__py3-none-any.whl

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 (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,520 @@
1
+ """Unified verification tool with action routing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from dataclasses import asdict
8
+ from typing import Any, Dict, Optional
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+
12
+ from foundry_mcp.config import ServerConfig
13
+ from foundry_mcp.core.context import generate_correlation_id, get_correlation_id
14
+ from foundry_mcp.core.naming import canonical_tool
15
+ from foundry_mcp.core.observability import audit_log, get_metrics, mcp_tool
16
+ from foundry_mcp.core.responses import (
17
+ ErrorCode,
18
+ ErrorType,
19
+ error_response,
20
+ sanitize_error_message,
21
+ success_response,
22
+ )
23
+ from foundry_mcp.core.spec import find_specs_directory, load_spec, save_spec
24
+ from foundry_mcp.core.validation import add_verification, execute_verification
25
+ from foundry_mcp.tools.unified.router import (
26
+ ActionDefinition,
27
+ ActionRouter,
28
+ ActionRouterError,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+ _metrics = get_metrics()
33
+
34
+ _ACTION_SUMMARY = {
35
+ "add": "Persist verification results with optional dry-run preview",
36
+ "execute": "Execute verification commands and optionally record results",
37
+ }
38
+
39
+
40
+ def _metric_name(action: str) -> str:
41
+ return f"verification.{action}"
42
+
43
+
44
+ def _request_id() -> str:
45
+ return get_correlation_id() or generate_correlation_id(prefix="verification")
46
+
47
+
48
+ def _validation_error(
49
+ *,
50
+ action: str,
51
+ field: str,
52
+ message: str,
53
+ request_id: str,
54
+ remediation: Optional[str] = None,
55
+ code: ErrorCode = ErrorCode.VALIDATION_ERROR,
56
+ ) -> dict:
57
+ return asdict(
58
+ error_response(
59
+ f"Invalid field '{field}' for verification.{action}: {message}",
60
+ error_code=code,
61
+ error_type=ErrorType.VALIDATION,
62
+ remediation=remediation,
63
+ details={"field": field, "action": f"verification.{action}"},
64
+ request_id=request_id,
65
+ )
66
+ )
67
+
68
+
69
+ def _handle_add(
70
+ *,
71
+ config: ServerConfig, # noqa: ARG001 - reserved for future hooks
72
+ spec_id: Optional[str] = None,
73
+ verify_id: Optional[str] = None,
74
+ result: Optional[str] = None,
75
+ command: Optional[str] = None,
76
+ output: Optional[str] = None,
77
+ issues: Optional[str] = None,
78
+ notes: Optional[str] = None,
79
+ dry_run: bool = False,
80
+ path: Optional[str] = None,
81
+ **_: Any,
82
+ ) -> dict:
83
+ request_id = _request_id()
84
+ action = "add"
85
+
86
+ if not isinstance(spec_id, str) or not spec_id.strip():
87
+ return _validation_error(
88
+ action=action,
89
+ field="spec_id",
90
+ message="Provide a non-empty spec_id",
91
+ request_id=request_id,
92
+ remediation='Use spec(action="list") to discover valid specification IDs',
93
+ code=ErrorCode.MISSING_REQUIRED,
94
+ )
95
+ spec_id = spec_id.strip()
96
+
97
+ if not isinstance(verify_id, str) or not verify_id.strip():
98
+ return _validation_error(
99
+ action=action,
100
+ field="verify_id",
101
+ message="Provide the verification node identifier (e.g., verify-1-1)",
102
+ request_id=request_id,
103
+ code=ErrorCode.MISSING_REQUIRED,
104
+ )
105
+ verify_id = verify_id.strip()
106
+
107
+ if not isinstance(result, str) or not result.strip():
108
+ return _validation_error(
109
+ action=action,
110
+ field="result",
111
+ message="Provide the verification result (PASSED, FAILED, PARTIAL)",
112
+ request_id=request_id,
113
+ code=ErrorCode.MISSING_REQUIRED,
114
+ )
115
+ result_upper = result.strip().upper()
116
+ if result_upper not in {"PASSED", "FAILED", "PARTIAL"}:
117
+ return _validation_error(
118
+ action=action,
119
+ field="result",
120
+ message="Result must be one of PASSED, FAILED, or PARTIAL",
121
+ request_id=request_id,
122
+ remediation="Use one of: PASSED, FAILED, PARTIAL",
123
+ )
124
+
125
+ if path is not None and not isinstance(path, str):
126
+ return _validation_error(
127
+ action=action,
128
+ field="path",
129
+ message="Workspace path must be a string",
130
+ request_id=request_id,
131
+ )
132
+ if not isinstance(dry_run, bool):
133
+ return _validation_error(
134
+ action=action,
135
+ field="dry_run",
136
+ message="Expected a boolean value",
137
+ request_id=request_id,
138
+ code=ErrorCode.INVALID_FORMAT,
139
+ )
140
+
141
+ metric_key = _metric_name(action)
142
+ audit_log(
143
+ "tool_invocation",
144
+ tool="verification",
145
+ action=action,
146
+ spec_id=spec_id,
147
+ verify_id=verify_id,
148
+ result=result_upper,
149
+ dry_run=dry_run,
150
+ )
151
+
152
+ specs_dir = find_specs_directory(path)
153
+ if not specs_dir:
154
+ _metrics.counter(metric_key, labels={"status": "specs_not_found"})
155
+ return asdict(
156
+ error_response(
157
+ "Could not find specs directory",
158
+ error_code=ErrorCode.NOT_FOUND,
159
+ error_type=ErrorType.NOT_FOUND,
160
+ remediation="Ensure you are in a project with a specs/ directory",
161
+ request_id=request_id,
162
+ )
163
+ )
164
+
165
+ spec_data = load_spec(spec_id, specs_dir)
166
+ if not spec_data:
167
+ _metrics.counter(metric_key, labels={"status": "spec_not_found"})
168
+ return asdict(
169
+ error_response(
170
+ f"Specification '{spec_id}' not found",
171
+ error_code=ErrorCode.SPEC_NOT_FOUND,
172
+ error_type=ErrorType.NOT_FOUND,
173
+ remediation='Verify the spec ID exists using spec(action="list")',
174
+ request_id=request_id,
175
+ )
176
+ )
177
+
178
+ if dry_run:
179
+ hierarchy = spec_data.get("hierarchy", {})
180
+ if not isinstance(hierarchy, dict) or hierarchy.get(verify_id) is None:
181
+ _metrics.counter(metric_key, labels={"status": "verify_not_found"})
182
+ return asdict(
183
+ error_response(
184
+ f"Verification '{verify_id}' not found in spec",
185
+ error_code=ErrorCode.NOT_FOUND,
186
+ error_type=ErrorType.NOT_FOUND,
187
+ remediation="Verify the verification ID exists in the specification",
188
+ request_id=request_id,
189
+ )
190
+ )
191
+
192
+ data: Dict[str, Any] = {
193
+ "spec_id": spec_id,
194
+ "verify_id": verify_id,
195
+ "result": result_upper,
196
+ "dry_run": True,
197
+ }
198
+ if command:
199
+ data["command"] = command
200
+ _metrics.counter(metric_key, labels={"status": "dry_run"})
201
+ return asdict(
202
+ success_response(
203
+ data=data,
204
+ request_id=request_id,
205
+ )
206
+ )
207
+
208
+ try:
209
+ success, error_msg = add_verification(
210
+ spec_data=spec_data,
211
+ verify_id=verify_id,
212
+ result=result_upper,
213
+ command=command,
214
+ output=output,
215
+ issues=issues,
216
+ notes=notes,
217
+ )
218
+ except Exception as exc:
219
+ logger.exception("Unexpected error adding verification")
220
+ _metrics.counter(metric_key, labels={"status": "error"})
221
+ return asdict(
222
+ error_response(
223
+ sanitize_error_message(exc, context="verification"),
224
+ error_code=ErrorCode.INTERNAL_ERROR,
225
+ error_type=ErrorType.INTERNAL,
226
+ remediation="Check logs for details",
227
+ request_id=request_id,
228
+ )
229
+ )
230
+
231
+ if not success:
232
+ lowered = (error_msg or "").lower()
233
+ if "not found" in lowered:
234
+ code = ErrorCode.NOT_FOUND
235
+ error_type = ErrorType.NOT_FOUND
236
+ elif "already" in lowered or "duplicate" in lowered:
237
+ code = ErrorCode.CONFLICT
238
+ error_type = ErrorType.CONFLICT
239
+ else:
240
+ code = ErrorCode.VALIDATION_ERROR
241
+ error_type = ErrorType.VALIDATION
242
+ _metrics.counter(metric_key, labels={"status": error_type.value})
243
+ return asdict(
244
+ error_response(
245
+ error_msg or "Failed to add verification",
246
+ error_code=code,
247
+ error_type=error_type,
248
+ remediation="Check input parameters",
249
+ request_id=request_id,
250
+ )
251
+ )
252
+
253
+ if not save_spec(spec_id, spec_data, specs_dir):
254
+ _metrics.counter(metric_key, labels={"status": "save_failed"})
255
+ return asdict(
256
+ error_response(
257
+ "Failed to save specification",
258
+ error_code=ErrorCode.INTERNAL_ERROR,
259
+ error_type=ErrorType.INTERNAL,
260
+ remediation="Check file permissions",
261
+ request_id=request_id,
262
+ )
263
+ )
264
+
265
+ _metrics.counter(metric_key, labels={"status": "success"})
266
+ response_data = {
267
+ "spec_id": spec_id,
268
+ "verify_id": verify_id,
269
+ "result": result_upper,
270
+ "dry_run": False,
271
+ }
272
+ if command:
273
+ response_data["command"] = command
274
+
275
+ return asdict(
276
+ success_response(
277
+ data=response_data,
278
+ request_id=request_id,
279
+ )
280
+ )
281
+
282
+
283
+ def _handle_execute(
284
+ *,
285
+ config: ServerConfig, # noqa: ARG001 - reserved for future hooks
286
+ spec_id: Optional[str] = None,
287
+ verify_id: Optional[str] = None,
288
+ record: bool = False,
289
+ path: Optional[str] = None,
290
+ **_: Any,
291
+ ) -> dict:
292
+ request_id = _request_id()
293
+ action = "execute"
294
+
295
+ if not isinstance(spec_id, str) or not spec_id.strip():
296
+ return _validation_error(
297
+ action=action,
298
+ field="spec_id",
299
+ message="Provide a non-empty spec_id",
300
+ request_id=request_id,
301
+ code=ErrorCode.MISSING_REQUIRED,
302
+ )
303
+ spec_id = spec_id.strip()
304
+
305
+ if not isinstance(verify_id, str) or not verify_id.strip():
306
+ return _validation_error(
307
+ action=action,
308
+ field="verify_id",
309
+ message="Provide the verification identifier",
310
+ request_id=request_id,
311
+ code=ErrorCode.MISSING_REQUIRED,
312
+ )
313
+ verify_id = verify_id.strip()
314
+
315
+ if not isinstance(record, bool):
316
+ return _validation_error(
317
+ action=action,
318
+ field="record",
319
+ message="Expected a boolean value",
320
+ request_id=request_id,
321
+ code=ErrorCode.INVALID_FORMAT,
322
+ )
323
+
324
+ if path is not None and not isinstance(path, str):
325
+ return _validation_error(
326
+ action=action,
327
+ field="path",
328
+ message="Workspace path must be a string",
329
+ request_id=request_id,
330
+ )
331
+
332
+ metric_key = _metric_name(action)
333
+ audit_log(
334
+ "tool_invocation",
335
+ tool="verification",
336
+ action=action,
337
+ spec_id=spec_id,
338
+ verify_id=verify_id,
339
+ record=record,
340
+ )
341
+
342
+ specs_dir = find_specs_directory(path)
343
+ if not specs_dir:
344
+ _metrics.counter(metric_key, labels={"status": "specs_not_found"})
345
+ return asdict(
346
+ error_response(
347
+ "Could not find specs directory",
348
+ error_code=ErrorCode.NOT_FOUND,
349
+ error_type=ErrorType.NOT_FOUND,
350
+ remediation="Ensure you are in a project with a specs/ directory",
351
+ request_id=request_id,
352
+ )
353
+ )
354
+
355
+ spec_data = load_spec(spec_id, specs_dir)
356
+ if not spec_data:
357
+ _metrics.counter(metric_key, labels={"status": "spec_not_found"})
358
+ return asdict(
359
+ error_response(
360
+ f"Specification '{spec_id}' not found",
361
+ error_code=ErrorCode.SPEC_NOT_FOUND,
362
+ error_type=ErrorType.NOT_FOUND,
363
+ remediation='Verify the spec ID exists using spec(action="list")',
364
+ request_id=request_id,
365
+ )
366
+ )
367
+
368
+ start_time = time.perf_counter()
369
+ try:
370
+ result_data = execute_verification(
371
+ spec_data=spec_data,
372
+ verify_id=verify_id,
373
+ record=record,
374
+ cwd=path,
375
+ )
376
+ except Exception as exc:
377
+ logger.exception("Unexpected error executing verification")
378
+ _metrics.counter(metric_key, labels={"status": "error"})
379
+ return asdict(
380
+ error_response(
381
+ sanitize_error_message(exc, context="verification"),
382
+ error_code=ErrorCode.INTERNAL_ERROR,
383
+ error_type=ErrorType.INTERNAL,
384
+ remediation="Check logs for details",
385
+ request_id=request_id,
386
+ )
387
+ )
388
+
389
+ if record and result_data.get("recorded"):
390
+ if not save_spec(spec_id, spec_data, specs_dir):
391
+ result_data["recorded"] = False
392
+ result_data["error"] = (
393
+ result_data.get("error") or ""
394
+ ) + "; Failed to save spec"
395
+
396
+ if result_data.get("error") and not result_data.get("success"):
397
+ error_msg = result_data["error"]
398
+ lowered = error_msg.lower()
399
+ if "not found" in lowered:
400
+ code = ErrorCode.NOT_FOUND
401
+ error_type = ErrorType.NOT_FOUND
402
+ elif "no command" in lowered:
403
+ code = ErrorCode.VALIDATION_ERROR
404
+ error_type = ErrorType.VALIDATION
405
+ else:
406
+ code = ErrorCode.INTERNAL_ERROR
407
+ error_type = ErrorType.INTERNAL
408
+ _metrics.counter(metric_key, labels={"status": error_type.value})
409
+ return asdict(
410
+ error_response(
411
+ error_msg if error_msg else "Failed to execute verification",
412
+ error_code=code,
413
+ error_type=error_type,
414
+ remediation="Ensure the verification node has a valid command",
415
+ request_id=request_id,
416
+ )
417
+ )
418
+
419
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
420
+ response_data: Dict[str, Any] = {
421
+ "spec_id": spec_id,
422
+ "verify_id": verify_id,
423
+ "result": result_data.get("result", "UNKNOWN"),
424
+ "recorded": result_data.get("recorded", False),
425
+ }
426
+ if result_data.get("command"):
427
+ response_data["command"] = result_data["command"]
428
+ if result_data.get("output"):
429
+ response_data["output"] = result_data["output"]
430
+ if result_data.get("exit_code") is not None:
431
+ response_data["exit_code"] = result_data["exit_code"]
432
+
433
+ _metrics.counter(metric_key, labels={"status": "success"})
434
+ return asdict(
435
+ success_response(
436
+ data=response_data,
437
+ telemetry={"duration_ms": round(elapsed_ms, 2)},
438
+ request_id=request_id,
439
+ )
440
+ )
441
+
442
+
443
+ _VERIFICATION_ROUTER = ActionRouter(
444
+ tool_name="verification",
445
+ actions=[
446
+ ActionDefinition(
447
+ name="add",
448
+ handler=_handle_add,
449
+ summary=_ACTION_SUMMARY["add"],
450
+ aliases=("verification-add", "verification_add"),
451
+ ),
452
+ ActionDefinition(
453
+ name="execute",
454
+ handler=_handle_execute,
455
+ summary=_ACTION_SUMMARY["execute"],
456
+ aliases=("verification-execute", "verification_execute"),
457
+ ),
458
+ ],
459
+ )
460
+
461
+
462
+ def _dispatch_verification_action(
463
+ *, action: str, payload: Dict[str, Any], config: ServerConfig
464
+ ) -> dict:
465
+ try:
466
+ return _VERIFICATION_ROUTER.dispatch(action=action, config=config, **payload)
467
+ except ActionRouterError as exc:
468
+ request_id = _request_id()
469
+ allowed = ", ".join(exc.allowed_actions)
470
+ return asdict(
471
+ error_response(
472
+ f"Unsupported verification action '{action}'. Allowed actions: {allowed}",
473
+ error_code=ErrorCode.VALIDATION_ERROR,
474
+ error_type=ErrorType.VALIDATION,
475
+ remediation=f"Use one of: {allowed}",
476
+ request_id=request_id,
477
+ )
478
+ )
479
+
480
+
481
+ def register_unified_verification_tool(mcp: FastMCP, config: ServerConfig) -> None:
482
+ """Register the consolidated verification tool."""
483
+
484
+ @canonical_tool(mcp, canonical_name="verification")
485
+ @mcp_tool(tool_name="verification", emit_metrics=True, audit=True)
486
+ def verification( # noqa: PLR0913 - shared signature across actions
487
+ action: str,
488
+ spec_id: Optional[str] = None,
489
+ verify_id: Optional[str] = None,
490
+ result: Optional[str] = None,
491
+ command: Optional[str] = None,
492
+ output: Optional[str] = None,
493
+ issues: Optional[str] = None,
494
+ notes: Optional[str] = None,
495
+ dry_run: bool = False,
496
+ record: bool = False,
497
+ path: Optional[str] = None,
498
+ ) -> dict:
499
+ payload = {
500
+ "spec_id": spec_id,
501
+ "verify_id": verify_id,
502
+ "result": result,
503
+ "command": command,
504
+ "output": output,
505
+ "issues": issues,
506
+ "notes": notes,
507
+ "dry_run": dry_run,
508
+ "record": record,
509
+ "path": path,
510
+ }
511
+ return _dispatch_verification_action(
512
+ action=action, payload=payload, config=config
513
+ )
514
+
515
+ logger.debug("Registered unified verification tool")
516
+
517
+
518
+ __all__ = [
519
+ "register_unified_verification_tool",
520
+ ]