open source · Apache-2.0 · pre-alpha

Let agents read the code.
Never change it.

A read-only MCP server that lets any AI agent read code in git repositories — any branch, tag or commit, with its history — and never changes them.

Pre-alpha, on PyPI: uvx repo-reader-mcp or pip install repo-reader-mcp. A container image comes next.

  • 12 read-only tools
  • stdio or Streamable HTTP
  • groups like github: acme/shop-*
An agent asks, repo-reader-mcp answers read-only

youWhy does capture() retry — and since when?

→ blame(repo="shop-backend", path="src/payments/client.py", start_line=18)shop-backend · blame src/payments/client.py · main @ ef0faefaafd1 (2026-10-22) · lines 18–24 of 24 82822bb8dbc0 2026-08-10 Sam Ortiz — Add the payments client and the cart18	    def capture(self, order):47976fdfd726 2026-09-03 Dana Reyes — Retry card captures on gateway timeouts19	        for attempt in range(MAX_RETRIES):20	            try:21	                return self.gateway.capture(order.id, order.total_cents)22	            except GatewayTimeout:23	                sleep(backoff(attempt))24	        raise GatewayTimeout(order.id)
→ show_commit(repo="shop-backend", commit="47976fdfd726")  └ the message, and the file it changed

agentDana Reyes added the retry on 2026-09-03, in 47976fdfd726: the payment gateway times out under load, and a capture retried with backoff succeeds. (Read on main at ef0faefaafd1.)

Reads from
  • local repositories
  • GitHub
  • GitLab
  • Bitbucket
  • any git server
Answers
  • Claude Code
  • Claude Desktop
  • Cursor
  • Codex
  • any MCP client

history

Read the code as it was — not only as it is.

Every reading tool takes a ref: a branch, a tag, a commit id, main~3. A file as it was at a release, the history of one path, what a branch changes, a commit with its message — and, as in the exchange above, who last changed each line. All the tools →

A file as it was at a tag

Committed contents only, read from the commit — never from a working folder.

read_file
→ read_file(repo="shop-backend", ref="v2.3.0", path="src/payments/client.py", start_line=6) shop-backend · src/payments/client.py · v2.3.0 @ 82822bb8dbc0 (2026-08-10) · lines 6–11 of 11  6	class PaymentsClient: 7	    def __init__(self, gateway: Gateway): 8	        self.gateway = gateway 9	10	    def capture(self, order):11	        return self.gateway.capture(order.id, order.total_cents)

The history of a path

Newest first; narrowed by author, message or dates, and paged.

log
→ log(repo="shop-backend", path="src/payments/client.py") shop-backend · log of src/payments/client.py · main @ ef0faefaafd1 (2026-10-22) — newest first 47976fdfd726  2026-09-03  Dana Reyes  Retry card captures on gateway timeouts82822bb8dbc0  2026-08-10  Sam Ortiz  Add the payments client and the cart

What a branch changes

since_fork compares from the merge base — what a pull request would show.

diff
→ diff(repo="shop-backend", base="main", head="feature/refunds", since_fork=true, files_only=true) shop-backend · diff main @ ef0faefaafd1 (2026-10-22) → feature/refunds @ 65a2fa69f919 (2026-10-21) · from their merge base 47976fdfd726 1 file changed, +5 −0:  A  src/payments/refunds.py  +5

A commit, with its reason

The whole message, the files it changed, and the diff unless you ask for the files only.

show_commit
→ show_commit(repo="shop-backend", commit="47976fdfd726", files_only=true) shop-backend · commit 47976fdfd726Author: Dana Reyes, 2026-09-03T14:20:00+00:00Parent: 82822bb8dbc0     Retry card captures on gateway timeouts     The gateway times out under load; a capture retried with backoff succeeds. 1 file changed, +14 −1:  M  src/payments/client.py  +14 −1

search · grep

One question across every repository.

search looks for text or a regular expression in every repository at once, on each one's default branch, and answers with files and line numbers. grep searches one repository at any ref, with lines of context around each match.

  • No index to build or wait for: git reads the repositories as they are — measured at 0.2–0.4 s across 18 repositories.
  • Git is given the served files by name, so a file include / exclude keep out is never searched.
  • At most 5 lines a file in search, so one answer surveys many files.
