rag-mcp
# rag-mcp
An MCP server that gives a language model tools for searching a document
index — built against [rag-service](https://github.com/samishekhalard/rag-service).
```bash
pip install -r requirements.txt
RAG_MCP_SERVICE_URL=http://localhost:8000 python -m rag_mcp.server
```
## What it does
The Model Context Protocol is how a model gets tools it can call. This server
speaks MCP over stdio and exposes five:
| Tool | What it returns | Cost |
|---|---|---|
| `rag_search` | Ranked passages with document ids, scores and chunk ids | retrieval only |
| `rag_read_around` | One chunk in full plus its neighbours, by id | none |
| `rag_read_document` | An entire indexed document, in order | none |
| `rag_index_stats` | Chunk count, models, reranker, tracing state | none |
| `rag_answer` | A generated answer with sources, per-stage latency and USD cost | calls an LLM |
The tools are designed to chain: `rag_search` returns chunk ids, and
`rag_read_around` takes one and reads around it. The integration test drives
exactly that chain against a live service, so the promise in the tool
descriptions is checked rather than asserted.
## Connecting it to Claude Code
```json
{
"mcpServers": {
"rag": {
"command": "python",
"args": ["-m", "rag_mcp.server"],
"env": {
"PYTHONPATH": "/absolute/path/to/rag-mcp/src",
"RAG_MCP_SERVICE_URL": "http://localhost:8000"
}
}
}
}
```
Start `rag-service` first (`docker compose up` in that repo), then restart
Claude Code. The tools appear as `rag_search`, `rag_answer` and so on.
| Variable | Default | Meaning |
|---|---|---|
| `RAG_MCP_SERVICE_URL` | `http://localhost:8000` | Where the RAG service listens |
| `RAG_MCP_TIMEOUT` | `30` | Seconds before a request gives up |
## Three decisions worth explaining
**The tool description is the API.** A model picks a tool from its description
alone — it never sees the implementation. `rag_search` and `rag_answer` overlap,
so each description says when the *other* one is the better choice, and both
state what they cost. Tool descriptions here are written as carefully as the
code, because they are the part that determines whether the tools get used
correctly.
**Errors are instructions, not tracebacks.** If the service is down, the tool
returns "Cannot reach the RAG service at … Start it with `docker compose up`".
A model can act on that; it cannot act on a `ConnectionRefusedError`. Every
failure path is tested for the message it produces.
**It talks HTTP, not Python.** The server calls the RAG service over its public
API instead of importing it. The two deploy independently, and this repo tests
against a contract rather than someone else's internals.
## This repo changed the other one
`rag_read_around` originally searched for the chunk id as a query string and
filtered the results. That cannot work — a chunk id appears in neither the
lexical nor the dense index — so it was a lookup that would quietly return
nothing. The fix belonged in the service, which gained real endpoints:
```
GET /chunks/{chunk_id}?context=N
GET /documents/{doc_id}
```
Then the integration test, running the real service rather than a mock, failed
on `rag_answer`: asked for the overpressure limit, it returned the dry-run
paragraph. Two defects in the service's offline reader, both fixed there:
sentences were scored on raw token overlap, so stopwords decided the answer;
and the two best-scoring sentences were returned, when technical prose states
the subject in one sentence and the value in the next.
Both bugs were invisible to this repo's mocked tests, which is the argument for
having an integration test at all.
## Tests
```bash
pytest
```
26 tests. 21 run against a mock HTTP transport and need nothing installed; 5
are integration tests that start a real `rag-service` in a background thread
with its offline provider and an in-process Qdrant — no container, no API key,
no network. They skip automatically if the service source isn't present:
```bash
RAG_SERVICE_SRC=/path/to/rag-service/src pytest
```
## Requirements
Python 3.10+ and `mcp` 2.x. The SDK renamed `FastMCP` to `MCPServer` in 2.0,
so `requirements.txt` pins `mcp>=2.0,<3` rather than an open `>=1.x` that
resolves across the rename and breaks on a fresh install.
## Limitations
- **`rag_read_document` has no size limit.** A large document returns in full
and will fill a context window. Chunk-level reads are the safe default.
- **No write tools.** Ingestion is deliberately absent: a model that can index
documents can also poison the index.
- **No authentication.** The server assumes the RAG service is reachable on a
trusted network. Exposing it beyond localhost needs auth in front.
- **stdio transport only.** No HTTP or SSE transport yet.
- **Tested against one implementation.** The contract tests pin `rag-service`'s
API shape; another service would need its own.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool has a clearly distinct role: retrieval search, chunk-context reading, whole-document reading, index diagnostics, and LLM answer generation. The descriptions explicitly contrast rag_search with rag_answer and rag_read_around with rag_read_document, leaving little room for misselection.
All names use a consistent snake_case rag_ prefix, and most follow a verb or verb-noun pattern such as rag_search and rag_read_document. rag_index_stats is slightly noun-oriented rather than action-oriented, but it remains readable and predictable.
Five tools is well-scoped for a RAG retrieval server, with each tool covering a distinct part of the retrieval-and-answer workflow. There are no redundant tools and no obviously missing core operation within this small surface.
The set covers search, contextual reading, full-document reading, diagnostics, and generated answers, which are the core retrieval workflows. Minor gaps might include listing indexed documents or collections directly, but rag_index_stats and search largely compensate.