arxiv-mcp-server
arXiv論文検索と全文読解
ドキュメント
@cyanheads/arxiv-mcp-server
Search arXiv, fetch paper metadata, and read full-text content via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://arxiv.caseyjhand.com/mcp
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
| 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 |
Resources
| Resource | Description |
|---|---|
arxiv://paper/{paperId} | Paper metadata by arXiv ID |
arxiv://categories | Full arXiv category taxonomy |
Capability reference
arxiv_search_papers tool
- Query supports field prefixes
ti:,au:,abs:,cat:,co:,jr:,all:andAND/OR/ANDNOT, up to 1000 characters;categorytakes a code (cs.CL) or a whole archive (astro-ph,cs,math) - Query grammar follows arXiv's: space-separated terms are joined with
OR(writeANDto require every term), operators are uppercase only, and all three share one precedence grouped to the right (a AND b OR cisa 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 asofandtheoutside a quoted phrase - Sorts by
sort_by(relevance/submitted/updated) andsort_order, withsubmitted_from/submitted_toas inclusive UTCYYYY-MM-DDbounds; up to 50 results per call (a largermax_resultsreturns 50) - arXiv serves only the first 10,000 results of a query; a
startof 10,000 failsstart_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_idsfor the next call'spaper_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 optionalarXiv:prefix or an arxiv.orgabs/pdf/htmlURL - 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_servedis a superseded pre-2007 version arXiv's API answers HTTP 500 for, and itsdetailnames the latest version. With the local mirror,live_lookup_failedmarks an ID the mirror lacked whose live lookup failed; the papers the mirror served still come back - Fails
invalid_idwhen no ID is well-formed,no_matchwhen every ID misses,version_unavailablewhen a version is reachable only on the live API, andversion_not_servedwhen every ID is a version arXiv will not serve
arxiv_read_paper tool
- Tries arXiv HTML, then ar5iv, then PDF text extraction; the
sourcefield 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_idand carriesrevision_unconfirmed: true, unless the read named no version and arXiv's API reported v1 as the paper's only version;pdf_urlis exactly thepaper_idrevision max_charactersdefaults to 100,000 (passnullfor the whole paper); page withstart. The first page carries anoutlineof section headings with offsetspaper_idaccepts the same ID spellings asarxiv_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), orversion_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
groupfilter to narrow results - Static data — always succeeds
arxiv://paper/{paperId} resource
paperIdaccepts the same ID spellings asarxiv_get_metadata; percent-encode a legacy ID's slash (arxiv://paper/hep-th%2F9901001)- Fails
empty_id,invalid_id,no_match,version_unavailable, orversion_not_served
arxiv://categories resource
- Returns
{ categories: [...] }, a flat array withcode/name/groupper 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_papernames itssource;arxiv_search_papersechoes the effective queryarxiv_get_metadatareturns found papers with a typednot_found[]reason per miss- Typed
sourceandnot_found[].reasonenums 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:
{
"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
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
Installation
- Clone the repository:
git clone https://github.com/cyanheads/arxiv-mcp-server.git
- Navigate into the directory:
cd arxiv-mcp-server
- Install dependencies:
bun install
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). 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.
Running the server
Local development
-
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
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
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), 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. |
skills/arxiv-guide | Agent skill published to agents that install the server, and served over MCP: citation-to-ID resolution, search gates, version and section-by-section reading workflows. |
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, local mirror guide, and directory structure. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor 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.