search
→ search(pattern="currency") `currency` in 2 repositories (their default branches): 2 lines in 2 files of 2 repositories — at most 5 lines a file. ## shop-backend · main @ ef0faefaafd1 (2026-10-22)config/settings.yml  1: currency: EUR ## shop-frontend · main @ 6c7ad6d53a56 (2026-10-01)src/format/money.ts  1: export const currency = 'EUR';
grep
→ grep(repo="shop-backend", ref="v2.3.0", pattern="total_cents", context=1) shop-backend · grep `total_cents` · v2.3.0 @ 82822bb8dbc0 (2026-08-10) — 1 matching line in 1 file src/payments/client.py  10-     def capture(self, order):  11:         return self.gateway.capture(order.id, order.total_cents)

remote repositories · groups

A whole organisation, named by a pattern.

Name repositories on GitHub, GitLab, Bitbucket or any git server one by one, or a whole group at once — github: acme/shop-*, gitlab: acme/platform/**/svc-*. The server keeps a mirror of each; people's agents never clone anything. Remote repositories →

  1. The code host

    Lists a group's repositories before every sync: a new one joins, an archived or deleted one leaves.

  2. A mirror on the server

    A bare partial clone: the whole history of the kept branches and tags, the contents of served files only. Synced at start and every 10 minutes; force-pushes followed.

  3. Everyone's agents

    Read the mirrors through the server. Reading never reaches the network; older contents are fetched on purpose, for served paths only.

config.yaml
repositories:  - github: acme/shop-*      # every repository named shop-…    token_env: GITHUB_TOKEN  # holds a read-only token    skip: [shop-sandbox]     # names or patterns left out    description: One of the shop's services.    exclude: ["**/fixtures/**", "*.pem"]   - url: https://gitlab.example.com/acme/billing.git    token_env: GITLAB_TOKEN    token_user: oauth2    refs: [main, "release/*"]   # the branches and tags kept mirrors:  folder: /var/lib/repo-reader-mcp  every_minutes: 10
--check · the group's part
$ uv run repo-reader-mcp --config config.yaml --check…  github: acme/shop-*  https://github.com    the host lists 2 repositories it takes:    shop-api  acme/shop-api  (mirror made)    shop-web  acme/shop-web  (not mirrored yet)    left out: acme/shop-old: archived    left out: acme/shop-sandbox: skipped
list_repos · one of the group
→ list_repos() ## shop-apiOne of the shop's services. — The shop's public API.Mirrored from https://github.com/acme/shop-api.git (one of `github: acme/shop-*`), synced 2026-10-24 10:30 UTC.

A token is named by the variable that holds it — never written in the file — and goes to its host only, over https, in git's environment: never on a command line, in a URL or in an error.

safety

Safe to hand to a whole team.

Built to be pointed at many repositories and shared. Each protection has a test that fails when the protection is taken out. The whole safety model →

Read-only by construction

Only reading git commands, from a fixed set. No shell; nothing is checked out; never a push.

  • rev-parse
  • symbolic-ref
  • for-each-ref
  • ls-tree
  • cat-file
  • log
  • diff
  • blame
  • merge-base
  • grep

Only what you serve

include and exclude hold for every tool: a path left out is never listed, read, searched or diffed — and on a mirror, its contents never reach the server's disk.

include: ["docs/**", "README.md"]exclude: ["**/fixtures/**", "*.pem"]

An agent's input stays text

A ref is checked, then resolved to a commit id before git sees it — a branch named like an option is just read. Paths are literal and stay inside the commit's tree.

Secrets are hidden in every answer.

Every answer and every error passes a filter last: providers' tokens, passwords in URLs, private keys, values of settings named for secrets become [hidden: …], and the answer says how many it hid.

  • A guessed secret can't be confirmed — not by a match, a missing one, or the lines around it.
  • Files named for credentials are never served nor fetched into a mirror: .env*.pem*.keyid_rsaid_ed25519.netrc.pgpass*.tfstate.git-credentialsand more
  • The rules lean towards hiding: a value hidden by mistake costs a look at the host; a value shown costs a rotation.
secrets found in files
→ read_file(repo="shop-backend", path="config/deploy.yml") shop-backend · config/deploy.yml · main @ ef0faefaafd1 (2026-10-22) · lines 1–4 of 4 1	deploy:2	  host: shop.example.com3	  github_token: "[hidden: GitHub token]"4	  database: postgres://shop:[hidden: password]@db.example.com/shop [2 values that look like secrets are hidden as `[hidden: …]` — this server never shows them.]
files named for credentials
→ read_file(repo="shop-backend", path=".env") `.env` is outside what this server serves from shop-backend (its include / exclude)

