mcp-vector-search 0.12.6__py3-none-any.whl → 1.0.3__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 (65) hide show
  1. mcp_vector_search/__init__.py +2 -2
  2. mcp_vector_search/analysis/__init__.py +64 -0
  3. mcp_vector_search/analysis/collectors/__init__.py +39 -0
  4. mcp_vector_search/analysis/collectors/base.py +164 -0
  5. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  6. mcp_vector_search/analysis/metrics.py +341 -0
  7. mcp_vector_search/analysis/reporters/__init__.py +5 -0
  8. mcp_vector_search/analysis/reporters/console.py +222 -0
  9. mcp_vector_search/cli/commands/analyze.py +408 -0
  10. mcp_vector_search/cli/commands/chat.py +1262 -0
  11. mcp_vector_search/cli/commands/index.py +21 -3
  12. mcp_vector_search/cli/commands/init.py +13 -0
  13. mcp_vector_search/cli/commands/install.py +597 -335
  14. mcp_vector_search/cli/commands/install_old.py +8 -4
  15. mcp_vector_search/cli/commands/mcp.py +78 -6
  16. mcp_vector_search/cli/commands/reset.py +68 -26
  17. mcp_vector_search/cli/commands/search.py +30 -7
  18. mcp_vector_search/cli/commands/setup.py +1133 -0
  19. mcp_vector_search/cli/commands/status.py +37 -2
  20. mcp_vector_search/cli/commands/uninstall.py +276 -357
  21. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  22. mcp_vector_search/cli/commands/visualize/cli.py +276 -0
  23. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  24. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  25. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
  26. mcp_vector_search/cli/commands/visualize/graph_builder.py +714 -0
  27. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  28. mcp_vector_search/cli/commands/visualize/server.py +311 -0
  29. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  30. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  31. mcp_vector_search/cli/commands/visualize/templates/base.py +180 -0
  32. mcp_vector_search/cli/commands/visualize/templates/scripts.py +2507 -0
  33. mcp_vector_search/cli/commands/visualize/templates/styles.py +1313 -0
  34. mcp_vector_search/cli/commands/visualize.py.original +2536 -0
  35. mcp_vector_search/cli/didyoumean.py +22 -2
  36. mcp_vector_search/cli/main.py +115 -159
  37. mcp_vector_search/cli/output.py +24 -8
  38. mcp_vector_search/config/__init__.py +4 -0
  39. mcp_vector_search/config/default_thresholds.yaml +52 -0
  40. mcp_vector_search/config/settings.py +12 -0
  41. mcp_vector_search/config/thresholds.py +185 -0
  42. mcp_vector_search/core/auto_indexer.py +3 -3
  43. mcp_vector_search/core/boilerplate.py +186 -0
  44. mcp_vector_search/core/config_utils.py +394 -0
  45. mcp_vector_search/core/database.py +369 -94
  46. mcp_vector_search/core/exceptions.py +11 -0
  47. mcp_vector_search/core/git_hooks.py +4 -4
  48. mcp_vector_search/core/indexer.py +221 -4
  49. mcp_vector_search/core/llm_client.py +751 -0
  50. mcp_vector_search/core/models.py +3 -0
  51. mcp_vector_search/core/project.py +17 -0
  52. mcp_vector_search/core/scheduler.py +11 -11
  53. mcp_vector_search/core/search.py +179 -29
  54. mcp_vector_search/mcp/server.py +24 -5
  55. mcp_vector_search/utils/__init__.py +2 -0
  56. mcp_vector_search/utils/gitignore_updater.py +212 -0
  57. mcp_vector_search/utils/monorepo.py +66 -4
  58. mcp_vector_search/utils/timing.py +10 -6
  59. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/METADATA +182 -52
  60. mcp_vector_search-1.0.3.dist-info/RECORD +97 -0
  61. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/WHEEL +1 -1
  62. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/entry_points.txt +1 -0
  63. mcp_vector_search/cli/commands/visualize.py +0 -1467
  64. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  65. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,311 @@
