tree-sitter-analyzer 1.9.17.1__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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Utils Module
|
|
4
|
+
|
|
5
|
+
This module provides utility functions and classes for the MCP server
|
|
6
|
+
including error handling and other utilities.
|
|
7
|
+
|
|
8
|
+
Note: Cache and performance monitoring functionality has been moved to
|
|
9
|
+
the unified core services for better architecture.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Export main utility classes and functions
|
|
15
|
+
from .error_handler import (
|
|
16
|
+
AnalysisError,
|
|
17
|
+
ErrorCategory,
|
|
18
|
+
ErrorHandler,
|
|
19
|
+
ErrorSeverity,
|
|
20
|
+
FileAccessError,
|
|
21
|
+
MCPError,
|
|
22
|
+
ParsingError,
|
|
23
|
+
ResourceError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
get_error_handler,
|
|
26
|
+
handle_mcp_errors,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Export path resolver utilities
|
|
30
|
+
from .path_resolver import PathResolver, resolve_path
|
|
31
|
+
|
|
32
|
+
# Module metadata
|
|
33
|
+
__author__ = "Tree-Sitter Analyzer Team"
|
|
34
|
+
|
|
35
|
+
# MCP Utils capabilities
|
|
36
|
+
MCP_UTILS_CAPABILITIES = {
|
|
37
|
+
"version": "1.1.0",
|
|
38
|
+
"features": [
|
|
39
|
+
"Comprehensive Error Handling",
|
|
40
|
+
"Unified Core Services Integration",
|
|
41
|
+
"Cross-Platform Path Resolution",
|
|
42
|
+
],
|
|
43
|
+
"deprecated_features": [
|
|
44
|
+
"LRU Cache with TTL (moved to core.cache_service)",
|
|
45
|
+
"Performance Monitoring (moved to core.analysis_engine)",
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Import unified services for backward compatibility
|
|
50
|
+
try:
|
|
51
|
+
from ...core.analysis_engine import UnifiedAnalysisEngine
|
|
52
|
+
from ...core.cache_service import CacheService as UnifiedCacheService
|
|
53
|
+
|
|
54
|
+
# Provide backward compatibility aliases
|
|
55
|
+
class BackwardCompatibleCacheManager:
|
|
56
|
+
"""Backward compatible cache manager wrapper"""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._cache_service = UnifiedCacheService()
|
|
60
|
+
|
|
61
|
+
def clear_all_caches(self) -> None:
|
|
62
|
+
"""Backward compatibility: clear all caches"""
|
|
63
|
+
return self._cache_service.clear()
|
|
64
|
+
|
|
65
|
+
def get_cache_stats(self) -> dict[str, Any]:
|
|
66
|
+
"""Backward compatibility: get cache statistics"""
|
|
67
|
+
return self._cache_service.get_stats()
|
|
68
|
+
|
|
69
|
+
def __getattr__(self, name: str) -> Any:
|
|
70
|
+
"""Delegate other methods to the cache service"""
|
|
71
|
+
return getattr(self._cache_service, name)
|
|
72
|
+
|
|
73
|
+
def get_cache_manager() -> Any:
|
|
74
|
+
"""Backward compatibility: Get unified cache service"""
|
|
75
|
+
return BackwardCompatibleCacheManager()
|
|
76
|
+
|
|
77
|
+
def get_performance_monitor() -> Any:
|
|
78
|
+
"""Backward compatibility: Get unified analysis engine for performance monitoring"""
|
|
79
|
+
return UnifiedAnalysisEngine()
|
|
80
|
+
|
|
81
|
+
except ImportError:
|
|
82
|
+
# Fallback if core services are not available
|
|
83
|
+
def get_cache_manager() -> Any:
|
|
84
|
+
"""Fallback cache manager"""
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def get_performance_monitor() -> Any:
|
|
88
|
+
"""Fallback performance monitor"""
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
# Error handling
|
|
94
|
+
"ErrorHandler",
|
|
95
|
+
"MCPError",
|
|
96
|
+
"FileAccessError",
|
|
97
|
+
"ParsingError",
|
|
98
|
+
"AnalysisError",
|
|
99
|
+
"ValidationError",
|
|
100
|
+
"ResourceError",
|
|
101
|
+
"ErrorSeverity",
|
|
102
|
+
"ErrorCategory",
|
|
103
|
+
"handle_mcp_errors",
|
|
104
|
+
"get_error_handler",
|
|
105
|
+
# Path resolution
|
|
106
|
+
"PathResolver",
|
|
107
|
+
"resolve_path",
|
|
108
|
+
# Backward compatibility
|
|
109
|
+
"get_cache_manager",
|
|
110
|
+
"get_performance_monitor",
|
|
111
|
+
# Module metadata
|
|
112
|
+
"MCP_UTILS_CAPABILITIES",
|
|
113
|
+
]
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Error Handler for MCP Server
|
|
4
|
+
|
|
5
|
+
This module provides comprehensive error handling and recovery
|
|
6
|
+
mechanisms for the MCP server operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import traceback
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from functools import wraps
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ErrorSeverity(Enum):
|
|
22
|
+
"""Error severity levels"""
|
|
23
|
+
|
|
24
|
+
LOW = "low"
|
|
25
|
+
MEDIUM = "medium"
|
|
26
|
+
HIGH = "high"
|
|
27
|
+
CRITICAL = "critical"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ErrorCategory(Enum):
|
|
31
|
+
"""Error categories for classification"""
|
|
32
|
+
|
|
33
|
+
FILE_ACCESS = "file_access"
|
|
34
|
+
PARSING = "parsing"
|
|
35
|
+
ANALYSIS = "analysis"
|
|
36
|
+
NETWORK = "network"
|
|
37
|
+
VALIDATION = "validation"
|
|
38
|
+
RESOURCE = "resource"
|
|
39
|
+
CONFIGURATION = "configuration"
|
|
40
|
+
UNKNOWN = "unknown"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MCPError(Exception):
|
|
44
|
+
"""Base exception class for MCP-specific errors"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
message: str,
|
|
49
|
+
category: ErrorCategory = ErrorCategory.UNKNOWN,
|
|
50
|
+
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
|
51
|
+
details: dict[str, Any] | None = None,
|
|
52
|
+
recoverable: bool = True,
|
|
53
|
+
):
|
|
54
|
+
super().__init__(message)
|
|
55
|
+
self.message = message
|
|
56
|
+
self.category = category
|
|
57
|
+
self.severity = severity
|
|
58
|
+
self.details = details or {}
|
|
59
|
+
self.recoverable = recoverable
|
|
60
|
+
self.timestamp = datetime.now()
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
"""Convert error to dictionary representation"""
|
|
64
|
+
return {
|
|
65
|
+
"error_type": self.__class__.__name__,
|
|
66
|
+
"message": self.message,
|
|
67
|
+
"category": self.category.value,
|
|
68
|
+
"severity": self.severity.value,
|
|
69
|
+
"details": self.details,
|
|
70
|
+
"recoverable": self.recoverable,
|
|
71
|
+
"timestamp": self.timestamp.isoformat(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FileAccessError(MCPError):
|
|
76
|
+
"""Error related to file access operations"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, message: str, file_path: str, **kwargs: Any):
|
|
79
|
+
super().__init__(
|
|
80
|
+
message,
|
|
81
|
+
category=ErrorCategory.FILE_ACCESS,
|
|
82
|
+
details={"file_path": file_path},
|
|
83
|
+
**kwargs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ParsingError(MCPError):
|
|
88
|
+
"""Error related to code parsing operations"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
file_path: str,
|
|
94
|
+
language: str | None = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
):
|
|
97
|
+
super().__init__(
|
|
98
|
+
message,
|
|
99
|
+
category=ErrorCategory.PARSING,
|
|
100
|
+
details={"file_path": file_path, "language": language},
|
|
101
|
+
**kwargs,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AnalysisError(MCPError):
|
|
106
|
+
"""Error related to code analysis operations"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, message: str, operation: str, **kwargs: Any):
|
|
109
|
+
super().__init__(
|
|
110
|
+
message,
|
|
111
|
+
category=ErrorCategory.ANALYSIS,
|
|
112
|
+
details={"operation": operation},
|
|
113
|
+
**kwargs,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ValidationError(MCPError):
|
|
118
|
+
"""Error related to input validation"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, message: str, field: str, value: Any = None, **kwargs: Any):
|
|
121
|
+
super().__init__(
|
|
122
|
+
message,
|
|
123
|
+
category=ErrorCategory.VALIDATION,
|
|
124
|
+
details={
|
|
125
|
+
"field": field,
|
|
126
|
+
"value": str(value) if value is not None else None,
|
|
127
|
+
},
|
|
128
|
+
**kwargs,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ResourceError(MCPError):
|
|
133
|
+
"""Error related to resource operations"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, message: str, resource_uri: str, **kwargs: Any):
|
|
136
|
+
super().__init__(
|
|
137
|
+
message,
|
|
138
|
+
category=ErrorCategory.RESOURCE,
|
|
139
|
+
details={"resource_uri": resource_uri},
|
|
140
|
+
**kwargs,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ErrorHandler:
|
|
145
|
+
"""
|
|
146
|
+
Centralized error handling and recovery system
|
|
147
|
+
|
|
148
|
+
Provides error classification, logging, recovery mechanisms,
|
|
149
|
+
and error statistics for the MCP server.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
"""Initialize error handler"""
|
|
154
|
+
self.error_counts: dict[str, int] = {}
|
|
155
|
+
self.error_history: list[dict[str, Any]] = []
|
|
156
|
+
self.max_history_size = 1000
|
|
157
|
+
self.recovery_strategies: dict[type[Exception], Callable] = {}
|
|
158
|
+
|
|
159
|
+
# Register default recovery strategies
|
|
160
|
+
self._register_default_strategies()
|
|
161
|
+
|
|
162
|
+
logger.info("Error handler initialized")
|
|
163
|
+
|
|
164
|
+
def _register_default_strategies(self) -> None:
|
|
165
|
+
"""Register default error recovery strategies"""
|
|
166
|
+
|
|
167
|
+
def file_not_found_recovery(
|
|
168
|
+
error: FileNotFoundError, context: dict[str, Any]
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""Recovery strategy for file not found errors"""
|
|
171
|
+
return {
|
|
172
|
+
"error": f"File not found: {context.get('file_path', 'unknown')}",
|
|
173
|
+
"suggestion": "Please check the file path and ensure the file exists",
|
|
174
|
+
"recoverable": True,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def permission_error_recovery(
|
|
178
|
+
error: PermissionError, context: dict[str, Any]
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Recovery strategy for permission errors"""
|
|
181
|
+
return {
|
|
182
|
+
"error": f"Permission denied: {context.get('file_path', 'unknown')}",
|
|
183
|
+
"suggestion": "Please check file permissions or run with appropriate privileges",
|
|
184
|
+
"recoverable": False,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def value_error_recovery(
|
|
188
|
+
error: ValueError, context: dict[str, Any]
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""Recovery strategy for value errors"""
|
|
191
|
+
return {
|
|
192
|
+
"error": f"Invalid value: {str(error)}",
|
|
193
|
+
"suggestion": "Please check input parameters and try again",
|
|
194
|
+
"recoverable": True,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
self.recovery_strategies.update(
|
|
198
|
+
{
|
|
199
|
+
FileNotFoundError: file_not_found_recovery,
|
|
200
|
+
PermissionError: permission_error_recovery,
|
|
201
|
+
ValueError: value_error_recovery,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def register_recovery_strategy(
|
|
206
|
+
self,
|
|
207
|
+
exception_type: type[Exception],
|
|
208
|
+
strategy: Callable[[Exception, dict[str, Any]], dict[str, Any]],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Register a custom recovery strategy for an exception type
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
exception_type: Type of exception to handle
|
|
215
|
+
strategy: Recovery function that takes (exception, context) and returns recovery info
|
|
216
|
+
"""
|
|
217
|
+
self.recovery_strategies[exception_type] = strategy
|
|
218
|
+
logger.debug(f"Registered recovery strategy for {exception_type.__name__}")
|
|
219
|
+
|
|
220
|
+
def handle_error(
|
|
221
|
+
self,
|
|
222
|
+
error: Exception,
|
|
223
|
+
context: dict[str, Any] | None = None,
|
|
224
|
+
operation: str = "unknown",
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
"""
|
|
227
|
+
Handle an error with classification, logging, and recovery
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
error: The exception that occurred
|
|
231
|
+
context: Additional context information
|
|
232
|
+
operation: Name of the operation that failed
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Error information dictionary with recovery suggestions
|
|
236
|
+
"""
|
|
237
|
+
context = context or {}
|
|
238
|
+
|
|
239
|
+
# Classify error
|
|
240
|
+
if isinstance(error, MCPError):
|
|
241
|
+
error_info = error.to_dict()
|
|
242
|
+
else:
|
|
243
|
+
error_info = self._classify_error(error, context, operation)
|
|
244
|
+
|
|
245
|
+
# Log error
|
|
246
|
+
self._log_error(error, error_info, context, operation)
|
|
247
|
+
|
|
248
|
+
# Update statistics
|
|
249
|
+
self._update_error_stats(error_info)
|
|
250
|
+
|
|
251
|
+
# Add to history
|
|
252
|
+
self._add_to_history(error_info, context, operation)
|
|
253
|
+
|
|
254
|
+
# Attempt recovery
|
|
255
|
+
recovery_info = self._attempt_recovery(error, context)
|
|
256
|
+
if recovery_info:
|
|
257
|
+
error_info.update(recovery_info)
|
|
258
|
+
|
|
259
|
+
return error_info
|
|
260
|
+
|
|
261
|
+
def _classify_error(
|
|
262
|
+
self, error: Exception, context: dict[str, Any], operation: str
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
Classify a generic exception into MCP error categories
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
error: The exception to classify
|
|
269
|
+
context: Error context
|
|
270
|
+
operation: Operation that failed
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Error information dictionary
|
|
274
|
+
"""
|
|
275
|
+
error_type = type(error).__name__
|
|
276
|
+
message = str(error)
|
|
277
|
+
|
|
278
|
+
# Determine category based on error type and context
|
|
279
|
+
category = ErrorCategory.UNKNOWN
|
|
280
|
+
severity = ErrorSeverity.MEDIUM
|
|
281
|
+
recoverable = True
|
|
282
|
+
|
|
283
|
+
if isinstance(
|
|
284
|
+
error, FileNotFoundError | IsADirectoryError | NotADirectoryError
|
|
285
|
+
):
|
|
286
|
+
category = ErrorCategory.FILE_ACCESS
|
|
287
|
+
severity = ErrorSeverity.MEDIUM
|
|
288
|
+
elif isinstance(error, PermissionError):
|
|
289
|
+
category = ErrorCategory.FILE_ACCESS
|
|
290
|
+
severity = ErrorSeverity.HIGH
|
|
291
|
+
recoverable = False
|
|
292
|
+
elif isinstance(error, ValueError | TypeError):
|
|
293
|
+
category = ErrorCategory.VALIDATION
|
|
294
|
+
severity = ErrorSeverity.LOW
|
|
295
|
+
elif isinstance(error, OSError | IOError):
|
|
296
|
+
category = ErrorCategory.FILE_ACCESS
|
|
297
|
+
severity = ErrorSeverity.HIGH
|
|
298
|
+
elif isinstance(error, RuntimeError | AttributeError):
|
|
299
|
+
category = ErrorCategory.ANALYSIS
|
|
300
|
+
severity = ErrorSeverity.MEDIUM
|
|
301
|
+
elif isinstance(error, MemoryError):
|
|
302
|
+
category = ErrorCategory.RESOURCE
|
|
303
|
+
severity = ErrorSeverity.CRITICAL
|
|
304
|
+
recoverable = False
|
|
305
|
+
elif isinstance(error, asyncio.TimeoutError):
|
|
306
|
+
category = ErrorCategory.NETWORK
|
|
307
|
+
severity = ErrorSeverity.MEDIUM
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"error_type": error_type,
|
|
311
|
+
"message": message,
|
|
312
|
+
"category": category.value,
|
|
313
|
+
"severity": severity.value,
|
|
314
|
+
"details": context,
|
|
315
|
+
"recoverable": recoverable,
|
|
316
|
+
"timestamp": datetime.now().isoformat(),
|
|
317
|
+
"operation": operation,
|
|
318
|
+
"traceback": traceback.format_exc(),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def _log_error(
|
|
322
|
+
self,
|
|
323
|
+
error: Exception,
|
|
324
|
+
error_info: dict[str, Any],
|
|
325
|
+
context: dict[str, Any],
|
|
326
|
+
operation: str,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Log error with appropriate level based on severity
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
error: The original exception
|
|
333
|
+
error_info: Classified error information
|
|
334
|
+
context: Error context
|
|
335
|
+
operation: Operation that failed
|
|
336
|
+
"""
|
|
337
|
+
severity = error_info.get("severity", "medium")
|
|
338
|
+
message = f"Error in {operation}: {error_info['message']}"
|
|
339
|
+
|
|
340
|
+
if severity == "critical":
|
|
341
|
+
logger.critical(
|
|
342
|
+
message, extra={"error_info": error_info, "context": context}
|
|
343
|
+
)
|
|
344
|
+
elif severity == "high":
|
|
345
|
+
logger.error(message, extra={"error_info": error_info, "context": context})
|
|
346
|
+
elif severity == "medium":
|
|
347
|
+
logger.warning(
|
|
348
|
+
message, extra={"error_info": error_info, "context": context}
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
logger.info(message, extra={"error_info": error_info, "context": context})
|
|
352
|
+
|
|
353
|
+
def _update_error_stats(self, error_info: dict[str, Any]) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Update error statistics
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
error_info: Error information
|
|
359
|
+
"""
|
|
360
|
+
error_type = error_info.get("error_type", "Unknown")
|
|
361
|
+
category = error_info.get("category", "unknown")
|
|
362
|
+
|
|
363
|
+
# Count by type
|
|
364
|
+
self.error_counts[f"type:{error_type}"] = (
|
|
365
|
+
self.error_counts.get(f"type:{error_type}", 0) + 1
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Count by category
|
|
369
|
+
self.error_counts[f"category:{category}"] = (
|
|
370
|
+
self.error_counts.get(f"category:{category}", 0) + 1
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Count by severity
|
|
374
|
+
severity = error_info.get("severity", "medium")
|
|
375
|
+
self.error_counts[f"severity:{severity}"] = (
|
|
376
|
+
self.error_counts.get(f"severity:{severity}", 0) + 1
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _add_to_history(
|
|
380
|
+
self, error_info: dict[str, Any], context: dict[str, Any], operation: str
|
|
381
|
+
) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Add error to history with size limit
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
error_info: Error information
|
|
387
|
+
context: Error context
|
|
388
|
+
operation: Operation that failed
|
|
389
|
+
"""
|
|
390
|
+
history_entry = {**error_info, "context": context, "operation": operation}
|
|
391
|
+
|
|
392
|
+
self.error_history.append(history_entry)
|
|
393
|
+
|
|
394
|
+
# Maintain history size limit
|
|
395
|
+
if len(self.error_history) > self.max_history_size:
|
|
396
|
+
self.error_history = self.error_history[-self.max_history_size :]
|
|
397
|
+
|
|
398
|
+
def _attempt_recovery(
|
|
399
|
+
self, error: Exception, context: dict[str, Any]
|
|
400
|
+
) -> dict[str, Any] | None:
|
|
401
|
+
"""
|
|
402
|
+
Attempt to recover from error using registered strategies
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
error: The exception to recover from
|
|
406
|
+
context: Error context
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Recovery information or None
|
|
410
|
+
"""
|
|
411
|
+
error_type = type(error)
|
|
412
|
+
|
|
413
|
+
# Try exact type match first
|
|
414
|
+
if error_type in self.recovery_strategies:
|
|
415
|
+
try:
|
|
416
|
+
result = self.recovery_strategies[error_type](error, context)
|
|
417
|
+
return result if result is not None else {}
|
|
418
|
+
except Exception as recovery_error:
|
|
419
|
+
logger.warning(f"Recovery strategy failed: {recovery_error}")
|
|
420
|
+
|
|
421
|
+
# Try parent class matches
|
|
422
|
+
for registered_type, strategy in self.recovery_strategies.items():
|
|
423
|
+
if isinstance(error, registered_type):
|
|
424
|
+
try:
|
|
425
|
+
result = strategy(error, context)
|
|
426
|
+
return result if result is not None else {}
|
|
427
|
+
except Exception as recovery_error:
|
|
428
|
+
logger.warning(f"Recovery strategy failed: {recovery_error}")
|
|
429
|
+
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def get_error_stats(self) -> dict[str, Any]:
|
|
433
|
+
"""
|
|
434
|
+
Get error statistics
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Dictionary containing error statistics
|
|
438
|
+
"""
|
|
439
|
+
total_errors = (
|
|
440
|
+
sum(self.error_counts.values()) // 3
|
|
441
|
+
) # Divide by 3 because we count type, category, severity
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
"total_errors": total_errors,
|
|
445
|
+
"error_counts": self.error_counts.copy(),
|
|
446
|
+
"recent_errors": len(
|
|
447
|
+
[
|
|
448
|
+
e
|
|
449
|
+
for e in self.error_history
|
|
450
|
+
if (datetime.now() - datetime.fromisoformat(e["timestamp"])).seconds
|
|
451
|
+
< 3600
|
|
452
|
+
]
|
|
453
|
+
),
|
|
454
|
+
"history_size": len(self.error_history),
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
def get_recent_errors(self, limit: int = 10) -> list[dict[str, Any]]:
|
|
458
|
+
"""
|
|
459
|
+
Get recent errors from history
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
limit: Maximum number of errors to return
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
List of recent error entries
|
|
466
|
+
"""
|
|
467
|
+
return self.error_history[-limit:] if self.error_history else []
|
|
468
|
+
|
|
469
|
+
def clear_history(self) -> None:
|
|
470
|
+
"""Clear error history and reset statistics"""
|
|
471
|
+
self.error_history.clear()
|
|
472
|
+
self.error_counts.clear()
|
|
473
|
+
logger.info("Error history and statistics cleared")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def handle_mcp_errors(
|
|
477
|
+
operation: str = "unknown",
|
|
478
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
479
|
+
"""
|
|
480
|
+
Decorator for automatic error handling in MCP operations
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
operation: Name of the operation for logging
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Decorated function with error handling
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
490
|
+
@wraps(func)
|
|
491
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
492
|
+
try:
|
|
493
|
+
return await func(*args, **kwargs)
|
|
494
|
+
except RuntimeError as e:
|
|
495
|
+
# Handle initialization errors specifically
|
|
496
|
+
if "not fully initialized" in str(e):
|
|
497
|
+
logger.warning(
|
|
498
|
+
f"Request received before initialization complete: {operation}"
|
|
499
|
+
)
|
|
500
|
+
raise MCPError(
|
|
501
|
+
"Server is still initializing. Please wait a moment and try again.",
|
|
502
|
+
category=ErrorCategory.CONFIGURATION,
|
|
503
|
+
severity=ErrorSeverity.LOW,
|
|
504
|
+
) from e
|
|
505
|
+
# Handle other runtime errors normally
|
|
506
|
+
error_handler = get_error_handler()
|
|
507
|
+
context = {
|
|
508
|
+
"function": func.__name__,
|
|
509
|
+
"args": str(args)[:200], # Limit length
|
|
510
|
+
"kwargs": str(kwargs)[:200],
|
|
511
|
+
}
|
|
512
|
+
error_info = error_handler.handle_error(e, context, operation)
|
|
513
|
+
raise
|
|
514
|
+
except Exception as e:
|
|
515
|
+
error_handler = get_error_handler()
|
|
516
|
+
context = {
|
|
517
|
+
"function": func.__name__,
|
|
518
|
+
"args": str(args)[:200], # Limit length
|
|
519
|
+
"kwargs": str(kwargs)[:200],
|
|
520
|
+
}
|
|
521
|
+
error_info = error_handler.handle_error(e, context, operation)
|
|
522
|
+
|
|
523
|
+
# Re-raise as MCPError if not already
|
|
524
|
+
if not isinstance(e, MCPError):
|
|
525
|
+
raise AnalysisError(
|
|
526
|
+
f"Operation failed: {error_info['message']}",
|
|
527
|
+
operation=operation,
|
|
528
|
+
severity=ErrorSeverity(error_info["severity"]),
|
|
529
|
+
) from e
|
|
530
|
+
raise
|
|
531
|
+
|
|
532
|
+
@wraps(func)
|
|
533
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
534
|
+
try:
|
|
535
|
+
return func(*args, **kwargs)
|
|
536
|
+
except Exception as e:
|
|
537
|
+
error_handler = get_error_handler()
|
|
538
|
+
context = {
|
|
539
|
+
"function": func.__name__,
|
|
540
|
+
"args": str(args)[:200],
|
|
541
|
+
"kwargs": str(kwargs)[:200],
|
|
542
|
+
}
|
|
543
|
+
error_info = error_handler.handle_error(e, context, operation)
|
|
544
|
+
|
|
545
|
+
if not isinstance(e, MCPError):
|
|
546
|
+
raise AnalysisError(
|
|
547
|
+
f"Operation failed: {error_info['message']}",
|
|
548
|
+
operation=operation,
|
|
549
|
+
severity=ErrorSeverity(error_info["severity"]),
|
|
550
|
+
) from e
|
|
551
|
+
raise
|
|
552
|
+
|
|
553
|
+
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
|
554
|
+
|
|
555
|
+
return decorator
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# Global error handler instance
|
|
559
|
+
_error_handler = ErrorHandler()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def get_error_handler() -> ErrorHandler:
|
|
563
|
+
"""
|
|
564
|
+
Get the global error handler instance
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Global error handler
|
|
568
|
+
"""
|
|
569
|
+
return _error_handler
|