Skip to main content
Glama
suhasdesai02

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.