ViromeChat MCP server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ViromeChat MCP serverSearch PubMed for recent papers on bat coronaviruses"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
C'est quoi un MCP, et pourquoi ?
Un MCP (Model Context Protocol), c'est une façon standard de donner accès à des données à une IA. Plutôt que de recoder une intégration à chaque fois, on expose des « outils » une bonne fois, et n'importe quel assistant qui parle MCP sait les utiliser tout seul, en langage naturel.
Dans ce projet, c'est ce serveur qui fait le vrai boulot : il a les accès S3, il charge la taxonomie, il écrit le SQL, il pose les garde-fous. L'IA, elle, ne voit jamais les données, ni les acces S3, ni les mots de passe, elle demande juste « les hôtes de tel virus » et le serveur lui renvois lme resultats.
L'intérêt est surtout là : les données et l'IA ne se touchent plus directement. On peut changer de modèle ou de front sans rien casser ici, toute la logique reste au même endroit, et comme c'est un standard, le même serveur pourrait être branché sur un autre assistant sans le réécrire. Ce qui peut etre tres pratique pour ouvir l'acces a un plus grand nombre.
La techno du MCp n'est absolument pas un truc niche, de geek de l'IA, c'est en pleine expension : data.gouv.fr a fait pareil avec un serveur MCP par-dessus ses ~74 000 jeux de données publiques, pour qu'on puisse les explorer en langage naturel au lieu de passer par des API. Ils le résument bien, ça n'ouvre aucune donnée nouvelle, ça rend juste l'existant beaucoup plus simple à atteindre. C'est exactement l'idée ici, voir plus fino dans leur article
Related MCP server: TogoMCP
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.pyDocker
cp .env.example .env # fill in your S3 credentials
docker compose up --buildEither 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
pytestThe 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 |
| JSON map |
|
| Full JSON schema (name, description, columns, primary key, row definition) of |
|
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, 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
| Emitted by | Shape | Consumed by the client as |
|
|
| Wikipedia link in the "Sources" panel |
|
|
| PubMed links + PMID whitelist for the hallucination guard |
|
|
| NCBI Taxonomy link in the "Sources" panel |
|
|
| Tracked as executed SQL/code in "Sources"; |
|
|
| 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, and so on), 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 |
| yes | (none) | S3-compatible endpoint hostname |
| yes | (none) | S3 access key |
| yes | (none) | S3 secret key |
| yes | (none) | S3 bucket name |
| yes |
| Object key of the Parquet dataset inside the bucket |
| no |
| S3 region |
| no |
| DuckDB |
| no |
| Local path to the taxonomy CSV |
Non-secret settings live in mcp_config.py.
This server cannot be deployed
Maintenance
Related MCP Connectors
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
Connect AI clients to biomedical data and tools.
Let AI agents query data and act across all your business apps via MCP.
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to discover and execute tools via a secure MCP server with JWT authentication, RBAC, rate limiting, and audit logging.1MIT
- AlicenseAqualityAmaintenanceAn MCP server that gives AI assistants access to biological and biomedical RDF databases via SPARQL at the RDF Portal, as well as selected REST APIs (NCBI E-utilities, UniProt, ChEMBL, PDB, Reactome, Rhea, MeSH, and more).2913MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to query live schema, lineage, and query-context across data warehouses, dbt projects, orchestration systems, and BI tools via MCP tools.Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to access a unified catalog of tools from various APIs (OpenAPI, GraphQL, MCP, Google Discovery) through the MCP protocol.MIT