
Resolve DOIs, search ~155M scholarly works, and fetch references via the Crossref REST API.
Resolve DOIs, search ~155M scholarly works, fetch references, and look up publishers via the Crossref REST API. STDIO or Streamable HTTP.
Public Hosted Server: https://crossref.caseyjhand.com/mcp
Scholarly metadata from the Crossref REST API. Resolve DOIs to full metadata records, search across ~155 million scholarly works by free text or structured filters, fetch outgoing reference lists, and look up journals, funders, and publishers. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
crossref_get_work | Resolve 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_works | Search 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_references | Return the outgoing reference list for a DOI — the works cited by this paper, with 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 |
crossref_search_funders | Find funders in the Crossref Funder Registry by name, registry ID, or funder DOI; optionally retrieve a page of funded works |
crossref_get_member | Resolve a Crossref member ID to its publisher record — name, owned DOI prefixes, DOI counts, and per-category metadata deposit coverage |
crossref_get_prefix | Resolve a DOI prefix (e.g. 10.1038) to its owning publisher, chaining into crossref_get_member |
crossref_get_work tool10.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 lookupvolume, 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 absenteditors in the author entry shape, returned whole and never counted in authorCountoffset/limit (default 25, max 500); authorCount reports the full deposited total and a nextOffset continues when authors remain — every other field is returned in full on every pageupdatedBy 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 updatedrelations 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 toolror in place of name, never as a blank entrypublished, 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 itreferencesCount) — entries come from crossref_get_references; incoming citation count (isReferencedByCount) is included, but citing works are not exposed by Crossref — use OpenAlex for citation graphscrossref_search_works toolquery plus a structured filter object using Crossref's hyphenated keys (from-pub-date, type, funder, issn, has-abstract, license.url, etc.)queryTitle, queryAuthor, queryContainerTitle, and queryBibliographic combine with query and with each otherrelevance, 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 cursorfields narrows the payload to any of the 17 select names the summary projects — including the citation locators volume, issue, page, article-number, and ISSN — and refuses any other name; DOI is always returned, and crossref_get_work carries the rest of the recordsort or order, is read as omitted; when that leaves nothing to search by, the notice says the page is an unfiltered listingauthorLimit caps authors per work (default 25, max 500); authorCount reports the full deposited total — chain a cut work's DOI into crossref_get_work for the restcursor="*" starts deep paging via chained nextCursor tokens — cursor and offset cannot be combinednextCursor — an empty page never carries one; every empty page's notice names which of the three causes appliescrossref_get_references toolcrossref_get_work acceptscrossref_get_workoffset/limit (default 100, max 500); referenceCount is the full deposited total and nextOffset continues when more remaincrossref_search_journals toolinclude_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 ISSNsoffset (ceiling offset + rows ≤ 100,000); the works list pages separately by works_offset (ceiling ≤ 10,000) — a page that stalls at either ceiling, or an offset past the end of either list, carries a notice naming itworks_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)include_works is skipped with a notice rather than returning an empty listcrossref_search_funders toolquery, 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 IDsoffset (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 itcrossref_search_works filter on {"funder": "10.13039/<id>"} does notnotice names the superseding ID via replacedBy; the replacement is never followed automaticallycrossref_get_member toolcrossref_get_prefix to resolve a DOI prefix to the member ID firstcrossref_get_prefix tool10.1038, no /suffix)crossref_get_member for the full recordBuilt 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:
User-Agent header injected on every request — priority access via CROSSREF_MAILTO, keyless otherwise; no API token requiredRetry-After), 5xx, HTTP 408/504, and network failures; a malformed response body and a request that hits CROSSREF_TIMEOUT_MS are not retriedunknown_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 bodyx_i, A^{−1}, √(m), (a+b)/cAgent-friendly output:
doi, issn, and date fields without re-verificationnotice 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 nextWorksCursornextCursor, 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 noneambiguous_journal and ambiguous_funder list every candidate and its identifier in the error data rather than silently resolving to the first matchA public instance is available at https://crossref.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"crossref-mcp-server": {
"type": "streamable-http",
"url": "https://crossref.caseyjhand.com/mcp"
}
}
}
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
CROSSREF_MAILTO is optional but recommended — Crossref's polite pool grants priority access to clients that identify themselves. No account or token is required.git clone https://github.com/cyanheads/crossref-mcp-server.git
cd crossref-mcp-server
bun install
cp .env.example .env
# edit .env and optionally set CROSSREF_MAILTO for polite-pool access
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_SESSION_MODE | HTTP 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_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.
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
| 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). |
src/services/crossref | CrossrefService — HTTP client, polite-pool header, retry, pagination helpers. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/tools/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.