@cyanheads/crossref-mcp-server
Resolve DOIs, search ~155M scholarly works, fetch references, and look up publishers via the Crossref REST API. STDIO or Streamable HTTP.
Tools
Seven tools for working with Crossref data — DOI resolution, full-text search across all scholarly works, outgoing reference lists, and journal, funder, and publisher lookup:
| Tool | Description |
|---|---|
crossref_get_work | Resolve a DOI to its full Crossref metadata record: title, authors, affiliations, abstract (when deposited), journal, publication date, type, license, full-text links, funder acknowledgements, and outgoing reference count |
crossref_search_works | Search the Crossref works index by free text and/or structured filters. Supports sort, field selection, and cursor-based deep paging. |
crossref_get_references | Return the outgoing reference list for a DOI — the works cited by this paper, with raw citation strings and resolved DOIs where available |
crossref_search_journals | Find Crossref journal records by ISSN or title query; optionally retrieve a page of the journal's most recent works by publication date. Both lists page by offset. |
crossref_search_funders | Find funders registered in the Crossref Funder Registry by name, bare registry ID, or funder DOI; optionally retrieve a page of funded works. Both lists page by offset. |
crossref_get_member | Resolve a Crossref member ID to its publisher record — name, owned DOI prefixes, DOI counts, per-work-type breakdown, and per-category metadata deposit coverage |
crossref_get_prefix | Resolve a DOI prefix (e.g. 10.1038) to its owning publisher — name and member ID, chaining into crossref_get_member |
crossref_get_work
Resolve a DOI to its canonical Crossref record.
- DOI validated against
10.NNNN/suffixregex before the upstream call - Returns title, authors with affiliations, abstract (when deposited), container/journal, publication date, work type, ISSN, license URLs, full-text link URLs, and funder acknowledgements
- Outgoing references are reported as a count; the entries themselves come from
crossref_get_references - Incoming citation count (
is-referenced-by-count) is included; citing works are not — Crossref does not expose that data. Use OpenAlex for citation graphs.
crossref_search_works
Search across ~155M Crossref-registered works.
- Free-text
queryplus a structuredfilterobject using Crossref's hyphen-separated key syntax:from-pub-date,until-pub-date,type,funder,issn,member,has-abstract,has-references,has-full-text,directory(useDOAJto restrict to open-access content) - Field-specific query parameters scope matching beyond the generic
query:queryTitle,queryAuthor,queryContainerTitle(journal/book name), andqueryBibliographic(whole-citation match to resolve a known reference to its DOI) — all combine with each other and withquery - Sort by
relevance,is-referenced-by-count,published,deposited, orscore fieldsparameter narrows response payload — useful for large result sets. Names are case-sensitive;DOIis always returned whether or not it is listed, so every result stays resolvable bycrossref_get_work.- Offset paging up to ~10K results; deep paging requires
cursor=*on the first call, then pass the returnednextCursortoken. Cursor and offset cannot be combined. - A cursor walk ends on the page that omits
nextCursor. Crossref keeps minting a token past the end of a list, so the token is withheld on an empty page rather than relayed — the rule theworks_cursorwalks below follow too. Here that page also carries anoticesaying the walk is complete, becauseworksis this tool's whole payload and an empty page nothing is said about renders as blank text.
crossref_get_references
Fetch the outgoing reference list for a DOI.
- Each reference includes its raw citation string and, where Crossref has resolved it, a DOI for follow-up lookup
- Paged with
offsetandlimit(default 100, max 500).referenceCountis the full deposited total; when more remain, the response carries anextOffsetto pass back asoffset. Most works fit in a single page — bibliography records can carry tens of thousands of references. - Coverage varies by publisher — pre-2000 literature and non-participating publishers may have no reference list
- Single-hop only; agents that need N-hop traversal chain calls explicitly
crossref_search_journals
Find journal records by ISSN or title.
include_works: truealso returns a page of the journal's most recent works by publication date- Returns journal title, publisher, ISSN-L, subject areas, and total DOI count
- Title-query results page with
offset;journalsTotalreports the full match count andnextOffsetcarries the input for the following page. The journal works list pages separately withworks_offsetandnextWorksOffset. - The two lists have different ceilings: title search allows
offset + rowsup to 100,000, the works list only 10,000. A page that stops at either ceiling carries anoticesaying so — a missing continuation offset would otherwise read as the end of the list. - The journal works list also pages by cursor, which has no ceiling: pass
works_cursor="*"and chain thenextWorksCursortoken from each response to read the whole list. A cursor walk starts at the newest work and cannot resume from an offset, and the two cannot be combined —works_cursorwith a nonzeroworks_offsetreturnsworks_cursor_offset_conflict. Each token runs about 1500 characters on both result surfaces, a cost per page rather than per record, so a long walk is cheaper at a highrows. include_worksneeds an unambiguous journal. A title query matching more than one — measured by the upstream match count, not by how many fit on the requested page — returnsambiguous_journal, naming the page's candidates and their ISSNs in the message and incandidateson the error data, alongside the full match count. Pass one back asissn, or narrow the query when the journal you want is not among them.
crossref_search_funders
Find funders in the Crossref Funder Registry.
- Accepts a name query, a bare registry ID (
100000001), or a full funder DOI (10.13039/100000001, optionally behind adoi:orhttps://doi.org/prefix) include_works: truealso returns a page of works funded by the matched funder- Returns funder name, registry ID, country, and alternate names
- Name-query results page with
offset;fundersTotalreports the full match count andnextOffsetcarries the input for the following page. The funded works list pages separately withworks_offsetandnextWorksOffset. - The two lists have different ceilings: name search allows
offset + rowsup to 100,000, the works list only 10,000. A page that stops at either ceiling carries anoticesaying so — a missing continuation offset would otherwise read as the end of the list. - The funded works list also pages by cursor, which has no ceiling: pass
works_cursor="*"and chain thenextWorksCursortoken from each response to read the whole list. A cursor walk starts at the newest work and cannot resume from an offset, and the two cannot be combined —works_cursorwith a nonzeroworks_offsetreturnsworks_cursor_offset_conflict. Each token runs about 1500 characters on both result surfaces, a cost per page rather than per record, so a long walk is cheaper at a highrows. This list counts works funded by the funder's registry descendants, which acrossref_search_worksfilter on{"funder": "10.13039/<id>"}does not. include_worksneeds an unambiguous funder. A name query matching more than one — measured by the upstream match count, not by how many fit on the requested page — returnsambiguous_funderrather than resolving one silently, naming the page's candidates and their registry IDs in the message and incandidateson the error data, alongside the full match count. Pass one back asfunder_doi, or narrow the query when the funder you want is not among them.
crossref_get_member
Resolve a Crossref member ID to its publisher/organization record.
- 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_prefixto resolve a DOI prefix to the member ID first
crossref_get_prefix
Resolve a DOI prefix to its owning publisher.
- 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_memberfor 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
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool definitions — single file per tool, framework handles registration and validation
- Unified error handling across all tools
- Pluggable auth (
none,jwt,oauth) - Swappable storage backends:
in-memory,filesystem,Supabase,Cloudflare KV/R2/D1 - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
Crossref-specific:
- Polite-pool
User-Agentheader injected on every request — priority access granted viaCROSSREF_MAILTOemail address, no API token required - Retry with exponential backoff on 429 (honoring
Retry-After), 5xx, HTTP 408/504, and network failures. Two failures are not retried: a malformed response body, which an identical request re-serializes, and a request that hitsCROSSREF_TIMEOUT_MS, where every attempt costs the full deadline - Upstream failures arrive classified and with recovery guidance on both result surfaces: rate limit, service unavailable, timeout, and malformed response each say what to do next in
content[]as well as instructuredContent - Cursor-based deep paging on the works search and on both works sub-resources, for result sets beyond the offset cap
- Filter key validation: Crossref uses hyphens (
has-abstract,has-references,from-pub-date); the server enforces correct syntax and surfaces API validation errors with actionable recovery hints
Getting started
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.
{
"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):
{
"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:
{
"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:
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.3.14 or higher (or Node.js v24+).
- An email address for
CROSSREF_MAILTOis optional but recommended — Crossref's polite pool grants priority access to clients that identify themselves. No account or token is required.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/crossref-mcp-server.git
- Navigate into the directory:
cd crossref-mcp-server
- Install dependencies:
bun install
- Configure environment:
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.
| Variable | Description | Default |
|---|---|---|
CROSSREF_MAILTO | Email address embedded in the polite-pool User-Agent header. Optional — server starts without it but logs a warning and uses the anonymous pool with stricter rate limits. | — |
CROSSREF_BASE_URL | Crossref API base URL. Override for testing against a local proxy. | https://api.crossref.org |
CROSSREF_TIMEOUT_MS | Per-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_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Seven tools for Crossref data access. |
src/services/crossref | CrossrefService — 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/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor 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 and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.