Metadata-Version: 2.4
Name: leo-mcp
Version: 0.3.1
Summary: MCP server that gives your coding agent the real call graph of your repo (where / who_calls / impact / trace / guard, cited file:line) plus compressed AST context.
Author-email: Ismael Manzano Leon <manzzaano@pm.me>
Project-URL: Homepage, https://github.com/manzzaano/leo-code
Project-URL: Documentation, https://github.com/manzzaano/leo-code#readme
Project-URL: Repository, https://github.com/manzzaano/leo-code.git
Project-URL: Issues, https://github.com/manzzaano/leo-code/issues
Keywords: mcp,model-context-protocol,claude-code,code-search,static-analysis,ast,call-graph
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Compilers
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: mcp<2.0,>=1.0
Requires-Dist: orjson>=3.9
Requires-Dist: rank-bm25>=0.2
Requires-Dist: tree-sitter>=0.23
Requires-Dist: tree-sitter-typescript>=0.23
Requires-Dist: tree-sitter-javascript>=0.23
Requires-Dist: watchdog>=4
Provides-Extra: semantic
Requires-Dist: sentence-transformers>=2.7; extra == "semantic"
Requires-Dist: qdrant-client>=1.9; extra == "semantic"
Provides-Extra: http
Requires-Dist: fastapi>=0.115; extra == "http"
Requires-Dist: uvicorn>=0.30; extra == "http"
Requires-Dist: httpx>=0.27; extra == "http"
Requires-Dist: redis>=5; extra == "http"
Provides-Extra: dev
Requires-Dist: leo-mcp[http,semantic]; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: jedi>=0.19; extra == "dev"

<div align="center">

# leo-mcp

**Give your coding agent the real call graph of your repo, with proof,**
**instead of letting it guess by grepping and reading whole files.**

