MCP server: One MCP server.

crossref-mcp-server

@cyanheadsMCP server · External

Resolve DOIs, search ~188M scholarly works, and fetch references via the Crossref REST API.

Academic papersv0.4.4TypeScript
Freeno charge per call

One address for any MCP client. No sign-in needed on this server.

What it does

7 read

Grouped by tool name. At most: only reads or looks up information.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Tools

7 · grouped by tool name · checked 20 Oct 2026

Read · 7Look things up

  • crossref_get_work
  • crossref_get_references
  • crossref_search_works
  • crossref_search_journals
  • crossref_search_funders
  • crossref_get_member
  • crossref_get_prefix
Try it in the Inspector
README16 min read

@cyanheads/crossref-mcp-server

Resolve DOIs, search ~188M scholarly works, fetch references, and look up publishers via the Crossref REST API. STDIO or Streamable HTTP.

7 Tools

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

Public Hosted Server: https://crossref.caseyjhand.com/mcp


Overview

Scholarly metadata from the Crossref REST API. Resolve DOIs to full metadata records, search across ~188 million scholarly works by free text or structured filters, fetch outgoing reference lists, and look up journals, funders, and publishers.

Tools

ToolDescription
crossref_get_workResolve a DOI to its full Crossref metadata record: title, authors, editors, affiliations, abstract, journal with volume/issue/pages, ISSNs and ISBNs, publication date, license, full-text links, funder acknowledgements, post-publication updates, and related identifiers
crossref_search_worksSearch the Crossref works index by free text and/or structured filters, with field-scoped query parameters, sort, field selection, and offset or cursor-based paging
crossref_get_referencesReturn the outgoing reference list for a DOI — the works cited by this paper, with citation strings and resolved DOIs where available
crossref_search_journalsFind Crossref journal records by ISSN or title query; optionally retrieve a page of the journal's most recent works
crossref_search_fundersFind funders in the Crossref Funder Registry by name, registry ID, or funder DOI; optionally retrieve a page of funded works
crossref_get_memberResolve a Crossref member ID to its publisher record — name, owned DOI prefixes, DOI counts, and per-category metadata deposit coverage
crossref_get_prefixResolve a DOI prefix (e.g. 10.1038) to its owning publisher, chaining into crossref_get_member

Capability reference

