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,728 @@
1
+ """Error data collection infrastructure for foundry-mcp.
2
+
3
+ Provides structured error capture, fingerprinting, and collection for
4
+ future introspection and analysis. This module focuses on data collection
5
+ only - analysis/insights are handled separately.
6
+
7
+ Usage:
8
+ from foundry_mcp.core.error_collection import get_error_collector
9
+
10
+ collector = get_error_collector()
11
+
12
+ # Collect tool errors
13
+ try:
14
+ do_something()
15
+ except Exception as e:
16
+ collector.collect_tool_error(
17
+ tool_name="my-tool",
18
+ error=e,
19
+ input_params={"key": "value"},
20
+ duration_ms=42.5,
21
+ )
22
+ raise
23
+
24
+ # Collect AI provider errors
25
+ collector.collect_provider_error(
26
+ provider_id="gemini",
27
+ error=exc,
28
+ request_context={"workflow": "plan_review"},
29
+ )
30
+
31
+ # Query errors
32
+ records, cursor = collector.query(tool_name="my-tool", limit=10)
33
+ stats = collector.get_stats(group_by="fingerprint")
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import hashlib
39
+ import logging
40
+ import re
41
+ import threading
42
+ import traceback
43
+ import uuid
44
+ from dataclasses import dataclass, asdict
45
+ from datetime import datetime
46
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
47
+
48
+ from foundry_mcp.core.context import get_correlation_id
49
+ from foundry_mcp.core.observability import redact_sensitive_data
50
+
51
+ if TYPE_CHECKING:
52
+ from foundry_mcp.core.error_store import ErrorStore
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ # =============================================================================
58
+ # ErrorRecord Dataclass
59
+ # =============================================================================
60
+
61
+
62
+ @dataclass
63
+ class ErrorRecord:
64
+ """Rich error record for storage and analysis.
65
+
66
+ Captures comprehensive error context including classification,
67
+ timing, provider info (for AI errors), and aggregation metadata.
68
+
69
+ Attributes:
70
+ id: Unique error identifier (err_<uuid>)
71
+ fingerprint: Computed signature for deduplication
72
+ error_code: Machine-readable error code (from ErrorCode enum)
73
+ error_type: Error category (from ErrorType enum)
74
+ tool_name: Name of the tool that generated the error
75
+ correlation_id: Request correlation ID for tracing
76
+ message: User-facing error message (sanitized)
77
+ exception_type: Python exception class name
78
+ stack_trace: Sanitized stack trace (server-side only)
79
+ provider_id: AI provider ID (for provider errors)
80
+ provider_model: AI model used (for provider errors)
81
+ provider_status: Provider status code (for provider errors)
82
+ input_summary: Redacted summary of input parameters
83
+ timestamp: ISO 8601 timestamp of error occurrence
84
+ duration_ms: Operation duration before failure
85
+ count: Occurrence count (for aggregated records)
86
+ first_seen: First occurrence timestamp
87
+ last_seen: Last occurrence timestamp
88
+ """
89
+
90
+ # Identity
91
+ id: str
92
+ fingerprint: str
93
+
94
+ # Classification
95
+ error_code: str
96
+ error_type: str
97
+
98
+ # Context
99
+ tool_name: str
100
+ correlation_id: str
101
+
102
+ # Error details
103
+ message: str
104
+ exception_type: Optional[str] = None
105
+ stack_trace: Optional[str] = None
106
+
107
+ # Provider context (for AI errors)
108
+ provider_id: Optional[str] = None
109
+ provider_model: Optional[str] = None
110
+ provider_status: Optional[str] = None
111
+
112
+ # Input context (redacted)
113
+ input_summary: Optional[Dict[str, Any]] = None
114
+
115
+ # Timing
116
+ timestamp: str = ""
117
+ duration_ms: Optional[float] = None
118
+
119
+ # Aggregation support
120
+ count: int = 1
121
+ first_seen: Optional[str] = None
122
+ last_seen: Optional[str] = None
123
+
124
+ def __post_init__(self) -> None:
125
+ """Set defaults for timestamp fields."""
126
+ if not self.timestamp:
127
+ self.timestamp = datetime.utcnow().isoformat() + "Z"
128
+ if not self.first_seen:
129
+ self.first_seen = self.timestamp
130
+ if not self.last_seen:
131
+ self.last_seen = self.timestamp
132
+
133
+ def to_dict(self) -> Dict[str, Any]:
134
+ """Convert to dictionary for JSON serialization."""
135
+ return {k: v for k, v in asdict(self).items() if v is not None}
136
+
137
+ @classmethod
138
+ def from_dict(cls, data: Dict[str, Any]) -> "ErrorRecord":
139
+ """Create ErrorRecord from dictionary."""
140
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
141
+
142
+
143
+ # =============================================================================
144
+ # ErrorFingerprinter
145
+ # =============================================================================
146
+
147
+
148
+ class ErrorFingerprinter:
149
+ """Generate consistent fingerprints for error deduplication.
150
+
151
+ Fingerprints capture the "signature" of an error for grouping:
152
+ - Same tool + same error_code + same exception_type = same fingerprint
153
+ - Provider errors include provider_id in fingerprint
154
+ - Message patterns are normalized (remove IDs, timestamps, etc.)
155
+ """
156
+
157
+ # Patterns to normalize in error messages
158
+ _NORMALIZE_PATTERNS = [
159
+ # UUIDs
160
+ (r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "<UUID>"),
161
+ # Timestamps (ISO 8601)
162
+ (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?", "<TIMESTAMP>"),
163
+ # Unix timestamps
164
+ (r"\b\d{10,13}\b", "<UNIX_TS>"),
165
+ # File paths
166
+ (r"(/[\w\-.]+)+(\.\w+)?", "<PATH>"),
167
+ # Line numbers in stack traces
168
+ (r"line \d+", "line <N>"),
169
+ # Numeric IDs
170
+ (r"\b\d{5,}\b", "<ID>"),
171
+ # Correlation IDs
172
+ (r"(req|tool|task|err)_[a-f0-9]{12}", "<CORR_ID>"),
173
+ ]
174
+
175
+ def __init__(self) -> None:
176
+ """Initialize fingerprinter with compiled patterns."""
177
+ self._compiled_patterns = [
178
+ (re.compile(pattern, re.IGNORECASE), replacement)
179
+ for pattern, replacement in self._NORMALIZE_PATTERNS
180
+ ]
181
+
182
+ def _normalize_message(self, message: str) -> str:
183
+ """Normalize a message by removing dynamic content."""
184
+ normalized = message
185
+ for pattern, replacement in self._compiled_patterns:
186
+ normalized = pattern.sub(replacement, normalized)
187
+ return normalized.strip().lower()
188
+
189
+ def fingerprint(
190
+ self,
191
+ error_code: str,
192
+ error_type: str,
193
+ tool_name: str,
194
+ exception_type: Optional[str] = None,
195
+ message: Optional[str] = None,
196
+ provider_id: Optional[str] = None,
197
+ ) -> str:
198
+ """Generate a fingerprint based on error characteristics.
199
+
200
+ Args:
201
+ error_code: Machine-readable error code
202
+ error_type: Error category
203
+ tool_name: Name of the tool
204
+ exception_type: Python exception class name
205
+ message: Error message (will be normalized)
206
+ provider_id: AI provider ID (for provider errors)
207
+
208
+ Returns:
209
+ SHA256 hash of normalized components (first 16 chars)
210
+ """
211
+ components = [
212
+ tool_name,
213
+ error_code,
214
+ error_type,
215
+ ]
216
+
217
+ if exception_type:
218
+ components.append(exception_type)
219
+
220
+ if provider_id:
221
+ components.append(f"provider:{provider_id}")
222
+
223
+ if message:
224
+ normalized_msg = self._normalize_message(message)
225
+ # Only include first 100 chars of normalized message
226
+ components.append(normalized_msg[:100])
227
+
228
+ fingerprint_input = "|".join(components)
229
+ hash_obj = hashlib.sha256(fingerprint_input.encode("utf-8"))
230
+ return hash_obj.hexdigest()[:16]
231
+
232
+
233
+ # =============================================================================
234
+ # ErrorCollector
235
+ # =============================================================================
236
+
237
+
238
+ class ErrorCollector:
239
+ """Central error collection point with enrichment and storage.
240
+
241
+ Provides methods to collect tool errors and AI provider errors,
242
+ automatically enriching them with context (correlation ID, timing)
243
+ and storing them for later analysis.
244
+ """
245
+
246
+ def __init__(
247
+ self,
248
+ store: Optional["ErrorStore"] = None,
249
+ fingerprinter: Optional[ErrorFingerprinter] = None,
250
+ enabled: bool = True,
251
+ include_stack_traces: bool = True,
252
+ redact_inputs: bool = True,
253
+ ) -> None:
254
+ """Initialize the error collector.
255
+
256
+ Args:
257
+ store: Error storage backend (lazy-loaded if None)
258
+ fingerprinter: Fingerprint generator (created if None)
259
+ enabled: Whether error collection is enabled
260
+ include_stack_traces: Whether to capture stack traces
261
+ redact_inputs: Whether to redact input parameters
262
+ """
263
+ self._store = store
264
+ self._fingerprinter = fingerprinter or ErrorFingerprinter()
265
+ self._enabled = enabled
266
+ self._include_stack_traces = include_stack_traces
267
+ self._redact_inputs = redact_inputs
268
+ self._lock = threading.Lock()
269
+
270
+ @property
271
+ def store(self) -> "ErrorStore":
272
+ """Get the error store, lazy-loading if necessary."""
273
+ if self._store is None:
274
+ from foundry_mcp.core.error_store import get_error_store
275
+
276
+ self._store = get_error_store()
277
+ return self._store
278
+
279
+ def is_enabled(self) -> bool:
280
+ """Check if error collection is enabled."""
281
+ return self._enabled
282
+
283
+ def initialize(
284
+ self,
285
+ store: "ErrorStore",
286
+ config: Any,
287
+ ) -> None:
288
+ """Initialize the collector with a store and configuration.
289
+
290
+ Called by server initialization to set up the error collection
291
+ infrastructure with the configured storage backend.
292
+
293
+ Args:
294
+ store: Error storage backend
295
+ config: ErrorCollectionConfig instance
296
+ """
297
+ with self._lock:
298
+ self._store = store
299
+ self._enabled = config.enabled
300
+ self._include_stack_traces = config.include_stack_traces
301
+ self._redact_inputs = config.redact_inputs
302
+ logger.debug(
303
+ f"ErrorCollector initialized: enabled={self._enabled}, "
304
+ f"include_stack_traces={self._include_stack_traces}, "
305
+ f"redact_inputs={self._redact_inputs}"
306
+ )
307
+
308
+ def _generate_id(self) -> str:
309
+ """Generate a unique error ID."""
310
+ return f"err_{uuid.uuid4().hex[:12]}"
311
+
312
+ def _extract_exception_info(
313
+ self, error: Exception
314
+ ) -> Tuple[str, Optional[str]]:
315
+ """Extract exception type and sanitized stack trace.
316
+
317
+ Args:
318
+ error: The exception to extract info from
319
+
320
+ Returns:
321
+ Tuple of (exception_type, sanitized_stack_trace)
322
+ """
323
+ exception_type = type(error).__name__
324
+
325
+ stack_trace = None
326
+ if self._include_stack_traces:
327
+ try:
328
+ # Get the full traceback
329
+ tb_lines = traceback.format_exception(
330
+ type(error), error, error.__traceback__
331
+ )
332
+ raw_trace = "".join(tb_lines)
333
+ # Redact sensitive data from stack trace
334
+ stack_trace = redact_sensitive_data(raw_trace)
335
+ except Exception:
336
+ pass
337
+
338
+ return exception_type, stack_trace
339
+
340
+ # Sensitive key name patterns that should always be redacted
341
+ _SENSITIVE_KEY_PATTERNS = [
342
+ "api_key", "apikey", "api-key",
343
+ "password", "passwd", "pwd",
344
+ "secret", "token", "auth",
345
+ "credential", "private", "key",
346
+ "bearer", "access_token", "refresh_token",
347
+ ]
348
+
349
+ def _is_sensitive_key(self, key: str) -> bool:
350
+ """Check if a parameter key name indicates sensitive data."""
351
+ key_lower = key.lower()
352
+ return any(pattern in key_lower for pattern in self._SENSITIVE_KEY_PATTERNS)
353
+
354
+ def _redact_input_params(
355
+ self, params: Optional[Dict[str, Any]]
356
+ ) -> Optional[Dict[str, Any]]:
357
+ """Redact sensitive data from input parameters.
358
+
359
+ Args:
360
+ params: Input parameters dictionary
361
+
362
+ Returns:
363
+ Redacted parameters or None
364
+ """
365
+ if not params or not self._redact_inputs:
366
+ return None
367
+
368
+ try:
369
+ # Create a summary with redacted values
370
+ summary: Dict[str, Any] = {}
371
+ for key, value in params.items():
372
+ if value is None:
373
+ summary[key] = None
374
+ elif self._is_sensitive_key(key):
375
+ # Always redact values for sensitive key names
376
+ summary[key] = "<REDACTED>"
377
+ elif isinstance(value, str):
378
+ # Redact string values using pattern matching
379
+ redacted = redact_sensitive_data(value)
380
+ # Truncate long strings
381
+ if len(redacted) > 100:
382
+ redacted = redacted[:100] + "..."
383
+ summary[key] = redacted
384
+ elif isinstance(value, (int, float, bool)):
385
+ summary[key] = value
386
+ elif isinstance(value, (list, tuple)):
387
+ summary[key] = f"<{type(value).__name__}[{len(value)}]>"
388
+ elif isinstance(value, dict):
389
+ summary[key] = f"<dict[{len(value)}]>"
390
+ else:
391
+ summary[key] = f"<{type(value).__name__}>"
392
+ return summary
393
+ except Exception:
394
+ return None
395
+
396
+ def _map_exception_to_codes(
397
+ self, error: Exception
398
+ ) -> Tuple[str, str]:
399
+ """Map an exception to error_code and error_type.
400
+
401
+ Args:
402
+ error: The exception to map
403
+
404
+ Returns:
405
+ Tuple of (error_code, error_type)
406
+ """
407
+ # Import here to avoid circular imports
408
+ from foundry_mcp.core.responses import ErrorCode, ErrorType
409
+
410
+ exception_name = type(error).__name__
411
+
412
+ # Map common exceptions to error codes
413
+ exception_mapping: Dict[str, Tuple[str, str]] = {
414
+ "FileNotFoundError": (ErrorCode.NOT_FOUND.value, ErrorType.NOT_FOUND.value),
415
+ "PermissionError": (ErrorCode.FORBIDDEN.value, ErrorType.AUTHORIZATION.value),
416
+ "ValueError": (ErrorCode.VALIDATION_ERROR.value, ErrorType.VALIDATION.value),
417
+ "TypeError": (ErrorCode.VALIDATION_ERROR.value, ErrorType.VALIDATION.value),
418
+ "KeyError": (ErrorCode.NOT_FOUND.value, ErrorType.NOT_FOUND.value),
419
+ "TimeoutError": (ErrorCode.UNAVAILABLE.value, ErrorType.UNAVAILABLE.value),
420
+ "ConnectionError": (ErrorCode.UNAVAILABLE.value, ErrorType.UNAVAILABLE.value),
421
+ "JSONDecodeError": (ErrorCode.INVALID_FORMAT.value, ErrorType.VALIDATION.value),
422
+ }
423
+
424
+ if exception_name in exception_mapping:
425
+ return exception_mapping[exception_name]
426
+
427
+ # Check for provider-specific exceptions
428
+ if "Provider" in exception_name or "AI" in exception_name:
429
+ if "Timeout" in exception_name:
430
+ return (ErrorCode.AI_PROVIDER_TIMEOUT.value, ErrorType.AI_PROVIDER.value)
431
+ elif "Unavailable" in exception_name:
432
+ return (ErrorCode.AI_NO_PROVIDER.value, ErrorType.AI_PROVIDER.value)
433
+ else:
434
+ return (ErrorCode.AI_PROVIDER_ERROR.value, ErrorType.AI_PROVIDER.value)
435
+
436
+ # Default to internal error
437
+ return (ErrorCode.INTERNAL_ERROR.value, ErrorType.INTERNAL.value)
438
+
439
+ def collect_tool_error(
440
+ self,
441
+ tool_name: str,
442
+ error: Exception,
443
+ error_code: Optional[str] = None,
444
+ error_type: Optional[str] = None,
445
+ input_params: Optional[Dict[str, Any]] = None,
446
+ duration_ms: Optional[float] = None,
447
+ ) -> Optional[ErrorRecord]:
448
+ """Collect and store a tool error with full context.
449
+
450
+ Args:
451
+ tool_name: Name of the tool that generated the error
452
+ error: The exception that occurred
453
+ error_code: Override error code (auto-detected if None)
454
+ error_type: Override error type (auto-detected if None)
455
+ input_params: Tool input parameters (will be redacted)
456
+ duration_ms: Operation duration before failure
457
+
458
+ Returns:
459
+ The stored ErrorRecord, or None if collection failed/disabled
460
+ """
461
+ if not self._enabled:
462
+ return None
463
+
464
+ try:
465
+ # Auto-detect error codes if not provided
466
+ if error_code is None or error_type is None:
467
+ detected_code, detected_type = self._map_exception_to_codes(error)
468
+ error_code = error_code or detected_code
469
+ error_type = error_type or detected_type
470
+
471
+ # Extract exception info
472
+ exception_type, stack_trace = self._extract_exception_info(error)
473
+
474
+ # Get correlation ID from context
475
+ correlation_id = get_correlation_id() or "unknown"
476
+
477
+ # Generate fingerprint
478
+ fingerprint = self._fingerprinter.fingerprint(
479
+ error_code=error_code,
480
+ error_type=error_type,
481
+ tool_name=tool_name,
482
+ exception_type=exception_type,
483
+ message=str(error),
484
+ )
485
+
486
+ # Create error record
487
+ record = ErrorRecord(
488
+ id=self._generate_id(),
489
+ fingerprint=fingerprint,
490
+ error_code=error_code,
491
+ error_type=error_type,
492
+ tool_name=tool_name,
493
+ correlation_id=correlation_id,
494
+ message=str(error),
495
+ exception_type=exception_type,
496
+ stack_trace=stack_trace,
497
+ input_summary=self._redact_input_params(input_params),
498
+ duration_ms=duration_ms,
499
+ )
500
+
501
+ # Store the record
502
+ self.store.append(record)
503
+
504
+ logger.debug(
505
+ "Collected tool error",
506
+ extra={
507
+ "error_id": record.id,
508
+ "fingerprint": record.fingerprint,
509
+ "tool_name": tool_name,
510
+ "error_code": error_code,
511
+ },
512
+ )
513
+
514
+ return record
515
+
516
+ except Exception as e:
517
+ logger.warning(f"Failed to collect tool error: {e}")
518
+ return None
519
+
520
+ def collect_provider_error(
521
+ self,
522
+ provider_id: str,
523
+ error: Optional[Exception] = None,
524
+ provider_result: Optional[Any] = None,
525
+ request_context: Optional[Dict[str, Any]] = None,
526
+ ) -> Optional[ErrorRecord]:
527
+ """Collect and store an AI provider error.
528
+
529
+ Args:
530
+ provider_id: AI provider identifier
531
+ error: The exception that occurred (if any)
532
+ provider_result: ProviderResult object (if available)
533
+ request_context: Request context (workflow, prompt_id, etc.)
534
+
535
+ Returns:
536
+ The stored ErrorRecord, or None if collection failed/disabled
537
+ """
538
+ if not self._enabled:
539
+ return None
540
+
541
+ try:
542
+ # Import here to avoid circular imports
543
+ from foundry_mcp.core.responses import ErrorCode, ErrorType
544
+
545
+ # Determine error details
546
+ if error:
547
+ exception_type, stack_trace = self._extract_exception_info(error)
548
+ message = str(error)
549
+ error_code, error_type = self._map_exception_to_codes(error)
550
+ else:
551
+ exception_type = None
552
+ stack_trace = None
553
+ message = "Provider error"
554
+ error_code = ErrorCode.AI_PROVIDER_ERROR.value
555
+ error_type = ErrorType.AI_PROVIDER.value
556
+
557
+ # Extract provider result info if available
558
+ provider_model = None
559
+ provider_status = None
560
+ if provider_result:
561
+ provider_model = getattr(provider_result, "model", None)
562
+ if hasattr(provider_result, "status"):
563
+ provider_status = str(provider_result.status)
564
+ # Include stderr in message if present
565
+ stderr = getattr(provider_result, "stderr", None)
566
+ if stderr:
567
+ message = f"{message} - {stderr[:200]}"
568
+
569
+ # Get correlation ID
570
+ correlation_id = get_correlation_id() or "unknown"
571
+
572
+ # Determine tool name from context
573
+ tool_name = "ai-consultation"
574
+ if request_context and "workflow" in request_context:
575
+ tool_name = f"ai-consultation:{request_context['workflow']}"
576
+
577
+ # Generate fingerprint (includes provider_id)
578
+ fingerprint = self._fingerprinter.fingerprint(
579
+ error_code=error_code,
580
+ error_type=error_type,
581
+ tool_name=tool_name,
582
+ exception_type=exception_type,
583
+ message=message,
584
+ provider_id=provider_id,
585
+ )
586
+
587
+ # Create error record
588
+ record = ErrorRecord(
589
+ id=self._generate_id(),
590
+ fingerprint=fingerprint,
591
+ error_code=error_code,
592
+ error_type=error_type,
593
+ tool_name=tool_name,
594
+ correlation_id=correlation_id,
595
+ message=message,
596
+ exception_type=exception_type,
597
+ stack_trace=stack_trace,
598
+ provider_id=provider_id,
599
+ provider_model=provider_model,
600
+ provider_status=provider_status,
601
+ input_summary=self._redact_input_params(request_context),
602
+ )
603
+
604
+ # Store the record
605
+ self.store.append(record)
606
+
607
+ logger.debug(
608
+ "Collected provider error",
609
+ extra={
610
+ "error_id": record.id,
611
+ "fingerprint": record.fingerprint,
612
+ "provider_id": provider_id,
613
+ "error_code": error_code,
614
+ },
615
+ )
616
+
617
+ return record
618
+
619
+ except Exception as e:
620
+ logger.warning(f"Failed to collect provider error: {e}")
621
+ return None
622
+
623
+ def query(
624
+ self,
625
+ tool_name: Optional[str] = None,
626
+ error_code: Optional[str] = None,
627
+ error_type: Optional[str] = None,
628
+ fingerprint: Optional[str] = None,
629
+ provider_id: Optional[str] = None,
630
+ since: Optional[str] = None,
631
+ until: Optional[str] = None,
632
+ limit: int = 100,
633
+ offset: int = 0,
634
+ ) -> List[ErrorRecord]:
635
+ """Query collected errors with filtering.
636
+
637
+ Args:
638
+ tool_name: Filter by tool name
639
+ error_code: Filter by error code
640
+ error_type: Filter by error type
641
+ fingerprint: Filter by fingerprint
642
+ provider_id: Filter by provider ID
643
+ since: ISO 8601 timestamp - filter errors after this time
644
+ until: ISO 8601 timestamp - filter errors before this time
645
+ limit: Maximum records to return
646
+ offset: Number of records to skip
647
+
648
+ Returns:
649
+ List of matching ErrorRecord objects
650
+ """
651
+ return self.store.query(
652
+ tool_name=tool_name,
653
+ error_code=error_code,
654
+ error_type=error_type,
655
+ fingerprint=fingerprint,
656
+ provider_id=provider_id,
657
+ since=since,
658
+ until=until,
659
+ limit=limit,
660
+ offset=offset,
661
+ )
662
+
663
+ def get(self, error_id: str) -> Optional[ErrorRecord]:
664
+ """Get a specific error record by ID.
665
+
666
+ Args:
667
+ error_id: Error record ID
668
+
669
+ Returns:
670
+ ErrorRecord or None if not found
671
+ """
672
+ return self.store.get(error_id)
673
+
674
+ def get_stats(self) -> Dict[str, Any]:
675
+ """Get aggregated error statistics.
676
+
677
+ Returns:
678
+ Statistics dictionary with total_errors, unique_patterns,
679
+ by_tool, by_error_code, and top_patterns
680
+ """
681
+ return self.store.get_stats()
682
+
683
+
684
+ # =============================================================================
685
+ # Singleton Instance
686
+ # =============================================================================
687
+
688
+ _collector: Optional[ErrorCollector] = None
689
+ _collector_lock = threading.Lock()
690
+
691
+
692
+ def get_error_collector() -> ErrorCollector:
693
+ """Get the singleton ErrorCollector instance.
694
+
695
+ Returns:
696
+ ErrorCollector singleton instance
697
+ """
698
+ global _collector
699
+
700
+ if _collector is None:
701
+ with _collector_lock:
702
+ if _collector is None:
703
+ _collector = ErrorCollector()
704
+
705
+ return _collector
706
+
707
+
708
+ def reset_error_collector() -> None:
709
+ """Reset the singleton error collector (mainly for testing)."""
710
+ global _collector
711
+ with _collector_lock:
712
+ _collector = None
713
+
714
+
715
+ # =============================================================================
716
+ # Exports
717
+ # =============================================================================
718
+
719
+ __all__ = [
720
+ # Data classes
721
+ "ErrorRecord",
722
+ # Fingerprinting
723
+ "ErrorFingerprinter",
724
+ # Collection
725
+ "ErrorCollector",
726
+ "get_error_collector",
727
+ "reset_error_collector",
728
+ ]