[![Tests](https://github.com/manzzaano/leo-code/actions/workflows/test.yml/badge.svg)](https://github.com/manzzaano/leo-code/actions/workflows/test.yml)
[![Formal Audit](https://github.com/manzzaano/leo-code/actions/workflows/audit.yml/badge.svg)](https://github.com/manzzaano/leo-code/actions/workflows/audit.yml)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](pyproject.toml)
[![Protocol](https://img.shields.io/badge/protocol-MCP-6b4fbb)](https://modelcontextprotocol.io)

<table>
<tr>
<td align="center" width="220"><b>100%</b><br/><sub>call-graph precision and recall<br/>vs. Python's own <code>ast</code></sub></td>
<td align="center" width="220"><b>80–97%</b><br/><sub>fewer tokens per lookup<br/>vs. reading the files</sub></td>
<td align="center" width="220"><b>&lt;2 ms</b><br/><sub>slowest graph query<br/>at 271k indexed symbols</sub></td>
</tr>
</table>

</div>

---

## Quickstart

leo-mcp is in beta, published on PyPI and npm:

```bash
uv tool install leo-mcp   # installs the leo-mcp command
leo-mcp doctor                  # checks your setup and prints the config snippet
claude mcp add leo-mcp -- leo-mcp   # Claude Code
```

Other clients: run `leo-mcp init --client opencode|cursor|codex` inside your project.

Requirements: [uv](https://docs.astral.sh/uv/), which fetches Python for you. Without uv: `pip install leo-mcp` with Python ≥ 3.11. Or without installing anything: `claude mcp add leo-mcp -- npx -y leo-mcp`.
No API key: leo-mcp never calls an LLM. It parses your repo locally.

## What your agent gets

Two tools, plus server instructions that tell the agent when to use them.

**`graph`** gives deterministic answers from the parsed call graph, each one cited `file:line`:

```
> graph op=who_calls symbol=get_session
[who_calls] get_session — 10 result(s), with proof:
  · execute_batch (method) @ src/agents/executor_agent.py:32
  · _persist_jobs_sync (function) @ main.py:42
  · analyze_and_report (method) @ src/agents/advisor_agent.py:38
  · jobs (endpoint) @ src/api/app.py:94
  ...
```

| op | answers |
|---|---|
| `where` | where a symbol is defined |
| `who_calls` | its direct callers |
| `impact` | everything that transitively depends on it: what breaks if you change it |
| `trace` | the call path from A to B, including frontend `fetch('/api/x')` → backend route |
| `guard` | `impact` plus which affected symbols have **no test** |

**`get_context`** takes a question in plain language and returns the AST subgraph that answers it: the relevant symbols with their bodies, compressed. Every response says what it saved:

```
[leo-mcp · 1,381 tokens of code context · the 6 source files it draws from are ~7,891 tokens (-82%) · 476 symbols indexed]
```

## CLI

```bash
leo-mcp index        # index the current repo and show what leo-mcp sees
leo-mcp doctor       # check the install, print the config snippet
leo-mcp init         # add leo-mcp to this project's .mcp.json + the pre-edit guard hook
leo-mcp init --no-hook   # ...without the hook
```

```
leo-mcp indexed C:\...\NEXUS  (build in 0.17s)
  476 symbols in 35 files
    python           450
    typescript        25
  most depended-on (run graph op=guard before touching these):
    click                             11 callers  src/automation/human_behavior.py:37
    get_session                       10 callers  src/database/db_session.py:26
```

## Languages, and what it doesn't do

- **Full AST:** Python (`ast`) and TypeScript/TSX/JavaScript (tree-sitter), including functions declared as variables or object properties (`const Page = () => …`, `const load = useCallback(() => …)`, `api = { jobs: () => get("/api/jobs") }`) and Python class attributes (`min_match_score: int = 60`).
- **Approximate** (regex-level): Go, Java, Rust, C/C++, C#, Ruby, PHP, Kotlin, Swift and others.
- **Skipped:** dependencies, build output (`node_modules`, `.next`, `dist`, `target`…), minified bundles, files over 512 KB, and anything in `.gitignore` when the repo uses git.
- **Not in the graph:** it is a static graph. Calls through WebSockets, subprocesses, queues, reflection or string-built dispatch are not edges, and using a type (a dataclass in an annotation) is not a call. Values that merely come out of a call (`const rows = items.filter(…)`) are not symbols.
- **Your files stay put.** The index lives in your user cache (`%LOCALAPPDATA%\leo-mcp` or `~/.cache/leo-mcp`), never inside your repo. It is kept up to date incrementally: a filesystem watcher re-parses only what actually changed, so a call that finds nothing new costs nothing (on a 20,000-file tree, re-scanning it cost 1.5-2.0 s per call).

## Configuration

| Env var | Default | Effect |
|---|---|---|
| `LEO_REPO` | client's working dir | repo to index when a tool call gives no `repo_path` |
| `LEO_SEMANTIC` | off | semantic recall in `get_context`; needs the `semantic` extra (`uv tool install "leo-mcp[semantic]"`, sentence-transformers, ~2 GB with torch). `graph` doesn't need it |
| `LEO_CACHE_DIR` | user cache | where indexes and logs go |
| `LEO_MAX_FILE_KB` | `512` | skip source files larger than this |
| `LEO_WATCH` | `1` | filesystem watcher; `0` falls back to re-scanning the tree (network mounts that drop events) |
| `LEO_GUARD_HOOK` | `1` | the warning before an edit; `0` silences it without touching your settings |
| `LEO_MCP_FROM` | `leo-mcp==<version>` | source `uvx` uses when the leo-mcp command isn't installed (local wheel, git URL) |


---

## It warns you before your agent breaks something

`leo-mcp init` also installs a `PreToolUse` hook in Claude Code. Before the agent edits a
symbol, leo looks up who depends on it. If some of those dependents have **no test**, it says
so — to the agent, so that it checks them, and to you, in one line:

```
leo: ensure_structural -> 208 dependents, 11 with no test
```

The rest of the time it says nothing, by design: a warning that fires on every edit is a
warning you turn off. Dependents that *are* covered by a test raise nothing, and editing a
test file raises nothing.

It costs 0.28 s per edit on a 1,220-symbol index. It never builds an index (no index yet →
silence), it goes quiet above a 4 MB index instead of adding seconds to every edit, and it
**never blocks an edit**: it has no way to. It emits no permission decision and always exits
0, and both invariants have tests.

**Its silence is not a guarantee.** Same static graph as the rest of the product: a dependency
through a WebSocket, a queue, a subprocess or a name built from strings is not in it, so the
list can be incomplete. It is a net, not a proof — the decision stays yours.

## The guarantee: structural correctness, proven

`graph` doesn't estimate structure. It walks the parsed graph, and CI checks that graph against parsers that are not leo's. `benchmark/audit_formal.py` re-derives every edge with Python's `ast` and the TypeScript compiler `tsc`, compares edge by edge, and a regression can't merge (`.github/workflows/audit.yml`).

Last green CI run (`FORMAL: 6/6 → IRREFUTABLE`):

| Check | Scale | Measured |
|---|---|---|
| (a) Python graph vs `ast` | leo-code + 3 external repos (~100,400 symbols, ~480,200 edges) | **100.000% precision · 100.000% recall** |
| (a-ts) TS/JS graph vs `tsc` | TypeScript compiler repo (36,739 symbols) | **99.950% precision · 99.926% recall** (31 false / 46 missed of 62.2k edges) |
| (b) guardian coverage vs `coverage.py` | 146 executed functions | **0 false** in either direction |
| (c) blast radius vs real mutation testing | mutated `classify_task`, `get_budget` | every failing test **⊆** predicted |
| (d) latency at scale | 271,493 indexed symbols | worst query **1.98 ms** · guardian **13 ms** |
| (e) post-edit budget | the graph rebuilt after one edit | **≤20 µs/symbol** amortized, and the index is never written inside a tool call |

The TS/JS figure is left unrounded.

## Measured with real agents

**opencode, 15 tasks × 3 runs, full token accounting including subagents** (2026-07-14):

| Metric | Threshold | Result |
|---|---|---|
| Answer quality (LLM judge) | with MCP ≥ without | ✅ +0.18 (5.00 vs 4.82) |
| Duration | ≤ +10% | ✅ −14.2% |
| End-to-end tokens | ≤ +10% median per task | ✅ +1.9% median · **−30.1% pooled** |

The honest claim: savings are per lookup and show up on heavy tasks (pooled −30%). On a typical task the token cost is neutral. What an agent without it can't get is a structurally correct answer. An early "−40% end-to-end" goal was dropped after six configurations measured worse or neutral.

**Field test on a project that isn't this one** (2026-09-15, NEXUS: FastAPI + Next.js/TSX, not a git repo, with 49 MB of saved web pages inside):

| | Result |
|---|---|
| Install from scratch (`uvx`, cold cache) | 6 s, 114 MB, no torch |
| Indexed | 476 symbols (Python 450 · TypeScript 25); 59 of 60 minified bundles skipped |
| Index time | cold 1.0 s · reload from cache 0.05 s (CLI) |
| `graph` latency | < 10 ms |
| `get_context` vs reading the files it covers | −82% and −90% on the two sample questions |
| Files written inside the project | 0 (directory listing diffed before/after) |

Then real agents answered 8 questions about NEXUS whose answers are known, each with and without leo-mcp (read-only, n=1 per question):

| | Claude Code + Sonnet | opencode + DeepSeek V4 Flash |
|---|---|---|
| Tokens, median per task | **−21%** | −4% |
| Tokens, pooled | **−26%** | **−19%** |
| Duration | +0.4% | −2.2% |
| Questions where the agent used leo-mcp | 8/8 | 6/8 |
| Correct answers (without → with) | 7/8 → 7/8 | 7/8 → 7/8 |

Savings came from structural questions (impact of changing a type −59%, "what happens when I press Start" −69%). Simple lookups cost the same or more. With n=1 per question, treat per-task numbers as noisy.

The field test found and fixed several bugs a user would have hit: TS/TSX was never indexed through MCP, two first-use hangs on Windows (a `git` call and a native import, both blocked by the stdio pipe), a Python `d.get()` counted as a caller of a TypeScript `get()`, `who_calls` cited the caller's definition line instead of the call line, and asking about an HTTP endpoint returned its signature without the body.

---

## How it works

```
source ─► AST (Python ast, tree-sitter) ─► capsules: functions/classes with calls, file:line
        ─► per-repo index in the user cache, updated incrementally
        ─► graph: where / who_calls / impact / trace / guard     (no model involved)
        ─► get_context: exact + BM25 + structural scorer (+ semantic, optional) → RRF → compressed
```

Frontend/backend boundary: a `fetch('/api/users')` is stitched to the `@app.get('/api/users')` that serves it (`core/boundary.py`), so `trace` follows data across services. Edges never cross languages by name alone: a Python `d.get()` is not a caller of a TypeScript `get()`.

```
leo_code/
├── cli.py              # leo-mcp serve | index | doctor | init
├── engine.py           # per-repo persistent index + hybrid retrieval + compression
├── core/               # parsers, graphquery (the graph), guardian, boundary
├── rag/                # indexer, compressor, bm25, scorer, optional vector store
└── server/mcp_server.py
npm/                    # npm launcher for a future npm release (runs the Python package via uv)
```

## Development

```bash
git clone https://github.com/manzzaano/leo-code.git && cd leo-code
pip install -e ".[dev]"
pytest -q                               # 279 tests, including an end-to-end stdio server test
node --test npm/bin/leo-mcp.test.js     # launcher
python benchmark/mcp_client_bench.py    # real MCP client, token-reduction gate
```

---

## En español

`leo-mcp` es un servidor MCP que le da a tu agente de código (Claude Code, opencode, Cursor, Codex) el grafo de llamadas real de tu repo. Con él responde dónde se define algo, quién lo llama, qué se rompe si lo cambias y el camino de A a B, citando siempre `archivo:línea`. Sin LLM y sin API key.

Está en beta, publicado en PyPI y npm:

```bash
uv tool install leo-mcp
leo-mcp doctor
claude mcp add leo-mcp -- leo-mcp
```

Requisitos: [uv](https://docs.astral.sh/uv/), o Python ≥ 3.11 con `pip install leo-mcp`. El índice vive en la caché de usuario, nunca dentro de tu proyecto. Los números y la metodología están arriba, con los comandos para reproducirlos. El caso de estudio completo está en [leosoftware.dev/sistemas/leo-mcp](https://leosoftware.dev/sistemas/leo-mcp).
