Search arXiv, fetch paper metadata, and read full-text content.
One address for any MCP client. No sign-in needed on this server.
4 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
Search arXiv, fetch paper metadata, and read full-text content via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://arxiv.caseyjhand.com/mcp
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.
| Tool | Description |
|---|---|
arxiv_search_papers | Search arXiv papers by query with field prefixes, category, and date filters |
arxiv_get_metadata | Fetch metadata for one or more papers by arXiv ID |
arxiv_read_paper | Read full paper text via HTML, ar5iv, or PDF-extracted fallback |
arxiv_list_categories | List the arXiv category taxonomy, optionally filtered by group |
| Resource | Description |
|---|---|
arxiv://paper/{paperId} | Paper metadata by arXiv ID |
arxiv://categories | Full arXiv category taxonomy |
arxiv_search_papers toolti:, 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)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 phrasesort_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)start of 10,000 fails start_beyond_result_limit. Use date windows to go furthertotalFoundIsLowerBound: truearxiv_get_metadata toolremaining_ids for the next call's paper_ids2401.12345v2), unversioned, and pre-2007 (hep-th/9901001) IDs, with an optional arXiv: prefix or an arxiv.org abs / pdf / html URLnot_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 backinvalid_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 servearxiv_read_paper toolsource field reports which answered. PDF text is plain, so math, tables, and headings flattenpaper_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 revisionmax_characters defaults to 100,000 (pass null for the whole paper); page with start. The first page carries an outline of section headings with offsetspaper_id accepts the same ID spellings as arxiv_get_metadatainvalid_id, 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 toolcs, econ, eess, math, physics, q-bio, q-fin, stat)group filter to narrow resultsarxiv://paper/{paperId} resourcepaperId accepts the same ID spellings as arxiv_get_metadata; percent-encode a legacy ID's slash (arxiv://paper/hep-th%2F9901001)empty_id, invalid_id, no_match, version_unavailable, or version_not_servedarxiv://categories resource{ categories: [...] }, a flat array with code / name / group per entryBuilt 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:
Agent-friendly output:
arxiv_read_paper names its source; arxiv_search_papers echoes the effective queryarxiv_get_metadata returns found papers with a typed not_found[] reason per misssource and not_found[].reason enums let callers branch on dataA public instance is available at https://arxiv.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"arxiv-mcp-server": {
"type": "streamable-http",
"url": "https://arxiv.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file.
{
"mcpServers": {
"arxiv-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/arxiv-mcp-server@latest"]
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"arxiv-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/arxiv-mcp-server@latest"]
}
}
}
Or with Docker:
{
"mcpServers": {
"arxiv-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/arxiv-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/arxiv-mcp-server.git
cd arxiv-mcp-server
bun install
All configuration is optional — the server works out of the box with sensible defaults.
| Variable | Description | Default |
|---|---|---|
ARXIV_API_BASE_URL | arXiv API base URL. | https://export.arxiv.org/api |
ARXIV_REQUEST_DELAY_MS | Minimum delay between arXiv API requests (ms). | 3000 |
ARXIV_CONTENT_TIMEOUT_MS | Timeout for paper body fetches — HTML renders and PDF downloads (ms). | 30000 |
ARXIV_API_TIMEOUT_MS | Timeout for API search/metadata requests (ms). With the mirror enabled, also the longest an unversioned arxiv_read_paper waits on its metadata lookup, queue included, before a mirror holding the paper answers. | 15000 |
ARXIV_READ_TIMEOUT_MS | Time budget for one arxiv_read_paper call — its metadata lookup, every content request and retry, and PDF text extraction (ms). Keep it under your MCP client's request timeout (60 s by default). | 50000 |
ARXIV_MIRROR_ENABLED | Enable the local OAI-PMH metadata mirror. | false |
ARXIV_MIRROR_PATH | SQLite path for the mirror. | ./data/arxiv-mirror.db |
ARXIV_MIRROR_REFRESH_CRON | UTC cron expression for in-process daily refresh (HTTP mode only). | unset |
ARXIV_MIRROR_FALLBACK_LIVE | Fall through to the live API on a local ID-lookup miss. | true |
ARXIV_MIRROR_RECENT_DAYS_LIVE | Positive values send sort_by=submitted, descending queries to the live API; 0 disables. | 2 |
ARXIV_MIRROR_OAI_BASE_URL | arXiv OAI-PMH endpoint base URL. | https://oaipmh.arxiv.org/oai |
ARXIV_MIRROR_OAI_REQUEST_DELAY_MS | Minimum delay between OAI-PMH requests (ms). | 3000 |
ARXIV_MIRROR_REFRESH_TIMEOUT_MS | Time budget for one scheduled refresh (ms). | 7200000 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_SESSION_MODE | auto, stateful, or stateless. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
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:http
# or
bun run start:stdio
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security audit
bun run test # Vitest test suite
For self-hosted deployments behind a single egress IP, arXiv's ~3-second crawl delay serializes concurrent users. An optional local mirror removes that rate-limit exposure for arxiv_search_papers and arxiv_get_metadata by serving from a SQLite + FTS5 store harvested via OAI-PMH. arxiv_read_paper always fetches the body live — full-content harvesting is against arXiv's data policy — and takes its metadata from the mirror only when the live lookup fails or, for an ID without a version, is still pending after ARXIV_API_TIMEOUT_MS, reading the stored version, which can trail arXiv's latest until the next refresh.
Disabled by default. To enable:
# 1. Cold-start harvest (~4.4h sequential). One-time per installation.
# Waits out arXiv outages; Ctrl+C stops it, and a rerun resumes from its checkpoint.
bun run mirror:init
# 2. Enable the mirror.
export ARXIV_MIRROR_ENABLED=true
# 3. Start the server — reads switch to the mirror once the harvest completes.
bun run start:http
From an npm install, the mirror:* commands belong to the package rather than your project: run them in the package directory (bun run --cwd node_modules/@cyanheads/arxiv-mcp-server mirror:init), or run a script directly from your project root (bun node_modules/@cyanheads/arxiv-mcp-server/scripts/arxiv-mirror-init.ts). Set ARXIV_MIRROR_PATH to an absolute path outside node_modules, the same one the server reads, so a reinstall leaves the mirror in place.
Keep it current with bun run mirror:refresh (cron/systemd/launchd, or ARXIV_MIRROR_REFRESH_CRON in HTTP mode) and check it with bun run mirror:verify. An interrupted refresh picks up on the next run at the last datestamp it checkpointed, re-reading that day; when the in-process refresh runs out of its ARXIV_MIRROR_REFRESH_TIMEOUT_MS budget before leaving the day it started on, the next run resumes where it stopped instead. When a release changes the mirror's schema, the server answers from the live API until a writer migrates the file in place, with no re-harvest: with ARXIV_MIRROR_REFRESH_CRON in HTTP mode the server starts that refresh itself; otherwise run bun run mirror:refresh or bun run mirror:verify. One writer migrates the file at a time: a mirror command started meanwhile changes nothing and exits 75, naming the process that holds the migration, so run it again once that finishes. A schema v3 mirror migrates to v4 this way on the next writer open: it adds a column to the category table, fills it, and rebuilds five date indexes. On a full-size mirror that takes minutes, not seconds (about five minutes), and it needs free disk for a WAL that peaks near 450 MB and a file that grows about 9%. A schema v4 mirror migrates to v5 the same way: it rewrites each paper whose text holds ' or " with the quote characters, about 1.2 million of 3.2 million papers, then rebuilds the category index searches use, in about 4.5 to 7.5 minutes on a full-size mirror depending on host load, with a WAL that peaks near 160 MB and a file that grows about 10%. Rolling back to 1.7.1 or earlier and refreshing stores those references again in each paper the refresh rewrites; after rolling forward, run UPDATE schema_version SET version = 4 on the file and then bun run mirror:verify to repeat the repair.
Mirror search ranks with FTS5 BM25, so sort_by=relevance returns a different top set than the live API. A mirror search with no free-text term counts exactly when it filters on one category code; one that spans several codes (cat:cs, two categories) or a submitted-date window alone stops counting at 50,000, and a total that reaches it comes with totalFoundIsLowerBound: true, meaning 50,000 or more. Searches that go to the live API instead: sort_by=submitted descending (while ARXIV_MIRROR_RECENT_DAYS_LIVE is positive), searches whose free-text terms match 50,000 or more papers, queries with a cat: or submittedDate: operand the mirror cannot apply as a filter (one joined to the rest by OR or by a space, or on the right of an ANDNOT) or that leave only ANDNOT terms once those operands are taken out, queries with unbalanced parentheses, a field prefix with no value (ti: x, ti:), an empty quoted phrase, or nothing left to match (()), queries with an rn:, id:, or lastUpdatedDate: operand or a malformed submittedDate:, queries with a word arXiv's index drops (of, the, and, …) outside a longer quoted phrase, a cat: wildcard that names no category (cat:zzz.*), and version-pinned requests (the mirror holds only the latest version).
docker build -t arxiv-mcp-server .
docker run --rm -p 3010:3010 arxiv-mcp-server
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.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources and starts the optional mirror-refresh scheduler. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts). |
src/mcp-server/resources/definitions | Resource definitions (*.resource.ts). |
src/services/arxiv | ArxivService — live arXiv API client (search, metadata, HTML), and PDF text extraction in a worker thread (pdf-text.ts). |
src/services/arxiv/mirror | Optional OAI-PMH mirror — harvester, SQLite + FTS5 store, query translator, runner. |
scripts/arxiv-mirror-*.ts | Mirror lifecycle scripts (init, refresh, verify); scripts/tsconfig.json resolves their @/ imports to src/, or to dist/ in an install. |
tests/ | Unit and integration tests. |
docs/ | Design document and directory structure. |
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 storageIssues are welcome. Run checks before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.