From 19958ca9d09134b49710710ff8be90063f597874 Mon Sep 17 00:00:00 2001 From: mochan Date: Sat, 30 May 2026 19:01:03 +0530 Subject: [PATCH] fix(init): write robust .mcp.json with absolute binary path, --db, and cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.1.1 fix wrote .mcp.json but used a bare `codegraph` command with no --db flag and no cwd. That worked when the MCP client's $PATH and working directory happened to line up; in practice users had to hand-fix all three (absolute path → resolved from sys.executable.parent → shutil.which → bare fallback; --db pointing at .codegraph/graph.db; cwd set to repo root). Pre-0.1.2 default entries are migrated forward in place. User-customised entries (different command, extra args, env, cwd) are left untouched. v0.1.2 backlog item #1. --- CHANGELOG.md | 13 +++++ codegraph/cli.py | 60 ++++++++++++++++++- tests/test_cli_mcp_json.py | 116 +++++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 tests/test_cli_mcp_json.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3a727..f26dc99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **`codegraph init` now writes a robust `.mcp.json`** — uses the absolute + path to the `codegraph` binary (resolved from the running interpreter's + bin dir, then `shutil.which`, then bare fallback), passes an explicit + `--db .codegraph/graph.db` flag, and sets `cwd` to the repo root. The + 0.1.1 fix wrote the file but used a bare `codegraph` command with no + `--db` and no `cwd`, which forced users to hand-edit the file when + their MCP client's `$PATH` or working directory didn't line up. Old + pre-0.1.2 default entries are migrated forward in place; user-customised + entries are left alone. (Surfaced when a user added polycodegraph to a + FastAPI project — Claude reported needing to fix all three.) + ## [0.1.1] — 2026-05-24 ### Fixed diff --git a/codegraph/cli.py b/codegraph/cli.py index 1ceec2d..6d00bba 100644 --- a/codegraph/cli.py +++ b/codegraph/cli.py @@ -256,12 +256,57 @@ def _update_gitignore(repo_root: Path) -> None: gi_path.write_text(f"{entry}\n") +def _resolve_codegraph_binary() -> str: + """Best-effort absolute path to the `codegraph` executable. + + Order: sibling of the running interpreter (venv/pipx install), then + `shutil.which`, then bare "codegraph" as a last-resort fallback. + """ + import shutil + import sys + + candidate = Path(sys.executable).parent / "codegraph" + if candidate.exists(): + return str(candidate) + which = shutil.which("codegraph") + if which: + return which + return "codegraph" + + +def _build_mcp_entry(repo_root: Path) -> dict[str, object]: + """The canonical codegraph entry for project-level `.mcp.json`.""" + db_path = repo_root / ".codegraph" / "graph.db" + return { + "command": _resolve_codegraph_binary(), + "args": ["mcp", "serve", "--db", str(db_path)], + "cwd": str(repo_root), + } + + +def _is_default_codegraph_entry(entry: object) -> bool: + """True if the entry looks like a pre-0.1.2 default (no --db, no cwd). + + Used to migrate users forward without clobbering a customised entry. + """ + if not isinstance(entry, dict): + return False + if entry.get("command") != "codegraph": + return False + args = entry.get("args") + if args != ["mcp", "serve"]: + return False + return "cwd" not in entry + + def _write_project_mcp_json(repo_root: Path) -> str: """Register the codegraph MCP server in the repo-local .mcp.json. Returns "created" if the file was newly written, "merged" if an existing - .mcp.json was extended with the `codegraph` entry, or "already-present" if - a `codegraph` entry was already there (left untouched in that case). + .mcp.json was extended with the `codegraph` entry, "migrated" if a + pre-0.1.2 default entry was rewritten with the robust shape, or + "already-present" if a customised `codegraph` entry was already there + (left untouched in that case). Project-level .mcp.json is auto-loaded by Claude Code and Cursor as soon as you open the project — no global config edits needed. Other clients @@ -271,7 +316,7 @@ def _write_project_mcp_json(repo_root: Path) -> str: import json mcp_path = repo_root / ".mcp.json" - entry = {"command": "codegraph", "args": ["mcp", "serve"]} + entry = _build_mcp_entry(repo_root) if mcp_path.exists(): try: @@ -286,6 +331,10 @@ def _write_project_mcp_json(repo_root: Path) -> str: servers = {} data["mcpServers"] = servers if "codegraph" in servers: + if _is_default_codegraph_entry(servers["codegraph"]): + servers["codegraph"] = entry + mcp_path.write_text(json.dumps(data, indent=2) + "\n") + return "migrated" return "already-present" servers["codegraph"] = entry mcp_path.write_text(json.dumps(data, indent=2) + "\n") @@ -391,6 +440,11 @@ def init( console.print( "[green]✓[/green] Added `codegraph` entry to existing .mcp.json" ) + elif mcp_state == "migrated": + console.print( + "[green]✓[/green] Migrated .mcp.json `codegraph` entry to the " + "0.1.2 shape (absolute binary path + --db + cwd)" + ) elif mcp_state == "already-present": console.print( "[dim]·[/dim] .mcp.json already has a `codegraph` entry — left as-is" diff --git a/tests/test_cli_mcp_json.py b/tests/test_cli_mcp_json.py new file mode 100644 index 0000000..2ff8255 --- /dev/null +++ b/tests/test_cli_mcp_json.py @@ -0,0 +1,116 @@ +"""Tests for `_write_project_mcp_json` (item #1 of v0.1.2 backlog). + +The 0.1.1 fix wrote `.mcp.json` but used a bare `codegraph` command with +no `--db` and no `cwd`. 0.1.2 fixes that to use an absolute binary path, +an explicit `--db` pointing at `.codegraph/graph.db`, and `cwd` set to +the repo root. Pre-0.1.2 default entries are migrated forward in place; +customised entries are left alone. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from codegraph.cli import ( + _build_mcp_entry, + _is_default_codegraph_entry, + _resolve_codegraph_binary, + _write_project_mcp_json, +) + + +def test_build_mcp_entry_has_absolute_path_db_and_cwd(tmp_path: Path) -> None: + entry = _build_mcp_entry(tmp_path) + assert isinstance(entry["command"], str) + # Either an absolute resolved path or the bare fallback. The path-aware + # branch is exercised by test_resolve_codegraph_binary_prefers_sibling. + args = entry["args"] + assert isinstance(args, list) + assert args[0] == "mcp" + assert args[1] == "serve" + assert "--db" in args + db_idx = args.index("--db") + 1 + assert args[db_idx] == str(tmp_path / ".codegraph" / "graph.db") + assert entry["cwd"] == str(tmp_path) + + +def test_resolve_codegraph_binary_prefers_sibling( + tmp_path: Path, monkeypatch +) -> None: + fake_venv = tmp_path / "venv" / "bin" + fake_venv.mkdir(parents=True) + fake_py = fake_venv / "python" + fake_py.write_text("") + fake_cg = fake_venv / "codegraph" + fake_cg.write_text("") + monkeypatch.setattr(sys, "executable", str(fake_py)) + assert _resolve_codegraph_binary() == str(fake_cg) + + +def test_is_default_codegraph_entry_detects_old_shape() -> None: + assert _is_default_codegraph_entry( + {"command": "codegraph", "args": ["mcp", "serve"]} + ) + # Customised: different binary + assert not _is_default_codegraph_entry( + {"command": "/opt/cg/bin/codegraph", "args": ["mcp", "serve"]} + ) + # Customised: extra cwd + assert not _is_default_codegraph_entry( + {"command": "codegraph", "args": ["mcp", "serve"], "cwd": "/repo"} + ) + # Customised: different args + assert not _is_default_codegraph_entry( + {"command": "codegraph", "args": ["mcp", "serve", "--db", "x"]} + ) + assert not _is_default_codegraph_entry("not-a-dict") + + +def test_write_mcp_json_fresh(tmp_path: Path) -> None: + state = _write_project_mcp_json(tmp_path) + assert state == "created" + data = json.loads((tmp_path / ".mcp.json").read_text()) + entry = data["mcpServers"]["codegraph"] + assert entry["cwd"] == str(tmp_path) + assert "--db" in entry["args"] + + +def test_write_mcp_json_migrates_pre_012_default(tmp_path: Path) -> None: + (tmp_path / ".mcp.json").write_text( + json.dumps( + {"mcpServers": {"codegraph": {"command": "codegraph", "args": ["mcp", "serve"]}}} + ) + ) + state = _write_project_mcp_json(tmp_path) + assert state == "migrated" + entry = json.loads((tmp_path / ".mcp.json").read_text())["mcpServers"]["codegraph"] + assert entry["cwd"] == str(tmp_path) + assert "--db" in entry["args"] + + +def test_write_mcp_json_preserves_customised_entry(tmp_path: Path) -> None: + custom = { + "command": "/opt/cg/bin/codegraph", + "args": ["mcp", "serve", "--verbose"], + "env": {"FOO": "bar"}, + } + (tmp_path / ".mcp.json").write_text( + json.dumps({"mcpServers": {"codegraph": custom, "other": {"command": "x"}}}) + ) + state = _write_project_mcp_json(tmp_path) + assert state == "already-present" + data = json.loads((tmp_path / ".mcp.json").read_text()) + assert data["mcpServers"]["codegraph"] == custom + assert data["mcpServers"]["other"] == {"command": "x"} + + +def test_write_mcp_json_merges_when_codegraph_missing(tmp_path: Path) -> None: + (tmp_path / ".mcp.json").write_text( + json.dumps({"mcpServers": {"other": {"command": "x"}}}) + ) + state = _write_project_mcp_json(tmp_path) + assert state == "merged" + data = json.loads((tmp_path / ".mcp.json").read_text()) + assert "codegraph" in data["mcpServers"] + assert data["mcpServers"]["other"] == {"command": "x"}