Skip to main content
Glama
Romumrn

ViromeChat MCP server

by Romumrn

MCP-сервер ViromeChat

Сервер FastMCP, который владеет всем доступом к наборам данных, внешними вызовами API и бизнес-логикой для Viromech@t. Клиент (бэкенд FastAPI / React-фронтенд, в отдельном репозитории viromechat) никогда не обращается напрямую ни к dataframe, ни к учётным данным S3, ни к именам колонок — он только общается с этим сервером по MCP/HTTP, обобщённо, читая те инструменты и ресурсы, которые сервер публикует в данный момент.

Этот репозиторий — автономный дом этого сервера. У него нет зависимости от репозитория приложения; единственный контракт между ними — набор MCP-инструментов и ресурсов, описанный ниже, который бэкенд использует через переменную окружения MCP_SERVER_URL.


Запуск

Предварительные требования: набор данных таксономии (data/TAXONOMY.csv, ~327 МБ) хранится через Git LFS. Выполните git lfs install один раз на каждой машине перед клонированием или git lfs pull после клонирования, чтобы материализовать его.

Локально

git lfs pull                      # fetch data/TAXONOMY.csv
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env              # fill in your S3 credentials
python server_mcp.py

Docker

cp .env.example .env              # fill in your S3 credentials
docker compose up --build