1
+ """HTTP server for visualization with streaming JSON support.
2
+
3
+ This module handles running the local HTTP server to serve the
4
+ D3.js visualization interface with chunked transfer for large JSON files.
5
+ """
6
+
7
+ import asyncio
8
+ import socket
9
+ import webbrowser
10
+ from collections.abc import AsyncGenerator
11
+ from pathlib import Path
12
+
13
+ import uvicorn
14
+ from fastapi import FastAPI, Response
15
+ from fastapi.responses import FileResponse, StreamingResponse
16
+ from fastapi.staticfiles import StaticFiles
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+
20
+ console = Console()
21
+
22
+
23
+ def find_free_port(start_port: int = 8080, end_port: int = 8099) -> int:
24
+ """Find a free port in the given range.
25
+
26
+ Args:
27
+ start_port: Starting port number to check
28
+ end_port: Ending port number to check
29
+
30
+ Returns:
31
+ First available port in the range
32
+
33
+ Raises:
34
+ OSError: If no free ports available in range
35
+ """
36
+ for test_port in range(start_port, end_port + 1):
37
+ try:
38
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
39
+ s.bind(("", test_port))
40
+ return test_port
41
+ except OSError:
42
+ continue
43
+ raise OSError(f"No free ports available in range {start_port}-{end_port}")
44
+
45
+
46
+ def create_app(viz_dir: Path) -> FastAPI:
47
+ """Create FastAPI application for visualization server.
48
+
49
+ Args:
50
+ viz_dir: Directory containing visualization files
51
+
52
+ Returns:
53
+ Configured FastAPI application
54
+
55
+ Design Decision: Streaming JSON with chunked transfer
56
+
57
+ Rationale: Safari's JSON.parse() cannot handle 6.3MB files in memory.
58
+ Selected streaming approach to send JSON in 100KB chunks, avoiding
59
+ browser memory limits and parser crashes.
60
+
61
+ Trade-offs:
62
+ - Memory: Constant memory usage vs. 6.3MB loaded at once
63
+ - Complexity: Requires streaming parser vs. simple JSON.parse()
64
+ - Performance: Slightly slower parsing but prevents crashes
65
+
66
+ Alternatives Considered:
67
+ 1. Compress JSON (gzip): Rejected - still requires full parse after decompression
68
+ 2. Split into multiple files: Rejected - requires graph structure changes
69
+ 3. Binary format (protobuf): Rejected - requires major refactoring
70
+
71
+ Error Handling:
72
+ - File not found: Returns 404 with clear error message
73
+ - Read errors: Logs exception and returns 500
74
+ - Connection interruption: Stream closes gracefully
75
+
76
+ Performance:
77
+ - Time: O(n) single file read pass
78
+ - Space: O(1) constant memory (100KB buffer)
79
+ - Expected: <10s for 6.3MB file on localhost
80
+ """
81
+ app = FastAPI(title="MCP Vector Search Visualization")
82
+
83
+ @app.get("/api/graph")
84
+ async def get_graph_data() -> Response:
85
+ """Get graph data for D3 tree visualization.
86
+
87
+ Returns:
88
+ JSON response with nodes and links
89
+ """
90
+ graph_file = viz_dir / "chunk-graph.json"
91
+
92
+ if not graph_file.exists():
93
+ return Response(
94
+ content='{"error": "Graph data not found", "nodes": [], "links": []}',
95
+ status_code=404,
96
+ media_type="application/json",
97
+ )
98
+
99
+ try:
100
+ import json
101
+
102
+ with open(graph_file) as f:
103
+ data = json.load(f)
104
+
105
+ # Return nodes and links
106
+ return Response(
107
+ content=json.dumps(
108
+ {"nodes": data.get("nodes", []), "links": data.get("links", [])}
109
+ ),
110
+ media_type="application/json",
111
+ headers={"Cache-Control": "no-cache"},
112
+ )
113
+ except Exception as e:
114
+ console.print(f"[red]Error loading graph data: {e}[/red]")
115
+ return Response(
116
+ content='{"error": "Failed to load graph data", "nodes": [], "links": []}',
117
+ status_code=500,
118
+ media_type="application/json",
119
+ )
120
+
121
+ @app.get("/api/chunks")
122
+ async def get_file_chunks(file_id: str) -> Response:
123
+ """Get code chunks for a specific file.
124
+
125
+ Args:
126
+ file_id: File node ID
127
+
128
+ Returns:
129
+ JSON response with chunks array
130
+ """
131
+ graph_file = viz_dir / "chunk-graph.json"
132
+
133
+ if not graph_file.exists():
134
+ return Response(
135
+ content='{"error": "Graph data not found", "chunks": []}',
136
+ status_code=404,
137
+ media_type="application/json",
138
+ )
139
+
140
+ try:
141
+ import json
142
+
143
+ with open(graph_file) as f:
144
+ data = json.load(f)
145
+
146
+ # Find chunks associated with this file
147
+ # Look for nodes that have this file as parent via containment links
148
+ chunks = []
149
+ for node in data.get("nodes", []):
150
+ if node.get("type") == "chunk" and node.get("file_id") == file_id:
151
+ chunks.append(
152
+ {
153
+ "id": node.get("id"),
154
+ "type": node.get("chunk_type", "code"),
155
+ "content": node.get("content", ""),
156
+ "start_line": node.get("start_line"),
157
+ "end_line": node.get("end_line"),
158
+ }
159
+ )
160
+
161
+ return Response(
162
+ content=json.dumps({"chunks": chunks}),
163
+ media_type="application/json",
164
+ headers={"Cache-Control": "no-cache"},
165
+ )
166
+ except Exception as e:
167
+ console.print(f"[red]Error loading chunks: {e}[/red]")
168
+ return Response(
169
+ content='{"error": "Failed to load chunks", "chunks": []}',
170
+ status_code=500,
171
+ media_type="application/json",
172
+ )
173
+
174
+ @app.get("/api/graph-data")
175
+ async def stream_graph_data() -> StreamingResponse:
176
+ """Stream chunk-graph.json in 100KB chunks (legacy endpoint).
177
+
178
+ Returns:
179
+ StreamingResponse with chunked transfer encoding
180
+
181
+ Performance:
182
+ - Chunk Size: 100KB (optimal for localhost transfer)
183
+ - Memory: O(1) constant buffer, not O(n) file size
184
+ - Transfer: Progressive, allows incremental parsing
185
+ """
186
+ graph_file = viz_dir / "chunk-graph.json"
187
+
188
+ if not graph_file.exists():
189
+ return Response(
190
+ content='{"error": "Graph data not found"}',
191
+ status_code=404,
192
+ media_type="application/json",
193
+ )
194
+
195
+ async def generate_chunks() -> AsyncGenerator[bytes, None]:
196
+ """Generate 100KB chunks from graph file.
197
+
198
+ Yields:
199
+ Byte chunks of JSON data
200
+ """
201
+ try:
202
+ # Read file in chunks to avoid loading entire file in memory
203
+ chunk_size = 100 * 1024 # 100KB chunks
204
+ with open(graph_file, "rb") as f:
205
+ while chunk := f.read(chunk_size):
206
+ yield chunk
207
+ # Small delay to prevent overwhelming the browser
208
+ await asyncio.sleep(0.01)
209
+ except Exception as e:
210
+ console.print(f"[red]Error streaming graph data: {e}[/red]")
211
+ raise
212
+
213
+ return StreamingResponse(
214
+ generate_chunks(),
215
+ media_type="application/json",
216
+ headers={"Cache-Control": "no-cache", "X-Content-Type-Options": "nosniff"},
217
+ )
218
+
219
+ @app.get("/")
220
+ async def serve_index() -> FileResponse:
221
+ """Serve index.html with no-cache headers to prevent stale content."""
222
+ return FileResponse(
223
+ viz_dir / "index.html",
224
+ headers={
225
+ "Cache-Control": "no-cache, no-store, must-revalidate",
226
+ "Pragma": "no-cache",
227
+ "Expires": "0",
228
+ },
229
+ )
230
+
231
+ # Mount static files AFTER API routes are defined
232
+ # Using /static prefix to avoid conflicts with API routes
233
+ app.mount("/static", StaticFiles(directory=str(viz_dir)), name="static")
234
+
235
+ # Also serve files directly at root level for backward compatibility
236
+ # BUT place this after explicit routes so /api/graph-data works
237
+ @app.get("/{path:path}")
238
+ async def serve_static(path: str) -> FileResponse:
239
+ """Serve static files from visualization directory."""
240
+ file_path = viz_dir / path
241
+ if file_path.exists() and file_path.is_file():
242
+ return FileResponse(file_path)
243
+ # Fallback to index.html for SPA routing
244
+ return FileResponse(
245
+ viz_dir / "index.html",
246
+ headers={
247
+ "Cache-Control": "no-cache, no-store, must-revalidate",
248
+ "Pragma": "no-cache",
249
+ "Expires": "0",
250
+ },
251
+ )
252
+
253
+ return app
254
+
255
+
256
+ def start_visualization_server(
257
+ port: int, viz_dir: Path, auto_open: bool = True
258
+ ) -> None:
259
+ """Start HTTP server for visualization with streaming support.
260
+
261
+ Args:
262
+ port: Port number to use
263
+ viz_dir: Directory containing visualization files
264
+ auto_open: Whether to automatically open browser
265
+
266
+ Raises:
267
+ typer.Exit: If server fails to start
268
+ """
269
+ try:
270
+ app = create_app(viz_dir)
271
+ url = f"http://localhost:{port}"
272
+
273
+ console.print()
274
+ console.print(
275
+ Panel.fit(
276
+ f"[green]✓[/green] Visualization server running\n\n"
277
+ f"URL: [cyan]{url}[/cyan]\n"
278
+ f"Directory: [dim]{viz_dir}[/dim]\n\n"
279
+ f"[dim]Press Ctrl+C to stop[/dim]",
280
+ title="Server Started",
281
+ border_style="green",
282
+ )
283
+ )
284
+
285
+ # Open browser
286
+ if auto_open:
287
+ webbrowser.open(url)
288
+
289
+ # Run server
290
+ config = uvicorn.Config(
291
+ app,
292
+ host="127.0.0.1",
293
+ port=port,
294
+ log_level="warning", # Reduce noise
295
+ access_log=False,
296
+ )
297
+ server = uvicorn.Server(config)
298
+ server.run()
299
+
300
+ except KeyboardInterrupt:
301
+ console.print("\n[yellow]Stopping server...[/yellow]")
302
+ except OSError as e:
303
+ if "Address already in use" in str(e):
304
+ console.print(
305
+ f"[red]✗ Port {port} is already in use. Try a different port with --port[/red]"
306
+ )
307
+ else:
308
+ console.print(f"[red]✗ Server error: {e}[/red]")
309
+ import typer
310
+
311
+ raise typer.Exit(1)