rag-mcp-server
rag-mcp-server
MCP-сервер, оборачивающий production RAG pipeline (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 (для эмбеддингов).
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):
python -m rag_mcp_server.core.indexing(Опционально) реальный Qdrant вместо embedded-режима:
docker-compose up -d
# затем в .env: QDRANT_MODE=serverЗапустить сервер (stdio-транспорт):
python -m rag_mcp_server.serverПроверить через MCP Inspector (встроенный инспектор из mcp[cli], открывает веб-UI для ручного вызова tools):
mcp dev rag_mcp_server/server.pyТесты (не требуют живого Qdrant/OpenAI — векторный поиск и cross-encoder замоканы):
pytestКак подключить в Claude Desktop / Claude Code
Добавьте в claude_desktop_config.json (Claude Desktop: Settings → Developer → Edit Config; Claude Code: .mcp.json в проекте или claude mcp add):
{
"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)[
{
"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"]
)[
{
"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 — там же живёт 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-режим отлично подходит для демо и тестов, но для конкурентного доступа нескольких процессов нужен сервер.