codebase-bridge-mcp
# codebase-bridge-mcp
A read-only [MCP](https://modelcontextprotocol.io) server that lets a Claude chat — in
the desktop or web app — explore your local repository and answer questions about it,
returning synthesized answers with `file:line` references. **No copy-pasting files, no
giving the chat write access to your machine.**
`ask_codebase` runs headless Claude Code (`claude -p`) inside your repo with an
**exclusive** read-only tool set (`Read Grep Glob` — no Edit, Write, Bash, agents, or
MCP) and a hook that **confines reads to the target repo**, so a disposable agent
explores the code and returns a synthesized, `file:line` answer. See
[Security](#security) for exactly what is and isn't enforced.
```
Claude client --MCP--> codebase-bridge --> claude -p (Read/Grep/Glob only)
└─ explores /path/to/your/repo
```
## Why I built this
I do my best thinking about a problem in a conversational Claude chat, not in a coding
agent — the back-and-forth on an idea tends to go deeper there. But every time I wanted
to ask about a specific part of my code, I had to go hunting for the right file, find
the relevant section, and paste it in by hand — every single question. It broke the
flow exactly when I wanted to think freely.
I went looking for something to close the gap and found the existing codebase tools all
point the wrong way: filesystem and repo-packing servers (like Repomix) feed a coding
agent that already has your repo, and structural-index servers (like
codebase-memory-mcp) need a coding agent to be the brain. Nothing brought a finished,
read-only answer about my code into the chat where I was actually thinking. So I built
it: it shells out to a headless, read-only Claude scoped to a single repo (Read/Grep/Glob
only — no Write, no Bash), and hands the synthesized answer back to my chat. I can
interrogate my codebase mid-conversation without ever leaving the thinking.
## Requirements
- [Claude Code](https://claude.com/claude-code) (`claude`) installed and authenticated.
- [`uv`](https://docs.astral.sh/uv/) (runs the server with zero install — deps are
declared inline in `server.py`).
- For the optional `http` transport: [`cloudflared`](https://github.com/cloudflare/cloudflared)
or any HTTPS tunnel.
## Tools
| Tool | What it does |
| ---- | ------------ |
| `ask_codebase(question, thread="", model="", effort="", show_steps=False)` | Explore the repo read-only and answer. |
| `bridge_cost()` | Return the per-thread + grand-total cost ledger for this server process. |
| `bridge_forget(thread="")` | Drop a thread's session (or all threads) so the next call starts fresh. |
`ask_codebase` parameters:
- **`thread`** — a friendly name (e.g. `"auth-investigation"`). Reuse the **same name**
to keep one Claude Code session alive — **turn-based steering**: each follow-up keeps
prior context, so you redirect it and it stays cheap (prompt-cache hits, measured
45% → 95% → 99% cache-read across turns). Omit for a one-off fresh session.
- **`model`** — e.g. `"sonnet"` / `"opus"`. **Binds** to a thread on its first call; a
*different* model on an existing thread is **refused with a note**, because switching
models forces a full-context reprocess. Start a new thread for a different model.
- **`effort`** — reasoning effort: `low`, `medium`, `high`, `xhigh`, `max`. Per-call
(safe to vary within a thread — unlike `model` it doesn't invalidate the cache).
Lower = cheaper/faster; raise it for hard cross-file reasoning. Unknown values are
ignored with a note.
- **`show_steps`** — append the exploration trail so you see *how* it reached the
answer and can steer the next turn.
Example answer:
```
Auth is enforced in app/middleware.py:42 (verify_token), called from the
@requires_auth decorator in app/security.py:18.
[bridge] explored via 2 step(s):
1. grep 'verify_token'
2. read app/middleware.py
--
[bridge] model=opus-4-8 | call=$0.0383 | cache 58k read / 19 fresh (100% cached)
[bridge] thread 'auth' total=$0.81 over 3 call(s) -- reuse this thread for cheap follow-ups
```
## Install / run
### stdio (recommended — Claude Desktop & Claude Code)
stdio is launched as a local subprocess by the client: no URL, no tunnel, no token.
**Claude Code:**
```sh
claude mcp add codebase-bridge -- uv run --script /ABS/PATH/server.py --repo /ABS/PATH/your-repo
```
**Claude Desktop:** edit `~/Library/Application Support/Claude/claude_desktop_config.json`
(macOS) — see [`examples/claude_desktop_config.json`](examples/claude_desktop_config.json):
```json
{
"mcpServers": {
"codebase-bridge": {
"command": "/opt/homebrew/bin/uv",
"args": ["run", "--script", "/ABS/PATH/server.py", "--repo", "/ABS/PATH/your-repo"]
}
}
}
```
Use the **absolute path to `uv`** (`which uv`) — Claude Desktop doesn't inherit your
shell `PATH`. Restart Desktop after editing. Then ask:
*"Use ask_codebase on thread 'auth', show_steps: how does login work?"*
### http (only for claude.ai web)
claude.ai reaches servers from Anthropic's cloud, so it needs a public URL + token:
```sh
export BRIDGE_TOKEN=$(openssl rand -hex 16); echo "token: $BRIDGE_TOKEN"
uv run --script server.py --transport http --repo /ABS/PATH/your-repo # 127.0.0.1:8765
cloudflared tunnel --url http://localhost:8765 # new terminal
```
http transport **refuses to start without `BRIDGE_TOKEN`** (pass `--insecure-no-auth`
to deliberately run an open server, e.g. when you front it with your own auth). It
binds **`127.0.0.1`** by default — the tunnel connects to localhost; set `--host
0.0.0.0` only if you really need other interfaces. The token check is constant-time.
Add `https://<tunnel>.trycloudflare.com/mcp` (no trailing slash) as a custom connector
in claude.ai with the bearer token.
## Configuration
All knobs have CLI flags and/or env vars:
| Env | CLI | Default | Meaning |
| --- | --- | ------- | ------- |
| `BRIDGE_REPO` | `--repo` | current dir | repo Claude Code explores |
| `BRIDGE_TRANSPORT` | `--transport` | `stdio` | `stdio` or `http` |
| `BRIDGE_TOKEN` | — | unset | http bearer token (http refuses to start unless set or `--insecure-no-auth`) |
| `BRIDGE_HOST` | `--host` | `127.0.0.1` | http bind address |
| `BRIDGE_PORT` | — | `8765` | http listen port |
| `BRIDGE_TIMEOUT` | — | `240` | per-call wall-clock cap (seconds) |
| `BRIDGE_MODEL` | — | Claude Code default | default model for new threads / ephemeral calls |
| `BRIDGE_MAX_CONCURRENT` | — | `4` | cap on concurrent `claude` subprocesses (DoS/cost guard) |
| `BRIDGE_THREAD_TTL` | — | `1800` | idle seconds before a thread session is dropped (`0` = never) |
| `BRIDGE_MAX_THREADS` | — | `256` | live-thread cap before LRU eviction |
| `BRIDGE_CONFINE_READS` | — | `1` | PreToolUse hook confines reads to `--repo` (`0` = read anywhere) |
| — | `--insecure-no-auth` | off | allow http without a token (explicit opt-in; refused on non-loopback host) |
## Security
This project has **not been independently security-audited.** It is hardened, and the
guarantees below are enforced and were verified empirically — but read this before
exposing it to untrusted input.
**What is enforced (default), at Claude's tool-dispatch layer:**
- **Exclusive read-only tool set.** The agent is launched with `--tools "Read Grep
Glob"` — the *exclusive* built-in tool flag (unlike `--allowedTools`, which is merely
additive and lets unlisted tools still run). The agent literally has **no Bash, Edit,
Write, agent/subagent, or MCP tool** — verified: it reports only `Read/Grep/Glob`.
- **Reads confined to `--repo`.** A `PreToolUse` hook (this script, registered via
`--settings`) runs host-side before every tool call and **denies any read whose path
resolves outside the target repo** (absolute paths, `../`, `/proc/self/environ`, etc.)
— verified: reading `/etc/hosts` is rejected with "outside the target repo". This is
the one thing no `claude` flag does (Read accepts absolute paths by design). Disable
with `BRIDGE_CONFINE_READS=0` for trusted local cross-repo use.
- **No shell injection / no flag injection.** Argv list (no shell); the question is
passed after a `--` end-of-options separator so it can't be reparsed as a flag.
- **The server's own secrets aren't inherited.** `BRIDGE_*` env vars (incl.
`BRIDGE_TOKEN`) are stripped from the child env, and `/proc/self/environ` is outside
the repo so the read hook blocks it too.
- **http transport.** Refuses to start without `BRIDGE_TOKEN` (override with
`--insecure-no-auth`, itself refused on a non-loopback host), binds `127.0.0.1`,
constant-time token compare, concurrent subprocesses capped (`BRIDGE_MAX_CONCURRENT`).
**The honest caveats:**
- **This is policy enforcement, not a kernel sandbox.** The tool restriction and read
hook are evaluated by Claude Code host-side and are deterministic (the model can't
reason around them), but they are not an OS-level boundary. For a **fully untrusted**
client (e.g. exposed over a public tunnel), additionally wrap the process in an OS
sandbox (`sandbox-exec` on macOS, `bubblewrap` on Linux, or a container) so even a
hypothetical `claude`/Node compromise can't escape. (`code.claude.com/docs/sandboxing`.)
- **Files *inside* the repo are readable** — including any secrets committed there
(`.env`, keys). Point it at repos you trust to expose.
- **Prompt injection.** Untrusted content inside repo files could try to misdirect the
agent; the read-confinement hook limits the blast radius to the repo itself.
## Known limitations
- **State is in-memory and per-process.** Thread sessions and the cost ledger reset on
restart; there is no cross-restart persistence (by design for now). Idle threads are
dropped after `BRIDGE_THREAD_TTL` and evicted by LRU past `BRIDGE_MAX_THREADS`.
- **No mid-flight cancellation.** Cancelling the MCP request does not kill the running
`claude` subprocess; it finishes (bounded by `BRIDGE_TIMEOUT`).
- **Depends on the `claude` CLI's output schema.** It parses `--output-format
stream-json`; a future CLI change could require an update (the smoke test guards the
tool surface, not the parser).
- **Not independently security-audited.** Reviewed adversarially (incl. by separate AI
reviewers) but not professionally audited.
## Cost model
Each `ask_codebase` call spins up a fresh headless Claude Code agent, so dollar cost
varies with how much it explores. The win from `thread` reuse shows as a climbing
**cache-hit rate** and cheap short follow-ups — not a strictly lower price every call.
State is in-memory per server process; restarting resets threads and the ledger.
## Development
```sh
uv run tests/smoke.py # MCP handshake + tool-surface check (no API key needed)
uv build # build wheel + sdist
```
CI runs both on every push/PR (`.github/workflows/ci.yml`).
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 3 tools
The three tools have clearly distinct purposes: ask_codebase for Q&A with turn-based steering, bridge_cost for cost reporting, and bridge_forget for thread management. There is no overlap in functionality.
Tool names follow a predictable pattern with an underscore separator, though the primary tool uses 'ask_' while the auxiliary tools use 'bridge_' prefix. This is a common and acceptable namespace convention but slightly deviates from a fully uniform verb_noun pattern.
With three tools, the count is appropriate for the server's focused scope of codebase Q&A with supporting cost and thread management. Each tool serves a necessary function without redundancy.
The toolset covers the full workflow: asking questions with thread support, tracking costs, and forgetting threads when needed. No obvious gaps exist for the stated purpose of exploring a codebase with a conversational agent.