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.
- mcp_code_indexer/__init__.py +16 -0
- mcp_code_indexer/database/__init__.py +1 -0
- mcp_code_indexer/database/database.py +480 -0
- mcp_code_indexer/database/models.py +123 -0
- mcp_code_indexer/error_handler.py +365 -0
- mcp_code_indexer/file_scanner.py +375 -0
- mcp_code_indexer/logging_config.py +183 -0
- mcp_code_indexer/main.py +129 -0
- mcp_code_indexer/merge_handler.py +386 -0
- mcp_code_indexer/middleware/__init__.py +7 -0
- mcp_code_indexer/middleware/error_middleware.py +286 -0
- mcp_code_indexer/server/__init__.py +1 -0
- mcp_code_indexer/server/mcp_server.py +699 -0
- mcp_code_indexer/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- mcp_code_indexer/token_counter.py +243 -0
- mcp_code_indexer/tools/__init__.py +1 -0
- mcp_code_indexer-1.0.0.dist-info/METADATA +364 -0
- mcp_code_indexer-1.0.0.dist-info/RECORD +22 -0
- mcp_code_indexer-1.0.0.dist-info/WHEEL +5 -0
- mcp_code_indexer-1.0.0.dist-info/entry_points.txt +2 -0
- mcp_code_indexer-1.0.0.dist-info/licenses/LICENSE +21 -0
- mcp_code_indexer-1.0.0.dist-info/top_level.txt +1 -0
@@ -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
|