mcp-code-indexer 4.2.16__py3-none-any.whl → 4.2.17__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.
@@ -35,7 +35,10 @@ from mcp_code_indexer.database.models import (
35
35
  WordFrequencyResult,
36
36
  WordFrequencyTerm,
37
37
  )
38
- from mcp_code_indexer.database.retry_executor import create_retry_executor
38
+ from mcp_code_indexer.database.retry_executor import (
39
+ create_retry_executor,
40
+ DatabaseLockError,
41
+ )
39
42
  from mcp_code_indexer.query_preprocessor import preprocess_search_query
40
43
 
41
44
  logger = logging.getLogger(__name__)
@@ -54,7 +57,7 @@ class DatabaseManager:
54
57
  db_path: Path,
55
58
  pool_size: int = 3,
56
59
  retry_count: int = 5,
57
- timeout: float = 10.0,
60
+ timeout: float = 30.0,
58
61
  enable_wal_mode: bool = True,
59
62
  health_check_interval: float = 30.0,
60
63
  retry_min_wait: float = 0.1,
@@ -222,7 +225,7 @@ class DatabaseManager:
222
225
  "PRAGMA cache_size = -64000", # 64MB cache
223
226
  "PRAGMA temp_store = MEMORY", # Use memory for temp tables
224
227
  "PRAGMA mmap_size = 268435456", # 256MB memory mapping
225
- "PRAGMA busy_timeout = 10000", # 10s timeout (reduced from 30s)
228
+ f"PRAGMA busy_timeout = {int(self.timeout * 1000)}", # Use configured timeout
226
229
  "PRAGMA optimize", # Enable query planner optimizations
227
230
  ]
228
231
  )
@@ -490,26 +493,39 @@ class DatabaseManager:
490
493
 
491
494
  return result
492
495
 
493
- except (aiosqlite.OperationalError, asyncio.TimeoutError) as e:
496
+ except aiosqlite.OperationalError as e:
494
497
  # Record locking event for metrics
495
- if self._metrics_collector and "locked" in str(e).lower():
498
+ error_msg = str(e).lower()
499
+ if self._metrics_collector and "locked" in error_msg:
496
500
  self._metrics_collector.record_locking_event(operation_name, str(e))
497
501
 
498
- # Classify the error for better handling
499
- classified_error = classify_sqlite_error(e, operation_name)
500
-
501
- # Record failed operation metrics for non-retryable errors
502
- if not is_retryable_error(classified_error):
503
- if self._metrics_collector:
504
- self._metrics_collector.record_operation(
505
- operation_name,
506
- timeout_seconds * 1000,
507
- False,
508
- len(self._connection_pool),
509
- )
502
+ # For retryable errors (locked/busy), re-raise the ORIGINAL error
503
+ # so tenacity can retry. Only classify non-retryable errors.
504
+ if "locked" in error_msg or "busy" in error_msg:
505
+ raise # Let tenacity retry this
510
506
 
507
+ # Non-retryable OperationalError - classify and raise
508
+ classified_error = classify_sqlite_error(e, operation_name)
509
+ if self._metrics_collector:
510
+ self._metrics_collector.record_operation(
511
+ operation_name,
512
+ timeout_seconds * 1000,
513
+ False,
514
+ len(self._connection_pool),
515
+ )
511
516
  raise classified_error
512
517
 
518
+ except asyncio.TimeoutError as e:
519
+ # Timeout on BEGIN IMMEDIATE - this is retryable
520
+ if self._metrics_collector:
521
+ self._metrics_collector.record_locking_event(
522
+ operation_name, "timeout waiting for lock"
523
+ )
524
+ # Re-raise as OperationalError so tenacity can retry
525
+ raise aiosqlite.OperationalError(
526
+ f"Timeout waiting for database lock: {e}"
527
+ ) from e
528
+
513
529
  try:
514
530
  # Create a temporary retry executor with custom max_retries if different
515
531
  # from default
