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