codebase-mcp 0.1.0__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.
- codebase_mcp/__init__.py +3 -0
- codebase_mcp/__main__.py +524 -0
- codebase_mcp/config.py +211 -0
- codebase_mcp/db.py +541 -0
- codebase_mcp/exporter.py +243 -0
- codebase_mcp/handoff.py +317 -0
- codebase_mcp/indexer.py +415 -0
- codebase_mcp/models.py +46 -0
- codebase_mcp/parsers/__init__.py +15 -0
- codebase_mcp/parsers/base.py +157 -0
- codebase_mcp/parsers/config_parsers.py +462 -0
- codebase_mcp/parsers/generic.py +95 -0
- codebase_mcp/parsers/go.py +222 -0
- codebase_mcp/parsers/python.py +231 -0
- codebase_mcp/parsers/rust.py +205 -0
- codebase_mcp/parsers/typescript.py +303 -0
- codebase_mcp/parsers/universal.py +625 -0
- codebase_mcp/server.py +1291 -0
- codebase_mcp/watcher.py +169 -0
- codebase_mcp/webui.py +611 -0
- codebase_mcp-0.1.0.dist-info/METADATA +424 -0
- codebase_mcp-0.1.0.dist-info/RECORD +24 -0
- codebase_mcp-0.1.0.dist-info/WHEEL +4 -0
- codebase_mcp-0.1.0.dist-info/entry_points.txt +2 -0
codebase_mcp/__init__.py
ADDED
codebase_mcp/__main__.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for codebase-mcp.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
codebase-mcp setup # Auto-install MCP config in all IDEs (run once)
|
|
6
|
+
codebase-mcp serve # Run MCP server (stdio, for Claude Code / Cursor / Cline)
|
|
7
|
+
codebase-mcp serve --watch # Serve + auto-reindex on file changes
|
|
8
|
+
codebase-mcp index [PATH] # Index a project (or cwd)
|
|
9
|
+
codebase-mcp status [PATH] # Show index status + what changed
|
|
10
|
+
codebase-mcp github URL # Clone any GitHub repo and index it
|
|
11
|
+
codebase-mcp handoff [PATH] # Create portable bundle for agent/IDE switch
|
|
12
|
+
codebase-mcp ui [PATH] # Open the web UI in a browser
|
|
13
|
+
codebase-mcp export [PATH] # Export context to JSON
|
|
14
|
+
codebase-mcp import FILE [PATH] # Import a context snapshot
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
err_console = Console(stderr=True) # for MCP stdio mode (stdout is the MCP protocol wire)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group()
|
|
31
|
+
@click.version_option(package_name="codebase-mcp")
|
|
32
|
+
def cli():
|
|
33
|
+
"""Codebase Intelligence MCP Server — persistent, portable, incremental."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cli.command()
|
|
38
|
+
@click.option("--transport", default="stdio", type=click.Choice(["stdio", "http"]),
|
|
39
|
+
show_default=True, help="MCP transport mode.")
|
|
40
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="HTTP host (http transport only).")
|
|
41
|
+
@click.option("--port", default=8765, show_default=True, help="HTTP port (http transport only).")
|
|
42
|
+
@click.option("--project-root", default="", help="Project root to pre-configure.")
|
|
43
|
+
@click.option("--watch", is_flag=True, help="Start file watcher for auto incremental re-index.")
|
|
44
|
+
def serve(transport: str, host: str, port: int, project_root: str, watch: bool):
|
|
45
|
+
"""Start the MCP server. Use with Claude Code, Cursor, Cline, or any MCP client."""
|
|
46
|
+
from .config import Config, set_config
|
|
47
|
+
root = os.path.abspath(project_root) if project_root else os.getcwd()
|
|
48
|
+
cfg = Config(project_root=root)
|
|
49
|
+
set_config(cfg)
|
|
50
|
+
|
|
51
|
+
if watch:
|
|
52
|
+
from .watcher import start_watcher, WatcherNotAvailable
|
|
53
|
+
try:
|
|
54
|
+
start_watcher(root, cfg.db_path, cfg)
|
|
55
|
+
err_console.print(f"[dim]File watcher active on {root}[/dim]")
|
|
56
|
+
except WatcherNotAvailable as e:
|
|
57
|
+
err_console.print(f"[yellow]Watch disabled: {e}[/yellow]")
|
|
58
|
+
|
|
59
|
+
from .server import mcp
|
|
60
|
+
if transport == "stdio":
|
|
61
|
+
err_console.print("[dim]codebase-mcp running on stdio[/dim]")
|
|
62
|
+
mcp.run(transport="stdio")
|
|
63
|
+
else:
|
|
64
|
+
err_console.print(f"[dim]codebase-mcp HTTP server on {host}:{port}[/dim]")
|
|
65
|
+
mcp.run(transport="streamable-http", host=host, port=port)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@cli.command()
|
|
69
|
+
@click.argument("path", default="", required=False)
|
|
70
|
+
@click.option("--full", is_flag=True, help="Force full re-index (ignore cached hashes).")
|
|
71
|
+
@click.option("--exclude", multiple=True, help="Additional glob patterns to exclude.")
|
|
72
|
+
def index(path: str, full: bool, exclude: tuple):
|
|
73
|
+
"""Index a project directory (defaults to current directory)."""
|
|
74
|
+
from .config import Config, set_config
|
|
75
|
+
from .indexer import run_index
|
|
76
|
+
|
|
77
|
+
root = os.path.abspath(path or os.getcwd())
|
|
78
|
+
cfg = Config(project_root=root, exclude_patterns=[])
|
|
79
|
+
# Merge defaults + custom excludes
|
|
80
|
+
from .config import DEFAULT_EXCLUDE_PATTERNS
|
|
81
|
+
cfg.exclude_patterns = list(DEFAULT_EXCLUDE_PATTERNS) + list(exclude)
|
|
82
|
+
set_config(cfg)
|
|
83
|
+
|
|
84
|
+
console.print(f"[bold]Indexing[/bold] {root} {'(full re-index)' if full else '(incremental)'}...")
|
|
85
|
+
|
|
86
|
+
result = run_index(cfg.db_path, root, cfg, full_reindex=full)
|
|
87
|
+
|
|
88
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
89
|
+
table.add_row("[cyan]Files scanned[/cyan]", str(result.files_scanned))
|
|
90
|
+
table.add_row("[cyan]Files changed[/cyan]", str(result.files_changed))
|
|
91
|
+
table.add_row("[cyan]Files re-parsed[/cyan]", str(result.files_reparsed))
|
|
92
|
+
table.add_row("[cyan]Files deleted[/cyan]", str(result.files_deleted))
|
|
93
|
+
table.add_row("[cyan]Files errored[/cyan]", str(result.files_errored))
|
|
94
|
+
table.add_row("[cyan]Symbols added[/cyan]", str(result.symbols_added))
|
|
95
|
+
table.add_row("[cyan]Duration[/cyan]", f"{result.duration_ms:.0f}ms")
|
|
96
|
+
table.add_row("[cyan]Database[/cyan]", cfg.db_path)
|
|
97
|
+
console.print(table)
|
|
98
|
+
|
|
99
|
+
if result.errors:
|
|
100
|
+
console.print(f"\n[yellow]Parse errors ({len(result.errors)}):[/yellow]")
|
|
101
|
+
for e in result.errors[:10]:
|
|
102
|
+
console.print(f" [red]{e['path']}[/red]: {e['error']}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command()
|
|
106
|
+
@click.argument("path", default="", required=False)
|
|
107
|
+
def status(path: str):
|
|
108
|
+
"""Show index status and staleness for a project."""
|
|
109
|
+
from .config import Config, set_config
|
|
110
|
+
from .db import open_db, get_all_meta, get_stats
|
|
111
|
+
from .indexer import find_stale_files
|
|
112
|
+
|
|
113
|
+
root = os.path.abspath(path or os.getcwd())
|
|
114
|
+
cfg = Config(project_root=root)
|
|
115
|
+
set_config(cfg)
|
|
116
|
+
|
|
117
|
+
if not os.path.exists(cfg.db_path):
|
|
118
|
+
console.print(f"[yellow]No index found at {cfg.db_path}[/yellow]")
|
|
119
|
+
console.print("Run [bold]codebase-mcp index[/bold] to create one.")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
conn = open_db(cfg.db_path)
|
|
123
|
+
meta = get_all_meta(conn)
|
|
124
|
+
stats = get_stats(conn)
|
|
125
|
+
stale = find_stale_files(conn, root, cfg)
|
|
126
|
+
db_size = os.path.getsize(cfg.db_path)
|
|
127
|
+
|
|
128
|
+
console.print(f"\n[bold]Codebase Index Status[/bold] — {root}\n")
|
|
129
|
+
|
|
130
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
131
|
+
table.add_row("Project", meta.get("project_name", "(unnamed)"))
|
|
132
|
+
table.add_row("Last indexed", meta.get("last_indexed", "never"))
|
|
133
|
+
table.add_row("Database", cfg.db_path)
|
|
134
|
+
table.add_row("DB size", f"{db_size:,} bytes")
|
|
135
|
+
table.add_row("Files indexed", str(stats["files_indexed"]))
|
|
136
|
+
table.add_row("Symbols total", str(stats["symbols_total"]))
|
|
137
|
+
table.add_row("Functions", str(stats["functions"]))
|
|
138
|
+
table.add_row("Classes", str(stats["classes"]))
|
|
139
|
+
table.add_row("Decisions", str(stats["decisions_active"]))
|
|
140
|
+
table.add_row("Notes", str(stats["notes"]))
|
|
141
|
+
table.add_row("Languages", ", ".join(f"{k}:{v}" for k, v in stats["language_breakdown"].items()))
|
|
142
|
+
console.print(table)
|
|
143
|
+
|
|
144
|
+
if stale:
|
|
145
|
+
console.print(f"\n[yellow]Index is stale[/yellow] — {len(stale)} file(s) changed:")
|
|
146
|
+
for f in stale[:15]:
|
|
147
|
+
console.print(f" [dim]{f}[/dim]")
|
|
148
|
+
if len(stale) > 15:
|
|
149
|
+
console.print(f" ... and {len(stale) - 15} more")
|
|
150
|
+
console.print("\nRun [bold]codebase-mcp index[/bold] to update.")
|
|
151
|
+
else:
|
|
152
|
+
console.print("\n[green]Index is fresh.[/green]")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command("export")
|
|
156
|
+
@click.argument("path", default="", required=False)
|
|
157
|
+
@click.option("--output", "-o", default="", help="Output file path.")
|
|
158
|
+
@click.option("--no-index", is_flag=True, help="Skip the structural index (decisions+notes only).")
|
|
159
|
+
@click.option("--compress", "-z", is_flag=True, help="Gzip-compress the output.")
|
|
160
|
+
def export_cmd(path: str, output: str, no_index: bool, compress: bool):
|
|
161
|
+
"""Export the full context to a portable JSON snapshot."""
|
|
162
|
+
from .config import Config, set_config
|
|
163
|
+
from .exporter import export_context as _export
|
|
164
|
+
|
|
165
|
+
root = os.path.abspath(path or os.getcwd())
|
|
166
|
+
cfg = Config(project_root=root)
|
|
167
|
+
set_config(cfg)
|
|
168
|
+
|
|
169
|
+
result = _export(
|
|
170
|
+
cfg.db_path, output or None,
|
|
171
|
+
include_index=not no_index,
|
|
172
|
+
include_decisions=True,
|
|
173
|
+
include_notes=True,
|
|
174
|
+
compress=compress,
|
|
175
|
+
)
|
|
176
|
+
console.print(f"[green]Exported[/green] → {result['path']}")
|
|
177
|
+
console.print(f" Size: {result['size_bytes']:,} bytes")
|
|
178
|
+
for k, v in result.items():
|
|
179
|
+
if k.endswith("_exported"):
|
|
180
|
+
console.print(f" {k.replace('_exported', '')}: {v}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@cli.command("import")
|
|
184
|
+
@click.argument("import_file")
|
|
185
|
+
@click.argument("path", default="", required=False)
|
|
186
|
+
@click.option("--replace", is_flag=True, help="Replace existing decisions instead of merging.")
|
|
187
|
+
@click.option("--with-index", is_flag=True, help="Also import the structural index.")
|
|
188
|
+
def import_cmd(import_file: str, path: str, replace: bool, with_index: bool):
|
|
189
|
+
"""Import a context snapshot from a JSON export file."""
|
|
190
|
+
from .config import Config, set_config
|
|
191
|
+
from .exporter import import_context as _import
|
|
192
|
+
|
|
193
|
+
root = os.path.abspath(path or os.getcwd())
|
|
194
|
+
cfg = Config(project_root=root)
|
|
195
|
+
set_config(cfg)
|
|
196
|
+
|
|
197
|
+
result = _import(
|
|
198
|
+
cfg.db_path, import_file,
|
|
199
|
+
merge_decisions=not replace,
|
|
200
|
+
reimport_index=with_index,
|
|
201
|
+
)
|
|
202
|
+
console.print(f"[green]Imported[/green] from {import_file}")
|
|
203
|
+
for k, v in result.get("imported", {}).items():
|
|
204
|
+
console.print(f" {k}: +{v}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@cli.command("handoff")
|
|
208
|
+
@click.argument("path", default="", required=False)
|
|
209
|
+
@click.option("--output", "-o", default="", help="Output directory path. Default: auto-named in exports/.")
|
|
210
|
+
@click.option("--zip", "as_zip", is_flag=True, help="Package as a .zip file instead of a directory.")
|
|
211
|
+
@click.option("--no-index", is_flag=True, help="Omit the structural index (decisions+notes only).")
|
|
212
|
+
def handoff_cmd(path: str, output: str, as_zip: bool, no_index: bool):
|
|
213
|
+
"""Create a portable handoff bundle for transferring context to a new agent or IDE."""
|
|
214
|
+
from .config import Config, set_config
|
|
215
|
+
from .handoff import create_handoff_bundle
|
|
216
|
+
|
|
217
|
+
root = os.path.abspath(path or os.getcwd())
|
|
218
|
+
cfg = Config(project_root=root)
|
|
219
|
+
set_config(cfg)
|
|
220
|
+
|
|
221
|
+
console.print(f"[bold]Creating handoff bundle[/bold] for {root}...")
|
|
222
|
+
result = create_handoff_bundle(
|
|
223
|
+
cfg.db_path, root,
|
|
224
|
+
output_dir=output or None,
|
|
225
|
+
as_zip=as_zip,
|
|
226
|
+
include_index=not no_index,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
console.print(f"[green]Handoff bundle created[/green] -> {result['path']}")
|
|
230
|
+
console.print(f" Decisions: {result['decisions']}")
|
|
231
|
+
stats = result.get("stats", {})
|
|
232
|
+
console.print(f" Files indexed: {stats.get('files_indexed', 0)}")
|
|
233
|
+
console.print(f" Symbols: {stats.get('symbols_total', 0)}")
|
|
234
|
+
if as_zip:
|
|
235
|
+
console.print(f"\n[dim]Share {result['path']} with your next agent.[/dim]")
|
|
236
|
+
else:
|
|
237
|
+
console.print(f"\n[dim]The receiving agent should run:[/dim]")
|
|
238
|
+
console.print(f" [bold]codebase-mcp import {result['path']}/context.json[/bold]")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@cli.command("ui")
|
|
242
|
+
@click.argument("path", default="", required=False)
|
|
243
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="Host to bind to.")
|
|
244
|
+
@click.option("--port", default=8766, show_default=True, help="Port to listen on.")
|
|
245
|
+
@click.option("--no-browser", is_flag=True, help="Don't auto-open the browser.")
|
|
246
|
+
def ui_cmd(path: str, host: str, port: int, no_browser: bool):
|
|
247
|
+
"""Open the web UI for browsing symbols, decisions, and notes."""
|
|
248
|
+
from .config import Config, set_config
|
|
249
|
+
from .webui import start_ui_server
|
|
250
|
+
|
|
251
|
+
root = os.path.abspath(path or os.getcwd())
|
|
252
|
+
cfg = Config(project_root=root)
|
|
253
|
+
set_config(cfg)
|
|
254
|
+
|
|
255
|
+
url = f"http://{host}:{port}"
|
|
256
|
+
console.print(f"[bold]Web UI[/bold] starting at {url}")
|
|
257
|
+
console.print(f" Project: {root}")
|
|
258
|
+
console.print(f" Database: {cfg.db_path}")
|
|
259
|
+
console.print(" Press Ctrl+C to stop.\n")
|
|
260
|
+
|
|
261
|
+
if not no_browser:
|
|
262
|
+
import threading
|
|
263
|
+
import webbrowser
|
|
264
|
+
threading.Timer(0.8, lambda: webbrowser.open(url)).start()
|
|
265
|
+
|
|
266
|
+
start_ui_server(cfg.db_path, host=host, port=port)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@cli.command("github")
|
|
270
|
+
@click.argument("url")
|
|
271
|
+
@click.option("--output", "-o", default="", help="Directory to clone into. Default: ~/codebase-mcp-repos/<repo-name>.")
|
|
272
|
+
@click.option("--branch", "-b", default="", help="Branch or tag to checkout. Default: repo default branch.")
|
|
273
|
+
@click.option("--depth", default=1, show_default=True, help="Git clone depth. 1 = shallow (fast). 0 = full history.")
|
|
274
|
+
@click.option("--handoff", "make_handoff", is_flag=True, help="Also create a handoff bundle after indexing.")
|
|
275
|
+
@click.option("--full", is_flag=True, help="Force full re-index (useful if repo was already cloned).")
|
|
276
|
+
def github_cmd(url: str, output: str, branch: str, depth: int, make_handoff: bool, full: bool):
|
|
277
|
+
"""
|
|
278
|
+
Clone a GitHub (or any git) repo and index it immediately.
|
|
279
|
+
|
|
280
|
+
Examples:\n
|
|
281
|
+
codebase-mcp github https://github.com/owner/repo\n
|
|
282
|
+
codebase-mcp github https://github.com/owner/repo --branch dev --handoff\n
|
|
283
|
+
codebase-mcp github git@github.com:owner/repo.git --output /tmp/myrepo
|
|
284
|
+
"""
|
|
285
|
+
import re
|
|
286
|
+
import subprocess
|
|
287
|
+
from .config import Config, set_config, DEFAULT_EXCLUDE_PATTERNS
|
|
288
|
+
from .indexer import run_index
|
|
289
|
+
|
|
290
|
+
# Derive a clean repo name from the URL
|
|
291
|
+
repo_name = re.sub(r"\.git$", "", url.rstrip("/").split("/")[-1])
|
|
292
|
+
if not repo_name:
|
|
293
|
+
console.print("[red]Could not parse repo name from URL.[/red]")
|
|
294
|
+
raise click.Abort()
|
|
295
|
+
|
|
296
|
+
# Determine clone destination
|
|
297
|
+
if output:
|
|
298
|
+
clone_dir = os.path.abspath(output)
|
|
299
|
+
else:
|
|
300
|
+
default_base = os.path.join(os.path.expanduser("~"), "codebase-mcp-repos")
|
|
301
|
+
clone_dir = os.path.join(default_base, repo_name)
|
|
302
|
+
|
|
303
|
+
# Clone or update
|
|
304
|
+
if os.path.exists(os.path.join(clone_dir, ".git")):
|
|
305
|
+
console.print(f"[dim]Repo already cloned at {clone_dir} — pulling latest...[/dim]")
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
["git", "-C", clone_dir, "pull", "--ff-only"],
|
|
308
|
+
capture_output=True, text=True,
|
|
309
|
+
)
|
|
310
|
+
if result.returncode != 0:
|
|
311
|
+
console.print(f"[yellow]git pull failed (may be detached HEAD): {result.stderr.strip()}[/yellow]")
|
|
312
|
+
console.print("[dim]Continuing with existing clone.[/dim]")
|
|
313
|
+
else:
|
|
314
|
+
os.makedirs(os.path.dirname(clone_dir), exist_ok=True)
|
|
315
|
+
|
|
316
|
+
# On Windows, enable long path support before cloning (avoids 260-char limit)
|
|
317
|
+
if sys.platform == "win32":
|
|
318
|
+
subprocess.run(["git", "config", "--global", "core.longpaths", "true"],
|
|
319
|
+
capture_output=True)
|
|
320
|
+
|
|
321
|
+
cmd = ["git", "clone"]
|
|
322
|
+
if depth > 0:
|
|
323
|
+
cmd += ["--depth", str(depth)]
|
|
324
|
+
if branch:
|
|
325
|
+
cmd += ["--branch", branch, "--single-branch"]
|
|
326
|
+
cmd += [url, clone_dir]
|
|
327
|
+
|
|
328
|
+
console.print(f"[bold]Cloning[/bold] {url}")
|
|
329
|
+
console.print(f" Into: {clone_dir}")
|
|
330
|
+
if depth > 0:
|
|
331
|
+
console.print(f" [dim](shallow clone --depth {depth} for speed)[/dim]")
|
|
332
|
+
|
|
333
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
334
|
+
if result.returncode != 0:
|
|
335
|
+
# If checkout partially failed (e.g. long filenames), warn but continue
|
|
336
|
+
# if the .git dir was at least created
|
|
337
|
+
if os.path.exists(os.path.join(clone_dir, ".git")):
|
|
338
|
+
console.print(f"[yellow]Clone warning (partial checkout): {result.stderr.strip()[:300]}[/yellow]")
|
|
339
|
+
console.print("[dim]Indexing files that were successfully checked out...[/dim]")
|
|
340
|
+
else:
|
|
341
|
+
console.print(f"[red]git clone failed:[/red]\n{result.stderr.strip()}")
|
|
342
|
+
raise click.Abort()
|
|
343
|
+
|
|
344
|
+
console.print(f"[green]Cloned[/green] -> {clone_dir}\n")
|
|
345
|
+
|
|
346
|
+
# Index it
|
|
347
|
+
cfg = Config(project_root=clone_dir, exclude_patterns=list(DEFAULT_EXCLUDE_PATTERNS))
|
|
348
|
+
set_config(cfg)
|
|
349
|
+
|
|
350
|
+
console.print(f"[bold]Indexing[/bold] {repo_name} {'(full)' if full else '(incremental)'}...")
|
|
351
|
+
idx = run_index(cfg.db_path, clone_dir, cfg, full_reindex=full)
|
|
352
|
+
|
|
353
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
354
|
+
table.add_row("[cyan]Files scanned[/cyan]", str(idx.files_scanned))
|
|
355
|
+
table.add_row("[cyan]Files parsed[/cyan]", str(idx.files_reparsed))
|
|
356
|
+
table.add_row("[cyan]Symbols found[/cyan]", str(idx.symbols_added))
|
|
357
|
+
table.add_row("[cyan]Duration[/cyan]", f"{idx.duration_ms:.0f}ms")
|
|
358
|
+
table.add_row("[cyan]Database[/cyan]", cfg.db_path)
|
|
359
|
+
console.print(table)
|
|
360
|
+
|
|
361
|
+
if idx.errors:
|
|
362
|
+
console.print(f"\n[yellow]Parse errors ({len(idx.errors)}):[/yellow]")
|
|
363
|
+
for e in idx.errors[:5]:
|
|
364
|
+
console.print(f" [red]{e['path']}[/red]: {e['error']}")
|
|
365
|
+
|
|
366
|
+
# Optionally create handoff bundle
|
|
367
|
+
if make_handoff:
|
|
368
|
+
from .handoff import create_handoff_bundle
|
|
369
|
+
console.print("\n[bold]Creating handoff bundle...[/bold]")
|
|
370
|
+
bundle = create_handoff_bundle(cfg.db_path, clone_dir)
|
|
371
|
+
console.print(f"[green]Handoff bundle[/green] -> {bundle['path']}")
|
|
372
|
+
|
|
373
|
+
console.print(f"""
|
|
374
|
+
[bold]Done.[/bold] To use this index:
|
|
375
|
+
|
|
376
|
+
[bold]MCP server:[/bold]
|
|
377
|
+
codebase-mcp serve --project-root {clone_dir}
|
|
378
|
+
|
|
379
|
+
[bold]Web UI:[/bold]
|
|
380
|
+
codebase-mcp ui {clone_dir}
|
|
381
|
+
|
|
382
|
+
[bold]Handoff bundle (if not created above):[/bold]
|
|
383
|
+
codebase-mcp handoff {clone_dir}
|
|
384
|
+
""")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@cli.command("setup")
|
|
388
|
+
@click.argument("path", default="", required=False)
|
|
389
|
+
@click.option("--ide", default="all",
|
|
390
|
+
type=click.Choice(["all", "claude-code", "cursor", "windsurf", "vscode", "cline", "zed", "print"]),
|
|
391
|
+
show_default=True, help="Which IDE/agent to generate config for.")
|
|
392
|
+
@click.option("--global", "is_global", is_flag=True,
|
|
393
|
+
help="Write to global IDE config instead of project-local.")
|
|
394
|
+
def setup_cmd(path: str, ide: str, is_global: bool):
|
|
395
|
+
"""
|
|
396
|
+
Generate and install MCP server config for your IDE or agent.
|
|
397
|
+
|
|
398
|
+
Supports: Claude Code, Cursor, Windsurf, VS Code (with Cline/Continue), Zed.
|
|
399
|
+
Run once per project (or with --global for all projects).
|
|
400
|
+
|
|
401
|
+
Examples:\n
|
|
402
|
+
codebase-mcp setup # Install for all supported IDEs\n
|
|
403
|
+
codebase-mcp setup --ide cursor # Cursor only\n
|
|
404
|
+
codebase-mcp setup --ide claude-code --global # Claude Code global config\n
|
|
405
|
+
codebase-mcp setup --ide print # Just print the config, don't write
|
|
406
|
+
"""
|
|
407
|
+
import shutil
|
|
408
|
+
|
|
409
|
+
root = os.path.abspath(path or os.getcwd())
|
|
410
|
+
|
|
411
|
+
# Find the codebase-mcp executable
|
|
412
|
+
mcp_exe = shutil.which("codebase-mcp") or "codebase-mcp"
|
|
413
|
+
python_exe = sys.executable
|
|
414
|
+
|
|
415
|
+
# MCP server config block (works for all IDEs)
|
|
416
|
+
server_config = {
|
|
417
|
+
"command": python_exe,
|
|
418
|
+
"args": ["-m", "codebase_mcp", "serve", "--project-root", root],
|
|
419
|
+
"env": {},
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
ide_configs = {
|
|
423
|
+
"claude-code": {
|
|
424
|
+
"file": os.path.join(os.path.expanduser("~"), ".claude", "settings.json") if is_global
|
|
425
|
+
else os.path.join(root, ".claude", "settings.json"),
|
|
426
|
+
"format": "claude",
|
|
427
|
+
"description": "Claude Code (Anthropic CLI)",
|
|
428
|
+
},
|
|
429
|
+
"cursor": {
|
|
430
|
+
"file": os.path.join(os.path.expanduser("~"), ".cursor", "mcp.json") if is_global
|
|
431
|
+
else os.path.join(root, ".cursor", "mcp.json"),
|
|
432
|
+
"format": "cursor",
|
|
433
|
+
"description": "Cursor IDE",
|
|
434
|
+
},
|
|
435
|
+
"windsurf": {
|
|
436
|
+
"file": os.path.join(os.path.expanduser("~"), ".codeium", "windsurf", "mcp_config.json"),
|
|
437
|
+
"format": "windsurf",
|
|
438
|
+
"description": "Windsurf (Codeium)",
|
|
439
|
+
},
|
|
440
|
+
"vscode": {
|
|
441
|
+
"file": os.path.join(root, ".vscode", "mcp.json"),
|
|
442
|
+
"format": "vscode",
|
|
443
|
+
"description": "VS Code (with MCP extension / Cline / Continue)",
|
|
444
|
+
},
|
|
445
|
+
"cline": {
|
|
446
|
+
"file": os.path.join(os.path.expanduser("~"), ".vscode", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
447
|
+
"format": "cline",
|
|
448
|
+
"description": "Cline (VS Code extension)",
|
|
449
|
+
},
|
|
450
|
+
"zed": {
|
|
451
|
+
"file": os.path.join(os.path.expanduser("~"), ".config", "zed", "settings.json"),
|
|
452
|
+
"format": "zed",
|
|
453
|
+
"description": "Zed editor",
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
targets = list(ide_configs.keys()) if ide in ("all", "print") else [ide]
|
|
458
|
+
|
|
459
|
+
console.print(f"\n[bold]codebase-mcp setup[/bold] for project: {root}\n")
|
|
460
|
+
|
|
461
|
+
for target in targets:
|
|
462
|
+
cfg_info = ide_configs[target]
|
|
463
|
+
config_file = cfg_info["file"]
|
|
464
|
+
desc = cfg_info["description"]
|
|
465
|
+
fmt = cfg_info["format"]
|
|
466
|
+
|
|
467
|
+
# Build the config snippet for this IDE
|
|
468
|
+
if fmt == "claude":
|
|
469
|
+
snippet = {"mcpServers": {"codebase-intel": server_config}}
|
|
470
|
+
elif fmt in ("cursor", "windsurf", "cline"):
|
|
471
|
+
snippet = {"mcpServers": {"codebase-intel": server_config}}
|
|
472
|
+
elif fmt == "vscode":
|
|
473
|
+
snippet = {"servers": {"codebase-intel": {"type": "stdio", **server_config}}}
|
|
474
|
+
elif fmt == "zed":
|
|
475
|
+
snippet = {"context_servers": {"codebase-intel": {"command": {"path": python_exe, "args": server_config["args"]}}}}
|
|
476
|
+
else:
|
|
477
|
+
snippet = {"mcpServers": {"codebase-intel": server_config}}
|
|
478
|
+
|
|
479
|
+
if ide == "print":
|
|
480
|
+
console.print(f"[bold cyan]{desc}[/bold cyan] ({config_file})")
|
|
481
|
+
console.print(json.dumps(snippet, indent=2))
|
|
482
|
+
console.print()
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# Read existing config and merge
|
|
486
|
+
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
|
487
|
+
existing = {}
|
|
488
|
+
if os.path.exists(config_file):
|
|
489
|
+
try:
|
|
490
|
+
with open(config_file, encoding="utf-8") as f:
|
|
491
|
+
existing = json.load(f)
|
|
492
|
+
except (json.JSONDecodeError, OSError):
|
|
493
|
+
pass # Overwrite corrupt config
|
|
494
|
+
|
|
495
|
+
# Deep merge at the mcpServers / servers / context_servers level
|
|
496
|
+
for top_key, inner in snippet.items():
|
|
497
|
+
if top_key not in existing:
|
|
498
|
+
existing[top_key] = {}
|
|
499
|
+
existing[top_key].update(inner)
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
503
|
+
json.dump(existing, f, indent=2)
|
|
504
|
+
console.print(f"[green]OK[/green] {desc}")
|
|
505
|
+
console.print(f" {config_file}")
|
|
506
|
+
except OSError as e:
|
|
507
|
+
console.print(f"[yellow]SKIP[/yellow] {desc}: {e}")
|
|
508
|
+
|
|
509
|
+
console.print(f"""
|
|
510
|
+
[bold]Done.[/bold] The MCP server entry is named [cyan]codebase-intel[/cyan].
|
|
511
|
+
|
|
512
|
+
To verify in Claude Code:
|
|
513
|
+
/mcp
|
|
514
|
+
|
|
515
|
+
To verify in Cursor/Windsurf:
|
|
516
|
+
Open MCP settings and look for codebase-intel.
|
|
517
|
+
|
|
518
|
+
The server starts automatically when your IDE opens.
|
|
519
|
+
First use in a new session: call session_bootstrap() to load full context.
|
|
520
|
+
""")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
if __name__ == "__main__":
|
|
524
|
+
cli()
|