@@ -534,8 +550,27 @@ class DatabaseManager:
534
550
  execute_transaction, operation_name
535
551
  )
536
552
 
553
+ except DatabaseLockError as e:
554
+ # Retries exhausted - record metrics and convert to DatabaseError
555
+ if self._metrics_collector:
556
+ self._metrics_collector.record_operation(
557
+ operation_name,
558
+ timeout_seconds * 1000,
559
+ False,
560
+ len(self._connection_pool),
561
+ )
562
+ # Convert to a proper DatabaseError for consistent error handling
563
+ raise DatabaseError(
564
+ f"Database operation failed after retries: {e.message}",
565
+ error_context={
566
+ "operation": operation_name,
567
+ "retry_count": e.retry_count,
568
+ "retryable": False, # Retries already exhausted
569
+ },
570
+ ) from e
571
+
537
572
  except DatabaseError:
538
- # Record failed operation metrics for final failure
573
+ # Non-retryable DatabaseError from classification
539
574
  if self._metrics_collector:
540
575
  self._metrics_collector.record_operation(
541
576
  operation_name,
@@ -721,7 +756,7 @@ class DatabaseManager:
721
756
  cursor = await db.execute("SELECT changes()")
722
757
  changes = await cursor.fetchone()
723
758
  if changes[0] == 0:
724
- raise ValueError(f"Project not found: {project_id}")
759
+ raise DatabaseError(f"Project not found: {project_id}")
725
760
 
726
761
  await db.commit()
727
762
  logger.debug(f"Set vector_mode={enabled} for project: {project_id}")
@@ -737,12 +772,18 @@ class DatabaseManager:
737
772
  projects = []
738
773
  for row in rows:
739
774
  aliases = json.loads(row[2]) if row[2] else []
775
+ created = row[3]
776
+ last_accessed = row[4]
777
+ if isinstance(created, str):
778
+ created = datetime.fromisoformat(created)
779
+ if isinstance(last_accessed, str):
780
+ last_accessed = datetime.fromisoformat(last_accessed)
740
781
  project = Project(
741
782
  id=row[0],
742
783
  name=row[1],
743
784
  aliases=aliases,
744
- created=row[3],
745
- last_accessed=row[4],
785
+ created=created,
786
+ last_accessed=last_accessed,
746
787
  vector_mode=bool(row[5]),
747
788
  )
748
789
  projects.append(project)
@@ -760,12 +801,18 @@ class DatabaseManager:
760
801
  projects = []
761
802
  for row in rows:
762
803
  aliases = json.loads(row[2]) if row[2] else []
804
+ created = row[3]
805
+ last_accessed = row[4]
806
+ if isinstance(created, str):
807
+ created = datetime.fromisoformat(created)
808
+ if isinstance(last_accessed, str):
809
+ last_accessed = datetime.fromisoformat(last_accessed)
763
810
  project = Project(
764
811
  id=row[0],
765
812
  name=row[1],
766
813
  aliases=aliases,
767
- created=row[3],
768
- last_accessed=row[4],
814
+ created=created,
815
+ last_accessed=last_accessed,
769
816
  vector_mode=bool(row[5]),
770
817
  )
771
818
  projects.append(project)
@@ -776,9 +823,7 @@ class DatabaseManager:
776
823
 
777
824
  async def create_file_description(self, file_desc: FileDescription) -> None:
778
825
  """Create or update a file description."""
779
- async with self.get_write_connection_with_retry(
780
- "create_file_description"
781
- ) as db:
826
+ async def operation(db: aiosqlite.Connection) -> None:
782
827
  await db.execute(
783
828
  """
784
829
  INSERT INTO file_descriptions
@@ -806,8 +851,12 @@ class DatabaseManager:
806
851
  file_desc.to_be_cleaned,
807
852
  ),
808
853
  )
809
- await db.commit()
810
- logger.debug(f"Saved file description: {file_desc.file_path}")
854
+
855
+ await self.execute_transaction_with_retry(
856
+ operation,
857
+ "create_file_description"
858
+ )
859
+ logger.debug(f"Saved file description: {file_desc.file_path}")
811
860
 
812
861
  async def get_file_description(
813
862
  self, project_id: str, file_path: str
@@ -1018,7 +1067,7 @@ class DatabaseManager:
1018
1067
 
1019
1068
  async def create_project_overview(self, overview: ProjectOverview) -> None:
1020
1069
  """Create or update a project overview."""
1021
- async with self.get_write_connection() as db:
1070
+ async def operation(db: aiosqlite.Connection) -> None:
1022
1071
  await db.execute(
1023
1072
  """
1024
1073
  INSERT OR REPLACE INTO project_overviews
@@ -1033,8 +1082,12 @@ class DatabaseManager:
1033
1082
  overview.total_tokens,
1034
1083
  ),
1035
1084
  )
