foundry-mcp 0.3.3__py3-none-any.whl → 0.7.0__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.
- foundry_mcp/__init__.py +7 -1
- foundry_mcp/cli/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +235 -5
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/discovery.py +6 -6
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +20 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/progress.py +70 -0
- foundry_mcp/core/prompts/fidelity_review.py +149 -4
- foundry_mcp/core/prompts/markdown_plan_review.py +5 -1
- foundry_mcp/core/prompts/plan_review.py +5 -1
- foundry_mcp/core/providers/claude.py +6 -47
- foundry_mcp/core/providers/codex.py +6 -57
- foundry_mcp/core/providers/cursor_agent.py +3 -44
- foundry_mcp/core/providers/gemini.py +6 -57
- foundry_mcp/core/providers/opencode.py +35 -5
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +425 -0
- foundry_mcp/core/research/models.py +437 -0
- foundry_mcp/core/research/workflows/__init__.py +22 -0
- foundry_mcp/core/research/workflows/base.py +204 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +396 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +450 -0
- foundry_mcp/core/spec.py +2438 -236
- foundry_mcp/core/task.py +1064 -19
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +313 -42
- foundry_mcp/dashboard/components/charts.py +0 -57
- foundry_mcp/dashboard/launcher.py +11 -0
- foundry_mcp/dashboard/views/metrics.py +25 -35
- foundry_mcp/dashboard/views/overview.py +1 -65
- foundry_mcp/resources/specs.py +25 -25
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +33 -5
- foundry_mcp/server.py +38 -0
- foundry_mcp/tools/unified/__init__.py +4 -2
- foundry_mcp/tools/unified/authoring.py +2423 -267
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +235 -6
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +113 -1
- foundry_mcp/tools/unified/research.py +658 -0
- foundry_mcp/tools/unified/review.py +370 -16
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1163 -48
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/METADATA +7 -1
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/RECORD +60 -48
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
"""Metrics page -
|
|
1
|
+
"""Metrics page - viewer with summaries."""
|
|
2
2
|
|
|
3
3
|
import streamlit as st
|
|
4
4
|
|
|
5
5
|
from foundry_mcp.dashboard.components.filters import time_range_filter
|
|
6
|
-
from foundry_mcp.dashboard.components.charts import line_chart, empty_chart
|
|
7
6
|
from foundry_mcp.dashboard.components.cards import kpi_row
|
|
8
7
|
from foundry_mcp.dashboard.data.stores import (
|
|
9
8
|
get_metrics_list,
|
|
@@ -77,8 +76,8 @@ def render():
|
|
|
77
76
|
|
|
78
77
|
st.divider()
|
|
79
78
|
|
|
80
|
-
#
|
|
81
|
-
st.subheader(f"
|
|
79
|
+
# Data table
|
|
80
|
+
st.subheader(f"Data: {selected_metric}")
|
|
82
81
|
timeseries_df = get_metrics_timeseries(selected_metric, since_hours=hours)
|
|
83
82
|
|
|
84
83
|
# Check if we have data, if not try longer time ranges
|
|
@@ -97,42 +96,33 @@ def render():
|
|
|
97
96
|
if display_df is not None and not display_df.empty:
|
|
98
97
|
if time_range_note:
|
|
99
98
|
st.caption(time_range_note)
|
|
100
|
-
|
|
99
|
+
|
|
100
|
+
st.dataframe(
|
|
101
101
|
display_df,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
title=None,
|
|
105
|
-
height=400,
|
|
102
|
+
use_container_width=True,
|
|
103
|
+
hide_index=True,
|
|
106
104
|
)
|
|
107
105
|
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
# Export
|
|
107
|
+
col1, col2 = st.columns(2)
|
|
108
|
+
with col1:
|
|
109
|
+
csv = display_df.to_csv(index=False)
|
|
110
|
+
st.download_button(
|
|
111
|
+
label="Download CSV",
|
|
112
|
+
data=csv,
|
|
113
|
+
file_name=f"{selected_metric}_export.csv",
|
|
114
|
+
mime="text/csv",
|
|
115
|
+
)
|
|
116
|
+
with col2:
|
|
117
|
+
json_data = display_df.to_json(orient="records")
|
|
118
|
+
st.download_button(
|
|
119
|
+
label="Download JSON",
|
|
120
|
+
data=json_data,
|
|
121
|
+
file_name=f"{selected_metric}_export.json",
|
|
122
|
+
mime="application/json",
|
|
114
123
|
)
|
|
115
|
-
|
|
116
|
-
# Export
|
|
117
|
-
col1, col2 = st.columns(2)
|
|
118
|
-
with col1:
|
|
119
|
-
csv = display_df.to_csv(index=False)
|
|
120
|
-
st.download_button(
|
|
121
|
-
label="Download CSV",
|
|
122
|
-
data=csv,
|
|
123
|
-
file_name=f"{selected_metric}_export.csv",
|
|
124
|
-
mime="text/csv",
|
|
125
|
-
)
|
|
126
|
-
with col2:
|
|
127
|
-
json_data = display_df.to_json(orient="records")
|
|
128
|
-
st.download_button(
|
|
129
|
-
label="Download JSON",
|
|
130
|
-
data=json_data,
|
|
131
|
-
file_name=f"{selected_metric}_export.json",
|
|
132
|
-
mime="application/json",
|
|
133
|
-
)
|
|
134
124
|
else:
|
|
135
|
-
|
|
125
|
+
st.info(f"No data available for {selected_metric}")
|
|
136
126
|
|
|
137
127
|
# Tool Action Breakdown
|
|
138
128
|
st.divider()
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
"""Overview page - dashboard home with KPIs and summary
|
|
1
|
+
"""Overview page - dashboard home with KPIs and summary."""
|
|
2
2
|
|
|
3
3
|
import streamlit as st
|
|
4
4
|
|
|
5
5
|
from foundry_mcp.dashboard.components.cards import kpi_row
|
|
6
|
-
from foundry_mcp.dashboard.components.charts import line_chart, empty_chart
|
|
7
6
|
from foundry_mcp.dashboard.data.stores import (
|
|
8
7
|
get_overview_summary,
|
|
9
|
-
get_metrics_timeseries,
|
|
10
8
|
get_errors,
|
|
11
9
|
get_error_patterns,
|
|
12
10
|
get_top_tool_actions,
|
|
@@ -40,68 +38,6 @@ def render():
|
|
|
40
38
|
|
|
41
39
|
st.divider()
|
|
42
40
|
|
|
43
|
-
# Charts Row
|
|
44
|
-
col1, col2 = st.columns(2)
|
|
45
|
-
|
|
46
|
-
with col1:
|
|
47
|
-
st.subheader("Tool Invocations (Last 24h)")
|
|
48
|
-
invocations_df = get_metrics_timeseries("tool_invocations_total", since_hours=24)
|
|
49
|
-
if invocations_df is not None and not invocations_df.empty:
|
|
50
|
-
line_chart(
|
|
51
|
-
invocations_df,
|
|
52
|
-
x="timestamp",
|
|
53
|
-
y="value",
|
|
54
|
-
title=None,
|
|
55
|
-
height=300,
|
|
56
|
-
)
|
|
57
|
-
else:
|
|
58
|
-
# Try with longer time range if 24h is empty
|
|
59
|
-
invocations_df_7d = get_metrics_timeseries("tool_invocations_total", since_hours=168)
|
|
60
|
-
if invocations_df_7d is not None and not invocations_df_7d.empty:
|
|
61
|
-
st.caption("No data in last 24h - showing last 7 days")
|
|
62
|
-
line_chart(
|
|
63
|
-
invocations_df_7d,
|
|
64
|
-
x="timestamp",
|
|
65
|
-
y="value",
|
|
66
|
-
title=None,
|
|
67
|
-
height=300,
|
|
68
|
-
)
|
|
69
|
-
else:
|
|
70
|
-
empty_chart("No invocation data available (metrics may be older than 7 days)")
|
|
71
|
-
|
|
72
|
-
with col2:
|
|
73
|
-
st.subheader("Error Rate (Last 24h)")
|
|
74
|
-
errors_df = get_errors(since_hours=24, limit=500)
|
|
75
|
-
|
|
76
|
-
# Try fallback to 7 days if 24h is empty
|
|
77
|
-
time_note = None
|
|
78
|
-
if errors_df is None or errors_df.empty:
|
|
79
|
-
errors_df = get_errors(since_hours=168, limit=500)
|
|
80
|
-
if errors_df is not None and not errors_df.empty:
|
|
81
|
-
time_note = "No data in last 24h - showing last 7 days"
|
|
82
|
-
|
|
83
|
-
if errors_df is not None and not errors_df.empty:
|
|
84
|
-
if time_note:
|
|
85
|
-
st.caption(time_note)
|
|
86
|
-
# Group by hour for error rate
|
|
87
|
-
try:
|
|
88
|
-
errors_df["hour"] = errors_df["timestamp"].dt.floor("H")
|
|
89
|
-
hourly_errors = errors_df.groupby("hour").size().reset_index(name="count")
|
|
90
|
-
hourly_errors.columns = ["timestamp", "value"]
|
|
91
|
-
line_chart(
|
|
92
|
-
hourly_errors,
|
|
93
|
-
x="timestamp",
|
|
94
|
-
y="value",
|
|
95
|
-
title=None,
|
|
96
|
-
height=300,
|
|
97
|
-
)
|
|
98
|
-
except Exception:
|
|
99
|
-
empty_chart("Could not process error data")
|
|
100
|
-
else:
|
|
101
|
-
empty_chart("No error data available")
|
|
102
|
-
|
|
103
|
-
st.divider()
|
|
104
|
-
|
|
105
41
|
# Bottom Row - Patterns and Recent
|
|
106
42
|
col1, col2 = st.columns(2)
|
|
107
43
|
|
foundry_mcp/resources/specs.py
CHANGED
|
@@ -85,7 +85,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
85
85
|
"success": False,
|
|
86
86
|
"schema_version": SCHEMA_VERSION,
|
|
87
87
|
"error": "No specs directory found",
|
|
88
|
-
})
|
|
88
|
+
}, separators=(",", ":"))
|
|
89
89
|
|
|
90
90
|
specs = list_specs(specs_dir=specs_dir)
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
94
94
|
"schema_version": SCHEMA_VERSION,
|
|
95
95
|
"specs": specs,
|
|
96
96
|
"count": len(specs),
|
|
97
|
-
})
|
|
97
|
+
}, separators=(",", ":"))
|
|
98
98
|
|
|
99
99
|
# Resource: foundry://specs/{status}/ - List specs by status
|
|
100
100
|
@mcp.resource("foundry://specs/{status}/")
|
|
@@ -113,7 +113,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
113
113
|
"success": False,
|
|
114
114
|
"schema_version": SCHEMA_VERSION,
|
|
115
115
|
"error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
|
|
116
|
-
})
|
|
116
|
+
}, separators=(",", ":"))
|
|
117
117
|
|
|
118
118
|
specs_dir = _get_specs_dir()
|
|
119
119
|
if not specs_dir:
|
|
@@ -121,7 +121,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
121
121
|
"success": False,
|
|
122
122
|
"schema_version": SCHEMA_VERSION,
|
|
123
123
|
"error": "No specs directory found",
|
|
124
|
-
})
|
|
124
|
+
}, separators=(",", ":"))
|
|
125
125
|
|
|
126
126
|
specs = list_specs(specs_dir=specs_dir, status=status)
|
|
127
127
|
|
|
@@ -131,7 +131,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
131
131
|
"status": status,
|
|
132
132
|
"specs": specs,
|
|
133
133
|
"count": len(specs),
|
|
134
|
-
})
|
|
134
|
+
}, separators=(",", ":"))
|
|
135
135
|
|
|
136
136
|
# Resource: foundry://specs/{status}/{spec_id} - Get specific spec
|
|
137
137
|
@mcp.resource("foundry://specs/{status}/{spec_id}")
|
|
@@ -151,7 +151,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
151
151
|
"success": False,
|
|
152
152
|
"schema_version": SCHEMA_VERSION,
|
|
153
153
|
"error": f"Invalid status: {status}. Must be one of: {', '.join(sorted(valid_statuses))}",
|
|
154
|
-
})
|
|
154
|
+
}, separators=(",", ":"))
|
|
155
155
|
|
|
156
156
|
specs_dir = _get_specs_dir()
|
|
157
157
|
if not specs_dir:
|
|
@@ -159,7 +159,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
159
159
|
"success": False,
|
|
160
160
|
"schema_version": SCHEMA_VERSION,
|
|
161
161
|
"error": "No specs directory found",
|
|
162
|
-
})
|
|
162
|
+
}, separators=(",", ":"))
|
|
163
163
|
|
|
164
164
|
# Verify spec is in the specified status folder
|
|
165
165
|
spec_file = specs_dir / status / f"{spec_id}.json"
|
|
@@ -168,7 +168,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
168
168
|
"success": False,
|
|
169
169
|
"schema_version": SCHEMA_VERSION,
|
|
170
170
|
"error": f"Spec not found in {status}: {spec_id}",
|
|
171
|
-
})
|
|
171
|
+
}, separators=(",", ":"))
|
|
172
172
|
|
|
173
173
|
# Validate sandbox
|
|
174
174
|
if not _validate_sandbox(spec_file):
|
|
@@ -176,7 +176,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
176
176
|
"success": False,
|
|
177
177
|
"schema_version": SCHEMA_VERSION,
|
|
178
178
|
"error": "Access denied: path outside workspace sandbox",
|
|
179
|
-
})
|
|
179
|
+
}, separators=(",", ":"))
|
|
180
180
|
|
|
181
181
|
spec_data = load_spec(spec_id, specs_dir)
|
|
182
182
|
if spec_data is None:
|
|
@@ -184,7 +184,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
184
184
|
"success": False,
|
|
185
185
|
"schema_version": SCHEMA_VERSION,
|
|
186
186
|
"error": f"Failed to load spec: {spec_id}",
|
|
187
|
-
})
|
|
187
|
+
}, separators=(",", ":"))
|
|
188
188
|
|
|
189
189
|
# Calculate progress
|
|
190
190
|
hierarchy = spec_data.get("hierarchy", {})
|
|
@@ -206,7 +206,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
206
206
|
"hierarchy": hierarchy,
|
|
207
207
|
"metadata": spec_data.get("metadata", {}),
|
|
208
208
|
"journal": spec_data.get("journal", []),
|
|
209
|
-
})
|
|
209
|
+
}, separators=(",", ":"))
|
|
210
210
|
|
|
211
211
|
# Resource: foundry://specs/{spec_id}/journal - Get spec journal
|
|
212
212
|
@mcp.resource("foundry://specs/{spec_id}/journal")
|
|
@@ -225,7 +225,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
225
225
|
"success": False,
|
|
226
226
|
"schema_version": SCHEMA_VERSION,
|
|
227
227
|
"error": "No specs directory found",
|
|
228
|
-
})
|
|
228
|
+
}, separators=(",", ":"))
|
|
229
229
|
|
|
230
230
|
# Find spec file (in any status folder)
|
|
231
231
|
spec_file = find_spec_file(spec_id, specs_dir)
|
|
@@ -234,7 +234,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
234
234
|
"success": False,
|
|
235
235
|
"schema_version": SCHEMA_VERSION,
|
|
236
236
|
"error": f"Spec not found: {spec_id}",
|
|
237
|
-
})
|
|
237
|
+
}, separators=(",", ":"))
|
|
238
238
|
|
|
239
239
|
# Validate sandbox
|
|
240
240
|
if not _validate_sandbox(spec_file):
|
|
@@ -242,7 +242,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
242
242
|
"success": False,
|
|
243
243
|
"schema_version": SCHEMA_VERSION,
|
|
244
244
|
"error": "Access denied: path outside workspace sandbox",
|
|
245
|
-
})
|
|
245
|
+
}, separators=(",", ":"))
|
|
246
246
|
|
|
247
247
|
spec_data = load_spec(spec_id, specs_dir)
|
|
248
248
|
if spec_data is None:
|
|
@@ -250,7 +250,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
250
250
|
"success": False,
|
|
251
251
|
"schema_version": SCHEMA_VERSION,
|
|
252
252
|
"error": f"Failed to load spec: {spec_id}",
|
|
253
|
-
})
|
|
253
|
+
}, separators=(",", ":"))
|
|
254
254
|
|
|
255
255
|
# Get journal entries
|
|
256
256
|
entries = get_journal_entries(spec_data)
|
|
@@ -275,7 +275,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
275
275
|
"spec_id": spec_id,
|
|
276
276
|
"journal": journal_data,
|
|
277
277
|
"count": len(journal_data),
|
|
278
|
-
})
|
|
278
|
+
}, separators=(",", ":"))
|
|
279
279
|
|
|
280
280
|
# Resource: foundry://templates/ - List available templates
|
|
281
281
|
@mcp.resource("foundry://templates/")
|
|
@@ -291,7 +291,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
291
291
|
"success": False,
|
|
292
292
|
"schema_version": SCHEMA_VERSION,
|
|
293
293
|
"error": "No specs directory found",
|
|
294
|
-
})
|
|
294
|
+
}, separators=(",", ":"))
|
|
295
295
|
|
|
296
296
|
# Look for templates in specs/templates/ directory
|
|
297
297
|
templates_dir = specs_dir / "templates"
|
|
@@ -342,7 +342,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
342
342
|
"builtin_templates": builtin_templates,
|
|
343
343
|
"count": len(templates),
|
|
344
344
|
"builtin_count": len(builtin_templates),
|
|
345
|
-
})
|
|
345
|
+
}, separators=(",", ":"))
|
|
346
346
|
|
|
347
347
|
# Resource: foundry://templates/{template_id} - Get specific template
|
|
348
348
|
@mcp.resource("foundry://templates/{template_id}")
|
|
@@ -361,7 +361,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
361
361
|
"success": False,
|
|
362
362
|
"schema_version": SCHEMA_VERSION,
|
|
363
363
|
"error": "No specs directory found",
|
|
364
|
-
})
|
|
364
|
+
}, separators=(",", ":"))
|
|
365
365
|
|
|
366
366
|
# Check for custom template
|
|
367
367
|
templates_dir = specs_dir / "templates"
|
|
@@ -374,7 +374,7 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
374
374
|
"success": False,
|
|
375
375
|
"schema_version": SCHEMA_VERSION,
|
|
376
376
|
"error": "Access denied: path outside workspace sandbox",
|
|
377
|
-
})
|
|
377
|
+
}, separators=(",", ":"))
|
|
378
378
|
|
|
379
379
|
try:
|
|
380
380
|
with open(template_file, "r") as f:
|
|
@@ -386,13 +386,13 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
386
386
|
"template_id": template_id,
|
|
387
387
|
"template": template_data,
|
|
388
388
|
"builtin": False,
|
|
389
|
-
})
|
|
389
|
+
}, separators=(",", ":"))
|
|
390
390
|
except (json.JSONDecodeError, IOError) as e:
|
|
391
391
|
return json.dumps({
|
|
392
392
|
"success": False,
|
|
393
393
|
"schema_version": SCHEMA_VERSION,
|
|
394
394
|
"error": f"Failed to load template: {e}",
|
|
395
|
-
})
|
|
395
|
+
}, separators=(",", ":"))
|
|
396
396
|
|
|
397
397
|
# Check for builtin template
|
|
398
398
|
builtin_templates = {
|
|
@@ -408,13 +408,13 @@ def register_spec_resources(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
408
408
|
"template_id": template_id,
|
|
409
409
|
"template": builtin_templates[template_id],
|
|
410
410
|
"builtin": True,
|
|
411
|
-
})
|
|
411
|
+
}, separators=(",", ":"))
|
|
412
412
|
|
|
413
413
|
return json.dumps({
|
|
414
414
|
"success": False,
|
|
415
415
|
"schema_version": SCHEMA_VERSION,
|
|
416
416
|
"error": f"Template not found: {template_id}",
|
|
417
|
-
})
|
|
417
|
+
}, separators=(",", ":"))
|
|
418
418
|
|
|
419
419
|
logger.debug("Registered spec resources: foundry://specs/, foundry://specs/{status}/, "
|
|
420
420
|
"foundry://specs/{status}/{spec_id}, foundry://specs/{spec_id}/journal, "
|
|
@@ -516,7 +516,7 @@ def _get_feature_template() -> dict:
|
|
|
516
516
|
"parent": "phase-3",
|
|
517
517
|
"children": [],
|
|
518
518
|
"metadata": {
|
|
519
|
-
"verification_type": "
|
|
519
|
+
"verification_type": "run-tests",
|
|
520
520
|
},
|
|
521
521
|
},
|
|
522
522
|
},
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Intake Item Schema",
|
|
4
|
+
"description": "Schema for bikelane intake items - fast work idea capture",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": [
|
|
7
|
+
"schema_version",
|
|
8
|
+
"id",
|
|
9
|
+
"title",
|
|
10
|
+
"status",
|
|
11
|
+
"created_at",
|
|
12
|
+
"updated_at"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"schema_version": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"const": "intake-v1",
|
|
18
|
+
"description": "Schema version identifier, fixed value 'intake-v1'"
|
|
19
|
+
},
|
|
20
|
+
"id": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^intake-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
|
|
23
|
+
"description": "Unique intake item identifier in format 'intake-<uuid4>'"
|
|
24
|
+
},
|
|
25
|
+
"title": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"maxLength": 140,
|
|
29
|
+
"description": "Brief title for the intake item (1-140 characters)"
|
|
30
|
+
},
|
|
31
|
+
"description": {
|
|
32
|
+
"type": ["string", "null"],
|
|
33
|
+
"maxLength": 2000,
|
|
34
|
+
"description": "Detailed description of the work item (max 2000 characters)"
|
|
35
|
+
},
|
|
36
|
+
"status": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["new", "dismissed"],
|
|
39
|
+
"description": "Item status: 'new' for triage queue, 'dismissed' for archived"
|
|
40
|
+
},
|
|
41
|
+
"priority": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["p0", "p1", "p2", "p3", "p4"],
|
|
44
|
+
"default": "p2",
|
|
45
|
+
"description": "Priority level from p0 (highest) to p4 (lowest), defaults to p2"
|
|
46
|
+
},
|
|
47
|
+
"tags": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1,
|
|
52
|
+
"maxLength": 32,
|
|
53
|
+
"pattern": "^[a-z0-9_-]+$"
|
|
54
|
+
},
|
|
55
|
+
"maxItems": 20,
|
|
56
|
+
"uniqueItems": true,
|
|
57
|
+
"default": [],
|
|
58
|
+
"description": "Tags for categorization (max 20 items, each 1-32 chars, lowercase alphanumeric with _ and -)"
|
|
59
|
+
},
|
|
60
|
+
"source": {
|
|
61
|
+
"type": ["string", "null"],
|
|
62
|
+
"maxLength": 100,
|
|
63
|
+
"description": "Origin of the intake item (e.g., 'user-note', 'slack', 'email') max 100 chars"
|
|
64
|
+
},
|
|
65
|
+
"requester": {
|
|
66
|
+
"type": ["string", "null"],
|
|
67
|
+
"maxLength": 100,
|
|
68
|
+
"description": "Person or entity who requested the work (max 100 chars)"
|
|
69
|
+
},
|
|
70
|
+
"idempotency_key": {
|
|
71
|
+
"type": ["string", "null"],
|
|
72
|
+
"maxLength": 64,
|
|
73
|
+
"description": "Optional client-provided key for deduplication (max 64 chars)"
|
|
74
|
+
},
|
|
75
|
+
"created_at": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"format": "date-time",
|
|
78
|
+
"pattern": ".*Z$",
|
|
79
|
+
"description": "ISO 8601 UTC timestamp with Z suffix when item was created"
|
|
80
|
+
},
|
|
81
|
+
"updated_at": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"format": "date-time",
|
|
84
|
+
"pattern": ".*Z$",
|
|
85
|
+
"description": "ISO 8601 UTC timestamp with Z suffix of last update"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"additionalProperties": false
|
|
89
|
+
}
|
|
@@ -30,6 +30,22 @@
|
|
|
30
30
|
"format": "date-time",
|
|
31
31
|
"description": "ISO 8601 timestamp of last update"
|
|
32
32
|
},
|
|
33
|
+
"status": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["pending", "in_progress", "completed", "blocked"],
|
|
36
|
+
"description": "Overall spec status (computed from task progress)"
|
|
37
|
+
},
|
|
38
|
+
"progress_percentage": {
|
|
39
|
+
"type": "integer",
|
|
40
|
+
"minimum": 0,
|
|
41
|
+
"maximum": 100,
|
|
42
|
+
"description": "Overall progress as percentage (0-100), computed from completed/total tasks"
|
|
43
|
+
},
|
|
44
|
+
"current_phase": {
|
|
45
|
+
"type": ["string", "null"],
|
|
46
|
+
"pattern": "^phase-\\d+(?:-[\\w-]+)*$",
|
|
47
|
+
"description": "ID of the currently active phase (first in_progress or first pending)"
|
|
48
|
+
},
|
|
33
49
|
"metadata": {
|
|
34
50
|
"type": "object",
|
|
35
51
|
"description": "Optional spec-level metadata",
|
|
@@ -42,6 +58,10 @@
|
|
|
42
58
|
"type": "string",
|
|
43
59
|
"description": "High-level description of the specification."
|
|
44
60
|
},
|
|
61
|
+
"mission": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Concise mission/goal statement for the entire specification."
|
|
64
|
+
},
|
|
45
65
|
"objectives": {
|
|
46
66
|
"type": "array",
|
|
47
67
|
"items": {
|
|
@@ -60,10 +80,6 @@
|
|
|
60
80
|
"owner": {
|
|
61
81
|
"type": "string"
|
|
62
82
|
},
|
|
63
|
-
"status": {
|
|
64
|
-
"type": "string",
|
|
65
|
-
"description": "Workflow status for the specification."
|
|
66
|
-
},
|
|
67
83
|
"assumptions": {
|
|
68
84
|
"type": "array",
|
|
69
85
|
"items": {
|
|
@@ -227,6 +243,17 @@
|
|
|
227
243
|
"type": "string",
|
|
228
244
|
"description": "Primary file impacted by the node."
|
|
229
245
|
},
|
|
246
|
+
"description": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"description": "Task or node description."
|
|
249
|
+
},
|
|
250
|
+
"acceptance_criteria": {
|
|
251
|
+
"type": "array",
|
|
252
|
+
"items": {
|
|
253
|
+
"type": "string"
|
|
254
|
+
},
|
|
255
|
+
"description": "Acceptance criteria for the node."
|
|
256
|
+
},
|
|
230
257
|
"details": {
|
|
231
258
|
"oneOf": [
|
|
232
259
|
{"type": "string"},
|
|
@@ -253,7 +280,8 @@
|
|
|
253
280
|
"type": "string",
|
|
254
281
|
"enum": [
|
|
255
282
|
"run-tests",
|
|
256
|
-
"fidelity"
|
|
283
|
+
"fidelity",
|
|
284
|
+
"manual"
|
|
257
285
|
]
|
|
258
286
|
},
|
|
259
287
|
"agent": {
|
foundry_mcp/server.py
CHANGED
|
@@ -137,10 +137,48 @@ def create_server(config: Optional[ServerConfig] = None) -> FastMCP:
|
|
|
137
137
|
return mcp
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def _patch_fastmcp_json_serialization() -> None:
|
|
141
|
+
"""Patch FastMCP to use minified JSON for tool responses.
|
|
142
|
+
|
|
143
|
+
FastMCP serializes dict responses with indent=2 by default.
|
|
144
|
+
This patch makes responses minified (no indentation) for smaller payloads.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
import pydantic_core
|
|
148
|
+
from itertools import chain
|
|
149
|
+
from mcp.types import TextContent, ContentBlock
|
|
150
|
+
from mcp.server.fastmcp.utilities import func_metadata
|
|
151
|
+
from mcp.server.fastmcp.utilities.types import Image, Audio
|
|
152
|
+
|
|
153
|
+
def _minified_convert_to_content(result):
|
|
154
|
+
if result is None:
|
|
155
|
+
return []
|
|
156
|
+
if isinstance(result, ContentBlock):
|
|
157
|
+
return [result]
|
|
158
|
+
if isinstance(result, Image):
|
|
159
|
+
return [result.to_image_content()]
|
|
160
|
+
if isinstance(result, Audio):
|
|
161
|
+
return [result.to_audio_content()]
|
|
162
|
+
if isinstance(result, (list, tuple)):
|
|
163
|
+
return list(chain.from_iterable(
|
|
164
|
+
_minified_convert_to_content(item) for item in result
|
|
165
|
+
))
|
|
166
|
+
if not isinstance(result, str):
|
|
167
|
+
# Minified: no indent
|
|
168
|
+
result = pydantic_core.to_json(result, fallback=str).decode()
|
|
169
|
+
return [TextContent(type="text", text=result)]
|
|
170
|
+
|
|
171
|
+
func_metadata._convert_to_content = _minified_convert_to_content
|
|
172
|
+
logger.debug("Patched FastMCP for minified JSON responses")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.warning("Failed to patch FastMCP JSON serialization: %s", e)
|
|
175
|
+
|
|
176
|
+
|
|
140
177
|
def main() -> None:
|
|
141
178
|
"""Main entry point for the foundry-mcp server."""
|
|
142
179
|
|
|
143
180
|
try:
|
|
181
|
+
_patch_fastmcp_json_serialization()
|
|
144
182
|
config = get_config()
|
|
145
183
|
server = create_server(config)
|
|
146
184
|
|
|
@@ -19,6 +19,7 @@ from .review import register_unified_review_tool
|
|
|
19
19
|
from .spec import register_unified_spec_tool
|
|
20
20
|
from .server import register_unified_server_tool
|
|
21
21
|
from .test import register_unified_test_tool
|
|
22
|
+
from .research import register_unified_research_tool
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING: # pragma: no cover - import-time typing only
|
|
@@ -27,8 +28,7 @@ if TYPE_CHECKING: # pragma: no cover - import-time typing only
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def register_unified_tools(mcp: "FastMCP", config: "ServerConfig") -> None:
|
|
30
|
-
"""Register
|
|
31
|
-
|
|
31
|
+
"""Register all unified tool routers."""
|
|
32
32
|
register_unified_health_tool(mcp, config)
|
|
33
33
|
register_unified_plan_tool(mcp, config)
|
|
34
34
|
register_unified_pr_tool(mcp, config)
|
|
@@ -49,6 +49,7 @@ def register_unified_tools(mcp: "FastMCP", config: "ServerConfig") -> None:
|
|
|
49
49
|
register_unified_verification_tool(mcp, config)
|
|
50
50
|
register_unified_server_tool(mcp, config)
|
|
51
51
|
register_unified_test_tool(mcp, config)
|
|
52
|
+
register_unified_research_tool(mcp, config)
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
__all__ = [
|
|
@@ -68,4 +69,5 @@ __all__ = [
|
|
|
68
69
|
"register_unified_verification_tool",
|
|
69
70
|
"register_unified_server_tool",
|
|
70
71
|
"register_unified_test_tool",
|
|
72
|
+
"register_unified_research_tool",
|
|
71
73
|
]
|