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,591 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spec resources for foundry-mcp.
|
|
3
|
+
|
|
4
|
+
Provides MCP resources for accessing specs, journals, and templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
|
|
14
|
+
from foundry_mcp.config import ServerConfig
|
|
15
|
+
from foundry_mcp.core.spec import (
|
|
16
|
+
load_spec,
|
|
17
|
+
list_specs,
|
|
18
|
+
find_specs_directory,
|
|
19
|
+
find_spec_file,
|
|
20
|
+
)
|
|
21
|
+
from foundry_mcp.core.journal import get_journal_entries
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Schema version for resource responses
|
|
27
|
+
SCHEMA_VERSION = "1.0.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register spec resources with the FastMCP server.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
mcp: FastMCP server instance
|
|
36
|
+
config: Server configuration
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def _get_specs_dir(workspace: Optional[str] = None) -> Optional[Path]:
|
|
40
|
+
"""Get the specs directory for the given workspace."""
|
|
41
|
+
if workspace:
|
|
42
|
+
ws_path = Path(workspace)
|
|
43
|
+
if ws_path.is_dir():
|
|
44
|
+
specs_path = ws_path / "specs"
|
|
45
|
+
if specs_path.is_dir():
|
|
46
|
+
return specs_path
|
|
47
|
+
return find_specs_directory(workspace)
|
|
48
|
+
return config.specs_dir or find_specs_directory()
|
|
49
|
+
|
|
50
|
+
def _validate_sandbox(path: Path, workspace: Optional[Path] = None) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Validate that a path is within the workspace sandbox.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: Path to validate
|
|
56
|
+
workspace: Workspace root (defaults to specs_dir parent)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if path is within sandbox, False otherwise
|
|
60
|
+
"""
|
|
61
|
+
if workspace is None:
|
|
62
|
+
specs_dir = _get_specs_dir()
|
|
63
|
+
if specs_dir:
|
|
64
|
+
workspace = specs_dir.parent
|
|
65
|
+
else:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
path.resolve().relative_to(workspace.resolve())
|
|
70
|
+
return True
|
|
71
|
+
except ValueError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Resource: foundry://specs/ - List all specs
|
|
75
|
+
@mcp.resource("foundry://specs/")
|
|
76
|
+
def resource_specs_list() -> str:
|
|
77
|
+
"""
|
|
78
|
+
List all specifications.
|
|
79
|
+
|
|
80
|
+
Returns JSON with all specs across all status folders.
|
|
81
|
+
"""
|
|
82
|
+
specs_dir = _get_specs_dir()
|
|
83
|
+
if not specs_dir:
|
|
84
|
+
return json.dumps({
|
|
85
|
+
"success": False,
|
|
86
|
+
"schema_version": SCHEMA_VERSION,
|
|
87
|
+
"error": "No specs directory found",
|
|
88
|
+
}, separators=(",", ":"))
|
|
89
|
+
|
|
90
|
+
specs = list_specs(specs_dir=specs_dir)
|
|
91
|
+
|
|
92
|
+
return json.dumps({
|
|
93
|
+
"success": True,
|
|
94
|
+
"schema_version": SCHEMA_VERSION,
|
|
95
|
+
"specs": specs,
|
|
96
|
+
"count": len(specs),
|
|
97
|
+
}, separators=(",", ":"))
|
|
98
|
+
|
|
99
|
+
# Resource: foundry://specs/{status}/ - List specs by status
|
|
100
|
+
@mcp.resource("foundry://specs/{status}/")
|
|
101
|
+
def resource_specs_by_status(status: str) -> str:
|
|
102
|
+
"""
|
|
103
|
+
List specifications filtered by status.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
status: Status folder (active, pending, completed, archived)
|
|
107
|
+
|
|
108
|
+
Returns JSON with specs in the specified status folder.
|
|
109
|
+
"""
|
|
110
|
+
valid_statuses = {"active", "pending", "completed", "archived"}
|
|
111
|
+
if status not in valid_statuses:
|
|
112
|
+
return json.dumps({
|
|
113
|
+
"success": False,
|
|
114
|
+
"schema_version": SCHEMA_VERSION,
|
|
115
|
+
"error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
|
|
116
|
+
}, separators=(",", ":"))
|
|
117
|
+
|
|
118
|
+
specs_dir = _get_specs_dir()
|
|
119
|
+
if not specs_dir:
|
|
120
|
+
return json.dumps({
|
|
121
|
+
"success": False,
|
|
122
|
+
"schema_version": SCHEMA_VERSION,
|
|
123
|
+
"error": "No specs directory found",
|
|
124
|
+
}, separators=(",", ":"))
|
|
125
|
+
|
|
126
|
+
specs = list_specs(specs_dir=specs_dir, status=status)
|
|
127
|
+
|
|
128
|
+
return json.dumps({
|
|
129
|
+
"success": True,
|
|
130
|
+
"schema_version": SCHEMA_VERSION,
|
|
131
|
+
"status": status,
|
|
132
|
+
"specs": specs,
|
|
133
|
+
"count": len(specs),
|
|
134
|
+
}, separators=(",", ":"))
|
|
135
|
+
|
|
136
|
+
# Resource: foundry://specs/{status}/{spec_id} - Get specific spec
|
|
137
|
+
@mcp.resource("foundry://specs/{status}/{spec_id}")
|
|
138
|
+
def resource_spec_by_status(status: str, spec_id: str) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Get a specification by status and ID.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
status: Status folder (active, pending, completed, archived)
|
|
144
|
+
spec_id: Specification ID
|
|
145
|
+
|
|
146
|
+
Returns JSON with full spec data.
|
|
147
|
+
"""
|
|
148
|
+
valid_statuses = {"active", "pending", "completed", "archived"}
|
|
149
|
+
if status not in valid_statuses:
|
|
150
|
+
return json.dumps({
|
|
151
|
+
"success": False,
|
|
152
|
+
"schema_version": SCHEMA_VERSION,
|
|
153
|
+
"error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
|
|
154
|
+
}, separators=(",", ":"))
|
|
155
|
+
|
|
156
|
+
specs_dir = _get_specs_dir()
|
|
157
|
+
if not specs_dir:
|
|
158
|
+
return json.dumps({
|
|
159
|
+
"success": False,
|
|
160
|
+
"schema_version": SCHEMA_VERSION,
|
|
161
|
+
"error": "No specs directory found",
|
|
162
|
+
}, separators=(",", ":"))
|
|
163
|
+
|
|
164
|
+
# Verify spec is in the specified status folder
|
|
165
|
+
spec_file = specs_dir / status / f"{spec_id}.json"
|
|
166
|
+
if not spec_file.exists():
|
|
167
|
+
return json.dumps({
|
|
168
|
+
"success": False,
|
|
169
|
+
"schema_version": SCHEMA_VERSION,
|
|
170
|
+
"error": f"Spec not found in {status}: {spec_id}",
|
|
171
|
+
}, separators=(",", ":"))
|
|
172
|
+
|
|
173
|
+
# Validate sandbox
|
|
174
|
+
if not _validate_sandbox(spec_file):
|
|
175
|
+
return json.dumps({
|
|
176
|
+
"success": False,
|
|
177
|
+
"schema_version": SCHEMA_VERSION,
|
|
178
|
+
"error": "Access denied: path outside workspace sandbox",
|
|
179
|
+
}, separators=(",", ":"))
|
|
180
|
+
|
|
181
|
+
spec_data = load_spec(spec_id, specs_dir)
|
|
182
|
+
if spec_data is None:
|
|
183
|
+
return json.dumps({
|
|
184
|
+
"success": False,
|
|
185
|
+
"schema_version": SCHEMA_VERSION,
|
|
186
|
+
"error": f"Failed to load spec: {spec_id}",
|
|
187
|
+
}, separators=(",", ":"))
|
|
188
|
+
|
|
189
|
+
# Calculate progress
|
|
190
|
+
hierarchy = spec_data.get("hierarchy", {})
|
|
191
|
+
total_tasks = len(hierarchy)
|
|
192
|
+
completed_tasks = sum(
|
|
193
|
+
1 for task in hierarchy.values()
|
|
194
|
+
if task.get("status") == "completed"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return json.dumps({
|
|
198
|
+
"success": True,
|
|
199
|
+
"schema_version": SCHEMA_VERSION,
|
|
200
|
+
"spec_id": spec_id,
|
|
201
|
+
"status": status,
|
|
202
|
+
"title": spec_data.get("metadata", {}).get("title", spec_data.get("title", "Untitled")),
|
|
203
|
+
"total_tasks": total_tasks,
|
|
204
|
+
"completed_tasks": completed_tasks,
|
|
205
|
+
"progress_percentage": int((completed_tasks / total_tasks * 100)) if total_tasks > 0 else 0,
|
|
206
|
+
"hierarchy": hierarchy,
|
|
207
|
+
"metadata": spec_data.get("metadata", {}),
|
|
208
|
+
"journal": spec_data.get("journal", []),
|
|
209
|
+
}, separators=(",", ":"))
|
|
210
|
+
|
|
211
|
+
# Resource: foundry://specs/{spec_id}/journal - Get spec journal
|
|
212
|
+
@mcp.resource("foundry://specs/{spec_id}/journal")
|
|
213
|
+
def resource_spec_journal(spec_id: str) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Get journal entries for a specification.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
spec_id: Specification ID
|
|
219
|
+
|
|
220
|
+
Returns JSON with journal entries.
|
|
221
|
+
"""
|
|
222
|
+
specs_dir = _get_specs_dir()
|
|
223
|
+
if not specs_dir:
|
|
224
|
+
return json.dumps({
|
|
225
|
+
"success": False,
|
|
226
|
+
"schema_version": SCHEMA_VERSION,
|
|
227
|
+
"error": "No specs directory found",
|
|
228
|
+
}, separators=(",", ":"))
|
|
229
|
+
|
|
230
|
+
# Find spec file (in any status folder)
|
|
231
|
+
spec_file = find_spec_file(spec_id, specs_dir)
|
|
232
|
+
if not spec_file:
|
|
233
|
+
return json.dumps({
|
|
234
|
+
"success": False,
|
|
235
|
+
"schema_version": SCHEMA_VERSION,
|
|
236
|
+
"error": f"Spec not found: {spec_id}",
|
|
237
|
+
}, separators=(",", ":"))
|
|
238
|
+
|
|
239
|
+
# Validate sandbox
|
|
240
|
+
if not _validate_sandbox(spec_file):
|
|
241
|
+
return json.dumps({
|
|
242
|
+
"success": False,
|
|
243
|
+
"schema_version": SCHEMA_VERSION,
|
|
244
|
+
"error": "Access denied: path outside workspace sandbox",
|
|
245
|
+
}, separators=(",", ":"))
|
|
246
|
+
|
|
247
|
+
spec_data = load_spec(spec_id, specs_dir)
|
|
248
|
+
if spec_data is None:
|
|
249
|
+
return json.dumps({
|
|
250
|
+
"success": False,
|
|
251
|
+
"schema_version": SCHEMA_VERSION,
|
|
252
|
+
"error": f"Failed to load spec: {spec_id}",
|
|
253
|
+
}, separators=(",", ":"))
|
|
254
|
+
|
|
255
|
+
# Get journal entries
|
|
256
|
+
entries = get_journal_entries(spec_data)
|
|
257
|
+
|
|
258
|
+
# Convert to serializable format
|
|
259
|
+
journal_data = [
|
|
260
|
+
{
|
|
261
|
+
"timestamp": entry.timestamp,
|
|
262
|
+
"entry_type": entry.entry_type,
|
|
263
|
+
"title": entry.title,
|
|
264
|
+
"content": entry.content,
|
|
265
|
+
"author": entry.author,
|
|
266
|
+
"task_id": entry.task_id,
|
|
267
|
+
"metadata": entry.metadata,
|
|
268
|
+
}
|
|
269
|
+
for entry in entries
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
return json.dumps({
|
|
273
|
+
"success": True,
|
|
274
|
+
"schema_version": SCHEMA_VERSION,
|
|
275
|
+
"spec_id": spec_id,
|
|
276
|
+
"journal": journal_data,
|
|
277
|
+
"count": len(journal_data),
|
|
278
|
+
}, separators=(",", ":"))
|
|
279
|
+
|
|
280
|
+
# Resource: foundry://templates/ - List available templates
|
|
281
|
+
@mcp.resource("foundry://templates/")
|
|
282
|
+
def resource_templates_list() -> str:
|
|
283
|
+
"""
|
|
284
|
+
List available spec templates.
|
|
285
|
+
|
|
286
|
+
Returns JSON with template information.
|
|
287
|
+
"""
|
|
288
|
+
specs_dir = _get_specs_dir()
|
|
289
|
+
if not specs_dir:
|
|
290
|
+
return json.dumps({
|
|
291
|
+
"success": False,
|
|
292
|
+
"schema_version": SCHEMA_VERSION,
|
|
293
|
+
"error": "No specs directory found",
|
|
294
|
+
}, separators=(",", ":"))
|
|
295
|
+
|
|
296
|
+
# Look for templates in specs/templates/ directory
|
|
297
|
+
templates_dir = specs_dir / "templates"
|
|
298
|
+
templates = []
|
|
299
|
+
|
|
300
|
+
if templates_dir.is_dir():
|
|
301
|
+
for template_file in sorted(templates_dir.glob("*.json")):
|
|
302
|
+
try:
|
|
303
|
+
with open(template_file, "r") as f:
|
|
304
|
+
template_data = json.load(f)
|
|
305
|
+
|
|
306
|
+
templates.append({
|
|
307
|
+
"template_id": template_file.stem,
|
|
308
|
+
"title": template_data.get("metadata", {}).get("title", template_data.get("title", template_file.stem)),
|
|
309
|
+
"description": template_data.get("metadata", {}).get("description", ""),
|
|
310
|
+
"file": str(template_file.name),
|
|
311
|
+
})
|
|
312
|
+
except (json.JSONDecodeError, IOError):
|
|
313
|
+
# Skip invalid templates
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# Add built-in templates info
|
|
317
|
+
builtin_templates = [
|
|
318
|
+
{
|
|
319
|
+
"template_id": "basic",
|
|
320
|
+
"title": "Basic Spec",
|
|
321
|
+
"description": "Minimal spec with a single phase",
|
|
322
|
+
"builtin": True,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"template_id": "feature",
|
|
326
|
+
"title": "Feature Development",
|
|
327
|
+
"description": "Standard feature with design, implementation, and verification phases",
|
|
328
|
+
"builtin": True,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"template_id": "bugfix",
|
|
332
|
+
"title": "Bug Fix",
|
|
333
|
+
"description": "Bug investigation, fix, and verification",
|
|
334
|
+
"builtin": True,
|
|
335
|
+
},
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
return json.dumps({
|
|
339
|
+
"success": True,
|
|
340
|
+
"schema_version": SCHEMA_VERSION,
|
|
341
|
+
"templates": templates,
|
|
342
|
+
"builtin_templates": builtin_templates,
|
|
343
|
+
"count": len(templates),
|
|
344
|
+
"builtin_count": len(builtin_templates),
|
|
345
|
+
}, separators=(",", ":"))
|
|
346
|
+
|
|
347
|
+
# Resource: foundry://templates/{template_id} - Get specific template
|
|
348
|
+
@mcp.resource("foundry://templates/{template_id}")
|
|
349
|
+
def resource_template(template_id: str) -> str:
|
|
350
|
+
"""
|
|
351
|
+
Get a specific template by ID.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
template_id: Template ID
|
|
355
|
+
|
|
356
|
+
Returns JSON with template data.
|
|
357
|
+
"""
|
|
358
|
+
specs_dir = _get_specs_dir()
|
|
359
|
+
if not specs_dir:
|
|
360
|
+
return json.dumps({
|
|
361
|
+
"success": False,
|
|
362
|
+
"schema_version": SCHEMA_VERSION,
|
|
363
|
+
"error": "No specs directory found",
|
|
364
|
+
}, separators=(",", ":"))
|
|
365
|
+
|
|
366
|
+
# Check for custom template
|
|
367
|
+
templates_dir = specs_dir / "templates"
|
|
368
|
+
template_file = templates_dir / f"{template_id}.json"
|
|
369
|
+
|
|
370
|
+
if template_file.exists():
|
|
371
|
+
# Validate sandbox
|
|
372
|
+
if not _validate_sandbox(template_file):
|
|
373
|
+
return json.dumps({
|
|
374
|
+
"success": False,
|
|
375
|
+
"schema_version": SCHEMA_VERSION,
|
|
376
|
+
"error": "Access denied: path outside workspace sandbox",
|
|
377
|
+
}, separators=(",", ":"))
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
with open(template_file, "r") as f:
|
|
381
|
+
template_data = json.load(f)
|
|
382
|
+
|
|
383
|
+
return json.dumps({
|
|
384
|
+
"success": True,
|
|
385
|
+
"schema_version": SCHEMA_VERSION,
|
|
386
|
+
"template_id": template_id,
|
|
387
|
+
"template": template_data,
|
|
388
|
+
"builtin": False,
|
|
389
|
+
}, separators=(",", ":"))
|
|
390
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
391
|
+
return json.dumps({
|
|
392
|
+
"success": False,
|
|
393
|
+
"schema_version": SCHEMA_VERSION,
|
|
394
|
+
"error": f"Failed to load template: {e}",
|
|
395
|
+
}, separators=(",", ":"))
|
|
396
|
+
|
|
397
|
+
# Check for builtin template
|
|
398
|
+
builtin_templates = {
|
|
399
|
+
"basic": _get_basic_template(),
|
|
400
|
+
"feature": _get_feature_template(),
|
|
401
|
+
"bugfix": _get_bugfix_template(),
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if template_id in builtin_templates:
|
|
405
|
+
return json.dumps({
|
|
406
|
+
"success": True,
|
|
407
|
+
"schema_version": SCHEMA_VERSION,
|
|
408
|
+
"template_id": template_id,
|
|
409
|
+
"template": builtin_templates[template_id],
|
|
410
|
+
"builtin": True,
|
|
411
|
+
}, separators=(",", ":"))
|
|
412
|
+
|
|
413
|
+
return json.dumps({
|
|
414
|
+
"success": False,
|
|
415
|
+
"schema_version": SCHEMA_VERSION,
|
|
416
|
+
"error": f"Template not found: {template_id}",
|
|
417
|
+
}, separators=(",", ":"))
|
|
418
|
+
|
|
419
|
+
logger.debug("Registered spec resources: foundry://specs/, foundry://specs/{status}/, "
|
|
420
|
+
"foundry://specs/{status}/{spec_id}, foundry://specs/{spec_id}/journal, "
|
|
421
|
+
"foundry://templates/, foundry://templates/{template_id}")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _get_basic_template() -> dict:
|
|
425
|
+
"""Get the basic builtin template."""
|
|
426
|
+
return {
|
|
427
|
+
"spec_id": "{{spec_id}}",
|
|
428
|
+
"title": "{{title}}",
|
|
429
|
+
"metadata": {
|
|
430
|
+
"title": "{{title}}",
|
|
431
|
+
"description": "{{description}}",
|
|
432
|
+
"created_at": "{{timestamp}}",
|
|
433
|
+
},
|
|
434
|
+
"hierarchy": {
|
|
435
|
+
"spec-root": {
|
|
436
|
+
"type": "spec",
|
|
437
|
+
"title": "{{title}}",
|
|
438
|
+
"status": "pending",
|
|
439
|
+
"children": ["phase-1"],
|
|
440
|
+
},
|
|
441
|
+
"phase-1": {
|
|
442
|
+
"type": "phase",
|
|
443
|
+
"title": "Implementation",
|
|
444
|
+
"status": "pending",
|
|
445
|
+
"parent": "spec-root",
|
|
446
|
+
"children": ["task-1-1"],
|
|
447
|
+
},
|
|
448
|
+
"task-1-1": {
|
|
449
|
+
"type": "task",
|
|
450
|
+
"title": "Initial task",
|
|
451
|
+
"status": "pending",
|
|
452
|
+
"parent": "phase-1",
|
|
453
|
+
"children": [],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
"journal": [],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _get_feature_template() -> dict:
|
|
461
|
+
"""Get the feature development builtin template."""
|
|
462
|
+
return {
|
|
463
|
+
"spec_id": "{{spec_id}}",
|
|
464
|
+
"title": "{{title}}",
|
|
465
|
+
"metadata": {
|
|
466
|
+
"title": "{{title}}",
|
|
467
|
+
"description": "{{description}}",
|
|
468
|
+
"created_at": "{{timestamp}}",
|
|
469
|
+
},
|
|
470
|
+
"hierarchy": {
|
|
471
|
+
"spec-root": {
|
|
472
|
+
"type": "spec",
|
|
473
|
+
"title": "{{title}}",
|
|
474
|
+
"status": "pending",
|
|
475
|
+
"children": ["phase-1", "phase-2", "phase-3"],
|
|
476
|
+
},
|
|
477
|
+
"phase-1": {
|
|
478
|
+
"type": "phase",
|
|
479
|
+
"title": "Design",
|
|
480
|
+
"status": "pending",
|
|
481
|
+
"parent": "spec-root",
|
|
482
|
+
"children": ["task-1-1"],
|
|
483
|
+
},
|
|
484
|
+
"task-1-1": {
|
|
485
|
+
"type": "task",
|
|
486
|
+
"title": "Design document",
|
|
487
|
+
"status": "pending",
|
|
488
|
+
"parent": "phase-1",
|
|
489
|
+
"children": [],
|
|
490
|
+
},
|
|
491
|
+
"phase-2": {
|
|
492
|
+
"type": "phase",
|
|
493
|
+
"title": "Implementation",
|
|
494
|
+
"status": "pending",
|
|
495
|
+
"parent": "spec-root",
|
|
496
|
+
"children": ["task-2-1"],
|
|
497
|
+
},
|
|
498
|
+
"task-2-1": {
|
|
499
|
+
"type": "task",
|
|
500
|
+
"title": "Core implementation",
|
|
501
|
+
"status": "pending",
|
|
502
|
+
"parent": "phase-2",
|
|
503
|
+
"children": [],
|
|
504
|
+
},
|
|
505
|
+
"phase-3": {
|
|
506
|
+
"type": "phase",
|
|
507
|
+
"title": "Verification",
|
|
508
|
+
"status": "pending",
|
|
509
|
+
"parent": "spec-root",
|
|
510
|
+
"children": ["verify-3-1"],
|
|
511
|
+
},
|
|
512
|
+
"verify-3-1": {
|
|
513
|
+
"type": "verify",
|
|
514
|
+
"title": "All tests pass",
|
|
515
|
+
"status": "pending",
|
|
516
|
+
"parent": "phase-3",
|
|
517
|
+
"children": [],
|
|
518
|
+
"metadata": {
|
|
519
|
+
"verification_type": "run-tests",
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
"journal": [],
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _get_bugfix_template() -> dict:
|
|
528
|
+
"""Get the bugfix builtin template."""
|
|
529
|
+
return {
|
|
530
|
+
"spec_id": "{{spec_id}}",
|
|
531
|
+
"title": "{{title}}",
|
|
532
|
+
"metadata": {
|
|
533
|
+
"title": "{{title}}",
|
|
534
|
+
"description": "{{description}}",
|
|
535
|
+
"created_at": "{{timestamp}}",
|
|
536
|
+
},
|
|
537
|
+
"hierarchy": {
|
|
538
|
+
"spec-root": {
|
|
539
|
+
"type": "spec",
|
|
540
|
+
"title": "{{title}}",
|
|
541
|
+
"status": "pending",
|
|
542
|
+
"children": ["phase-1", "phase-2"],
|
|
543
|
+
},
|
|
544
|
+
"phase-1": {
|
|
545
|
+
"type": "phase",
|
|
546
|
+
"title": "Investigation",
|
|
547
|
+
"status": "pending",
|
|
548
|
+
"parent": "spec-root",
|
|
549
|
+
"children": ["task-1-1", "task-1-2"],
|
|
550
|
+
},
|
|
551
|
+
"task-1-1": {
|
|
552
|
+
"type": "task",
|
|
553
|
+
"title": "Reproduce bug",
|
|
554
|
+
"status": "pending",
|
|
555
|
+
"parent": "phase-1",
|
|
556
|
+
"children": [],
|
|
557
|
+
},
|
|
558
|
+
"task-1-2": {
|
|
559
|
+
"type": "task",
|
|
560
|
+
"title": "Root cause analysis",
|
|
561
|
+
"status": "pending",
|
|
562
|
+
"parent": "phase-1",
|
|
563
|
+
"children": [],
|
|
564
|
+
},
|
|
565
|
+
"phase-2": {
|
|
566
|
+
"type": "phase",
|
|
567
|
+
"title": "Fix & Verify",
|
|
568
|
+
"status": "pending",
|
|
569
|
+
"parent": "spec-root",
|
|
570
|
+
"children": ["task-2-1", "verify-2-1"],
|
|
571
|
+
},
|
|
572
|
+
"task-2-1": {
|
|
573
|
+
"type": "task",
|
|
574
|
+
"title": "Implement fix",
|
|
575
|
+
"status": "pending",
|
|
576
|
+
"parent": "phase-2",
|
|
577
|
+
"children": [],
|
|
578
|
+
},
|
|
579
|
+
"verify-2-1": {
|
|
580
|
+
"type": "verify",
|
|
581
|
+
"title": "Bug no longer reproduces",
|
|
582
|
+
"status": "pending",
|
|
583
|
+
"parent": "phase-2",
|
|
584
|
+
"children": [],
|
|
585
|
+
"metadata": {
|
|
586
|
+
"verification_type": "manual",
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
"journal": [],
|
|
591
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Bundled JSON schemas for SDD specifications."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_schema(name: str = "sdd-spec-schema.json") -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
9
|
+
"""Load a bundled JSON schema by name.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
name: Schema filename (default: sdd-spec-schema.json).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Tuple of (schema_dict, error_message). On success, error is None.
|
|
16
|
+
On failure, schema is None and error contains the reason.
|
|
17
|
+
"""
|
|
18
|
+
schema_path = Path(__file__).parent / name
|
|
19
|
+
|
|
20
|
+
if not schema_path.exists():
|
|
21
|
+
return None, f"Schema file not found: {name}"
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(schema_path, "r") as f:
|
|
25
|
+
return json.load(f), None
|
|
26
|
+
except json.JSONDecodeError as e:
|
|
27
|
+
return None, f"Invalid JSON in schema: {e}"
|
|
28
|
+
except Exception as e:
|
|
29
|
+
return None, f"Error loading schema: {e}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_spec_schema() -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
33
|
+
"""Load the SDD spec JSON schema.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (schema_dict, error_message).
|
|
37
|
+
"""
|
|
38
|
+
return load_schema("sdd-spec-schema.json")
|