Skip to main content
Glama
q6066697

rag-mcp-server

by q6066697
README.md
# rag-mcp-server

MCP-сервер, оборачивающий production RAG pipeline ([rag-eval-service](https://github.com/q6066697/rag-eval-service)) в стандартный протокол (Model Context Protocol) для агентов — гибридный поиск (dense-эмбеддинги + BM25 + RRF) становится инструментом, который может вызвать любой MCP-клиент: Claude Desktop, Claude Code или собственный агент.

## Архитектура

    Client (Claude Desktop / Claude Code / любой MCP-клиент)
        |
        v  MCP over stdio (JSON-RPC)
    FastMCP server  (rag_mcp_server/server.py)
        |
        +--> search_documents(query, top_k)
        +--> get_document(doc_id)
        +--> rerank_results(query, doc_ids)
        |
        v
    Hybrid retrieval  (rag_mcp_server/core/retrieval.py)
        |
        +---> Dense retriever  --- OpenAI text-embedding-3-small ---> Qdrant (cosine, 1536d)
        |                                                                 |
        +---> Sparse retriever -- BM25 (rank-bm25 / BM25Okapi) ----------+
        |                                                                 |
        |                                 +-------------------------------+
        |                                 v
        |                        Reciprocal Rank Fusion (k=60)
        |                                 |
        +---------------------------------v
                                  Top-k документов --> клиент (LLM формирует ответ)

Qdrant по умолчанию работает в **embedded-режиме** (файл на диске, без отдельного процесса) — сервер self-contained и не требует внешней инфраструктуры для демо. При желании можно переключиться на полноценный Qdrant-сервер через `docker-compose.yml` (см. ниже).

## Что это и зачем

Это MCP-обёртка, а не переизобретение retrieval-логики: сам гибридный поиск (dense + BM25 + RRF) портирован из `rag-eval-service` почти без изменений. Новое, что добавляет именно эта обёртка:

- **Протокол вместо HTTP API** — инструменты видны любому MCP-клиенту (Claude Desktop, Claude Code) без написания кастомного HTTP-клиента и без необходимости держать сервис постоянно поднятым за REST.
- **Self-contained демо-режим** — embedded Qdrant вместо докер-контейнера, чтобы `git clone` → работающий инструмент занимал минимум шагов.
- **doc_id/title-схема** и агрегация результатов на уровне документа (а не чанка) — под нужды LLM-клиента, который вызывает `search_documents` → `get_document`, а не работает с сырыми чанками.
- **Docstring-контракты**, написанные для LLM-потребителя инструмента (см. `rag_mcp_server/server.py`), а не для человека, читающего API-документацию.

## Как запустить локально

**Требования:** Python 3.10+, ключ OpenAI API (для эмбеддингов).

```bash
git clone https://github.com/q6066697/rag-mcp-server.git
cd rag-mcp-server

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt  # или: pip install -e ".[dev]"

cp .env.example .env
# впишите свой OPENAI_API_KEY в .env
```

**Построить индекс** (одноразово; читает `data/`, эмбеддит чанки через OpenAI и заливает их в embedded Qdrant):

```bash
python -m rag_mcp_server.core.indexing
```

**(Опционально) реальный Qdrant вместо embedded-режима:**

```bash
docker-compose up -d
# затем в .env: QDRANT_MODE=server
```

**Запустить сервер** (stdio-транспорт):

```bash
python -m rag_mcp_server.server
```

**Проверить через MCP Inspector** (встроенный инспектор из `mcp[cli]`, открывает веб-UI для ручного вызова tools):

```bash
mcp dev rag_mcp_server/server.py
```

**Тесты** (не требуют живого Qdrant/OpenAI — векторный поиск и cross-encoder замоканы):

```bash
pytest
```

## Как подключить в Claude Desktop / Claude Code

Добавьте в `claude_desktop_config.json` (Claude Desktop: Settings → Developer → Edit Config; Claude Code: `.mcp.json` в проекте или `claude mcp add`):

```json
{
  "mcpServers": {
    "rag-mcp-server": {
      "command": "/absolute/path/to/rag-mcp-server/.venv/bin/python",
      "args": ["-m", "rag_mcp_server.server"],
      "cwd": "/absolute/path/to/rag-mcp-server",
      "env": {
        "OPENAI_API_KEY": "sk-..."
      }
    }
  }
}
```

После перезапуска клиента инструменты `search_documents`, `get_document` и `rerank_results` должны появиться в списке доступных tools.

## Примеры вызова tools

**`search_documents`**

```
search_documents(query="что такое Reciprocal Rank Fusion", top_k=3)
```

```json
[
  {
    "doc_id": "reciprocal-rank-fusion.md",
    "title": "Reciprocal Rank Fusion",
    "snippet": "RRF сливает несколько ранжированных списков без нормализации сырых score — документ на позиции r получает вклад 1/(k+r)…",
    "score": 0.0328
  },
  {
    "doc_id": "hybrid-search.md",
    "title": "Hybrid Search",
    "snippet": "Гибридный поиск комбинирует dense-эмбеддинги и BM25, чтобы ловить и семантическое сходство, и точные термины…",
    "score": 0.0301
  }
]
```

**`get_document`**

```
get_document(doc_id="reciprocal-rank-fusion.md")
```

```
"# Reciprocal Rank Fusion (RRF)\n\nRRF — метод слияния нескольких ранжированных списков результатов…"
```

**`rerank_results`**

```
rerank_results(
    query="как оценивать качество ретривера",
    doc_ids=["eval-retrieval-metrics.md", "reranking.md", "hybrid-search.md"]
)
```

```json
[
  {
    "doc_id": "eval-retrieval-metrics.md",
    "title": "Evaluating Retrieval Quality",
    "snippet": "Метрики retrieval — hit@k, recall@k, MRR, nDCG@k — измеряют, находит ли поиск релевантные документы…",
    "score": 4.81
  },
  {
    "doc_id": "reranking.md",
    "title": "Reranking",
    "snippet": "Cross-encoder реранкинг переупорядочивает шортлист кандидатов, читая query и passage вместе…",
    "score": 1.02
  }
]
```

## Структура репозитория

```
rag-mcp-server/
├── rag_mcp_server/
│   ├── server.py          # точка входа, FastMCP инстанс, регистрация tools
│   ├── core/
│   │   ├── retrieval.py   # портированная гибридная логика поиска (dense + BM25 + RRF + rerank)
│   │   └── indexing.py    # загрузка корпуса, чанкинг, индексация в Qdrant
│   └── config.py          # конфигурация из .env
├── data/                  # bootstrap-корпус (15 markdown-доков, копия из rag-eval-service)
├── tests/
│   └── test_server.py     # unit-тесты на MCP tools (мокают поиск)
├── docker-compose.yml     # опциональный Qdrant-сервер
├── pyproject.toml / requirements.txt
├── .env.example
└── LICENSE (MIT)
```

## Источник retrieval-логики

Гибридный поиск (`rag_mcp_server/core/retrieval.py`, `core/indexing.py`) портирован из [rag-eval-service](https://github.com/q6066697/rag-eval-service) — там же живёт eval-harness (NFCorpus/BEIR-бенчмарк, custom golden set, метрики hit@k/recall@k/MRR/nDCG), которого в этом репозитории намеренно нет: `rag-mcp-server` — это тонкая протокольная обёртка над уже провалидированным retrieval-пайплайном, а не его переоценка.

## Известная проблема и её решение

При запуске через mcp dev изначально возникала ошибка AlreadyLocked от embedded Qdrant — Storage folder is already accessed by another instance of Qdrant client.

Причина: mcp dev импортирует модуль сервера дважды — сам инспектор (чтобы прочитать зависимости) и дочерний процесс uv run mcp run, который реально запускает сервер. Индексы (DenseIndex, SparseIndex, CrossEncoderReranker) изначально создавались на уровне модуля при импорте — второй импорт натыкался на файловый лок, оставленный первым.

Решение: инициализация индексов сделана ленивой — через геттер с кэшированием, вызываемый при первом обращении к tool, а не при импорте модуля. Это заодно и более правильная архитектура для MCP-сервера: сервер стартует мгновенно, тяжёлая инициализация (эмбеддинг-клиент, cross-encoder) откладывается до первого реального запроса.

## Что бы добавил дальше

- **Docker для самого MCP-сервера** — сейчас в контейнер оборачивается только Qdrant; для деплоя самого сервера нужен свой Dockerfile.
- **SSE/HTTP-транспорт** — stdio предполагает локальный процесс на одной машине с клиентом; для удалённого доступа (несколько пользователей, облачный деплой) нужен SSE или Streamable HTTP транспорт из MCP SDK.
- **Авторизация** — у stdio-транспорта её нет по конструкции (процесс доверенный, запущен локально); при переходе на сетевой транспорт понадобится API-ключ/OAuth на уровне сервера.
- **Инкрементальная индексация** — сейчас `core.indexing` пересоздаёт коллекцию целиком; для растущего корпуса нужен upsert только изменившихся документов.
- **Реальный Qdrant по умолчанию в CI/проде** — embedded-режим отлично подходит для демо и тестов, но для конкурентного доступа нескольких процессов нужен сервер.

TDQS

A4.7/5.0

Scored across 3 tools

Disambiguation5/5

Tools have clear boundaries: search_documents retrieves candidates, get_document fetches full text by ID, rerank_results reorders given IDs. No overlap in purpose, and each description explicitly states when to use it.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (search_documents, get_document, rerank_results) in snake_case. Naming convention is uniform and predictable.

Tool Count5/5

Three tools is well-scoped for a focused retrieval server: search, fetch full content, and re-rank. Each tool serves a distinct step in the pipeline without redundancy, fitting the typical 3-15 range.

Completeness5/5

The tool surface covers the full read-only retrieval workflow: hybrid search with snippets, full document access, and optional cross-encoder re-ranking. No missing operations are needed for the stated purpose.

Maintenance

ActivitySlowing
ResponsivenessNo issues