Skip to main content
Glama

research-mcp

Безсостоятельный MCP-фасад, который скрывает пирамиду поисковых/читающих провайдеров за единой streamable-http MCP-точкой и предоставляет всего 3 чистых инструмента с хорошими русскими текстами помощи. LLM получает простой набор «поиск → чтение»; за ним несколько провайдеров автоматически перебираются, объединяются и выполняют отказоустойчивый переход.

Приложение не выполняет аутентификацию — оно публикуется через Traefik + basicAuth на хосте. Оно не хранит состояние приложения: единственное, что сохраняется, — это файл журнала в data/ (хранится на томе).

Инструменты

Инструмент

Что делает

web_search(query, num_results=8, page=1, language=None)

Поиск по всем включённым провайдерам, объединение + дедупликация → ранжированный список (заголовок, URL, сниппет). Только поиск.

read_page(url)

Одна страница или PDF → чистый Markdown. Автоопределение типа, проход по конвейеру чтения (лёгкий → тяжёлый), пока один не сработает.

read_pages(urls)

До 20 URL одновременно → список {url, ok, markdown|error}.

Related MCP server: Web Search MCP

Архитектура: типы и экземпляры

Провайдеры — это плагины. Мы разделяем:

  • тип — класс реализации (например, поисковый провайдер searxng), один на модуль в src/providers/, регистрируется с помощью @register("type").

  • экземпляр — настроенная копия типа с его секретами/URL, разрешёнными из именованных переменных окружения (допускается несколько экземпляров одного типа, например tavily-1 / tavily-2 с разными ключами).

Какие экземпляры существуют и порядок, в котором каждый конвейер их пробует, настраивается в коде (src/pipeline_config.py); ключи/URL берутся из ENV по имени переменной.

  • Поисковый конвейер (searxng → brave → jina-search → serper → exa): включённые экземпляры работают параллельно; результаты объединяются и дедуплицируются по нормализованному URL (ранняя позиция в конвейере выигрывает). Когда задан JINA_API_KEY (и SEARCH_RERANK_ENABLED не отключён), полный объединённый список затем переранжируется с помощью jina-reranker-v3.5, чтобы усечение до num_results сохраняло наиболее релевантные результаты, а не слепой префикс порядка конвейера; любой сбой переранжирования возвращается к порядку объединения. searxng и brave дополнительно ограничивают себя локально (один запрос за 45 с и за 1,1 с соответственно, что соответствует измеренному лимиту вышестоящего сервиса); когда слот занят, они пропускают текущий поиск, а не ждут его.

  • Конвейер чтения (trafilatura → jina → crawl4ai → tavily-1 → tavily-2 → firecrawl): одиночный пробный GET классифицирует URL. PDF (Content-Type / .pdf / магическое %PDF) извлекаются с помощью pypdf; для HTML то же тело передаётся в trafilatura, чтобы горячий путь не делал второй GET, затем остальные экземпляры пробуются по порядку, и первый, вернувший содержимое >= FALLBACK_MIN_CHARS, побеждает.

Сквозные аспекты: одна повторная попытка при временных ошибках (5xx / транспортные ошибки) с короткой задержкой; 402 (исчерпаны кредиты) / 429 (ограничение скорости) рассматриваются как сбой провайдера → следующий экземпляр (именно это обеспечивает отказоустойчивый переход tavily-1 → tavily-2).

Экземпляр включён, только если заданы его обязательные переменные окружения; в противном случае он пропускается с записью в журнал. trafilatura не требует настройки (всегда включён); jina работает без ключа (его ключ необязателен). При запуске сервер требует хотя бы один поисковый и один читающий экземпляр, иначе завершается с понятным сообщением.

Добавление провайдера

  1. Напишите src/providers/<type>.py с классом, декорированным @register("<type>"), реализующим SearchProvider.search(...) или ReadProvider.read(...).

  2. Импортируйте модуль в src/providers/__init__.py (чтобы декоратор выполнился).

  3. Добавьте строку Instance("name", "<type>", api_key_env="YOUR_ENV_NAME") в src/pipeline_config.py и укажите его name в SEARCH_PIPELINE / READ_PIPELINE. Используйте ИМЯ переменной ENV, никогда не значение.

  4. Документируйте переменную окружения в .env.example.

Быстрый старт

make install                # create .venv + install dev/test deps
cp .env.example .env        # fill in the keys you have  (shortcut: make env)
make test                   # run tests
make run                    # run the server (streamable-http on MCP_HOST:MCP_PORT, endpoint /mcp)

