Skip to main content

repodigest-mcp

Token-efficient code context for AI coding assistants: Python signatures, call graphs and budget-packed context, served locally over MCP.

License: MIT Python 3.10+ MCP 2.x Tests: 61 passing

repodigest-mcp is a Model Context Protocol server that wraps RepoDigest's static analysis (AST parsing, call graph, BM25 search, token-budgeted packing) so that Claude Code, Cursor, or any MCP client can ask for exactly the slice of a codebase it needs instead of reading whole files.


The problem

Coding assistants explore a repository by reading files. To learn what one function accepts and returns, the assistant pulls in the entire file: every other function, every body, every import. On a real module that is routinely 90%+ noise for the question being asked, and it compounds over a session:

  • Context bloat. The window fills with code that doesn't matter, leaving less room for the code that does.
  • Cost and latency. You pay for every token in, on every subsequent turn.
  • Worse answers. Relevant details get buried in long contexts.

Most of what an assistant needs is structural, and structure can be extracted deterministically. A function's interface is its signature and docstring. Its blast radius is its callers and callees. The code relevant to a task is a neighbourhood in the call graph around the best-matching symbol. None of that needs a model; it needs an AST.

repodigest-mcp exposes those three operations as MCP tools. It runs as a local stdio subprocess: no daemon, no cloud service, no telemetry, and your source is parsed on your machine. (The only network access is tiktoken fetching its tokenizer vocabulary once on first use, after which it is cached.)

Tools

All three tools are read-only. Failures come back as MCP tool errors (is_error=True) with a message the model can act on, never as a crash or an empty result.

Tool Purpose
get_symbol_signature A function/class/method's signature and docstring, without the body
get_symbol_dependencies Direct callers and callees, across files
pack_task_context Best-matching code for a task, packed into a token budget

symbol_name accepts a fully-qualified name (pkg.mod.Class.method) or any dotted suffix (Class.method, method). path is the repository root and defaults to ., the server's working directory.

get_symbol_signature(symbol_name, path)

Returns the definition header and docstring only. Classes come back with every method stubbed to its signature. For a method in a large file this is typically 90%+ fewer tokens than reading the file (measured below).

# repodigest.search.ranker.SymbolRanker.score (repodigest/search/ranker.py)
def score(self, query: str, symbol: str) -> float:
    ...

The first line names the symbol the request resolved to and where it lives, which matters when you passed a short suffix.

get_symbol_dependencies(symbol_name, path)

Direct callers and callees from the repository call graph, across files. Returned as structured content:

{
  "symbol": "repodigest.search.ranker.SymbolRanker.rank",
  "kind": "method",
  "file": "repodigest/search/ranker.py",
  "callers": ["repodigest.search.ranker.SymbolRanker.top"],
  "callees": ["repodigest.search.ranker.SymbolRanker.score"]
}

Edges are matched by simple name, with no import or type resolution. Common names (get, run) can produce false positives, and Foo() links to the class Foo, not to Foo.__init__.

pack_task_context(query, budget, signatures_only, path)

Given a natural-language query, the tool:

  1. ranks every symbol in the repo with BM25 and takes the best match as the root;
  2. expands breadth-first through the call graph, alternating callees and callers, nearest first;
  3. packs symbols until the token budget is spent, skipping (never truncating) anything that no longer fits;
  4. returns compact XML.
Parameter Default Meaning
query required What you're working on, e.g. "score symbols with BM25"
budget 2000 Max tokens (cl100k_base) of packed source
signatures_only false Pack everything except the root as signature + docstring
path . Repository root

A real call, query="score symbols with BM25", budget=1500, signatures_only=true (abbreviated with …):

<context>
  <file path="…/repodigest/search/ranker.py">
    <symbol name="repodigest.search.ranker.SymbolRanker" kind="class" tokens="525">
<![CDATA[
class SymbolRanker:
    """BM25 ranking over a corpus of `{symbol: text}` documents."""
    …
]]>
    </symbol>
  </file>
  <file path="…/repodigest/cli.py">
    <symbol name="repodigest.cli.pack_command" kind="function" tokens="247">
      …
    </symbol>
  </file>
  <usage total_tokens="772" budget="1500" symbols_packed="2" symbols_skipped="0" />
