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.
Files changed (57) hide show
  1. mcp_code_indexer/__init__.py +7 -5
  2. mcp_code_indexer/ask_handler.py +2 -2
  3. mcp_code_indexer/claude_api_handler.py +10 -5
  4. mcp_code_indexer/cleanup_manager.py +20 -12
  5. mcp_code_indexer/commands/makelocal.py +85 -63
  6. mcp_code_indexer/data/stop_words_english.txt +1 -1
  7. mcp_code_indexer/database/connection_health.py +29 -20
  8. mcp_code_indexer/database/database.py +44 -31
  9. mcp_code_indexer/database/database_factory.py +19 -20
  10. mcp_code_indexer/database/exceptions.py +10 -10
  11. mcp_code_indexer/database/models.py +126 -1
  12. mcp_code_indexer/database/path_resolver.py +22 -21
  13. mcp_code_indexer/database/retry_executor.py +37 -19
  14. mcp_code_indexer/deepask_handler.py +3 -3
  15. mcp_code_indexer/error_handler.py +46 -20
  16. mcp_code_indexer/file_scanner.py +15 -12
  17. mcp_code_indexer/git_hook_handler.py +71 -76
  18. mcp_code_indexer/logging_config.py +13 -5
  19. mcp_code_indexer/main.py +85 -22
  20. mcp_code_indexer/middleware/__init__.py +1 -1
  21. mcp_code_indexer/middleware/auth.py +47 -43
  22. mcp_code_indexer/middleware/error_middleware.py +15 -15
  23. mcp_code_indexer/middleware/logging.py +44 -42
  24. mcp_code_indexer/middleware/security.py +84 -76
  25. mcp_code_indexer/migrations/002_performance_indexes.sql +1 -1
  26. mcp_code_indexer/migrations/004_remove_branch_dependency.sql +14 -14
  27. mcp_code_indexer/migrations/006_vector_mode.sql +189 -0
  28. mcp_code_indexer/query_preprocessor.py +2 -2
  29. mcp_code_indexer/server/mcp_server.py +158 -94
  30. mcp_code_indexer/transport/__init__.py +1 -1
  31. mcp_code_indexer/transport/base.py +19 -17
  32. mcp_code_indexer/transport/http_transport.py +89 -76
  33. mcp_code_indexer/transport/stdio_transport.py +12 -8
  34. mcp_code_indexer/vector_mode/__init__.py +36 -0
  35. mcp_code_indexer/vector_mode/chunking/__init__.py +19 -0
  36. mcp_code_indexer/vector_mode/chunking/ast_chunker.py +403 -0
  37. mcp_code_indexer/vector_mode/chunking/chunk_optimizer.py +500 -0
  38. mcp_code_indexer/vector_mode/chunking/language_handlers.py +478 -0
  39. mcp_code_indexer/vector_mode/config.py +155 -0
  40. mcp_code_indexer/vector_mode/daemon.py +335 -0
  41. mcp_code_indexer/vector_mode/monitoring/__init__.py +19 -0
  42. mcp_code_indexer/vector_mode/monitoring/change_detector.py +312 -0
  43. mcp_code_indexer/vector_mode/monitoring/file_watcher.py +445 -0
  44. mcp_code_indexer/vector_mode/monitoring/merkle_tree.py +418 -0
  45. mcp_code_indexer/vector_mode/providers/__init__.py +72 -0
  46. mcp_code_indexer/vector_mode/providers/base_provider.py +230 -0
  47. mcp_code_indexer/vector_mode/providers/turbopuffer_client.py +338 -0
  48. mcp_code_indexer/vector_mode/providers/voyage_client.py +212 -0
  49. mcp_code_indexer/vector_mode/security/__init__.py +11 -0
  50. mcp_code_indexer/vector_mode/security/patterns.py +297 -0
  51. mcp_code_indexer/vector_mode/security/redactor.py +368 -0
  52. {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/METADATA +82 -24
  53. mcp_code_indexer-4.1.0.dist-info/RECORD +66 -0
  54. mcp_code_indexer-4.0.1.dist-info/RECORD +0 -47
  55. {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/LICENSE +0 -0
  56. {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/WHEEL +0 -0
  57. {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.logging_config import get_logger
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 = None # Will be set during initialize()
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
- return json.loads(json_str)
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
- logger.info(
236
- f"Successfully repaired JSON. Original: {json_str[:100]}..."
237
- )
238
- logger.info(f"Repaired: {repaired[:100]}...")
239
- return result
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
- self.cleanup_manager = CleanupManager(self.db_manager, retention_months=6)
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
- return result
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(f"Using existing local project: {project.name} (ID: {project.id})")
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=[folder_path], # Store for reference but don't rely on it for matching
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(f"Created new local project: {project_name} (ID: {project_id})")
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(normalized_name, folder_path, db_manager)
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(project, normalized_name, folder_path, db_manager)
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(f"Created new global project: {normalized_name} (ID: {project_id})")
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, project: Project, normalized_name: str, folder_path: str, db_manager: DatabaseManager
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(f"Invalid or inaccessible project directory: {folder_path_obj}")
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
- hasattr(self.db_manager, "_health_monitor")
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
- else:
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("is_healthy", True),
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("metrics", {}).get("avg_response_time_ms", 0),
1414
- "success_rate": comprehensive_diagnostics.get("current_status", {}).get("recent_success_rate_percent", 100)
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("total_operations", 0),
1418
- "pool_size": database_stats.get("connection_pool", {}).get("current_size", 0)
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
- hasattr(self.db_manager, "_health_monitor")
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(self, project_id: str = None, project_root: Path = None) -> int:
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 (self._last_cleanup_time and
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(str(project_root))
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 = await self.cleanup_manager.perform_cleanup(
1611
- project_id=project_id
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(str(folder_path))
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 = await self.cleanup_manager.perform_cleanup(
1645
- project_id=project.id
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
- await self.db_manager.close_pool()
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"]