1036
- await db.commit()
1037
- logger.debug(f"Created/updated overview for project {overview.project_id}")
1085
+
1086
+ await self.execute_transaction_with_retry(
1087
+ operation,
1088
+ "create_project_overview"
1089
+ )
1090
+ logger.debug(f"Created/updated overview for project {overview.project_id}")
1038
1091
 
1039
1092
  async def get_project_overview(self, project_id: str) -> Optional[ProjectOverview]:
1040
1093
  """Get project overview by ID."""
@@ -1373,7 +1426,7 @@ class DatabaseManager:
1373
1426
  cursor = await db.execute("SELECT changes()")
1374
1427
  changes = await cursor.fetchone()
1375
1428
  if changes[0] == 0:
1376
- raise ValueError(
1429
+ raise DatabaseError(
1377
1430
  f"Index metadata not found for project: {index_meta.project_id}"
1378
1431
  )
1379
1432
 
@@ -28,7 +28,7 @@ class DatabaseFactory:
28
28
  global_db_path: Path,
29
29
  pool_size: int = 3,
30
30
  retry_count: int = 5,
31
- timeout: float = 10.0,
31
+ timeout: float = 30.0,
32
32
  enable_wal_mode: bool = True,
33
33
  health_check_interval: float = 30.0,
34
34
  retry_min_wait: float = 0.1,
@@ -236,7 +236,7 @@ def classify_sqlite_error(error: Exception, operation_name: str = "") -> Databas
236
236
  for msg in [
237
237
  "no such table",
238
238
  "no such column",
239
- "table already exists",
239
+ "already exists",
240
240
  "syntax error",
241
241
  ]
242
242
  ):
@@ -13,7 +13,7 @@ import random
13
13
  import re
14
14
  import time
15
15
  import uuid
16
- from datetime import datetime
16
+ from datetime import datetime, timedelta
17
17
  from pathlib import Path
18
18
  from typing import Any, Dict, List, Optional, Callable, cast
19
19
 
@@ -56,7 +56,7 @@ class MCPCodeIndexServer:
56
56
  cache_dir: Optional[Path] = None,
57
57
  db_pool_size: int = 3,
58
58
  db_retry_count: int = 5,
59
- db_timeout: float = 10.0,
59
+ db_timeout: float = 30.0,
60
60
  enable_wal_mode: bool = True,
61
61
  health_check_interval: float = 30.0,
62
62
  retry_min_wait: float = 0.1,
@@ -882,8 +882,11 @@ class MCPCodeIndexServer:
882
882
  all_projects = await db_manager.get_all_projects()
883
883
  if all_projects:
884
884
  project = all_projects[0] # Use the first (and should be only) project
885
- # Update last accessed time
886
- await db_manager.update_project_access_time(project.id)
885
+
886
+ # Update last accessed time only if older than 5 minutes
887
+ if datetime.utcnow() - project.last_accessed > timedelta(minutes=5):
888
+ await db_manager.update_project_access_time(project.id)
889
+
887
890
  logger.info(
888
891
  f"Using existing local project: {project.name} (ID: {project.id})"
889
892
  )
