mcp-vector-search 1.0.3__py3-none-any.whl → 1.1.22__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.
Files changed (63) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +48 -1
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +35 -0
  7. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  8. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  9. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  10. mcp_vector_search/analysis/collectors/smells.py +325 -0
  11. mcp_vector_search/analysis/debt.py +516 -0
  12. mcp_vector_search/analysis/interpretation.py +685 -0
  13. mcp_vector_search/analysis/metrics.py +74 -1
  14. mcp_vector_search/analysis/reporters/__init__.py +3 -1
  15. mcp_vector_search/analysis/reporters/console.py +424 -0
  16. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  17. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  18. mcp_vector_search/analysis/storage/__init__.py +93 -0
  19. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  20. mcp_vector_search/analysis/storage/schema.py +245 -0
  21. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  22. mcp_vector_search/analysis/trends.py +308 -0
  23. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  24. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  25. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  26. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  27. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  28. mcp_vector_search/cli/commands/analyze.py +665 -11
  29. mcp_vector_search/cli/commands/chat.py +193 -0
  30. mcp_vector_search/cli/commands/index.py +600 -2
  31. mcp_vector_search/cli/commands/index_background.py +467 -0
  32. mcp_vector_search/cli/commands/search.py +194 -1
  33. mcp_vector_search/cli/commands/setup.py +64 -13
  34. mcp_vector_search/cli/commands/status.py +302 -3
  35. mcp_vector_search/cli/commands/visualize/cli.py +26 -10
  36. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
  37. mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
  38. mcp_vector_search/cli/commands/visualize/server.py +304 -15
  39. mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
  40. mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
  41. mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
  42. mcp_vector_search/cli/didyoumean.py +5 -0
  43. mcp_vector_search/cli/main.py +16 -5
  44. mcp_vector_search/cli/output.py +134 -5
  45. mcp_vector_search/config/thresholds.py +89 -1
  46. mcp_vector_search/core/__init__.py +16 -0
  47. mcp_vector_search/core/database.py +39 -2
  48. mcp_vector_search/core/embeddings.py +24 -0
  49. mcp_vector_search/core/git.py +380 -0
  50. mcp_vector_search/core/indexer.py +445 -84
  51. mcp_vector_search/core/llm_client.py +9 -4
  52. mcp_vector_search/core/models.py +88 -1
  53. mcp_vector_search/core/relationships.py +473 -0
  54. mcp_vector_search/core/search.py +1 -1
  55. mcp_vector_search/mcp/server.py +795 -4
  56. mcp_vector_search/parsers/python.py +285 -5
  57. mcp_vector_search/utils/gitignore.py +0 -3
  58. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
  59. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
  60. mcp_vector_search/cli/commands/visualize.py.original +0 -2536
  61. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
  62. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
  63. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module handles running the local HTTP server to serve the
4
4
  D3.js visualization interface with chunked transfer for large JSON files.
5
+ Uses orjson for 5-10x faster JSON serialization.
5
6
  """
6
7
 
7
8
  import asyncio
@@ -10,6 +11,7 @@ import webbrowser
10
11
  from collections.abc import AsyncGenerator
11
12
  from pathlib import Path
12
13
 
14
+ import orjson
13
15
  import uvicorn
14
16
  from fastapi import FastAPI, Response
15
17
  from fastapi.responses import FileResponse, StreamingResponse
@@ -17,15 +19,17 @@ from fastapi.staticfiles import StaticFiles
17
19
  from rich.console import Console
18
20
  from rich.panel import Panel
19
21
 
22
+ from mcp_vector_search import __version__
23
+
20
24
  console = Console()
21
25
 
22
26
 
23
- def find_free_port(start_port: int = 8080, end_port: int = 8099) -> int:
27
+ def find_free_port(start_port: int = 8501, end_port: int = 8599) -> int:
24
28
  """Find a free port in the given range.
25
29
 
26
30
  Args:
27
- start_port: Starting port number to check
28
- end_port: Ending port number to check
31
+ start_port: Starting port number to check (default: 8501)
32
+ end_port: Ending port number to check (default: 8599)
29
33
 
30
34
  Returns:
31
35
  First available port in the range
