Skip to main content
Glama
radiovisual

zoekt-mcp

by radiovisual

zoekt-mcp

CI PyPI ghcr.io MCP Registry License

An MCP server that exposes Sourcegraph Zoekt code search to any MCP-capable AI agent — Claude Code, Claude Desktop, Cursor, MCP Inspector, etc. — so the agent can run fast, indexed, regex/symbol-aware code search over your repositories regardless of the language you're working in.

  • MCP server: Python, built on FastMCP, runs over stdio so clients can spawn it as a subprocess. Published to PyPI, ghcr.io, and listed in the official MCP Registry, so no clone is required to use it.

  • Backend: a zoekt-webserver you run yourself via the Docker Compose file attached to every GitHub release — or point the MCP server at any existing zoekt-webserver you have lying around.

  • Tools exposed: search_code, list_repos, get_file.

Architecture

flowchart LR
    subgraph Client["MCP client"]
        CC["Claude Code<br/>Claude Desktop<br/>Cursor, etc."]
    end

    subgraph Server["zoekt-mcp (Python)"]
        Tools["search_code<br/>list_repos<br/>get_file"]
    end

    subgraph Backend["Docker: zoekt backend"]
        Web["zoekt-webserver"]
        Idx[("zoekt index<br/>named volume")]
        Indexer["zoekt-indexer<br/>(one-shot)"]
    end

    Code[("Your code<br/>bind mount")]

    CC <-->|"stdio<br/>MCP protocol"| Tools
    Tools <-->|"HTTP JSON<br/>/api/search<br/>/api/list<br/>/print"| Web
    Web --> Idx
    Code --> Indexer
    Indexer --> Idx

How a single search flows through the system

sequenceDiagram
    actor You
    participant Claude as Claude Code
    participant MCP as zoekt-mcp
    participant Web as zoekt-webserver
    participant Idx as zoekt index

    You->>Claude: "where is getVideoId defined?"
    Claude->>MCP: search_code("sym:getVideoId")
    MCP->>Web: POST /api/search
    Web->>Idx: scan shards
    Idx-->>Web: matches + ctags symbols
    Web-->>MCP: raw JSON result
    Note over MCP: trim to {repo, file,<br/>line, text, symbols}
    MCP-->>Claude: shaped result
    Claude-->>You: "src/index.js:17 (function)"

Related MCP server: sourcegraph-mcp

Quickstart

Getting from "nothing installed" to "Claude can search my code" is three steps: install the MCP server, run the backend, wire it into your client. No git clone required in any of them.

Prerequisites

You need exactly one of these to run the MCP server, plus Docker for the backend:

  • uv on your PATH — for the uvx zoekt-mcp install path. MCP clients spawn the server via uvx, so which uv must resolve in whatever shell your client launches processes in. Install once per machine:

    # Official installer (macOS / Linux)
    curl -LsSf https://astral.sh/uv/install.sh | sh
    
    # Homebrew
    brew install uv
    
    # pipx
    pipx install uv

    The installer drops uv and uvx into ~/.local/bin/ (Linux/macOS) or %USERPROFILE%\.local\bin\ (Windows). Verify with uv --version.

  • …or Docker — for the docker run ghcr.io/radiovisual/zoekt-mcp install path. Any recent Docker Desktop or engine works. You need Docker anyway for the backend, so this path saves you from installing uv if you don't already have it.

And for the backend:

  • Docker with Compose v2 — runs zoekt-webserver and the one-shot indexer via the compose file attached to every release.

1. Start the backend (once per machine)

The zoekt backend is a regular Docker Compose stack you run yourself — zoekt-mcp does not lifecycle-manage it. Grab the compose file and helper script from the latest GitHub release and bring them up against whatever directory holds your code:

# Fetch the two files you need from the latest release.
mkdir -p ~/.zoekt-mcp && cd ~/.zoekt-mcp
curl -LO https://github.com/radiovisual/zoekt-mcp/releases/latest/download/docker-compose.yml
curl -LO https://github.com/radiovisual/zoekt-mcp/releases/latest/download/index.sh
chmod +x index.sh

# Point the indexer at any parent directory on your machine.
# Every top-level subdirectory becomes one searchable repo.
echo "ZOEKT_REPOS_DIR=/home/you/code" > .env

# Bring up zoekt-webserver + the one-shot indexer.
docker compose up -d

So if /home/you/code looks like this:

