Data Aggregator Mcp
π π π πͺ π§ - Search and fetch research datasets across Zenodo, DataCite (Dryad/Figshare/Dataverse/OSF), NCBI omics (GEO/SRA/BioProject), and literature (PubMed/OpenAIRE) behind one normalized model β DOI deduplication, NCBI-Taxonomy synonym expansion, paperβdata linking, and checksum-verified download. uvx data-aggregator-mcp.
Quick Install
{
"mcpServers": {
"musharna-data-aggregator-mcp": {
"command": "npx",
"args": [
"-y",
"musharna-data-aggregator-mcp"
]
}
}
}Using an AI coding agent (Claude Code, Cursor, etc.)? Copy a ready-made prompt that tells it to fetch the setup instructions and install this server for you.
Documentation Overview
π data-aggregator-mcp
One MCP server to find and fetch research data across archives, omics registries, and literature β behind a single normalized model.
search one query across 17 sources β Zenodo, DataCite (Dryad /
Figshare / Dataverse / OSF / OpenNeuro / Mendeley), NCBI omics
(GEO / SRA / BioProject), BioStudies (EBI, incl. ArrayExpress),
literature (PubMed / OpenAIRE), HuggingFace datasets, DataONE
(eco / environmental), OmicsDI (proteomics / metabolomics), DANDI
(neurophysiology), CZ CELLxGENE (single-cell), OpenML (ML datasets),
RCSB PDB (structures), UniProtKB (proteins), the GWAS Catalog,
GBIF (biodiversity), data.gov (US federal open data), and NASA CMR
(Earth science) β deduplicated, normalized, and cross-linked. resolve any hit to its file
manifest, citation, trust signals, and the data it points at. fetch it to
disk with checksum verification.
mcp-name: io.github.musharna/data-aggregator-mcp
β¨ Why this
Most data MCPs wrap a single source. This one unifies them behind six tools
and one DataResource model, so an agent searches once and gets back comparable
records:
- Multi-domain, one model β generalist archives + raw omics + literature, deduplicated by DOI (the fetchable record wins over bare metadata).
- Taxonomy synonym expansion β
organism="Orobanche aegyptiaca"also matchesPhelipanche aegyptiaca(NCBI Taxonomy), so a species rename doesn't cost you results. - Paper β data bridge β resolve a paper and get links to the GEO / SRA / BioProject / DataCite records it produced.
- Verified fetch β streams to disk with md5 verification where the source exposes a checksum, optional archive unpacking, and a fail-loud integrity sniff that rejects an HTML paywall page served as a "PDF".
- Citations, access & full text β render a citation in any CSL style, get
normalized access/license, and pull open-access full text β all in one
resolve. - Trust signals β usage
metrics(citations / views / downloads / likes), version status (is_latest/superseded_by), andlast_updatedfreshness, surfaced wherever the source exposes them. - Interop exports β
resolve(format="croissant")or"ro-crate"hands a dataset to an ML or research-packaging pipeline as standard JSON-LD. - Operate on data in place β
operatereads the schema, previews rows, or runs a read-only SQLSELECTagainst a remote Parquet/CSV/TSV without downloading it (Parquet footer + DuckDB httpfs range reads). Optional[operate]extra; base install is unchanged. - Relate across records β
relatetakes a handful of resolved ids and reports how they connect β shared accession, shared cross-identifier, an explicit link, or version lineage β naming the literal shared value as evidence. Metadata hints only: it never reads files or executes a join.
β Full rationale and a comparison vs. single-source servers, breadth gateways, and ML-dataset tools: docs/POSITIONING.md.
β‘ Quickstart
Run with no install:
uvx data-aggregator-mcp
Register with Claude Code:
claude mcp add data-aggregator -- uvx data-aggregator-mcp
A typical agent flow:
search("drought stress RNA-seq", organism="Sorghum bicolor")
β [ geo:GSE..., sra:SRX..., zenodo:..., pubmed:... ] # deduped, taxa-normalized
resolve("sra:SRX079566")
β DataResource{ files: [ENA FASTQ urlsβ¦], access: "open", taxa: [...] }
fetch("sra:SRX079566", dest="./data")
β ["./data/SRX079566_1.fastq.gz", β¦] # md5-verified
Other ways to run (pip, python -m, raw client config)
pip install data-aggregator-mcp
data-aggregator-mcp # or: python -m data_aggregator_mcp
To use the operate tool (query remote tabular files in place), install the
optional extra:
pip install "data-aggregator-mcp[operate]"
Add to a client's MCP config (e.g. Claude Desktop claude_desktop_config.json):
{
"mcpServers": {
"data-aggregator": {
"command": "uvx",
"args": ["data-aggregator-mcp"],
"env": { "NCBI_API_KEY": "your-optional-key" }
}
}
}
π Transports
stdio (default) β the server runs as a child of the client, so fetch()
writes to your own disk. Nothing to configure; every command above uses it.
Streamable HTTP β the same six tools, prompts, and resources over HTTP:
data-aggregator-mcp --transport http # β http://127.0.0.1:8000/mcp/
| flag | default | notes |
|---|---|---|
--transport {stdio,http} | stdio | |
--host | 127.0.0.1 | this machine only; any non-loopback value requires --allow-host |
--port | 8000 | |
--allow-host HOST:PORT | auto on loopback | permitted Host header, repeatable β required off loopback |
--allow-origin ORIGIN | derived | permitted browser Origin header, repeatable |
--stateless | off | fresh transport per request, no session affinity |
--json-response | off | plain JSON responses instead of SSE streams |
The endpoint is served at /mcp/ β with the trailing slash. /mcp answers
307 redirecting there, which is fine for any client that follows redirects (a
307 preserves the POST body); point one that doesn't straight at /mcp/. In
stateful mode, sessions idle for 30 minutes are reaped.
DNS-rebinding protection is always on. A loopback bind derives its own
host/origin allowlist, so the default needs no configuration. A non-loopback bind
(--host 0.0.0.0, a LAN address, a container interface) refuses to start
without at least one explicit --allow-host β guessing an allowlist there is
precisely the hole the protection exists to close, so it fails loud instead of
open:
data-aggregator-mcp --transport http --host 0.0.0.0 \
--allow-host data.example.org:8000
Once running, a request whose Host header is outside the allowlist is refused
with 421 Invalid Host header.
β οΈ
fetch(dest=β¦)writes to the server's filesystem, not the client's. Over stdio those are the same disk; over HTTP they may be different machines, and the caller gets back paths it cannot read. Treatdeston an HTTP deployment as server-side staging, or use stdio when you need the bytes locally.search,resolve,operate,relate, andlist_sourcesare unaffected β they return data, not paths.
ποΈ Sources
| Source | Discover | Fetch | Checksum |
|---|---|---|---|
| Zenodo | β | β | md5 |
| DataCite β Figshare | β | β | md5 |
| DataCite β Dataverse | β | β | md5 |
| DataCite β OSF | β | β | md5 |
| DataCite β Dryad | β | manifest onlyΒΉ | sha-256 (listed) |
| DataCite β Mendeley & others | β | β | β |
| NCBI SRA | β | β (ENA FASTQ) | md5 |
| NCBI GEO | β | β
(suppl/) | noneΒ² |
| NCBI BioProject | β | β SRA links | β |
| PubMed / OpenAIRE | β | β (OA full text) | noneΒ² |
| HuggingFace datasets | β | β (resolve URL) | none |
| DataONE (eco/env) | β | β (Member Node) | md5 / sha-256 |
| OmicsDI β PRIDE | β | β (HTTPS FTP) | size only |
| OmicsDI β MetaboLights | β | β (HTTPS FTP) | none |
| OmicsDI β other MS repos | β | β | β |
| DataCite β OpenNeuro | β | β (snapshot) | noneΒ² |
| DANDI (neurophysiology) | β | β (302βS3) | noneΒ² |
| CZ CELLxGENE (single-cell) | β | β (H5AD/RDS) | noneΒ² |
| OpenML (ML datasets) | β | β (ARFF) | md5 |
| RCSB PDB (structures) | β | β (.cif/.pdb) | noneΒ² |
| GWAS Catalog | β | β PMID bridge | β |
ΒΉ Dryad downloads are token / bot-challenge gated, so fetch fails loud;
resolve still lists the files.
Β² No upstream checksum β fetch verifies content-type instead (rejects an HTML
page served in place of a binary).
π οΈ Tools
search(query?, size?, sources?, organism?, disease?, tissue?, chemical?, assay?, kind?, published_after?, published_before?, rank?, cursor?, collapse_mirrors?, understand?, multi_query?, provenance?)
Fan out across all wired sources in parallel and return compact DataResource
records, deduped by DOI. Per-source failures land in errors{} β never silently
dropped.
organismβ expand the query with NCBI-Taxonomy synonyms; the expansion is echoed intaxon_expansion, and results carry normalizedtaxa[]({taxid, name}) plus adescribed_inlink to plant-genomics-mcp for plant taxa.sourcesβ restrict the fan-out, e.g.["omics"].sizeβ max results (1β50).kindβ keep onlydataset/sequencing_run/study/publication/software.published_after/published_beforeβ filter by publication year.rankβrelevance(default) orsemantic(re-rank the fetched page by embedding similarity to the query; needsEMBEDDING_API_BASE, degrades to relevance order otherwise).understandβ opt into LLM query understanding (default false). A free-text query is normalized into a focused keyword query: conversational fluff ("I'm looking forβ¦","where can I findβ¦") is stripped while the scientific and entity terms are kept so they still match by text. The LLM also detects structured entities (organism/disease/tissue/chemical/assay, kind) β these are echoed inquery_understanding.extractedfor transparency but not auto-applied, because ANDing LLM-inferred facets across free-text keyword upstreams over-constrains and hurts recall. Only the cleanedkeyword_coreand explicityearscopes are applied; the ontology resolvers still run on the facets you pass (the LLM proposes, you dispose). Needs an LLM endpoint (LLM_API_BASE); with none configured the search runs unchanged and notes it inerrors['understand']. Effectiveness is query- and model-dependent β opt-in / default-off; validate the recall lift on your own corpus and LLM (see the eval harness below). On our small verified setmulti_query=is the stronger, always-safe recall lever;understand=is approximately neutral with a weak local model.multi_queryβ opt into diverse multi-query recall expansion (default false). An LLM generates up to a few deliberately-diverse reformulations of your query (different facets/synonyms/framings, not paraphrases), each is fanned out across every source, and the deduped union is re-ranked against your original query β surfacing relevant records a single keyword query would miss. Bounded atMAX_QUERY_VARIANTS(4, incl. the original, which is always kept so recall never drops below baseline), so it costs at most NΓ the upstream calls. Composes withunderstand=(which structures variant 0). The variants used are echoed inquery_expansion. Needs an LLM endpoint (LLM_API_BASE); with none configured the search runs as a normal single query and notes it inerrors['multi_query'].cursorβ opaque token from a prior result'snext_cursor; pages forward across every source. Incursormode the other params are read from the token, soqueryis optional.
resolve(id, cite?, format?, trust?, fair?, use?)
Full record + files manifest. Routes by id shape β zenodo:7654321, a bare DOI,
datacite:10.5061/dryad.x, an omics id (sra:SRX079566, geo:GSE332789,
bioproject:PRJNA1468572), a literature id (pubmed:34320281, openaire:<id>),
a HuggingFace id (hf:owner/name), a DataONE id (dataone:doi:10.5063/F1HT2M7Q),
or an OmicsDI id (omicsdi:pride:PXD000001). Attaches, where available:
files[]β ENA FASTQ manifest (SRA), GEOsuppl/, or the host repo's native manifest (Figshare / Dataverse / OSF / Dryad).links[]β paper β data:pubmed:βsra:/geo:/bioproject:(NCBI elink);openaire:βdatacite:(ScholeXplorer Scholix).access/licenseβ normalized status (open/embargoed/restricted/closed/unknown) and license where the source exposes it.identifiersβ normalized{pmid, pmcid, doi}, plus an open-access full-textFileEntry(EuropePMC XML, or an Unpaywall PDF fallback) for papers.citationβ passcite=<format>:bibtex,ris,csl-json, or any CSL style name (apa,mla,vancouver, β¦). DOI records use content negotiation; others render CSL-JSON from metadata. Off by default; failures degrade quietly.- trust signals β
metrics(citations / views / downloads / likes),is_latest/superseded_by(derived from version links), andlast_updatedfreshness, where the source provides them. trust=trueβ attach retraction status (via Crossref) undertrust{}. One extra Crossref call; meaningful for DOI-bearing records only.fair=trueβ attach an RDA-grounded FAIRness score (0β100 + F/A/I/R sub-scores + actionable gaps) computed from the record metadata underfair{}. Pure/local β no extra network call.use=<intent>β attach a licence-compatibility advisory underlicense_compat{}for the intended use (commercial/redistribute/modify/ml-training). Returns ALLOW/REVIEW/DENY with the governing clause. Metadata-derived advisory, not legal advice; an absent/unrecognized licence yields REVIEW.formatβ passformat="croissant"(file-level Croissant JSON-LD),"ro-crate"(minimal RO-Crate 1.1), or"provenance"(one-call RO-Crate 1.1 data-availability dossier bundling version-currency, licence+SPDX, FAIR score, and retraction status) to attach a standard manifest under the matching field.
fetch(id, dest?, files?, max_bytes?, force?, extract?)
Download files to disk and return their paths. Streams under a max_bytes guard
(force to override) with md5 verification wherever a checksum exists.
filesβ restrict to a subset of the resolved manifest.extractβ unpack downloaded zip / tar archives in place, guarded against path traversal and runaway extracted size. Off by default.- Unverified fetches (GEO
suppl/, literature full text) get a content-type sniff that fails loud if a declared binary is actually an HTML page. - Fetchable: Zenodo, SRA, GEO, DataONE (Member-Node objects,
md5/sha-256 verified), DataCite-hosted Figshare / Dataverse / OSF,
HuggingFace datasets, PRIDE / MetaboLights (via OmicsDI, unverified),
and literature open-access full text. Dryad, other DataCite repos, and
other OmicsDI repos (MassIVE / GNPS / ...) are discovery-only and raise
FetchNotSupportedError.
list_sources()
Wired sources with their capabilities β layer, kinds, supported filters,
fetchability, operable flag, id examples, auth, and rate limits.
operate(op, id, file?, query?, n?, columns?)
Inspect or query a remote tabular file (Parquet / CSV / TSV) without
downloading it. Addresses a file by catalog id + file name (defaults to the
first tabular file on the resolved record). Ops:
schemaβ column names + types (reads the Parquet footer / sniffs the CSV header; no full load).previewβ a small sample of rows.headβ the firstnrows (default 20), optionally restricted tocolumns.sqlβ a read-onlySELECT(the file is the viewdata), e.g.SELECT col, count(*) FROM data GROUP BY 1.peekβ per-column profile via DuckDBSUMMARIZE(type, null-rate, approximate distinct count, min/max, numeric quartiles) without downloading the file. Likehead/sql, reads the whole file and honors the source-size ceiling.
Backed by the Parquet footer reader + DuckDB httpfs range reads. sql runs in
a locked-down DuckDB (read-only, local filesystem disabled, single-SELECT
validation, row / wall-clock caps). Requires the optional [operate] extra
(pip install data-aggregator-mcp[operate]); without it, operate returns a
clear install-the-extra message and the other four tools are unaffected.
Any HuggingFace dataset with a datasets-server converted view is operable
(schema / preview / head / sql): resolve surfaces the auto-converted
Parquet files (source="hf-datasets-server") even for datasets stored as
JSON/JSONL/arrow, so pass file=<config>/<split>/...parquet to pick a split when
there are several.
relate(ids)
Cross-resource join/harmonization hints. Given 2β10 resource ids, relate resolves
each (TTL-cached) and reports how they relate and on what key they could be joined:
shared_accessionβ same BioProject/SRA/GEO accession on β₯2 records β joinable key.shared_identifierβ same doi/pmid/pmcid across records β same work / paperβdata link.explicit_linkβ one record'slinks[]points at another input record.version_lineageβ one record supersedes another (dedupe, don't join, those).
Hints only. relate never reads file columns, fetches files, or executes a
join/merge/conversion β every hint names the shared value as evidence. Per-id resolve
failures are reported in errors, not fatal; an empty result carries an explanatory
note.
Prompts
Three workflow prompts surface in clients (e.g. /mcp__data_aggregator__* in
Claude Code):
find_dataβ find datasets for a topic, optionally scoped to an organism.data_behind_paperβ find the datasets / accessions behind a paper.search_resolve_fetchβ walk the end-to-end search β resolve β fetch flow.
βοΈ Configuration
All optional, set via environment variables:
NCBI_API_KEYβ raises the NCBI E-utilities rate limit (3 β 10 req/s) used by the omics, literature, and taxonomy lookups.DATA_GOV_API_KEYβ a free api.data.gov key for the data.gov source. Absent β requests fall back to the shared publicDEMO_KEY, rate-limited to roughly 30 requests/hour per IP β fine for light discovery, worth setting for anything heavier.UNPAYWALL_EMAILβ enables the Unpaywall fallback leg of literature full-text retrieval (the EuropePMC leg works without it).NCBI_EMAILβ contact address sent to NCBI's ID converter; falls back toUNPAYWALL_EMAILwhen unset.DATAVERSE_BASE_URLβ resolve Dataverse DOIs against a different installation (defaulthttps://dataverse.harvard.edu).CACHE_TTL_SECONDSβ resolve-cache lifetime in seconds (default3600; an unparseable value falls back to that default).EMBEDDING_API_BASE/EMBEDDING_API_KEY/EMBEDDING_MODELβ an OpenAI-compatible embeddings endpoint enablingrank=semantic. Absent β semantic re-rank degrades to relevance order. Key is optional (keyless local servers supported); model defaults totext-embedding-3-small.LLM_API_BASE/LLM_API_KEY/LLM_MODELβ an OpenAI-compatible/chat/completionsendpoint enablingsearch(understand=true)(NLβstructured query rewriting) andsearch(multi_query=true)(diverse multi-query recall expansion). Absent β both run the raw query unchanged and note it inerrors['understand']/errors['multi_query']. Key is optional (keyless local servers supported); model defaults togpt-4o-mini(a passthrough string β set it to whatever your endpoint serves).multi_queryfans out at mostMAX_QUERY_VARIANTS(4, incl. the original) variants, bounding the NΓ cost.
To measure the recall lift of understand=true / multi_query=true on a small
labeled set, run the gated eval harnesses (need a live LLM endpoint):
DATA_AGGREGATOR_MCP_LIVE=1 LLM_API_BASE=... python scripts/eval_understand.py
DATA_AGGREGATOR_MCP_LIVE=1 LLM_API_BASE=... python scripts/eval_multi_query.py
They print per-query and mean recall@20 (understand / multi-query off vs. on). See
the fixtures at scripts/eval_understand_fixture.json and
scripts/eval_multi_query_fixture.json.
π§ͺ Develop
uv venv && uv pip install -e ".[dev]"
uv run pytest -q
uv run ruff check src tests
DATA_AGGREGATOR_MCP_LIVE=1 uv run pytest -k live -q # real-API probes
The README demo (examples/assets/demo.svg) is recorded network-free from
examples/_demo_stdio.py β see the header of that file to re-record.
License
MIT β see LICENSE.