@@ -80,6 +84,39 @@ def create_app(viz_dir: Path) -> FastAPI:
80
84
  """
81
85
  app = FastAPI(title="MCP Vector Search Visualization")
82
86
 
87
+ @app.get("/api/graph-status")
88
+ async def graph_status() -> Response:
89
+ """Get graph data generation status.
90
+
91
+ Returns:
92
+ JSON response with ready flag and file size
93
+ """
94
+ graph_file = viz_dir / "chunk-graph.json"
95
+
96
+ if not graph_file.exists():
97
+ return Response(
98
+ content='{"ready": false, "size": 0}',
99
+ media_type="application/json",
100
+ headers={"Cache-Control": "no-cache"},
101
+ )
102
+
103
+ try:
104
+ size = graph_file.stat().st_size
105
+ # Consider graph ready if file exists and has content (>100 bytes)
106
+ is_ready = size > 100
107
+ return Response(
108
+ content=f'{{"ready": {str(is_ready).lower()}, "size": {size}}}',
109
+ media_type="application/json",
110
+ headers={"Cache-Control": "no-cache"},
111
+ )
112
+ except Exception as e:
113
+ console.print(f"[red]Error checking graph status: {e}[/red]")
114
+ return Response(
115
+ content='{"ready": false, "size": 0}',
116
+ media_type="application/json",
117
+ headers={"Cache-Control": "no-cache"},
118
+ )
119
+
83
120
  @app.get("/api/graph")
84
121
  async def get_graph_data() -> Response:
85
122
  """Get graph data for D3 tree visualization.
@@ -97,14 +134,12 @@ def create_app(viz_dir: Path) -> FastAPI:
97
134
  )
98
135
 
99
136
  try:
100
- import json
101
-
102
- with open(graph_file) as f:
103
- data = json.load(f)
137
+ with open(graph_file, "rb") as f:
138
+ data = orjson.loads(f.read())
104
139
 
