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.
@@ -0,0 +1,3 @@
1
+ """Codebase MCP — persistent, portable codebase intelligence server."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()