</context>

The root is always included in full. Here it is a class, followed by one of its callers.

budget counts the packed source only. The XML tags around it are not counted, so leave roughly 10% headroom.

If the best match cannot fit, the tool says so instead of returning something misleading. This is a real response from the same repository at budget=500:

Error executing tool pack_task_context: Best match 'repodigest.search.ranker.SymbolRanker' needs 525 tokens but the budget is 500; raise budget to at least 525.

Errors the tools handle

Situation Behaviour
Symbol not found Error, with "did you mean" suggestions for near misses
Ambiguous suffix (run matches 2 methods) Error listing the candidates (first 5, then +N more)
path missing, not a directory, or empty Error naming the path
Directory with no Python symbols Error, rather than an empty result
Empty symbol_name / query, budget < 1 Error explaining the constraint
Root symbol larger than budget Error stating the budget needed
Unparsable or non-UTF-8 .py file Skipped with a warning on stderr; the rest is indexed

Architecture

MCP client (Claude Code, Cursor, ...)
        │  JSON-RPC over stdio
        ▼
server.py    three read-only tools; translates failures into tool errors
        │
        ▼
index.py     cached per-repo index: symbol registry · call graph · BM25 ranker
        │    file discovery, error-tolerant parsing, symbol resolution
        ▼
RepoDigest   py_parser · CallGraph · SymbolRanker · ContextPacker

RepoDigest does the analysis. index.py decides which files it sees and remembers the result.

Optimizations

mtime/size-cached indexer. Each repo root gets one index (registry, call graph, ranker), stamped with a fingerprint of (path, mtime, size) for every Python file. A repeated call against an unchanged tree reuses the index. Editing, adding or deleting a file changes the fingerprint and triggers a rebuild on the next call. A warm lookup on the RepoDigest repo took under 5 ms.

Virtual environments and vendored code are excluded automatically. Discovery prunes:

  • hidden directories (.venv, .git, .tox, .cache, ...);
  • any directory containing a pyvenv.cfg, so virtualenvs are caught whatever they are named (venv, myenv), while an ordinary package that merely happens to be called env is kept;
  • site-packages, node_modules and __pycache__.

This matters because RepoDigest's own directory walker globs every *.py under the root. Pointed at this project's directory, it collected 1,825 files (nearly all of them from the virtualenv) and took about 5 s. repodigest-mcp indexed the same directory in 0.03 s, and none of the virtualenv's symbols leaked into search results. If you deliberately pass a virtualenv as the root, it is indexed.

Fault-tolerant parsing. One file with a syntax error or a stray non-UTF-8 byte does not take down the index. That file is skipped and logged.

Protocol-safe logging. On stdio, stdout belongs to the protocol. All logging goes to stderr; use repodigest-mcp --log-level DEBUG to see more.

Token efficiency

Measured on RepoDigest's own source with cl100k_base. "Signature" is the exact string get_symbol_signature returns, header line included. "Saved" compares it to reading the file the symbol lives in.

Symbol Signature Symbol source Whole file Saved vs. file
ContextPacker.pack 40 200 1,221 96.7%
SymbolRanker.score 40 146 766 94.8%
CallGraph.from_files 48 346 877 94.5%
parse_source 110 196 1,803 93.9%
to_xml 37 189 450 91.8%
ContextPacker (class) 166 677 1,221 86.4%

Across these six symbols the saving versus reading the whole file is 86% to 97%. The gap narrows against the symbol's own body (44% to 86% here) because the body of a short function is not much larger than its signature: the big win comes from not reading the rest of the file. Your numbers will vary with file size and docstring density.

Installation

Requires Python 3.10+. repodigest is not published to PyPI, so install it from GitHub first, then install this package in editable mode:

git clone https://github.com/nagendra-kon/repodigest-mcp.git
cd repodigest-mcp

python3 -m venv venv
source venv/bin/activate

pip install "repodigest @ git+https://github.com/nagendra-kon/repodigest.git"   # the analysis engine
pip install -e ".[dev]"                                                           # this server + pytest

repodigest-mcp --version

If you already have a local RepoDigest checkout, pip install -e ../repodigest works in place of the GitHub line.

Claude Code

Register the server with the absolute path to the venv's executable, because the client launches it as a subprocess and it must use the interpreter that has the dependencies installed. From the repodigest-mcp directory:

claude mcp add repodigest -- "$(pwd)/venv/bin/repodigest-mcp"

Add --scope user to make it available in every project, or --scope project to share it via .mcp.json. Check it with claude mcp list.

Cursor

Add the server to .cursor/mcp.json in your project (or ~/.cursor/mcp.json for all projects):

{
  "mcpServers": {
    "repodigest": {
      "command": "/absolute/path/to/repodigest-mcp/venv/bin/repodigest-mcp",
      "args": []
    }
  }
}

Which repository does it read?

Tools default to path=".", the server's working directory. If your client starts the server somewhere other than the project you're working on, pass path explicitly (for example, tell the assistant which directory to use) or start the server from the right directory. The server is read-only, but path is not sandboxed: it will read .py files under any directory the client names.

Try it

Once registered, ask your assistant things like:

  • "Show me the signature of AuthService.login."
  • "What calls hash_password, and what does it call?"
  • "Pack context for adding rate limiting to the login handler, in 1,500 tokens."

Testing

pytest tests/ -v          # 61 tests, ~2 s

The suite runs against a synthetic multi-file project built in a temp directory. That project includes a fake virtualenv, a hidden directory, node_modules, a file with a syntax error and a non-UTF-8 file, so the exclusion and fault-tolerance paths are exercised for real.

File Tests Covers
tests/test_server.py 38 MCP client integration: every tool called through a real in-process MCP client session (schema, structured output, is_error results), plus a subprocess transport test that launches the installed repodigest-mcp console script over stdio and calls a tool. Also token-budget guarantees, signatures_only, ambiguous and unknown symbols, and invalid paths.
tests/test_index.py 23 Indexing edge cases: virtualenv, hidden-dir and vendored-dir exclusion (venv detected by pyvenv.cfg, not by name), root validation, skipped unparsable and undecodable files, cache reuse, invalidation on edit / add / delete, symbol resolution and BM25 matching.

The budget tests assert that reported total_tokens never exceeds budget, that the per-symbol token counts sum to the total, that smaller budgets pack fewer symbols and report what was skipped, and that a root symbol that cannot fit is an error rather than a silent empty result. Integration tests use the official MCP Python client.

Limitations

  • Python only. Symbols are top-level functions, classes and methods; nested functions are not indexed separately.
  • Name-based call graph. See the note under get_symbol_dependencies.
  • Token counts are a proxy. They use tiktoken's cl100k_base, not Claude's own tokenizer, so treat budgets as close approximations. The budget also excludes the XML markup.
  • mcp>=2.0 only. MCP SDK 2.x renamed FastMCP to MCPServer; this package targets the 2.x API.

License

MIT. Built on RepoDigest.

Release files for repodigest-mcp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for repodigest-mcp 0.1.0
File Size Uploaded
repodigest_mcp-0.1.0.tar.gz 22.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for repodigest-mcp 0.1.0
File Interpreter ABI Platform
repodigest_mcp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.6 kB

Release files / repodigest_mcp-0.1.0.tar.gz

Download URL repodigest_mcp-0.1.0.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f10b624ad05695279aecb8c4401800af2c48f450ac14826270a08f6f299a5d8a
BLAKE2b-256 checksum
How to use checksums
28979ed50182aa5aca5e99629b053f530920b644443622e75e1edd27cf8e827e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release files / repodigest_mcp-0.1.0-py3-none-any.whl

Download URL repodigest_mcp-0.1.0-py3-none-any.whl
Size 13.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a5c89284f76a1a668cdea343ee5447819da7297b5b21973ff65546aec8cb0ff7
BLAKE2b-256 checksum
How to use checksums
2fc32a62f249276146d7c8f543e60d9f983961b1348ad64094eb3e3f5f013e6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page