@cyanheads/pubmed-mcp-server
Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.
12 Tools • 1 Resource • 1 Prompt
Overview
Biomedical literature from PubMed, PubMed Central, and Europe PMC. Search it, fetch metadata and full text, resolve identifiers and partial citations, format references, and ground queries in MeSH vocabulary. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above. Ships the pubmed-guide agent skill (skill://pubmed-guide/SKILL.md), which MCP clients read for PubMed query syntax, DOI-to-PMID routes outside PMC, reference-list resolution, citation counts, and full-text misses.
Tools
Resources
Prompts
Capability reference
pubmed_search_articles tool
- Full PubMed query syntax plus filters (author, journal, MeSH, publication type, language, species, abstract, free full text) and publication/modification/Entrez date ranges
query is optional: with an author, journal, MeSH, or publication-type filter, the filters alone make the search (a journal's output over a date range, for example)
- Up to 1,000 results per page and offsets up to 9,998, or
maxResults: 0 for the match count alone; summaryCount adds briefs for up to 50 hits. A larger page size is served at the cap with a notice naming where to continue
- Reports
totalCount, the effectiveQuery PubMed ran, and the appliedFilters
- Rejects blank filter values, impossible dates, and reversed date ranges before PubMed is called
pubmed_fetch_articles tool
- Up to 200 PMIDs per call; misses are listed in
unavailablePmids
- Abstract, authors, journal, DOI, publication types, linked retraction/erratum/comment notices, and links; MeSH terms on by default, grants via
includeGrants, and the members of a collective author (its InvestigatorList, thousands on some consortium trials) via includeInvestigators
- A retracted, corrected, or questioned article opens with the same notice block as
pubmed_format_citations, directly under its title
dataBanks lists the record's trial registrations and data deposits (ClinicalTrials.gov NCT IDs, GenBank and PDB accessions, Dryad DOIs) verbatim; search pubmed_search_articles for <accession>[si] to find other records citing one
recordType separates journal articles from Bookshelf chapters and books; opt-in maxResponseCharacters defers overflow to deferred.ids
pubmed_fetch_fulltext tool
pmcids, pmids, and dois in any mix, up to 50 distinct identifiers per call: the first 10 are fetched and the rest returned in overLimit for a follow-up call. Each is tried against PMC, then Europe PMC, then Unpaywall (needs UNPAYWALL_EMAIL); an article several of them name is fetched and returned once, and viaSource names the tier that answered
source: "pmc" returns sections (the body, then back matter such as acknowledgments, declarations, and appendices), tables, and figures; source: "unpaywall" returns a best-effort HTML or PDF text body
- Europe PMC full text is fetched for records Europe PMC marks open access, preprints included; articles carry the
licenses their PMC JATS states
- Misses carry a typed
reason and triedTiers; sections, maxSections, maxCharacters, overflowMode, and maxResponseCharacters bound the response, truncation reports what was cut, and sectionCap names the sections maxSections cut
pubmed_europepmc_search tool
- Adds preprints (
PPR), patents (PAT), and Agricola (AGR) to MED and PMC; defaults to MED, PMC, PPR
- Cursor pagination via
cursorMark / nextCursorMark, up to 100 per page; a larger page size is served at the cap with a notice naming where to continue. Abstracts arrive as 400-character snippets
- Optional
sort of comma-separated <field> asc|desc keys; relevance is the default order, and a sort Europe PMC cannot use fails as europepmc_invalid_input
- Hits carry
license and hasFullTextXml
- Not registered when
EUROPEPMC_ENABLED=false
pubmed_europepmc_fetch tool
- Up to 25 records per call as
records: [{ source, epmcId }], each pair copied from a search hit, with the full abstract, license, and hasFullTextXml; unresolved ids land in notFound; opt-in maxResponseCharacters defers overflow to deferred.records
- Not registered when
EUROPEPMC_ENABLED=false
pubmed_fetch_citation_metrics tool
- Up to 1,000 PMIDs per call; the first 200 distinct are fetched and the rest returned in
overLimit, PMIDs NIH iCite has no record for in unavailablePmids
- Relative Citation Ratio (with a provisional flag), NIH percentile, citation count and rates, Approximate Potential to Translate, MeSH category shares, and clinical citations
- iCite does not flag retractions; check
pubmed_fetch_articles for notices
- Source: NIH iCite
pubmed_format_citations tool
- Up to 50 PMIDs per call, in any mix of
apa, mla, bibtex, ris, and vancouver
- Bookshelf records cite as edited books, each chapter dated by its own last revision or contribution date; PMIDs that can't be fetched are reported as unavailable
- Vancouver writes page ranges in elided Citing Medicine form (
1637-1645 → 1637-45); BibTeX issn and RIS SN carry the journal's linking ISSN, and RIS TI drops its trailing period
- Retractions, errata, and expressions of concern NCBI links to an article come back as
notices and are shown ahead of its citations; Vancouver adds a Retraction in: / Erratum in: note and APA a retraction parenthetical
pubmed_find_related tool
similar, cited_by, or references, up to 50 per page with offset pagination. A larger page size is served at the cap with a notice naming where to continue
- Falls back to Europe PMC, then OpenAlex, and names the provider that answered; fails as
all_providers_failed if none can
pubmed_spell_check tool
- Returns
original, corrected, and hasSuggestion; corrections land in place, so field tags, operators, spacing, and gene symbols stay as written, and a single letter such as an author initial is never swapped for another
- Values of indexed field tags (
aged[mh], zhang y[au], free full text[sb]) are kept as written; free-text fields ([tiab], [ti], [tw]) are still corrected
- Run it after a zero-hit or thin search, then retry
pubmed_search_articles with corrected
pubmed_lookup_mesh tool
- Descriptors by name or free-text term, with an exact heading match pinned first (the descriptor, for a drug-class heading); up to 50 per page, continued via
nextOffset. A larger page size is served at the cap with a notice naming where to continue
- Records carry their MeSH unique ID (
meshId, e.g. D000072657) beside the Entrez UID; includeDetails (default on) adds tree numbers, scope notes, and entry terms
pubmed_lookup_citation tool
- Up to 25 partial citations per call; a journal or a four-digit year is required, and volume, first page, and author sharpen the match
- Each is matched independently and comes back
matched, not_found, or ambiguous, with recovery detail
pubmed_convert_ids tool
- Up to 50 ids per call, all of one declared
idType (doi, pmid, pmcid); only PMC-indexed articles resolve
- One success or error row per id, in order, so a partial batch never fails
pubmed://database/info resource
- Live EInfo call for the
pubmed database, returned as application/json
count, lastUpdate, and fields[], whose names are the search tags pubmed_search_articles accepts
research_plan prompt
- Arguments:
title, goal, and keywords required; organism and includeAgentPrompts optional
- Returns a four-phase research plan;
includeAgentPrompts: "true" adds an agent-guidance block under each of its nine sub-steps, two of which point at tools: the literature review names pubmed_search_articles and pubmed_lookup_mesh, and the interpretation step names pubmed_search_articles
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
PubMed-specific:
- NCBI E-utilities (ESearch, EFetch, ESummary, ELink, ESpell, EInfo, ECitMatch) and the PMC ID Converter, with Europe PMC, OpenAlex, and Unpaywall filling the gaps
- Shared NCBI request queue: paced request starts, capped concurrency, a cooldown that holds every caller after an NCBI 429, and one deadline covering queue wait and retries
- XML parsing built for PubMed's inconsistent records: structured abstracts, missing fields, varying date formats
- Forgiving identifiers: a zero-padded PMID resolves as the PMID it spells in every tool that takes one;
pubmed_fetch_fulltext also matches DOIs case-insensitively and PMC IDs with or without the PMC prefix
- Hand-rolled citation formatters (APA, MLA, BibTeX, RIS, Vancouver) with no dependencies
Agent-friendly output:
- Provenance in responses: source labels, license fields on full-text and Europe PMC results, best-effort warnings on Unpaywall results, and effective-query echo on searches, so agents can judge what to trust
- Graceful partial failure: batch tools return per-item success/error rows instead of failing the request, with structured status codes and actionable next-step text
- Discriminated output contracts:
source: "pmc" | "unpaywall", typed unavailable reasons, viaSource and triedTiers fields, so callers branch on data, not string parsing
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
An NCBI API key in NCBI_API_KEY raises the rate limit from 3 to 10 requests per second (-e NCBI_API_KEY=your-key, or an env entry). For Docker or a source build, see Running the server.
Configuration
See .env.example for every server setting and the common framework overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
Docker
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/pubmed-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/pubmed-mcp-server:latest"]. To build locally, docker build -t pubmed-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for logging, ctx.state for storage
- Register new tools and resources in the
createApp() arrays in src/index.ts
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.