Document Search MCP Server
by suhasdesai02
README.md
# Document Search — an MCP server
Wraps document ingestion and retrieval (the same chunking + Voyage
embedding + Chroma logic from Project 2) as an **MCP server** — so
Claude Desktop or Claude Code can search your own documents directly,
as a native tool call, instead of you running a separate CLI.
This is Project 4 in a self-directed AI engineering learning path. It
builds on Project 2 (RAG) and Project 3 (tool use) — MCP is essentially
"tool use, standardized so any client can use your tools, not just code
you wrote yourself."
## The key design decision, and why it matters
Unlike Project 2's `doc_chat.py`, **this server never calls the Claude
API to generate an answer.** It only retrieves and returns relevant
chunks. This is intentional, not a missing feature:
When Claude Desktop (or Claude Code) calls `search_documents`, it's
*already* a Claude model in the loop — the client itself. It receives
the retrieved chunks as a tool result and writes the final answer using
its own reasoning, the same way it would use any other tool's output.
Having the server *also* call Claude to generate an answer would be
redundant — two models doing the same generation job — and would take
control away from the client, which is supposed to decide how to use
retrieved context, not just relay someone else's answer.
**This is the real shift MCP represents:** in Project 2, *you* wrote
the code that calls Claude. In Project 4, Claude *is* the code calling
your tool. The server's only job is to be a good, honest data source.
## Setup
```bash
pip install -r requirements.txt
```
```powershell
$env:VOYAGE_API_KEY = "your-voyage-key"
```
(No `ANTHROPIC_API_KEY` needed for the server itself — see above. You'll
still need one configured wherever Claude Desktop/Code itself runs, but
that's separate from this server.)
## Connecting it to Claude Code
```bash
claude mcp add doc-search -- python /full/path/to/mcp_doc_server.py
```
Use the **full absolute path** to the script — relative paths won't
resolve correctly once Claude Code launches it as a subprocess from a
different working directory.
Then, in a Claude Code session, you can just ask naturally:
> "Ingest the documents in ./sample_docs, then tell me the meal
> reimbursement policy."
Claude Code will call `ingest_documents`, then `search_documents`, then
write the answer itself — the same multi-step tool chaining you saw in
Project 3, except now the tool is your own MCP server instead of a
Python function baked into the same script.
## Connecting it to Claude Desktop
Add this to your Claude Desktop MCP config file (`claude_desktop_config.json`
— check Claude Desktop's settings for its exact location on your system):
```json
{
"mcpServers": {
"doc-search": {
"command": "python",
"args": ["/full/path/to/mcp_doc_server.py"],
"env": {
"VOYAGE_API_KEY": "your-voyage-key"
}
}
}
}
```
Restart Claude Desktop after editing the config. You should see
"doc-search" appear as an available tool source in the app.
## Testing without a live MCP client
`fastmcp` ships an in-process test client, which is what verified this
server before you ever connect it to Claude Desktop:
```python
import asyncio
from fastmcp import Client
from mcp_doc_server import mcp
async def main():
async with Client(mcp) as client:
tools = await client.list_tools()
print(tools)
asyncio.run(main())
```
This calls the real MCP protocol layer — tool discovery, schema
generation, invocation — without needing a full Claude Desktop install
or a live API key, which is how the error-path behavior was verified
during development.
## Design notes
- **Tool docstrings are the interface, more so than in Project 3.**
With direct API tool use, you write a separate `description` field.
With `fastmcp`, the Python docstring *is* the description the client
sees — meaning writing a clear, accurate docstring isn't just good
Python practice here, it's the actual mechanism by which Claude
decides when and how to call your tool.
- **Global client caching (`_voyage_client`, `_collection`).** MCP
servers are typically long-running processes (started once, called
many times), unlike a CLI script that runs once and exits. Creating a
new Voyage client or Chroma connection on every single tool call would
be wasteful; lazy-initializing them once and reusing them across calls
is the correct pattern for a persistent server.
- **Tools return strings, not raising exceptions, for expected error
cases** (missing folder, empty collection) — same philosophy as
Project 3's `execute_tool`. The calling model needs to see *and
reason about* a failure, not have the whole server crash.
## What's next
- Add a `list_ingested_sources` tool so the client can check what's
already searchable before deciding whether to re-ingest.
- Add authentication if this were ever exposed over network transport
instead of run locally via stdio.
- Package as a proper installable MCP server others could add to their
own Claude Desktop config without cloning the repo.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues