mcp-code-indexer 1.0.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.
@@ -0,0 +1,365 @@
1
+ """
2
+ Comprehensive error handling for the MCP Code Indexer.
3
+
4
+ This module provides structured error handling with JSON logging,
5
+ MCP-compliant error responses, and proper async exception management.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import traceback
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from typing import Any, Dict, Optional, Type, Union
14
+ from pathlib import Path
15
+
16
+ from mcp import types
17
+
18
+
19
+ class ErrorCategory(Enum):
20
+ """Categories of errors for better handling and logging."""
21
+ DATABASE = "database"
22
+ FILE_SYSTEM = "file_system"
23
+ VALIDATION = "validation"
24
+ NETWORK = "network"
25
+ AUTHENTICATION = "authentication"
26
+ PERMISSION = "permission"
27
+ RESOURCE = "resource"
28
+ INTERNAL = "internal"
29
+
30
+
31
+ class MCPError(Exception):
32
+ """Base exception for MCP-specific errors."""
33
+
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ category: ErrorCategory = ErrorCategory.INTERNAL,
38
+ code: int = -32603, # JSON-RPC internal error
39
+ details: Optional[Dict[str, Any]] = None
40
+ ):
41
+ super().__init__(message)
42
+ self.message = message
43
+ self.category = category
44
+ self.code = code
45
+ self.details = details or {}
46
+ self.timestamp = datetime.utcnow()
47
+
48
+
49
+ class DatabaseError(MCPError):
50
+ """Database-related errors."""
51
+
52
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
53
+ super().__init__(
54
+ message=message,
55
+ category=ErrorCategory.DATABASE,
56
+ code=-32603,
57
+ details=details
58
+ )
59
+
60
+
61
+ class ValidationError(MCPError):
62
+ """Input validation errors."""
63
+
64
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
65
+ super().__init__(
66
+ message=message,
67
+ category=ErrorCategory.VALIDATION,
68
+ code=-32602, # Invalid params
69
+ details=details
70
+ )
71
+
72
+
73
+ class FileSystemError(MCPError):
74
+ """File system access errors."""
75
+
76
+ def __init__(self, message: str, path: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
77
+ details = details or {}
78
+ if path:
79
+ details["path"] = path
80
+
81
+ super().__init__(
82
+ message=message,
83
+ category=ErrorCategory.FILE_SYSTEM,
84
+ code=-32603,
85
+ details=details
86
+ )
87
+
88
+
89
+ class ResourceError(MCPError):
90
+ """Resource exhaustion or limit errors."""
91
+
92
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
93
+ super().__init__(
94
+ message=message,
95
+ category=ErrorCategory.RESOURCE,
96
+ code=-32603,
97
+ details=details
98
+ )
99
+
100
+
101
+ class ErrorHandler:
102
+ """
103
+ Centralized error handling with structured logging and MCP compliance.
104
+ """
105
+
106
+ def __init__(self, logger: Optional[logging.Logger] = None):
107
+ """Initialize error handler with optional logger."""
108
+ self.logger = logger or logging.getLogger(__name__)
109
+ self._setup_structured_logging()
110
+
111
+ def _setup_structured_logging(self) -> None:
112
+ """Configure structured JSON logging."""
113
+ # Create structured formatter
114
+ formatter = StructuredFormatter()
115
+
116
+ # Apply to logger handlers
117
+ for handler in self.logger.handlers:
118
+ handler.setFormatter(formatter)
119
+
120
+ def log_error(
121
+ self,
122
+ error: Exception,
123
+ context: Optional[Dict[str, Any]] = None,
124
+ tool_name: Optional[str] = None
125
+ ) -> None:
126
+ """
127
+ Log error with structured format.
128
+
129
+ Args:
130
+ error: Exception to log
131
+ context: Additional context information
132
+ tool_name: Name of the tool where error occurred
133
+ """
134
+ error_data = {
135
+ "error_type": type(error).__name__,
136
+ "error_message": str(error),
137
+ "timestamp": datetime.utcnow().isoformat(),
138
+ }
139
+
140
+ if tool_name:
141
+ error_data["tool_name"] = tool_name
142
+
143
+ if context:
144
+ error_data["context"] = context
145
+
146
+ if isinstance(error, MCPError):
147
+ error_data.update({
148
+ "category": error.category.value,
149
+ "code": error.code,
150
+ "details": error.details
151
+ })
152
+
153
+ # Add traceback for debugging
154
+ error_data["traceback"] = traceback.format_exc()
155
+
156
+ self.logger.error("MCP Error occurred", extra={"structured_data": error_data})
157
+
158
+ def create_mcp_error_response(
159
+ self,
160
+ error: Exception,
161
+ tool_name: str,
162
+ arguments: Dict[str, Any]
163
+ ) -> types.TextContent:
164
+ """
165
+ Create MCP-compliant error response.
166
+
167
+ Args:
168
+ error: Exception that occurred
169
+ tool_name: Name of the tool
170
+ arguments: Tool arguments
171
+
172
+ Returns:
173
+ MCP TextContent with error information
174
+ """
175
+ if isinstance(error, MCPError):
176
+ error_response = {
177
+ "error": {
178
+ "code": error.code,
179
+ "message": error.message,
180
+ "category": error.category.value,
181
+ "details": error.details
182
+ },
183
+ "tool": tool_name,
184
+ "timestamp": error.timestamp.isoformat()
185
+ }
186
+ else:
187
+ error_response = {
188
+ "error": {
189
+ "code": -32603, # Internal error
190
+ "message": str(error),
191
+ "category": ErrorCategory.INTERNAL.value,
192
+ "details": {"type": type(error).__name__}
193
+ },
194
+ "tool": tool_name,
195
+ "timestamp": datetime.utcnow().isoformat()
196
+ }
197
+
198
+ # Add arguments for debugging (excluding sensitive data)
199
+ safe_arguments = self._sanitize_arguments(arguments)
200
+ if safe_arguments:
201
+ error_response["arguments"] = safe_arguments
202
+
203
+ import json
204
+ return types.TextContent(
205
+ type="text",
206
+ text=json.dumps(error_response, indent=2, default=str)
207
+ )
208
+
209
+ def _sanitize_arguments(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
210
+ """Remove sensitive information from arguments."""
211
+ sanitized = {}
212
+ sensitive_keys = {"password", "token", "secret", "key", "auth"}
213
+
214
+ for key, value in arguments.items():
215
+ if any(sensitive in key.lower() for sensitive in sensitive_keys):
216
+ sanitized[key] = "[REDACTED]"
217
+ elif isinstance(value, str) and len(value) > 100:
218
+ sanitized[key] = value[:100] + "..."
219
+ else:
220
+ sanitized[key] = value
221
+
222
+ return sanitized
223
+
224
+ async def handle_async_task_error(
225
+ self,
226
+ task: asyncio.Task,
227
+ task_name: str,
228
+ context: Optional[Dict[str, Any]] = None
229
+ ) -> None:
230
+ """
231
+ Handle errors from async tasks.
232
+
233
+ Args:
234
+ task: The completed task
235
+ task_name: Name of the task for logging
236
+ context: Additional context
237
+ """
238
+ try:
239
+ if task.done() and not task.cancelled():
240
+ exception = task.exception()
241
+ if exception:
242
+ self.log_error(
243
+ exception,
244
+ context={**(context or {}), "task_name": task_name},
245
+ tool_name="async_task"
246
+ )
247
+ except Exception as e:
248
+ self.logger.error(f"Error handling task error for {task_name}: {e}")
249
+
250
+
251
+ class StructuredFormatter(logging.Formatter):
252
+ """Custom formatter for structured JSON logging."""
253
+
254
+ def format(self, record: logging.LogRecord) -> str:
255
+ """Format log record as structured JSON."""
256
+ import json
257
+
258
+ log_data = {
259
+ "timestamp": datetime.utcnow().isoformat(),
260
+ "level": record.levelname,
261
+ "logger": record.name,
262
+ "message": record.getMessage(),
263
+ "module": record.module,
264
+ "function": record.funcName,
265
+ "line": record.lineno
266
+ }
267
+
268
+ # Add structured data if present
269
+ if hasattr(record, 'structured_data'):
270
+ log_data.update(record.structured_data)
271
+
272
+ # Add exception info if present
273
+ if record.exc_info:
274
+ log_data["exception"] = self.formatException(record.exc_info)
275
+
276
+ return json.dumps(log_data, default=str)
277
+
278
+
279
+ def setup_error_handling(logger: logging.Logger) -> ErrorHandler:
280
+ """
281
+ Set up comprehensive error handling for the application.
282
+
283
+ Args:
284
+ logger: Logger instance to configure
285
+
286
+ Returns:
287
+ Configured ErrorHandler instance
288
+ """
289
+ error_handler = ErrorHandler(logger)
290
+
291
+ # Set up asyncio exception handler
292
+ def asyncio_exception_handler(loop, context):
293
+ exception = context.get('exception')
294
+ if exception:
295
+ error_handler.log_error(
296
+ exception,
297
+ context={
298
+ "asyncio_context": context,
299
+ "loop": str(loop)
300
+ }
301
+ )
302
+ else:
303
+ logger.error(f"Asyncio error: {context}")
304
+
305
+ # Apply to current event loop if available
306
+ try:
307
+ loop = asyncio.get_running_loop()
308
+ loop.set_exception_handler(asyncio_exception_handler)
309
+ except RuntimeError:
310
+ # No running loop, will be set when loop starts
311
+ pass
312
+
313
+ return error_handler
314
+
315
+
316
+ # Decorators for common error handling patterns
317
+
318
+ def handle_database_errors(func):
319
+ """Decorator to handle database errors."""
320
+ async def wrapper(*args, **kwargs):
321
+ try:
322
+ return await func(*args, **kwargs)
323
+ except Exception as e:
324
+ if "database" in str(e).lower() or "sqlite" in str(e).lower():
325
+ raise DatabaseError(f"Database operation failed: {e}") from e
326
+ raise
327
+ return wrapper
328
+
329
+
330
+ def handle_file_errors(func):
331
+ """Decorator to handle file system errors."""
332
+ async def wrapper(*args, **kwargs):
333
+ try:
334
+ return await func(*args, **kwargs)
335
+ except (FileNotFoundError, PermissionError, OSError) as e:
336
+ raise FileSystemError(f"File system error: {e}") from e
337
+ except Exception:
338
+ raise
339
+ return wrapper
340
+
341
+
342
+ def validate_arguments(required_fields: list, optional_fields: list = None):
343
+ """Decorator to validate tool arguments."""
344
+ def decorator(func):
345
+ async def wrapper(self, arguments: Dict[str, Any], *args, **kwargs):
346
+ # Check required fields
347
+ missing_fields = [field for field in required_fields if field not in arguments]
348
+ if missing_fields:
349
+ raise ValidationError(
350
+ f"Missing required fields: {', '.join(missing_fields)}",
351
+ details={"missing_fields": missing_fields}
352
+ )
353
+
354
+ # Check for unexpected fields
355
+ all_fields = set(required_fields + (optional_fields or []))
356
+ unexpected_fields = [field for field in arguments.keys() if field not in all_fields]
357
+ if unexpected_fields:
358
+ raise ValidationError(
359
+ f"Unexpected fields: {', '.join(unexpected_fields)}",
360
+ details={"unexpected_fields": unexpected_fields}
361
+ )
362
+
363
+ return await func(self, arguments, *args, **kwargs)
364
+ return wrapper
365
+ return decorator