@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.
| Tool | Description |
|---|
arxiv_search | 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 |
Resources
| Resource | Description |
|---|
arxiv://paper/{paperId} | Paper metadata by arXiv ID |
arxiv://categories | Full arXiv category taxonomy |
Capability reference
- Field prefixes
ti:, au:, abs:, cat:, co: (comment), jr: (journal ref), all: (all fields); boolean AND / OR / ANDNOT; query capped at 1000 characters
category accepts a leaf code (cs.CL) or a whole archive (astro-ph, cs, math) β a bare archive matches its subject classes plus pre-subdivision legacy papers
sort_by (relevance / submitted / updated) and sort_order (ascending / descending); up to 50 results per call (max_results)
submitted_from / submitted_to bound submission date inclusively (UTC YYYY-MM-DD); consecutive windows cover matches with no gap β de-duplicate by ID at the seam β the way to reach results past the 10,000 start pagination ceiling
- Response enrichment echoes the effective query (every filter folded in, replayable), total match count, and page offset; empty or overshot pages carry recovery guidance instead of an error
- Up to 10 IDs per call (single string or array); versioned (
2401.12345v2), unversioned, and legacy (hep-th/9901001) formats accepted
- Partial-batch results: found papers plus a typed
not_found[] (not_in_arxiv / version_not_in_mirror) for the rest β never fails the whole batch for one bad ID
- Fails
no_match only when every ID misses; fails version_unavailable when every miss is a mirror-only version gap reachable on the live API
- Tries native arXiv HTML first, then ar5iv, then text extracted from the PDF β the
source field reports which one answered
- Strips HTML head/boilerplate and collapses MathML to dollar-delimited LaTeX (
$β¦$ inline, $$β¦$$ block) so the character budget targets paper content
- Returns raw HTML for HTML sources β the LLM interprets content directly; PDF-extracted bodies are plain text, so prose is reliable but math, tables, and heading structure flatten
max_characters defaults to 100,000; pass null for the whole paper in one call. Raw HTML can run 500KB-3MB+ for math-heavy papers β page with start instead
- Typed failures:
content_unavailable (no render, no PDF), pdf_extraction_failed (PDF has no text layer), version_unavailable (version-pinned ID needs the live API)
- ~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 versioned, unversioned, and legacy formats β same resolution as arxiv_get_metadata
- Percent-encode a legacy ID's slash:
arxiv://paper/hep-th%2F9901001
- Typed errors:
empty_id, no_match, version_unavailable
arxiv://categories resource
- Full arXiv category taxonomy as
{ categories: [...] }, one flat array with code / name / group per entry
- Cacheable for 24h (
cacheHint.ttlMs: 86400000), public scope
- No parameters
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 β arXiv API is free, metadata is CC0
- Sequential request queue enforcing arXiv's 3-second crawl delay; rate-limit responses (429, or 200 OK with a
Rate exceeded. body) fail fast with a server-computed cooldown rather than retrying blindly
- Content fallback chain:
arxiv.org/html β ar5iv β PDF text extraction, in that order β the source field reports which one answered
- Full arXiv category taxonomy embedded as static data
- Optional local OAI-PMH metadata mirror (SQLite + FTS5) β opt-in, eliminates rate-limit exposure for
arxiv_search and arxiv_get_metadata. See Optional: local mirror
Agent-friendly output:
- Provenance on every read β
arxiv_read_paper's source field names which upstream artifact answered; arxiv_search echoes the effective query so results are reproducible
- Graceful partial failure β
arxiv_get_metadata returns found papers alongside a typed not_found[] reason per miss instead of failing the whole batch
- Discriminated output contracts β typed
source and not_found[].reason enums let callers branch on data, not string parsing
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:
{
"mcpServers": {
"arxiv-mcp-server": {
"type": "streamable-http",
"url": "https://arxiv.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
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
Prerequisites
Installation
- Clone the repository:
git clone https://github.com/cyanheads/arxiv-mcp-server.git
- Navigate into the directory:
- Install dependencies:
Configuration
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). | 15000 |
ARXIV_MIRROR_ENABLED | Enable the local OAI-PMH metadata mirror for search and metadata. | 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 live API on local ID-lookup miss. | true |
ARXIV_MIRROR_RECENT_DAYS_LIVE | Positive values route every sort_by=submitted, descending query to the live API; 0 disables the bypass. | 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 | Abort budget for one scheduled refresh subprocess (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. The server declares stateless in src/index.ts β it holds no per-session state β so every run path resolves the same way unless this variable overrides it. | 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.
Running the server
Local development
-
Build and run:
bun run rebuild
bun run start:http
bun run start:stdio
-
Run checks and tests:
bun run devcheck
bun run test
Optional: local mirror
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 and arxiv_get_metadata by serving from a SQLite + FTS5 store harvested via OAI-PMH. arxiv_read_paper always uses the live API β full-content harvesting is against arXiv's data policy.
Disabled by default. To enable:
bun run mirror:init
export ARXIV_MIRROR_ENABLED=true
bun run start:http
Keep it current with bun run mirror:refresh (wire to cron/systemd/launchd, or set ARXIV_MIRROR_REFRESH_CRON to schedule it in-process in HTTP mode) and check integrity with bun run mirror:verify. A newer server migrates an existing mirror's schema in place on first open β never a re-harvest β and an upgrade that rebuilds the full-text index makes that first start noticeably slower on a full-corpus mirror; mirror:verify reports the schema version and exits non-zero if a migration didn't complete.
FTS5 BM25 ranking differs from arXiv's own relevance ranking, so sort_by=relevance returns a different top-K against the mirror than against the live API. The mirror serves only the latest version of each paper β a version-pinned request falls through to the live API. A stale or failed refresh keeps serving the last completed harvest rather than dropping to the live API mid-request.
Docker
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.
Project structure
| 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). |
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). |
tests/ | Unit and integration tests. |
docs/ | Design document and directory 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:
bun run devcheck
bun run test
License
Apache-2.0 β see LICENSE for details.