Skip to main content
Glama
devsinsight

RAG MCP Server

by devsinsight
README.md
# RAG MCP Server

MCP server with Streamable HTTP transport that indexes PDF documents into Qdrant and exposes semantic search as MCP tools. Works with any MCP-compatible client: Claude Desktop, Claude Code, or a custom agent.

---

## Stack

| Layer | Library |
|-------|---------|
| HTTP framework | [Hono](https://hono.dev) + `@hono/node-server` |
| MCP protocol | `@modelcontextprotocol/sdk` (Streamable HTTP transport) |
| Vector DB | [Qdrant](https://qdrant.tech) via `@qdrant/js-client-rest` |
| Embeddings | [FastEmbed](https://github.com/Anush008/fastembed-js) — `BAAI/bge-small-en-v1.5` (384 dims, local ONNX) |
| PDF parsing | `pdf-parse` |

---

## Architecture

```
┌──────────────────────────────────────────────────────────┐
│                    MCP Client                            │
│        (Claude Desktop / Claude Code / Custom Agent)     │
└────────────────────┬─────────────────────────────────────┘
                     │  HTTP  port 38080  →  /mcp
                     ▼
┌──────────────────────────────────────────────────────────┐
│              RAG MCP Server  (Hono)                      │
│                                                          │
│  ┌──────────────────┐  ┌─────────────────┐  ┌────────┐   │
│  │  index_document  │  │ semantic_search │  │  list  │   │
│  └────────┬─────────┘  └────────┬────────┘  └───┬────┘   │
│           │                     │               │        │
│  ┌────────▼─────────────────────▼───────────────▼──────┐ │
│  │          FastEmbed  —  BAAI/bge-small-en-v1.5       │ │
│  │               384 dims · local ONNX                 │ │
│  └──────────────────────────┬──────────────────────────┘ │
└─────────────────────────────┼────────────────────────────┘
                              │  REST  port 39333
                              ▼
┌──────────────────────────────────────────────────────────┐
│                      Qdrant                              │
│                  Vector database                         │
│             collection: rag_documents                    │
│             volume: qdrant_storage (persistent)          │
└──────────────────────────────────────────────────────────┘
```

---

## Why RAG with Qdrant instead of a direct summary?

When you ask an LLM to summarize a full document, it receives **all the text at once**. With RAG, only the most relevant chunks for your query are retrieved first. The difference is substantial:

```
WITHOUT RAG  —  Full document as context
────────────────────────────────────────────────────────────
┌─────────┐     ┌──────────────────────────────────────┐     ┌─────┐
│  Query  │────►│  Entire book  (~150 000 tokens)      │────►│ LLM │
└─────────┘     │  ⚠ May exceed the context window     │     └─────┘
                │  ⚠ High cost proportional to size    │
                │  ⚠ Model gets distracted by noise    │
                └──────────────────────────────────────┘

WITH RAG  —  Only relevant chunks
────────────────────────────────────────────────────────────
┌─────────┐     ┌────────────┐     ┌───────────────────────┐     ┌─────┐
│  Query  │────►│   Qdrant   │────►│  Top-k chunks         │────►│ LLM │
└─────────┘     │ (semantic  │     │  (~5 000–8 000 tokens)│     └─────┘
                │  search)   │     │  ✓ Only what matters  │
                └────────────┘     └───────────────────────┘
```

### Benefits

| Aspect | Without RAG | With RAG + Qdrant |
|--------|-------------|-------------------|
| Input tokens | Entire document | Only relevant chunks |
| Cost per query | High (scales with document size) | Low and predictable |
| Accuracy | LLM must filter noise itself | Qdrant pre-filters by similarity |
| Context window limit | Can be exceeded | Not a concern |
| Multiple documents | Impossible in a single call | Unified search across all |
| Response speed | Slower (more tokens) | Faster |
| Citable sources | No | Yes (chunk + score + source file) |

> **Core idea:** the LLM reasons, Qdrant remembers. Each does what it does best.

---

## Quick start with Docker

The recommended way to run the server is Docker Compose. A single command starts both Qdrant and the MCP server.

### Exposed ports

| Service | Host port | Internal port | Description |
|---------|-----------|---------------|-------------|
| MCP Server | **38080** | 3000 | MCP endpoint (`/mcp`) and health check (`/health`) |
| Qdrant REST | **39333** | 6333 | Qdrant REST API (dashboard + client) |
| Qdrant gRPC | **39334** | 6334 | Qdrant gRPC API |

### Step 1 — Add your documents

Drop your PDFs into the `.docs/` folder:

```
rag-demo/
└── .docs/
    └── my-document.pdf
```

### Step 2 — Start the services

```bash
docker compose up -d
```

This starts:
1. **Qdrant** — vector database with a persistent volume
2. **RAG MCP Server** — auto-indexes all PDFs in `.docs/` on startup

```
[+] Running 2/2
 ✔ Container rag-qdrant  Started
 ✔ Container rag-mcp     Started
```

> **First run:** FastEmbed downloads `BAAI/bge-small-en-v1.5` (~25 MB) into a persistent volume. Subsequent starts are instant.

### Step 3 — Verify everything is running

```bash
curl http://localhost:38080/health
```

Expected response:

```json
{
  "status": "ok",
  "sessions": 0,
  "qdrant": "http://qdrant:6333",
  "collection": "rag_documents"
}
```

### Step 4 — Connect your MCP client

MCP endpoint:

```
http://localhost:38080/mcp
```

**Claude Desktop** — add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "rag": {
      "type": "http",
      "url": "http://localhost:38080/mcp"
    }
  }
}
```

**Claude Code:**

```bash
claude mcp add rag --transport http http://localhost:38080/mcp
```

---

## Quick start (local dev)

If you prefer to run without Docker:

```bash
# Start only Qdrant
docker compose up qdrant -d

# Configure environment
cp .env.example .env

# Install and run
npm install
npm run dev
```

Server listens on `http://localhost:3000/mcp`.

---

## Step-by-step usage guide

### Indexing flow

```
PDF on disk
    │
    ▼
index_document
    │
    ├─► Extract full text        (pdf-parse)
    │
    ├─► Split into chunks        (512 chars · 64 overlap)
    │
    ├─► Generate embeddings      (FastEmbed · 384 dims)
    │
    └─► Store in Qdrant          (vectors + metadata)
```

### Search and summarization flow

```
Natural language query
    │
    ▼
semantic_search
    │
    ├─► Embed the query          (FastEmbed)
    │
    ├─► Cosine similarity search (Qdrant)
    │
    └─► Return top-k chunks with similarity score
            │
            ▼
       LLM (Claude)
            │
            └─► Generate answer grounded in the retrieved chunks
```

---

### Full example: *The 7 Habits of Highly Effective People*

A real end-to-end walkthrough using Stephen R. Covey's book.

#### 1. Index the document

Place the PDF in `.docs/` and call `index_document`:

**Input:**
```
Tool: index_document
path: .docs/the-7-habits-of-highly-effective-people.pdf
```

**Output:**
```
╔══════════════════════════════════════╗
║   Document indexed successfully      ║
╚══════════════════════════════════════╝

  File    : the-7-habits-of-highly-effective-people.pdf
  Chunks  : 1823
  Vectors : 1823 × 384 dims
  Time    : 47.3s

Ready for semantic search.
```

> **Note:** Large documents may take a few minutes on first indexing. Re-indexing the same file automatically replaces its previous vectors.

---

#### 2. List indexed documents

**Input:**
```
Tool: list_indexed_documents
```

**Output:**
```
Indexed documents (1):

1. the-7-habits-of-highly-effective-people.pdf
```

---

#### 3. Request a summary using semantic search

To summarize all 7 habits, run one targeted search per habit and then ask the LLM to synthesize the results.

**Searches (one per habit):**

```
semantic_search: "habit 1 be proactive personal vision"
semantic_search: "habit 2 begin with the end in mind personal mission"
semantic_search: "habit 3 put first things first quadrant II time management"
semantic_search: "habit 4 think win-win mutual benefit agreements"
semantic_search: "habit 5 seek first to understand empathic listening"
semantic_search: "habit 6 synergize creative cooperation"
semantic_search: "habit 7 sharpen the saw balanced renewal"
```

Each search returns ranked chunks with a similarity score and their source:

```
[1] score=0.8205  source="the-7-habits-..."  chunk=261
    "Habit 1 says: You are the creator. You are in charge.
     It is based on the four human endowments — self-awareness,
     imagination, conscience, and independent will..."

[2] score=0.8100  ...
[3] score=0.8095  ...
```

**Summary generated from the retrieved chunks:**

---

> **The 7 Habits of Highly Effective People — Summary**
>
> The book builds an effectiveness framework organized in two blocks: **Private Victory** (habits 1–3, independence) and **Public Victory** (habits 4–6, interdependence), topped by a seventh renewal habit.
>
> **Habit 1 — Be Proactive:** You are the creator of your life. You choose your response to any stimulus using four human endowments: self-awareness, imagination, conscience, and independent will.
>
> **Habit 2 — Begin with the End in Mind:** Everything is created twice — first mentally, then physically. Define a personal mission statement that guides every decision.
>
> **Habit 3 — Put First Things First:** Focus on Quadrant II (important, not urgent): planning, relationships, personal renewal. Learn to say a firm "no" to urgent but unimportant demands.
>
> **Habit 4 — Think Win/Win:** Seek agreements where all parties benefit. Built on five elements: desired results, guidelines, resources, accountability, and consequences.
>
> **Habit 5 — Seek First to Understand:** Listen genuinely before speaking. Empathic listening requires consideration; being understood requires courage. Both are essential.
>
> **Habit 6 — Synergize:** The whole is greater than the sum of its parts. High trust and high cooperation produce outcomes no individual party could reach alone.
>
> **Habit 7 — Sharpen the Saw:** Renew yourself continuously across four dimensions: physical, mental, social/emotional, and spiritual. Without renewal, the other six habits deteriorate.

---

## MCP endpoint

```
http://localhost:38080/mcp        (Docker)
http://localhost:3000/mcp         (local dev)
```

The server implements the **Streamable HTTP** MCP transport with stateful sessions:

| Method | Purpose |
|--------|---------|
| `POST /mcp` | Initialise session / send client messages |
| `GET /mcp` | SSE stream for server → client notifications |
| `DELETE /mcp` | Explicitly close a session |

Sessions are tracked with `Mcp-Session-Id` headers.

---

## MCP Tools

### `index_document`

Index a single PDF file. Re-indexing the same file replaces its previous vectors automatically.

```json
{ "path": "/absolute/path/to/file.pdf" }
```

```json
{ "path": ".docs/my-book.pdf" }
```

### `semantic_search`

Search indexed documents with natural language.

```json
{
  "query": "What are the principles of the Private Victory?",
  "limit": 5,
  "source": "the-7-habits-of-highly-effective-people.pdf"
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | `string` | Natural language search query |
| `limit` | `number` | Max results to return (default: 5, max: 20) |
| `source` | `string` | Restrict search to a specific document (optional) |

### `list_indexed_documents`

List all documents currently indexed.

```json
{}
```

---

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `3000` | HTTP server port |
| `QDRANT_URL` | `http://localhost:6333` | Qdrant instance URL |
| `QDRANT_API_KEY` | _(empty)_ | API key for Qdrant Cloud |
| `QDRANT_COLLECTION` | `rag_documents` | Collection name |
| `DOCS_DIR` | `.docs` | Default directory for auto-indexing on startup |
| `CHUNK_SIZE` | `512` | Characters per chunk |
| `CHUNK_OVERLAP` | `64` | Overlap between adjacent chunks |

---

## Scripts

```bash
npm run dev              # development with hot-reload (tsx watch)
npm run build            # production build (tsup → dist/)
npm start                # run production build
npm run typecheck        # type check without emitting
npm run test             # unit tests (vitest)
npm run test:integration # integration tests (requires Qdrant)
npm run test:all         # unit + integration
```