crossref_get_work tool

  • DOI validated against the 10.NNNN/suffix regex before the upstream call, accepted either bare or wrapped in its resolver (https://doi.org/…, https://dx.doi.org/…, doi:…) and unwrapped before the lookup
  • Returns title, authors with affiliations, abstract (when deposited), container/journal, publication date, work type, ISSN, license URLs, full-text link URLs, and funder acknowledgements
  • Citation locators volume, issue, page, and articleNumber under the same names and on the same rendered line as crossref_search_works, plus isbn — each as deposited, omitted when absent
  • editors in the author entry shape, returned whole and never counted in authorCount
  • Author list paged by offset/limit (default 25, up to 500 per call — a larger limit is clamped to 500 with a notice); authorCount reports the full deposited total and a nextOffset continues when authors remain — every other field is returned in full on every page
  • updatedBy lists the corrections, retractions, expressions of concern, and new versions Crossref records against the work — notice DOI, type, source (publisher or retraction-watch, with its record ID), and date, entry for entry — and updateTo the works this record is a notice for; when updatedBy is present, notice names each update type with its sources. An absent updatedBy does not mean the work was never updated
  • relations groups related identifiers (preprint ↔ published version, versions, reviews, supplements) by relation type, identifier type, and asserting party, returned whole; only Crossref-registered DOIs among them resolve through this tool
  • A funder or affiliation asserted only through the ROR registry (no name deposited) carries ror in place of name, never as a blank entry
  • Publication date is the first of published, published-print, published-online, and issued (this tool only) that names a value; a date component Crossref records as unknown is omitted, along with everything less precise below it
  • Outgoing references are reported as a count (referencesCount) — entries come from crossref_get_references; incoming citation count (isReferencedByCount) is included, but citing works are not exposed by Crossref — use OpenAlex for citation graphs
  • A DOI Crossref holds no record for returns doi_not_found naming the agency the DOI registry says holds it (registrationAgency — DataCite, JaLC, mEDRA, …), a Funder Registry pointer when Crossref registered it, or that no agency registered it at all

crossref_search_works tool

  • Free-text query plus a structured filter object using Crossref's hyphenated keys (from-pub-date, type, funder, issn, has-abstract, license.url, etc.)
  • Field-scoped parameters queryTitle, queryAuthor, queryContainerTitle, and queryBibliographic combine with query and with each other
  • Sort by relevance, score, is-referenced-by-count, published, deposited, or other listed fields — the publication-date sorts page by offset only, since Crossref refuses them with a cursor. A cursor walk with a query term and no sort runs by relevance (sent as sort=score, without order) and can repeat or skip a record at a page boundary; sort: "created" walks without repeats. A filter-only walk runs in Crossref registration order
  • fields narrows the payload to any of the 19 select names the summary projects — including the citation locators volume, issue, page, article-number, and ISSN, and the update links updated-by and update-to — and refuses any other name; DOI is always returned, and crossref_get_work carries the rest of the record
  • Each work carries updatedBy (corrections, retractions, expressions of concern, new versions recorded against it) and updateTo (the works it is a notice for) in the entry shape and rendered line crossref_get_work uses, so a page of results can be screened for retractions without a call per DOI
  • A blank or whitespace-only query term or filter value, and an empty sort or order, is read as omitted; when that leaves nothing to search by, the notice says the page is an unfiltered listing
  • rows returns up to 100 works per call and authorLimit caps authors per work (default 25, up to 500); a larger value of either is clamped to its cap with a notice, which on an offset page also names the offset that continues after the records returned (and, near the ~10K ceiling, the smaller rows that still fits beside it); authorCount reports the full deposited total — chain a cut work's DOI into crossref_get_work for the rest
  • Offset paging is capped at offset + rows ≤ 10,000; a rejected offset below the cap names the rows that still fit, and cursor="*" starts deep paging via chained nextCursor tokens — cursor and offset cannot be combined
  • A cursor walk ends on the page that omits nextCursor — an empty page never carries one; every empty page's notice names which of the three causes applies

crossref_get_references tool

  • Takes the citing work's DOI bare or resolver-wrapped, the same forms crossref_get_work accepts
  • Each reference carries its deposited citation string and, when Crossref has resolved it, a DOI for crossref_get_work, with doiAssertedBy saying whether the publisher deposited that DOI or Crossref matched it
  • Structured parts as deposited: author, year, article title, journal title, the title of a cited book or volume (volumeTitle, which stands in as the title when there is no article title), series title, volume, issue, first page, edition, and ISSN
  • Citation strings have formatting markup stripped and character references decoded; a bracketed span that isn't a recognized tag (a cited URL, a Miller index, a DOI fragment) is left exactly as deposited
  • Paged by offset/limit (default 100, up to 500 per call — a larger limit is clamped to 500 with a notice); referenceCount is the full deposited total and nextOffset continues when more remain
  • Coverage varies by publisher — pre-2000 works and non-participating publishers often have no indexed references
  • Single-hop only; incoming citations are not available through Crossref — use OpenAlex for citation graphs
  • A DOI Crossref holds no record for returns doi_not_found naming its registration agency (registrationAgency), or saying no agency registered it, the same way crossref_get_work does

crossref_search_journals tool

  • include_works: true also returns a page of the journal's most recent works — newest published first by works_offset, newest registered first on a works_cursor walk; requires an unambiguous journal — a title query matching more than one returns ambiguous_journal, naming candidates and ISSNs
  • Returns journal title, publisher, ISSNs with their print/electronic form, subject areas, DOI counts (total/current/backfile), and per-category metadata deposit coverage (abstracts, references, ORCIDs, funders, licenses, and more) as current/backfile fractions — the same breakdown crossref_get_member gives per publisher
  • An issn lookup also returns DOI counts by issued year, ascending; title-query results omit them
  • Title-query results page by offset (ceiling offset + rows ≤ 100,000); the works list pages separately by works_offset (ceiling ≤ 10,000) — an offset past the end of either list carries a notice naming it, and so does a page that stalls at either ceiling, along with the offset and smaller rows that reach the last records below it
  • works_cursor="*" pages the works list with no ceiling via chained nextWorksCursor tokens; a cursor walk runs by Crossref registration date, newest first (Crossref does not walk a publication-date sort by cursor), and cannot combine with works_offset > 0 (works_cursor_offset_conflict)
  • A matched journal with no ISSN registered has no addressable works list — include_works is skipped with a notice rather than returning an empty list

crossref_search_funders tool

  • Accepts a name query, a bare registry ID (100000001), or a full funder DOI (10.13039/100000001, optionally behind a doi:/https://doi.org/ prefix)
  • include_works: true also returns a page of funded works; requires an unambiguous funder — a name query matching more than one returns ambiguous_funder, naming candidates and registry IDs
  • Returns funder name, registry ID, country, and alternate names
  • Name-query results page by offset (ceiling ≤ 100,000); the funded-works list pages separately by works_offset (ceiling ≤ 10,000) or, with no ceiling, works_cursor="*" chaining nextWorksCursor — offset pages run newest published first, a cursor walk newest registered first, and the two cannot combine (works_offset > 0); an offset past the end of either list carries a notice naming it, and a page that stalls at either ceiling names the offset and smaller rows that reach the last records below it
  • The funded-works list also counts works funded by the funder's registry descendants, which a crossref_search_works filter on {"funder": "10.13039/<id>"} does not
  • A deprecated registry entry answers to its successor's name while counting only its own works — the response's notice names the superseding ID via replacedBy; the replacement is never followed automatically

crossref_get_member tool

  • Members are the organizations that register DOIs — this answers "what does this publisher publish, and how completely do they deposit metadata?"
  • Returns primary name, alternate imprint names, owned DOI prefixes, DOI counts (total/current/backfile), a per-work-type breakdown, and per-category metadata deposit coverage (references, abstracts, ORCIDs, funders, licenses, and more) as current/backfile fractions
  • Pair with crossref_get_prefix to resolve a DOI prefix to the member ID first

crossref_get_prefix tool

  • Accepts the registrant prefix of a DOI (e.g. 10.1038, no /suffix)
  • Returns the publisher name and numeric member ID — the ID chains directly into crossref_get_member for the full record
  • The Crossref prefix record is thin by design (owner name and member link only); richer publisher data lives on the member record
  • A prefix with no Crossref member returns prefix_not_found naming the agency that registered it (registrationAgency, e.g. DataCite for 10.5281), or saying no agency registered it

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.

Crossref-specific:

  • Polite-pool User-Agent header injected on every Crossref request — priority access via CROSSREF_MAILTO, keyless otherwise; no API token required
  • Retry with exponential backoff on 429 (honoring Retry-After), 5xx, HTTP 408/504, and network failures; a malformed response body and a request that hits CROSSREF_TIMEOUT_MS are not retried
  • Cursor-based deep paging on the works search and on both works sub-resources, for result sets beyond the offset cap
  • Crossref's rejections of a request come back as declared reasons — unknown_filter (with the hyphenated key when Crossref lists one), invalid_parameter, sort_cursor_conflict, invalid_cursor — carrying the rejected inputs and a recovery hint; a malformed issn filter value is refused before the request, and no upstream error relays Crossref's raw response body
  • Text normalization on every human-readable value: HTML character references decoded and whitespace collapsed; citation strings additionally have formatting markup stripped, so titles and abstracts read as plain text instead of raw JATS XML; a MathML formula keeps its structure — the TeX annotation where the deposit carries one, otherwise a linear form such as x_i, A^{−1}, √(m), (a+b)/c

Agent-friendly output:

  • Provenance — identifiers, URLs, and dates are returned byte-exact while human-readable text is normalized, so a caller can trust doi, issn, and date fields without re-verification
  • Graceful partial failure — an empty page is a success, not an error: an offset past the end of a list, or a query nothing matched, carries a notice saying which, and so does the page that ends a crossref_search_works cursor walk; a journal or funder works walk ends on the page that omits nextWorksCursor
  • Discriminated output contracts — the nextCursor, nextOffset, nextWorksOffset, and nextWorksCursor continuation fields are withheld once a list is exhausted, and a page that stops at an offset ceiling says so in its notice; a cursor token can still ride the last partial page, and the empty page after it carries none
  • Ambiguity handled explicitly — ambiguous_journal and ambiguous_funder list every candidate and its identifier in the error data rather than silently resolving to the first match

Getting started

Public Hosted Instance

A public instance is available at https://crossref.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:

json
{
  "mcpServers": {
    "crossref-mcp-server": {
      "type": "streamable-http",
      "url": "https://crossref.caseyjhand.com/mcp"
    }
  }
}

Self-Hosted / Local

Add the following to your MCP client configuration file. CROSSREF_MAILTO is optional but recommended — without it the server uses Crossref's anonymous pool with stricter rate limits.

json
{
  "mcpServers": {
    "crossref-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["@cyanheads/crossref-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "CROSSREF_MAILTO": "your-email@example.com"
      }
    }
  }
}

