@cyanheads/arxiv-mcp-server
Search arXiv, fetch paper metadata, and read full-text content via MCP. STDIO or Streamable HTTP.
4 Tools • 2 Resources
Overview
arXiv papers, metadata, and full text from the arXiv API and its OAI-PMH metadata feed. Search papers by query, category, and submission date; fetch structured metadata by ID; and read full paper text with automatic fallback across HTML and PDF renders. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
Resources
Capability reference
arxiv_search_papers tool
- Query supports field prefixes
ti:, au:, abs:, cat:, co:, jr:, all: and AND / OR / ANDNOT, up to 1000 characters; category takes a code (cs.CL) or a whole archive (astro-ph, cs, math)
- Query grammar follows arXiv's: space-separated terms are joined with
OR (write AND to require every term), operators are uppercase only, and all three share one precedence grouped to the right (a AND b OR c is a AND (b OR c)). Quote a multi-word topic ("graph neural networks" AND cat:cs.LG) and put alternatives in parentheses before a filter; arXiv ignores common words such as of and the outside a quoted phrase
- Sorts by
sort_by (relevance / submitted / updated) and sort_order, with submitted_from / submitted_to as inclusive UTC YYYY-MM-DD bounds; up to 50 results per call (a larger max_results returns 50)
- arXiv serves only the first 10,000 results of a query; a
start of 10,000 fails start_beyond_result_limit. Use date windows to go further
- Output echoes the effective query, total match count, and page offset; empty pages carry recovery guidance instead of an error. A total that is a floor (the local mirror counting to 50,000) carries
totalFoundIsLowerBound: true
arxiv_get_metadata tool
- Takes up to 100 IDs and looks up the first 10 distinct ones; the rest come back in
remaining_ids for the next call's paper_ids. An ID repeated in any accepted spelling is looked up once, while different versions of one paper stay separate
- Accepts versioned (
2401.12345v2), unversioned, and pre-2007 (hep-th/9901001) IDs, with an optional arXiv: prefix or an arxiv.org abs / pdf / html URL
- Returns found papers plus a typed
not_found[] (not_in_arxiv / version_not_in_mirror / version_not_served / live_lookup_failed / invalid_id) for the rest; version_not_served is a superseded pre-2007 version arXiv's API answers HTTP 500 for, and its detail names the latest version. With the local mirror, live_lookup_failed marks an ID the mirror lacked whose live lookup failed; the papers the mirror served still come back
- Fails
invalid_id when no ID is well-formed, no_match when every ID misses, version_unavailable when a version is reachable only on the live API, and version_not_served when every ID is a version arXiv will not serve
arxiv_read_paper tool
- Tries arXiv HTML, then ar5iv, then PDF text extraction; the
source field reports which answered. PDF text is plain, so math, tables, and headings flatten
- ar5iv keeps one render per paper, so an ar5iv body may be another revision than
paper_id and carries revision_unconfirmed: true, unless the read named no version and arXiv's API reported v1 as the paper's only version; pdf_url is exactly the paper_id revision
max_characters defaults to 100,000 (pass null for the whole paper); page with start. The first page carries an outline of section headings with offsets
paper_id accepts the same ID spellings as arxiv_get_metadata
- Fails
invalid_id, no_match (arXiv has no such paper, or no such version of it), content_unavailable (no render, no PDF), pdf_extraction_failed (the PDF has no text layer or does not parse), version_unavailable (version-pinned ID needs the live API), or version_not_served (a superseded pre-2007 version arXiv will not serve; names the latest version and this version's PDF)
arxiv_list_categories tool
- ~155 categories across 8 top-level groups (
cs, econ, eess, math, physics, q-bio, q-fin, stat)
- Optional
group filter to narrow results
- Static data — always succeeds
arxiv://paper/{paperId} resource
paperId accepts the same ID spellings as arxiv_get_metadata; percent-encode a legacy ID's slash (arxiv://paper/hep-th%2F9901001)
- Fails
empty_id, invalid_id, no_match, version_unavailable, or version_not_served
arxiv://categories resource
- Returns
{ categories: [...] }, a flat array with code / name / group per entry
- No parameters; cacheable for 24h
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.
arXiv-specific:
- Read-only, no authentication required
- Request queue enforcing arXiv's 3-second crawl delay; rate-limit responses fail fast with a cooldown
- Paper text falls back from arXiv HTML to ar5iv to PDF extraction
- Full arXiv category taxonomy embedded as static data
- Optional local OAI-PMH metadata mirror (SQLite + FTS5) that removes rate-limit exposure for search and metadata. See Optional: local mirror
Agent-friendly output:
arxiv_read_paper names its source; arxiv_search_papers echoes the effective query
arxiv_get_metadata returns found papers with a typed not_found[] reason per miss
- Typed
source and not_found[].reason enums let callers branch on data
Getting started
Public Hosted Instance
A public instance is available at https://arxiv.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
Self-Hosted / Local
Add the following to your MCP client configuration file.
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
Prerequisites
Installation
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
Configuration
All configuration is optional — the server works out of the box with sensible defaults.
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
-
Run checks and tests:
Optional: local mirror
For self-hosted deployments behind one egress IP, an optional SQLite + FTS5 mirror of arXiv's OAI-PMH metadata serves arxiv_search_papers and arxiv_get_metadata locally, so concurrent users stop queueing behind arXiv's 3-second crawl delay; arxiv_read_paper still fetches paper text live. Harvest it once with bun run mirror:init (~4.4h), then set ARXIV_MIRROR_ENABLED=true. Running it from an npm install, scheduling refreshes, schema upgrades, and where mirror search differs from the live API are covered in docs/mirror.md.
Docker
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/arxiv-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- arXiv API returns HTTP 200 for everything — including rate limits — so check content-type and body before parsing
- Validate raw arXiv responses → normalize to domain types → return the output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks before submitting:
License
Apache-2.0 — see LICENSE for details.