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.
Files changed (60) hide show
  1. foundry_mcp/__init__.py +7 -1
  2. foundry_mcp/cli/commands/plan.py +10 -3
  3. foundry_mcp/cli/commands/review.py +19 -4
  4. foundry_mcp/cli/commands/specs.py +38 -208
  5. foundry_mcp/cli/output.py +3 -3
  6. foundry_mcp/config.py +235 -5
  7. foundry_mcp/core/ai_consultation.py +146 -9
  8. foundry_mcp/core/discovery.py +6 -6
  9. foundry_mcp/core/error_store.py +2 -2
  10. foundry_mcp/core/intake.py +933 -0
  11. foundry_mcp/core/llm_config.py +20 -2
  12. foundry_mcp/core/metrics_store.py +2 -2
  13. foundry_mcp/core/progress.py +70 -0
  14. foundry_mcp/core/prompts/fidelity_review.py +149 -4
  15. foundry_mcp/core/prompts/markdown_plan_review.py +5 -1
  16. foundry_mcp/core/prompts/plan_review.py +5 -1
  17. foundry_mcp/core/providers/claude.py +6 -47
  18. foundry_mcp/core/providers/codex.py +6 -57
  19. foundry_mcp/core/providers/cursor_agent.py +3 -44
  20. foundry_mcp/core/providers/gemini.py +6 -57
  21. foundry_mcp/core/providers/opencode.py +35 -5
  22. foundry_mcp/core/research/__init__.py +68 -0
  23. foundry_mcp/core/research/memory.py +425 -0
  24. foundry_mcp/core/research/models.py +437 -0
  25. foundry_mcp/core/research/workflows/__init__.py +22 -0
  26. foundry_mcp/core/research/workflows/base.py +204 -0
  27. foundry_mcp/core/research/workflows/chat.py +271 -0
  28. foundry_mcp/core/research/workflows/consensus.py +396 -0
  29. foundry_mcp/core/research/workflows/ideate.py +682 -0
  30. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  31. foundry_mcp/core/responses.py +450 -0
  32. foundry_mcp/core/spec.py +2438 -236
  33. foundry_mcp/core/task.py +1064 -19
  34. foundry_mcp/core/testing.py +512 -123
  35. foundry_mcp/core/validation.py +313 -42
  36. foundry_mcp/dashboard/components/charts.py +0 -57
  37. foundry_mcp/dashboard/launcher.py +11 -0
  38. foundry_mcp/dashboard/views/metrics.py +25 -35
  39. foundry_mcp/dashboard/views/overview.py +1 -65
  40. foundry_mcp/resources/specs.py +25 -25
  41. foundry_mcp/schemas/intake-schema.json +89 -0
  42. foundry_mcp/schemas/sdd-spec-schema.json +33 -5
  43. foundry_mcp/server.py +38 -0
  44. foundry_mcp/tools/unified/__init__.py +4 -2
  45. foundry_mcp/tools/unified/authoring.py +2423 -267
  46. foundry_mcp/tools/unified/documentation_helpers.py +69 -6
  47. foundry_mcp/tools/unified/environment.py +235 -6
  48. foundry_mcp/tools/unified/error.py +18 -1
  49. foundry_mcp/tools/unified/lifecycle.py +8 -0
  50. foundry_mcp/tools/unified/plan.py +113 -1
  51. foundry_mcp/tools/unified/research.py +658 -0
  52. foundry_mcp/tools/unified/review.py +370 -16
  53. foundry_mcp/tools/unified/spec.py +367 -0
  54. foundry_mcp/tools/unified/task.py +1163 -48
  55. foundry_mcp/tools/unified/test.py +69 -8
  56. {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/METADATA +7 -1
  57. {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/RECORD +60 -48
  58. {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/WHEEL +0 -0
  59. {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
  60. {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 - time-series viewer with summaries."""
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
- # Time-series chart
81
- st.subheader(f"Time Series: {selected_metric}")
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
- line_chart(
99
+
100
+ st.dataframe(
101
101
  display_df,
102
- x="timestamp",
103
- y="value",
104
- title=None,
105
- height=400,
102
+ use_container_width=True,
103
+ hide_index=True,
106
104
  )
107
105
 
108
- # Data table
109
- with st.expander("View Raw Data"):
110
- st.dataframe(
111
- display_df,
112
- use_container_width=True,
113
- hide_index=True,
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
- empty_chart(f"No data available for {selected_metric} (all time)")
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 charts."""
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
 
@@ -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": "auto",
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 consolidated tool families."""
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
  ]