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,294 @@
|
|
|
1
|
+
"""Unified PR workflow tool with action routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from foundry_mcp.config import ServerConfig
|
|
14
|
+
from foundry_mcp.core.journal import get_journal_entries
|
|
15
|
+
from foundry_mcp.core.naming import canonical_tool
|
|
16
|
+
from foundry_mcp.core.observability import get_metrics, mcp_tool
|
|
17
|
+
from foundry_mcp.core.progress import get_progress_summary
|
|
18
|
+
from foundry_mcp.core.responses import (
|
|
19
|
+
ErrorCode,
|
|
20
|
+
ErrorType,
|
|
21
|
+
error_response,
|
|
22
|
+
success_response,
|
|
23
|
+
sanitize_error_message,
|
|
24
|
+
)
|
|
25
|
+
from foundry_mcp.core.spec import find_spec_file, find_specs_directory, load_spec
|
|
26
|
+
from foundry_mcp.tools.unified.router import (
|
|
27
|
+
ActionDefinition,
|
|
28
|
+
ActionRouter,
|
|
29
|
+
ActionRouterError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
_metrics = get_metrics()
|
|
34
|
+
|
|
35
|
+
_ACTION_SUMMARY = {
|
|
36
|
+
"create": "Delegate PR creation to external CLI integration",
|
|
37
|
+
"get-context": "Generate PR context from spec progress",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def perform_pr_create_with_spec(
|
|
42
|
+
*,
|
|
43
|
+
spec_id: str,
|
|
44
|
+
title: Optional[str],
|
|
45
|
+
base_branch: str,
|
|
46
|
+
include_journals: bool,
|
|
47
|
+
include_diffs: bool,
|
|
48
|
+
model: Optional[str],
|
|
49
|
+
path: Optional[str],
|
|
50
|
+
dry_run: bool,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""Return the not-implemented response for PR creation."""
|
|
53
|
+
|
|
54
|
+
return asdict(
|
|
55
|
+
error_response(
|
|
56
|
+
"PR creation requires GitHub CLI integration and LLM-powered description generation. "
|
|
57
|
+
"Use the sdd-toolkit:sdd-pr skill for AI-powered PR creation.",
|
|
58
|
+
error_code=ErrorCode.UNAVAILABLE,
|
|
59
|
+
error_type=ErrorType.UNAVAILABLE,
|
|
60
|
+
data={
|
|
61
|
+
"spec_id": spec_id,
|
|
62
|
+
"title": title,
|
|
63
|
+
"base_branch": base_branch,
|
|
64
|
+
"dry_run": dry_run,
|
|
65
|
+
"include_journals": include_journals,
|
|
66
|
+
"include_diffs": include_diffs,
|
|
67
|
+
"path": path,
|
|
68
|
+
"model": model,
|
|
69
|
+
"alternative": "sdd-toolkit:sdd-pr skill",
|
|
70
|
+
"feature_status": "requires_external_integration",
|
|
71
|
+
},
|
|
72
|
+
remediation="Use the sdd-toolkit:sdd-pr skill which provides GitHub CLI integration and LLM-powered PR description generation.",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def perform_pr_get_context(
|
|
78
|
+
*,
|
|
79
|
+
spec_id: str,
|
|
80
|
+
include_tasks: bool,
|
|
81
|
+
include_journals: bool,
|
|
82
|
+
include_progress: bool,
|
|
83
|
+
path: Optional[str],
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Gather context for manual or automated PR preparation."""
|
|
86
|
+
|
|
87
|
+
start_time = time.perf_counter()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
ws_path = Path(path) if path else Path.cwd()
|
|
91
|
+
|
|
92
|
+
specs_dir = find_specs_directory(str(ws_path))
|
|
93
|
+
if not specs_dir:
|
|
94
|
+
return asdict(
|
|
95
|
+
error_response(
|
|
96
|
+
f"Specs directory not found in {ws_path}",
|
|
97
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
98
|
+
error_type=ErrorType.NOT_FOUND,
|
|
99
|
+
data={"spec_id": spec_id, "workspace": str(ws_path)},
|
|
100
|
+
remediation="Ensure you're in a project with a specs/ directory or pass a valid path.",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
spec_file = find_spec_file(spec_id, specs_dir)
|
|
105
|
+
if not spec_file:
|
|
106
|
+
return asdict(
|
|
107
|
+
error_response(
|
|
108
|
+
f"Spec '{spec_id}' not found",
|
|
109
|
+
error_code=ErrorCode.SPEC_NOT_FOUND,
|
|
110
|
+
error_type=ErrorType.NOT_FOUND,
|
|
111
|
+
data={"spec_id": spec_id, "specs_dir": str(specs_dir)},
|
|
112
|
+
remediation='Verify the spec ID exists using spec(action="list").',
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
spec_data = load_spec(str(spec_file), specs_dir)
|
|
117
|
+
if not spec_data:
|
|
118
|
+
return asdict(
|
|
119
|
+
error_response(
|
|
120
|
+
f"Failed to load spec '{spec_id}'",
|
|
121
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
122
|
+
error_type=ErrorType.INTERNAL,
|
|
123
|
+
data={"spec_id": spec_id, "spec_file": str(spec_file)},
|
|
124
|
+
remediation="Check spec JSON validity and retry.",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
context: Dict[str, Any] = {
|
|
129
|
+
"spec_id": spec_id,
|
|
130
|
+
"title": spec_data.get("metadata", {}).get("title", ""),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if include_progress:
|
|
134
|
+
progress_data = get_progress_summary(spec_data)
|
|
135
|
+
context["progress"] = {
|
|
136
|
+
"total_tasks": progress_data.get("total_tasks", 0),
|
|
137
|
+
"completed_tasks": progress_data.get("completed_tasks", 0),
|
|
138
|
+
"percentage": progress_data.get("percentage", 0),
|
|
139
|
+
"current_phase": progress_data.get("current_phase"),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if include_tasks:
|
|
143
|
+
hierarchy = spec_data.get("hierarchy", {})
|
|
144
|
+
completed_tasks = []
|
|
145
|
+
for node_id, node in hierarchy.items():
|
|
146
|
+
if (
|
|
147
|
+
node.get("type") in ("task", "subtask")
|
|
148
|
+
and node.get("status") == "completed"
|
|
149
|
+
):
|
|
150
|
+
completed_tasks.append(
|
|
151
|
+
{
|
|
152
|
+
"task_id": node_id,
|
|
153
|
+
"title": node.get("title", ""),
|
|
154
|
+
"completed_at": node.get("metadata", {}).get(
|
|
155
|
+
"completed_at", ""
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
context["tasks"] = completed_tasks
|
|
160
|
+
|
|
161
|
+
if include_journals:
|
|
162
|
+
journal_entries = get_journal_entries(spec_data, limit=5)
|
|
163
|
+
context["journals"] = [
|
|
164
|
+
{
|
|
165
|
+
"timestamp": entry.timestamp,
|
|
166
|
+
"entry_type": entry.entry_type,
|
|
167
|
+
"title": entry.title,
|
|
168
|
+
"task_id": entry.task_id,
|
|
169
|
+
}
|
|
170
|
+
for entry in journal_entries
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
174
|
+
_metrics.timer("pr_workflow.pr_get_spec_context.duration_ms", duration_ms)
|
|
175
|
+
|
|
176
|
+
return asdict(
|
|
177
|
+
success_response(
|
|
178
|
+
**context,
|
|
179
|
+
telemetry={"duration_ms": round(duration_ms, 2)},
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except Exception as exc: # pragma: no cover - defensive fallback
|
|
184
|
+
logger.exception(f"Error getting spec context for {spec_id}")
|
|
185
|
+
return asdict(
|
|
186
|
+
error_response(
|
|
187
|
+
sanitize_error_message(exc, context="PR workflow"),
|
|
188
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
189
|
+
error_type=ErrorType.INTERNAL,
|
|
190
|
+
data={"spec_id": spec_id},
|
|
191
|
+
remediation="Check logs for details and retry.",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _handle_pr_create(**payload: Any) -> dict:
|
|
197
|
+
return perform_pr_create_with_spec(
|
|
198
|
+
spec_id=payload["spec_id"],
|
|
199
|
+
title=payload.get("title"),
|
|
200
|
+
base_branch=payload.get("base_branch", "main"),
|
|
201
|
+
include_journals=payload.get("include_journals", True),
|
|
202
|
+
include_diffs=payload.get("include_diffs", True),
|
|
203
|
+
model=payload.get("model"),
|
|
204
|
+
path=payload.get("path"),
|
|
205
|
+
dry_run=payload.get("dry_run", False),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _handle_pr_get_context(**payload: Any) -> dict:
|
|
210
|
+
return perform_pr_get_context(
|
|
211
|
+
spec_id=payload["spec_id"],
|
|
212
|
+
include_tasks=payload.get("include_tasks", True),
|
|
213
|
+
include_journals=payload.get("include_journals", True),
|
|
214
|
+
include_progress=payload.get("include_progress", True),
|
|
215
|
+
path=payload.get("path"),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_PR_ROUTER = ActionRouter(
|
|
220
|
+
tool_name="pr",
|
|
221
|
+
actions=[
|
|
222
|
+
ActionDefinition(
|
|
223
|
+
name="create",
|
|
224
|
+
handler=_handle_pr_create,
|
|
225
|
+
summary=_ACTION_SUMMARY["create"],
|
|
226
|
+
),
|
|
227
|
+
ActionDefinition(
|
|
228
|
+
name="get-context",
|
|
229
|
+
handler=_handle_pr_get_context,
|
|
230
|
+
summary=_ACTION_SUMMARY["get-context"],
|
|
231
|
+
aliases=("context", "get_context"),
|
|
232
|
+
),
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _dispatch_pr_action(action: str, payload: Dict[str, Any]) -> dict:
|
|
238
|
+
try:
|
|
239
|
+
return _PR_ROUTER.dispatch(action=action, **payload)
|
|
240
|
+
except ActionRouterError as exc:
|
|
241
|
+
allowed = ", ".join(exc.allowed_actions)
|
|
242
|
+
return asdict(
|
|
243
|
+
error_response(
|
|
244
|
+
f"Unsupported pr action '{action}'. Allowed actions: {allowed}",
|
|
245
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
246
|
+
error_type=ErrorType.VALIDATION,
|
|
247
|
+
remediation=f"Use one of: {allowed}",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def register_unified_pr_tool(mcp: FastMCP, config: ServerConfig) -> None:
|
|
253
|
+
"""Register the consolidated PR tool."""
|
|
254
|
+
|
|
255
|
+
@canonical_tool(
|
|
256
|
+
mcp,
|
|
257
|
+
canonical_name="pr",
|
|
258
|
+
)
|
|
259
|
+
@mcp_tool(tool_name="pr", emit_metrics=True, audit=False)
|
|
260
|
+
def pr(
|
|
261
|
+
action: str,
|
|
262
|
+
spec_id: str,
|
|
263
|
+
title: Optional[str] = None,
|
|
264
|
+
base_branch: str = "main",
|
|
265
|
+
include_journals: bool = True,
|
|
266
|
+
include_diffs: bool = True,
|
|
267
|
+
model: Optional[str] = None,
|
|
268
|
+
path: Optional[str] = None,
|
|
269
|
+
dry_run: bool = False,
|
|
270
|
+
include_tasks: bool = True,
|
|
271
|
+
include_progress: bool = True,
|
|
272
|
+
) -> dict:
|
|
273
|
+
"""Execute PR workflows via `action` parameter."""
|
|
274
|
+
|
|
275
|
+
payload = {
|
|
276
|
+
"spec_id": spec_id,
|
|
277
|
+
"title": title,
|
|
278
|
+
"base_branch": base_branch,
|
|
279
|
+
"include_journals": include_journals,
|
|
280
|
+
"include_diffs": include_diffs,
|
|
281
|
+
"model": model,
|
|
282
|
+
"path": path,
|
|
283
|
+
"dry_run": dry_run,
|
|
284
|
+
"include_tasks": include_tasks,
|
|
285
|
+
"include_progress": include_progress,
|
|
286
|
+
}
|
|
287
|
+
return _dispatch_pr_action(action=action, payload=payload)
|
|
288
|
+
|
|
289
|
+
logger.debug("Registered unified pr tool")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
__all__ = [
|
|
293
|
+
"register_unified_pr_tool",
|
|
294
|
+
]
|