В любом случае запускается HTTP-сервер на 0.0.0.0:8000, endpoint MCP на /mcp (http://localhost:8000/mcp — именно это бэкенд указывает через MCP_SERVER_URL). При запуске он:

  1. Полностью загружает data/TAXONOMY.csv в память как df_taxo.

  2. Загружает два файла описания колонок (data/v@_columns_description.csv и data/TAXONOMY_columns_description.json), которые лежат в основе двух ресурсов MCP, описанных ниже.

  3. Открывает in-memory-соединение DuckDB, устанавливает расширения httpfs и spatial и регистрирует представление host поверх S3-набора данных Parquet — Parquet-файл никогда не загружается в память; каждый запрос query_host_printer выполняется DuckDB прямо в S3 (отсечение по колонкам и группам строк).

Тесты

pip install pytest
pytest

Вспомогательные тесты покрывают чистые функции (_ok/_fail, построители figure/таблиц, SQL-защиты) и не требуют живого подключения к S3.


Related MCP server: Alma Atlas

Подключение клиента

Любой MCP-клиент может работать с этим сервером. Бэкенд Viromech@t делает это с помощью fastmcp.Client:

from fastmcp import Client

async with Client("http://localhost:8000/mcp") as mcp:
    tools = await mcp.list_tools()
    result = await mcp.call_tool("wikipedia_search", {"search_term": "Lentivirus"})

Клиент должен динамически находить инструменты и ресурсы (list_tools() / list_reseources()) и выполнять диспетчеризацию по artifact["type"] — никогда не захардкоживать имена инструментов или знание о колонках. Именно это держит два репозитория развязанными: добавление инструмента, который переиспользует существующий тип артефакта, не требует изменения клиента.


Ресурсы

Ресурсы — это статическое знание, которое читается один раз, а не то, что LLM «вызывает» как инструмент. Клиент читает их один раз за диалог и встраивает их содержимое в системный промпт.

URI

Содержимое

Источник

resource://datasets/host/schema

JSON-карта {column_name: {description, Type}} для каждой колонки таблицы host

data/v@_columns_description.csv

resource://datasets/taxonomy/schema

Полная JSON-схема (имя, описание, колонки, первичный ключ, определение строки) для df_taxo

data/TAXONOMY_columns_description.json

Добавление нового ресурса (например, третьего набора данных) не требует изменений на клиенте: клиент обнаруживает ресурсы через list_resources() и читает каждый из них универсально.


Контракт ответа

Каждый инструмент возвращает ровно такую структуру, независимо от того, что он делает:

{
  "success": true,           // or false
  "content": "human-readable text — this is what the LLM reads back as the tool result",
  "artifacts": [ ... ]        // structured extras the client can render; [] if none
}

При ошибке content содержит сообщение об ошибке (где возможно — с подсказкой, как повторить), а artifacts пуст. Две вспомогательные функции _ok(content, artifacts) / _fail(content) в начале server_mcp.py формируют эту структуру — всегда используйте их вместо ручной сборки dict.

Типы артефактов

type

Источник производит

Структура

Использование клиентом

url

wikipedia_search

{"type": "url", "url": "..."}

Ссылка на Википедию в панели «Sources»

pubmed

pubmed_search

{"type": "pubmed", "pmids": [123, 456]}

Ссылки на PubMed + белый список PMID для защиты от галлюцинаций

ncbi_taxonomy

ncbi_taxonomy_search

{"type": "ncbi_taxonomy", "url": "...", "tax_id": "..."}

Ссылка на NCBI Taxonomy в панели «Sources»

table

query_host_sql, query_dataframe

{"type": "table", "rows": [...], "columns": [...], "total_rows": N}

Отслеживается как выполненный SQL/код в «Sources»; rows ограничен preview_rows

plotly

create_visualization, create_map

{"type": "plotly", "figure": {...}} (из fig.to_json(), разобранного обратно в dict)

Отрисованный график plotly

Клиент выполняет диспетчеризацию только по artifact["type"] — никогда по имени инструмента. Добавление инструмента, который переиспользует существующий тип артефакта (например, ещё одного инструмента, возвращающего "table"), вообще не требует изменений в клиенте.


Инструменты

wikipedia_search(search_term: str, wikipedia_limit: int = 4000) -> dict

Находит страницу в Википедии; при отсутствии точного совпадения по названию возвращает ближайший результат полнотекстового поиска (в содержимом это помечено как «нечёткое совпадение»). Возвращает артефакт url.

pubmed_search(query: str, max_results: int = 5) -> dict

Выполняет поиск по PubMed (NCBI E-utilities esearch + efetch, db=pubmed) и возвращает для каждого результата название, авторов, журнал, год, аннотацию, DOI и PMID. Возвращает артефакт pubmed со всеми реально найденными PMID — это единственный источник правды для защиты клиента от галлюцинаций по PMID.

ncbi_taxonomy_search(name: str) -> dict

Сопоставляет любое название организма — аббревиатуру, бытовое называние или научное название — с базой данных NCBI Taxonomy (E-utilities, db=taxonomy). Для каждого совпадения возвращает: научное название, ранг (вид/род/семейство/…), отдел, полную родословную и известные синонимы/аббревиатуры. Это авторитетный способ превратить HIV в Human immunodeficiency virus 1 / род Lentivirus или проверить, является ли название родом или семейством, не полагаясь на формулировку Википедии. Возвращает артефакт ncbi_taxonomy для лучшего совпадения.

Примечание к реализации: XML NCBI efetch содержит по одному <Taxon> на каждый ранг-предок внутри элемента <LineageEx> каждого результата. Парсер перебирает только root.findall("Taxon") (прямые потомки) — использование .//Taxon захватило бы каждого предка как отдельное совпадение.

query_host_sql(sql: str, preview_rows: int = 50) -> dict

Выполняет read-only запрос SELECT к представлению host (набор S3 Parquet) и возвращает артефакт table. Это обязательный первый шаг перед тем, как query_dataframe, create_visualization или create_map смогут использовать df_host, — эти инструменты работают с результатом последнего вызова query_host_sql (ctx.last_host_result), а не с полным набором данных.

Защиты, действующие перед выполнением:

  • Разрешается только одна операция SELECT — INSERT/UPDATE/DELETE/DDL/PRAGMA/... отклоняются _FORBIDDEN_SQL_KEYWORDS.

  • Голый SELECT * отклоняется сразу. В host около ~65 колонок, включая тяжёлое бинарное поле geometry; выбор всех колонок для всех подходящих строк через S3 — причина многотаимных таймаутов до ввода этой защиты. Вызывающий должен проецировать только те колонки, которые ему нужны.

  • Координаты находятся в нативной колонке точек типа GEOMETRY, а не в обычных lat/lon — извлекайте их как ST_X(geometry) AS lon, ST_Y(geometry) AS lat (расширение spatial загружается при старте).

query_dataframe(code: str, preview_rows: int = 50) -> dict

Выполняет pandas-код, получая в области видимости df_taxo, df_host (= ctx.last_host_result, либо точную ошибку, если query_host_sql ещё не вызывался), pd и np. Код должен присвоить DataFrame переменной result. Возвращает артефакт table.

create_visualization(code: str) -> dict

Та же среда выполнения, что и у query_dataframe, плюс px/go. Код должен присвоить Plotly figure переменной fig. Пустые фигуры (0 точек данных) отклоняются с подсказкой, а не молча превращаются в пустой график. Возвращает артефакт plotly.

create_map(code: str) -> dict

То же, что и create_visualization, но требует px.scatter_mapbox(...) (никогда не scatter_map) и того, что предыдущий вызов query_host_sql уже извлёк lon/lat из geometry. Возвращает артефакт plotly.

Обязательный идентификатор образца: результирующая фигура отклоняется, если в hover_data не встречается primary_id (номер BioSample) — каждая нанесённая на график точка должна прослеживаться до конкретного образца. Это проверяется в коде (_check_hover_has_column(fig, "primary_id")), а не просто упоминается в docstring — карта без него получает жёсткий _fail(...).


Расширение сервера

Чтобы добавить новый инструмент:

  1. Напишите его как обычную функцию с декоратором @mcp.tool, возвращающую _ok(content, artifacts) или _fail(content) — никогда не вручную собранный dict.

  2. Если инструмент создаёт нечто, что клиент должен отрисовать особым образом (ссылку, таблицу, фигуру), используйте существующий type артефакта из таблицы выше, когда форма подходит, — это ноль изменений в клиенте. Вводите новый type (и подключайте его в диспетчерский цикл клиента), только если форма действительно новая.

  3. Все правила использования, предостережения и примеры пишете в docstring инструмента. Она передаётся LLM дословно как описание инструмента — это единственное место, где должна находиться специфичная для данных информация.

  4. Если инструменту нужна настройка по умолчанию, принимаемая в интерфейсе (как preview_rows или wikipedia_limit), дайте параметру именно то имя — клиент применит подходящую экспертную настройку любому инструменту, в JSON-схеме которого заявлен параметр с таким именем.


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

server_mcp.py читает .env (см. .env.example) при импорте через load_env_file() из mcp_config.py:

Переменную

Обязательно

По умолчанию

Описание

ENDPOINT

да

—

Хост S3-совместимого endpoint

ACCESS_KEY

да

—

Ключ доступа S3

SECRET_KEY

да

—

Секретный ключ S3

BUCKET

да

—

Имя бакета S3

VIRTUAL_DATASET

да

*.parquet

Ключ объекта Parquet-набора внутри бакета

REGION

нет

fr

Регион S3

S3_URL_STYLE

нет

path

Настройка DuckDB s3_url_style

TAXO_DB_PATH

нет

data/TAXONOMY.csv

Локальный путь к CSV-файлу таксономии

Несекретные параметры находятся в mcp_config.py.

Related MCP Connectors

Related MCP Servers