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,195 @@
1
+ """Data table components for dashboard."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import streamlit as st
6
+
7
+ # Try importing pandas
8
+ try:
9
+ import pandas as pd
10
+
11
+ PANDAS_AVAILABLE = True
12
+ except ImportError:
13
+ PANDAS_AVAILABLE = False
14
+ pd = None
15
+
16
+
17
+ def data_table(
18
+ df: "pd.DataFrame",
19
+ columns: Optional[dict[str, Any]] = None,
20
+ height: Optional[int] = None,
21
+ selection_mode: Optional[str] = None,
22
+ on_select: Optional[callable] = None,
23
+ ) -> Optional[dict]:
24
+ """Render an interactive data table.
25
+
26
+ Args:
27
+ df: DataFrame to display
28
+ columns: Column configuration dict for st.column_config
29
+ height: Optional fixed height
30
+ selection_mode: "single-row", "multi-row", or None
31
+ on_select: Callback for selection events
32
+
33
+ Returns:
34
+ Selection info if selection_mode is set, None otherwise
35
+ """
36
+ if not PANDAS_AVAILABLE:
37
+ st.warning("Pandas not installed")
38
+ return None
39
+
40
+ if df is None or df.empty:
41
+ st.info("No data to display")
42
+ return None
43
+
44
+ # Build kwargs
45
+ kwargs = {
46
+ "use_container_width": True,
47
+ "hide_index": True,
48
+ }
49
+
50
+ if columns:
51
+ kwargs["column_config"] = columns
52
+
53
+ if height:
54
+ kwargs["height"] = height
55
+
56
+ if selection_mode:
57
+ kwargs["selection_mode"] = selection_mode
58
+ event = st.dataframe(df, **kwargs, on_select="rerun")
59
+ return event
60
+ else:
61
+ st.dataframe(df, **kwargs)
62
+ return None
63
+
64
+
65
+ def error_table_config() -> dict:
66
+ """Get column configuration for error tables."""
67
+ return {
68
+ "id": st.column_config.TextColumn(
69
+ "ID",
70
+ width="small",
71
+ help="Error ID",
72
+ ),
73
+ "timestamp": st.column_config.DatetimeColumn(
74
+ "Time",
75
+ format="HH:mm:ss",
76
+ width="small",
77
+ ),
78
+ "tool_name": st.column_config.TextColumn(
79
+ "Tool",
80
+ width="medium",
81
+ ),
82
+ "error_code": st.column_config.TextColumn(
83
+ "Code",
84
+ width="small",
85
+ ),
86
+ "message": st.column_config.TextColumn(
87
+ "Message",
88
+ width="large",
89
+ ),
90
+ "fingerprint": st.column_config.TextColumn(
91
+ "Pattern",
92
+ width="small",
93
+ help="Error fingerprint for pattern matching",
94
+ ),
95
+ }
96
+
97
+
98
+ def metrics_table_config() -> dict:
99
+ """Get column configuration for metrics tables."""
100
+ return {
101
+ "metric_name": st.column_config.TextColumn(
102
+ "Metric",
103
+ width="medium",
104
+ ),
105
+ "count": st.column_config.NumberColumn(
106
+ "Records",
107
+ width="small",
108
+ ),
109
+ "first_seen": st.column_config.DatetimeColumn(
110
+ "First Seen",
111
+ format="MMM DD, HH:mm",
112
+ width="medium",
113
+ ),
114
+ "last_seen": st.column_config.DatetimeColumn(
115
+ "Last Seen",
116
+ format="MMM DD, HH:mm",
117
+ width="medium",
118
+ ),
119
+ }
120
+
121
+
122
+ def task_table_config() -> dict:
123
+ """Get column configuration for task tables."""
124
+ return {
125
+ "id": st.column_config.TextColumn(
126
+ "ID",
127
+ width="small",
128
+ ),
129
+ "title": st.column_config.TextColumn(
130
+ "Title",
131
+ width="large",
132
+ ),
133
+ "status": st.column_config.TextColumn(
134
+ "Status",
135
+ width="small",
136
+ ),
137
+ "estimated_hours": st.column_config.NumberColumn(
138
+ "Est. Hours",
139
+ format="%.1f",
140
+ width="small",
141
+ ),
142
+ "actual_hours": st.column_config.NumberColumn(
143
+ "Act. Hours",
144
+ format="%.1f",
145
+ width="small",
146
+ ),
147
+ }
148
+
149
+
150
+ def paginated_table(
151
+ df: "pd.DataFrame",
152
+ page_size: int = 50,
153
+ key: str = "table_page",
154
+ columns: Optional[dict] = None,
155
+ ) -> None:
156
+ """Render a paginated data table.
157
+
158
+ Args:
159
+ df: DataFrame to display
160
+ page_size: Rows per page
161
+ key: Unique key for pagination state
162
+ columns: Column configuration
163
+ """
164
+ if not PANDAS_AVAILABLE:
165
+ st.warning("Pandas not installed")
166
+ return
167
+
168
+ if df is None or df.empty:
169
+ st.info("No data to display")
170
+ return
171
+
172
+ total_rows = len(df)
173
+ total_pages = (total_rows + page_size - 1) // page_size
174
+
175
+ # Page selector
176
+ col1, col2 = st.columns([3, 1])
177
+ with col2:
178
+ page = st.number_input(
179
+ "Page",
180
+ min_value=1,
181
+ max_value=max(1, total_pages),
182
+ value=1,
183
+ key=key,
184
+ )
185
+
186
+ with col1:
187
+ st.caption(f"Showing {min(page_size, total_rows)} of {total_rows} rows")
188
+
189
+ # Slice data for current page
190
+ start_idx = (page - 1) * page_size
191
+ end_idx = start_idx + page_size
192
+ page_df = df.iloc[start_idx:end_idx]
193
+
194
+ # Display table
195
+ data_table(page_df, columns=columns)
@@ -0,0 +1,11 @@
1
+ """Data access layer for dashboard.
2
+
3
+ Provides cached access to MetricsStore, ErrorStore, and other data sources,
4
+ returning pandas DataFrames for easy use with Streamlit components.
5
+ """
6
+
7
+ from foundry_mcp.dashboard.data import stores
8
+
9
+ __all__ = [
10
+ "stores",
11
+ ]
@@ -0,0 +1,433 @@
1
+ """Data access layer for dashboard.
2
+
3
+ Wraps MetricsStore, ErrorStore, and other data sources with:
4
+ - Singleton store instances (stores handle internal caching)
5
+ - pandas DataFrame conversion for easy use with st.dataframe
6
+ - Graceful handling when stores are disabled
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Any, Optional
14
+
15
+ import streamlit as st
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Try importing pandas - it's an optional dependency
20
+ try:
21
+ import pandas as pd
22
+
23
+ PANDAS_AVAILABLE = True
24
+ except ImportError:
25
+ PANDAS_AVAILABLE = False
26
+ pd = None
27
+
28
+
29
+ def _get_config():
30
+ """Get foundry-mcp configuration."""
31
+ try:
32
+ from foundry_mcp.config import get_config
33
+
34
+ return get_config()
35
+ except Exception as e:
36
+ logger.warning("Could not load config: %s", e)
37
+ return None
38
+
39
+
40
+ def _get_error_store():
41
+ """Get error store singleton instance if enabled."""
42
+ config = _get_config()
43
+ if config is None or not config.error_collection.enabled:
44
+ return None
45
+
46
+ try:
47
+ from foundry_mcp.core.error_store import get_error_store
48
+
49
+ storage_path = config.error_collection.get_storage_path()
50
+ return get_error_store(storage_path) # Returns singleton
51
+ except Exception as e:
52
+ logger.warning("Could not initialize error store: %s", e)
53
+ return None
54
+
55
+
56
+ def _get_metrics_store():
57
+ """Get metrics store singleton instance if enabled."""
58
+ config = _get_config()
59
+ if config is None or not config.metrics_persistence.enabled:
60
+ return None
61
+
62
+ try:
63
+ from foundry_mcp.core.metrics_store import get_metrics_store
64
+
65
+ storage_path = config.metrics_persistence.get_storage_path()
66
+ return get_metrics_store(storage_path) # Returns singleton
67
+ except Exception as e:
68
+ logger.warning("Could not initialize metrics store: %s", e)
69
+ return None
70
+
71
+
72
+ # =============================================================================
73
+ # Error Data Functions
74
+ # =============================================================================
75
+
76
+
77
+ def get_errors(
78
+ tool_name: Optional[str] = None,
79
+ error_code: Optional[str] = None,
80
+ since_hours: int = 24,
81
+ limit: int = 100,
82
+ ) -> "pd.DataFrame":
83
+ """Get errors as a DataFrame.
84
+
85
+ Args:
86
+ tool_name: Filter by tool name
87
+ error_code: Filter by error code
88
+ since_hours: Hours to look back
89
+ limit: Maximum records to return
90
+
91
+ Returns:
92
+ DataFrame with error records, or empty DataFrame if disabled
93
+ """
94
+ if not PANDAS_AVAILABLE:
95
+ return None
96
+
97
+ store = _get_error_store()
98
+ if store is None:
99
+ return pd.DataFrame()
100
+
101
+ since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
102
+
103
+ try:
104
+ records = store.query(
105
+ tool_name=tool_name,
106
+ error_code=error_code,
107
+ since=since,
108
+ limit=limit,
109
+ )
110
+
111
+ if not records:
112
+ return pd.DataFrame()
113
+
114
+ # Convert to list of dicts
115
+ data = []
116
+ for r in records:
117
+ data.append(
118
+ {
119
+ "id": r.error_id,
120
+ "timestamp": r.timestamp,
121
+ "tool_name": r.tool_name,
122
+ "error_code": r.error_code,
123
+ "message": r.message,
124
+ "error_type": r.error_type,
125
+ "fingerprint": r.fingerprint,
126
+ }
127
+ )
128
+
129
+ df = pd.DataFrame(data)
130
+ if "timestamp" in df.columns:
131
+ df["timestamp"] = pd.to_datetime(df["timestamp"])
132
+ return df
133
+
134
+ except Exception as e:
135
+ logger.exception("Error querying errors: %s", e)
136
+ return pd.DataFrame()
137
+
138
+
139
+ def get_error_stats() -> dict[str, Any]:
140
+ """Get error statistics."""
141
+ store = _get_error_store()
142
+ if store is None:
143
+ return {"enabled": False, "total": 0}
144
+
145
+ try:
146
+ stats = store.get_stats()
147
+ stats["enabled"] = True
148
+ return stats
149
+ except Exception as e:
150
+ logger.exception("Error getting error stats: %s", e)
151
+ return {"enabled": True, "error": str(e)}
152
+
153
+
154
+ def get_error_patterns(min_count: int = 3) -> list[dict]:
155
+ """Get recurring error patterns."""
156
+ store = _get_error_store()
157
+ if store is None:
158
+ return []
159
+
160
+ try:
161
+ return store.get_patterns(min_count=min_count)
162
+ except Exception as e:
163
+ logger.exception("Error getting patterns: %s", e)
164
+ return []
165
+
166
+
167
+ def get_error_by_id(error_id: str) -> Optional[dict]:
168
+ """Get a single error by ID (not cached for freshness)."""
169
+ store = _get_error_store()
170
+ if store is None:
171
+ return None
172
+
173
+ try:
174
+ record = store.get(error_id)
175
+ if record is None:
176
+ return None
177
+
178
+ return {
179
+ "id": record.error_id,
180
+ "timestamp": record.timestamp,
181
+ "tool_name": record.tool_name,
182
+ "error_code": record.error_code,
183
+ "message": record.message,
184
+ "error_type": record.error_type,
185
+ "fingerprint": record.fingerprint,
186
+ "stack_trace": record.stack_trace,
187
+ "context": record.context,
188
+ }
189
+ except Exception as e:
190
+ logger.exception("Error getting error by ID: %s", e)
191
+ return None
192
+
193
+
194
+ # =============================================================================
195
+ # Metrics Data Functions
196
+ # =============================================================================
197
+
198
+
199
+ def get_metrics_list() -> list[dict]:
200
+ """Get list of available metrics."""
201
+ store = _get_metrics_store()
202
+ if store is None:
203
+ return []
204
+
205
+ try:
206
+ return store.list_metrics()
207
+ except Exception as e:
208
+ logger.exception("Error listing metrics: %s", e)
209
+ return []
210
+
211
+
212
+ def get_metrics_timeseries(
213
+ metric_name: str,
214
+ since_hours: int = 24,
215
+ limit: int = 1000,
216
+ ) -> "pd.DataFrame":
217
+ """Get time-series data for a metric.
218
+
219
+ Args:
220
+ metric_name: Name of the metric
221
+ since_hours: Hours to look back
222
+ limit: Maximum data points
223
+
224
+ Returns:
225
+ DataFrame with timestamp and value columns
226
+ """
227
+ if not PANDAS_AVAILABLE:
228
+ return None
229
+
230
+ store = _get_metrics_store()
231
+ if store is None:
232
+ return pd.DataFrame()
233
+
234
+ since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
235
+
236
+ try:
237
+ data_points = store.query(
238
+ metric_name=metric_name,
239
+ since=since,
240
+ limit=limit,
241
+ )
242
+
243
+ if not data_points:
244
+ return pd.DataFrame()
245
+
246
+ data = [
247
+ {
248
+ "timestamp": dp.timestamp,
249
+ "value": dp.value,
250
+ "labels": str(dp.labels) if dp.labels else "",
251
+ }
252
+ for dp in data_points
253
+ ]
254
+
255
+ df = pd.DataFrame(data)
256
+ if "timestamp" in df.columns:
257
+ df["timestamp"] = pd.to_datetime(df["timestamp"])
258
+ return df
259
+
260
+ except Exception as e:
261
+ logger.exception("Error querying metrics: %s", e)
262
+ return pd.DataFrame()
263
+
264
+
265
+ def get_metrics_summary(metric_name: str, since_hours: int = 24) -> dict[str, Any]:
266
+ """Get summary statistics for a metric."""
267
+ store = _get_metrics_store()
268
+ if store is None:
269
+ return {"enabled": False}
270
+
271
+ since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
272
+
273
+ try:
274
+ summary = store.get_summary(metric_name, since=since)
275
+ summary["enabled"] = True
276
+ return summary
277
+ except Exception as e:
278
+ logger.exception("Error getting summary: %s", e)
279
+ return {"enabled": True, "error": str(e)}
280
+
281
+
282
+ # =============================================================================
283
+ # Tool Usage Breakdown Functions
284
+ # =============================================================================
285
+
286
+
287
+ def get_tool_action_breakdown(
288
+ since_hours: int = 24,
289
+ limit: int = 1000,
290
+ ) -> "pd.DataFrame":
291
+ """Get tool invocations broken down by tool and action.
292
+
293
+ Args:
294
+ since_hours: Hours to look back
295
+ limit: Maximum data points to query
296
+
297
+ Returns:
298
+ DataFrame with tool, action, status, and count columns
299
+ """
300
+ if not PANDAS_AVAILABLE:
301
+ return None
302
+
303
+ store = _get_metrics_store()
304
+ if store is None:
305
+ return pd.DataFrame()
306
+
307
+ since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
308
+
309
+ try:
310
+ data_points = store.query(
311
+ metric_name="tool_invocations_total",
312
+ since=since,
313
+ limit=limit,
314
+ )
315
+
316
+ if not data_points:
317
+ return pd.DataFrame()
318
+
319
+ # Group by tool, action, status
320
+ breakdown: dict[tuple[str, str, str], float] = {}
321
+ for dp in data_points:
322
+ tool = dp.labels.get("tool", "unknown")
323
+ action = dp.labels.get("action", "") # Empty string for legacy data
324
+ status = dp.labels.get("status", "unknown")
325
+ key = (tool, action, status)
326
+ breakdown[key] = breakdown.get(key, 0) + dp.value
327
+
328
+ data = [
329
+ {"tool": k[0], "action": k[1] or "(no action)", "status": k[2], "count": int(v)}
330
+ for k, v in breakdown.items()
331
+ ]
332
+
333
+ return pd.DataFrame(data)
334
+
335
+ except Exception as e:
336
+ logger.exception("Error getting tool action breakdown: %s", e)
337
+ return pd.DataFrame()
338
+
339
+
340
+ def get_top_tool_actions(since_hours: int = 24, top_n: int = 10) -> list[dict]:
341
+ """Get top N most called tool+action combinations.
342
+
343
+ Args:
344
+ since_hours: Hours to look back
345
+ top_n: Number of top items to return
346
+
347
+ Returns:
348
+ List of dicts with tool, action, count
349
+ """
350
+ df = get_tool_action_breakdown(since_hours=since_hours)
351
+ if df is None or df.empty:
352
+ return []
353
+
354
+ try:
355
+ # Group by tool+action, sum counts across statuses
356
+ grouped = df.groupby(["tool", "action"])["count"].sum().reset_index()
357
+ grouped = grouped.sort_values("count", ascending=False).head(top_n)
358
+
359
+ return grouped.to_dict("records")
360
+ except Exception as e:
361
+ logger.exception("Error getting top tool actions: %s", e)
362
+ return []
363
+
364
+
365
+ # =============================================================================
366
+ # Health & Provider Data Functions
367
+ # =============================================================================
368
+
369
+
370
+ @st.cache_data(ttl=10)
371
+ def get_health_status() -> dict[str, Any]:
372
+ """Get server health status."""
373
+ try:
374
+ from foundry_mcp.core.health import get_health_manager
375
+
376
+ manager = get_health_manager()
377
+ result = manager.check_health()
378
+
379
+ return {
380
+ "healthy": result.healthy,
381
+ "status": result.status.value if hasattr(result.status, "value") else str(result.status),
382
+ "checks": {
383
+ name: {
384
+ "healthy": check.healthy,
385
+ "message": check.message,
386
+ }
387
+ for name, check in (result.checks or {}).items()
388
+ },
389
+ }
390
+ except Exception as e:
391
+ logger.exception("Error getting health: %s", e)
392
+ return {"healthy": False, "error": str(e)}
393
+
394
+
395
+ @st.cache_data(ttl=30)
396
+ def get_providers() -> list[dict]:
397
+ """Get list of AI providers with status."""
398
+ try:
399
+ from foundry_mcp.core.providers import describe_providers
400
+
401
+ return describe_providers()
402
+ except ImportError:
403
+ # Providers module may not exist yet
404
+ return []
405
+ except Exception as e:
406
+ logger.exception("Error getting providers: %s", e)
407
+ return []
408
+
409
+
410
+ # =============================================================================
411
+ # Overview Summary Functions
412
+ # =============================================================================
413
+
414
+
415
+ def get_overview_summary() -> dict[str, Any]:
416
+ """Get aggregated overview metrics for dashboard."""
417
+ summary = {
418
+ "total_invocations": 0,
419
+ "error_count": 0,
420
+ }
421
+
422
+ # Get metrics summary
423
+ metrics_list = get_metrics_list()
424
+ for m in metrics_list:
425
+ if m.get("metric_name") == "tool_invocations_total":
426
+ summary["total_invocations"] = m.get("count", 0)
427
+
428
+ # Get error count from store (single source of truth)
429
+ error_store = _get_error_store()
430
+ if error_store is not None:
431
+ summary["error_count"] = error_store.get_total_count()
432
+
433
+ return summary