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.
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. The author list pages by offset/limit. |
crossref_search_works | Search the Crossref works index by free text and/or structured filters. Supports sort, field selection, a per-work author cap, and cursor-based deep paging. |
crossref_get_references | Return the outgoing reference list for a DOI — the works cited by this paper, with deposited 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_workResolve a DOI to its canonical Crossref record.
10.NNNN/suffix regex before the upstream calloffset and limit (default 25, max 500). authorCount is the full deposited total; when authors remain, the response carries a nextOffset to pass back as offset. Ordinary records fit in a single page — large-collaboration papers deposit thousands of authors, enough to fill a client's context from one record. Only the author list is paged; every other field comes back in full on every page.ror, and no name. The entry still names an organization instead of coming back as an award number with nothing attached to it.null in place of the number — is omitted rather than reported, so a record with no registered year comes back with no publication date.published, published-print, published-online, and issued that names one. The three search tools read the same order without issued, which is this tool's alone. A source deposited holding only unknown components names nothing and is passed over; one naming a coarser date than the source behind it still answers, since the four are different facts rather than one fact at four precisions.crossref_get_referencesis-referenced-by-count) is included; citing works are not — Crossref does not expose that data. Use OpenAlex for citation graphs.crossref_search_worksSearch across ~155M Crossref-registered works.
query plus a structured filter object 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 (use DOAJ to restrict to open-access content)query: queryTitle, queryAuthor, queryContainerTitle (journal/book name), and queryBibliographic (whole-citation match to resolve a known reference to its DOI) — all combine with each other and with queryrelevance, is-referenced-by-count, published, deposited, or scorefields parameter narrows response payload — useful for large result sets. Names are case-sensitive; DOI is always returned whether or not it is listed, so every result stays resolvable by crossref_get_work.authorLimit authors (default 25, max 500), with authorCount reporting that work's full deposited total. A single page of large-collaboration papers can carry tens of thousands of author entries; pass a cut work's DOI to crossref_get_work to page its whole author list, or raise authorLimit to widen the cap here.cursor=* on the first call, then pass the returned nextCursor token. Cursor and offset cannot be combined.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 the works_cursor walks below follow too. Here that page also carries a notice saying the walk is complete, because works is this tool's whole payload and an empty page nothing is said about renders as blank text.crossref_get_referencesFetch the outgoing reference list for a DOI.
<i>, <em>, <small>, <span>), scripts (<sub>, <sup>, <inf>), block boundaries (<p>, <br>, <refersplit />), MathML and TeX formula wrappers, and a whole JATS <mixed-citation> deposited into a free-text field. A bracket comes out only when it is a well-formed tag whose element name is on a closed list, so an angle-bracket span that is not one — a cited URL, a Miller index, a DOI fragment, a bracketed phrase — is returned exactly as deposited. A link (<a>, <ext-link>, <uri>) is decided against its own text: its tags come out where the text already carries what the href holds, and stay where the href addresses something the text does not name.offset and limit (default 100, max 500). referenceCount is the full deposited total; when more remain, the response carries a nextOffset to pass back as offset. Most works fit in a single page — bibliography records can carry tens of thousands of references.crossref_search_journalsFind journal records by ISSN or title.
include_works: true also returns a page of the journal's most recent works by publication dateoffset; journalsTotal reports the full match count and nextOffset carries the input for the following page. The journal works list pages separately with works_offset and nextWorksOffset.offset + rows up to 100,000, the works list only 10,000. A page that stops at either ceiling carries a notice saying so — a missing continuation offset would otherwise read as the end of the list.works_cursor="*" and chain the nextWorksCursor token 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_cursor with a nonzero works_offset returns works_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 high rows.include_works needs 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 — returns ambiguous_journal, naming the page's candidates and their ISSNs in the message and in candidates on the error data, alongside the full match count. Pass one back as issn, or narrow the query when the journal you want is not among them.include_works is then skipped and the response carries a notice saying so — an absent recentWorks would otherwise read as a journal with no works, and totalDois is the journal's own DOI count rather than an answer about the lookup. There is no alternative identifier to retry with; use crossref_search_works with queryContainerTitle instead.crossref_search_fundersFind funders in the Crossref Funder Registry.
100000001), or a full funder DOI (10.13039/100000001, optionally behind a doi: or https://doi.org/ prefix)include_works: true also returns a page of works funded by the matched funderoffset; fundersTotal reports the full match count and nextOffset carries the input for the following page. The funded works list pages separately with works_offset and nextWorksOffset.offset + rows up to 100,000, the works list only 10,000. A page that stops at either ceiling carries a notice saying so — a missing continuation offset would otherwise read as the end of the list.works_cursor="*" and chain the nextWorksCursor token 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_cursor with a nonzero works_offset returns works_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 high rows. This list counts works funded by the funder's registry descendants, which a crossref_search_works filter on {"funder": "10.13039/<id>"} does not.include_works needs 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 — returns ambiguous_funder rather than resolving one silently, naming the page's candidates and their registry IDs in the message and in candidates on the error data, alongside the full match count. Pass one back as funder_doi, or narrow the query when the funder you want is not among them.replacedBy with the superseding registry ID (and the current record carries replaces), and the response carries a notice naming the successor on both the funder_doi and query paths. The replacement is never followed automatically: re-run with funder_doi set to that ID to get the current entry.crossref_get_memberResolve a Crossref member ID to its publisher/organization record.
crossref_get_prefix to resolve a DOI prefix to the member ID firstcrossref_get_prefixResolve a DOI prefix to its owning publisher.
10.1038, no /suffix)crossref_get_member for the full recordBuilt on @cyanheads/mcp-ts-core:
none, jwt, oauth)in-memory, filesystem, Supabase, Cloudflare KV/R2/D1Crossref-specific:
User-Agent header injected on every request — priority access granted via CROSSREF_MAILTO email address, no API token requiredRetry-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 hits CROSSREF_TIMEOUT_MS, where every attempt costs the full deadlinecontent[] as well as in structuredContenthas-abstract, has-references, from-pub-date); the server enforces correct syntax and surfaces API validation errors with actionable recovery hints&lt; reads as the literal <, never as < — and a reference has to end in a semicolon, so the bare & in R&D or in a URL's query string comes back as deposited. The fields publishers deposit as JATS XML — work titles, subtitles, container titles, and abstracts — and the citation strings in a reference list additionally have markup stripped, so an italicized species name reaches content[] as text instead of an <i> tag and a newline that splits the Markdown heading. Both run one rule: a bracket comes out only when it is a well-formed tag — one inside a MathML or structured-citation region, or one the element-name classification recognizes — so a cited URL, a Miller index, a DOI fragment, and a bracketed phrase come back as deposited. A JATS <alternatives> wrapper holds one object encoded several ways, and its first text-bearing child is the one returned, so a formula deposited as both TeX and MathML renders once instead of twice, and it stands as its own token in the sentence the way a MathML formula does, whichever encoding it kept. A link (<a>, <ext-link>, <uri>) is the one element decided against its own text rather than by name, because that is where the address it carries may or may not already be: the tags come out where the text holds what the href holds — verbatim or less its scheme — and stay where the href addresses something the text does not name, so a trial registration deposited as both text and href reaches the reader as plain text, while a link showing only a site's homepage over an href that points at one record keeps its tag rather than losing the record. Removing a tag leaves the separator its class calls for: scripts and formula wrappers leave none, because their content continues the token around them (CO<sub>2</sub> reads CO2, not CO 2, and a MathML formula reads as one expression); inline emphasis leaves a space only where the text would otherwise run together, between two word characters or where a sentence ends and the next word begins; block boundaries always leave one. The two surfaces differ in one thing — an element name neither recognizes is structure in a JATS field and is removed, and is presumed content in a citation string and stays. A bracket the rule keeps then has to survive the client's Markdown renderer, which would otherwise consume a kept link down to the text it wraps and drop the address behind it, resolve an escaped character reference, and pair a deposited * with the emphasis format() writes around a journal title — so every normalized value is escaped on its way into content[], and only where a reader would take a character for markup: R&D and [18F]FDG are untouched while < and <ext-link …> are not. A marker that opens a Markdown block — the 19. an abstract begins on, a leading - — is escaped only on the one line that renders a deposited value at column zero, since anywhere else it is inert. Identifiers and machine-format values (DOIs, URLs, ISSNs, prefixes, dates, work types) are returned byte-exact on both surfaces, since they are what a reader copiesAdd 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_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). 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/. |
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 and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
cyanheads/internet-archive-mcp-server
cyanheads/openlibrary-mcp-server-ec6a4bb0
cyanheads/stackexchange-mcp-server
devids77/mcp-market-russia
dimitry/parse-mcp
domainkits/domainkits