Or with npx (no Bun required):

json
{
  "mcpServers": {
    "crossref-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@cyanheads/crossref-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "CROSSREF_MAILTO": "your-email@example.com"
      }
    }
  }
}

Or with Docker:

json
{
  "mcpServers": {
    "crossref-mcp-server": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "MCP_TRANSPORT_TYPE=stdio",
        "-e", "CROSSREF_MAILTO=your-email@example.com",
        "ghcr.io/cyanheads/crossref-mcp-server:latest"
      ]
    }
  }
}

For Streamable HTTP, set the transport and start the server:

sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 CROSSREF_MAILTO=your-email@example.com bun run start:http
# Server listens at http://localhost:3010/mcp

Prerequisites

  • Bun v1.4.0 or higher (or Node.js v24+).
  • An email address for CROSSREF_MAILTO is optional but recommended — Crossref's polite pool grants priority access to clients that identify themselves. No account or token is required.

Installation

  1. Clone the repository:
sh
git clone https://github.com/cyanheads/crossref-mcp-server.git
  1. Navigate into the directory:
sh
cd crossref-mcp-server
  1. Install dependencies:
sh
bun install
  1. Configure environment:
sh
cp .env.example .env
# edit .env and optionally set CROSSREF_MAILTO for polite-pool access

Configuration

