mcp-vector-search 0.0.3__py3-none-any.whl → 0.4.12__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 (49) hide show
  1. mcp_vector_search/__init__.py +3 -2
  2. mcp_vector_search/cli/commands/auto_index.py +397 -0
  3. mcp_vector_search/cli/commands/config.py +88 -40
  4. mcp_vector_search/cli/commands/index.py +198 -52
  5. mcp_vector_search/cli/commands/init.py +471 -58
  6. mcp_vector_search/cli/commands/install.py +284 -0
  7. mcp_vector_search/cli/commands/mcp.py +495 -0
  8. mcp_vector_search/cli/commands/search.py +241 -87
  9. mcp_vector_search/cli/commands/status.py +184 -58
  10. mcp_vector_search/cli/commands/watch.py +34 -35
  11. mcp_vector_search/cli/didyoumean.py +184 -0
  12. mcp_vector_search/cli/export.py +320 -0
  13. mcp_vector_search/cli/history.py +292 -0
  14. mcp_vector_search/cli/interactive.py +342 -0
  15. mcp_vector_search/cli/main.py +175 -27
  16. mcp_vector_search/cli/output.py +63 -45
  17. mcp_vector_search/config/defaults.py +50 -36
  18. mcp_vector_search/config/settings.py +49 -35
  19. mcp_vector_search/core/auto_indexer.py +298 -0
  20. mcp_vector_search/core/connection_pool.py +322 -0
  21. mcp_vector_search/core/database.py +335 -25
  22. mcp_vector_search/core/embeddings.py +73 -29
  23. mcp_vector_search/core/exceptions.py +19 -2
  24. mcp_vector_search/core/factory.py +310 -0
  25. mcp_vector_search/core/git_hooks.py +345 -0
  26. mcp_vector_search/core/indexer.py +237 -73
  27. mcp_vector_search/core/models.py +21 -19
  28. mcp_vector_search/core/project.py +73 -58
  29. mcp_vector_search/core/scheduler.py +330 -0
  30. mcp_vector_search/core/search.py +574 -86
  31. mcp_vector_search/core/watcher.py +48 -46
  32. mcp_vector_search/mcp/__init__.py +4 -0
  33. mcp_vector_search/mcp/__main__.py +25 -0
  34. mcp_vector_search/mcp/server.py +701 -0
  35. mcp_vector_search/parsers/base.py +30 -31
  36. mcp_vector_search/parsers/javascript.py +74 -48
  37. mcp_vector_search/parsers/python.py +57 -49
  38. mcp_vector_search/parsers/registry.py +47 -32
  39. mcp_vector_search/parsers/text.py +179 -0
  40. mcp_vector_search/utils/__init__.py +40 -0
  41. mcp_vector_search/utils/gitignore.py +229 -0
  42. mcp_vector_search/utils/timing.py +334 -0
  43. mcp_vector_search/utils/version.py +47 -0
  44. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/METADATA +173 -7
  45. mcp_vector_search-0.4.12.dist-info/RECORD +54 -0
  46. mcp_vector_search-0.0.3.dist-info/RECORD +0 -35
  47. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/WHEEL +0 -0
  48. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/entry_points.txt +0 -0
  49. {mcp_vector_search-0.0.3.dist-info → mcp_vector_search-0.4.12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,701 @@
1
+ """MCP server implementation for MCP Vector Search."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from loguru import logger
11
+ from mcp.server import Server
12
+ from mcp.server.stdio import stdio_server
13
+ from mcp.types import (
14
+ CallToolRequest,
15
+ CallToolResult,
16
+ ListToolsRequest,
17
+ ListToolsResult,
18
+ ServerCapabilities,
19
+ TextContent,
20
+ Tool,
21
+ )
22
+ from mcp.server.models import InitializationOptions
23
+
24
+ from ..core.database import ChromaVectorDatabase
25
+ from ..core.embeddings import create_embedding_function
26
+ from ..core.exceptions import ProjectNotFoundError
27
+ from ..core.indexer import SemanticIndexer
28
+ from ..core.project import ProjectManager
29
+ from ..core.search import SemanticSearchEngine
30
+ from ..core.watcher import FileWatcher
31
+
32
+
33
+ class MCPVectorSearchServer:
34
+ """MCP server for vector search functionality."""
35
+
36
+ def __init__(self, project_root: Optional[Path] = None, enable_file_watching: Optional[bool] = None):
37
+ """Initialize the MCP server.
38
+
39
+ Args:
40
+ project_root: Project root directory. If None, will auto-detect.
41
+ enable_file_watching: Enable file watching for automatic reindexing.
42
+ If None, checks MCP_ENABLE_FILE_WATCHING env var (default: True).
43
+ """
44
+ self.project_root = project_root or Path.cwd()
45
+ self.project_manager = ProjectManager(self.project_root)
46
+ self.search_engine: Optional[SemanticSearchEngine] = None
47
+ self.file_watcher: Optional[FileWatcher] = None
48
+ self.indexer: Optional[SemanticIndexer] = None
49
+ self.database: Optional[ChromaVectorDatabase] = None
50
+ self._initialized = False
51
+
52
+ # Determine if file watching should be enabled
53
+ if enable_file_watching is None:
54
+ # Check environment variable, default to True
55
+ env_value = os.getenv("MCP_ENABLE_FILE_WATCHING", "true").lower()
56
+ self.enable_file_watching = env_value in ("true", "1", "yes", "on")
57
+ else:
58
+ self.enable_file_watching = enable_file_watching
59
+
60
+ async def initialize(self) -> None:
61
+ """Initialize the search engine and database."""
62
+ if self._initialized:
63
+ return
64
+
65
+ try:
66
+ # Load project configuration
67
+ config = self.project_manager.load_config()
68
+
69
+ # Setup embedding function
70
+ embedding_function, _ = create_embedding_function(
71
+ model_name=config.embedding_model
72
+ )
73
+
74
+ # Setup database
75
+ self.database = ChromaVectorDatabase(
76
+ persist_directory=config.index_path,
77
+ embedding_function=embedding_function,
78
+ )
79
+
80
+ # Initialize database
81
+ await self.database.__aenter__()
82
+
83
+ # Setup search engine
84
+ self.search_engine = SemanticSearchEngine(
85
+ database=self.database,
86
+ project_root=self.project_root
87
+ )
88
+
89
+ # Setup indexer for file watching
90
+ if self.enable_file_watching:
91
+ self.indexer = SemanticIndexer(
92
+ database=self.database,
93
+ project_root=self.project_root,
94
+ file_extensions=config.file_extensions
95
+ )
96
+
97
+ # Setup file watcher
98
+ self.file_watcher = FileWatcher(
99
+ project_root=self.project_root,
100
+ config=config,
101
+ indexer=self.indexer,
102
+ database=self.database
103
+ )
104
+
105
+ # Start file watching
106
+ await self.file_watcher.start()
107
+ logger.info(f"File watching enabled for automatic reindexing")
108
+ else:
109
+ logger.info(f"File watching disabled")
110
+
111
+ self._initialized = True
112
+ logger.info(f"MCP server initialized for project: {self.project_root}")
113
+
114
+ except ProjectNotFoundError:
115
+ logger.error(f"Project not initialized at {self.project_root}")
116
+ raise
117
+ except Exception as e:
118
+ logger.error(f"Failed to initialize MCP server: {e}")
119
+ raise
120
+
121
+ async def cleanup(self) -> None:
122
+ """Cleanup resources."""
123
+ # Stop file watcher if running
124
+ if self.file_watcher and self.file_watcher.is_running:
125
+ logger.info("Stopping file watcher...")
126
+ await self.file_watcher.stop()
127
+ self.file_watcher = None
128
+
129
+ # Cleanup database connection
130
+ if self.database and hasattr(self.database, '__aexit__'):
131
+ await self.database.__aexit__(None, None, None)
132
+ self.database = None
133
+
134
+ # Clear references
135
+ self.search_engine = None
136
+ self.indexer = None
137
+ self._initialized = False
138
+ logger.info("MCP server cleanup completed")
139
+
140
+ def get_tools(self) -> List[Tool]:
141
+ """Get available MCP tools."""
142
+ tools = [
143
+ Tool(
144
+ name="search_code",
145
+ description="Search for code using semantic similarity",
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {
149
+ "query": {
150
+ "type": "string",
151
+ "description": "The search query to find relevant code"
152
+ },
153
+ "limit": {
154
+ "type": "integer",
155
+ "description": "Maximum number of results to return",
156
+ "default": 10,
157
+ "minimum": 1,
158
+ "maximum": 50
159
+ },
160
+ "similarity_threshold": {
161
+ "type": "number",
162
+ "description": "Minimum similarity threshold (0.0-1.0)",
163
+ "default": 0.3,
164
+ "minimum": 0.0,
165
+ "maximum": 1.0
166
+ },
167
+ "file_extensions": {
168
+ "type": "array",
169
+ "items": {"type": "string"},
170
+ "description": "Filter by file extensions (e.g., ['.py', '.js'])"
171
+ },
172
+ "language": {
173
+ "type": "string",
174
+ "description": "Filter by programming language"
175
+ },
176
+ "function_name": {
177
+ "type": "string",
178
+ "description": "Filter by function name"
179
+ },
180
+ "class_name": {
181
+ "type": "string",
182
+ "description": "Filter by class name"
183
+ },
184
+ "files": {
185
+ "type": "string",
186
+ "description": "Filter by file patterns (e.g., '*.py' or 'src/*.js')"
187
+ }
188
+ },
189
+ "required": ["query"]
190
+ }
191
+ ),
192
+ Tool(
193
+ name="search_similar",
194
+ description="Find code similar to a specific file or function",
195
+ inputSchema={
196
+ "type": "object",
197
+ "properties": {
198
+ "file_path": {
199
+ "type": "string",
200
+ "description": "Path to the file to find similar code for"
201
+ },
202
+ "function_name": {
203
+ "type": "string",
204
+ "description": "Optional function name within the file"
205
+ },
206
+ "limit": {
207
+ "type": "integer",
208
+ "description": "Maximum number of results to return",
209
+ "default": 10,
210
+ "minimum": 1,
211
+ "maximum": 50
212
+ },
213
+ "similarity_threshold": {
214
+ "type": "number",
215
+ "description": "Minimum similarity threshold (0.0-1.0)",
216
+ "default": 0.3,
217
+ "minimum": 0.0,
218
+ "maximum": 1.0
219
+ }
220
+ },
221
+ "required": ["file_path"]
222
+ }
223
+ ),
224
+ Tool(
225
+ name="search_context",
226
+ description="Search for code based on contextual description",
227
+ inputSchema={
228
+ "type": "object",
229
+ "properties": {
230
+ "description": {
231
+ "type": "string",
232
+ "description": "Contextual description of what you're looking for"
233
+ },
234
+ "focus_areas": {
235
+ "type": "array",
236
+ "items": {"type": "string"},
237
+ "description": "Areas to focus on (e.g., ['security', 'authentication'])"
238
+ },
239
+ "limit": {
240
+ "type": "integer",
241
+ "description": "Maximum number of results to return",
242
+ "default": 10,
243
+ "minimum": 1,
244
+ "maximum": 50
245
+ }
246
+ },
247
+ "required": ["description"]
248
+ }
249
+ ),
250
+ Tool(
251
+ name="get_project_status",
252
+ description="Get project indexing status and statistics",
253
+ inputSchema={
254
+ "type": "object",
255
+ "properties": {},
256
+ "required": []
257
+ }
258
+ ),
259
+ Tool(
260
+ name="index_project",
261
+ description="Index or reindex the project codebase",
262
+ inputSchema={
263
+ "type": "object",
264
+ "properties": {
265
+ "force": {
266
+ "type": "boolean",
267
+ "description": "Force reindexing even if index exists",
268
+ "default": False
269
+ },
270
+ "file_extensions": {
271
+ "type": "array",
272
+ "items": {"type": "string"},
273
+ "description": "File extensions to index (e.g., ['.py', '.js'])"
274
+ }
275
+ },
276
+ "required": []
277
+ }
278
+ )
279
+ ]
280
+
281
+ return tools
282
+
283
+ def get_capabilities(self) -> ServerCapabilities:
284
+ """Get server capabilities."""
285
+ return ServerCapabilities(
286
+ tools={"listChanged": True},
287
+ logging={}
288
+ )
289
+
290
+ async def call_tool(self, request: CallToolRequest) -> CallToolResult:
291
+ """Handle tool calls."""
292
+ if not self._initialized:
293
+ await self.initialize()
294
+
295
+ try:
296
+ if request.params.name == "search_code":
297
+ return await self._search_code(request.params.arguments)
298
+ elif request.params.name == "search_similar":
299
+ return await self._search_similar(request.params.arguments)
300
+ elif request.params.name == "search_context":
301
+ return await self._search_context(request.params.arguments)
302
+ elif request.params.name == "get_project_status":
303
+ return await self._get_project_status(request.params.arguments)
304
+ elif request.params.name == "index_project":
305
+ return await self._index_project(request.params.arguments)
306
+ else:
307
+ return CallToolResult(
308
+ content=[TextContent(
309
+ type="text",
310
+ text=f"Unknown tool: {request.params.name}"
311
+ )],
312
+ isError=True
313
+ )
314
+ except Exception as e:
315
+ logger.error(f"Tool call failed: {e}")
316
+ return CallToolResult(
317
+ content=[TextContent(
318
+ type="text",
319
+ text=f"Tool execution failed: {str(e)}"
320
+ )],
321
+ isError=True
322
+ )
323
+
324
+ async def _search_code(self, args: Dict[str, Any]) -> CallToolResult:
325
+ """Handle search_code tool call."""
326
+ query = args.get("query", "")
327
+ limit = args.get("limit", 10)
328
+ similarity_threshold = args.get("similarity_threshold", 0.3)
329
+ file_extensions = args.get("file_extensions")
330
+ language = args.get("language")
331
+ function_name = args.get("function_name")
332
+ class_name = args.get("class_name")
333
+ files = args.get("files")
334
+
335
+ if not query:
336
+ return CallToolResult(
337
+ content=[TextContent(
338
+ type="text",
339
+ text="Query parameter is required"
340
+ )],
341
+ isError=True
342
+ )
343
+
344
+ # Build filters
345
+ filters = {}
346
+ if file_extensions:
347
+ filters["file_extension"] = {"$in": file_extensions}
348
+ if language:
349
+ filters["language"] = language
350
+ if function_name:
351
+ filters["function_name"] = function_name
352
+ if class_name:
353
+ filters["class_name"] = class_name
354
+ if files:
355
+ # Convert file pattern to filter (simplified)
356
+ filters["file_pattern"] = files
357
+
358
+ # Perform search
359
+ results = await self.search_engine.search(
360
+ query=query,
361
+ limit=limit,
362
+ similarity_threshold=similarity_threshold,
363
+ filters=filters
364
+ )
365
+
366
+ # Format results
367
+ if not results:
368
+ response_text = f"No results found for query: '{query}'"
369
+ else:
370
+ response_lines = [f"Found {len(results)} results for query: '{query}'\n"]
371
+
372
+ for i, result in enumerate(results, 1):
373
+ response_lines.append(f"## Result {i} (Score: {result.similarity_score:.3f})")
374
+ response_lines.append(f"**File:** {result.file_path}")
375
+ if result.function_name:
376
+ response_lines.append(f"**Function:** {result.function_name}")
377
+ if result.class_name:
378
+ response_lines.append(f"**Class:** {result.class_name}")
379
+ response_lines.append(f"**Lines:** {result.start_line}-{result.end_line}")
380
+ response_lines.append("**Code:**")
381
+ response_lines.append("```" + (result.language or ""))
382
+ response_lines.append(result.content)
383
+ response_lines.append("```\n")
384
+
385
+ response_text = "\n".join(response_lines)
386
+
387
+ return CallToolResult(
388
+ content=[TextContent(type="text", text=response_text)]
389
+ )
390
+
391
+ async def _get_project_status(self, args: Dict[str, Any]) -> CallToolResult:
392
+ """Handle get_project_status tool call."""
393
+ try:
394
+ config = self.project_manager.load_config()
395
+
396
+ # Get database stats
397
+ if self.search_engine:
398
+ stats = await self.search_engine.database.get_stats()
399
+
400
+ status_info = {
401
+ "project_root": str(config.project_root),
402
+ "index_path": str(config.index_path),
403
+ "file_extensions": config.file_extensions,
404
+ "embedding_model": config.embedding_model,
405
+ "languages": config.languages,
406
+ "total_chunks": stats.total_chunks,
407
+ "total_files": stats.total_files,
408
+ "index_size": f"{stats.index_size_mb:.2f} MB" if hasattr(stats, 'index_size_mb') else "Unknown"
409
+ }
410
+ else:
411
+ status_info = {
412
+ "project_root": str(config.project_root),
413
+ "index_path": str(config.index_path),
414
+ "file_extensions": config.file_extensions,
415
+ "embedding_model": config.embedding_model,
416
+ "languages": config.languages,
417
+ "status": "Not indexed"
418
+ }
419
+
420
+ response_text = f"# Project Status\n\n"
421
+ response_text += f"**Project Root:** {status_info['project_root']}\n"
422
+ response_text += f"**Index Path:** {status_info['index_path']}\n"
423
+ response_text += f"**File Extensions:** {', '.join(status_info['file_extensions'])}\n"
424
+ response_text += f"**Embedding Model:** {status_info['embedding_model']}\n"
425
+ response_text += f"**Languages:** {', '.join(status_info['languages'])}\n"
426
+
427
+ if "total_chunks" in status_info:
428
+ response_text += f"**Total Chunks:** {status_info['total_chunks']}\n"
429
+ response_text += f"**Total Files:** {status_info['total_files']}\n"
430
+ response_text += f"**Index Size:** {status_info['index_size']}\n"
431
+ else:
432
+ response_text += f"**Status:** {status_info['status']}\n"
433
+
434
+ return CallToolResult(
435
+ content=[TextContent(type="text", text=response_text)]
436
+ )
437
+
438
+ except ProjectNotFoundError:
439
+ return CallToolResult(
440
+ content=[TextContent(
441
+ type="text",
442
+ text=f"Project not initialized at {self.project_root}. Run 'mcp-vector-search init' first."
443
+ )],
444
+ isError=True
445
+ )
446
+
447
+ async def _index_project(self, args: Dict[str, Any]) -> CallToolResult:
448
+ """Handle index_project tool call."""
449
+ force = args.get("force", False)
450
+ file_extensions = args.get("file_extensions")
451
+
452
+ try:
453
+ # Import indexing functionality
454
+ from ..cli.commands.index import run_indexing
455
+
456
+ # Run indexing
457
+ await run_indexing(
458
+ project_root=self.project_root,
459
+ force_reindex=force,
460
+ extensions=file_extensions,
461
+ show_progress=False # Disable progress for MCP
462
+ )
463
+
464
+ # Reinitialize search engine after indexing
465
+ await self.cleanup()
466
+ await self.initialize()
467
+
468
+ return CallToolResult(
469
+ content=[TextContent(
470
+ type="text",
471
+ text="Project indexing completed successfully!"
472
+ )]
473
+ )
474
+
475
+ except Exception as e:
476
+ return CallToolResult(
477
+ content=[TextContent(
478
+ type="text",
479
+ text=f"Indexing failed: {str(e)}"
480
+ )],
481
+ isError=True
482
+ )
483
+
484
+ async def _search_similar(self, args: Dict[str, Any]) -> CallToolResult:
485
+ """Handle search_similar tool call."""
486
+ file_path = args.get("file_path", "")
487
+ function_name = args.get("function_name")
488
+ limit = args.get("limit", 10)
489
+ similarity_threshold = args.get("similarity_threshold", 0.3)
490
+
491
+ if not file_path:
492
+ return CallToolResult(
493
+ content=[TextContent(
494
+ type="text",
495
+ text="file_path parameter is required"
496
+ )],
497
+ isError=True
498
+ )
499
+
500
+ try:
501
+ from pathlib import Path
502
+ from ..cli.commands.search import run_similar_search
503
+
504
+ # Convert to Path object
505
+ file_path_obj = Path(file_path)
506
+ if not file_path_obj.is_absolute():
507
+ file_path_obj = self.project_root / file_path_obj
508
+
509
+ if not file_path_obj.exists():
510
+ return CallToolResult(
511
+ content=[TextContent(
512
+ type="text",
513
+ text=f"File not found: {file_path}"
514
+ )],
515
+ isError=True
516
+ )
517
+
518
+ # Run similar search
519
+ results = await self.search_engine.search_similar(
520
+ file_path=file_path_obj,
521
+ function_name=function_name,
522
+ limit=limit,
523
+ similarity_threshold=similarity_threshold
524
+ )
525
+
526
+ # Format results
527
+ if not results:
528
+ return CallToolResult(
529
+ content=[TextContent(
530
+ type="text",
531
+ text=f"No similar code found for {file_path}"
532
+ )]
533
+ )
534
+
535
+ result_text = f"Found {len(results)} similar code snippets for {file_path}:\n\n"
536
+ for i, result in enumerate(results, 1):
537
+ result_text += f"{i}. {result.file_path}:{result.line_number}\n"
538
+ result_text += f" Similarity: {result.similarity:.3f}\n"
539
+ result_text += f" {result.content[:100]}...\n\n"
540
+
541
+ return CallToolResult(
542
+ content=[TextContent(
543
+ type="text",
544
+ text=result_text
545
+ )]
546
+ )
547
+
548
+ except Exception as e:
549
+ return CallToolResult(
550
+ content=[TextContent(
551
+ type="text",
552
+ text=f"Similar search failed: {str(e)}"
553
+ )],
554
+ isError=True
555
+ )
556
+
557
+ async def _search_context(self, args: Dict[str, Any]) -> CallToolResult:
558
+ """Handle search_context tool call."""
559
+ description = args.get("description", "")
560
+ focus_areas = args.get("focus_areas")
561
+ limit = args.get("limit", 10)
562
+
563
+ if not description:
564
+ return CallToolResult(
565
+ content=[TextContent(
566
+ type="text",
567
+ text="description parameter is required"
568
+ )],
569
+ isError=True
570
+ )
571
+
572
+ try:
573
+ # Perform context search
574
+ results = await self.search_engine.search_by_context(
575
+ context_description=description,
576
+ focus_areas=focus_areas,
577
+ limit=limit
578
+ )
579
+
580
+ # Format results
581
+ if not results:
582
+ return CallToolResult(
583
+ content=[TextContent(
584
+ type="text",
585
+ text=f"No contextually relevant code found for: {description}"
586
+ )]
587
+ )
588
+
589
+ result_text = f"Found {len(results)} contextually relevant code snippets"
590
+ if focus_areas:
591
+ result_text += f" (focus: {', '.join(focus_areas)})"
592
+ result_text += f" for: {description}\n\n"
593
+
594
+ for i, result in enumerate(results, 1):
595
+ result_text += f"{i}. {result.file_path}:{result.line_number}\n"
596
+ result_text += f" Similarity: {result.similarity:.3f}\n"
597
+ result_text += f" {result.content[:100]}...\n\n"
598
+
599
+ return CallToolResult(
600
+ content=[TextContent(
601
+ type="text",
602
+ text=result_text
603
+ )]
604
+ )
605
+
606
+ except Exception as e:
607
+ return CallToolResult(
608
+ content=[TextContent(
609
+ type="text",
610
+ text=f"Context search failed: {str(e)}"
611
+ )],
612
+ isError=True
613
+ )
614
+
615
+
616
+ def create_mcp_server(project_root: Optional[Path] = None, enable_file_watching: Optional[bool] = None) -> Server:
617
+ """Create and configure the MCP server.
618
+
619
+ Args:
620
+ project_root: Project root directory. If None, will auto-detect.
621
+ enable_file_watching: Enable file watching for automatic reindexing.
622
+ If None, checks MCP_ENABLE_FILE_WATCHING env var (default: True).
623
+ """
624
+ server = Server("mcp-vector-search")
625
+ mcp_server = MCPVectorSearchServer(project_root, enable_file_watching)
626
+
627
+ @server.list_tools()
628
+ async def handle_list_tools() -> List[Tool]:
629
+ """List available tools."""
630
+ return mcp_server.get_tools()
631
+
632
+ @server.call_tool()
633
+ async def handle_call_tool(name: str, arguments: dict | None):
634
+ """Handle tool calls."""
635
+ # Create a mock request object for compatibility
636
+ from types import SimpleNamespace
637
+ mock_request = SimpleNamespace()
638
+ mock_request.params = SimpleNamespace()
639
+ mock_request.params.name = name
640
+ mock_request.params.arguments = arguments or {}
641
+
642
+ result = await mcp_server.call_tool(mock_request)
643
+
644
+ # Return the content from the result
645
+ return result.content
646
+
647
+ # Store reference for cleanup
648
+ server._mcp_server = mcp_server
649
+
650
+ return server
651
+
652
+
653
+ async def run_mcp_server(project_root: Optional[Path] = None, enable_file_watching: Optional[bool] = None) -> None:
654
+ """Run the MCP server using stdio transport.
655
+
656
+ Args:
657
+ project_root: Project root directory. If None, will auto-detect.
658
+ enable_file_watching: Enable file watching for automatic reindexing.
659
+ If None, checks MCP_ENABLE_FILE_WATCHING env var (default: True).
660
+ """
661
+ server = create_mcp_server(project_root, enable_file_watching)
662
+
663
+ # Create initialization options with proper capabilities
664
+ init_options = InitializationOptions(
665
+ server_name="mcp-vector-search",
666
+ server_version="0.4.0",
667
+ capabilities=ServerCapabilities(
668
+ tools={"listChanged": True},
669
+ logging={}
670
+ )
671
+ )
672
+
673
+ try:
674
+ async with stdio_server() as (read_stream, write_stream):
675
+ await server.run(read_stream, write_stream, init_options)
676
+ except KeyboardInterrupt:
677
+ logger.info("Received interrupt signal, shutting down...")
678
+ except Exception as e:
679
+ logger.error(f"MCP server error: {e}")
680
+ raise
681
+ finally:
682
+ # Cleanup
683
+ if hasattr(server, '_mcp_server'):
684
+ logger.info("Performing server cleanup...")
685
+ await server._mcp_server.cleanup()
686
+
687
+
688
+ if __name__ == "__main__":
689
+ # Allow specifying project root as command line argument
690
+ project_root = Path(sys.argv[1]) if len(sys.argv) > 1 else None
691
+
692
+ # Check for file watching flag in command line args
693
+ enable_file_watching = None
694
+ if "--no-watch" in sys.argv:
695
+ enable_file_watching = False
696
+ sys.argv.remove("--no-watch")
697
+ elif "--watch" in sys.argv:
698
+ enable_file_watching = True
699
+ sys.argv.remove("--watch")
700
+
701
+ asyncio.run(run_mcp_server(project_root, enable_file_watching))