foundry-mcp 0.3.3__py3-none-any.whl → 0.8.10__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/__init__.py +0 -13
- foundry_mcp/cli/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/session.py +1 -8
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/context.py +39 -0
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +615 -11
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/batch_operations.py +1196 -0
- foundry_mcp/core/discovery.py +7 -7
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +28 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/naming.py +25 -2
- foundry_mcp/core/progress.py +70 -0
- foundry_mcp/core/prometheus.py +0 -13
- 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/__init__.py +12 -0
- foundry_mcp/core/providers/base.py +39 -0
- foundry_mcp/core/providers/claude.py +51 -48
- foundry_mcp/core/providers/codex.py +70 -60
- foundry_mcp/core/providers/cursor_agent.py +25 -47
- foundry_mcp/core/providers/detectors.py +34 -7
- foundry_mcp/core/providers/gemini.py +69 -58
- foundry_mcp/core/providers/opencode.py +101 -47
- foundry_mcp/core/providers/package-lock.json +4 -4
- foundry_mcp/core/providers/package.json +1 -1
- foundry_mcp/core/providers/validation.py +128 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1220 -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 +4020 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +690 -0
- foundry_mcp/core/spec.py +2439 -236
- foundry_mcp/core/task.py +1205 -31
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +319 -43
- 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 +0 -14
- foundry_mcp/tools/unified/__init__.py +39 -18
- foundry_mcp/tools/unified/authoring.py +2371 -248
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +434 -32
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +133 -2
- foundry_mcp/tools/unified/provider.py +0 -40
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +374 -17
- foundry_mcp/tools/unified/review_helpers.py +16 -1
- foundry_mcp/tools/unified/server.py +9 -24
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1664 -30
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/METADATA +8 -1
- foundry_mcp-0.8.10.dist-info/RECORD +153 -0
- foundry_mcp/cli/flags.py +0 -266
- foundry_mcp/core/feature_flags.py +0 -592
- foundry_mcp-0.3.3.dist-info/RECORD +0 -135
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.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
|
@@ -17,7 +17,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
17
17
|
|
|
18
18
|
from foundry_mcp.config import ServerConfig, get_config
|
|
19
19
|
from foundry_mcp.core.observability import audit_log, get_observability_manager
|
|
20
|
-
from foundry_mcp.core.feature_flags import get_flag_service
|
|
21
20
|
from foundry_mcp.resources.specs import register_spec_resources
|
|
22
21
|
from foundry_mcp.prompts.workflows import register_workflow_prompts
|
|
23
22
|
from foundry_mcp.tools.unified import register_unified_tools
|
|
@@ -99,18 +98,6 @@ def _init_metrics_persistence(config: ServerConfig) -> None:
|
|
|
99
98
|
logger.warning("Failed to initialize metrics persistence: %s", exc)
|
|
100
99
|
|
|
101
100
|
|
|
102
|
-
def _apply_feature_flag_overrides_from_env() -> None:
|
|
103
|
-
"""Apply comma-separated feature flag overrides from `FEATURE_FLAGS`."""
|
|
104
|
-
|
|
105
|
-
raw = os.environ.get("FEATURE_FLAGS")
|
|
106
|
-
if not raw:
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
flag_service = get_flag_service()
|
|
110
|
-
for name in [part.strip() for part in raw.split(",") if part.strip()]:
|
|
111
|
-
flag_service.set_override("anonymous", name, True)
|
|
112
|
-
|
|
113
|
-
|
|
114
101
|
def create_server(config: Optional[ServerConfig] = None) -> FastMCP:
|
|
115
102
|
"""Create and configure the FastMCP server instance."""
|
|
116
103
|
|
|
@@ -119,7 +106,6 @@ def create_server(config: Optional[ServerConfig] = None) -> FastMCP:
|
|
|
119
106
|
|
|
120
107
|
config.setup_logging()
|
|
121
108
|
|
|
122
|
-
_apply_feature_flag_overrides_from_env()
|
|
123
109
|
_init_observability(config)
|
|
124
110
|
_init_error_collection(config)
|
|
125
111
|
_init_metrics_persistence(config)
|
|
@@ -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,28 +28,47 @@ 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
|
+
"""Register all unified tool routers."""
|
|
32
|
+
disabled = set(config.disabled_tools)
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if "health" not in disabled:
|
|
35
|
+
register_unified_health_tool(mcp, config)
|
|
36
|
+
if "plan" not in disabled:
|
|
37
|
+
register_unified_plan_tool(mcp, config)
|
|
38
|
+
if "pr" not in disabled:
|
|
39
|
+
register_unified_pr_tool(mcp, config)
|
|
40
|
+
if "error" not in disabled:
|
|
41
|
+
register_unified_error_tool(mcp, config)
|
|
42
|
+
if "metrics" not in disabled:
|
|
43
|
+
register_unified_metrics_tool(mcp, config)
|
|
44
|
+
if "journal" not in disabled:
|
|
45
|
+
register_unified_journal_tool(mcp, config)
|
|
46
|
+
if "authoring" not in disabled:
|
|
47
|
+
register_unified_authoring_tool(mcp, config)
|
|
48
|
+
if "review" not in disabled:
|
|
49
|
+
register_unified_review_tool(mcp, config)
|
|
50
|
+
if "spec" not in disabled:
|
|
51
|
+
register_unified_spec_tool(mcp, config)
|
|
41
52
|
|
|
42
53
|
from importlib import import_module
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
if "task" not in disabled:
|
|
56
|
+
_task_router = import_module("foundry_mcp.tools.unified.task")
|
|
57
|
+
_task_router.register_unified_task_tool(mcp, config)
|
|
58
|
+
if "provider" not in disabled:
|
|
59
|
+
register_unified_provider_tool(mcp, config)
|
|
60
|
+
if "environment" not in disabled:
|
|
61
|
+
register_unified_environment_tool(mcp, config)
|
|
62
|
+
if "lifecycle" not in disabled:
|
|
63
|
+
register_unified_lifecycle_tool(mcp, config)
|
|
64
|
+
if "verification" not in disabled:
|
|
65
|
+
register_unified_verification_tool(mcp, config)
|
|
66
|
+
if "server" not in disabled:
|
|
67
|
+
register_unified_server_tool(mcp, config)
|
|
68
|
+
if "test" not in disabled:
|
|
69
|
+
register_unified_test_tool(mcp, config)
|
|
70
|
+
if "research" not in disabled:
|
|
71
|
+
register_unified_research_tool(mcp, config)
|
|
52
72
|
|
|
53
73
|
|
|
54
74
|
__all__ = [
|
|
@@ -68,4 +88,5 @@ __all__ = [
|
|
|
68
88
|
"register_unified_verification_tool",
|
|
69
89
|
"register_unified_server_tool",
|
|
70
90
|
"register_unified_test_tool",
|
|
91
|
+
"register_unified_research_tool",
|
|
71
92
|
]
|