Skip to main content
Glama
maxbaluev

mcp-retrieve

by maxbaluev
README.md
# mcp-retrieve

An [MCP](https://modelcontextprotocol.io) server that exposes **late-interaction
document retrieval** (ColBERT-style MaxSim) over a local folder. Point it at a
directory of text/markdown/code, and any MCP client — Claude Desktop, an IDE
agent, your own host — can `index` it and `search` it with token-level
relevance.

It ships with a **deterministic, model-free default embedder**, so the server
and its full test suite run offline with **no model weights, no API key, no
network**. When you want production-grade semantics, drop in a real ColBERT /
ColQwen encoder behind a small protocol — ranking code does not change.

## What is MCP?

The Model Context Protocol is an open standard that lets LLM applications
connect to external tools and data through a uniform server interface. A host
(e.g. Claude Desktop) launches MCP servers and calls the **tools** they
advertise. `mcp-retrieve` is such a server; it advertises two tools:

| Tool | Purpose |
|------|---------|
| `index_folder(folder)` | Read text files under `folder`, chunk them, embed each chunk into a multi-vector representation, and build an in-memory index. |
| `search(query, k=5)` | Rank indexed chunks by MaxSim late interaction and return the top `k` with source path, score, and snippet. |

## The retrieval approach: late interaction (ColBERT)

Most dense retrievers compress a passage into one vector and compare it to one
query vector — cheap, but lossy. **Late interaction**, introduced by ColBERT
(Khattab & Zaharia, *ColBERT: Efficient and Effective Passage Search via
Contextualized Late Interaction over BERT*, SIGIR 2020,
[arXiv:2004.12832](https://arxiv.org/abs/2004.12832)), keeps **one vector per
token** for both the query and the document and defers their interaction to
scoring time via the **MaxSim** operator:

```
score(q, d) = Σ_i  max_j  sim(q_i, d_j)
```

Each query token `q_i` is matched to its single most similar document token
`d_j`, and those per-token maxima are summed. This preserves fine-grained term
matching (a query term can find its evidence anywhere in the passage) while
staying efficient. With L2-normalised vectors, `sim` is cosine similarity, so
MaxSim reduces to a dot product followed by a row-wise max and a sum — which is
exactly what `mcp_retrieve.retrieval.maxsim` computes.

## Install

```bash
pip install -e .          # core: mcp + numpy
pip install -e ".[dev]"   # plus pytest for the test suite
```

## Register with an MCP client

For **Claude Desktop**, add the server to its `mcpServers` config
(`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "mcp-retrieve": {
      "command": "mcp-retrieve"
    }
  }
}
```

If you installed into a virtual environment, use the absolute path to the
`mcp-retrieve` console script (or `"command": "python", "args": ["-m",
"mcp_retrieve"]`). Restart the client, then ask it to index a folder and search
it — the model will call the `index_folder` and `search` tools.

## Usage from Python

```python
from mcp_retrieve import RetrievalIndex

index = RetrievalIndex()                 # deterministic default embedder
index.index_folder("./docs")
for hit in index.search("late interaction maxsim", k=5):
    print(f"{hit.score:.3f}  {hit.chunk.source}  {hit.snippet}")
```

## Plugging in a real late-interaction model

The default `HashingEmbedder` makes the project run anywhere, but it matches on
character n-grams, not meaning. For real semantics, implement the `Embedder`
protocol around a trained encoder and pass it in:

```python
import numpy as np
from mcp_retrieve import RetrievalIndex
from mcp_retrieve.server import create_server

class ColbertEmbedder:
    """Wrap a ColBERT checkpoint as a multi-vector Embedder."""
    def __init__(self, checkpoint: str) -> None:
        from colbert.modeling.checkpoint import Checkpoint
        from colbert.infra import ColBERTConfig
        self._ckpt = Checkpoint(checkpoint, ColBERTConfig())

    @property
    def dim(self) -> int:
        return 128

    def embed(self, text: str) -> "np.ndarray":
        vecs = self._ckpt.docFromText([text])[0]   # (num_tokens, 128)
        return np.asarray(vecs, dtype=np.float32)

# Use it from Python …
index = RetrievalIndex(embedder=ColbertEmbedder("colbert-ir/colbertv2.0"))

# … or run the MCP server with it.
server = create_server(embedder=ColbertEmbedder("colbert-ir/colbertv2.0"))
server.run()
```

Any object exposing `dim: int` and `embed(text) -> ndarray[num_tokens, dim]`
with L2-normalised rows satisfies the protocol — ColBERT, ColQwen, ColPali, or
your own. The ranking and chunking code is encoder-agnostic.

## Architecture

```
src/mcp_retrieve/
  embedder.py    # Embedder protocol + deterministic HashingEmbedder default
  retrieval.py   # chunking, MaxSim, RetrievalIndex (pure — no MCP dependency)
  server.py      # FastMCP server exposing index_folder + search (only MCP import)
```

The retrieval and embedding cores import nothing MCP-related, so they are
testable and reusable on their own; the SDK is isolated to `server.py` and
imported lazily.

## Testing

```bash
python -m pytest
```

All retrieval and embedder tests run offline with the default embedder. The
end-to-end FastMCP tool test is skipped automatically when the `mcp` package is
not installed.

## License

[MIT](LICENSE) © 2026 Max Baluev

TDQS

A4.4/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have entirely non-overlapping purposes: one indexes a folder, the other performs searches. There is no ambiguity between them.

Naming Consistency5/5

Both tool names follow a clear verb_noun pattern (index_folder, search). The second tool uses a single verb, which is consistent and concise for its action.

Tool Count4/5

With only two tools, the server is minimal but well-scoped for its purpose (index and retrieve). It covers the essential operations without unnecessary extras.

Completeness4/5

The pair index+search provides a complete core workflow for retrieval. Missing operations like listing indexed files or deleting specific entries are minor gaps that do not break the main use case.

Maintenance

ActivityInactive
ResponsivenessNo issues