PaperShelf exposes an MCP (Model Context Protocol) server so Claude and other MCP clients can search arXiv, query the local paper library, save papers, and retrieve full paper content — all programmatically.
Inspired by arxbar, which pioneered this pattern as a menu bar + MCP server for arXiv.
┌───────────────────────────────────────────────┐
│ Electron Main Process │
│ │
│ ┌─────────────┐ ┌────────────────────────┐ │
│ │ App Window │ │ MCP HTTP Server │ │
│ │ (React UI) │ │ (127.0.0.1:3847) │ │
│ └──────┬───────┘ └──────────┬─────────────┘ │
│ │ │ │
│ ├─────────────────────┤ │
│ │ Shared Services │ │
│ │ ┌───────────────┐ │ │
│ │ │ arXiv client │ │ │
│ │ │ SQLite DB │ │ │
│ │ │ PDF processor │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
└───────────────────────────────────────────────┘
The Electron app runs both the UI and an MCP HTTP server on 127.0.0.1:3847. Both share the same database and services. MCP clients connect via the "url" config field — no stdio bridge needed.
@modelcontextprotocol/sdk— MCP protocol implementationStreamableHTTPServerTransport— HTTP server on 127.0.0.1:3847zod— tool parameter validation
src/main/
├── mcp/
│ ├── server.ts # MCP server creation + registration
│ ├── tools.ts # Tool definitions (search, fetch, save, etc.)
│ └── http-server.ts # HTTP transport (StreamableHTTPServerTransport)
├── arxiv/
│ ├── client.ts # Existing arXiv API client (refactored from arxiv-client.ts)
│ ├── parser.ts # XML parsing (extracted from client)
│ ├── html.ts # HTML→markdown fetcher (new, from arxbar)
│ ├── categories.ts # arXiv category definitions (new, from arxbar)
│ ├── rate-limiter.ts # 3-second rate limiting (new)
│ └── types.ts # ArXiv-specific types (moved from shared)
├── database.ts # Existing SQLite (unchanged)
├── pdf-processor.ts # Existing PDF download + extraction
├── ipc-handlers.ts # Existing IPC (unchanged)
├── preload.ts # Existing preload (unchanged)
└── index.ts # Updated: dual-mode entry point
The key refactor is extracting the existing arxiv-client.ts into a proper arxiv/ module with separated concerns, and adding the mcp/ module.
The Electron app starts normally and launches the MCP HTTP server from app.whenReady() if enabled in Settings.
Search arXiv papers by query. Reuses existing arXiv client.
Input:
{
query: string; // arXiv query (supports arXiv search syntax)
max_results?: number; // 1–100, default 10
sort_by?: 'relevance' | 'lastUpdatedDate' | 'submittedDate';
categories?: string[]; // Filter: ['cs.AI', 'cs.LG', ...]
}Output: Array of paper objects with id, title, authors, summary, dates, urls, categories.
Full-text search across saved papers using FTS5.
Input:
{
query: string; // FTS5 query
}Output: Array of library papers matching the query.
Get full details for a paper by arXiv ID or library ID.
Input:
{
id: string; // arXiv ID (e.g., "2301.12345") or library UUID
}Output: Full paper metadata including collections, tags, and availability info.
List papers in the library with optional filtering.
Input:
{
filter?: 'all' | 'favorites' | 'recent';
collection_id?: string;
tag_id?: string;
limit?: number; // Default 50
}Output: Array of library papers.
Save an arXiv paper to the local library. Downloads PDF and extracts text.
Input:
{
arxiv_id: string; // e.g., "2301.12345"
}Output:
{
success: boolean;
paper_id?: string; // Library UUID
already_exists?: boolean;
error?: string;
}Fetch the full paper as markdown from arXiv's HTML rendering. Much richer than the extracted PDF text — preserves structure, math, figures, and references.
Input:
{
arxiv_id: string;
}Output:
{
arxiv_id: string;
title: string;
markdown: string; // Full paper content as markdown
url: string; // arxiv.org/html/{id}
available: boolean; // false if no HTML rendering exists
}Implementation: Fetch https://arxiv.org/html/{arxiv_id}, convert HTML→markdown using Turndown. Preserve math elements. Same approach as arxbar.
Generate a BibTeX citation entry for a paper.
Input:
{
arxiv_id: string;
}Output:
{
bibtex: string; // Formatted BibTeX entry
}List all collections with paper counts.
Input: None.
Output: Array of { id, name, color, paper_count }.
List all tags with paper counts.
Input: None.
Output: Array of { id, name, color, paper_count }.
List all arXiv categories.
Input: None.
Output: Array of { id, name, group } (e.g., { id: "cs.AI", name: "Artificial Intelligence", group: "Computer Science" }).
arXiv requests are rate-limited to 1 request per 3 seconds (arXiv's policy). A shared RateLimiter instance is used by both MCP tools and the existing IPC handlers.
// src/main/arxiv/rate-limiter.ts
let lastRequestTime = 0;
const MIN_DELAY_MS = 3000;
export async function waitForRateLimit(): Promise<void> {
const elapsed = Date.now() - lastRequestTime;
if (elapsed < MIN_DELAY_MS) {
await new Promise(r => setTimeout(r, MIN_DELAY_MS - elapsed));
}
lastRequestTime = Date.now();
}Add to your MCP client config (e.g. Claude Desktop's claude_desktop_config.json):
{
"mcpServers": {
"papershelf": {
"url": "http://127.0.0.1:3847/mcp"
}
}
}Port 3847 in production, 13847 in development. PaperShelf must be running with the MCP server enabled in Settings.
All tools return JSON wrapped in MCP text content:
function textResult(data: unknown) {
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }]
};
}{
"@modelcontextprotocol/sdk": "^1.12.0",
"turndown": "^7.2.0",
"zod": "^3.24.0"
}axios is not needed — the existing codebase uses fetch and fast-xml-parser.
- Install deps —
@modelcontextprotocol/sdk,zod,turndown - Refactor arxiv client — Extract
arxiv-client.tsintoarxiv/module with rate limiter - Create
mcp/server.ts— Server setup, tool registration - Create
mcp/tools.ts— Implement all tools using existing services - Create
mcp/http-server.ts— HTTP transport on port 3847 - Create
arxiv/html.ts— HTML→markdown fetcher (port from arxbar) - Create
arxiv/categories.ts— Category definitions (port from arxbar) - Update
index.ts— Dual-mode entry point - Test with MCP inspector —
npx @modelcontextprotocol/inspector - Test with Claude Desktop — Configure and verify all tools