@cyanheads/openalex-mcp-server
Accepts DOIs (bare or in URL form) and resolves them to canonical OpenAlex entities — works can be retrieved directly by DOI, DOIs are resolved to OpenAlex IDs via name resolution, and DOI seeds are validated before walking the citation graph.
Accepts ORCID iDs to resolve researcher/author records to their canonical OpenAlex IDs and disambiguate people against the 90M+ author catalog.
Accepts PMIDs and PubMed/PubMed Central URLs, normalizing them for entity retrieval, ID resolution, and citation-graph seeding of biomedical literature (PMCIDs are recognized but resolve to nothing, since OpenAlex indexes none).
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/openalex-mcp-serverSearch for papers about climate change from 2020 to 2023"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://openalex.caseyjhand.com/mcp
Overview
Scholarly catalog data from OpenAlex — 270M+ works, 90M+ authors, 100K+ sources, plus institutions, topics, keywords, publishers, funders, and awards. Search, filter, and aggregate across all nine entity types, resolve ambiguous names to canonical IDs, and walk the citation graph one hop at a time.
Tools
Tool | Description |
| Search, filter, sort, or retrieve by ID across all 9 entity types |
| Group-by aggregation for trend and distribution analysis |
| Resolve a name or an identifier (DOI, ORCID, ROR, PMID, ISSN, OpenAlex ID) to an OpenAlex ID |
| Walk the citation graph one hop from a seed work: |
| List valid filter, group_by, and select field names for an entity type |
Prompts
Prompt | Description |
| Guides a systematic literature search: formulate query, search, filter, analyze citation network, synthesize findings |
| Analyzes the research landscape for a topic: volume trends, top authors/institutions, open access rates, funding sources |
The server also publishes an agent skill, openalex-guide (skill://openalex-guide/SKILL.md, source in skills/openalex-guide/): which filter key answers the question, how to read group_by counts, where each ID type comes from, and how to pin an author without picking a namesake. It ships in the npm package, the Docker image, the .mcpb bundle, and plugin installs.
Related MCP server: mcp-openalex
Capability reference
openalex_search_entities tool
Retrieve a single entity by
id: an OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID, bare or as a URL (OpenAlex record-page and API URLs such ashttps://openalex.org/works/W2741809807included), or a keyword by its slug or URL. Underentity_type: "funders", a Crossref Funder Registry DOI (10.13039/…) also resolvesidtakes precedence: search parameters passed alongside it are dropped, and the response names which onesLookups by ID are unbilled, except a keyword slug or URL, an
idwith characters past the identifier (W2741809807#xstill returns W2741809807), and a funder DOI, which each bill as one list callFails before any request as
id_type_mismatch(an OpenAlex ID of another entity type) orid_dot_segment(a.or..path segment); a?,#,\, tab, or line break inside an identifier is percent-encoded, so SICI DOIs ending in#resolveA PMCID resolves nothing (OpenAlex indexes none); use the work's PMID or DOI
entity_type: "awards"reaches grants (G…IDs), whichopenalex_resolve_namedoes not look up since OpenAlex has no award autocompleteKeyword search (boolean operators, quoted phrases, wildcards, fuzzy match) plus
exactandsemanticsearch modes — semantic ranks a query-dependent candidate set at ~1 req/sec (up to 50 per page), and itsmeta.countreports the candidate count rather than a match totalfiltersaccepts a record object or an unambiguous native string ("issn:0920-8542"), including through thefilteralias. Conversion is disclosed innotice; commas outside paired double quotes separate clauses, and the first colon separates a key from its value, preserving DOI/URL colons and quoted comma phrases. Exact duplicate keys, bare fragments, and unmatched quotes are rejected with record-form recovery guidance. A record holds one value per key; distinct aliases of the same upstream field still AND together. SetOPENALEX_FILTER_STRING_INPUT=falseto require record objects. Native strings apply only to this tool's filters, with the record form remaining the advertised schemaFilter values sent unquoted must have paired double quotes on all four filter-taking tools (
unmatched_quote_in_filter_value); citation filters are checked before the seed lookup. Balanced search phrases such as"graph neural" networksremain valid, including.search.exact. A value sent double-quoted still cannot contain its own interior double quote (quote_in_filter_value)Rich filter syntax: AND across fields, OR within a field (
|), AND within a field (+), NOT (!), ranges, comparisonsOne value cannot mix
|with an AND (a+, or a space between IDs or codes): OpenAlex would silently drop the AND-ed alternatives, soDE|FR+USandauthorships.author.id: A1 A2|A3fail asmixed_and_or_in_filter_valuebefore any request. Put the AND in a second key instead (institutions.country_code: USbesideauthorships.institutions.country_code: DE|FRon works)Fields OpenAlex matches as whole text (
display_name,awards.funder_display_name,awards.funder_award_id,geo.city,geo.region,raw_affiliation_strings,block_key,bibliofields) are sent double-quoted, so a value with spaces or commas matches as one phrase (awards.funder_display_name: National Science Foundation).!and|still work (geo.city: New York|Boston);+cannot AND it with another valueA comma is allowed in
.searchvalues and whole-text fields; anywhere else it fails ascomma_in_filter_value(use|for OR)+is a literal character in.searchvalues, DOIs, dates, URLs, exactly matched names (C++,Erasmus+),bibliofields (issue1+2), andawards.funder_award_idA double quote inside a value sent quoted fails as
quote_in_filter_value
Country and language values are converted to the codes OpenAlex stores, in every tool that takes
filters:Country fields (keys ending in
country_code,country_codes, orcountries): ISO alpha-3 codes, numeric codes, and English names (ISO's or OpenAlex's own facet labels, such asIvory Coast) become alpha-2 (DEU,276,Germany→DE;UK→GB). A value that cannot be a country (Germanyy,999,DE US) fails asinvalid_country_codebefore any requestlanguage: ISO 639-2 codes and English names become ISO 639-1 (eng,English→en;Cebuano→ceb)Each
|/+segment converts on its own (South Korea|Japan→KR|JP); anything else (null,XK,zh-cn, thecountries/DEUpath form) is sent as given
selectreturns a curated per-entity-type default unless overridden, or["*"]for the full record; invalid field names error with the valid set. On works,author_listreturns each author in authorship order as{id, display_name, orcid}— about a tenth the size ofauthorships, which adds institutions and affiliations — and is never part of the default or["*"]Cursor pagination for keyword and exact search, up to 100 results per page (default 25); semantic search walks its candidates with
page(1-based) instead, up to 50 per page, and mixing either knob with the wrongsearch_modeis rejected before the upstream callsample(up to 10,000) plus a deterministicseedfor reproducible random sampling; keyword and exact modes only, and never alongsidesortorcursor— OpenAlex does not sample a semantic search and refuses a sorted sample, so those combinations are rejected before the upstream call.per_pagedefaults tomin(sample, 100), so a sample of 100 or fewer returns whole in one call; a larger one is walked withpageand aseed(sample_page_without_seedwithout one), up to record 10,000 (sample_page_too_deeppast it)A page is
per_pagerecords, whole, and anidlookup returns every selected array whole;slice: {field, offset, limit}on anidlookup pages one array by count (limitdefault 50, clamped to 5,000). A response over 8,000,000 bytes on either surface (structuredContent, orcontent[]text) fails asresponse_too_largeand names aper_pageorslice.limitthat fits, or a narrowerselectA list record carrying exactly 100
authorshipsorauthor_listentries is flagged as possibly capped, with anid+slicecall to retrieve more stored authors. Singleton lookups and snapshots retain at most 5,000 authorships per work; at that ceiling both author fields remain possibly capped on every slice.totalcounts stored entries rather than a complete byline, andnextbecomes null when those entries are exhausted. Another OpenAlex fetch or snapshot cannot recover authors beyond the storage ceilingOversized positive integers clamp to their cap, with requested/applied sizes in
notice, a log, and a counter:per_pageabove 100 (200 on analysis, 50 under semantic search),slice.limitabove 5,000, andsampleabove 10,000. SetOPENALEX_CLAMP_PER_PAGE=falseto reject each over its cap instead. Zero, negatives, and fractions always reject. Anidignores page sizedisplay_nameis nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
openalex_analyze_trends tool
Group any supported field for trend, distribution, or comparative analysis; combine with
filtersto scope the population before aggregationUp to 200 groups per page (default). Without a cursor,
order: "count"(default) returns the top-N by count with no further pages;order: "key"enumerates distinct values key-ascending with cursor pagination. Boolean fields keep count order even with"key"and do not support cursor pagination. A suppliedcursorselects key order even whenorderis omitted or"count". OpenAlex can wrap a key-ordered walk to earlier groups: stop when a page's first key repeats an earlier key, or when a page is short or empty, even ifnext_cursoris still presentinclude_unknown(defaultfalse) adds a group for entities with no value for the grouped field, flaggedis_unknown: trueand labeled as unknown in the text output. The native:include_unknownsuffix ongroup_byalso enables it, even with an explicitfalse; passing both adds the suffix once. OpenAlex keys it-111/-111.0(numeric fields),unknown(text fields andorder: "key"), or an ID ending in/unknown; the key is a sentinel, not a filter value. Boolean fields have no such group — a missing value counts asfalsekey_display_nameremains present but can benullwhen OpenAlex supplies no label; text output renders the group key in that case, including unknown bucketsNot every field is groupable — raw date fields,
.searchoperators,from_*/to_*range modifiers, decimal scores such asfwci,display_name, external-ID fields such asdoi, and fields OpenAlex looks up from a related entity such asprimary_location.source.country_codeare rejected;openalex_describe_fields(entity_type, "group_by")lists the fields that groupA works group_by on
authorships.countriessays in its notice that OpenAlex counts only each work's first 100 authorships for that field, and namesauthorships.institutions.country_codeas the field that counts all of thementity_type: "awards"groups by fields such asstart_year,funder.id, orfunding_type; groups count awards and do not sumamount_usdReports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
openalex_resolve_name tool
A name or partial name runs an autocomplete search over works, authors, sources, institutions, topics, keywords, publishers, and funders: up to 10 matches with disambiguation hints (last institution, host organization, place, etc.)
An award ID (
G…, bare or as an OpenAlex URL) sends no request: OpenAlex has no award autocomplete, so the response is empty with a notice naming theopenalex_search_entitiescall that opens the award. Under an explicitentity_type, a bareG…is searched as a name of that typeAn identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or keyword URL, bare or in URL form — resolves directly to the record it addresses, as the entity type it belongs to. A ROR can name an organization's institution, funder, and publisher records at once: with no
entity_type(or any other type) all three are looked up in parallel as unbilled singleton fetches and every record found is returned, institution first;entity_typeinstitutions,funders, orpublishersnarrows it to that one. A Crossref Funder Registry DOI (10.13039/…) resolves to the funder unlessentity_typeisworks. A PMCID is recognized but resolves nothing — OpenAlex indexes noneShort names share the bare OpenAlex ID shape (keyword
p53reads as publisherP53), so a bare OpenAlex ID that resolves nothing is searched as a name next, underentity_typewhen given, and the notice names both outcomes; a real ID still resolves with its one request. Underentity_type: "keywords"the shape is searched as a keyword name from the startfiltersnarrows autocomplete only and requiresentity_type— a name query passingfilterswithout it is rejected before any request (filters_without_entity_type). On an identifier lookup they're ignored and named in a notice; a name search that matches nothing underfiltersechoes them and suggests loosening them firstWith
entity_typeset, OpenAlex autocomplete fails on a name over 1,000 characters; that failure is reported once asquery_too_longwith advice to shorten the name, not retried as an outageReports OpenAlex daily-budget cost and remaining balance
openalex_get_citation_graph tool
directionsets the edge:cites(works citing the seed),cited_by(the seed's own reference list),related_to(OpenAlex's algorithmic related works, ~8-30 typical, may be empty)seed_idaccepts an OpenAlex work ID (bare, or its OpenAlex record-page or API URL), DOI, or PMID (PMCID recognized but resolves nothing); validated against a live lookup first, so a non-existent seed fails asNotFoundrather than returning an empty graph, and the ID of another entity type fails asid_type_mismatchbefore any upstream callStacks with
filters/sort/select(author_listincluded) to narrow the graph;filterscannot setcites/cited_by/related_to, nor an alias of one such ascited_works— those keys are reserved fordirectionCursor pagination, up to 100 results per page (default 25); a page is
per_pageworks, wholeThe same 8,000,000-byte
response_too_largefailure (naming aper_pagethat fits) and 100-authorship cap disclosure asopenalex_search_entitiesReports OpenAlex daily-budget cost, covering both the seed-validation lookup and the graph page, plus remaining balance
openalex_describe_fields tool
Lists every valid field name for an entity type + context (
filter,group_by,select) — the complete pool, never truncatedgroup_byis the filter set minus what OpenAlex rejects as an aggregation key: raw date fields,.search/.search.exactoperators,from_*/to_*range modifiers, and a per-entity-type set found by grouping every listed field against the live API — decimal scores such asfwci, several source year fields,display_name, and external-ID fields among themcreated_date,updated_date, and theirfrom_/to_forms are listed filters that OpenAlex serves only on a paid plan; without one, a filter on them fails on its first request asupstream_plan_required, with no retryOptional
queryreorders results by name similarity without dropping any field — a nested value's parent object stays reachable further down the listBacked by a generated field catalog — no live API calls
openalex_literature_review prompt
Arguments:
topicrequired;scope(narrow/broad) optional, defaults tonarrowReturns one user message walking a 6-step workflow: resolve entities, search literature, identify key papers, trace citations, analyze the landscape, synthesize findings
scopechanges the search step:narrowfavors exact search with tight topic filters;broadadds semantic search across multiple related topic IDs
openalex_research_landscape prompt
Arguments:
topicrequiredReturns one user message walking a 7-step quantitative workflow: resolve the topic ID, volume trends, top contributors (institutions/countries/journals), open access rate, funding sources, most-cited works, emerging fronts
The funding step groups by
funders.id, which counts every work that lists a funder and labels each group with the funder's name; for grants it groups the topic's works byawards.idand opens the topG…IDs onentity_type: "awards"for amounts and dates, since few awards carry a topic of their own
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.
OpenAlex-specific:
Typed API client with automatic ID normalization (DOI, ORCID, ROR, PMID, PMCID, ISSN, OpenAlex record-page and API URLs over
http,https, or no scheme, PubMed/PubMed Central URLs); a PMCID normalizes but resolves nothing since OpenAlex indexes noneKeyless by default — an optional API key raises rate and daily-budget limits, and an optional
mailtoidentifies the caller to OpenAlex's polite poolHTTP status codes mapped to specific MCP error classes (400 → InvalidParams, or ServiceUnavailable when a semantic search's query embedding failed upstream; 422 → ValidationError; 429 → RateLimited, or Forbidden when the 429 refuses a paid-plan filter) with upstream messages surfaced
Timeout-aware request retries and cancellation support via
AbortSignal
Agent-friendly output:
Provenance — every API-calling tool reports OpenAlex daily-budget cost, remaining balance, and reset time (
budget.costUsd,remainingUsd,resetsInSeconds)Effective-query echo — search, trends, and citation-graph responses echo the criteria that actually ran, so an empty result is diagnosable without re-reading the request
Discriminated output contracts — typed error reasons (
entity_not_found,upstream_budget_exhausted,semantic_per_page_cap,reserved_filter_key, and more) each carrying an explicit recovery hintResponse shaping — abstracts are reconstructed from OpenAlex's inverted-index encoding into plaintext, and
display_namestaysnullfor untitled or paratext records instead of being backfilledClean provider text — OpenAlex passes titles, abstracts, and names through unsanitized; HTML entities are decoded against the full WHATWG table, comments and HTML/JATS/MathML formatting tags are removed, CDATA wrappers and IOP's
<?CDATA …?>math markers are unwrapped with their TeX kept, MathType equation blobs (% MathType!MTEF!…!44A1!) are removed while the TeX that restates them stays, and a double-escaped line break (a literal\n) becomes a space; literal text such asA < B,95% CI, and TeX such as\nuis kept. Identifier and URL fields (id,doi,ids,orcid,ror,issn,*_url, lineages) are returned exactly as OpenAlex stores them, so each one resolves when passed back. The text output escapes that text for Markdown, so a stray*,<tag>, or[x](y)renders as written, and line breaks, tabs, and leading or trailing spaces read back as the structured output holds them; a multi-linedisplay_namerenders on its own**Display Name:**line. Notices escape caller text the same way. IDs, and URLs that render as links, stay byte-identical
Getting started
Connect to the hosted instance, no install:
claude mcp add --transport http openalex-mcp-server https://openalex.caseyjhand.com/mcpOr run it locally:
claude mcp add openalex-mcp-server -- npx -y @cyanheads/openalex-mcp-serverFor other clients, add this to the MCP config file:
{
"mcpServers": {
"openalex-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/openalex-mcp-server"]
}
}
}An optional OPENALEX_API_KEY — a free OpenAlex account key — raises the rate limits and daily budget over anonymous access; OPENALEX_MAILTO identifies you to OpenAlex's polite pool. For Docker or a source build, see Running the server.
Configuration
Variable | Description | Default |
| Transport: |
|
| Port for HTTP server. |
|
| HTTP session mode: |
|
| Auth mode: |
|
| Comma-separated allow-list of browser | loopback only |
| Log level (RFC 5424). |
|
| Directory for log files (Node.js only). |
|
| Storage backend. |
|
| OpenAlex account API key, sent upstream as | — |
| Email sent upstream as | — |
| OpenAlex API base URL. |
|
| Accept unambiguous native filter strings on |
|
| Clamp oversized positive integer |
|
| Enable OpenTelemetry instrumentation (spans, metrics, completion logs). |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
Clone and install:
git clone https://github.com/cyanheads/openalex-mcp-server.git cd openalex-mcp-server && bun install cp .env.example .env # optionally set OPENALEX_API_KEYBuild and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lints, formats, type-checks bun run test # Runs the test suite
Docker
docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 ghcr.io/cyanheads/openalex-mcp-server:latestThe image defaults to HTTP transport, stateless session mode, and logs to /var/log/openalex-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/openalex-mcp-server:latest"]. To build locally, docker build -t openalex-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Directory | Purpose |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Prompt definitions ( |
| OpenAlex API client, field catalog, and domain types. |
| The published |
| Unit and integration tests, mirroring the |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor logging,ctx.statefor storageWrap OpenAlex responses: validate the raw payload → normalize to a domain type → return the output schema; never fabricate missing fields
Always resolve names to IDs via
openalex_resolve_namebefore filtering by entity
Contributing
Issues are welcome. Run checks before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
OpenAlex MCP — wraps the OpenAlex API (scholarly works, free, no auth)
Query SEC EDGAR filings, XBRL financials, and company data via MCP. STDIO or Streamable HTTP.
Crossref MCP — wraps the Crossref REST API (academic papers, free, no auth)
arXiv, PubMed, Crossref and OpenAlex scholarly metadata.
Related MCP Servers
- AlicenseBqualityDmaintenanceProvides access to OpenAlex's catalog of 240M+ scholarly works, enabling search and retrieval of research papers, authors, institutions, journals, concepts, and funders with advanced filtering and classification capabilities.1174 npm5MIT
- AlicenseAqualityDmaintenanceMCP server for the OpenAlex scholarly database, providing AI agents with tools to search and retrieve academic works, authors, and institutions via natural language queries.8MIT
- FlicenseNot gradedqualityDmaintenanceMCP server for academic research using the OpenAlex API, enabling article search, details retrieval, and author profile lookup.-
- AlicenseNot gradedqualityCmaintenanceAn MCP server for searching and exploring OpenAlex's index of 250M+ scholarly works, authors, institutions, journals, funders, and grants through Claude.74 npm1MIT