sourcecode 1.30.30__py3-none-any.whl → 1.31.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.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.30.30"
3
+ __version__ = "1.31.0"
sourcecode/cli.py CHANGED
@@ -154,6 +154,10 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
154
154
 
155
155
  [bold]Subcommands:[/bold]
156
156
  prepare-context TASK [PATH] [dim]# task-specific context (onboard, delta, fix-bug, ...)[/dim]
157
+ mcp init [dim]# setup MCP integration (Claude Desktop, Cursor)[/dim]
158
+ mcp status [dim]# show MCP integration status[/dim]
159
+ mcp remove [dim]# remove MCP integration safely[/dim]
160
+ mcp serve [dim]# start MCP server for AI agent integration[/dim]
157
161
  telemetry status|enable|disable
158
162
  version
159
163
  """
@@ -161,7 +165,7 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
161
165
  # Known subcommand names — tokens matching these are routed as subcommands,
162
166
  # not consumed as a repository path.
163
167
  _SUBCOMMANDS: frozenset[str] = frozenset(
164
- {"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir"}
168
+ {"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir", "mcp"}
165
169
  )
166
170
 
167
171
  # Mutable container holding the path extracted by _preprocess_argv().
@@ -300,6 +304,9 @@ except Exception:
300
304
  telemetry_app = typer.Typer(help="Manage anonymous telemetry (opt-in).", rich_markup_mode="rich")
301
305
  app.add_typer(telemetry_app, name="telemetry")
302
306
 
307
+ mcp_app = typer.Typer(help="MCP integration: setup, status, serve, remove.", rich_markup_mode="rich")
308
+ app.add_typer(mcp_app, name="mcp")
309
+
303
310
 
304
311
  def _maybe_ask_consent() -> None:
305
312
  """Show first-run consent prompt once, on interactive TTYs only."""
@@ -317,6 +324,26 @@ def _maybe_ask_consent() -> None:
317
324
  pass
318
325
 
319
326
 
327
+ def _maybe_show_mcp_hint() -> None:
328
+ """Show MCP integration hint once after first install, on TTY only."""
329
+ import sys as _sys
330
+ try:
331
+ if not _sys.stderr.isatty():
332
+ return
333
+ from sourcecode.telemetry.config import _CONFIG_FILE, _load, _save
334
+ data = _load()
335
+ if data.get("mcp", {}).get("hint_shown"):
336
+ return
337
+ typer.echo("", err=True)
338
+ typer.echo(" MCP integration available:", err=True)
339
+ typer.echo(" → sourcecode mcp init", err=True)
340
+ typer.echo("", err=True)
341
+ data.setdefault("mcp", {})["hint_shown"] = True
342
+ _save(data)
343
+ except Exception:
344
+ pass
345
+
346
+
320
347
  def _active_flags(
321
348
  dependencies: bool, graph_modules: bool, docs: bool, full_metrics: bool,
322
349
  semantics: bool, architecture: bool, git_context: bool, env_map: bool,
@@ -616,6 +643,7 @@ def main(
616
643
  # First-run consent (skip for telemetry/version/config subcommands)
617
644
  if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
618
645
  _maybe_ask_consent()
646
+ _maybe_show_mcp_hint()
619
647
 
620
648
  # When a subcommand is invoked, skip the main analysis.
621
649
  if ctx.invoked_subcommand is not None:
@@ -2432,6 +2460,249 @@ def analyze_cmd(
2432
2460
  raise typer.Exit(code=1)
2433
2461
 
2434
2462
 
2463
+ # ── MCP server ────────────────────────────────────────────────────────────────
2464
+
2465
+ @mcp_app.command("serve")
2466
+ def mcp_serve() -> None:
2467
+ """Start the MCP server on stdio for AI agent integration.
2468
+
2469
+ \b
2470
+ Requires the 'mcp' extra:
2471
+ pip install sourcecode[mcp]
2472
+
2473
+ \b
2474
+ Configure in your MCP client (e.g. Claude Desktop):
2475
+ {
2476
+ "sourcecode": {
2477
+ "command": "sourcecode",
2478
+ "args": ["mcp", "serve"]
2479
+ }
2480
+ }
2481
+ """
2482
+ import logging
2483
+ import sys as _sys
2484
+
2485
+ logging.basicConfig(
2486
+ stream=_sys.stderr,
2487
+ level=logging.INFO,
2488
+ format="[sourcecode-mcp] %(levelname)s %(message)s",
2489
+ )
2490
+ try:
2491
+ from sourcecode.mcp.server import mcp as _mcp
2492
+ except ImportError:
2493
+ typer.echo(
2494
+ "MCP support not available. Install with:\n"
2495
+ " pip install sourcecode[mcp]",
2496
+ err=True,
2497
+ )
2498
+ raise typer.Exit(code=1)
2499
+
2500
+ log = logging.getLogger(__name__)
2501
+ log.info("sourcecode-mcp starting (stdio transport)")
2502
+ try:
2503
+ _mcp.run()
2504
+ except KeyboardInterrupt:
2505
+ log.info("sourcecode-mcp stopped")
2506
+ except Exception as exc:
2507
+ log.critical("sourcecode-mcp fatal error: %s", exc, exc_info=True)
2508
+ raise typer.Exit(code=1)
2509
+
2510
+
2511
+ # ── MCP onboarding ────────────────────────────────────────────────────────────
2512
+
2513
+ @mcp_app.command("init")
2514
+ def mcp_init(
2515
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
2516
+ ) -> None:
2517
+ """Setup MCP integration for Claude Desktop, Cursor, and other clients.
2518
+
2519
+ \b
2520
+ Detects installed MCP clients, backs up their config files, and safely
2521
+ inserts the sourcecode server entry. Fully idempotent — safe to re-run.
2522
+ """
2523
+ from sourcecode.mcp.onboarding.detector import detect_clients
2524
+ from sourcecode.mcp.onboarding.planner import build_install_plan
2525
+ from sourcecode.mcp.onboarding import backup, applier
2526
+
2527
+ typer.echo("Detecting MCP clients...")
2528
+ typer.echo("")
2529
+
2530
+ clients = detect_clients()
2531
+ if not clients:
2532
+ typer.echo("No MCP clients found on this system.")
2533
+ typer.echo("")
2534
+ typer.echo("Manual setup — add to your MCP client config:")
2535
+ typer.echo(' "sourcecode": {"command": "sourcecode", "args": ["mcp", "serve"]}')
2536
+ raise typer.Exit(code=0)
2537
+
2538
+ # Show detection results
2539
+ for client in clients:
2540
+ mark = "✓" if client.app_installed else "○"
2541
+ note = "" if client.app_installed else " (not found)"
2542
+ typer.echo(f" {mark} {client.name:<18} {client.config_path}{note}")
2543
+ typer.echo("")
2544
+
2545
+ # Build plan
2546
+ plan = build_install_plan(clients)
2547
+ actionable = [a for a in plan if a.client.app_installed and not a.already_installed]
2548
+ already_done = [a for a in plan if a.client.app_installed and a.already_installed]
2549
+
2550
+ if already_done and not actionable:
2551
+ typer.echo("Already configured:")
2552
+ for a in already_done:
2553
+ typer.echo(f" ✓ {a.client.name} {a.client.config_path}")
2554
+ typer.echo("")
2555
+ typer.echo("Nothing to do. Remove: sourcecode mcp remove")
2556
+ raise typer.Exit(code=0)
2557
+
2558
+ if already_done:
2559
+ typer.echo("Already configured:")
2560
+ for a in already_done:
2561
+ typer.echo(f" ✓ {a.client.name} {a.client.config_path}")
2562
+ typer.echo("")
2563
+
2564
+ # Show plan for actionable items
2565
+ typer.echo("This will:")
2566
+ for a in actionable:
2567
+ verb = "Create " if a.will_create_file else "Modify "
2568
+ typer.echo(f" {verb} {a.client.config_path}")
2569
+ typer.echo(f" Backup → ~/.config/sourcecode/mcp-backups/")
2570
+ typer.echo("")
2571
+
2572
+ if not yes:
2573
+ confirmed = typer.confirm("Proceed?", default=False)
2574
+ if not confirmed:
2575
+ typer.echo("Aborted.")
2576
+ raise typer.Exit(code=0)
2577
+ typer.echo("")
2578
+
2579
+ # Apply
2580
+ errors: list[str] = []
2581
+ for a in actionable:
2582
+ try:
2583
+ config = applier.read_config(a.client.config_path)
2584
+ if a.client.config_path.exists():
2585
+ bak = backup.create(a.client.config_path)
2586
+ typer.echo(f" ✓ Backup {bak}")
2587
+ updated = applier.apply_entry(config)
2588
+ applier.write_config(a.client.config_path, updated)
2589
+ if not applier.validate(a.client.config_path):
2590
+ errors.append(f"{a.client.name}: JSON validation failed after write")
2591
+ continue
2592
+ typer.echo(f" ✓ Updated {a.client.config_path}")
2593
+ except Exception as exc:
2594
+ errors.append(f"{a.client.name}: {exc}")
2595
+
2596
+ typer.echo("")
2597
+
2598
+ if errors:
2599
+ for err in errors:
2600
+ typer.echo(f" ✗ {err}", err=True)
2601
+ raise typer.Exit(code=1)
2602
+
2603
+ typer.echo("MCP integration active.")
2604
+ typer.echo("")
2605
+
2606
+ restart_needed = [a.client.name for a in actionable if not a.will_create_file]
2607
+ if restart_needed:
2608
+ typer.echo(f" Restart {', '.join(restart_needed)} to apply changes.")
2609
+ typer.echo(" Remove: sourcecode mcp remove")
2610
+
2611
+
2612
+ @mcp_app.command("status")
2613
+ def mcp_status() -> None:
2614
+ """Show MCP integration status for all detected clients."""
2615
+ from sourcecode.mcp.onboarding.detector import detect_clients
2616
+ from sourcecode.mcp.onboarding import applier
2617
+
2618
+ clients = detect_clients()
2619
+ typer.echo("MCP Integration Status")
2620
+ typer.echo("")
2621
+
2622
+ if not clients:
2623
+ typer.echo(" No MCP clients detected on this system.")
2624
+ raise typer.Exit(code=0)
2625
+
2626
+ for client in clients:
2627
+ if not client.app_installed:
2628
+ typer.echo(f" ○ {client.name:<18} not found")
2629
+ continue
2630
+ config = applier.read_config(client.config_path)
2631
+ if applier.is_installed(config):
2632
+ typer.echo(f" ✓ {client.name:<18} configured {client.config_path}")
2633
+ else:
2634
+ typer.echo(f" ✗ {client.name:<18} not configured")
2635
+ typer.echo("")
2636
+ typer.echo(" Setup: sourcecode mcp init")
2637
+ typer.echo(" Remove: sourcecode mcp remove")
2638
+
2639
+
2640
+ @mcp_app.command("remove")
2641
+ def mcp_remove(
2642
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
2643
+ ) -> None:
2644
+ """Remove sourcecode MCP integration from all configured clients.
2645
+
2646
+ \b
2647
+ Backs up config files before modifying. Restores from backup when available,
2648
+ otherwise removes the sourcecode entry while preserving all other config.
2649
+ """
2650
+ from sourcecode.mcp.onboarding.detector import detect_clients
2651
+ from sourcecode.mcp.onboarding.planner import build_remove_plan
2652
+ from sourcecode.mcp.onboarding import backup, applier
2653
+
2654
+ clients = detect_clients()
2655
+ plan = build_remove_plan(clients)
2656
+ installed = [a for a in plan if a.already_installed]
2657
+
2658
+ if not installed:
2659
+ typer.echo("sourcecode MCP integration not found in any client config.")
2660
+ typer.echo(" Setup: sourcecode mcp init")
2661
+ raise typer.Exit(code=0)
2662
+
2663
+ typer.echo("Remove sourcecode MCP integration from:")
2664
+ typer.echo("")
2665
+ for a in installed:
2666
+ typer.echo(f" {a.client.name} {a.client.config_path}")
2667
+ bak = backup.latest(a.client.config_path)
2668
+ if bak:
2669
+ typer.echo(f" Backup available: {bak}")
2670
+ typer.echo("")
2671
+
2672
+ if not yes:
2673
+ confirmed = typer.confirm("Proceed?", default=False)
2674
+ if not confirmed:
2675
+ typer.echo("Aborted.")
2676
+ raise typer.Exit(code=0)
2677
+ typer.echo("")
2678
+
2679
+ errors: list[str] = []
2680
+ for a in installed:
2681
+ try:
2682
+ bak = backup.create(a.client.config_path)
2683
+ typer.echo(f" ✓ Backup {bak}")
2684
+ config = applier.read_config(a.client.config_path)
2685
+ updated = applier.remove_entry(config)
2686
+ applier.write_config(a.client.config_path, updated)
2687
+ if not applier.validate(a.client.config_path):
2688
+ errors.append(f"{a.client.name}: JSON validation failed — restoring backup")
2689
+ backup.restore(bak, a.client.config_path)
2690
+ continue
2691
+ typer.echo(f" ✓ Updated {a.client.config_path}")
2692
+ except Exception as exc:
2693
+ errors.append(f"{a.client.name}: {exc}")
2694
+
2695
+ typer.echo("")
2696
+
2697
+ if errors:
2698
+ for err in errors:
2699
+ typer.echo(f" ✗ {err}", err=True)
2700
+ raise typer.Exit(code=1)
2701
+
2702
+ typer.echo("MCP integration removed.")
2703
+ typer.echo(" Re-add: sourcecode mcp init")
2704
+
2705
+
2435
2706
  # ── Entry point ───────────────────────────────────────────────────────────────
2436
2707
 
2437
2708
  def main_entry() -> None:
@@ -0,0 +1,5 @@
1
+ """MCP server integration for sourcecode CLI.
2
+
3
+ Exposes all CLI capabilities as MCP tools callable by AI agents.
4
+ Install the 'mcp' extra to enable: pip install sourcecode[mcp]
5
+ """
@@ -0,0 +1 @@
1
+ """MCP client onboarding: detect, plan, apply, backup, remove."""
@@ -0,0 +1,63 @@
1
+ """Safe JSON config applier for MCP client configuration files."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ _MCP_SERVERS_KEY = "mcpServers"
8
+ _ENTRY_NAME = "sourcecode"
9
+ _ENTRY_VALUE: dict[str, object] = {
10
+ "command": "sourcecode",
11
+ "args": ["mcp", "serve"],
12
+ }
13
+
14
+
15
+ def read_config(path: Path) -> dict:
16
+ """Parse JSON config from path. Returns empty dict if missing or empty."""
17
+ if not path.exists():
18
+ return {}
19
+ raw = path.read_text(encoding="utf-8").strip()
20
+ if not raw:
21
+ return {}
22
+ return json.loads(raw) # type: ignore[no-any-return]
23
+
24
+
25
+ def is_installed(config: dict) -> bool:
26
+ """True if sourcecode entry already present in mcpServers."""
27
+ return _ENTRY_NAME in config.get(_MCP_SERVERS_KEY, {})
28
+
29
+
30
+ def apply_entry(config: dict) -> dict:
31
+ """Return new config dict with sourcecode merged into mcpServers."""
32
+ config = dict(config)
33
+ servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
34
+ servers[_ENTRY_NAME] = _ENTRY_VALUE
35
+ config[_MCP_SERVERS_KEY] = servers
36
+ return config
37
+
38
+
39
+ def remove_entry(config: dict) -> dict:
40
+ """Return new config dict with sourcecode removed from mcpServers."""
41
+ config = dict(config)
42
+ servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
43
+ servers.pop(_ENTRY_NAME, None)
44
+ if servers:
45
+ config[_MCP_SERVERS_KEY] = servers
46
+ elif _MCP_SERVERS_KEY in config:
47
+ del config[_MCP_SERVERS_KEY]
48
+ return config
49
+
50
+
51
+ def write_config(path: Path, config: dict) -> None:
52
+ """Atomically write config as formatted JSON."""
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
55
+
56
+
57
+ def validate(path: Path) -> bool:
58
+ """True if path contains parseable JSON."""
59
+ try:
60
+ json.loads(path.read_text(encoding="utf-8"))
61
+ return True
62
+ except Exception:
63
+ return False
@@ -0,0 +1,40 @@
1
+ """Timestamped backup management for MCP config files."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ _BACKUP_DIR = Path.home() / ".config" / "sourcecode" / "mcp-backups"
8
+
9
+
10
+ def _backup_stem(config_path: Path) -> str:
11
+ """Stable prefix derived from the config path, safe for filenames."""
12
+ parts = config_path.parts
13
+ # Use last two path components to keep names readable but unique enough.
14
+ label = "_".join(p for p in parts[-2:] if p).replace(".", "_")
15
+ return label
16
+
17
+
18
+ def create(config_path: Path) -> Path:
19
+ """Copy config_path to a timestamped backup file. Returns backup path."""
20
+ _BACKUP_DIR.mkdir(parents=True, exist_ok=True)
21
+ ts = datetime.now().strftime("%Y%m%dT%H%M%S")
22
+ stem = _backup_stem(config_path)
23
+ backup_path = _BACKUP_DIR / f"{stem}.{ts}.bak"
24
+ backup_path.write_bytes(config_path.read_bytes())
25
+ return backup_path
26
+
27
+
28
+ def restore(backup_path: Path, target_path: Path) -> None:
29
+ """Overwrite target_path with contents of backup_path."""
30
+ target_path.parent.mkdir(parents=True, exist_ok=True)
31
+ target_path.write_bytes(backup_path.read_bytes())
32
+
33
+
34
+ def latest(config_path: Path) -> Path | None:
35
+ """Find the most recent backup for config_path, or None."""
36
+ if not _BACKUP_DIR.exists():
37
+ return None
38
+ stem = _backup_stem(config_path)
39
+ matches = sorted(_BACKUP_DIR.glob(f"{stem}.*.bak"))
40
+ return matches[-1] if matches else None
@@ -0,0 +1,59 @@
1
+ """Detect MCP-capable clients installed on the current machine."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ # Registry of known MCP clients and their config paths per platform.
11
+ # Keys: "darwin", "linux", "win32" — matching sys.platform values.
12
+ _CLIENT_REGISTRY: dict[str, dict[str, str]] = {
13
+ "Claude Desktop": {
14
+ "darwin": "~/Library/Application Support/Claude/claude_desktop_config.json",
15
+ "linux": "~/.config/Claude/claude_desktop_config.json",
16
+ "win32": "{APPDATA}/Claude/claude_desktop_config.json",
17
+ },
18
+ "Cursor": {
19
+ "darwin": "~/.cursor/mcp.json",
20
+ "linux": "~/.cursor/mcp.json",
21
+ "win32": "{USERPROFILE}/.cursor/mcp.json",
22
+ },
23
+ }
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class MCPClient:
28
+ name: str
29
+ config_path: Path
30
+ app_installed: bool # True if the config file (or its parent dir) exists
31
+
32
+
33
+ def _resolve(template: str) -> Path:
34
+ """Expand env vars in Windows-style {VAR} templates, then expanduser."""
35
+ result = template
36
+ for var in ("APPDATA", "LOCALAPPDATA", "USERPROFILE"):
37
+ val = os.environ.get(var, "")
38
+ if val:
39
+ result = result.replace(f"{{{var}}}", val)
40
+ return Path(result).expanduser()
41
+
42
+
43
+ def detect_clients() -> list[MCPClient]:
44
+ """Return all known MCP clients with their resolved config paths."""
45
+ plat = sys.platform
46
+ clients: list[MCPClient] = []
47
+ for name, paths in _CLIENT_REGISTRY.items():
48
+ template = paths.get(plat) or paths.get("linux")
49
+ if not template:
50
+ continue
51
+ config_path = _resolve(template)
52
+ # Consider client "installed" if its config file OR parent app dir exists.
53
+ app_installed = config_path.exists() or config_path.parent.exists()
54
+ clients.append(MCPClient(
55
+ name=name,
56
+ config_path=config_path,
57
+ app_installed=app_installed,
58
+ ))
59
+ return clients
@@ -0,0 +1,40 @@
1
+ """Build an install/remove plan from detected MCP clients."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from .applier import is_installed, read_config
7
+ from .detector import MCPClient
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ClientAction:
12
+ client: MCPClient
13
+ already_installed: bool # sourcecode entry already in config
14
+ will_create_file: bool # config file doesn't exist yet — will be created
15
+
16
+
17
+ def build_install_plan(clients: list[MCPClient]) -> list[ClientAction]:
18
+ """Describe what `mcp init` would do for each detected client."""
19
+ actions: list[ClientAction] = []
20
+ for client in clients:
21
+ config = read_config(client.config_path)
22
+ actions.append(ClientAction(
23
+ client=client,
24
+ already_installed=is_installed(config),
25
+ will_create_file=not client.config_path.exists(),
26
+ ))
27
+ return actions
28
+
29
+
30
+ def build_remove_plan(clients: list[MCPClient]) -> list[ClientAction]:
31
+ """Describe what `mcp remove` would do for each detected client."""
32
+ actions: list[ClientAction] = []
33
+ for client in clients:
34
+ config = read_config(client.config_path)
35
+ actions.append(ClientAction(
36
+ client=client,
37
+ already_installed=is_installed(config),
38
+ will_create_file=False,
39
+ ))
40
+ return actions
@@ -0,0 +1,40 @@
1
+ """In-process CLI runner for MCP tool execution.
2
+
3
+ Replaces the subprocess adapter from the standalone sourcecode-mcp project.
4
+ Calls CLI commands directly in the same process via CliRunner — no binary
5
+ lookup, no process fork, no stdout encoding issues.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typer.testing import CliRunner
10
+
11
+ _runner = CliRunner()
12
+
13
+
14
+ def run_command(args: list[str]) -> str:
15
+ """Invoke a sourcecode CLI command in-process and return stdout.
16
+
17
+ Raises RuntimeError on non-zero exit or empty output.
18
+ """
19
+ from sourcecode.cli import _detected_path, _preprocess_args, app
20
+
21
+ _detected_path[0] = "."
22
+ processed = _preprocess_args(list(args))
23
+ result = _runner.invoke(app, processed)
24
+
25
+ if result.exit_code != 0:
26
+ snippet = (result.output or "").strip()
27
+ raise RuntimeError(
28
+ f"sourcecode command failed (exit {result.exit_code}).\n"
29
+ f"Args: {args}\n"
30
+ f"Output: {snippet or '(empty)'}"
31
+ )
32
+
33
+ output = (result.output or "").strip()
34
+ if not output:
35
+ raise RuntimeError(
36
+ f"sourcecode command produced no output.\n"
37
+ f"Args: {args}"
38
+ )
39
+
40
+ return output
@@ -0,0 +1,147 @@
1
+ """MCP server for sourcecode CLI.
2
+
3
+ Exposes sourcecode capabilities as MCP tools. Each tool maps to a CLI command
4
+ and delegates execution to the in-process runner — no subprocess, no binary
5
+ lookup, same process as the CLI.
6
+
7
+ All tools return the canonical contract:
8
+ {"success": bool, "data": str | None, "error": {"code": str, "message": str} | None}
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal
13
+
14
+ from mcp.server.fastmcp import FastMCP
15
+
16
+ from sourcecode.mcp.runner import run_command
17
+
18
+ mcp = FastMCP("sourcecode")
19
+
20
+ _PREPARE_CONTEXT_TASKS = frozenset({
21
+ "delta", "review-pr", "fix-bug", "onboard",
22
+ "explain", "refactor", "generate-tests",
23
+ })
24
+ _TELEMETRY_ACTIONS = frozenset({"status", "enable", "disable"})
25
+
26
+
27
+ def _ok(data: str) -> dict:
28
+ return {"success": True, "data": data, "error": None}
29
+
30
+
31
+ def _err(message: str, code: str = "EXECUTION_FAILED") -> dict:
32
+ return {"success": False, "data": None, "error": {"code": code, "message": message}}
33
+
34
+
35
+ def _execute(args: list[str]) -> dict:
36
+ try:
37
+ return _ok(run_command(args))
38
+ except RuntimeError as exc:
39
+ return _err(str(exc))
40
+
41
+
42
+ @mcp.tool()
43
+ def compact(path: str, git_context: bool = False) -> dict:
44
+ """Compact analysis of a repository (~1000-3000 tokens).
45
+
46
+ Maps to: sourcecode <path> --compact [--git-context]
47
+ """
48
+ if not isinstance(path, str):
49
+ return _err("path must be a string", "INVALID_ARGUMENT")
50
+ if not isinstance(git_context, bool):
51
+ return _err("git_context must be boolean", "INVALID_ARGUMENT")
52
+ args = [path, "--compact"]
53
+ if git_context:
54
+ args.append("--git-context")
55
+ return _execute(args)
56
+
57
+
58
+ @mcp.tool()
59
+ def agent(path: str, git_context: bool = False) -> dict:
60
+ """Agent-optimised analysis: identity, entry points, dependencies, gaps.
61
+
62
+ Maps to: sourcecode <path> --agent [--git-context]
63
+ """
64
+ if not isinstance(path, str):
65
+ return _err("path must be a string", "INVALID_ARGUMENT")
66
+ if not isinstance(git_context, bool):
67
+ return _err("git_context must be boolean", "INVALID_ARGUMENT")
68
+ args = [path, "--agent"]
69
+ if git_context:
70
+ args.append("--git-context")
71
+ return _execute(args)
72
+
73
+
74
+ @mcp.tool()
75
+ def prepare_context(
76
+ task: Literal[
77
+ "delta", "review-pr", "fix-bug", "onboard",
78
+ "explain", "refactor", "generate-tests",
79
+ ],
80
+ path: str,
81
+ ) -> dict:
82
+ """Task-specific context for AI coding agents.
83
+
84
+ Maps to: sourcecode prepare-context <task> <path>
85
+
86
+ task must be one of:
87
+ explain Architecture, entry points, key dependencies
88
+ fix-bug Risk-ranked files, suspected areas, annotations
89
+ refactor Structural issues, improvement opportunities
90
+ generate-tests Untested source files, test gap analysis
91
+ onboard Full project context for new agents/developers
92
+ review-pr PR diff with runtime signals and security impact
93
+ delta Incremental context: git-changed files only
94
+ """
95
+ if task not in _PREPARE_CONTEXT_TASKS:
96
+ return _err(
97
+ f"task must be one of {sorted(_PREPARE_CONTEXT_TASKS)}",
98
+ "INVALID_ARGUMENT",
99
+ )
100
+ if not isinstance(path, str):
101
+ return _err("path must be a string", "INVALID_ARGUMENT")
102
+ return _execute(["prepare-context", task, path])
103
+
104
+
105
+ @mcp.tool()
106
+ def repo_ir(path: str) -> dict:
107
+ """Deterministic symbol-level IR for Java repositories.
108
+
109
+ Maps to: sourcecode repo-ir <path>
110
+ Output is JSON: graph{nodes,edges}, analysis, impact, subsystems, change_set.
111
+ """
112
+ if not isinstance(path, str):
113
+ return _err("path must be a string", "INVALID_ARGUMENT")
114
+ return _execute(["repo-ir", path])
115
+
116
+
117
+ @mcp.tool()
118
+ def version() -> dict:
119
+ """Return the installed sourcecode version.
120
+
121
+ Maps to: sourcecode version
122
+ """
123
+ return _execute(["version"])
124
+
125
+
126
+ @mcp.tool()
127
+ def config() -> dict:
128
+ """Show sourcecode configuration (version, telemetry state, config path).
129
+
130
+ Maps to: sourcecode config
131
+ """
132
+ return _execute(["config"])
133
+
134
+
135
+ @mcp.tool()
136
+ def telemetry(action: Literal["status", "enable", "disable"]) -> dict:
137
+ """Manage telemetry settings.
138
+
139
+ Maps to: sourcecode telemetry <status|enable|disable>
140
+ action must be one of: status, enable, disable
141
+ """
142
+ if action not in _TELEMETRY_ACTIONS:
143
+ return _err(
144
+ f"action must be one of {sorted(_TELEMETRY_ACTIONS)}",
145
+ "INVALID_ARGUMENT",
146
+ )
147
+ return _execute(["telemetry", action])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.30.30
3
+ Version: 1.31.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -212,16 +212,19 @@ Requires-Dist: tree-sitter-javascript>=0.21; extra == 'ast'
212
212
  Requires-Dist: tree-sitter-typescript>=0.21; extra == 'ast'
213
213
  Requires-Dist: tree-sitter>=0.21; extra == 'ast'
214
214
  Provides-Extra: dev
215
+ Requires-Dist: mcp>=1.0.0; extra == 'dev'
215
216
  Requires-Dist: mypy>=1.10; extra == 'dev'
216
217
  Requires-Dist: pytest>=8; extra == 'dev'
217
218
  Requires-Dist: ruff>=0.15; extra == 'dev'
219
+ Provides-Extra: mcp
220
+ Requires-Dist: mcp>=1.0.0; extra == 'mcp'
218
221
  Description-Content-Type: text/markdown
219
222
 
220
223
  # sourcecode
221
224
 
222
225
  **Deterministic, behavior-aware codebase context for AI agents and PR review.**
223
226
 
224
- ![Version](https://img.shields.io/badge/version-1.30.30-blue)
227
+ ![Version](https://img.shields.io/badge/version-1.31.0-blue)
225
228
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
226
229
 
227
230
  ---
@@ -257,7 +260,7 @@ pipx install sourcecode
257
260
 
258
261
  ```bash
259
262
  sourcecode version
260
- # sourcecode 1.30.30
263
+ # sourcecode 1.31.0
261
264
  ```
262
265
 
263
266
  ---
@@ -1,10 +1,10 @@
1
- sourcecode/__init__.py,sha256=ANLn7vd3QuDTWulbelwm9eIp93h5ZEZKi-nKm911_so,104
1
+ sourcecode/__init__.py,sha256=MiA5_ZlVOCsJoykaFJRoZJpA8VwhlU7ns_GdhCt64L4,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
6
6
  sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
7
- sourcecode/cli.py,sha256=_WsbkJWvI_xv4aSh3mdPvTbh-eKGV5DZbPj2ZFuA2GI,100189
7
+ sourcecode/cli.py,sha256=wncSKjhpDuN5t-tU7vj-kY2x6FREFyXoRetZnUensMs,110219
8
8
  sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
9
9
  sourcecode/confidence_analyzer.py,sha256=H9VHYRzZhqMFlSCZffjtsMUGYLnDvrq1g5FjzyQ1hxE,16381
10
10
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -58,14 +58,22 @@ sourcecode/detectors/rust.py,sha256=Tij1vz8BFZ332GEvVkL6vyMli2OMHJfHyDAppWfe66c,
58
58
  sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
59
59
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
60
60
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
61
+ sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
62
+ sourcecode/mcp/runner.py,sha256=B167PyUBUwAJC6stKRCd3KHpmrpZOMsAA3gOtIuUauU,1186
63
+ sourcecode/mcp/server.py,sha256=grK87-zeJ98W-DIXqaNg_PXGecFB_WzooKkyQcjJDa0,4480
64
+ sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
65
+ sourcecode/mcp/onboarding/applier.py,sha256=yfSMT0NKdZsjavtLkC8yQ7OtkfepOl5IXGByqg6bdEY,1894
66
+ sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
67
+ sourcecode/mcp/onboarding/detector.py,sha256=lcBkERH1WlxTXGTKVtxKv1fvql4NgSnVxGftra-gMvE,1961
68
+ sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
61
69
  sourcecode/telemetry/__init__.py,sha256=M0eQZFNkmJiLbI_oNP4QEXwVju1dQ2d4P-E1-Bw8PxE,3116
62
70
  sourcecode/telemetry/config.py,sha256=Pir0WHp4z-9Qclnn2NDZ3vwitqsMkOAJckmwjUSxrk4,1795
63
71
  sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWnII,2237
64
72
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
65
73
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
66
74
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
67
- sourcecode-1.30.30.dist-info/METADATA,sha256=TRgm7Qpvohc3ofAWDRDEfmcF2lCQ3b-dqvEAn-Rd6qw,28956
68
- sourcecode-1.30.30.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
- sourcecode-1.30.30.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
- sourcecode-1.30.30.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
- sourcecode-1.30.30.dist-info/RECORD,,
75
+ sourcecode-1.31.0.dist-info/METADATA,sha256=tp3EeSNMJlNOi0f6JNoXdDsfrQDbEm703TurOFj83kw,29057
76
+ sourcecode-1.31.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
77
+ sourcecode-1.31.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
78
+ sourcecode-1.31.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
79
+ sourcecode-1.31.0.dist-info/RECORD,,