guidance for agents

The repository's rules reach the agent reading it.

An agent reading through the server isn't inside the repository's folder, so its client loads none of the rules written for it. The server delivers them — with what the people who run it add. Guidance for agents →

AGENTS.md CLAUDE.md GEMINI.md
at the root, for the whole repository; in a folder, for that folder
.github/copilot-instructions.md
for the whole repository
.claude/rules/**/*.md .cursor/rules/**/*.mdc
for the whole repository, for the paths their frontmatter names, or — Cursor's — when it fits the task
.claude/skills/*/SKILL.md
a skill: named and described, read when it fits the task
config.yaml
guidance:              # from whoever runs the server  text: Say which commit you read. repositories:  - name: shop-api    path: /srv/code/shop-api    # guidance:          # or for one repository:    #   from: [{repo: acme-rules, path: shop-api}]    #   own: false       # ignore its own files — or take some:    #   own: [".claude/rules/architecture-*.md"]

Always read on the default branch — the one a team reviews — never on the branch an agent is reading.

read_guidance
→ read_guidance(repo="shop-api") === Guidance for `shop-api` ===Written for agents by the people who run this server and own `shop-api` — follow it while you work with `shop-api`. --- configuration (the server's configuration) ---Say which commit you read. --- AGENTS.md (shop-api @ main 5dd77a24995f) ---# shop-api Run `make test` before you suggest a change. Public endpoints live in `src/routes/`. --- .claude/rules/money.md (shop-api @ main 5dd77a24995f) — for src/billing/** ---Amounts are integer cents. Never use floats for money. More, when it applies:- `skill refund-flow`: How a refund moves through billing, payments and the ledger. — read it with read_guidance(repo="shop-api", name="refund-flow") when it fits the task=== End of guidance ===
read_file · a scoped rule rides along
→ read_file(repo="shop-api", path="src/billing/invoice.py") [`shop-api` carries guidance for agents from its owners: call read_guidance(repo="shop-api") once in this conversation, before you rely on this answer, and follow it.] === Guidance for `src/billing/invoice.py` in `shop-api` — follow it while you work there ===--- .claude/rules/money.md (shop-api @ main 5dd77a24995f) — for src/billing/** ---Amounts are integer cents. Never use floats for money.=== End of guidance — the answer follows === shop-api · src/billing/invoice.py · main @ 5dd77a24995f (2026-10-15) · lines 1–2 of 2 1	def invoice_total(lines):2	    return sum(line.amount_cents for line in lines)

any MCP client

One person over stdio. A whole team over HTTP.

Over stdio, each person's agent starts its own server. For a team, run one server on a shared machine: one set of mirrors, one configuration, one token to the code hosts — nothing to install on people's computers. Running it for a team →

stdio — one person
# one person's agent starts the server beside it, over stdio$ uvx repo-reader-mcp --config ~/repo-reader.yaml --check$ claude mcp add repo-reader -- \    uvx repo-reader-mcp --config ~/repo-reader.yaml
Streamable HTTP — a team
# one server for the team: Streamable HTTP, at /mcp$ export REPO_READER_MCP_TOKEN=…   # config.yaml: http.token_env$ uvx repo-reader-mcp --config config.yaml --http 0.0.0.0:8000 # each person's agent$ claude mcp add --transport http repo-reader \    https://code.example.com/mcp \    --header "Authorization: Bearer $REPO_READER_MCP_TOKEN"

where it fits

What it is — and isn't.

Deliberately narrow: reading code with its history, for agents, safely.

It is

  • A reader of git repositories for AI agents — files, folders, history, diffs, blame and search, at any branch, tag or commit.
  • One server for a team: local repositories and mirrors of any git host, a whole group by a pattern.
  • Configured in one YAML file; secrets named by their variables, never written in it.
  • Open source, Apache-2.0.

It isn't

  • A code host or a git client — it never commits, pushes or checks anything out.
  • Issues, pull requests or reviews — the hosts' own MCP servers do that.
  • An index or a code graph — search reads the repositories as they are.
  • A reader of uncommitted work — only what is committed is read.
  • Per-person access, yet — one token for everyone over HTTP, or a gateway in front decides who calls.

Point your agent at your repositories.

Run it with uvx, name the repositories in one YAML file, connect your agent over stdio — or run one server for the whole team.

Pre-alpha — on PyPI today; a container image comes next.