mcp-vector-search 0.9.3__py3-none-any.whl → 0.12.1__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 +2 -2
- mcp_vector_search/cli/commands/index.py +44 -22
- mcp_vector_search/cli/commands/install.py +502 -523
- mcp_vector_search/cli/commands/install_old.py +696 -0
- mcp_vector_search/cli/commands/status.py +7 -5
- mcp_vector_search/cli/commands/uninstall.py +485 -0
- mcp_vector_search/cli/commands/visualize.py +677 -53
- mcp_vector_search/cli/didyoumean.py +10 -0
- mcp_vector_search/cli/main.py +39 -21
- mcp_vector_search/core/connection_pool.py +49 -11
- mcp_vector_search/core/database.py +61 -28
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/indexer.py +146 -19
- mcp_vector_search/core/models.py +61 -0
- mcp_vector_search/core/project.py +16 -5
- mcp_vector_search/parsers/base.py +54 -18
- mcp_vector_search/parsers/javascript.py +41 -20
- mcp_vector_search/parsers/python.py +19 -11
- mcp_vector_search/parsers/registry.py +3 -2
- mcp_vector_search/utils/gitignore.py +17 -5
- mcp_vector_search/visualization/index.html +658 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/METADATA +87 -24
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/RECORD +26 -22
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/WHEEL +0 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -236,9 +236,12 @@ class ProjectManager:
|
|
|
236
236
|
count += 1
|
|
237
237
|
return count
|
|
238
238
|
|
|
239
|
-
def get_project_info(self) -> ProjectInfo:
|
|
239
|
+
def get_project_info(self, file_count: int | None = None) -> ProjectInfo:
|
|
240
240
|
"""Get comprehensive project information.
|
|
241
241
|
|
|
242
|
+
Args:
|
|
243
|
+
file_count: Optional pre-computed file count (avoids expensive filesystem scan)
|
|
244
|
+
|
|
242
245
|
Returns:
|
|
243
246
|
Project information
|
|
244
247
|
"""
|
|
@@ -247,13 +250,19 @@ class ProjectManager:
|
|
|
247
250
|
|
|
248
251
|
is_initialized = self.is_initialized()
|
|
249
252
|
languages = []
|
|
250
|
-
|
|
253
|
+
computed_file_count = 0
|
|
251
254
|
|
|
252
255
|
if is_initialized:
|
|
253
256
|
try:
|
|
254
257
|
config = self.config
|
|
255
258
|
languages = config.languages
|
|
256
|
-
file_count
|
|
259
|
+
# Use provided file_count if available to avoid filesystem scan
|
|
260
|
+
if file_count is not None:
|
|
261
|
+
computed_file_count = file_count
|
|
262
|
+
else:
|
|
263
|
+
computed_file_count = self.count_indexable_files(
|
|
264
|
+
config.file_extensions
|
|
265
|
+
)
|
|
257
266
|
except Exception:
|
|
258
267
|
# Ignore errors when getting detailed info
|
|
259
268
|
pass
|
|
@@ -265,7 +274,7 @@ class ProjectManager:
|
|
|
265
274
|
index_path=index_path,
|
|
266
275
|
is_initialized=is_initialized,
|
|
267
276
|
languages=languages,
|
|
268
|
-
file_count=
|
|
277
|
+
file_count=computed_file_count,
|
|
269
278
|
)
|
|
270
279
|
|
|
271
280
|
def _iter_source_files(self) -> list[Path]:
|
|
@@ -301,7 +310,9 @@ class ProjectManager:
|
|
|
301
310
|
"""
|
|
302
311
|
# First check gitignore rules if available
|
|
303
312
|
# PERFORMANCE: Pass is_directory hint to avoid redundant stat() calls
|
|
304
|
-
if self.gitignore_parser and self.gitignore_parser.is_ignored(
|
|
313
|
+
if self.gitignore_parser and self.gitignore_parser.is_ignored(
|
|
314
|
+
path, is_directory=is_directory
|
|
315
|
+
):
|
|
305
316
|
return True
|
|
306
317
|
|
|
307
318
|
# Check if any parent directory is in ignore patterns
|
|
@@ -79,7 +79,7 @@ class BaseParser(ABC):
|
|
|
79
79
|
if language is None:
|
|
80
80
|
language = self.language
|
|
81
81
|
|
|
82
|
-
if not hasattr(node,
|
|
82
|
+
if not hasattr(node, "children"):
|
|
83
83
|
return 1.0
|
|
84
84
|
|
|
85
85
|
complexity = 1.0 # Base complexity
|
|
@@ -87,39 +87,75 @@ class BaseParser(ABC):
|
|
|
87
87
|
# Language-specific decision node types
|
|
88
88
|
decision_nodes = {
|
|
89
89
|
"python": {
|
|
90
|
-
"if_statement",
|
|
91
|
-
"
|
|
92
|
-
"
|
|
90
|
+
"if_statement",
|
|
91
|
+
"elif_clause",
|
|
92
|
+
"while_statement",
|
|
93
|
+
"for_statement",
|
|
94
|
+
"except_clause",
|
|
95
|
+
"with_statement",
|
|
96
|
+
"conditional_expression",
|
|
97
|
+
"boolean_operator", # and, or
|
|
93
98
|
},
|
|
94
99
|
"javascript": {
|
|
95
|
-
"if_statement",
|
|
96
|
-
"
|
|
100
|
+
"if_statement",
|
|
101
|
+
"while_statement",
|
|
102
|
+
"for_statement",
|
|
103
|
+
"for_in_statement",
|
|
104
|
+
"switch_case",
|
|
105
|
+
"catch_clause",
|
|
106
|
+
"conditional_expression",
|
|
107
|
+
"ternary_expression",
|
|
97
108
|
},
|
|
98
109
|
"typescript": {
|
|
99
|
-
"if_statement",
|
|
100
|
-
"
|
|
110
|
+
"if_statement",
|
|
111
|
+
"while_statement",
|
|
112
|
+
"for_statement",
|
|
113
|
+
"for_in_statement",
|
|
114
|
+
"switch_case",
|
|
115
|
+
"catch_clause",
|
|
116
|
+
"conditional_expression",
|
|
117
|
+
"ternary_expression",
|
|
101
118
|
},
|
|
102
119
|
"dart": {
|
|
103
|
-
"if_statement",
|
|
104
|
-
"
|
|
120
|
+
"if_statement",
|
|
121
|
+
"while_statement",
|
|
122
|
+
"for_statement",
|
|
123
|
+
"for_in_statement",
|
|
124
|
+
"switch_case",
|
|
125
|
+
"catch_clause",
|
|
126
|
+
"conditional_expression",
|
|
105
127
|
},
|
|
106
128
|
"php": {
|
|
107
|
-
"if_statement",
|
|
108
|
-
"
|
|
129
|
+
"if_statement",
|
|
130
|
+
"elseif_clause",
|
|
131
|
+
"while_statement",
|
|
132
|
+
"foreach_statement",
|
|
133
|
+
"for_statement",
|
|
134
|
+
"switch_case",
|
|
135
|
+
"catch_clause",
|
|
136
|
+
"ternary_expression",
|
|
109
137
|
},
|
|
110
138
|
"ruby": {
|
|
111
|
-
"if",
|
|
112
|
-
"
|
|
113
|
-
|
|
139
|
+
"if",
|
|
140
|
+
"unless",
|
|
141
|
+
"while",
|
|
142
|
+
"until",
|
|
143
|
+
"for",
|
|
144
|
+
"case",
|
|
145
|
+
"rescue",
|
|
146
|
+
"conditional",
|
|
147
|
+
},
|
|
114
148
|
}
|
|
115
149
|
|
|
116
|
-
nodes_to_count = decision_nodes.get(
|
|
150
|
+
nodes_to_count = decision_nodes.get(
|
|
151
|
+
language, decision_nodes.get("python", set())
|
|
152
|
+
)
|
|
117
153
|
|
|
118
154
|
def count_decision_points(n):
|
|
119
155
|
nonlocal complexity
|
|
120
|
-
if hasattr(n,
|
|
156
|
+
if hasattr(n, "type") and n.type in nodes_to_count:
|
|
121
157
|
complexity += 1
|
|
122
|
-
if hasattr(n,
|
|
158
|
+
if hasattr(n, "children"):
|
|
123
159
|
for child in n.children:
|
|
124
160
|
count_decision_points(child)
|
|
125
161
|
|
|
@@ -54,7 +54,7 @@ class JavaScriptParser(BaseParser):
|
|
|
54
54
|
|
|
55
55
|
if self._use_tree_sitter:
|
|
56
56
|
try:
|
|
57
|
-
tree = self._parser.parse(content.encode(
|
|
57
|
+
tree = self._parser.parse(content.encode("utf-8"))
|
|
58
58
|
return self._extract_chunks_from_tree(tree, content, file_path)
|
|
59
59
|
except Exception as e:
|
|
60
60
|
logger.warning(f"Tree-sitter parsing failed for {file_path}: {e}")
|
|
@@ -77,10 +77,14 @@ class JavaScriptParser(BaseParser):
|
|
|
77
77
|
extracted = False
|
|
78
78
|
|
|
79
79
|
if node_type == "function_declaration":
|
|
80
|
-
chunks.extend(
|
|
80
|
+
chunks.extend(
|
|
81
|
+
self._extract_function(node, lines, file_path, current_class)
|
|
82
|
+
)
|
|
81
83
|
extracted = True
|
|
82
84
|
elif node_type == "arrow_function":
|
|
83
|
-
chunks.extend(
|
|
85
|
+
chunks.extend(
|
|
86
|
+
self._extract_arrow_function(node, lines, file_path, current_class)
|
|
87
|
+
)
|
|
84
88
|
extracted = True
|
|
85
89
|
elif node_type == "class_declaration":
|
|
86
90
|
class_chunks = self._extract_class(node, lines, file_path)
|
|
@@ -92,18 +96,22 @@ class JavaScriptParser(BaseParser):
|
|
|
92
96
|
visit_node(child, class_name)
|
|
93
97
|
extracted = True
|
|
94
98
|
elif node_type == "method_definition":
|
|
95
|
-
chunks.extend(
|
|
99
|
+
chunks.extend(
|
|
100
|
+
self._extract_method(node, lines, file_path, current_class)
|
|
101
|
+
)
|
|
96
102
|
extracted = True
|
|
97
103
|
elif node_type == "lexical_declaration":
|
|
98
104
|
# const/let declarations might be arrow functions
|
|
99
|
-
extracted_chunks = self._extract_variable_function(
|
|
105
|
+
extracted_chunks = self._extract_variable_function(
|
|
106
|
+
node, lines, file_path, current_class
|
|
107
|
+
)
|
|
100
108
|
if extracted_chunks:
|
|
101
109
|
chunks.extend(extracted_chunks)
|
|
102
110
|
extracted = True
|
|
103
111
|
|
|
104
112
|
# Only recurse into children if we didn't extract this node
|
|
105
113
|
# This prevents double-extraction of arrow functions in variable declarations
|
|
106
|
-
if not extracted and hasattr(node,
|
|
114
|
+
if not extracted and hasattr(node, "children"):
|
|
107
115
|
for child in node.children:
|
|
108
116
|
visit_node(child, current_class)
|
|
109
117
|
|
|
@@ -163,7 +171,7 @@ class JavaScriptParser(BaseParser):
|
|
|
163
171
|
) -> list[CodeChunk]:
|
|
164
172
|
"""Extract arrow function from AST."""
|
|
165
173
|
# Arrow functions often don't have explicit names, try to get from parent
|
|
166
|
-
parent = getattr(node,
|
|
174
|
+
parent = getattr(node, "parent", None)
|
|
167
175
|
function_name = None
|
|
168
176
|
|
|
169
177
|
if parent and parent.type == "variable_declarator":
|
|
@@ -219,7 +227,9 @@ class JavaScriptParser(BaseParser):
|
|
|
219
227
|
docstring = self._extract_jsdoc_from_node(child, lines)
|
|
220
228
|
|
|
221
229
|
# Calculate complexity
|
|
222
|
-
complexity = self._calculate_complexity(
|
|
230
|
+
complexity = self._calculate_complexity(
|
|
231
|
+
subchild, "javascript"
|
|
232
|
+
)
|
|
223
233
|
|
|
224
234
|
# Extract parameters
|
|
225
235
|
parameters = self._extract_js_parameters(subchild)
|
|
@@ -319,8 +329,8 @@ class JavaScriptParser(BaseParser):
|
|
|
319
329
|
|
|
320
330
|
def _get_node_text(self, node) -> str:
|
|
321
331
|
"""Get text content of a node."""
|
|
322
|
-
if hasattr(node,
|
|
323
|
-
return node.text.decode(
|
|
332
|
+
if hasattr(node, "text"):
|
|
333
|
+
return node.text.decode("utf-8")
|
|
324
334
|
return ""
|
|
325
335
|
|
|
326
336
|
def _extract_js_parameters(self, node) -> list[dict]:
|
|
@@ -330,12 +340,13 @@ class JavaScriptParser(BaseParser):
|
|
|
330
340
|
for child in node.children:
|
|
331
341
|
if child.type == "formal_parameters":
|
|
332
342
|
for param_node in child.children:
|
|
333
|
-
if param_node.type in (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
343
|
+
if param_node.type in (
|
|
344
|
+
"identifier",
|
|
345
|
+
"required_parameter",
|
|
346
|
+
"optional_parameter",
|
|
347
|
+
"rest_parameter",
|
|
348
|
+
):
|
|
349
|
+
param_info = {"name": None, "type": None, "default": None}
|
|
339
350
|
|
|
340
351
|
# Extract parameter details
|
|
341
352
|
if param_node.type == "identifier":
|
|
@@ -347,10 +358,20 @@ class JavaScriptParser(BaseParser):
|
|
|
347
358
|
param_info["name"] = self._get_node_text(subchild)
|
|
348
359
|
elif subchild.type == "type_annotation":
|
|
349
360
|
param_info["type"] = self._get_node_text(subchild)
|
|
350
|
-
elif
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
361
|
+
elif (
|
|
362
|
+
"default" in subchild.type
|
|
363
|
+
or subchild.type == "number"
|
|
364
|
+
):
|
|
365
|
+
param_info["default"] = self._get_node_text(
|
|
366
|
+
subchild
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if param_info["name"] and param_info["name"] not in (
|
|
370
|
+
"(",
|
|
371
|
+
")",
|
|
372
|
+
",",
|
|
373
|
+
"...",
|
|
374
|
+
):
|
|
354
375
|
# Clean up rest parameters
|
|
355
376
|
if param_info["name"].startswith("..."):
|
|
356
377
|
param_info["name"] = param_info["name"][3:]
|
|
@@ -452,12 +452,12 @@ class PythonParser(BaseParser):
|
|
|
452
452
|
for child in node.children:
|
|
453
453
|
if child.type == "parameters":
|
|
454
454
|
for param_node in child.children:
|
|
455
|
-
if param_node.type in (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
455
|
+
if param_node.type in (
|
|
456
|
+
"identifier",
|
|
457
|
+
"typed_parameter",
|
|
458
|
+
"default_parameter",
|
|
459
|
+
):
|
|
460
|
+
param_info = {"name": None, "type": None, "default": None}
|
|
461
461
|
|
|
462
462
|
# Extract parameter name
|
|
463
463
|
if param_node.type == "identifier":
|
|
@@ -470,9 +470,17 @@ class PythonParser(BaseParser):
|
|
|
470
470
|
elif subchild.type == "type":
|
|
471
471
|
param_info["type"] = self._get_node_text(subchild)
|
|
472
472
|
elif "default" in subchild.type:
|
|
473
|
-
param_info["default"] = self._get_node_text(
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
param_info["default"] = self._get_node_text(
|
|
474
|
+
subchild
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if param_info["name"] and param_info["name"] not in (
|
|
478
|
+
"self",
|
|
479
|
+
"cls",
|
|
480
|
+
"(",
|
|
481
|
+
")",
|
|
482
|
+
",",
|
|
483
|
+
):
|
|
476
484
|
parameters.append(param_info)
|
|
477
485
|
return parameters
|
|
478
486
|
|
|
@@ -485,8 +493,8 @@ class PythonParser(BaseParser):
|
|
|
485
493
|
|
|
486
494
|
def _get_node_text(self, node) -> str:
|
|
487
495
|
"""Get text content of a node."""
|
|
488
|
-
if hasattr(node,
|
|
489
|
-
return node.text.decode(
|
|
496
|
+
if hasattr(node, "text"):
|
|
497
|
+
return node.text.decode("utf-8")
|
|
490
498
|
return ""
|
|
491
499
|
|
|
492
500
|
def get_supported_extensions(self) -> list[str]:
|
|
@@ -162,14 +162,15 @@ class ParserRegistry:
|
|
|
162
162
|
info[language] = {
|
|
163
163
|
"class": parser.__class__.__name__,
|
|
164
164
|
"extensions": parser.get_supported_extensions(),
|
|
165
|
-
"language": parser
|
|
165
|
+
"language": getattr(parser, "language", None) or language,
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
# Add fallback parser info
|
|
169
|
+
fallback_lang = getattr(self._fallback_parser, "language", None) or "unknown"
|
|
169
170
|
info["fallback"] = {
|
|
170
171
|
"class": self._fallback_parser.__class__.__name__,
|
|
171
172
|
"extensions": ["*"],
|
|
172
|
-
"language":
|
|
173
|
+
"language": fallback_lang,
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
return info
|
|
@@ -51,14 +51,24 @@ class GitignorePattern:
|
|
|
51
51
|
Returns:
|
|
52
52
|
True if the pattern matches
|
|
53
53
|
"""
|
|
54
|
-
# Directory-only patterns only match directories
|
|
55
|
-
if self.is_directory_only and not is_directory:
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
54
|
# Convert path separators for consistent matching
|
|
59
55
|
path = path.replace("\\", "/")
|
|
60
56
|
pattern = self.pattern.replace("\\", "/")
|
|
61
57
|
|
|
58
|
+
# For directory-only patterns, check if any parent directory matches
|
|
59
|
+
# This implements Git's behavior where "dir/" excludes both the directory
|
|
60
|
+
# AND all files within it recursively
|
|
61
|
+
if self.is_directory_only:
|
|
62
|
+
path_parts = path.split("/")
|
|
63
|
+
# Check each parent directory component
|
|
64
|
+
for i in range(1, len(path_parts) + 1):
|
|
65
|
+
parent = "/".join(path_parts[:i])
|
|
66
|
+
if fnmatch.fnmatch(parent, pattern):
|
|
67
|
+
return True
|
|
68
|
+
# If no parent matches and this is not a directory, don't exclude
|
|
69
|
+
if not is_directory:
|
|
70
|
+
return False
|
|
71
|
+
|
|
62
72
|
# Try exact match first
|
|
63
73
|
if fnmatch.fnmatch(path, pattern):
|
|
64
74
|
return True
|
|
@@ -223,7 +233,9 @@ def create_gitignore_parser(project_root: Path) -> GitignoreParser:
|
|
|
223
233
|
return GitignoreParser(project_root)
|
|
224
234
|
|
|
225
235
|
|
|
226
|
-
def is_path_gitignored(
|
|
236
|
+
def is_path_gitignored(
|
|
237
|
+
path: Path, project_root: Path, is_directory: bool | None = None
|
|
238
|
+
) -> bool:
|
|
227
239
|
"""Quick function to check if a path is gitignored.
|
|
228
240
|
|
|
229
241
|
Args:
|