mcp-code-indexer 4.0.1__py3-none-any.whl → 4.1.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 +7 -5
- mcp_code_indexer/ask_handler.py +2 -2
- mcp_code_indexer/claude_api_handler.py +10 -5
- mcp_code_indexer/cleanup_manager.py +20 -12
- mcp_code_indexer/commands/makelocal.py +85 -63
- mcp_code_indexer/data/stop_words_english.txt +1 -1
- mcp_code_indexer/database/connection_health.py +29 -20
- mcp_code_indexer/database/database.py +44 -31
- mcp_code_indexer/database/database_factory.py +19 -20
- mcp_code_indexer/database/exceptions.py +10 -10
- mcp_code_indexer/database/models.py +126 -1
- mcp_code_indexer/database/path_resolver.py +22 -21
- mcp_code_indexer/database/retry_executor.py +37 -19
- mcp_code_indexer/deepask_handler.py +3 -3
- mcp_code_indexer/error_handler.py +46 -20
- mcp_code_indexer/file_scanner.py +15 -12
- mcp_code_indexer/git_hook_handler.py +71 -76
- mcp_code_indexer/logging_config.py +13 -5
- mcp_code_indexer/main.py +85 -22
- mcp_code_indexer/middleware/__init__.py +1 -1
- mcp_code_indexer/middleware/auth.py +47 -43
- mcp_code_indexer/middleware/error_middleware.py +15 -15
- mcp_code_indexer/middleware/logging.py +44 -42
- mcp_code_indexer/middleware/security.py +84 -76
- mcp_code_indexer/migrations/002_performance_indexes.sql +1 -1
- mcp_code_indexer/migrations/004_remove_branch_dependency.sql +14 -14
- mcp_code_indexer/migrations/006_vector_mode.sql +189 -0
- mcp_code_indexer/query_preprocessor.py +2 -2
- mcp_code_indexer/server/mcp_server.py +158 -94
- mcp_code_indexer/transport/__init__.py +1 -1
- mcp_code_indexer/transport/base.py +19 -17
- mcp_code_indexer/transport/http_transport.py +89 -76
- mcp_code_indexer/transport/stdio_transport.py +12 -8
- mcp_code_indexer/vector_mode/__init__.py +36 -0
- mcp_code_indexer/vector_mode/chunking/__init__.py +19 -0
- mcp_code_indexer/vector_mode/chunking/ast_chunker.py +403 -0
- mcp_code_indexer/vector_mode/chunking/chunk_optimizer.py +500 -0
- mcp_code_indexer/vector_mode/chunking/language_handlers.py +478 -0
- mcp_code_indexer/vector_mode/config.py +155 -0
- mcp_code_indexer/vector_mode/daemon.py +335 -0
- mcp_code_indexer/vector_mode/monitoring/__init__.py +19 -0
- mcp_code_indexer/vector_mode/monitoring/change_detector.py +312 -0
- mcp_code_indexer/vector_mode/monitoring/file_watcher.py +445 -0
- mcp_code_indexer/vector_mode/monitoring/merkle_tree.py +418 -0
- mcp_code_indexer/vector_mode/providers/__init__.py +72 -0
- mcp_code_indexer/vector_mode/providers/base_provider.py +230 -0
- mcp_code_indexer/vector_mode/providers/turbopuffer_client.py +338 -0
- mcp_code_indexer/vector_mode/providers/voyage_client.py +212 -0
- mcp_code_indexer/vector_mode/security/__init__.py +11 -0
- mcp_code_indexer/vector_mode/security/patterns.py +297 -0
- mcp_code_indexer/vector_mode/security/redactor.py +368 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/METADATA +82 -24
- mcp_code_indexer-4.1.0.dist-info/RECORD +66 -0
- mcp_code_indexer-4.0.1.dist-info/RECORD +0 -47
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/LICENSE +0 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -15,8 +15,8 @@ Key features:
|
|
|
15
15
|
- Special character handling: preserves special characters in quoted terms
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
import re
|
|
19
18
|
import logging
|
|
19
|
+
import re
|
|
20
20
|
from typing import List, Set
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
@@ -33,7 +33,7 @@ class QueryPreprocessor:
|
|
|
33
33
|
# FTS5 operators that need to be escaped when used as literal search terms
|
|
34
34
|
FTS5_OPERATORS: Set[str] = {"AND", "OR", "NOT", "NEAR"}
|
|
35
35
|
|
|
36
|
-
def __init__(self):
|
|
36
|
+
def __init__(self) -> None:
|
|
37
37
|
"""Initialize the query preprocessor."""
|
|
38
38
|
pass
|
|
39
39
|
|
|
@@ -15,30 +15,28 @@ import time
|
|
|
15
15
|
import uuid
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Any, Dict, List, Optional
|
|
18
|
+
from typing import Any, Dict, List, Optional, Callable, cast
|
|
19
19
|
|
|
20
20
|
from mcp import types
|
|
21
21
|
from mcp.server import Server
|
|
22
|
-
from mcp.server.stdio import stdio_server
|
|
23
22
|
from pydantic import ValidationError
|
|
24
23
|
|
|
24
|
+
from mcp_code_indexer.cleanup_manager import CleanupManager
|
|
25
25
|
from mcp_code_indexer.database.database import DatabaseManager
|
|
26
26
|
from mcp_code_indexer.database.database_factory import DatabaseFactory
|
|
27
|
-
from mcp_code_indexer.file_scanner import FileScanner
|
|
28
|
-
from mcp_code_indexer.token_counter import TokenCounter
|
|
29
27
|
from mcp_code_indexer.database.models import (
|
|
30
|
-
Project,
|
|
31
28
|
FileDescription,
|
|
29
|
+
Project,
|
|
32
30
|
ProjectOverview,
|
|
33
31
|
)
|
|
34
32
|
from mcp_code_indexer.error_handler import setup_error_handling
|
|
33
|
+
from mcp_code_indexer.file_scanner import FileScanner
|
|
34
|
+
from mcp_code_indexer.logging_config import get_logger
|
|
35
35
|
from mcp_code_indexer.middleware.error_middleware import (
|
|
36
|
-
create_tool_middleware,
|
|
37
36
|
AsyncTaskManager,
|
|
37
|
+
create_tool_middleware,
|
|
38
38
|
)
|
|
39
|
-
from mcp_code_indexer.
|
|
40
|
-
from mcp_code_indexer.cleanup_manager import CleanupManager
|
|
41
|
-
|
|
39
|
+
from mcp_code_indexer.token_counter import TokenCounter
|
|
42
40
|
|
|
43
41
|
logger = logging.getLogger(__name__)
|
|
44
42
|
|
|
@@ -65,6 +63,7 @@ class MCPCodeIndexServer:
|
|
|
65
63
|
retry_max_wait: float = 2.0,
|
|
66
64
|
retry_jitter: float = 0.2,
|
|
67
65
|
transport: Optional[Any] = None,
|
|
66
|
+
vector_mode: bool = False,
|
|
68
67
|
):
|
|
69
68
|
"""
|
|
70
69
|
Initialize the MCP Code Index Server.
|
|
@@ -82,10 +81,12 @@ class MCPCodeIndexServer:
|
|
|
82
81
|
retry_max_wait: Maximum wait time between retries in seconds
|
|
83
82
|
retry_jitter: Maximum jitter to add to retry delays in seconds
|
|
84
83
|
transport: Optional transport instance (if None, uses default stdio)
|
|
84
|
+
vector_mode: Enable vector search capabilities and tools
|
|
85
85
|
"""
|
|
86
86
|
self.token_limit = token_limit
|
|
87
87
|
self.db_path = db_path or Path.home() / ".mcp-code-index" / "tracker.db"
|
|
88
88
|
self.cache_dir = cache_dir or Path.home() / ".mcp-code-index" / "cache"
|
|
89
|
+
self.vector_mode = vector_mode
|
|
89
90
|
|
|
90
91
|
# Store database configuration
|
|
91
92
|
self.db_config = {
|
|
@@ -112,9 +113,11 @@ class MCPCodeIndexServer:
|
|
|
112
113
|
retry_jitter=retry_jitter,
|
|
113
114
|
)
|
|
114
115
|
# Keep reference to global db_manager for backwards compatibility
|
|
115
|
-
self.db_manager = None # Will be set during run()
|
|
116
|
+
self.db_manager: Optional[DatabaseManager] = None # Will be set during run()
|
|
116
117
|
self.token_counter = TokenCounter(token_limit)
|
|
117
|
-
self.cleanup_manager =
|
|
118
|
+
self.cleanup_manager: Optional[CleanupManager] = (
|
|
119
|
+
None # Will be set during initialize()
|
|
120
|
+
)
|
|
118
121
|
self.transport = transport
|
|
119
122
|
|
|
120
123
|
# Setup error handling
|
|
@@ -124,7 +127,7 @@ class MCPCodeIndexServer:
|
|
|
124
127
|
self.task_manager = AsyncTaskManager(self.error_handler)
|
|
125
128
|
|
|
126
129
|
# Create MCP server
|
|
127
|
-
self.server = Server("mcp-code-indexer")
|
|
130
|
+
self.server: Server = Server("mcp-code-indexer")
|
|
128
131
|
|
|
129
132
|
# Register handlers
|
|
130
133
|
self._register_handlers()
|
|
@@ -168,7 +171,7 @@ class MCPCodeIndexServer:
|
|
|
168
171
|
Returns:
|
|
169
172
|
Dictionary with HTML entities decoded in all string values
|
|
170
173
|
"""
|
|
171
|
-
cleaned = {}
|
|
174
|
+
cleaned: Dict[str, Any] = {}
|
|
172
175
|
|
|
173
176
|
for key, value in arguments.items():
|
|
174
177
|
if isinstance(value, str):
|
|
@@ -203,7 +206,11 @@ class MCPCodeIndexServer:
|
|
|
203
206
|
"""
|
|
204
207
|
# First try normal parsing
|
|
205
208
|
try:
|
|
206
|
-
|
|
209
|
+
result = json.loads(json_str)
|
|
210
|
+
if isinstance(result, dict):
|
|
211
|
+
return result
|
|
212
|
+
else:
|
|
213
|
+
raise ValueError(f"Parsed JSON is not a dictionary: {type(result)}")
|
|
207
214
|
except json.JSONDecodeError as original_error:
|
|
208
215
|
logger.warning(f"Initial JSON parse failed: {original_error}")
|
|
209
216
|
|
|
@@ -232,11 +239,16 @@ class MCPCodeIndexServer:
|
|
|
232
239
|
|
|
233
240
|
try:
|
|
234
241
|
result = json.loads(repaired)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
if isinstance(result, dict):
|
|
243
|
+
logger.info(
|
|
244
|
+
f"Successfully repaired JSON. Original: {json_str[:100]}..."
|
|
245
|
+
)
|
|
246
|
+
logger.info(f"Repaired: {repaired[:100]}...")
|
|
247
|
+
return result
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Repaired JSON is not a dictionary: {type(result)}"
|
|
251
|
+
)
|
|
240
252
|
except json.JSONDecodeError as repair_error:
|
|
241
253
|
logger.error(f"JSON repair failed. Original: {json_str}")
|
|
242
254
|
logger.error(f"Repaired attempt: {repaired}")
|
|
@@ -250,14 +262,15 @@ class MCPCodeIndexServer:
|
|
|
250
262
|
# Initialize global database manager for backwards compatibility
|
|
251
263
|
self.db_manager = await self.db_factory.get_database_manager()
|
|
252
264
|
# Update cleanup manager with initialized db_manager
|
|
253
|
-
|
|
265
|
+
if self.db_manager is not None:
|
|
266
|
+
self.cleanup_manager = CleanupManager(self.db_manager, retention_months=6)
|
|
254
267
|
self._start_background_cleanup()
|
|
255
268
|
logger.info("Server initialized successfully")
|
|
256
269
|
|
|
257
270
|
def _register_handlers(self) -> None:
|
|
258
271
|
"""Register MCP tool and resource handlers."""
|
|
259
272
|
|
|
260
|
-
@self.server.list_tools()
|
|
273
|
+
@self.server.list_tools() # type: ignore[misc]
|
|
261
274
|
async def list_tools() -> List[types.Tool]:
|
|
262
275
|
"""Return list of available tools."""
|
|
263
276
|
return [
|
|
@@ -673,7 +686,7 @@ class MCPCodeIndexServer:
|
|
|
673
686
|
),
|
|
674
687
|
]
|
|
675
688
|
|
|
676
|
-
@self.server.call_tool()
|
|
689
|
+
@self.server.call_tool() # type: ignore[misc]
|
|
677
690
|
async def call_tool(
|
|
678
691
|
name: str, arguments: Dict[str, Any]
|
|
679
692
|
) -> List[types.TextContent]:
|
|
@@ -719,7 +732,14 @@ class MCPCodeIndexServer:
|
|
|
719
732
|
f"MCP Tool '{name}' completed successfully in {elapsed_time:.2f}s"
|
|
720
733
|
)
|
|
721
734
|
|
|
722
|
-
|
|
735
|
+
# Ensure result is List[types.TextContent]
|
|
736
|
+
if isinstance(result, list) and all(
|
|
737
|
+
isinstance(item, types.TextContent) for item in result
|
|
738
|
+
):
|
|
739
|
+
return result
|
|
740
|
+
else:
|
|
741
|
+
# Fallback: convert to proper format
|
|
742
|
+
return [types.TextContent(type="text", text=str(result))]
|
|
723
743
|
except Exception as e:
|
|
724
744
|
elapsed_time = time.time() - start_time
|
|
725
745
|
logger.error(f"MCP Tool '{name}' failed after {elapsed_time:.2f}s: {e}")
|
|
@@ -727,7 +747,7 @@ class MCPCodeIndexServer:
|
|
|
727
747
|
raise
|
|
728
748
|
|
|
729
749
|
async def _execute_tool_handler(
|
|
730
|
-
self, handler, arguments: Dict[str, Any]
|
|
750
|
+
self, handler: Callable[[Dict[str, Any]], Any], arguments: Dict[str, Any]
|
|
731
751
|
) -> List[types.TextContent]:
|
|
732
752
|
"""Execute a tool handler and format the result."""
|
|
733
753
|
# Clean HTML entities from all arguments before processing
|
|
@@ -753,10 +773,10 @@ class MCPCodeIndexServer:
|
|
|
753
773
|
|
|
754
774
|
# Get the appropriate database manager for this folder
|
|
755
775
|
db_manager = await self.db_factory.get_database_manager(folder_path)
|
|
756
|
-
|
|
776
|
+
|
|
757
777
|
# Check if this is a local database
|
|
758
778
|
is_local = self.db_factory.get_path_resolver().is_local_database(folder_path)
|
|
759
|
-
|
|
779
|
+
|
|
760
780
|
if is_local:
|
|
761
781
|
# For local databases: just get the single project (there should only be one)
|
|
762
782
|
all_projects = await db_manager.get_all_projects()
|
|
@@ -764,7 +784,9 @@ class MCPCodeIndexServer:
|
|
|
764
784
|
project = all_projects[0] # Use the first (and should be only) project
|
|
765
785
|
# Update last accessed time
|
|
766
786
|
await db_manager.update_project_access_time(project.id)
|
|
767
|
-
logger.info(
|
|
787
|
+
logger.info(
|
|
788
|
+
f"Using existing local project: {project.name} (ID: {project.id})"
|
|
789
|
+
)
|
|
768
790
|
return project.id
|
|
769
791
|
else:
|
|
770
792
|
# No project in local database - create one
|
|
@@ -772,22 +794,30 @@ class MCPCodeIndexServer:
|
|
|
772
794
|
project = Project(
|
|
773
795
|
id=project_id,
|
|
774
796
|
name=project_name.lower(),
|
|
775
|
-
aliases=[
|
|
797
|
+
aliases=[
|
|
798
|
+
folder_path
|
|
799
|
+
], # Store for reference but don't rely on it for matching
|
|
776
800
|
created=datetime.utcnow(),
|
|
777
801
|
last_accessed=datetime.utcnow(),
|
|
778
802
|
)
|
|
779
803
|
await db_manager.create_project(project)
|
|
780
|
-
logger.info(
|
|
804
|
+
logger.info(
|
|
805
|
+
f"Created new local project: {project_name} (ID: {project_id})"
|
|
806
|
+
)
|
|
781
807
|
return project_id
|
|
782
808
|
else:
|
|
783
809
|
# For global databases: use the existing matching logic
|
|
784
810
|
normalized_name = project_name.lower()
|
|
785
811
|
|
|
786
812
|
# Find potential project matches
|
|
787
|
-
project = await self._find_matching_project(
|
|
813
|
+
project = await self._find_matching_project( # type: ignore[assignment]
|
|
814
|
+
normalized_name, folder_path, db_manager
|
|
815
|
+
)
|
|
788
816
|
if project:
|
|
789
817
|
# Update project metadata and aliases
|
|
790
|
-
await self._update_existing_project(
|
|
818
|
+
await self._update_existing_project(
|
|
819
|
+
project, normalized_name, folder_path, db_manager
|
|
820
|
+
)
|
|
791
821
|
else:
|
|
792
822
|
# Create new project with UUID
|
|
793
823
|
project_id = str(uuid.uuid4())
|
|
@@ -799,8 +829,12 @@ class MCPCodeIndexServer:
|
|
|
799
829
|
last_accessed=datetime.utcnow(),
|
|
800
830
|
)
|
|
801
831
|
await db_manager.create_project(project)
|
|
802
|
-
logger.info(
|
|
832
|
+
logger.info(
|
|
833
|
+
f"Created new global project: {normalized_name} (ID: {project_id})"
|
|
834
|
+
)
|
|
803
835
|
|
|
836
|
+
if project is None:
|
|
837
|
+
raise RuntimeError("Project should always be set in if/else branches above")
|
|
804
838
|
return project.id
|
|
805
839
|
|
|
806
840
|
async def _find_matching_project(
|
|
@@ -826,11 +860,7 @@ class MCPCodeIndexServer:
|
|
|
826
860
|
match_factors.append("name")
|
|
827
861
|
|
|
828
862
|
# Factor 2: Folder path in aliases
|
|
829
|
-
project_aliases =
|
|
830
|
-
json.loads(project.aliases)
|
|
831
|
-
if isinstance(project.aliases, str)
|
|
832
|
-
else project.aliases
|
|
833
|
-
)
|
|
863
|
+
project_aliases = project.aliases
|
|
834
864
|
if folder_path in project_aliases:
|
|
835
865
|
score += 1
|
|
836
866
|
match_factors.append("folder_path")
|
|
@@ -878,7 +908,7 @@ class MCPCodeIndexServer:
|
|
|
878
908
|
|
|
879
909
|
# Get appropriate database manager for this folder
|
|
880
910
|
db_manager = await self.db_factory.get_database_manager(folder_path)
|
|
881
|
-
|
|
911
|
+
|
|
882
912
|
# Get files already indexed for this project
|
|
883
913
|
indexed_files = await db_manager.get_all_file_descriptions(project.id)
|
|
884
914
|
indexed_basenames = {Path(fd.file_path).name for fd in indexed_files}
|
|
@@ -901,7 +931,11 @@ class MCPCodeIndexServer:
|
|
|
901
931
|
return False
|
|
902
932
|
|
|
903
933
|
async def _update_existing_project(
|
|
904
|
-
self,
|
|
934
|
+
self,
|
|
935
|
+
project: Project,
|
|
936
|
+
normalized_name: str,
|
|
937
|
+
folder_path: str,
|
|
938
|
+
db_manager: DatabaseManager,
|
|
905
939
|
) -> None:
|
|
906
940
|
"""Update an existing project with new metadata and folder alias."""
|
|
907
941
|
# Update last accessed time
|
|
@@ -915,11 +949,7 @@ class MCPCodeIndexServer:
|
|
|
915
949
|
should_update = True
|
|
916
950
|
|
|
917
951
|
# Add folder path to aliases if not already present
|
|
918
|
-
project_aliases =
|
|
919
|
-
json.loads(project.aliases)
|
|
920
|
-
if isinstance(project.aliases, str)
|
|
921
|
-
else project.aliases
|
|
922
|
-
)
|
|
952
|
+
project_aliases = project.aliases
|
|
923
953
|
if folder_path not in project_aliases:
|
|
924
954
|
project_aliases.append(folder_path)
|
|
925
955
|
project.aliases = project_aliases
|
|
@@ -975,12 +1005,15 @@ class MCPCodeIndexServer:
|
|
|
975
1005
|
logger.info(f"Resolved project_id: {project_id}")
|
|
976
1006
|
|
|
977
1007
|
file_desc = FileDescription(
|
|
1008
|
+
id=None, # Will be set by database
|
|
978
1009
|
project_id=project_id,
|
|
979
1010
|
file_path=arguments["filePath"],
|
|
980
1011
|
description=arguments["description"],
|
|
981
1012
|
file_hash=arguments.get("fileHash"),
|
|
982
1013
|
last_modified=datetime.utcnow(),
|
|
983
1014
|
version=1,
|
|
1015
|
+
source_project_id=None,
|
|
1016
|
+
to_be_cleaned=None,
|
|
984
1017
|
)
|
|
985
1018
|
|
|
986
1019
|
await db_manager.create_file_description(file_desc)
|
|
@@ -1039,13 +1072,13 @@ class MCPCodeIndexServer:
|
|
|
1039
1072
|
|
|
1040
1073
|
total_tokens = descriptions_tokens + overview_tokens
|
|
1041
1074
|
is_large = total_tokens > token_limit
|
|
1042
|
-
|
|
1075
|
+
|
|
1043
1076
|
# Smart recommendation logic:
|
|
1044
1077
|
# - If total is small, use overview
|
|
1045
1078
|
# - If total is large but overview is reasonable (< 8k tokens), recommend viewing overview + search
|
|
1046
1079
|
# - If both are large, use search only
|
|
1047
1080
|
overview_size_limit = 32000
|
|
1048
|
-
|
|
1081
|
+
|
|
1049
1082
|
if not is_large:
|
|
1050
1083
|
recommendation = "use_overview"
|
|
1051
1084
|
elif overview_tokens > 0 and overview_tokens <= overview_size_limit:
|
|
@@ -1103,7 +1136,9 @@ class MCPCodeIndexServer:
|
|
|
1103
1136
|
logger.info(f"Scanning project directory: {folder_path_obj}")
|
|
1104
1137
|
scanner = FileScanner(folder_path_obj)
|
|
1105
1138
|
if not scanner.is_valid_project_directory():
|
|
1106
|
-
logger.error(
|
|
1139
|
+
logger.error(
|
|
1140
|
+
f"Invalid or inaccessible project directory: {folder_path_obj}"
|
|
1141
|
+
)
|
|
1107
1142
|
return {
|
|
1108
1143
|
"error": f"Invalid or inaccessible project directory: {folder_path_obj}"
|
|
1109
1144
|
}
|
|
@@ -1207,28 +1242,28 @@ class MCPCodeIndexServer:
|
|
|
1207
1242
|
root = {"path": "", "files": [], "folders": {}}
|
|
1208
1243
|
|
|
1209
1244
|
for file_desc in file_descriptions:
|
|
1210
|
-
path_parts = Path(file_desc.file_path).parts
|
|
1245
|
+
path_parts = cast(List[str], list(Path(file_desc.file_path).parts))
|
|
1211
1246
|
current = root
|
|
1212
1247
|
|
|
1213
1248
|
# Navigate/create folder structure
|
|
1214
1249
|
for i, part in enumerate(path_parts[:-1]):
|
|
1215
1250
|
folder_path = "/".join(path_parts[: i + 1])
|
|
1216
1251
|
if part not in current["folders"]:
|
|
1217
|
-
current["folders"][part] = {
|
|
1252
|
+
current["folders"][part] = { # type: ignore[index]
|
|
1218
1253
|
"path": folder_path,
|
|
1219
1254
|
"files": [],
|
|
1220
1255
|
"folders": {},
|
|
1221
1256
|
}
|
|
1222
|
-
current = current["folders"][part]
|
|
1257
|
+
current = current["folders"][part] # type: ignore[index]
|
|
1223
1258
|
|
|
1224
1259
|
# Add file to current folder
|
|
1225
1260
|
if path_parts: # Handle empty paths
|
|
1226
|
-
current["files"].append(
|
|
1261
|
+
current["files"].append( # type: ignore[attr-defined]
|
|
1227
1262
|
{"path": file_desc.file_path, "description": file_desc.description}
|
|
1228
1263
|
)
|
|
1229
1264
|
|
|
1230
1265
|
# Convert nested dict structure to list format, skipping empty folders
|
|
1231
|
-
def convert_structure(node):
|
|
1266
|
+
def convert_structure(node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1232
1267
|
folders = []
|
|
1233
1268
|
for folder in node["folders"].values():
|
|
1234
1269
|
converted_folder = convert_structure(folder)
|
|
@@ -1389,40 +1424,56 @@ class MCPCodeIndexServer:
|
|
|
1389
1424
|
"""
|
|
1390
1425
|
# Get comprehensive health diagnostics from the enhanced monitor
|
|
1391
1426
|
if (
|
|
1392
|
-
|
|
1427
|
+
self.db_manager
|
|
1428
|
+
and hasattr(self.db_manager, "_health_monitor")
|
|
1393
1429
|
and self.db_manager._health_monitor
|
|
1394
1430
|
):
|
|
1395
1431
|
comprehensive_diagnostics = (
|
|
1396
1432
|
self.db_manager._health_monitor.get_comprehensive_diagnostics()
|
|
1397
1433
|
)
|
|
1398
|
-
|
|
1434
|
+
elif self.db_manager:
|
|
1399
1435
|
# Fallback to basic health check if monitor not available
|
|
1400
1436
|
health_check = await self.db_manager.check_health()
|
|
1401
1437
|
comprehensive_diagnostics = {
|
|
1402
1438
|
"basic_health_check": health_check,
|
|
1403
1439
|
"note": "Enhanced health monitoring not available",
|
|
1404
1440
|
}
|
|
1441
|
+
else:
|
|
1442
|
+
comprehensive_diagnostics = {
|
|
1443
|
+
"error": "Database manager not initialized",
|
|
1444
|
+
}
|
|
1405
1445
|
|
|
1406
1446
|
# Get additional database-level statistics
|
|
1407
|
-
database_stats = self.db_manager.get_database_stats()
|
|
1447
|
+
database_stats = self.db_manager.get_database_stats() if self.db_manager else {}
|
|
1408
1448
|
|
|
1409
1449
|
return {
|
|
1410
|
-
"is_healthy": comprehensive_diagnostics.get("current_status", {}).get(
|
|
1450
|
+
"is_healthy": comprehensive_diagnostics.get("current_status", {}).get(
|
|
1451
|
+
"is_healthy", True
|
|
1452
|
+
),
|
|
1411
1453
|
"status": comprehensive_diagnostics.get("current_status", {}),
|
|
1412
1454
|
"performance": {
|
|
1413
|
-
"avg_response_time_ms": comprehensive_diagnostics.get(
|
|
1414
|
-
|
|
1455
|
+
"avg_response_time_ms": comprehensive_diagnostics.get(
|
|
1456
|
+
"metrics", {}
|
|
1457
|
+
).get("avg_response_time_ms", 0),
|
|
1458
|
+
"success_rate": comprehensive_diagnostics.get("current_status", {}).get(
|
|
1459
|
+
"recent_success_rate_percent", 100
|
|
1460
|
+
),
|
|
1415
1461
|
},
|
|
1416
1462
|
"database": {
|
|
1417
|
-
"total_operations": database_stats.get("retry_executor", {}).get(
|
|
1418
|
-
|
|
1463
|
+
"total_operations": database_stats.get("retry_executor", {}).get(
|
|
1464
|
+
"total_operations", 0
|
|
1465
|
+
),
|
|
1466
|
+
"pool_size": database_stats.get("connection_pool", {}).get(
|
|
1467
|
+
"current_size", 0
|
|
1468
|
+
),
|
|
1419
1469
|
},
|
|
1420
1470
|
"server_info": {
|
|
1421
1471
|
"token_limit": self.token_limit,
|
|
1422
1472
|
"db_path": str(self.db_path),
|
|
1423
1473
|
"cache_dir": str(self.cache_dir),
|
|
1424
1474
|
"health_monitoring_enabled": (
|
|
1425
|
-
|
|
1475
|
+
self.db_manager is not None
|
|
1476
|
+
and hasattr(self.db_manager, "_health_monitor")
|
|
1426
1477
|
and self.db_manager._health_monitor is not None
|
|
1427
1478
|
),
|
|
1428
1479
|
},
|
|
@@ -1467,7 +1518,7 @@ class MCPCodeIndexServer:
|
|
|
1467
1518
|
}
|
|
1468
1519
|
|
|
1469
1520
|
async def _run_session_with_retry(
|
|
1470
|
-
self, read_stream, write_stream, initialization_options
|
|
1521
|
+
self, read_stream: Any, write_stream: Any, initialization_options: Any
|
|
1471
1522
|
) -> None:
|
|
1472
1523
|
"""Run a single MCP session with error handling and retry logic."""
|
|
1473
1524
|
max_retries = 3
|
|
@@ -1566,7 +1617,7 @@ class MCPCodeIndexServer:
|
|
|
1566
1617
|
try:
|
|
1567
1618
|
await asyncio.sleep(6 * 60 * 60) # 6 hours
|
|
1568
1619
|
await self._run_cleanup_if_needed()
|
|
1569
|
-
|
|
1620
|
+
|
|
1570
1621
|
except asyncio.CancelledError:
|
|
1571
1622
|
logger.info("Periodic cleanup task cancelled")
|
|
1572
1623
|
break
|
|
@@ -1574,43 +1625,48 @@ class MCPCodeIndexServer:
|
|
|
1574
1625
|
logger.error(f"Error in periodic cleanup: {e}")
|
|
1575
1626
|
# Continue running despite errors
|
|
1576
1627
|
|
|
1577
|
-
async def _run_cleanup_if_needed(
|
|
1628
|
+
async def _run_cleanup_if_needed(
|
|
1629
|
+
self, project_id: Optional[str] = None, project_root: Optional[Path] = None
|
|
1630
|
+
) -> int:
|
|
1578
1631
|
"""Run cleanup if conditions are met (not running, not run recently)."""
|
|
1579
1632
|
current_time = time.time()
|
|
1580
|
-
|
|
1633
|
+
|
|
1581
1634
|
# Check if cleanup is already running
|
|
1582
1635
|
if self._cleanup_running:
|
|
1583
1636
|
logger.debug("Cleanup already running, skipping")
|
|
1584
1637
|
return 0
|
|
1585
|
-
|
|
1638
|
+
|
|
1586
1639
|
# Check if cleanup was run in the last 30 minutes
|
|
1587
|
-
if
|
|
1588
|
-
current_time - self._last_cleanup_time < 30 * 60):
|
|
1640
|
+
if self._last_cleanup_time and current_time - self._last_cleanup_time < 30 * 60:
|
|
1589
1641
|
logger.debug("Cleanup ran recently, skipping")
|
|
1590
1642
|
return 0
|
|
1591
|
-
|
|
1643
|
+
|
|
1592
1644
|
# Set running flag and update time
|
|
1593
1645
|
self._cleanup_running = True
|
|
1594
1646
|
self._last_cleanup_time = current_time
|
|
1595
|
-
|
|
1647
|
+
|
|
1596
1648
|
try:
|
|
1597
1649
|
logger.info("Starting cleanup")
|
|
1598
1650
|
total_cleaned = 0
|
|
1599
|
-
|
|
1651
|
+
|
|
1600
1652
|
if project_id and project_root:
|
|
1601
1653
|
# Single project cleanup - use appropriate database for this project's folder
|
|
1602
1654
|
try:
|
|
1603
|
-
folder_db_manager = await self.db_factory.get_database_manager(
|
|
1655
|
+
folder_db_manager = await self.db_factory.get_database_manager(
|
|
1656
|
+
str(project_root)
|
|
1657
|
+
)
|
|
1604
1658
|
missing_files = await folder_db_manager.cleanup_missing_files(
|
|
1605
1659
|
project_id=project_id, project_root=project_root
|
|
1606
1660
|
)
|
|
1607
1661
|
total_cleaned = len(missing_files)
|
|
1608
|
-
|
|
1662
|
+
|
|
1609
1663
|
# Perform permanent cleanup (retention policy)
|
|
1610
|
-
deleted_count =
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1664
|
+
deleted_count = 0
|
|
1665
|
+
if self.cleanup_manager:
|
|
1666
|
+
deleted_count = await self.cleanup_manager.perform_cleanup(
|
|
1667
|
+
project_id=project_id
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1614
1670
|
if missing_files or deleted_count:
|
|
1615
1671
|
logger.info(
|
|
1616
1672
|
f"Cleanup: {len(missing_files)} marked, "
|
|
@@ -1620,31 +1676,38 @@ class MCPCodeIndexServer:
|
|
|
1620
1676
|
logger.error(f"Error during cleanup: {e}")
|
|
1621
1677
|
else:
|
|
1622
1678
|
# All projects cleanup (for periodic task) - start with global database
|
|
1679
|
+
if not self.db_manager:
|
|
1680
|
+
logger.error("Database manager not initialized")
|
|
1681
|
+
return 0
|
|
1623
1682
|
projects = await self.db_manager.get_all_projects()
|
|
1624
|
-
|
|
1683
|
+
|
|
1625
1684
|
for project in projects:
|
|
1626
1685
|
try:
|
|
1627
1686
|
# Skip projects without folder paths in aliases
|
|
1628
1687
|
if not project.aliases:
|
|
1629
1688
|
continue
|
|
1630
|
-
|
|
1689
|
+
|
|
1631
1690
|
# Use first alias as folder path
|
|
1632
1691
|
folder_path = Path(project.aliases[0])
|
|
1633
1692
|
if not folder_path.exists():
|
|
1634
1693
|
continue
|
|
1635
|
-
|
|
1694
|
+
|
|
1636
1695
|
# Get appropriate database manager for this project's folder
|
|
1637
|
-
project_db_manager = await self.db_factory.get_database_manager(
|
|
1696
|
+
project_db_manager = await self.db_factory.get_database_manager(
|
|
1697
|
+
str(folder_path)
|
|
1698
|
+
)
|
|
1638
1699
|
missing_files = await project_db_manager.cleanup_missing_files(
|
|
1639
1700
|
project_id=project.id, project_root=folder_path
|
|
1640
1701
|
)
|
|
1641
1702
|
total_cleaned += len(missing_files)
|
|
1642
|
-
|
|
1703
|
+
|
|
1643
1704
|
# Perform permanent cleanup (retention policy)
|
|
1644
|
-
deleted_count =
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1705
|
+
deleted_count = 0
|
|
1706
|
+
if self.cleanup_manager:
|
|
1707
|
+
deleted_count = await self.cleanup_manager.perform_cleanup(
|
|
1708
|
+
project_id=project.id
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1648
1711
|
if missing_files or deleted_count:
|
|
1649
1712
|
logger.info(
|
|
1650
1713
|
f"Cleanup for {project.name}: "
|
|
@@ -1655,10 +1718,10 @@ class MCPCodeIndexServer:
|
|
|
1655
1718
|
logger.error(
|
|
1656
1719
|
f"Error during cleanup for project {project.name}: {e}"
|
|
1657
1720
|
)
|
|
1658
|
-
|
|
1721
|
+
|
|
1659
1722
|
logger.info(f"Cleanup completed: {total_cleaned} files processed")
|
|
1660
1723
|
return total_cleaned
|
|
1661
|
-
|
|
1724
|
+
|
|
1662
1725
|
finally:
|
|
1663
1726
|
self._cleanup_running = False
|
|
1664
1727
|
|
|
@@ -1666,8 +1729,7 @@ class MCPCodeIndexServer:
|
|
|
1666
1729
|
"""Start the background cleanup task."""
|
|
1667
1730
|
if self._cleanup_task is None or self._cleanup_task.done():
|
|
1668
1731
|
self._cleanup_task = self.task_manager.create_task(
|
|
1669
|
-
self._periodic_cleanup(),
|
|
1670
|
-
name="periodic_cleanup"
|
|
1732
|
+
self._periodic_cleanup(), name="periodic_cleanup"
|
|
1671
1733
|
)
|
|
1672
1734
|
logger.info("Started background cleanup task (6-hour interval)")
|
|
1673
1735
|
|
|
@@ -1685,10 +1747,11 @@ class MCPCodeIndexServer:
|
|
|
1685
1747
|
else:
|
|
1686
1748
|
# Fall back to default stdio transport
|
|
1687
1749
|
from ..transport.stdio_transport import StdioTransport
|
|
1750
|
+
|
|
1688
1751
|
transport = StdioTransport(self)
|
|
1689
1752
|
await transport.initialize()
|
|
1690
1753
|
await transport._run_with_retry()
|
|
1691
|
-
|
|
1754
|
+
|
|
1692
1755
|
except KeyboardInterrupt:
|
|
1693
1756
|
logger.info("Server stopped by user interrupt")
|
|
1694
1757
|
except Exception as e:
|
|
@@ -1716,12 +1779,13 @@ class MCPCodeIndexServer:
|
|
|
1716
1779
|
await self._cleanup_task
|
|
1717
1780
|
except asyncio.CancelledError:
|
|
1718
1781
|
pass
|
|
1719
|
-
|
|
1782
|
+
|
|
1720
1783
|
# Cancel any running tasks
|
|
1721
1784
|
self.task_manager.cancel_all()
|
|
1722
1785
|
|
|
1723
1786
|
# Close database connections
|
|
1724
|
-
|
|
1787
|
+
if self.db_manager:
|
|
1788
|
+
await self.db_manager.close_pool()
|
|
1725
1789
|
|
|
1726
1790
|
self.logger.info("Server shutdown completed successfully")
|
|
1727
1791
|
|
|
@@ -1729,7 +1793,7 @@ class MCPCodeIndexServer:
|
|
|
1729
1793
|
self.error_handler.log_error(e, context={"phase": "shutdown"})
|
|
1730
1794
|
|
|
1731
1795
|
|
|
1732
|
-
async def main():
|
|
1796
|
+
async def main() -> None:
|
|
1733
1797
|
"""Main entry point for the MCP server."""
|
|
1734
1798
|
import sys
|
|
1735
1799
|
|
|
@@ -6,7 +6,7 @@ methods (stdio, HTTP) while maintaining common interface and functionality.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .base import Transport
|
|
9
|
-
from .stdio_transport import StdioTransport
|
|
10
9
|
from .http_transport import HTTPTransport
|
|
10
|
+
from .stdio_transport import StdioTransport
|
|
11
11
|
|
|
12
12
|
__all__ = ["Transport", "StdioTransport", "HTTPTransport"]
|