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.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,96 @@
1
+ """Overview page - dashboard home with KPIs and summary."""
2
+
3
+ import streamlit as st
4
+
5
+ from foundry_mcp.dashboard.components.cards import kpi_row
6
+ from foundry_mcp.dashboard.data.stores import (
7
+ get_overview_summary,
8
+ get_errors,
9
+ get_error_patterns,
10
+ get_top_tool_actions,
11
+ )
12
+
13
+
14
+ def render():
15
+ """Render the Overview page."""
16
+ st.header("Overview")
17
+
18
+ # Get summary data
19
+ summary = get_overview_summary()
20
+
21
+ # KPI Cards Row
22
+ st.subheader("Key Metrics")
23
+ kpi_row(
24
+ [
25
+ {
26
+ "label": "Total Invocations",
27
+ "value": summary.get("total_invocations", 0),
28
+ "help": "Total tool invocations recorded (all time)",
29
+ },
30
+ {
31
+ "label": "Total Errors",
32
+ "value": summary.get("error_count", 0),
33
+ "help": "Total errors recorded (all time)",
34
+ },
35
+ ],
36
+ columns=2,
37
+ )
38
+
39
+ st.divider()
40
+
41
+ # Bottom Row - Patterns and Recent
42
+ col1, col2 = st.columns(2)
43
+
44
+ with col1:
45
+ st.subheader("Top Error Patterns (All Time)")
46
+ patterns = get_error_patterns(min_count=2)
47
+ if patterns:
48
+ for i, p in enumerate(patterns[:5]):
49
+ with st.container(border=True):
50
+ st.markdown(f"**{p.get('tool_name', 'Unknown')}**")
51
+ st.caption(f"Count: {p.get('count', 0)} | Code: {p.get('error_code', 'N/A')}")
52
+ if p.get("message"):
53
+ st.text(p["message"][:100] + "..." if len(p.get("message", "")) > 100 else p.get("message", ""))
54
+ else:
55
+ st.info("No recurring error patterns detected")
56
+
57
+ with col2:
58
+ st.subheader("Recent Errors (Last Hour)")
59
+ errors_df = get_errors(since_hours=1, limit=5)
60
+
61
+ # Try fallback to 24h if 1h is empty
62
+ if errors_df is None or errors_df.empty:
63
+ errors_df = get_errors(since_hours=24, limit=5)
64
+ if errors_df is not None and not errors_df.empty:
65
+ st.caption("No errors in last hour - showing last 24h")
66
+
67
+ if errors_df is not None and not errors_df.empty:
68
+ for _, row in errors_df.iterrows():
69
+ with st.container(border=True):
70
+ st.markdown(f"**{row.get('tool_name', 'Unknown')}**")
71
+ st.caption(f"{row.get('timestamp', '')} | {row.get('error_code', 'N/A')}")
72
+ msg = row.get("message", "")
73
+ st.text(msg[:80] + "..." if len(msg) > 80 else msg)
74
+ else:
75
+ st.info("No recent errors")
76
+
77
+ st.divider()
78
+
79
+ # Tool Usage Breakdown
80
+ st.subheader("Top Tool Actions (Last 24h)")
81
+ top_actions = get_top_tool_actions(since_hours=24, top_n=10)
82
+
83
+ if top_actions:
84
+ col1, col2 = st.columns(2)
85
+ for i, item in enumerate(top_actions):
86
+ tool = item.get("tool", "unknown")
87
+ action = item.get("action", "")
88
+ count = int(item.get("count", 0))
89
+ display_name = f"{tool}.{action}" if action and action != "(no action)" else tool
90
+ target_col = col1 if i < 5 else col2
91
+ with target_col:
92
+ with st.container(border=True):
93
+ st.markdown(f"**{display_name}**")
94
+ st.caption(f"Invocations: {count:,}")
95
+ else:
96
+ st.info("No tool action data available yet")
@@ -0,0 +1,83 @@
1
+ """Providers page - AI provider status grid."""
2
+
3
+ import streamlit as st
4
+
5
+ from foundry_mcp.dashboard.components.cards import status_badge
6
+ from foundry_mcp.dashboard.data.stores import get_providers
7
+
8
+
9
+ def render():
10
+ """Render the Providers page."""
11
+ st.header("AI Providers")
12
+
13
+ providers = get_providers()
14
+
15
+ if not providers:
16
+ st.info("No AI providers configured or providers module not available.")
17
+ st.caption("Providers are configured in foundry-mcp.toml under [providers]")
18
+ return
19
+
20
+ # Summary
21
+ available_count = len([p for p in providers if p.get("available")])
22
+ total_count = len(providers)
23
+
24
+ col1, col2, col3 = st.columns(3)
25
+ with col1:
26
+ st.metric("Total Providers", total_count)
27
+ with col2:
28
+ st.metric("Available", available_count)
29
+ with col3:
30
+ st.metric("Unavailable", total_count - available_count)
31
+
32
+ st.divider()
33
+
34
+ # Provider grid
35
+ st.subheader("Provider Status")
36
+
37
+ # Create grid layout
38
+ cols = st.columns(3)
39
+
40
+ for i, provider in enumerate(providers):
41
+ with cols[i % 3]:
42
+ with st.container(border=True):
43
+ # Header with status
44
+ provider_id = provider.get("id", "unknown")
45
+ is_available = provider.get("available", False)
46
+
47
+ status = "available" if is_available else "unavailable"
48
+ status_badge(status, label=provider_id)
49
+
50
+ # Description
51
+ description = provider.get("description", "")
52
+ if description:
53
+ st.caption(description[:100] + "..." if len(description) > 100 else description)
54
+
55
+ # Tags
56
+ tags = provider.get("tags", [])
57
+ if tags:
58
+ st.markdown(" ".join([f"`{tag}`" for tag in tags[:5]]))
59
+
60
+ # Models
61
+ models = provider.get("models", [])
62
+ if models:
63
+ with st.expander("Models"):
64
+ for model in models[:10]:
65
+ if isinstance(model, dict):
66
+ st.text(f"- {model.get('id', model.get('name', 'unknown'))}")
67
+ else:
68
+ st.text(f"- {model}")
69
+ if len(models) > 10:
70
+ st.caption(f"...and {len(models) - 10} more")
71
+
72
+ # Metadata
73
+ metadata = provider.get("metadata", {})
74
+ if metadata:
75
+ with st.expander("Details"):
76
+ for key, value in list(metadata.items())[:5]:
77
+ st.text(f"{key}: {value}")
78
+
79
+ # Refresh button
80
+ st.divider()
81
+ if st.button("Refresh Provider Status", use_container_width=True):
82
+ st.cache_data.clear()
83
+ st.rerun()
@@ -0,0 +1,255 @@
1
+ """SDD Workflow page - spec progress, phase burndown, task tracking."""
2
+
3
+ import streamlit as st
4
+
5
+ from foundry_mcp.dashboard.components.cards import kpi_row
6
+ from foundry_mcp.dashboard.components.charts import pie_chart, bar_chart, empty_chart
7
+
8
+ # Try importing pandas
9
+ try:
10
+ import pandas as pd
11
+ PANDAS_AVAILABLE = True
12
+ except ImportError:
13
+ PANDAS_AVAILABLE = False
14
+ pd = None
15
+
16
+
17
+ def _get_specs():
18
+ """Get list of specifications."""
19
+ try:
20
+ from foundry_mcp.core.spec import list_specs
21
+ from foundry_mcp.config import get_config
22
+
23
+ config = get_config()
24
+ specs_dir = config.specs_dir
25
+ return list_specs(specs_dir)
26
+ except ImportError:
27
+ return []
28
+ except Exception:
29
+ return []
30
+
31
+
32
+ def _get_spec_data(spec_id: str):
33
+ """Get detailed spec data."""
34
+ try:
35
+ from foundry_mcp.core.spec import load_spec
36
+ from foundry_mcp.config import get_config
37
+
38
+ config = get_config()
39
+ return load_spec(config.specs_dir, spec_id)
40
+ except Exception:
41
+ return None
42
+
43
+
44
+ def _calculate_progress(spec_data: dict) -> dict:
45
+ """Calculate progress metrics from spec data."""
46
+ if not spec_data:
47
+ return {"total": 0, "completed": 0, "in_progress": 0, "pending": 0, "blocked": 0, "percentage": 0}
48
+
49
+ tasks = spec_data.get("tasks", {})
50
+ status_counts = {"completed": 0, "in_progress": 0, "pending": 0, "blocked": 0}
51
+
52
+ for task_id, task in tasks.items():
53
+ status = task.get("status", "pending")
54
+ if status in status_counts:
55
+ status_counts[status] += 1
56
+ else:
57
+ status_counts["pending"] += 1
58
+
59
+ total = sum(status_counts.values())
60
+ percentage = (status_counts["completed"] / total * 100) if total > 0 else 0
61
+
62
+ return {
63
+ "total": total,
64
+ **status_counts,
65
+ "percentage": percentage,
66
+ }
67
+
68
+
69
+ def render():
70
+ """Render the SDD Workflow page."""
71
+ st.header("SDD Workflow")
72
+
73
+ # Get specs
74
+ specs = _get_specs()
75
+
76
+ if not specs:
77
+ st.info("No specifications found.")
78
+ st.caption("Create specs using `foundry-cli spec create` or the MCP spec-create tool.")
79
+ return
80
+
81
+ # Spec selector
82
+ spec_options = {s.get("title", s.get("id", "unknown")): s.get("id") for s in specs if isinstance(s, dict)}
83
+
84
+ if not spec_options:
85
+ st.warning("Could not parse specification list")
86
+ return
87
+
88
+ selected_title = st.selectbox(
89
+ "Select Specification",
90
+ options=list(spec_options.keys()),
91
+ key="sdd_spec_selector",
92
+ )
93
+
94
+ selected_id = spec_options.get(selected_title)
95
+ if not selected_id:
96
+ return
97
+
98
+ # Load spec data
99
+ spec_data = _get_spec_data(selected_id)
100
+ if not spec_data:
101
+ st.warning(f"Could not load spec: {selected_id}")
102
+ return
103
+
104
+ # Calculate progress
105
+ progress = _calculate_progress(spec_data)
106
+
107
+ st.divider()
108
+
109
+ # Progress overview
110
+ st.subheader("Progress Overview")
111
+
112
+ # Progress bar
113
+ st.progress(progress["percentage"] / 100, text=f"Overall Progress: {progress['percentage']:.1f}%")
114
+
115
+ # KPI cards
116
+ kpi_row(
117
+ [
118
+ {"label": "Total Tasks", "value": progress["total"]},
119
+ {"label": "Completed", "value": progress["completed"]},
120
+ {"label": "In Progress", "value": progress["in_progress"]},
121
+ {"label": "Pending", "value": progress["pending"]},
122
+ {"label": "Blocked", "value": progress["blocked"]},
123
+ ],
124
+ columns=5,
125
+ )
126
+
127
+ st.divider()
128
+
129
+ # Charts row
130
+ col1, col2 = st.columns(2)
131
+
132
+ with col1:
133
+ st.subheader("Task Status Distribution")
134
+ if PANDAS_AVAILABLE and progress["total"] > 0:
135
+ status_df = pd.DataFrame([
136
+ {"status": "completed", "count": progress["completed"]},
137
+ {"status": "in_progress", "count": progress["in_progress"]},
138
+ {"status": "pending", "count": progress["pending"]},
139
+ {"status": "blocked", "count": progress["blocked"]},
140
+ ])
141
+ status_df = status_df[status_df["count"] > 0] # Filter zero counts
142
+
143
+ pie_chart(
144
+ status_df,
145
+ values="count",
146
+ names="status",
147
+ title=None,
148
+ height=300,
149
+ )
150
+ else:
151
+ empty_chart("No task data")
152
+
153
+ with col2:
154
+ st.subheader("Phase Progress")
155
+ phases = spec_data.get("phases", [])
156
+ if PANDAS_AVAILABLE and phases:
157
+ phase_data = []
158
+ for phase in phases:
159
+ if isinstance(phase, dict):
160
+ phase_tasks = phase.get("tasks", [])
161
+ completed = len([t for t in phase_tasks if isinstance(t, dict) and t.get("status") == "completed"])
162
+ total = len(phase_tasks)
163
+ phase_data.append({
164
+ "phase": phase.get("title", phase.get("id", "unknown"))[:20],
165
+ "completed": completed,
166
+ "total": total,
167
+ "percentage": (completed / total * 100) if total > 0 else 0,
168
+ })
169
+
170
+ if phase_data:
171
+ phase_df = pd.DataFrame(phase_data)
172
+ bar_chart(
173
+ phase_df,
174
+ x="phase",
175
+ y="percentage",
176
+ title=None,
177
+ height=300,
178
+ )
179
+ else:
180
+ empty_chart("No phase data")
181
+ else:
182
+ empty_chart("No phase data")
183
+
184
+ st.divider()
185
+
186
+ # Task list
187
+ st.subheader("Tasks")
188
+ tasks = spec_data.get("tasks", {})
189
+
190
+ if tasks:
191
+ # Status filter
192
+ status_filter = st.multiselect(
193
+ "Filter by Status",
194
+ options=["completed", "in_progress", "pending", "blocked"],
195
+ default=["in_progress", "pending", "blocked"],
196
+ key="task_status_filter",
197
+ )
198
+
199
+ # Build task list
200
+ task_list = []
201
+ for task_id, task in tasks.items():
202
+ if isinstance(task, dict):
203
+ status = task.get("status", "pending")
204
+ if status in status_filter:
205
+ task_list.append({
206
+ "id": task_id,
207
+ "title": task.get("title", "Untitled"),
208
+ "status": status,
209
+ "estimated_hours": task.get("estimated_hours", 0),
210
+ "actual_hours": task.get("actual_hours", 0),
211
+ })
212
+
213
+ if task_list and PANDAS_AVAILABLE:
214
+ task_df = pd.DataFrame(task_list)
215
+ st.dataframe(
216
+ task_df,
217
+ use_container_width=True,
218
+ hide_index=True,
219
+ column_config={
220
+ "id": st.column_config.TextColumn("ID", width="small"),
221
+ "title": st.column_config.TextColumn("Title", width="large"),
222
+ "status": st.column_config.TextColumn("Status", width="small"),
223
+ "estimated_hours": st.column_config.NumberColumn("Est. Hours", format="%.1f", width="small"),
224
+ "actual_hours": st.column_config.NumberColumn("Act. Hours", format="%.1f", width="small"),
225
+ },
226
+ )
227
+ elif task_list:
228
+ for t in task_list:
229
+ st.text(f"- [{t['status']}] {t['title']}")
230
+ else:
231
+ st.info("No tasks matching filter")
232
+ else:
233
+ st.info("No tasks defined in this spec")
234
+
235
+ # Time tracking summary
236
+ st.divider()
237
+ st.subheader("Time Tracking")
238
+
239
+ total_estimated = sum(t.get("estimated_hours", 0) for t in tasks.values() if isinstance(t, dict))
240
+ total_actual = sum(t.get("actual_hours", 0) for t in tasks.values() if isinstance(t, dict))
241
+ variance = total_actual - total_estimated
242
+
243
+ col1, col2, col3 = st.columns(3)
244
+ with col1:
245
+ st.metric("Estimated Hours", f"{total_estimated:.1f}h")
246
+ with col2:
247
+ st.metric("Actual Hours", f"{total_actual:.1f}h")
248
+ with col3:
249
+ delta_color = "inverse" if variance > 0 else "normal"
250
+ st.metric(
251
+ "Variance",
252
+ f"{variance:+.1f}h",
253
+ delta=f"{(variance / total_estimated * 100):+.1f}%" if total_estimated > 0 else "N/A",
254
+ delta_color=delta_color,
255
+ )
@@ -0,0 +1,139 @@
1
+ """Tool Usage page - detailed breakdown of tool and action invocations."""
2
+
3
+ import streamlit as st
4
+
5
+ from foundry_mcp.dashboard.components.filters import time_range_filter
6
+ from foundry_mcp.dashboard.components.charts import bar_chart, pie_chart, empty_chart
7
+ from foundry_mcp.dashboard.components.cards import kpi_row
8
+ from foundry_mcp.dashboard.data.stores import (
9
+ get_tool_action_breakdown,
10
+ get_top_tool_actions,
11
+ )
12
+
13
+ # Try importing pandas
14
+ try:
15
+ import pandas as pd
16
+ PANDAS_AVAILABLE = True
17
+ except ImportError:
18
+ PANDAS_AVAILABLE = False
19
+ pd = None
20
+
21
+
22
+ def render():
23
+ """Render the Tool Usage page."""
24
+ st.header("Tool Usage")
25
+
26
+ # Time range filter
27
+ col1, col2 = st.columns([3, 1])
28
+ with col2:
29
+ hours = time_range_filter(key="tool_usage_time_range", default="24h")
30
+
31
+ st.divider()
32
+
33
+ # Get breakdown data
34
+ breakdown_df = get_tool_action_breakdown(since_hours=hours)
35
+
36
+ if breakdown_df is None or breakdown_df.empty:
37
+ st.warning("No tool usage data available. Data will appear once tools are invoked.")
38
+ st.info("Tool invocations are recorded when MCP tools are called.")
39
+ return
40
+
41
+ # KPI Summary
42
+ total_invocations = int(breakdown_df["count"].sum())
43
+ unique_tools = breakdown_df["tool"].nunique()
44
+ unique_actions = breakdown_df[breakdown_df["action"] != "(no action)"]["action"].nunique()
45
+ success_count = int(breakdown_df[breakdown_df["status"] == "success"]["count"].sum())
46
+ success_rate = (success_count / total_invocations * 100) if total_invocations > 0 else 0
47
+
48
+ kpi_row(
49
+ [
50
+ {"label": "Total Invocations", "value": f"{total_invocations:,}"},
51
+ {"label": "Unique Tools", "value": unique_tools},
52
+ {"label": "Unique Actions", "value": unique_actions},
53
+ {"label": "Success Rate", "value": f"{success_rate:.1f}%"},
54
+ ],
55
+ columns=4,
56
+ )
57
+
58
+ st.divider()
59
+
60
+ # Charts Row
61
+ col1, col2 = st.columns(2)
62
+
63
+ with col1:
64
+ st.subheader("Top Tools")
65
+ tool_totals = breakdown_df.groupby("tool")["count"].sum().reset_index()
66
+ tool_totals = tool_totals.sort_values("count", ascending=True).tail(10)
67
+ if not tool_totals.empty:
68
+ bar_chart(tool_totals, x="count", y="tool", orientation="h", height=350)
69
+ else:
70
+ empty_chart("No tool data")
71
+
72
+ with col2:
73
+ st.subheader("Status Distribution")
74
+ status_totals = breakdown_df.groupby("status")["count"].sum().reset_index()
75
+ if not status_totals.empty:
76
+ pie_chart(status_totals, values="count", names="status", height=350)
77
+ else:
78
+ empty_chart("No status data")
79
+
80
+ st.divider()
81
+
82
+ # Detailed Action Breakdown
83
+ st.subheader("Action Breakdown by Tool")
84
+
85
+ # Tool selector
86
+ tools = sorted(breakdown_df["tool"].unique())
87
+ selected_tool = st.selectbox(
88
+ "Select Tool",
89
+ options=["All Tools"] + tools,
90
+ key="tool_usage_tool_selector",
91
+ )
92
+
93
+ if selected_tool == "All Tools":
94
+ filtered_df = breakdown_df
95
+ else:
96
+ filtered_df = breakdown_df[breakdown_df["tool"] == selected_tool]
97
+
98
+ if not filtered_df.empty and PANDAS_AVAILABLE:
99
+ # Create display name column
100
+ action_df = filtered_df.groupby(["tool", "action", "status"])["count"].sum().reset_index()
101
+ action_df["display_name"] = action_df.apply(
102
+ lambda r: f"{r['tool']}.{r['action']}" if r["action"] != "(no action)" else r["tool"],
103
+ axis=1
104
+ )
105
+
106
+ # Detailed table
107
+ st.subheader("Detailed Data")
108
+ display_df = action_df[["display_name", "status", "count"]].sort_values("count", ascending=False)
109
+ st.dataframe(
110
+ display_df,
111
+ use_container_width=True,
112
+ hide_index=True,
113
+ column_config={
114
+ "display_name": st.column_config.TextColumn("Tool.Action", width="medium"),
115
+ "status": st.column_config.TextColumn("Status", width="small"),
116
+ "count": st.column_config.NumberColumn("Invocations", width="small"),
117
+ },
118
+ )
119
+
120
+ # Export options
121
+ col1, col2 = st.columns(2)
122
+ with col1:
123
+ csv = display_df.to_csv(index=False)
124
+ st.download_button(
125
+ label="Download CSV",
126
+ data=csv,
127
+ file_name="tool_usage_export.csv",
128
+ mime="text/csv",
129
+ )
130
+ with col2:
131
+ json_data = display_df.to_json(orient="records")
132
+ st.download_button(
133
+ label="Download JSON",
134
+ data=json_data,
135
+ file_name="tool_usage_export.json",
136
+ mime="application/json",
137
+ )
138
+ else:
139
+ empty_chart("No data for selected filter")
@@ -0,0 +1,9 @@
1
+ """
2
+ MCP prompts for foundry-mcp.
3
+
4
+ Provides prompt templates for common SDD workflows.
5
+ """
6
+
7
+ from foundry_mcp.prompts.workflows import register_workflow_prompts
8
+
9
+ __all__ = ["register_workflow_prompts"]