Skip to main content
Glama
Romumrn

ViromeChat MCP server

by Romumrn

ViromeChat MCP server

Viromech@t를 위해 모든 데이터셋 액세스, 외부 API 호출, 비즈니스 로직을 담당하는 FastMCP 서버입니다. 클라이언트(별도 viromechat 저장소에 있는 FastAPI 백엔드 / React 프런트)는 데이터프레임, S3 자격 증명, 또는 컬럼 이름을 직접 건드리지 않습니다. 그 대신 MCP/HTTP를 통해 이 서버와만 통신하며, 현재 서버가 게시하는 도구와 리소스를 읽어서 제네릭하게 동작합니다.

이 저장소는 그 서버가 독립적으로 존재하는 저장소입니다. 앱 저장소에 대한 어떤 의존성도 없습니다. 둘 사이의 유일한 계약은 아래에 문서화된 MCP 도구/리소스 세트이며, 백엔드는 MCP_SERVER_URL 환경 변수를 통해 이를 소비합니다.


실행하기

전제 조건: taxonomy 데이터셋(data/TAXONOMY.csv, ~327 MB)은 Git LFS로 저장됩니다. 클론 전에 머신마다 한 번 git lfs install을 실행하거나, 클론 후에 git lfs pull을 실행해 실제 파일을 채우세요.

로컬 (Python)

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

어느 쪽이든 0.0.0.0:8000에 HTTP 서버가 뜨고, MCP 엔드포인트는 /mcp(http://localhost:8000/mcp — 백엔드가 MCP_SERVER_URL로 지정하는 주소)입니다. 시작 시:

  1. data/TAXONOMY.csv를 df_taxo로 메모리에 완전히 로드합니다.

  2. 아래 두 MCP 리소스를 뒷받침하는 두 컬럼 설명 파일(data/v@_columns_description.csv와 data/TAXONOMY_columns_description.json)을 로드합니다.

  3. 인메모리 DuckDB 연결을 열고 httpfs와 spatial 확장을 설치한 다음, S3 Parquet 데이터셋 위에 host 뷰를 등록합니다 — Parquet 파일은 절대 메모리에 로드되지 않습니다. 모든 query_host_sql 호출은 DuckDB가 S3로 푸시다운합니다(컬럼/행그룹 프루닝).

테스트

pip install pytest
pytest

헬퍼 테스트는 순수 함수(_ok/_fail, figure/table 빌더, 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_resources()) 그리고 artifact["type"]에 따라 dispatch해야 합니다 — 도구 이름이나 컬럼 지식을 절대 하드코딩하면 안 됩니다. 덕분에 두 저장소가 분리됩니다: 기존 artifact 타입을 재사용하는 도구를 여기에 추가해도 클라이언트 변경은 필요 없습니다.


리소스

펼리는 대상 등 정적이고 한 번 읽는 지식입니다 — 도구처럼 LLM이 "호출"하는 것이 아닙니다. 클라이언트는 대화당 한 번 읽어 그 내용을 시스템 프롬프트에 포함합니다.

URI

콘텐츠

소스

resource://datasets/host/schema

host 테이블의 모든 컬럼에 대한 JSON 맵 {column_name: {description, Type}}

data/v@_columns_description.csv

resource://datasets/taxonomy/schema

df_taxo의 전체 JSON 스키마(name, description, columns, primary key, row definition)

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는 비어 있습니다. server_mcp.py 상단에 있는 _ok(content, artifacts) / _fail(content) 두 헬퍼가 이 형태를 만들므로, 이들을 항상 사용하고 자체 dict를 수기 생성하지 마세요.

아티팩트 타입

type

생성 도구

형태

클라이언트가 사용하는 방식

url

wikipedia_search

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

"Sources" 패널의 Wikipedia 링크

pubmed

pubmed_search

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

PubMed 링크 + PMID 안티셉션 가드용 PMID 화이트리스트

ncbi_taxonomy

ncbi_taxonomy_search

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

"Sources" 패널의 NCBI Taxonomy 링크

table

query_host_sql, query_dataframe

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

"Sources"에서 실행된 SQL/코드로 추적됨; rows는 preview_rows까지로 제한됨

plotly

create_visualization, create_map

{"type": "plotly", "figure": {...}} (fig.to_json()에서 나와 다시 dict로 파싱된 것)

렌더링된 Plotly 차트

클라이언트는 artifact["type"] 만으로 완전히 dispatch합니다 — 도구 이름이 아니라. 기존 artifact 타입을 재사용하는 도구(예: 또 다른 "table" 반환 도구)를 추가해도 클라이언트 변경이 전혀 없어집니다.


도구

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

Wikipedia에서 페이지를 찾습니다. 정확한 제목 일치가 없으면 가장 근접한 전문(full-text) 검색 결과로 대체합니다(콘텐츠에 "퍼지 매치" 노트로 표시됨). url 아티팩트를 반환합니다.

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

PubMed(NCBI E-utilities의 esearch + efetch, db=pubmed)를 검색하여 각 결과에 대해 제목, 저자, 저널, 연도, 초록, DOI, PMID를 반환합니다. 실제 확인된 모든 PMID를 담은 pubmed 아티팩트를 반환하며, 이는 클라이언트의 PMID 환각조사에 유일한 판단 기준이 됩니다.

ncbi_taxonomy_search(name: str) -> dict

어떤생물학적 이름(약어, 일반명, 학명)이든 NCBI Taxonomy 데이터베이스(E-utilities, db=taxonomy)에 대비해 식별합니다. 모든 일치 항목에 대해 계명, 순위(species/genus/family/...), division(분류군), 전체 lineage(계통), 알려진 동의어/약어를 반환합니다. HIV를 Human 개복면역결핍바이러스 1 / 속 Lentivirus로 바꾸거나, 어떤 이름이 속인지 과인지 확인하는 정식 방법입니다. Wikipedia의 표현에 좌우되지 않습니다. 가장 높은 일치 항목에 대해 ncbi_taxonomy 아티팩트를 반환합니다.

구현 사항: NCBI의 efetch XML은 각 결과의 <LineageEx> 내부에 상위 계급(rank)마다 하나씩 <Taxon>을 중첩시킵니다. 파서는 root.findall("Taxon")(직접 자손만)만 반복합니다 — .//Taxon을 쓰면 모든 조상도 별도 일치 결과로 잡히므로 주의하세요.

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

S3 Parquet 데이터셋인 host 뷰에 대해 읽기 전용 SELECT를 실행하고 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에는 큰 geometry blob을 포함한 ~65개의 열이 있습니다. 모든 행의 모든 열을 S3에서 가져오는 것은 이 안전장치가 도입되기 전에는 수분에 달하는 타임아웃을 일으키는 원인이었습니다. 호출자는 필요한 열만 지정해야 합니다.

  • 좌표는 단순 lat/lon이 아니라 기본 GEOMETRY 포인트 열에 있습니다. ST_X(geometry) AS lon, ST_Y(geometry) AS lat로 추출해 사용하세요(spatial 확장은 시작할때 로드됩니다).

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

df_taxo, df_host(= ctx.last_host_result, 아직 query_host_sql이 호출되지 않았다면 명확한 오류), pd, np가 유효 범위에 있는 상태로 pandas 코드를 실행합니다. 반드시 result 변수에 DataFrame을 할당해야 합니다. table 아티팩트를 반환합니다.

create_visualization(code: str) -> dict

query_dataframe과 동일한 실행 환경에 px/grp가 추가됩니다. 반드시 fig 변수에 Plotly figure를 할당해야 합니다. 빈 figure(0개의 점)는 안내 메시지와 함께 거부하고, 빈 차트를 조용히 반환하지 않습니다. plotly 아티팩트를 반환합니다.

create_map(code: str) -> dict

create_visualization과 같지만 px.scatter_mapbox(...)을 강제합니다(scatter_map는 아님). 그리고 그 앞 query_host_sql 호출이 이미 geometry에서 lon/lat을 추출해 놓은 상태여야 합니다. plotly 아티팩트를 반환합니다.

필수 샘플 식별자: hover_data에 primary_id(BioSample accession)가 나타나지 않으면 결과 figure는 거부됩니다. 그려진 모든 점은 정확한 샘플까지 거슬러 갈 수 있어야 합니다. 이것은 docstring에 요청한 사항일 뿐 아니라 코드에서도 강제됩니다 (_check_hover_has_column(fig, "primary_id")). 이 식별자가 없는 지도는 무조건 _fail(...)입니다.


서버 확장

새 도구를 추가하려면:

  1. @mcp.tool 데코레이션된 순수 함수로 작성하고, 수제 dict가 아니라 _ok(content, artifacts) 또는 _fail(content)을 반환합니다.

  2. 클라이언트가 특별히 렌더링해야 할 것을 만들어낸다면(링크, 표, figure) 기존 artifact type 중에 해당 형태를 지원하는 것을 재사용하세요. 그러면 클라이언트 변경이 전혀 필요 없습니다. 정말 새로운 형태일 때만 새 type을 만들고 그에 맞게 클라이언트 디스패치 루프도 수정해야 합니다.

  3. 모든 사용 규칙, 주의사항, 예제는 도구의 docstring에 넣어라. 이 docstring은 그대로 LLM에게 도구의 설명으로 전달됩니다. 데이터셋별 안내가 들어갈 유일한 곳이 바로 여기입니다.

  4. 도구가 UI로 설정 가능한 기본값(예: preview_row 또는 wikipedia_limit)이 필요하면 해당 매개변수를 그 이름으로 지정하세요. 클라이언트는 그 이름이 JSON 파라미터에 선언된 어떤 도구든 그에 맞는 전문가 설정을 적용할 수 있으므로.


환경설정

server_mcp.py는 import 시 mcp_config.py의 load_env_file()를 통해 .env( .env.example 참고)를 읽습니다:

Variable

필수

기본값

의미

ENDPOINT

예

—

S3 호환 엔드포인트 호스트

ACCESS_KEY

예

—

S3 액세스 키

SECRET_KEY

예

—

S3 시크릿 키

BUCKET

예

—

S3 버킷 이름

VIRAL_HOST_DATASET

예

*.parquet

버킷 안에 있는 Parquet 데이터셋의 객체 키

REGION

아니

fr

S3 지역

S3_URL_STYLE

아니

path

DuckDB의 s3_url_style 설정

TAXO_DB_PATH

아니

data/TAXONOMY.csv

taxonomy CSV의 로컬 경로

비보안 설정은 mcp_config.py에 있습니다.

Related MCP Connectors

Related MCP Servers