~/code/
├── project-a/       → indexed as zoekt repo "project-a"
├── project-b/       → indexed as zoekt repo "project-b"
└── scratch-notes/   → indexed as zoekt repo "scratch-notes"

…zoekt indexes all three repos in one pass and you can scope any query with repo:project-a — or leave repo: off to search across everything at once. See Indexing multiple codebases below for more on the one-server-many-repos model.

Sanity check:

curl -s -XPOST -d '{"Q":"repo:."}' http://localhost:6070/api/list \
  | python3 -m json.tool | head -20

You should see each subdirectory of ZOEKT_REPOS_DIR listed as a zoekt repo.

On macOS / Windows Docker Desktop, the path you pick must be under an allowed file-sharing root (check Docker Desktop → Settings → Resources → File Sharing). On Linux there's no such restriction.

Just want to try it without touching your real code directory? Clone the repo and use the in-tree test fixture: ./tests/fixtures/up.sh — see Development at the bottom of this file.

2. Wire the MCP server into your client

Two install paths — pick whichever matches your existing tooling. Both end up running the same versioned server binary; the only difference is how it's launched.

Path A — uvx (recommended if you already have uv)

uvx downloads the latest zoekt-mcp from PyPI on first invocation, caches it, and spawns it. No permanent install, no venv to manage.

Claude Code (~/.claude.json):

{
  "mcpServers": {
    "zoekt": {
      "type": "stdio",
      "command": "uvx",
      "args": ["zoekt-mcp"],
      "env": { "ZOEKT_URL": "http://localhost:6070" }
    }
  }
}

Or via the claude CLI:

claude mcp add zoekt \
    --env ZOEKT_URL=http://localhost:6070 \
    -- uvx zoekt-mcp

Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "zoekt": {
      "command": "uvx",
      "args": ["zoekt-mcp"],
      "env": { "ZOEKT_URL": "http://localhost:6070" }
    }
  }
}

Cursor (~/.cursor/mcp.json or .cursor/mcp.json in a project):

{
  "mcpServers": {
    "zoekt": {
      "command": "uvx",
      "args": ["zoekt-mcp"],
      "env": { "ZOEKT_URL": "http://localhost:6070" }
    }
  }
}

To pin a specific version instead of always using the latest:

"args": ["zoekt-mcp==0.1.0"]

Path B — Docker image (no Python tooling required)

If you already have Docker running for the backend and would rather not install uv, use the container image instead. MCP clients spawn it over stdio exactly like the uvx path.

Claude Code / Claude Desktop / Cursor:

{
  "mcpServers": {
    "zoekt": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "--network=host",
        "-e", "ZOEKT_URL=http://localhost:6070",
        "ghcr.io/radiovisual/zoekt-mcp:latest"
      ]
    }
  }
}

On Docker Desktop (macOS/Windows) the host isn't reachable via localhost from inside a container. Drop --network=host and use host.docker.internal instead:

"args": [
  "run", "-i", "--rm",
  "-e", "ZOEKT_URL=http://host.docker.internal:6070",
  "ghcr.io/radiovisual/zoekt-mcp:latest"
]

To pin a specific version, replace :latest with the semver tag (e.g. :0.1.0). The image is multi-arch (linux/amd64 + linux/arm64), so it works on Apple Silicon and ARM Linux hosts without extra flags.

3. Restart the client

Restart Claude Code / Claude Desktop / Cursor and the three tools (search_code, list_repos, get_file) should appear. Try something like "where is getVideoId defined?" and watch it call search_code("sym:getVideoId").

Indexing multiple codebases

One zoekt-mcp server handles as many repos as you want — that's the default. Every top-level subdirectory of ZOEKT_REPOS_DIR becomes a separate searchable repo in one shared index. The search_code tool can scope to a subset with repo:NAME (regex matched against repo names) or leave repo: off to search across everything.

If your projects live under different parent directories (e.g. ~/work/ and ~/personal/), the simplest fix is to create a single "index root" directory with symlinks pointing at each project and set ZOEKT_REPOS_DIR to that index root. One server, one config, all repos searchable.

mkdir -p ~/.zoekt-root
ln -s ~/work/project-a       ~/.zoekt-root/project-a
ln -s ~/personal/side-thing  ~/.zoekt-root/side-thing

cd ~/.zoekt-mcp
echo "ZOEKT_REPOS_DIR=$HOME/.zoekt-root" > .env
docker compose up -d