Конфигурация

Вся конфигурация берётся из ENV / .env (см. .env.example). Секреты/URL провайдеров читаются по имени в загрузчике экземпляров, а не объявляются как поля Settings. Несекретные параметры (все со значениями по умолчанию): MCP_HOST, MCP_PORT, LOG_LEVEL, LOG_FILE, LOG_ROTATION, LOG_RETENTION, REQUEST_TIMEOUT, FALLBACK_MIN_CHARS, READ_PAGES_CONCURRENCY, RETRIES, SEARCH_RERANK_ENABLED, JINA_TOKEN_BUDGET. Ограничение числа URL на вызов read_pages — фиксированное 20 (жёсткая константа, соответствующая описанию инструмента) — не настраивается.

Переменные окружения провайдеров: SEARXNG_URL, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, JINA_API_KEY (один ключ включает читатель jina в режиме с ключом, провайдер jina-search и переранжировщик поиска; читатель также работает без ключа), CRAWL4AI_URL + CRAWL4AI_TOKEN, TAVILY_1_API_KEY, TAVILY_2_API_KEY, FIRECRAWL_API_KEY.

Прокси

Любой внешний экземпляр можно направить через собственный SOCKS5/HTTP-прокси, задав <INSTANCE>_PROXY — полезно для чистого исходящего трафика в обход блокировок по IP (например, Cloudflare перед Exa). Поддерживается для каждого экземпляра: EXA_PROXY, BRAVE_PROXY, SERPER_PROXY, JINA_PROXY, TAVILY_1_PROXY, TAVILY_2_PROXY, FIRECRAWL_PROXY. Внутренние экземпляры (searxng, crawl4ai, trafilatura) прокси не имеют.

Значение передаётся напрямую в httpx; socks5://host:port выполняет DNS на стороне прокси (целевое имя хоста разрешается прокси, как curl --socks5-hostname), также принимаются socks5h:// и http://host:port. Не задано → этот экземпляр идёт напрямую. Конвейер держит один пул httpx-клиентов на каждый отдельный URL прокси (и один прямой клиент), выбираемый для каждого экземпляра, так что проксированные и прямые провайдеры работают бок о бок. Требуется дополнительный пакет socks (httpx[socks], уже закреплён).

Журналирование

Помимо stderr (захватываемого драйвером Docker json-file с ротацией по размеру), сервер записывает постоянный файл журнала в data/research-mcp.log (по умолчанию; LOG_ROTATION=20 MB, LOG_RETENTION=14 days). Он находится на томе data/, поэтому переживает перезапуски контейнера и обновления образа. Файл содержит одну строку на запрос для каждого вызова инструмента — поиск (query, какие экземпляры провайдеров реально выполнялись, количество результатов, задержка) и чтение (url, победивший провайдер/уровень или pdf, ok, задержка), а также сводку read_pages count=N ok=K — что полезно для анализа распределения запросов по уровням провайдеров. Тела запросов и секреты не журналируются, только URL/запросы, имена провайдеров, количества, тайминги.

Развёртывание

Gitea Actions собирает образ и отправляет его в реестр Gitea gitea.vvzvlad.xyz/projects/research-mcp (test → build, теги latest + sha). На проде мы тянем предварительно собранный образ через docker-compose.yml (за Traefik + basicAuth, watchtower автоматически обновляет latest; том data/ сохраняет файл журнала между обновлениями) — мы никогда не собираем на проде.

Структура

Путь

Назначение

src/providers/base.py

Интерфейсы провайдеров + SearchResult / ProviderError.

src/providers/registry.py

Декоратор @register → REGISTRY.

src/providers/<type>.py

Один модуль на тип провайдера.

src/providers/pdf.py

Определение PDF + извлечение текста через pypdf (используется конвейером).

src/pipeline_config.py

Экземпляры в коде + порядок конвейеров.

src/pipeline.py

Загрузчик экземпляров + логика поиска/чтения.

src/rerank.py

JinaReranker — переранжирование результатов поиска после объединения.

src/settings.py

Несекретные параметры (pydantic-settings).

src/server.py

build_server() с тремя определениями @mcp.tool.

main.py

Тонкая точка входа: сборка сервера, запуск streamable-http.

tests/

Набор pytest (сеть замокана с помощью respx).

Maintenance

ActivityActive
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers