ViromeChat MCP server
Enables AI clients to access virome datasets and external bioinformatics APIs through MCP tools, including Wikipedia, PubMed, NCBI Taxonomy, read-only SQL over S3 Parquet, pandas/Plotly analyses, and map visualizations, while keeping the client decoupled from data and business logic.
README
ViromeChat MCP server
A FastMCP server that owns all dataset access, external API calls,
and business logic for Viromech@t. The client (the
FastAPI backend / React front, in the separate viromechat repo) never touches a dataframe, an S3
credential, or a column name directly — it only talks to this server over MCP/HTTP, generically,
by reading whatever tools and resources it currently publishes.
This repo is the standalone home of that server. It has no dependency on the app repo; the only
contract between them is the set of MCP tools/resources documented below, consumed by the backend
via its MCP_SERVER_URL env var.
Running it
Prerequisites: the taxonomy dataset (data/TAXONOMY.csv, ~327 MB) is stored via
Git LFS. Run git lfs install once per machine before cloning, or
git lfs pull after cloning, to materialize it.
Local (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
Either way it starts an HTTP server on 0.0.0.0:8000, MCP endpoint at /mcp
(http://localhost:8000/mcp — this is what the backend points MCP_SERVER_URL at). On startup it:
- Loads
data/TAXONOMY.csvfully into memory asdf_taxo. - Loads the two column-description files (
data/v@_columns_description.csvanddata/TAXONOMY_columns_description.json) that back the two MCP resources below. - Opens an in-memory DuckDB connection, installs the
httpfsandspatialextensions, and registers ahostview over the S3 Parquet dataset — the Parquet file is never loaded into memory; everyquery_host_sqlcall is pushed down to S3 by DuckDB (column/row-group pruning).
Tests
pip install pytest
pytest
The helper tests exercise the pure functions (_ok/_fail, figure/table builders, SQL guards) and
need no live S3 connection.
Integrating a client
Any MCP client can consume this server. The Viromech@t backend does it with a 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"})
The client should discover tools and resources dynamically (list_tools() / list_resources())
and dispatch on artifact["type"] — never hard-code tool names or column knowledge. That is
what keeps the two repos decoupled: adding a tool here that reuses an existing artifact type needs
no client change.
Resources
Resources are static, read-once knowledge — not something the LLM "calls" like a tool. The client reads them once per conversation and folds their content into the system prompt.
| URI | Content | Source |
|---|---|---|
resource://datasets/host/schema |
JSON map {column_name: {description, Type}} for every column of the host table |
data/v@_columns_description.csv |
resource://datasets/taxonomy/schema |
Full JSON schema (name, description, columns, primary key, row definition) of df_taxo |
data/TAXONOMY_columns_description.json |
Adding a new resource (e.g. a third dataset) requires no client-side change: the client discovers
resources via list_resources() and reads each one generically.
The response contract
Every tool returns exactly this shape, regardless of what it does:
{
"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
}
On failure, content holds the error message (with retry guidance where possible) and
artifacts is empty. The two helpers _ok(content, artifacts) / _fail(content) at the top of
server_mcp.py build this shape — always use them instead of hand-rolling a dict.
Artifact types
type |
Emitted by | Shape | Consumed by the client as |
|---|---|---|---|
url |
wikipedia_search |
{"type": "url", "url": "..."} |
Wikipedia link in the "Sources" panel |
pubmed |
pubmed_search |
{"type": "pubmed", "pmids": [123, 456]} |
PubMed links + PMID whitelist for the hallucination guard |
ncbi_taxonomy |
ncbi_taxonomy_search |
{"type": "ncbi_taxonomy", "url": "...", "tax_id": "..."} |
NCBI Taxonomy link in the "Sources" panel |
table |
query_host_sql, query_dataframe |
{"type": "table", "rows": [...], "columns": [...], "total_rows": N} |
Tracked as executed SQL/code in "Sources"; rows capped to preview_rows |
plotly |
create_visualization, create_map |
{"type": "plotly", "figure": {...}} (from fig.to_json(), parsed back to a dict) |
Rendered plotly chart |
The client dispatches purely on artifact["type"] — never on the tool's name. Adding a
tool that reuses an existing artifact type (e.g. another "table"-returning tool) requires no
client change at all.
Tools
wikipedia_search(search_term: str, wikipedia_limit: int = 4000) -> dict
Looks up a page on Wikipedia; falls back to the closest full-text search match if there's no exact
title match (flagged as a "fuzzy match" note in the content). Returns a url artifact.
pubmed_search(query: str, max_results: int = 5) -> dict
Searches PubMed (NCBI E-utilities esearch + efetch, db=pubmed) and returns title, authors,
journal, year, abstract, DOI, and PMID for each hit. Returns a pubmed artifact with every real
PMID found — this is the sole source of truth for the client's PMID hallucination guard.
ncbi_taxonomy_search(name: str) -> dict
Resolves any organism name — acronym, common name, or scientific name — against the NCBI
Taxonomy database (E-utilities, db=taxonomy). Returns, for every match: scientific name, rank
(species/genus/family/…), division, full lineage, and known synonyms/acronyms. This is the
authoritative way to turn HIV into Human immunodeficiency virus 1 / genus Lentivirus, or to
check whether a name is a genus or a family, without depending on Wikipedia's phrasing. Returns an
ncbi_taxonomy artifact for the top match.
Implementation note: NCBI's
efetchXML nests one<Taxon>per ancestor rank inside each result's<LineageEx>. The parser only iteratesroot.findall("Taxon")(direct children) — using.//Taxonwould also pick up every ancestor as if it were a separate match.
query_host_sql(sql: str, preview_rows: int = 50) -> dict
Runs a read-only SELECT against the host view (the S3 Parquet dataset), returning a table
artifact. This is the required first step before query_dataframe, create_visualization, or
create_map can use df_host — those tools operate on the result of the last query_host_sql
call (ctx.last_host_result), never on the full dataset.
Guardrails enforced before execution:
- Only a single
SELECTstatement —INSERT/UPDATE/DELETE/DDL/PRAGMA/...are rejected by_FORBIDDEN_SQL_KEYWORDS. - Bare
SELECT *is rejected outright.hosthas ~65 columns including a heavygeometryblob; pulling every column for every matching row over S3 is what caused multi-minute timeouts before this guard existed. Callers must project only the columns they need. - Coordinates live in a native
GEOMETRYpoint column, not plainlat/lon— extract them withST_X(geometry) AS lon, ST_Y(geometry) AS lat(thespatialextension is loaded at startup).
query_dataframe(code: str, preview_rows: int = 50) -> dict
Executes pandas code with df_taxo, df_host (= ctx.last_host_result, or a clear error if
query_host_sql hasn't been called yet), pd, and np in scope. Must assign a DataFrame to
result. Returns a table artifact.
create_visualization(code: str) -> dict
Same execution environment as query_dataframe, plus px/go. Must assign a Plotly figure to
fig. Rejects empty figures (0 data points) with a guidance message rather than silently returning
a blank chart. Returns a plotly artifact.
create_map(code: str) -> dict
Same as create_visualization, but enforces px.scatter_mapbox(...) (never scatter_map) and
that the preceding query_host_sql call already extracted lon/lat from geometry. Returns a
plotly artifact.
Mandatory sample identifier: the resulting figure is rejected unless primary_id (the
BioSample accession) appears in hover_data — every plotted point must be traceable back to its
exact sample. Enforced in code (_check_hover_has_column(fig, "primary_id")), not just requested
in the docstring — a map missing it is a hard _fail(...).
Extending the server
To add a new tool:
- Write it as a plain function decorated with
@mcp.tool, returning_ok(content, artifacts)or_fail(content)— never a hand-built dict. - If it produces something the client should render specially (a link, a table, a figure), reuse
an existing artifact
typefrom the table above whenever the shape fits — this means zero client changes. Only invent a newtype(and wire it into the client's dispatch loop) if the shape is genuinely new. - Put every usage rule, caveat, and example in the tool's docstring. It is sent verbatim to the LLM as the tool's description — this is the only place dataset-specific guidance should live.
- If the tool needs a UI-configurable default (like
preview_rowsorwikipedia_limit), just name the parameter that; the client applies the matching expert setting to any tool whose JSON schema declares a parameter with that name.
Configuration
server_mcp.py reads .env (see .env.example) at import time, via
load_env_file() from mcp_config.py:
| Variable | Required | Default | Meaning |
|---|---|---|---|
ENDPOINT |
yes | — | S3-compatible endpoint hostname |
ACCESS_KEY |
yes | — | S3 access key |
SECRET_KEY |
yes | — | S3 secret key |
BUCKET |
yes | — | S3 bucket name |
VIRAL_HOST_DATASET |
yes | *.parquet |
Object key of the Parquet dataset inside the bucket |
REGION |
no | fr |
S3 region |
S3_URL_STYLE |
no | path |
DuckDB s3_url_style setting |
TAXO_DB_PATH |
no | data/TAXONOMY.csv |
Local path to the taxonomy CSV |
Non-secret settings live in mcp_config.py.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.