Docker has to follow the symlinks when it resolves the bind mount, which works on Linux but is hit-or-miss on Docker Desktop. If the linked directories don't show up inside the container, fall back to putting real directories (or clones) under ~/.zoekt-root instead of symlinks.

When you actually need two servers

A second zoekt-mcp instance is only worth the setup cost when you want fully isolated index pools — for example, keeping work code and personal code in completely separate search namespaces, or running two different backends (e.g. different zoekt versions) side by side. It is not needed just to index more code; one server with many subdirectories is the right tool for that.

If you genuinely want two instances:

  1. Copy ~/.zoekt-mcp/docker-compose.yml to a second file, e.g. ~/.zoekt-mcp/docker-compose.personal.yml.

  2. In the copy, change:

    • the compose project name: (e.g. zoekt-mcp-personal)

    • the host port mapping (e.g. 6071:6070)

    • the named volume (e.g. zoekt-mcp-personal-index)

    • the container names (e.g. zoekt-mcp-personal-webserver)

  3. Give the second stack its own env file, e.g. ~/.zoekt-mcp/.env.personal, pointing ZOEKT_REPOS_DIR at a different directory.

  4. Bring each stack up with its own compose file and env file:

    cd ~/.zoekt-mcp
    docker compose up -d
    docker compose -f docker-compose.personal.yml \
      --env-file .env.personal up -d
  5. Wire both into your MCP client as distinct servers — same zoekt-mcp binary, different ZOEKT_URL values:

    {
      "mcpServers": {
        "zoekt-work": {
          "command": "uvx",
          "args": ["zoekt-mcp"],
          "env": { "ZOEKT_URL": "http://localhost:6070" }
        },
        "zoekt-personal": {
          "command": "uvx",
          "args": ["zoekt-mcp"],
          "env": { "ZOEKT_URL": "http://localhost:6071" }
        }
      }
    }

Claude Code sees two independent sets of tools (search_code / list_repos / get_file from each namespace) and decides which to call based on the question.

For most users, one server with a well-populated ZOEKT_REPOS_DIR is all you need. Don't reach for multi-server unless you have a concrete reason to isolate.

Advanced: staging code under a dedicated repos directory

As an alternative to pointing ZOEKT_REPOS_DIR at your real code, you can create a dedicated staging directory and drop clones or directories into it. Useful when you can't expose your real code directory to Docker (e.g. corporate file-sharing restrictions on Docker Desktop), or for one-off experiments with a repo you don't have locally:

mkdir -p ~/.zoekt-mcp/repos
git clone https://github.com/myorg/myrepo ~/.zoekt-mcp/repos/myrepo

cd ~/.zoekt-mcp
echo "ZOEKT_REPOS_DIR=$HOME/.zoekt-mcp/repos" > .env
docker compose up -d

The trade-off is a freshness trap: you now have two copies of every project — the one you actually edit, and the staged copy. Re-running the indexer re-reads the staged copy, so you'd need to git pull (or cp -r your edits) inside ~/.zoekt-mcp/repos/myrepo/ before each re-index. Prefer pointing ZOEKT_REPOS_DIR at your live code directory unless you have a specific reason not to.

Tool surface

Tool

Parameters

Returns

search_code

query: str, max_results: int = 50, context_lines: int = 3

{query, file_count, match_count, duration_ns, files: [{repo, file, language, branches, matches: [{line, text, ranges, symbols}]}]}

list_repos

filter: str = "" (optional repo: atom)

{count, repos: [{name, url, branches, index_time}]}

get_file

repo: str, path: str, branch: str = "HEAD"

{repo, path, branch, content}

Query language

Zoekt's query DSL (full reference):

Atom

Example

Meaning

repo:

repo:flask-app

Restrict to repos whose name matches (regex)

file:

file:app.py

Restrict to file paths matching

lang:

lang:python

Restrict to a language

sym:

sym:list_users

Match symbol definitions

case:yes

case:yes Foo

Case-sensitive content match

/regex/

/users?/

Regex content match

(whitespace)

lang:go func main

Boolean AND

or

def hello or function hello

Boolean OR

Keeping the index fresh

Zoekt searches a pre-built index, not your files directly. When you edit code, the index doesn't auto-update — your next search can return stale line numbers, miss newly-added symbols, or point Claude at functions that have moved or been renamed. Stale search is the main thing that burns tokens, because Claude falls back to reading whole files with get_file when search_code returns nothing useful.

