The Model Context Protocol (MCP)
is how modern LLM agents — Claude Desktop, IDE assistants, and custom agent
loops — discover and call tools. A very common pair of tools an agent wants is
search (find relevant documents for a query) and
fetch (read one document in full). That is exactly
what a full-text search engine is for.
Whoosh is a great fit for this job because it is pure Python and embedded: the index is just a directory of files, there is no server to run and no native wheel to compile. You can ship it inside the same process that hosts your MCP server, rebuild it in CI, and run it anywhere CPython runs — a laptop, a container, or serverless. No Elasticsearch cluster, no vector database, no GPU.
Install
pip install "whoosh3[mcp]"
whoosh3 is the actively-maintained continuation of Whoosh
(Python 3.9–3.14); the [mcp] extra pulls in the official Model
Context Protocol SDK.
Run it in one line
The package ships a whoosh-mcp console script. Point it at a
folder of .md/.txt/.rst files and it
serves them to an agent over stdio — one document per file:
whoosh-mcp ~/notes # or omit the path to serve built-in samples
A reusable search core (no MCP dependency)
Keep the search logic independent of the protocol so you can unit-test it or
reuse it behind any agent framework. This builds a small BM25F index and
exposes search() and fetch():
import os.path, tempfile
from dataclasses import dataclass
from whoosh import highlight
from whoosh.fields import ID, TEXT, Schema
from whoosh.index import create_in, open_dir
from whoosh.qparser import MultifieldParser
@dataclass
class SearchCore:
index_dir: str
@classmethod
def build(cls, docs, index_dir=None):
index_dir = index_dir or tempfile.mkdtemp(prefix="whoosh_mcp_")
os.makedirs(index_dir, exist_ok=True)
schema = Schema(id=ID(stored=True, unique=True),
title=TEXT(stored=True), body=TEXT(stored=True))
ix = create_in(index_dir, schema)
w = ix.writer()
for d in docs:
w.update_document(id=d["id"], title=d["title"], body=d["body"])
w.commit()
return cls(index_dir)
def search(self, query, limit=5):
ix = open_dir(self.index_dir)
q = MultifieldParser(["title", "body"], schema=ix.schema).parse(query)
out = []
with ix.searcher() as s:
results = s.search(q, limit=limit)
results.fragmenter = highlight.ContextFragmenter(maxchars=160, surround=40)
for hit in results:
out.append(dict(id=hit["id"], title=hit["title"],
score=round(hit.score, 4),
snippet=hit.highlights("body") or hit["body"][:160]))
return out
def fetch(self, doc_id):
ix = open_dir(self.index_dir)
with ix.searcher() as s:
hit = s.document(id=doc_id)
return {"id": doc_id, "error": "not found"} if hit is None else \
{"id": hit["id"], "title": hit["title"], "text": hit["body"]}
Wrap it as an MCP server
The official SDK's FastMCP turns plain functions into MCP tools.
Docstrings and type hints become the tool schema the agent sees, so write them
for the model:
from mcp.server.fastmcp import FastMCP
core = SearchCore.build(MY_DOCS)
mcp = FastMCP("whoosh-search")
@mcp.tool()
def search(query: str, limit: int = 5) -> list[dict]:
"""Full-text search the local corpus. Returns ranked {id, title, score, snippet}."""
return core.search(query, limit)
@mcp.tool()
def fetch(id: str) -> dict:
"""Fetch the full text of a document by its id (as returned by search)."""
return core.fetch(id)
if __name__ == "__main__":
mcp.run() # stdio transport, ready to be spawned by an MCP client
Connect an agent to it
Point any MCP client at the script. For Claude Desktop, add it to
claude_desktop_config.json:
{
"mcpServers": {
"whoosh": { "command": "whoosh-mcp", "args": ["/path/to/your/docs"] }
}
}
Prefer a container? The repository ships a Dockerfile, so you can
run the server without a local Python environment — mount your docs read-only
and keep stdin open for the stdio transport:
docker build -t whoosh-mcp .
docker run --rm -i -v "$HOME/notes:/corpus:ro" whoosh-mcp /corpus
The agent now sees a search tool and a fetch tool.
Ask it a question and it will search your corpus, read the most relevant
document, and answer with grounded, cited context — all backed by a local
BM25F index. A query like "agent tools protocol" against the
sample corpus returns the "Model Context Protocol" document with a highlighted
snippet; fetch("mcp") then returns its full text.
Why keyword search still matters for agents
- Exact terms win. Agents constantly look up identifiers, error codes, API names, filenames and version numbers — cases where lexical BM25 beats fuzzy embedding similarity.
- No embedding cost or drift. Nothing to re-embed when the
corpus changes;
update_documentkeeps the index in sync incrementally. - Transparent & debuggable. You can read the query, the scores, and the matched terms — no opaque vector space.
- Composable. Pair it with a vector tool for hybrid recall; see the hybrid RAG guide.
Why Whoosh specifically
- Pure Python. No compiler, no native wheels, no separate server process. It runs in the same environment as your agent.
- Embedded index. A directory of files you can ship, cache, or rebuild in CI — ideal for a tool that starts up with the agent.
- Real BM25F ranking, plus phrase, boolean, range, prefix and fielded queries when a bag of terms isn't enough.
- Actively maintained again.
pip install whoosh3installs the current release with Python 3.9–3.14 support.
pip install "whoosh3[mcp]" · Full runnable example:
examples/mcp_server.py
· Related:
hybrid RAG retrieval,
LangChain retriever,
index a knowledge base.
The Whoosh search core above was run against whoosh3 3.31.0 before publishing;
the MCP wiring uses the official mcp SDK's documented
FastMCP API. "Whoosh" is the pure-Python search library originally
by Matt Chaput; this is an actively maintained continuation. MCP is an open
protocol — the same server works with any compliant client.