Content
# 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).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available