All configuration is validated at startup via Zod schemas in src/config/server-config.ts.

VariableDescriptionDefault
CROSSREF_MAILTOEmail address embedded in the polite-pool User-Agent header of every Crossref request. Optional — server starts without it but logs a warning and uses the anonymous pool with stricter rate limits. Never sent to the DOI registry: the agency lookup after a Crossref not-found identifies the server and version only.—
CROSSREF_BASE_URLCrossref API base URL. Override for testing against a local proxy. The agency lookup after a Crossref not-found always goes to https://doi.org, whatever this is set to.https://api.crossref.org
CROSSREF_TIMEOUT_MSPer-request timeout in milliseconds. Also the worst-case wait against an unresponsive upstream — a request that hits the deadline is not retried.10000
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_SESSION_MODEHTTP session mode: auto, stateful, or stateless. This server needs no multi-round input, so src/index.ts declares stateless; Docker and .env.example pin the same value, and setting this variable overrides the declaration.stateless (declared in src/)
MCP_HTTP_PORTPort for the HTTP server.3010
MCP_AUTH_MODEAuth mode: none, jwt, or oauth.none
MCP_LOG_LEVELLog level (RFC 5424).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs
LOG_TOOL_FAILURE_PAYLOADSLog each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted.false
OTEL_ENABLEDEnable OpenTelemetry instrumentation.false

See .env.example for the full list of optional overrides.

Running the server

Local development

  • Build and run:

    sh
    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:stdio
    # or
    bun run start:http
    
  • Run checks and tests:

    sh
    bun run devcheck   # Lint, format, typecheck, security
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    

Project structure

DirectoryPurpose
src/index.tscreateApp() entry point — registers tools and inits services.
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts).
src/services/crossrefCrossrefService — HTTP client, polite-pool header, retry, pagination helpers.
tests/Unit and integration tests mirroring src/.

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 request-scoped logging, ctx.state for tenant-scoped storage
  • Register new tools via the barrel in src/mcp-server/tools/definitions/index.ts
  • Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields (abstracts, reference lists, and affiliations are frequently absent in Crossref records)

Contributing

Issues are welcome. Run checks and tests before submitting:

sh
bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Similar modules

Browse Academic papers