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.
- mcp_code_indexer/database/database.py +85 -32
- mcp_code_indexer/database/database_factory.py +1 -1
- mcp_code_indexer/database/exceptions.py +1 -1
- mcp_code_indexer/server/mcp_server.py +10 -6
- {mcp_code_indexer-4.2.16.dist-info → mcp_code_indexer-4.2.17.dist-info}/METADATA +3 -3
- {mcp_code_indexer-4.2.16.dist-info → mcp_code_indexer-4.2.17.dist-info}/RECORD +9 -9
- {mcp_code_indexer-4.2.16.dist-info → mcp_code_indexer-4.2.17.dist-info}/WHEEL +1 -1
- {mcp_code_indexer-4.2.16.dist-info → mcp_code_indexer-4.2.17.dist-info}/entry_points.txt +0 -0
- {mcp_code_indexer-4.2.16.dist-info → mcp_code_indexer-4.2.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
|
496
|
+
except aiosqlite.OperationalError as e:
|
|
494
497
|
# Record locking event for metrics
|
|
495
|
-
|
|
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
|
-
#
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
#
|
|
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
|
|
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=
|
|
745
|
-
last_accessed=
|
|
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=
|
|
768
|
-
last_accessed=
|
|
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
|
|
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
|
-
|
|
810
|
-
|
|
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
|
|
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
|
-
|
|
1037
|
-
|
|
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
|
|
1429
|
+
raise DatabaseError(
|
|
1377
1430
|
f"Index metadata not found for project: {index_meta.project_id}"
|
|
1378
1431
|
)
|
|
1379
1432
|
|
|
@@ -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 =
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
[](https://badge.fury.io/py/mcp-code-indexer)
|
|
53
|
+
[](https://pypi.org/project/mcp-code-indexer/)
|
|
54
54
|
[](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=
|
|
12
|
-
mcp_code_indexer/database/database_factory.py,sha256=
|
|
13
|
-
mcp_code_indexer/database/exceptions.py,sha256=
|
|
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=
|
|
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.
|
|
69
|
-
mcp_code_indexer-4.2.
|
|
70
|
-
mcp_code_indexer-4.2.
|
|
71
|
-
mcp_code_indexer-4.2.
|
|
72
|
-
mcp_code_indexer-4.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|