mcp-vector-search 0.15.7__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.

Potentially problematic release.


This version of mcp-vector-search might be problematic. Click here for more details.

Files changed (86) hide show
  1. mcp_vector_search/__init__.py +10 -0
  2. mcp_vector_search/cli/__init__.py +1 -0
  3. mcp_vector_search/cli/commands/__init__.py +1 -0
  4. mcp_vector_search/cli/commands/auto_index.py +397 -0
  5. mcp_vector_search/cli/commands/chat.py +534 -0
  6. mcp_vector_search/cli/commands/config.py +393 -0
  7. mcp_vector_search/cli/commands/demo.py +358 -0
  8. mcp_vector_search/cli/commands/index.py +762 -0
  9. mcp_vector_search/cli/commands/init.py +658 -0
  10. mcp_vector_search/cli/commands/install.py +869 -0
  11. mcp_vector_search/cli/commands/install_old.py +700 -0
  12. mcp_vector_search/cli/commands/mcp.py +1254 -0
  13. mcp_vector_search/cli/commands/reset.py +393 -0
  14. mcp_vector_search/cli/commands/search.py +796 -0
  15. mcp_vector_search/cli/commands/setup.py +1133 -0
  16. mcp_vector_search/cli/commands/status.py +584 -0
  17. mcp_vector_search/cli/commands/uninstall.py +404 -0
  18. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  19. mcp_vector_search/cli/commands/visualize/cli.py +265 -0
  20. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  21. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  22. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
  23. mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
  24. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  25. mcp_vector_search/cli/commands/visualize/server.py +201 -0
  26. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  27. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  28. mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
  29. mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
  30. mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
  31. mcp_vector_search/cli/commands/visualize.py.original +2536 -0
  32. mcp_vector_search/cli/commands/watch.py +287 -0
  33. mcp_vector_search/cli/didyoumean.py +520 -0
  34. mcp_vector_search/cli/export.py +320 -0
  35. mcp_vector_search/cli/history.py +295 -0
  36. mcp_vector_search/cli/interactive.py +342 -0
  37. mcp_vector_search/cli/main.py +484 -0
  38. mcp_vector_search/cli/output.py +414 -0
  39. mcp_vector_search/cli/suggestions.py +375 -0
  40. mcp_vector_search/config/__init__.py +1 -0
  41. mcp_vector_search/config/constants.py +24 -0
  42. mcp_vector_search/config/defaults.py +200 -0
  43. mcp_vector_search/config/settings.py +146 -0
  44. mcp_vector_search/core/__init__.py +1 -0
  45. mcp_vector_search/core/auto_indexer.py +298 -0
  46. mcp_vector_search/core/config_utils.py +394 -0
  47. mcp_vector_search/core/connection_pool.py +360 -0
  48. mcp_vector_search/core/database.py +1237 -0
  49. mcp_vector_search/core/directory_index.py +318 -0
  50. mcp_vector_search/core/embeddings.py +294 -0
  51. mcp_vector_search/core/exceptions.py +89 -0
  52. mcp_vector_search/core/factory.py +318 -0
  53. mcp_vector_search/core/git_hooks.py +345 -0
  54. mcp_vector_search/core/indexer.py +1002 -0
  55. mcp_vector_search/core/llm_client.py +453 -0
  56. mcp_vector_search/core/models.py +294 -0
  57. mcp_vector_search/core/project.py +350 -0
  58. mcp_vector_search/core/scheduler.py +330 -0
  59. mcp_vector_search/core/search.py +952 -0
  60. mcp_vector_search/core/watcher.py +322 -0
  61. mcp_vector_search/mcp/__init__.py +5 -0
  62. mcp_vector_search/mcp/__main__.py +25 -0
  63. mcp_vector_search/mcp/server.py +752 -0
  64. mcp_vector_search/parsers/__init__.py +8 -0
  65. mcp_vector_search/parsers/base.py +296 -0
  66. mcp_vector_search/parsers/dart.py +605 -0
  67. mcp_vector_search/parsers/html.py +413 -0
  68. mcp_vector_search/parsers/javascript.py +643 -0
  69. mcp_vector_search/parsers/php.py +694 -0
  70. mcp_vector_search/parsers/python.py +502 -0
  71. mcp_vector_search/parsers/registry.py +223 -0
  72. mcp_vector_search/parsers/ruby.py +678 -0
  73. mcp_vector_search/parsers/text.py +186 -0
  74. mcp_vector_search/parsers/utils.py +265 -0
  75. mcp_vector_search/py.typed +1 -0
  76. mcp_vector_search/utils/__init__.py +42 -0
  77. mcp_vector_search/utils/gitignore.py +250 -0
  78. mcp_vector_search/utils/gitignore_updater.py +212 -0
  79. mcp_vector_search/utils/monorepo.py +339 -0
  80. mcp_vector_search/utils/timing.py +338 -0
  81. mcp_vector_search/utils/version.py +47 -0
  82. mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
  83. mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
  84. mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
  85. mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
  86. mcp_vector_search-0.15.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,360 @@
