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.
- mcp_vector_search/__init__.py +2 -2
- mcp_vector_search/analysis/__init__.py +64 -0
- mcp_vector_search/analysis/collectors/__init__.py +39 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/metrics.py +341 -0
- mcp_vector_search/analysis/reporters/__init__.py +5 -0
- mcp_vector_search/analysis/reporters/console.py +222 -0
- mcp_vector_search/cli/commands/analyze.py +408 -0
- mcp_vector_search/cli/commands/chat.py +1262 -0
- mcp_vector_search/cli/commands/index.py +21 -3
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +30 -7
- mcp_vector_search/cli/commands/setup.py +1133 -0
- mcp_vector_search/cli/commands/status.py +37 -2
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +276 -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 +714 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +311 -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 +180 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2507 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1313 -0
- mcp_vector_search/cli/commands/visualize.py.original +2536 -0
- mcp_vector_search/cli/didyoumean.py +22 -2
- mcp_vector_search/cli/main.py +115 -159
- mcp_vector_search/cli/output.py +24 -8
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +185 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +369 -94
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +221 -4
- mcp_vector_search/core/llm_client.py +751 -0
- mcp_vector_search/core/models.py +3 -0
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +24 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/METADATA +182 -52
- mcp_vector_search-1.0.3.dist-info/RECORD +97 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {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)
|