foundry-mcp 0.8.22__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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.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
|
+
]
|