1
+ """Connection pooling for vector database operations."""
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import AsyncGenerator
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from loguru import logger
12
+
13
+ from .exceptions import DatabaseError
14
+
15
+
16
+ @dataclass
17
+ class PooledConnection:
18
+ """Represents a pooled database connection."""
19
+
20
+ client: Any
21
+ collection: Any
22
+ created_at: float
23
+ last_used: float
24
+ in_use: bool = False
25
+ use_count: int = 0
26
+
27
+ @property
28
+ def age(self) -> float:
29
+ """Get the age of this connection in seconds."""
30
+ return time.time() - self.created_at
31
+
32
+ @property
33
+ def idle_time(self) -> float:
34
+ """Get the idle time of this connection in seconds."""
35
+ return time.time() - self.last_used
36
+
37
+
38
+ class ChromaConnectionPool:
39
+ """Connection pool for ChromaDB operations."""
40
+
41
+ def __init__(
42
+ self,
43
+ persist_directory: Path,
44
+ embedding_function: Any,
45
+ collection_name: str = "code_search",
46
+ max_connections: int = 10,
47
+ min_connections: int = 2,
48
+ max_idle_time: float = 300.0, # 5 minutes
49
+ max_connection_age: float = 3600.0, # 1 hour
50
+ ):
51
+ """Initialize connection pool.
52
+
53
+ Args:
54
+ persist_directory: Directory to persist database
55
+ embedding_function: Function to generate embeddings
56
+ collection_name: Name of the collection
57
+ max_connections: Maximum number of connections in pool
58
+ min_connections: Minimum number of connections to maintain
59
+ max_idle_time: Maximum time a connection can be idle (seconds)
60
+ max_connection_age: Maximum age of a connection (seconds)
61
+ """
62
+ self.persist_directory = persist_directory
63
+ self.embedding_function = embedding_function
64
+ self.collection_name = collection_name
65
+ self.max_connections = max_connections
66
+ self.min_connections = min_connections
67
+ self.max_idle_time = max_idle_time
68
+ self.max_connection_age = max_connection_age
69
+
70
+ self._pool: list[PooledConnection] = []
71
+ self._lock = asyncio.Lock()
72
+ self._initialized = False
73
+ self._cleanup_task: asyncio.Task | None = None
74
+
75
+ # Statistics
76
+ self._stats = {
77
+ "connections_created": 0,
78
+ "connections_reused": 0,
79
+ "connections_expired": 0,
80
+ "pool_hits": 0,
81
+ "pool_misses": 0,
82
+ }
83
+
84
+ async def initialize(self) -> None:
85
+ """Initialize the connection pool."""
86
+ if self._initialized:
87
+ return
88
+
89
+ async with self._lock:
90
+ if self._initialized:
91
+ return
92
+
93
+ # Create minimum number of connections
94
+ for _ in range(self.min_connections):
95
+ conn = await self._create_connection()
96
+ self._pool.append(conn)
97
+
98
+ # Start cleanup task
99
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
100
+ self._initialized = True
101
+
102
+ logger.info(
103
+ f"Connection pool initialized with {len(self._pool)} connections"
104
+ )
105
+
106
+ async def close(self) -> None:
107
+ """Close all connections and cleanup."""
108
+ if not self._initialized:
109
+ return
110
+
111
+ async with self._lock:
112
+ # Cancel cleanup task
113
+ if self._cleanup_task:
114
+ self._cleanup_task.cancel()
115
+ try:
116
+ await self._cleanup_task
117
+ except asyncio.CancelledError:
118
+ pass
119
+
120
+ # Close all connections
121
+ for conn in self._pool:
122
+ await self._close_connection(conn)
123
+
124
+ self._pool.clear()
125
+ self._initialized = False
126
+
127
+ logger.info("Connection pool closed")
128
+
129
+ async def _create_connection(self) -> PooledConnection:
130
+ """Create a new database connection."""
131
+ try:
132
+ import chromadb
133
+
134
+ # Ensure directory exists
135
+ self.persist_directory.mkdir(parents=True, exist_ok=True)
136
+
137
+ # Create client
138
+ client = chromadb.PersistentClient(
139
+ path=str(self.persist_directory),
140
+ settings=chromadb.Settings(
141
+ anonymized_telemetry=False,
142
+ allow_reset=True,
143
+ ),
144
+ )
145
+
146
+ # Create or get collection
147
+ collection = client.get_or_create_collection(
148
+ name=self.collection_name,
149
+ embedding_function=self.embedding_function,
150
+ metadata={
151
+ "description": "Semantic code search collection",
152
+ },
153
+ )
154
+
155
+ conn = PooledConnection(
156
+ client=client,
157
+ collection=collection,
158
+ created_at=time.time(),
159
+ last_used=time.time(),
160
+ )
161
+
162
+ self._stats["connections_created"] += 1
163
+ logger.debug(
164
+ f"Created new database connection (total: {self._stats['connections_created']})"
165
+ )
166
+
167
+ return conn
168
+
169
+ except Exception as e:
170
+ logger.error(f"Failed to create database connection: {e}")
171
+ raise DatabaseError(f"Connection creation failed: {e}") from e
172
+
173
+ async def _close_connection(self, conn: PooledConnection) -> None:
174
+ """Close a database connection."""
175
+ try:
176
+ # ChromaDB doesn't require explicit closing
177
+ conn.client = None
178
+ conn.collection = None
179
+ logger.debug("Closed database connection")
180
+ except Exception as e:
181
+ logger.warning(f"Error closing connection: {e}")
182
+
183
+ @asynccontextmanager
184
+ async def get_connection(self) -> AsyncGenerator[PooledConnection, None]:
185
+ """Get a connection from the pool."""
186
+ if not self._initialized:
187
+ await self.initialize()
188
+
189
+ conn = None
190
+ try:
191
+ # Get connection from pool
192
+ conn = await self._acquire_connection()
193
+ yield conn
194
+ finally:
195
+ # Return connection to pool
196
+ if conn:
197
+ await self._release_connection(conn)
198
+
199
+ async def _acquire_connection(self) -> PooledConnection:
200
+ """Acquire a connection from the pool."""
201
+ async with self._lock:
202
+ # Try to find an available connection
203
+ for conn in self._pool:
204
+ if not conn.in_use and self._is_connection_valid(conn):
205
+ conn.in_use = True
206
+ conn.last_used = time.time()
207
+ conn.use_count += 1
208
+ self._stats["pool_hits"] += 1
209
+ self._stats["connections_reused"] += 1
210
+ logger.debug(f"Reused connection (use count: {conn.use_count})")
211
+ return conn
212
+
213
+ # No available connection, create new one if under limit
214
+ if len(self._pool) < self.max_connections:
215
+ conn = await self._create_connection()
216
+ conn.in_use = True
217
+ self._pool.append(conn)
218
+ self._stats["pool_misses"] += 1
219
+ logger.debug(f"Created new connection (pool size: {len(self._pool)})")
220
+ return conn
221
+
222
+ # Pool is full, wait for a connection to become available (outside lock)
223
+ self._stats["pool_misses"] += 1
224
+ logger.warning("Connection pool exhausted, waiting for available connection")
225
+
226
+ # Wait for a connection (with timeout) - release lock during wait
227
+ timeout = 30.0 # 30 seconds
228
+ start_time = time.time()
229
+
230
+ while time.time() - start_time < timeout:
231
+ await asyncio.sleep(0.1)
232
+ # Re-acquire lock to check for available connections
233
+ async with self._lock:
234
+ for conn in self._pool:
235
+ if not conn.in_use and self._is_connection_valid(conn):
236
+ conn.in_use = True
237
+ conn.last_used = time.time()
238
+ conn.use_count += 1
239
+ self._stats["connections_reused"] += 1
240
+ return conn
241
+
242
+ raise DatabaseError("Connection pool timeout: no connections available")
243
+
244
+ async def _release_connection(self, conn: PooledConnection) -> None:
245
+ """Release a connection back to the pool."""
246
+ async with self._lock:
247
+ conn.in_use = False
248
+ conn.last_used = time.time()
249
+ logger.debug(f"Released connection (use count: {conn.use_count})")
250
+
251
+ def _is_connection_valid(self, conn: PooledConnection) -> bool:
252
+ """Check if a connection is still valid."""
253
+ now = time.time()
254
+
255
+ # Check age
256
+ if now - conn.created_at > self.max_connection_age:
257
+ return False
258
+
259
+ # Check if idle too long
260
+ if now - conn.last_used > self.max_idle_time:
261
+ return False
262
+
263
+ # Check if client/collection are still valid
264
+ if not conn.client or not conn.collection:
265
+ return False
266
+
267
+ return True
268
+
269
+ async def _cleanup_loop(self) -> None:
270
+ """Background task to cleanup expired connections."""
271
+ while True:
272
+ try:
273
+ await asyncio.sleep(60) # Check every minute
274
+ await self._cleanup_expired_connections()
275
+ except asyncio.CancelledError:
276
+ break
277
+ except Exception as e:
278
+ logger.error(f"Error in connection cleanup: {e}")
279
+
280
+ async def _cleanup_expired_connections(self) -> None:
281
+ """Remove expired connections from the pool."""
282
+ async with self._lock:
283
+ expired_connections = []
284
+
285
+ for conn in self._pool:
286
+ if not conn.in_use and not self._is_connection_valid(conn):
287
+ expired_connections.append(conn)
288
+
289
+ # Remove expired connections
290
+ for conn in expired_connections:
291
+ self._pool.remove(conn)
292
+ await self._close_connection(conn)
293
+ self._stats["connections_expired"] += 1
294
+
295
+ if expired_connections:
296
+ logger.debug(
297
+ f"Cleaned up {len(expired_connections)} expired connections"
298
+ )
299
+
300
+ # Ensure minimum connections
301
+ while len(self._pool) < self.min_connections:
302
+ try:
303
+ conn = await self._create_connection()
304
+ self._pool.append(conn)
305
+ except Exception as e:
306
+ logger.error(f"Failed to create minimum connection: {e}")
307
+ break
308
+
309
+ def get_stats(self) -> dict[str, Any]:
310
+ """Get connection pool statistics."""
311
+ active_connections = sum(1 for conn in self._pool if conn.in_use)
312
+ idle_connections = len(self._pool) - active_connections
313
+
314
+ return {
315
+ **self._stats,
316
+ "pool_size": len(self._pool),
317
+ "active_connections": active_connections,
318
+ "idle_connections": idle_connections,
319
+ "max_connections": self.max_connections,
320
+ "min_connections": self.min_connections,
321
+ }
322
+
323
+ async def health_check(self) -> bool:
324
+ """Perform a health check on the connection pool."""
325
+ try:
326
+ async with self.get_connection() as conn:
327
+ # Try a simple operation
328
+ conn.collection.count()
329
+ return True
330
+ except Exception as e:
331
+ logger.error(f"Connection pool health check failed: {e}")
332
+ return False
333
+
334
+ # Backward compatibility aliases for old test API
335
+ async def cleanup(self) -> None:
336
+ """Alias for close() method (backward compatibility)."""
337
+ await self.close()
338
+
339
+ def _validate_connection(self, conn: PooledConnection) -> bool:
340
+ """Alias for _is_connection_valid() method (backward compatibility)."""
341
+ return self._is_connection_valid(conn)
342
+
343
+ async def _cleanup_idle_connections(self) -> None:
344
+ """Alias for _cleanup_expired_connections() method (backward compatibility)."""
345
+ await self._cleanup_expired_connections()
346
+
347
+ @property
348
+ def _connections(self) -> list[PooledConnection]:
349
+ """Alias for _pool attribute (backward compatibility)."""
350
+ return self._pool
351
+
352
+ @property
353
+ def _max_connections(self) -> int:
354
+ """Alias for max_connections attribute (backward compatibility)."""
355
+ return self.max_connections
356
+
357
+ @property
358
+ def _min_connections(self) -> int:
359
+ """Alias for min_connections attribute (backward compatibility)."""
360
+ return self.min_connections