@@ -1040,8 +1043,9 @@ class MCPCodeIndexServer:
1040
1043
  db_manager: DatabaseManager,
1041
1044
  ) -> None:
1042
1045
  """Update an existing project with new metadata and folder alias."""
1043
- # Update last accessed time
1044
- await db_manager.update_project_access_time(project.id)
1046
+ # Update last accessed time only if older than 5 minutes
1047
+ if datetime.utcnow() - project.last_accessed > timedelta(minutes=5):
1048
+ await db_manager.update_project_access_time(project.id)
1045
1049
 
1046
1050
  should_update = False
1047
1051
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-code-indexer
3
- Version: 4.2.16
3
+ Version: 4.2.17
4
4
  Summary: MCP server that tracks file descriptions across codebases, enabling AI agents to efficiently navigate and understand code through searchable summaries and token-aware overviews.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -49,8 +49,8 @@ Description-Content-Type: text/markdown
49
49
 
50
50
  # MCP Code Indexer 🚀
51
51
 
52
- [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?63)](https://badge.fury.io/py/mcp-code-indexer)
53
- [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?63)](https://pypi.org/project/mcp-code-indexer/)
52
+ [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?64)](https://badge.fury.io/py/mcp-code-indexer)
53
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?64)](https://pypi.org/project/mcp-code-indexer/)
54
54
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
55
55
 
56
56
  A production-ready **Model Context Protocol (MCP) server** that revolutionizes how AI agents navigate and understand codebases. Built for high-concurrency environments with advanced database resilience, the server provides instant access to intelligent descriptions, semantic search, and context-aware recommendations while maintaining 800+ writes/sec throughput.
@@ -8,9 +8,9 @@ mcp_code_indexer/commands/makelocal.py,sha256=T_44so96jcs1FNlft9E3nAq0LlOzQLhjLd
8
8
  mcp_code_indexer/data/stop_words_english.txt,sha256=feRGP8WG5hQPo-wZN5ralJiSv1CGw4h3010NBJnJ0Z8,6344
9
9
  mcp_code_indexer/database/__init__.py,sha256=aPq_aaRp0aSwOBIq9GkuMNjmLxA411zg2vhdrAuHm-w,38
10
10
  mcp_code_indexer/database/connection_health.py,sha256=jZr3tCbfjUJujdXe_uxtm1N4c31dMV4euiSY4ulamOE,25497
11
- mcp_code_indexer/database/database.py,sha256=8sLr7fQ4whzOMnAYaKxO2Q7t27Dv_0gyixgBeI6aLXI,55705
12
- mcp_code_indexer/database/database_factory.py,sha256=zm942m72mqCYTGh1GFyVw-hBsbZcZnx3znJ2ZQPwISM,4316
13
- mcp_code_indexer/database/exceptions.py,sha256=bamoC-ssw_TMRA5-6lzX6d_1DlcXXrcmiCMBdUEQ9dI,10479
11
+ mcp_code_indexer/database/database.py,sha256=GcXI6p99E4_fo9oMbbZXHIMpYm4Z7y2TdQJgSnZNvMI,57986
12
+ mcp_code_indexer/database/database_factory.py,sha256=VMw0tlutGgZoTI7Q_PCuFy5zAimq2xuMtDFAlF_FKtc,4316
13
+ mcp_code_indexer/database/exceptions.py,sha256=zciu7fDwF8y0Z4tkzTBFPPvXCvdnEJiA-Wd-cBLZBWw,10473
14
14
  mcp_code_indexer/database/models.py,sha256=w1U9zMGNt0LQeCiifYeXKW_Cia9BKV5uPChbOve-FZY,13467
15
15
  mcp_code_indexer/database/path_resolver.py,sha256=1Ubx6Ly5F2dnvhbdN3tqyowBHslABXpoA6wgL4BQYGo,3461
16
16
  mcp_code_indexer/database/retry_executor.py,sha256=r7eKn_xDc6hKz9qs9z9Dg8gyq4uZgnyrgFmQFTyDhdo,14409
@@ -33,7 +33,7 @@ mcp_code_indexer/migrations/005_remove_git_remotes.sql,sha256=vT84AaV1hyN4zq5W67
33
33
  mcp_code_indexer/migrations/006_vector_mode.sql,sha256=kN-UBPGoagqtpxpGEjdz-V3hevPAXxAdNmxF4iIPsY8,7448
34
34
  mcp_code_indexer/query_preprocessor.py,sha256=vi23sK2ffs4T5PGY7lHrbCBDL421AlPz2dldqX_3JKA,5491
35
35
  mcp_code_indexer/server/__init__.py,sha256=16xMcuriUOBlawRqWNBk6niwrvtv_JD5xvI36X1Vsmk,41
36
- mcp_code_indexer/server/mcp_server.py,sha256=cOFJETze1CzpTq2EieGGKD8Zl3MIRbd3Nepip_L1mfM,84183
36
+ mcp_code_indexer/server/mcp_server.py,sha256=nkm7R_lOnD_XDxk1mEiZgrT6D-G-QIt0g34VHNb1p5g,84424
37
37
  mcp_code_indexer/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4,sha256=Ijkht27pm96ZW3_3OFE-7xAPtR0YyTWXoRO8_-hlsqc,1681126
38
38
  mcp_code_indexer/token_counter.py,sha256=e6WsyCEWMMSkMwLbcVtr5e8vEqh-kFqNmiJErCNdqHE,8220
39
39
  mcp_code_indexer/tools/__init__.py,sha256=m01mxML2UdD7y5rih_XNhNSCMzQTz7WQ_T1TeOcYlnE,49
@@ -65,8 +65,8 @@ mcp_code_indexer/vector_mode/services/vector_mode_tools_service.py,sha256=K1_STy
65
65
  mcp_code_indexer/vector_mode/services/vector_storage_service.py,sha256=JI3VUc2mG8xZ_YqOvfKJivuMi4imeBLr2UFVrWgDWhk,21193
66
66
  mcp_code_indexer/vector_mode/types.py,sha256=M4lUF43FzjiVUezoRqozx_u0g1-xrX9qcRcRn-u65yw,1222
67
67
  mcp_code_indexer/vector_mode/utils.py,sha256=XtHrpOw0QJ0EjdzJ85jrbkmHy8Slkq_t7hz-q4RP-10,1283
68
- mcp_code_indexer-4.2.16.dist-info/METADATA,sha256=lZtjche-WN7CsUsb2eR22tZ0MvYOoxt3LF7IEpQR3zw,27689
69
- mcp_code_indexer-4.2.16.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
70
- mcp_code_indexer-4.2.16.dist-info/entry_points.txt,sha256=UABj7HZ0mC6rvF22gxaz2LLNLGQShTrFmp5u00iUtvo,67
71
- mcp_code_indexer-4.2.16.dist-info/licenses/LICENSE,sha256=JN9dyPPgYwH9C-UjYM7FLNZjQ6BF7kAzpF3_4PwY4rY,1086
72
- mcp_code_indexer-4.2.16.dist-info/RECORD,,
68
+ mcp_code_indexer-4.2.17.dist-info/METADATA,sha256=hlxYys7f-3mK2L00ZbP7UHyVxBa5AQO1xDw8t5HZ3jo,27689
69
+ mcp_code_indexer-4.2.17.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
70
+ mcp_code_indexer-4.2.17.dist-info/entry_points.txt,sha256=UABj7HZ0mC6rvF22gxaz2LLNLGQShTrFmp5u00iUtvo,67
71
+ mcp_code_indexer-4.2.17.dist-info/licenses/LICENSE,sha256=JN9dyPPgYwH9C-UjYM7FLNZjQ6BF7kAzpF3_4PwY4rY,1086
72
+ mcp_code_indexer-4.2.17.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.3.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any