Here's what happens every time the indexer runs:

flowchart LR
    Src["Your code<br/>(live files)"]
    Mount["/src<br/>(read-only<br/>bind mount)"]
    Scratch["/tmp/{repo}<br/>(ephemeral<br/>copy)"]
    Git["throwaway<br/>git repo<br/>+ snapshot commit"]
    Shard[("index shard<br/>/data/*.zoekt")]

    Src -->|bind mount| Mount
    Mount -->|cp -r| Scratch
    Scratch -->|"git init; git add -A;<br/>git commit"| Git
    Git -->|zoekt-git-index| Shard

The copy to /tmp/ is ephemeral — it happens fresh on every indexer run and never touches your real files. Each refresh always reads whatever is currently in the mounted source directory.

Fortunately, re-indexing is fast (seconds, even for large repos), runs entirely in Docker, involves no LLM calls, and costs zero tokens. You just need to decide how you want to trigger it.

Because the main quickstart already points ZOEKT_REPOS_DIR at your live code directory, every re-index automatically reflects your latest edits — no copy step to keep in sync. (If you're on the advanced staging workflow instead, update the clones under ~/.zoekt-mcp/repos/ before you trigger a re-index, otherwise zoekt just re-reads the stale copies.)

Recipes for triggering the re-index

All four recipes run out-of-band — no Claude, no tokens, no context window involvement. Pick whichever matches how you work.

1. Manual re-index

Run ~/.zoekt-mcp/index.sh whenever you know you've made significant changes. The script runs just the indexer container against the current ZOEKT_REPOS_DIR without bouncing the webserver, so search stays available throughout.

~/.zoekt-mcp/index.sh

Good when: you only use Claude for occasional sessions and don't mind typing one command before you start. Zero background cost.

2. Cron (scheduled re-index)

Background re-index on a schedule. No manual step, slightly stale between ticks.

# Re-index every 15 minutes
*/15 * * * * cd ~/.zoekt-mcp && ./index.sh >/dev/null 2>&1

Good when: you work on code most days and want fresh-ish search any time you open Claude. Once an hour is fine for most users.

3. Filesystem watcher

React to file changes in near-real-time via inotifywait (Linux) or fswatch (macOS). Catches every edit, idle otherwise.

# Linux: one-liner, run it in a tmux pane or as a systemd --user service
while inotifywait -r -e modify,create,delete,move \
    --exclude '\.git/|node_modules/|__pycache__/' \
    /path/to/your/code 2>/dev/null; do
  ~/.zoekt-mcp/index.sh
done
# macOS equivalent with fswatch (brew install fswatch)
fswatch -o /Users/you/code | xargs -n1 -I{} ~/.zoekt-mcp/index.sh

Good when: you want "search is always current, no matter when I ask." Caveat: on projects with noisy tooling (compilers writing to build dirs, IDE lockfiles), the excludes list is important — without them you'll re-index constantly.

4. Claude Code SessionStart hook

Re-index every time you launch a new Claude Code session, so the first search of every session is guaranteed fresh. This is probably the best default for most users: no background process, no cron entry, and freshness is tied exactly to when you'd actually notice staleness.

// ~/.claude.json
{
  "hooks": {
    "SessionStart": [
      {
        "command": "$HOME/.zoekt-mcp/index.sh"
      }
    ]
  }
}

Good when: you want zero ongoing processes and guaranteed fresh search at the moment you actually use Claude. The session start is blocked on the re-index, but that's a few seconds at most.

Which one should I pick?

If you…

Use

…occasionally fire up Claude and don't mind a manual step

Recipe 1 (manual)

…want "set it and forget it" but tolerate N-minute staleness

Recipe 2 (cron)

…want always-fresh search and can tune the exclude list

Recipe 3 (watcher)

…mostly interact with code via Claude Code sessions

Recipe 4 (SessionStart hook)

None of these recipes are exclusive — e.g. running cron and the SessionStart hook is fine if you want both ambient freshness and a guarantee at session start.

Manual testing with MCP Inspector

npx @modelcontextprotocol/inspector uvx --from . zoekt-mcp

The Inspector opens a browser UI on http://localhost:6274. Under Toolssearch_code, try:

  • lang:python def hello — expect a match in flask-app/app.py

  • lang:javascript USERS — expect a match in express-app/index.js

  • sym:users — expect matches in both examples

Under Tools → list_repos, an empty filter should return both flask-app and express-app.

Development

This section is for hacking on zoekt-mcp itself. If you just want to use it, the Quickstart above covers everything — no clone required. Only come here if you want to change the Python server, run the full test suite, or cut a release.

Setup

Clone the repo and let uv manage the venv for you:

git clone https://github.com/radiovisual/zoekt-mcp
cd zoekt-mcp
uv sync

uv sync creates .venv/, resolves everything against uv.lock, and installs all runtime + dev dependencies. The dev group (pytest, pytest-asyncio, respx, ruff, pre-commit, pymarkdownlnt) is installed by default; pass uv sync --no-dev for a runtime-only install.

Common dev commands:

uv run pytest                    # run the full test suite
uv run zoekt-mcp --help          # run the CLI from source
uv add <package>                 # add a new runtime dep
uv add --dev <package>           # add a new dev dep
uv lock --upgrade                # refresh uv.lock

To run zoekt-mcp from your local clone against a running backend (e.g. while iterating on the server code):

uv run zoekt-mcp --zoekt-url http://localhost:6070

Releasing

Releases are fully automated — a tag push triggers the pipeline that publishes to PyPI and ghcr.io and cuts a GitHub release with the compose file attached. See RELEASING.md for the cut-a-release flow (helper script + manual paths) and the one-time PyPI/GHCR setup required before the first tag.

Commit routine

Linting and tests are wired into the git flow via a pre-commit hook so you never have to remember to run them by hand. After uv sync, install both hook types once per clone:

uv run pre-commit install --hook-type pre-commit --hook-type pre-push

From then on, every git commit runs:

  • ruff (ruff check + ruff format --check) against staged Python files — config lives under [tool.ruff] in pyproject.toml.

  • pymarkdownlnt (pymarkdown scan) against staged Markdown files — config lives under [tool.pymarkdown] in pyproject.toml. We disable MD013 (line length) and MD046 (code block style) because they fight readable prose and wide tables, and MD033 so the troubleshooting <details> blocks are allowed.

And every git push runs the offline pytest suites (tests/test_client_unit.py and tests/test_server_shaping.py) before the push leaves the machine, so a broken test can never hit the remote. Tests are scoped to pre-push rather than pre-commit to keep local commits snappy; the integration suite is excluded because it needs a running zoekt-webserver.

The hooks shell out to uv run, so the tool versions pinned in uv.lock are what runs locally and in CI — no drift between environments. The same linters and the same unit tests run on every push to main and every pull request via .github/workflows/ci.yml.

To run everything manually (e.g. before opening a PR):

uv run pre-commit run --all-files

To fix Python formatting in place rather than just checking it:

uv run ruff format
uv run ruff check --fix

Automated tests

# Unit tests (no Docker required)
uv run pytest tests/test_client_unit.py tests/test_server_shaping.py -v

# Integration tests: bring the stack up against the examples/ corpus,
# then run the live assertions.
./tests/fixtures/up.sh
uv run pytest tests/test_integration.py -v
./tests/fixtures/down.sh

tests/fixtures/up.sh sets ZOEKT_REPOS_DIR=../examples and invokes the same deploy/docker-compose.yml, so the test fixtures don't leak into the production deploy path. The integration tests skip automatically when ZOEKT_URL is unreachable, so a plain uv run pytest in a fresh checkout without Docker still passes.

Configuration

Setting

Env var

Flag

Default

Zoekt backend URL

ZOEKT_URL

--zoekt-url

http://localhost:6070

HTTP timeout (s)

ZOEKT_TIMEOUT

--timeout

30

The env var and the flag are equivalent — pick whichever fits your MCP client's config shape better. Most clients set environment variables via an "env" block in their JSON config, which is why the uvx and Docker snippets above use ZOEKT_URL rather than --zoekt-url.

Repo layout

zoekt-mcp/
├── src/zoekt_mcp/         # the Python MCP server
├── tests/
│   ├── test_client_unit.py     # offline unit tests
│   ├── test_integration.py     # live tests (skip when backend down)
│   └── fixtures/               # test-only helpers (up.sh / down.sh)
├── deploy/
│   ├── docker-compose.yml      # generic zoekt backend (env-driven)
│   └── repos/                  # user-populated source mount (gitignored)
└── examples/
    ├── flask-app/              # Flask verification corpus
    └── express-app/            # Express verification corpus

Troubleshooting

Common indexing pitfalls, in Q&A form. Click any question to expand the answer.

Nine times out of ten the index doesn't actually contain your code — zoekt is searching a different (or stale) corpus. The MCP server itself doesn't filter or rewrite queries; whatever you send goes straight to /api/search, so 0 hits means 0 hits in the index.

Diagnose it in three steps:

  1. Ask the agent to call list_repos (or curl -s -XPOST -d '{"Q":"repo:."}' http://localhost:6070/api/list). This is the source of truth for what zoekt can see.

  2. If your project isn't in the list, the indexer was pointed somewhere else. Common culprits:

    • ~/.zoekt-mcp/.env is missing or has the wrong ZOEKT_REPOS_DIR, so docker compose up indexed an empty or unexpected directory.

    • Someone ran ./tests/fixtures/up.sh from a dev clone, which sets ZOEKT_REPOS_DIR=../examples and indexes only examples/express-app and examples/flask-app.

    • The indexer wipes /data/* on every run, so a previous good run does not persist alongside a later one — the most recent indexer invocation is the only thing the webserver can see.

  3. Re-run the indexer against the right directory:

    cd ~/.zoekt-mcp
    echo "ZOEKT_REPOS_DIR=/absolute/path/to/parent-of-your-repo" > .env
    docker compose up -d --force-recreate zoekt-indexer

    ZOEKT_REPOS_DIR must be a parent directory; every top-level subdirectory under it becomes one repo. Re-run list_repos after the indexer exits to confirm.

The directory pointed at by ZOEKT_REPOS_DIR has no top-level subdirectories the indexer could turn into repos. Set ZOEKT_REPOS_DIR to a parent that already contains your project subdirectories:

cd ~/.zoekt-mcp
echo "ZOEKT_REPOS_DIR=$HOME/code" > .env
docker compose up -d

Loose files at the top of ZOEKT_REPOS_DIR are ignored — the loop in the compose file only iterates over directories.

Those are the in-repo verification fixtures under examples/ in a dev clone. They end up in your index when something — usually tests/fixtures/up.sh from a local clone — ran the indexer with ZOEKT_REPOS_DIR=../examples. Re-index against your real project directory (see the first Q&A above) and they'll be replaced; the indexer wipes /data/ at the start of every run, so there's no need to clean up separately.

The index is a snapshot, not a live view. zoekt only sees what was in ZOEKT_REPOS_DIR the last time the indexer ran. Trigger a refresh with ~/.zoekt-mcp/index.sh, or set up one of the four automation recipes in Keeping the index fresh so it happens on its own. Re-indexing is fast (seconds, even for large repos) and runs entirely in Docker — no LLM calls, zero token cost.

The webserver was started without -rpc, so /api/* falls through to the HTML search handler. The release-bundled docker-compose.yml already passes -rpc (see the command: block under zoekt-webserver); if you're running your own zoekt-webserver elsewhere, add -rpc to its argv and restart.

License

MIT — see LICENSE.

Available Tools

3 tools
get_fileA

Fetch the full contents of a file from an indexed repository.

    ``repo`` is the repository name as reported by ``list_repos``;
    ``path`` is the file path within that repo; ``branch`` defaults
    to ``HEAD`` but can be any branch zoekt has indexed. Returns
    ``{"repo", "path", "branch", "content"}``.
    
ParametersJSON Schema
NameRequiredDescriptionDefault
repoYes
pathYes
branchNoHEAD

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Describes the return format and parameter usage, but does not mention error conditions or failure modes (e.g., file not found), and annotations are absent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise three-sentence description with clear front-loading: first sentence states purpose, second details parameters, third mentions return format, with no extraneous content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Adequate for a simple fetch tool: covers inputs, defaults, and output structure. Could mention that path is relative to repo root, but overall sufficient.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Adds meaning to each parameter (repo from list_repos, path within repo, branch default) despite 0% schema description coverage, though does not specify format constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states the tool fetches file contents from a repository, using specific verbs and resources. However, it does not explicitly differentiate from siblings like search_code, but the action is distinct enough.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides context by referencing list_repos for repo names and explaining branch defaults, but lacks explicit guidance on when to avoid this tool or when to use search_code instead.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_reposA

List indexed repositories.

    ``filter`` is an optional ``repo:`` atom (e.g. ``repo:flask`` or
    ``repo:.`` for all) used to narrow the list. Leave empty to list
    every indexed repository.

    Returns ``{"repos": [{name, url, branches, ...}, ...], "count": N}``.
    
ParametersJSON Schema
NameRequiredDescriptionDefault
filterNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the full burden. It discloses the return format (JSON with repos and count) but lacks details on safety (read-only nature), error conditions, or any side effects. Adequate but not thorough.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, all front-loaded with the core purpose. Every sentence is necessary: purpose, parameter explanation, return format. No redundant information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity and the presence of an output schema (mentioned in description), the description covers purpose, parameter, and return format. It could mention pagination or authentication, but the current level is sufficient for a list operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema provides no description for the 'filter' parameter (0% coverage). The description compensates by explaining the filter as an optional 'repo:' atom with concrete examples, adding significant meaning beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'List indexed repositories' with a specific verb and resource. It distinguishes from sibling tools (get_file, search_code) by focusing on listing repositories, not fetching single files or searching code.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context on when to use the tool (listing all repos or filtered by repo: atom). No explicit exclusions or comparisons with siblings, but the purpose is straightforward enough that the usage is implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_codeA

Run a Zoekt query against the indexed corpus.

    Query syntax highlights:

    - ``repo:NAME`` restrict to repos whose name matches NAME (regex)
    - ``file:PATH`` restrict to files whose path matches PATH (regex)
    - ``lang:LANGUAGE`` restrict to a language (``python``, ``go``, ...)
    - ``sym:IDENT`` match symbol definitions (functions, classes, ...)
    - ``case:yes`` case-sensitive content match
    - ``/regex/`` regex content match (literal match by default)
    - whitespace is AND; use ``or`` for boolean OR

    Examples:

    - ``lang:python def hello`` — Python files containing ``def hello``
    - ``sym:users`` — anything that defines a ``users`` symbol
    - ``repo:flask-app file:app.py`` — within one file of one repo

    Returns a compact object with ``files`` (each containing ``repo``,
    ``file``, ``language``, and ``matches``) plus top-level stats.
    Results are capped at ``max_results`` files.
    
ParametersJSON Schema
NameRequiredDescriptionDefault
queryYes
max_resultsNo
context_linesNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explains that results are capped at max_results and returns a compact object with files and stats. Since no annotations are provided, it carries the full burden; it is mostly transparent about the read-only nature and output structure, though it could mention authentication or rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with bullet points and examples, front-loading the purpose. Every sentence adds value, and it is appropriately sized for the tool's complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the existence of an output schema, the description adequately describes the return structure. It comprehensively covers query syntax but misses explaining the 'context_lines' parameter, and does not address error scenarios.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It thoroughly explains the 'query' parameter syntax but offers minimal detail on 'max_results' (only that results are capped) and no detail on 'context_lines'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Run a Zoekt query against the indexed corpus,' which is a specific verb-resource combination. It distinguishes itself from sibling tools 'get_file' and 'list_repos' by focusing on search across repos.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides detailed query syntax and examples, guiding effective use. However, it does not explicitly contrast with sibling tools or specify when not to use this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.3.2
    • First observedget_file
    • First observedlist_repos
    • First observedsearch_code

TDQS

A4.2/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing repositories, searching code, and retrieving file contents. No ambiguity between them.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern: list_repos, search_code, get_file.

Tool Count5/5

Three tools cover the essential operations for a code search index server (list, search, fetch) without being too few or too many.

Completeness4/5

The set covers the core workflows well. A minor gap might be listing files within a repository, but the current set is sufficient for common use cases.

Maintenance

ActivityInactive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides two-modal code search (lexical via Zoekt and semantic via ChromaDB embeddings) for AI coding agents through MCP tools, enabling fast regex and meaning-based code lookup.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server exposing Sourcegraph's AI-enhanced code search capabilities to coding agents, enabling advanced code search, repository discovery, and content fetching.
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Exposes Cursor's semantic code search through MCP, enabling tools like codebase_search to query indexed repos.
    1
    -
  • F
    license
    A
    quality
    B
    maintenance
    Provides a code search service via MCP, allowing AI clients to perform fast, indexed searches across large codebases using Zoekt, with support for filters like repository, language, and path.
    2
    -