Access the OpenAlex academic research catalog - 270M+ publications through MCP. STDIO & Streamable HTTP.
Public Hosted Server: https://openalex.caseyjhand.com/mcp
Scholarly catalog data from OpenAlex — 270M+ works, 90M+ authors, 100K+ sources, plus institutions, topics, keywords, publishers, and funders. Search, filter, and aggregate across all eight entity types, resolve ambiguous names to canonical IDs, and walk the citation graph one hop at a time. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
openalex_search_entities |
Search, filter, sort, or retrieve by ID across all 8 entity types |
openalex_analyze_trends |
Group-by aggregation for trend and distribution analysis |
openalex_resolve_name |
Resolve a name or an identifier (DOI, ORCID, ROR, PMID, ISSN, OpenAlex ID) to an OpenAlex ID |
openalex_get_citation_graph |
Walk the citation graph one hop from a seed work: cites, cited_by, or related_to |
openalex_describe_fields |
List valid filter, group_by, and select field names for an entity type |
| Prompt | Description |
|---|---|
openalex_literature_review |
Guides a systematic literature search: formulate query, search, filter, analyze citation network, synthesize findings |
openalex_research_landscape |
Analyzes the research landscape for a topic: volume trends, top authors/institutions, open access rates, funding sources |
- Retrieve a single entity by ID — OpenAlex ID, DOI, ORCID, ROR, PMID, ISSN, or PMCID (bare or URL form).
idtakes precedence: search parameters passed alongside it are dropped, and the response names which ones. A PMCID resolves nothing (OpenAlex indexes none) — use the work's PMID or DOI instead - Keyword search (boolean operators, quoted phrases, wildcards, fuzzy match) plus
exactandsemanticsearch modes — semantic caps at 50 results per page and ~1 req/sec - Rich filter syntax: AND across fields, OR within a field (
|), NOT (!), ranges, comparisons; a comma inside a filter value is rejected (use|, or a.searchfilter for free text) selectreturns a curated per-entity-type default unless overridden, or["*"]for the full record; invalid field names error with the valid set- Cursor pagination, up to 100 results per page (default 25);
sample(up to 100, single page only, nocursor) plus a deterministicseedfor reproducible random sampling display_nameis nullable for untitled records; every call reports OpenAlex daily-budget cost and remaining balance
- Group any supported field for trend, distribution, or comparative analysis; combine with
filtersto scope the population before aggregation - Up to 200 groups per page (default).
order: "count"(default) returns the top-N by count with no further pages;order: "key"enumerates all distinct values key-ascending with cursor pagination include_unknown(defaultfalse) adds a group for entities with no value for the grouped field- Not every field is groupable — raw date fields,
.searchoperators, andfrom_*/to_*range modifiers are rejected; check withopenalex_describe_fields(entity_type, "group_by") - Reports OpenAlex daily-budget cost and remaining balance — aggregation is priced far below paging the same entities
- A name or partial name runs an autocomplete search: up to 10 matches with disambiguation hints (last institution, host organization, place, etc.)
- An identifier — OpenAlex ID, DOI, ORCID, ROR, PMID, or ISSN, bare or in URL form — resolves directly to the one record it addresses; no
entity_typeneeded, since the identifier determines its own. A PMCID is recognized but resolves nothing — OpenAlex indexes none filtersnarrows autocomplete only; on an identifier lookup they're ignored and named in a notice- Reports OpenAlex daily-budget cost and remaining balance
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 ID, 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- Stacks with
filters/sort/selectto narrow the graph;filterscannot setcites/cited_by/related_todirectly — those are reserved fordirection - Cursor pagination, up to 100 results per page (default 25)
- Reports OpenAlex daily-budget cost, covering both the seed-validation lookup and the graph page, plus remaining balance
- Lists every valid field name for an entity type + context (
filter,group_by,select) — the complete pool, never truncated group_byis the filter set minus raw date fields,.search/.search.exactoperators, andfrom_*/to_*range modifiers, which OpenAlex rejects as aggregation keys- Optional
queryreorders results by name similarity without dropping any field — a nested value's parent object stays reachable further down the list - Backed by a generated field catalog — no live API calls
- Arguments:
topicrequired;scope(narrow/broad) optional, defaults tonarrow - Returns 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
- Arguments:
topicrequired - Returns 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
awards.funder_id(resolve names viaopenalex_resolve_name) orawards.funder_display_namefor readable labels in a single hop
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 and PubMed/PubMed Central URLs); a PMCID normalizes but resolves nothing since OpenAlex indexes none
- Keyless by default — an optional API key raises rate and daily-budget limits, and an optional
mailtoidentifies the caller to OpenAlex's polite pool - HTTP status codes mapped to specific MCP error classes (400 → InvalidParams, 422 → ValidationError, 429 → RateLimited) 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 hint - Response shaping — abstracts are reconstructed from OpenAlex's inverted-index encoding into plaintext, and
display_namestaysnullfor untitled or paratext records instead of being backfilled
A public instance is available at https://openalex.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"openalex-mcp-server": {
"type": "streamable-http",
"url": "https://openalex.caseyjhand.com/mcp"
}
}
}Add the following to your MCP client configuration file.
{
"mcpServers": {
"openalex-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/openalex-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENALEX_API_KEY": "your-api-key"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"openalex-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/openalex-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENALEX_API_KEY": "your-api-key"
}
}
}
}Or with Docker:
{
"mcpServers": {
"openalex-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "OPENALEX_API_KEY=your-api-key",
"ghcr.io/cyanheads/openalex-mcp-server:latest"
]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENALEX_API_KEY=... bun run start:http
# Server listens at http://localhost:3010/mcpOPENALEX_API_KEY is optional — set it to a free OpenAlex account key for keyed rate limits and budget under OpenAlex's usage-based pricing, or omit it for anonymous access. Set OPENALEX_MAILTO to an email if you want to identify yourself to OpenAlex (the polite pool).
- Bun v1.4.0 or higher (or Node.js v24+).
- Optional: an OpenAlex account API key for keyed rate limits and budget — omit for anonymous access.
- Clone the repository:
git clone https://github.com/cyanheads/openalex-mcp-server.git- Navigate into the directory:
cd openalex-mcp-server- Install dependencies:
bun install- Configure environment:
cp .env.example .env
# edit .env and set required vars| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE |
Transport: stdio or http. |
stdio |
MCP_HTTP_PORT |
Port for HTTP server. | 3010 |
MCP_SESSION_MODE |
HTTP session mode: stateless, stateful, or auto (resolves to stateful). This server ships stateless (see Dockerfile and .env.example). |
auto |
MCP_AUTH_MODE |
Auth mode: none, jwt, or oauth. |
none |
MCP_ALLOWED_ORIGINS |
Comma-separated allow-list of browser Origin headers for HTTP transport. Unset = loopback-only; set to * to disable. |
loopback only |
MCP_LOG_LEVEL |
Log level (RFC 5424). | debug |
LOGS_DIR |
Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE |
Storage backend. | in-memory |
OPENALEX_API_KEY |
OpenAlex account API key, sent upstream as api_key= (free from openalex.org/settings/api). Without it, anonymous rate limits apply. |
— |
OPENALEX_MAILTO |
Email sent upstream as mailto= to identify yourself to OpenAlex (the "polite pool"); a courtesy identifier, separate from the API key. |
— |
OPENALEX_BASE_URL |
OpenAlex API base URL. | https://api.openalex.org |
OTEL_ENABLED |
Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | 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 # Lints, formats, type-checks bun run test # Runs the test suite
docker build -t openalex-mcp-server .
docker run --rm -e OPENALEX_API_KEY=your-key -p 3010:3010 openalex-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/openalex-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts |
createApp() entry point — registers tools and prompts. |
src/config/ |
Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions/ |
Tool definitions (*.tool.ts). |
src/mcp-server/prompts/definitions/ |
Prompt definitions (*.prompt.ts). |
src/services/openalex/ |
OpenAlex API client, field catalog, and domain types. |
tests/ |
Unit and integration tests, mirroring the src/ structure. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Wrap 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
Issues are welcome. Run checks before submitting:
bun run devcheck
bun run testApache-2.0 — see LICENSE for details.