Skip to main content
Glama
pubmed-mcp-server

pubmed-mcp-server

by cyanheads
README.md
<div align="center">
  <h1>@cyanheads/pubmed-mcp-server</h1>
  <p><b>Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.</b>
  <div>12 Tools • 1 Resource • 1 Prompt</div>
  </p>
</div>

<div align="center">

[![Version](https://img.shields.io/badge/Version-2.12.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/pubmed-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pubmed-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pubmed-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)

</div>

<div align="center">

[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/pubmed-mcp-server/releases/latest/download/pubmed-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=pubmed-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvcHVibWVkLW1jcC1zZXJ2ZXIiXX0=) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22pubmed-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fpubmed-mcp-server%22%5D%7D)

[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)

</div>

<div align="center">

**Public Hosted Server:** [https://pubmed.caseyjhand.com/mcp](https://pubmed.caseyjhand.com/mcp)

</div>

---

## 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

| Tool | Description |
|:---|:---|
| `pubmed_search_articles` | Search PubMed with full query syntax, structured filters, date ranges, and optional summaries |
| `pubmed_fetch_articles` | Fetch article metadata by PMID: abstract, authors, journal, MeSH terms, databank accessions such as trial registration IDs, grants, a collective author's investigators on request, linked retraction and correction notices |
| `pubmed_fetch_fulltext` | Fetch full text by PMID, PMCID, or DOI, falling back from PMC to Europe PMC to Unpaywall |
| `pubmed_europepmc_search` | Search Europe PMC for preprints, patents, Agricola, and open-access records PubMed doesn't carry |
| `pubmed_europepmc_fetch` | Fetch complete Europe PMC records, including the untruncated abstract, as `records: [{ source, epmcId }]` |
| `pubmed_fetch_citation_metrics` | Fetch NIH iCite citation metrics by PMID: Relative Citation Ratio, NIH percentile, citation counts and rates |
| `pubmed_format_citations` | Format citations in APA 7th, MLA 9th, BibTeX, RIS, or Vancouver (ICMJE/NLM), with linked retraction and correction notices |
| `pubmed_find_related` | Find similar articles, citing articles, or references for a PMID |
| `pubmed_spell_check` | Correct a misspelled PubMed query via NCBI ESpell |
| `pubmed_lookup_mesh` | Look up MeSH records (descriptors, supplementary concepts, qualifiers, pharmacological actions) with the PubMed clause that searches each, tree numbers, scope notes, and entry terms |
| `pubmed_lookup_citation` | Resolve partial references, up to 25 at a time, to PMIDs via ECitMatch |
| `pubmed_convert_ids` | Convert between DOI, PMID, and PMCID via the PMC ID Converter |

### Resources

| Resource | Description |
|:---|:---|
| `pubmed://database/info` | PubMed database metadata via EInfo (field list, record count, last update) |

### Prompts

| Prompt | Description |
|:---|:---|
| `research_plan` | Generate a structured four-phase biomedical research plan |

## Capability reference

### `pubmed_search_articles` <sub>tool</sub>

- 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; a page that reaches PubMed's 9,999-record window says how many matches no `offset` can reach and advises narrowing or splitting the query by `dateRange`
- 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` <sub>tool</sub>

- 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` <sub>tool</sub>

- `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` <sub>tool</sub>

- 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` <sub>tool</sub>

- 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` <sub>tool</sub>

- 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](https://icite.od.nih.gov/)

---

### `pubmed_format_citations` <sub>tool</sub>

- 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` <sub>tool</sub>

- `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
- Rows carry the same identifiers as search summaries: `doi`, `pmcId` and `pmcUrl` when the record has them, and a `pubmedUrl` on every row
- Falls back to Europe PMC, then OpenAlex, and names the provider that answered; fails as `all_providers_failed` if none can

---

### `pubmed_spell_check` <sub>tool</sub>

- 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` <sub>tool</sub>

- MeSH records of every type (descriptors, supplementary concepts, qualifiers, pharmacological actions) by name or free-text term, with the exact heading match pinned first: the record the query names, a descriptor before a qualifier or pharmacological-action record of the same name, or the heading an entry term maps to; 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, their `recordType`, and a `searchTerm`, the PubMed clause that searches them (`"Name"[mh]`, `[nm]` for a supplementary concept, `[pa]`, `[pt]`, `[sh]`); a drug-class heading returns its descriptor and its pharmacological-action record as two records under one name and ID, told apart by `recordType`
- `includeDetails` (default on) adds tree numbers, scope notes, entry terms, `yearIntroduced`, and `previousIndexing`; the exact heading match on a query's first page also gets its allowable `subheadings`

---

### `pubmed_lookup_citation` <sub>tool</sub>

- 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` <sub>tool</sub>

- 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` <sub>resource</sub>

- 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` <sub>prompt</sub>

- 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`](https://github.com/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, one deadline covering queue wait and retries, and fail-fast for an endpoint whose retries were just spent on an outage: later calls to it make one attempt each until one succeeds
- 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:

```sh
claude mcp add --transport http pubmed-mcp-server https://pubmed.caseyjhand.com/mcp
```

Or run it locally:

```sh
claude mcp add pubmed-mcp-server -- npx -y @cyanheads/pubmed-mcp-server
```

For other clients, add this to the MCP config file:

```json
{
  "mcpServers": {
    "pubmed-mcp-server": {
      "command": "npx",
      "args": ["-y", "@cyanheads/pubmed-mcp-server"]
    }
  }
}
```

An [NCBI API key](https://www.ncbi.nlm.nih.gov/account/settings/) 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](#running-the-server).

## Configuration

| Variable | Description | Default |
|:---|:---|:---|
| `NCBI_API_KEY` | NCBI API key; raises the rate limit from 3 to 10 req/s. NCBI counts the limit per key (per IP address without one), so processes sharing a key or an address share it. | none |
| `NCBI_ADMIN_EMAIL` | Contact email sent with NCBI requests, as NCBI recommends. | none |
| `NCBI_REQUEST_DELAY_MS` | Minimum gap between NCBI request starts, in ms. | `400` (`120` with a key) |
| `NCBI_MAX_CONCURRENT` | Max concurrent in-flight NCBI requests. | `8` |
| `NCBI_MAX_RETRIES` | Retry attempts for failed NCBI requests. An endpoint whose last call failed unreachable on every attempt gets one attempt per call until one succeeds. | `6` |
| `NCBI_TIMEOUT_MS` | Per-request HTTP timeout, in ms. | `30000` |
| `NCBI_TOTAL_DEADLINE_MS` | Deadline for one NCBI call across queue wait, retries, and backoff, in ms. A call that can't start in time is rejected at once. | `60000` |
| `UNPAYWALL_EMAIL` | Contact email for Unpaywall. Setting it enables the Unpaywall tier of `pubmed_fetch_fulltext` for non-PMC DOIs. | none |
| `UNPAYWALL_TIMEOUT_MS` | Per-request timeout for Unpaywall lookups and content fetches, in ms. | `20000` |
| `EUROPEPMC_ENABLED` | Set `false` to disable every Europe PMC call: both Europe PMC tools go unregistered, `pubmed_fetch_fulltext` skips that tier, and `pubmed_find_related` skips that fallback. | `true` |
| `EUROPEPMC_EMAIL` | Optional contact email sent with Europe PMC requests. | 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 timeout for Europe PMC calls, in ms. | `20000` |
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `stateless` |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
| `LOGS_DIR` | Directory for log files (Node.js only). | `<app-root>/logs` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |

See [`.env.example`](./.env.example) for every server setting and the common framework overrides.

## Running the server

### Local development

Requires [Bun v1.4.4](https://bun.sh/) or higher (or Node.js v24+).

- **Clone and install**:

  ```sh
  git clone https://github.com/cyanheads/pubmed-mcp-server.git
  cd pubmed-mcp-server && bun install
  cp .env.example .env   # set NCBI_API_KEY, NCBI_ADMIN_EMAIL, and UNPAYWALL_EMAIL as needed
  ```

- **Build and run the production version**:

  ```sh
  # One-time build
  bun run rebuild

  # Run the built server
  bun run start:http
  # or
  bun run start:stdio
  ```

- **Run checks and tests**:
  ```sh
  bun run devcheck  # Lints, formats, type-checks, and more
  bun run test      # Runs the test suite
  ```

### Docker

```sh
docker run --rm -p 3010:3010 ghcr.io/cyanheads/pubmed-mcp-server:latest
```

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

| Directory | Purpose |
|:---|:---|
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Twelve tools across PubMed, PMC, Europe PMC, and NIH iCite. |
| `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/services/openalex` | OpenAlex service — last-resort provider for `pubmed_find_related`. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |
| `skills/` | The published `pubmed-guide` agent skill, served over MCP and shipped in the package, the `.mcpb` bundle, and the Docker image. |

## Development guide

See [`CLAUDE.md`](./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:

```sh
bun run devcheck
bun run test
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.

TDQS

A4.1/5.0

Scored across 12 tools

Disambiguation4/5

Each tool targets a distinct capability: keyword search, metadata fetch, full-text fetch, citation formatting, related-article discovery, MeSH lookup, citation-based ID lookup, ID conversion, spell check, and citation metrics. The main soft spots are pubmed_search_articles vs pubmed_europepmc_search and pubmed_fetch_articles vs pubmed_europepmc_fetch, but the descriptions explicitly carve out the corpus differences (preprints/patents, records with no PMID) so an agent can choose correctly.

Naming Consistency4/5

Nearly everything follows the pubmed_verb_noun pattern (pubmed_search_articles, pubmed_fetch_articles, pubmed_fetch_fulltext, pubmed_lookup_mesh, pubmed_format_citations, pubmed_convert_ids). The two Europe PMC tools invert the order (pubmed_europepmc_fetch, pubmed_europepmc_search), placing the noun before the verb, which is a minor but real deviation.

Tool Count5/5

12 tools is squarely in the well-scoped range for a biomedical literature server, and each one maps to a distinct research workflow step (discover, retrieve, enrich, cite, convert). No filler tools and no obviously missing companion operation.

Completeness5/5

The surface covers the full literature lifecycle: search (PubMed and Europe PMC), metadata and full-text retrieval, citation formatting, related-work traversal, MeSH vocabulary, citation-based ID resolution, ID conversion, spell correction, and iCite metrics. Edge cases like non-PMID preprint/patent records are explicitly handled via the Europe PMC path.

Maintenance

ActivityActive
ResponsivenessUsually handled