Skip to main content
Glama
fejinrp
by fejinrp
README.md
# lean-context-mcp

An MCP server that helps coding agents (Claude Code, Cursor, any MCP client) use **fewer tokens**.
Instead of reading whole files, dumping raw `grep` output, or pasting full test logs into the context, the agent asks for exactly what it needs:

- a file's **outline** (symbols and line ranges) instead of the whole file,
- **one function** instead of the whole file,
- a **compact search** that still lists every matching file,
- **test / lint / tsc output reduced to the failures**.

Measured on a real TypeScript monorepo: **84.5% fewer tokens** on like-for-like tasks, **81.7%** after counting the tool definitions themselves (details and honest limits [below](#measured-savings)).

## Quick start

Needs Node ≥ 20. [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) is recommended for fast search; without it a slower built-in JS search is used automatically.

```bash
npx -y lean-context-mcp        # starts the stdio server (your MCP client normally runs this for you)
```

### Claude Code

```bash
# for the current project (roots default to the directory Claude Code starts in)
claude mcp add lean-context -- npx -y lean-context-mcp

# for every project, with explicit roots
claude mcp add --scope user lean-context -e LEAN_ROOTS=/Users/you/code -- npx -y lean-context-mcp
```

### Cursor

`~/.cursor/mcp.json` (global) or `<repo>/.cursor/mcp.json` (project):

```json
{
  "mcpServers": {
    "lean-context": {
      "command": "npx",
      "args": ["-y", "lean-context-mcp"],
      "env": { "LEAN_ROOTS": "/Users/you/code/myrepo" }
    }
  }
}
```

Set `LEAN_ROOTS` for Cursor: the server's working directory is not guaranteed to be your project.

### Any other MCP client

Run `npx -y lean-context-mcp` as a **stdio** server (command `npx`, args `["-y", "lean-context-mcp"]`), with `LEAN_ROOTS` in its environment. You can also install it once with `npm i -g lean-context-mcp` and use the command `lean-context-mcp`.

### Make the agent use it

Agents prefer their built-in tools unless told otherwise. Add one line to `CLAUDE.md`, `AGENTS.md` or `.cursor/rules`:

> Use lean-context tools (`outline`, `read_symbol`, `search`, `run_compact`) before reading whole files or running raw commands.

## LEAN_ROOTS: what the server may touch

`LEAN_ROOTS` is the list of folders the tools may read, search and run commands in (separated by `:` or `,`). Anything outside, including `..` and symlink tricks, is refused. If it is not set, the server's current directory is the only root. Relative paths in tool calls resolve against the first root.

```bash
LEAN_ROOTS=/Users/you/code/app:/Users/you/code/lib
```

## Tools

| tool | what it returns |
|---|---|
| `outline` | A file's symbol map (kind, signature, line range) or a directory map (`file  name:count`). Uses tree-sitter for TS/JS/TSX, Python, Go, PHP and Swift, with a regex fallback for everything else. Honors `.gitignore` and skips `node_modules`, `vendor`, `dist`, `build`, `.git`, etc. |
| `read_symbol` | The source of one symbol (`fn`, `Class.method`, `Recv.Method`, `A::b`) or a `startLine`–`endLine` range. Suggests similar names on a miss. |
| `search` | ripgrep (JS fallback if `rg` is missing), grouped by file with per-file match counts. Literal or regex, globs, `context` (default 2). Up to `maxMatches` (default 30) matches are shown in full; above that it switches to compact mode: **every matching file is still listed** with its count, plus the first `perFile` (default 3) match lines without context, identical lines collapsed, and an explicit `+N more in this file (search path=…)` note. If the output cap is hit, later files keep their header line and say so; no file is ever dropped silently. |
| `run_compact` | Runs an **allowlisted** command with no shell and a timeout. Returns the exit code, the duration and only the failures, with dedupe and head+tail trimming. Has extractors for vitest, jest, phpunit/Pest, pytest, tsc, eslint and go test. The full log goes to a temp file (mode 600) whose path is returned. Small outputs (≤ ~1200 chars, or when squeezing would not shrink them) come back verbatim with only a `[ok]` / `[exit N]` marker, so they are never bigger than raw output plus a few tokens. |
| `summary_cache` | Per-file notes keyed by content hash in `~/.lean-context/cache.json`. A note is invalidated automatically when the file changes. |
| `project_notes` | Get, set or append `<repo>/.lean-context/notes.md`, for conventions, decisions and commands. |

The six tool definitions cost about **1,400 tokens** per session (measured).

## Measured savings

Measured on a ~10k-line TypeScript monorepo (Fastify + Prisma + workers) with `@anthropic-ai/tokenizer` (cl100k_base as a cross-check). "Before" is what an agent would otherwise read (whole file, raw `rg -n -C2`, raw command output); "after" is the tool's exact output. The run_compact rows ran on a scratch copy with injected failures.

| scenario | before tok | after tok | saved | needed info kept |
|---|---:|---:|---:|---|
| outline: approvals.ts | 3977 | 239 | 94% | yes |
| outline: orchestrator.ts | 4585 | 163 | 96.4% | yes |
| outline: auth.ts | 4179 | 72 | 98.3% | yes |
| read_symbol: decideApproval in approvals.ts | 3977 | 1174 | 70.5% | yes |
| read_symbol: createWorktree in git.ts | 2933 | 209 | 92.9% | yes |
| outline_dir: apps/worker/src read all files | 31314 | 127 | 99.6% | yes |
| outline_dir: apps/worker/src vs bare rg --files | 207 | 127 | 38.6% | yes |
| outline_dir: packages/core read all files | 20615 | 156 | 99.2% | yes |
| outline_dir: packages/core vs bare rg --files | 236 | 156 | 33.9% | yes |
| search: "consumeApproval" 14 matches | 2511 | 1678 | 33.2% | yes |
| search: "AppError" 120 matches | 16967 | 2102 | 87.6% | yes |
| search: "correlationId" 45 matches | 8158 | 1213 | 85.1% | yes |
| search: "AppError" full detail maxMatches 500 | 16967 | 11170 | 34.2% | yes |
| search: "correlationId" full detail maxMatches 500 | 8158 | 5298 | 35.1% | yes |
| run_compact: unit tests all passing exit 0 | 494 | 52 | 89.5% | yes |
| run_compact: unit tests 1 injected failure exit 1 | 939 | 249 | 73.5% | yes |
| run_compact: eslint 2 injected errors exit 1 | 120 | 123 | -2.5% | yes |
| run_compact: tsc injected type errors exit 2 | 66 | 70 | -6.1% | yes |
| schema: 6 tool definitions per session | 0 | 1398 | cost | yes |

- **Fair total** (default settings, only rows where all needed info was kept, folder view compared with a plain file list, duplicate full-detail rows left out): **49,349 → 7,627 tokens, 84.5% saved**.
- **Including the tool definitions** (1,398 tokens, paid once per session): **81.7% saved** for one session doing each task once.
- Comparing the folder view with reading every file instead gives 92.4%, but that overstates it, because the outline does not contain the code.

Honest limits:

- **Small command outputs** (≤ ~1,200 chars) come back verbatim with an `[exit N]` marker, so they cost a few tokens *more* than raw (eslint −2.5%, tsc −6.1% above).
- **Compact search** lists every matching file with its count, but shows only the first 3 match lines per file; the rest are named as `+N more in this file (search path=…)`. Searching that file, or raising `maxMatches`, shows everything. Full-detail search saves only ~35%.
- **Tool definitions** cost ~1.4k tokens per session, so a very short session can cost more than it saves.
- Savings depend on your repo and on the agent actually choosing these tools.
- `@anthropic-ai/tokenizer` matches older Claude models; for current models treat the numbers as close approximations. Ratios matter more than absolute counts.

## Configuration

Precedence is env > `LEAN_CONFIG` file (default `~/.lean-context/config.json`) > defaults. Config inside a repo is **never** read, so a cloned repo cannot widen its own allowlist.

| env | default | meaning |
|---|---|---|
| `LEAN_ROOTS` | cwd | Allowed roots, separated by `:` or `,`. Relative paths resolve against the first root. |
| `LEAN_ALLOW` | built-in list | Replaces the allowlist; patterns separated by `;`. |
| `LEAN_ALLOW_EXTRA` | — | Appends to the allowlist. |
| `LEAN_CACHE_DIR` | `~/.lean-context` | Location of `cache.json` and the default config file. |
| `LEAN_CONFIG` | `$LEAN_CACHE_DIR/config.json` | JSON config file `{ roots, allow, maxOutputChars, defaultTimeoutSec, maxTimeoutSec }`. |
| `LEAN_MAX_CHARS` | 8000 | Output cap per tool call. |
| `LEAN_TIMEOUT` | 300 | Default `run_compact` timeout in seconds. The maximum is 1800. |
| `LEAN_RG` | `rg` | Path to the ripgrep binary. |

The default allowlist is `npm test`, `npm run *`, `npx vitest`, `npx jest`, `npx tsc`, `npx eslint`, `tsc`, `eslint`, `phpunit`, `vendor/bin/phpunit`, `php artisan test`, `pytest`, `python -m pytest`, `python3 -m pytest` and `go test`. A pattern matches as a token prefix, and `*` stands for exactly one token. For example, `npm run *` allows `npm run lint -- --fix`.

## Security model

- Every path is realpath-checked against `LEAN_ROOTS`, so `..` and symlink escapes are rejected. Writes (notes) go through the same check, using the nearest existing ancestor.
- `run_compact` never uses a shell. It rejects shell operators and metacharacters (`|`, `&&`, `;`, `>`, backticks, `$(`), as well as program paths other than `vendor/bin/*` and `node_modules/.bin/*`. It also blocks arguments that would turn an allowed runner into arbitrary execution: `go -exec/-toolexec`, `npm --script-shell/--node-options`, npx `-p/--package/-c/--call` before the tool name, and `pytest -p/--pyargs/--rootdir/-c/--confcutdir`.
- Note that allowing `npm run *` or `npm test` means running whatever those scripts contain in a repo you trust. Use `LEAN_ALLOW` to narrow the list.
- On timeout, the whole process group is killed.
- The server only reads files and runs allowlisted commands. It makes no network calls and sends no telemetry.
- Run logs go to `$TMPDIR/lean-context/` (mode 600). Notes go to `~/.lean-context/cache.json` and `<repo>/.lean-context/notes.md`.

## Why it re-execs itself

Loading several tree-sitter WASM grammars can crash Node's optimizing WASM compiler ("Fatal process out of memory: Zone"). The entry point therefore relaunches itself once with `node --liftoff-only`. You don't need to pass the flag. Set `LEAN_NO_REEXEC=1` to disable it.

## Develop

```bash
npm install
npm run lint && npm run typecheck && npm test   # test builds first, then runs vitest
npm run measure -- /path/to/repo                 # token comparison on any repo
```

Grammars: the 7 tree-sitter grammars used (TS, TSX, JS, PHP, Python, Go, Swift) are copied from `tree-sitter-wasms` into `dist/grammars` at build time, so the installed package does not need that 50 MB dependency.

Squeezer fixtures in `test/fixtures/output` are real captured outputs, except `phpunit-*` and `artisan-fail`, which are hand-written in PHPUnit 10/11 and Pest formats.

## License

MIT

TDQS

B3.2/5.0

Scored across 6 tools

Disambiguation4/5

Tools are mostly distinct: outline (structure), read_symbol (content), search (find), run_compact (execute), and two memory tools. The only overlap is project_notes vs summary_cache, but descriptions clearly separate repo-level decisions from per-file notes.

Naming Consistency3/5

Mixed conventions: verb_noun (read_symbol, run_compact), bare verbs (search, outline), and noun phrases (project_notes, summary_cache). Still readable and predictable enough to navigate, but no single pattern holds.

Tool Count5/5

Six tools is well-scoped for a lean context server, with each tool covering a distinct stage of the read/navigate/remember workflow. No bloat or redundancy.

Completeness4/5

Covers navigation (outline, search), reading (read_symbol with line ranges), memory (project_notes, summary_cache get/set), and command execution with compact output. Minor gap: no explicit listing or invalidation control of cached notes beyond automatic file-change invalidation.

Maintenance

ActivityMaintained
ResponsivenessNo issues