105
- # Return nodes and links
140
+ # Return nodes and links using orjson for fast serialization
106
141
  return Response(
107
- content=json.dumps(
142
+ content=orjson.dumps(
108
143
  {"nodes": data.get("nodes", []), "links": data.get("links", [])}
109
144
  ),
110
145
  media_type="application/json",
@@ -118,6 +153,262 @@ def create_app(viz_dir: Path) -> FastAPI:
118
153
  media_type="application/json",
119
154
  )
120
155
 
156
+ @app.get("/api/relationships/{chunk_id}")
157
+ async def get_chunk_relationships(chunk_id: str) -> Response:
158
+ """Get all relationships for a chunk (semantic + callers) on-demand.
159
+
160
+ Lazy loads relationships when user expands a node, avoiding expensive
161
+ upfront computation. Results are cached in-memory for the session.
162
+
163
+ Args:
164
+ chunk_id: The chunk ID to find relationships for
165
+
166
+ Returns:
167
+ JSON response with semantic neighbors and callers
168
+ """
169
+ graph_file = viz_dir / "chunk-graph.json"
170
+
171
+ if not graph_file.exists():
172
+ return Response(
173
+ content='{"error": "Graph data not found"}',
174
+ status_code=404,
175
+ media_type="application/json",
176
+ )
177
+
178
+ try:
179
+ import ast
180
+
181
+ with open(graph_file, "rb") as f:
182
+ data = orjson.loads(f.read())
183
+
184
+ # Find the target chunk
185
+ target_node = None
186
+ for node in data.get("nodes", []):
187
+ if node.get("id") == chunk_id:
188
+ target_node = node
189
+ break
190
+
191
+ if not target_node:
192
+ return Response(
193
+ content='{"error": "Chunk not found"}',
194
+ status_code=404,
195
+ media_type="application/json",
196
+ )
197
+
198
+ function_name = target_node.get("function_name") or target_node.get(
199
+ "class_name"
200
+ )
201
+ target_file = target_node.get("file_path", "")
202
+ target_content = target_node.get("content", "")
203
+
204
+ # Compute callers (who calls this function)
205
+ callers = []
206
+
207
+ def extract_calls(code: str) -> set[str]:
208
+ calls = set()
209
+ try:
210
+ tree = ast.parse(code)
211
+ for node in ast.walk(tree):
212
+ if isinstance(node, ast.Call):
213
+ if isinstance(node.func, ast.Name):
214
+ calls.add(node.func.id)
215
+ elif isinstance(node.func, ast.Attribute):
216
+ calls.add(node.func.attr)
217
+ except SyntaxError:
218
+ pass
219
+ return calls
220
+
221
+ if function_name:
222
+ for node in data.get("nodes", []):
223
+ if node.get("type") != "chunk":
224
+ continue
225
+ node_file = node.get("file_path", "")
226
+ if node_file == target_file:
227
+ continue
228
+ content = node.get("content", "")
229
+ if function_name in extract_calls(content):
230
+ caller_name = node.get("function_name") or node.get(
231
+ "class_name"
232
+ )
233
+ if caller_name == "__init__":
234
+ continue
235
+ callers.append(
236
+ {
237
+ "id": node.get("id"),
238
+ "name": caller_name
239
+ or f"chunk_{node.get('start_line', 0)}",
240
+ "file": node_file,
241
+ "type": node.get("chunk_type", "code"),
242
+ }
243
+ )
244
+
245
+ # Compute semantic neighbors (similar code)
246
+ # Simple approach: find chunks with similar function names or content overlap
247
+ semantic = []
248
+ target_words = set(target_content.lower().split())
249
+
250
+ for node in data.get("nodes", []):
251
+ if node.get("type") != "chunk" or node.get("id") == chunk_id:
252
+ continue
253
+ content = node.get("content", "")
254
+ node_words = set(content.lower().split())
255
+ # Jaccard similarity
256
+ if target_words and node_words:
257
+ intersection = len(target_words & node_words)
258
+ union = len(target_words | node_words)
259
+ similarity = intersection / union if union > 0 else 0
260
+ if similarity > 0.3: # 30% threshold
261
+ semantic.append(
262
+ {
263
+ "id": node.get("id"),
264
+ "name": node.get("function_name")
265
+ or node.get("class_name")
266
+ or "chunk",
267
+ "file": node.get("file_path", ""),
268
+ "similarity": round(similarity, 2),
269
+ }
270
+ )
271
+
272
+ # Sort by similarity and limit
273
+ semantic.sort(key=lambda x: x["similarity"], reverse=True)
274
+ semantic = semantic[:10]
275
+
276
+ return Response(
277
+ content=orjson.dumps(
278
+ {
279
+ "chunk_id": chunk_id,
280
+ "callers": callers,
281
+ "caller_count": len(callers),
282
+ "semantic": semantic,
283
+ "semantic_count": len(semantic),
284
+ }
285
+ ),
286
+ media_type="application/json",
287
+ headers={"Cache-Control": "max-age=300"},
288
+ )
289
+ except Exception as e:
290
+ console.print(f"[red]Error computing relationships: {e}[/red]")
291
+ return Response(
292
+ content='{"error": "Failed to compute relationships"}',
293
+ status_code=500,
294
+ media_type="application/json",
295
+ )
296
+
297
+ @app.get("/api/callers/{chunk_id}")
298
+ async def get_chunk_callers(chunk_id: str) -> Response:
299
+ """Get callers for a specific code chunk (lazy loaded on-demand).
300
+
301
+ This computes callers for a single chunk instantly instead of
302
+ pre-computing all relationships (which takes 20+ minutes).
303
+
304
+ Args:
305
+ chunk_id: The chunk ID to find callers for
306
+
307
+ Returns:
308
+ JSON response with callers array
309
+ """
310
+ graph_file = viz_dir / "chunk-graph.json"
311
+
312
+ if not graph_file.exists():
313
+ return Response(
314
+ content='{"error": "Graph data not found", "callers": []}',
315
+ status_code=404,
316
+ media_type="application/json",
317
+ )
318
+
319
+ try:
320
+ import ast
321
+
322
+ with open(graph_file, "rb") as f:
323
+ data = orjson.loads(f.read())
324
+
325
+ # Find the target chunk
326
+ target_node = None
327
+ for node in data.get("nodes", []):
328
+ if node.get("id") == chunk_id:
329
+ target_node = node
330
+ break
331
+
332
+ if not target_node:
333
+ return Response(
334
+ content='{"error": "Chunk not found", "callers": []}',
335
+ status_code=404,
336
+ media_type="application/json",
337
+ )
338
+
339
+ # Get the function/class name from the target
340
+ function_name = target_node.get("function_name") or target_node.get(
341
+ "class_name"
342
+ )
343
+ if not function_name:
344
+ return Response(
345
+ content=orjson.dumps({"callers": [], "function_name": None}),
346
+ media_type="application/json",
347
+ )
348
+
349
+ target_file = target_node.get("file_path", "")
350
+
351
+ # Find callers by scanning other chunks
352
+ callers = []
353
+
354
+ def extract_calls(code: str) -> set[str]:
355
+ """Extract function calls from code using AST."""
356
+ calls = set()
357
+ try:
358
+ tree = ast.parse(code)
359
+ for node in ast.walk(tree):
360
+ if isinstance(node, ast.Call):
361
+ if isinstance(node.func, ast.Name):
362
+ calls.add(node.func.id)
363
+ elif isinstance(node.func, ast.Attribute):
364
+ calls.add(node.func.attr)
365
+ except SyntaxError:
366
+ pass
367
+ return calls
368
+
369
+ for node in data.get("nodes", []):
370
+ # Skip non-code chunks and same-file chunks
371
+ if node.get("type") != "chunk":
372
+ continue
373
+ node_file = node.get("file_path", "")
374
+ if node_file == target_file:
375
+ continue
376
+
377
+ # Check if this chunk calls our target function
378
+ content = node.get("content", "")
379
+ if function_name in extract_calls(content):
380
+ caller_name = node.get("function_name") or node.get("class_name")
381
+ if caller_name == "__init__":
382
+ continue # Skip noise
383
+
384
+ callers.append(
385
+ {
386
+ "id": node.get("id"),
387
+ "name": caller_name or f"chunk_{node.get('start_line', 0)}",
388
+ "file": node_file,
389
+ "type": node.get("chunk_type", "code"),
390
+ }
391
+ )
392
+
393
+ return Response(
394
+ content=orjson.dumps(
395
+ {
396
+ "callers": callers,
397
+ "function_name": function_name,
398
+ "count": len(callers),
399
+ }
400
+ ),
401
+ media_type="application/json",
402
+ headers={"Cache-Control": "max-age=300"}, # Cache for 5 minutes
403
+ )
404
+ except Exception as e:
405
+ console.print(f"[red]Error computing callers: {e}[/red]")
406
+ return Response(
407
+ content='{"error": "Failed to compute callers", "callers": []}',
408
+ status_code=500,
409
+ media_type="application/json",
410
+ )
411
+
121
412
  @app.get("/api/chunks")
122
413
  async def get_file_chunks(file_id: str) -> Response:
123
414
  """Get code chunks for a specific file.
@@ -138,10 +429,8 @@ def create_app(viz_dir: Path) -> FastAPI:
138
429
  )
139
430
 
140
431
  try:
141
- import json
142
-
143
- with open(graph_file) as f:
144
- data = json.load(f)
432
+ with open(graph_file, "rb") as f:
433
+ data = orjson.loads(f.read())
145
434
 
146
435
  # Find chunks associated with this file
147
436
  # Look for nodes that have this file as parent via containment links
@@ -159,7 +448,7 @@ def create_app(viz_dir: Path) -> FastAPI:
159
448
  )
160
449
 
161
450
  return Response(
162
- content=json.dumps({"chunks": chunks}),
451
+ content=orjson.dumps({"chunks": chunks}),
163
452
  media_type="application/json",
164
453
  headers={"Cache-Control": "no-cache"},
165
454
  )
@@ -277,7 +566,7 @@ def start_visualization_server(
277
566
  f"URL: [cyan]{url}[/cyan]\n"
278
567
  f"Directory: [dim]{viz_dir}[/dim]\n\n"
279
568
  f"[dim]Press Ctrl+C to stop[/dim]",
280
- title="Server Started",
569
+ title=f"Server Started v{__version__}",
281
570
  border_style="green",
282
571
  )
283
572
  )
@@ -6,6 +6,8 @@ to generate the complete HTML page for the D3.js visualization.
6
6
 
7
7
  import time
8
8
 
9
+ from mcp_vector_search import __build__, __version__
10
+
9
11
  from .scripts import get_all_scripts
10
12
  from .styles import get_all_styles
11
13
 
@@ -43,14 +45,10 @@ def generate_html_template() -> str:
43
45
  <body>
44
46
  <div id="controls">
45
47
  <h1>🔍 Code Tree</h1>
46
-
47
- <div class="search-container">
48
- <input type="text" id="search-input" placeholder="Search nodes..." oninput="handleSearchInput(event)" onkeydown="handleSearchKeydown(event)">
49
- <div id="search-results" class="search-results"></div>
50
- </div>
48
+ <div class="version-badge">v{__version__} (build {__build__})</div>
51
49
 
52
50
  <div class="control-group">
53
- <label style="color: #c9d1d9; margin-bottom: 8px;">Layout Mode</label>
51
+ <label style="color: var(--text-primary); margin-bottom: 8px;">Layout Mode</label>
54
52
  <div class="toggle-switch-container">
55
53
  <span class="toggle-label">Linear</span>
56
54
  <label class="toggle-switch">
@@ -61,6 +59,15 @@ def generate_html_template() -> str:
61
59
  </div>
62
60
  </div>
63
61
 
62
+ <div class="control-group">
63
+ <label style="color: var(--text-primary); margin-bottom: 8px;">Show Files</label>
64
+ <div class="filter-buttons">
65
+ <button class="filter-btn active" data-filter="all" onclick="setFileFilter('all')">All</button>
66
+ <button class="filter-btn" data-filter="code" onclick="setFileFilter('code')">Code</button>
67
+ <button class="filter-btn" data-filter="docs" onclick="setFileFilter('docs')">Docs</button>
68
+ </div>
69
+ </div>
70
+
64
71
  <h3>Legend</h3>
65
72
  <div class="legend">
66
73
  <div class="legend-category">
@@ -127,6 +134,53 @@ def generate_html_template() -> str:
127
134
  </div>
128
135
  </div>
129
136
 
137
+ <!-- Search Section -->
138
+ <h3>🔎 Search</h3>
139
+ <div class="search-container">
140
+ <input type="text" id="search-input" placeholder="Search nodes..." oninput="handleSearchInput(event)" onkeydown="handleSearchKeydown(event)">
141
+ <div id="search-results" class="search-results"></div>
142
+ </div>
143
+
144
+ <!-- Options Section -->
145
+ <h3>📋 Reports</h3>
146
+ <div class="legend" style="margin-top: 8px;">
147
+ <div class="legend-category" style="border-bottom: none;">
148
+ <div class="legend-item report-btn" onclick="showComplexityReport()">
149
+ <span class="report-icon">📊</span>
150
+ <span>Complexity</span>
151
+ </div>
152
+ <div class="legend-item report-btn" onclick="showCodeSmells()">
153
+ <span class="report-icon">🔍</span>
154
+ <span>Code Smells</span>
155
+ </div>
156
+ <div class="legend-item report-btn" onclick="showDependencies()">
157
+ <span class="report-icon">🔗</span>
158
+ <span>Dependencies</span>
159
+ </div>
160
+ <div class="legend-item report-btn" onclick="showTrends()">
161
+ <span class="report-icon">📈</span>
162
+ <span>Trends</span>
163
+ </div>
164
+ <div class="legend-item report-btn" onclick="generateRemediationReport()">
165
+ <span class="report-icon">📋</span>
166
+ <span>Remediation</span>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <h3 style="margin-top: 16px;">Options</h3>
172
+ <div class="legend" style="margin-top: 8px;">
173
+ <div class="legend-category" style="border-bottom: none;">
174
+ <!-- Theme Toggle -->
175
+ <div class="legend-item" style="margin-bottom: 12px; padding: 0;">
176
+ <button class="theme-toggle-icon-btn" onclick="toggleTheme()" title="Toggle dark/light theme">
177
+ <span class="theme-icon" id="theme-icon">🌙</span>
178
+ </button>
179
+ <span style="margin-left: 8px; color: var(--text-secondary); font-size: 12px;">Theme</span>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
130
184
  <div class="stats" id="stats"></div>
131
185
  </div>
132
186