Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms.
Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://pubmed.caseyjhand.com/mcp
The biomedical literature via NCBI's E-utilities, 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.
| Tool | Description |
|---|---|
pubmed_search_articles | Search PubMed with full query syntax, field-specific filters, date ranges, pagination, and optional brief summaries |
pubmed_europepmc_search | Search Europe PMC for preprints, patents, Agricola, and EPMC-only OA records that don't surface in PubMed. Cursor-based pagination. |
pubmed_europepmc_fetch | Fetch complete Europe PMC records — including the untruncated abstract — by source + epmcId, the only identifier many preprint, patent, and Agricola records carry |
pubmed_fetch_articles | Fetch full article metadata by PMIDs — abstract, authors, journal, MeSH terms, grants |
pubmed_fetch_fulltext | Fetch full-text articles via a chain: NCBI PMC EFetch → Europe PMC fullTextXML → Unpaywall. Accepts PMIDs, PMCIDs, or DOIs. |
pubmed_format_citations | Generate formatted citations in APA 7th, MLA 9th, BibTeX, RIS, or Vancouver (ICMJE/NLM) |
pubmed_find_related | Find similar articles, citing articles, or references for a given PMID |
pubmed_spell_check | Spell-check a PubMed query via NCBI ESpell — every misspelled token corrected in one call; the recovery step after a zero-hit or thin search |
pubmed_lookup_mesh | Search MeSH by heading — tree numbers, scope notes, entry terms — for building controlled-vocabulary queries |
pubmed_lookup_citation | Resolve partial bibliographic references — one citation or a batch of up to 25 — to PubMed IDs via ECitMatch |
pubmed_convert_ids | Convert between DOI, PMID, and PMCID using the PMC ID Converter API |
| Resource | Description |
|---|---|
pubmed://database/info | PubMed database metadata via EInfo (field list, record count, last update) |
| Prompt | Description |
|---|---|
research_plan | Generate a structured 4-phase biomedical research plan outline |
pubmed_search_articles toolbookTitle, publisherName, docType, and editors in place of the empty source; the rendered summary shows the doc type only for these, not for ordinary journal articles (citation)totalCount, stated in the header beside the page (Returned: 3 of 2924); echoes the original query, the fully applied PubMed query, and normalized filter metadata[pdat], or empty () — is rejected as blank_query rather than sent upstreamlimit is accepted for maxResultspubmed_fetch_articles toolids is accepted for pmids00000001) resolves as the PMID it spells; unavailablePmids lists misses as you sent themrecordType (journal-article / book-chapter / book) plus a book object (title, publisher, editors, ISBNs, Bookshelf accession); journalInfo is absent on themjournalInfo.elocationId + elocationIdType rather than a page rangemaxResponseCharacters keeps whole records in order until the ceiling, then defers the rest to deferred.ids for a follow-up callpubmed_fetch_fulltext toolpmcids, pmids, or dois (one id per element), up to 10 per request; a zero-padded PMID or PMC ID resolves as the ID it spells, DOIs match case-insensitively, and PMC IDs match with or without the PMC prefix in any case. Each record is fetched once however many spellings name it; unavailable[].id keeps your PMID or DOI spelling, and reports a PMC ID as PMC<digits>fullTextXML (EUROPEPMC_ENABLED, default on) → Unpaywall (needs UNPAYWALL_EMAIL); viaSource names which tier served each articlepubmed_europepmc_search but no full text through this chain — Europe PMC's fullTextXML is PMC-keyedsource: "pmc" returns structured sections plus tables[] (cells, caption, label, footnotes) and assets[] (figures and supplementary material, with [Figure: <label>] markers left in the body, and the file pointer read through <alternatives> when a figure offers several formats); source: "unpaywall" returns a best-effort body with contentFormat (html-markdown / pdf-text), a title from Unpaywall's record (else the Europe PMC record, else the HTML page), and journalName / year when Unpaywall has themuntitled section, the same name the truncation ledger gives it, and section titles in headings and ledger lines are Markdown-escapedreason (not-found, no-doi, doi-lookup-failed, no-oa, service-error, …), idType, triedTiers (per-tier outcome in execution order), and unqueriedTiers when an unconfigured tier could have served the idsections (case-insensitive title match), maxSections, includeTables, includeAssets, maxCharacters, maxCharactersPerSection, overflowMode (truncate / outline), and maxResponseCharacters, which defers whole articles past the ceiling to deferred.ids; a truncation object reports what was shortened or omitted, per section and subsectiontruncate drops every section and subsection past the cut, while outline keeps each heading and marks one the budget left emptypubmed_europepmc_search toolPPR), patents (PAT), Agricola (AGR), alongside MED and PMC; default sources is ["MED", "PMC", "PPR"]cursorMark — * for the first page, then nextCursorMark; pageSize up to 100, with max_results and limit accepted for itsource plus pmid / pmcId / doi when known; abstractSnippet is capped at 400 characters, with abstractTruncated flagging the cuttotalCount reports the full hit count, stated in the header beside the page; searchUrl opens the same source-filtered query on europepmc.orgEUROPEPMC_ENABLED=falsepubmed_europepmc_fetch toolsource + epmcId — the only identifier preprint, patent, and Agricola records reliably carrynotFound rather than failing the batchEUROPEPMC_ENABLED=falsepubmed_format_citations toolids is accepted for pmids, and a zero-padded PMID resolves as the PMID it spellspubmed_find_related toolsimilar, cited_by, or references for a PMID, in NCBI relevance order, enriched with title, authors, date, and source (or Bookshelf book title and publisher)all_providers_failed error rather than an empty resultmaxResults up to 50 (limit also accepted) with offset pagination; totalCount reports the full match count, stated in the header beside the pagepubmed_spell_check tooloriginal, corrected, and hasSuggestion; every misspelled token is corrected in one call (alzhiemer diseese treatmnt outcomse → alzheimer disease treatment outcomes)pubmed_search_articles result, or when a drug, gene, disease, or author name may be misspelled, then re-run the search with correctedpubmed_lookup_mesh toolmeshId (DescriptorUI), entrezUid, and, with includeDetails (default on), tree numbers, scope notes, and entry termsmaxResults up to 50 (limit also accepted) with offset pagination via nextOffset; totalCount reports the upstream match countpubmed_lookup_citation toolcitations takes an array of up to 25 or a single citation object; citation is accepted for itkey label is exemptmatched, not_found, and ambiguous statuses with recovery detailpubmed_convert_ids tool"23193287,37952131" is rejected rather than expandedrequestedId exactly as sent — repeats, a bare-digit PMCID, and a DOI's casing included; a partial batch never fails as a wholepubmed://database/info resourcepubmed database, returned as application/jsondbName, description, count, lastUpdate, and fields[] — each field's short name (the tag usable in pubmed_search_articles queries), fullName, and descriptionresearch_plan prompttitle, goal, keywords (comma-separated) required; organism and includeAgentPrompts ("true" / "false") optionalincludeAgentPrompts: "true" adds an agent-guidance block under each sub-step, several of which point at pubmed_search_articles and pubmed_lookup_meshBuilt 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:
isArray hints for PubMed's inconsistent XML structureAgent-friendly output:
source: "pmc" | "unpaywall", typed unavailable reasons, viaSource and triedTiers fields — callers branch on data, not string parsingA public instance is available at https://pubmed.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"pubmed-mcp-server": {
"type": "streamable-http",
"url": "https://pubmed.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file.
{
"mcpServers": {
"pubmed-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/pubmed-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"NCBI_API_KEY": "your-key-here"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"pubmed-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/pubmed-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"NCBI_API_KEY": "your-key-here"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"pubmed-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/pubmed-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/pubmed-mcp-server.git
cd pubmed-mcp-server
bun install
Key environment variables:
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path where the MCP server is mounted | /mcp |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto (resolves to stateful). This server ships stateless — it has no ctx.requestInput call sites. | stateless |
MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments (landing page, Server Card, RFC 9728 metadata). | none |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.) | info |
MCP_GC_PRESSURE_INTERVAL_MS | Opt-in Bun-only forced-GC pressure loop (ms). Drains the per-request McpServer/McpSessionTransport cycle under sustained low-traffic HTTP. Recommended starting point if heap growth is observed: 60000. | 0 (disabled) |
LOGS_DIR | Directory for log files (Node.js only). Relative paths resolve against the application root. | <app-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory |
NCBI_API_KEY | NCBI API key for higher rate limits (10 req/s vs 3 req/s) | none |
NCBI_ADMIN_EMAIL | Contact email sent with NCBI requests (recommended by NCBI) | none |
NCBI_REQUEST_DELAY_MS | Minimum gap between NCBI request starts in ms | 400 (100 with key) |
NCBI_MAX_CONCURRENT | Max concurrent in-flight NCBI requests | 8 |
NCBI_MAX_RETRIES | Retry attempts for failed NCBI requests | 6 |
NCBI_TIMEOUT_MS | Per-request HTTP timeout in ms | 30000 |
NCBI_TOTAL_DEADLINE_MS | Total deadline for one NCBI call — queue wait, retry attempts, and backoff — in ms. A call the queue cannot start before it is rejected at once | 60000 |
UNPAYWALL_EMAIL | Contact email for Unpaywall. When set, pubmed_fetch_fulltext falls back to Unpaywall open-access copies for non-PMC DOIs | none |
UNPAYWALL_TIMEOUT_MS | Per-request HTTP timeout for Unpaywall lookups and content fetches, in ms | 20000 |
EUROPEPMC_ENABLED | Enable Europe PMC search tool and the pubmed_fetch_fulltext JATS fallback chain. Set false to disable all EPMC calls and skip tool registration. | true |
EUROPEPMC_EMAIL | Optional contact email sent with Europe PMC requests (EBI courtesy). | none |
EUROPEPMC_REQUEST_DELAY_MS | Minimum gap between Europe PMC request starts in ms | 200 |
EUROPEPMC_MAX_RETRIES | Retry attempts for failed Europe PMC requests | 3 |
EUROPEPMC_TIMEOUT_MS | Per-request HTTP timeout for Europe PMC calls, in ms | 20000 |
OTEL_ENABLED | Enable OpenTelemetry | false |
Build and run the production version:
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
| Directory | Purpose |
|---|---|
src/mcp-server/tools | Tool definitions (*.tool.ts). Eleven tools across PubMed, PMC, and Europe PMC. |
src/mcp-server/resources | Resource definitions. Database info resource. |
src/mcp-server/prompts | Prompt definitions. Research plan prompt. |
src/services/ncbi | NCBI E-utilities service layer — API client, queue, parser, formatter. |
src/services/europe-pmc | Europe PMC service — search + fullTextXML JATS retrieval. Reuses the NCBI JATS parser. |
src/services/unpaywall | Unpaywall service — DOI → OA location resolution and content fetch (HTML/PDF). |
src/config | Server-specific environment variable parsing and validation with Zod. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagecreateApp() arraysIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/pubmed-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-pubmed-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/pubmed-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/pubmed-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.