# Directory Structure
```
├── .gitignore
├── bin
│   └── .keep
├── deno.json
├── deno.lock
├── images
│   └── sample_use.png
├── LICENSE
├── README.md
└── src
    ├── main.test.ts
    └── main.ts
```
# Files
--------------------------------------------------------------------------------
/bin/.keep:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# build output
bin/arxiv-search-mcp
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
[](https://mseep.ai/app/danimal141-arxiv-search-mcp)
# arXiv Search MCP Server
An MCP server that provides tools to search and fetch papers from arXiv.org.
## Features
- Search papers by category
- Get latest papers sorted by submission date
- Formatted output with title, authors, summary, and link
## Development
### Prerequisites
- [Deno](https://deno.land/) installed on your system
- MCP compatible environment
### Setup
1. Clone the repository
2. Install dependencies:
```bash
deno cache --reload src/main.ts
```
### Running the Server
Development mode with file watching:
```bash
deno task dev
```
Build executable:
```bash
deno task compile
```
## Integration with Claude Desktop
Add the following configuration to your `claude_desktop_config.json`:
```json
{
  "mcpServers": {
    "arxiv-search-mcp": {
      "command": "/path/to/dir/arxiv-search-mcp/bin/arxiv-search-mcp"
    }
  }
}
```
Replace `/path/to/dir` with the actual path to your compiled binary.
## Usage
Example usage screenshot:

The server provides a tool named `search_arxiv` that accepts the following parameters:
```typescript
{
  "category": string,    // arXiv category (e.g., cs.AI, cs.LG, astro-ph)
  "max_results": number  // Number of papers to fetch (1-100, default: 5)
}
```
### Example
Request:
```json
{
  "category": "cs.AI",
  "max_results": 5
}
```
This will return the 5 most recent papers from the Artificial Intelligence category.
### Available Categories
Some popular arXiv categories:
- `cs.AI`: Artificial Intelligence
- `cs.LG`: Machine Learning
- `cs.CL`: Computation and Language
- `cs.CV`: Computer Vision
- `cs.NE`: Neural and Evolutionary Computing
- `cs.RO`: Robotics
- `astro-ph`: Astrophysics
- `physics`: Physics
- `math`: Mathematics
- `q-bio`: Quantitative Biology
For a complete list of categories, visit [arXiv taxonomy](https://arxiv.org/category_taxonomy).
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
```json
{
  "tasks": {
    "dev": "deno run --watch --allow-read --allow-env --allow-net src/main.ts",
    "test": "deno test --allow-read --allow-env --allow-net",
    "compile": "deno compile --allow-read --allow-env --allow-net --output=./bin/arxiv-search-mcp src/main.ts"
  },
  "imports": {
    "@std/assert": "jsr:@std/assert@1",
    "fastmcp": "npm:fastmcp@^1.21.0",
    "zod": "npm:zod@^3.24.2",
    "fast-xml-parser": "npm:[email protected]"
  }
}
```
--------------------------------------------------------------------------------
/src/main.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
 * Unit tests for arXiv Search MCP Server
 */
import { assertEquals, assertMatch } from "@std/assert";
import {
  parseArxivEntry,
  SearchArxivParamsZod,
  search_arxiv_execute,
  type XMLFeedEntry,
} from "./main.ts";
// Testing utilities
const mockXMLFeedEntry: XMLFeedEntry = {
  title: "Test Paper Title",
  author: [
    { name: "John Doe" },
    { name: "Jane Smith" },
  ],
  summary: "This is a test paper summary",
  id: "https://arxiv.org/abs/test.123",
};
const mockXMLFeedSingleAuthor: XMLFeedEntry = {
  title: "Single Author Paper",
  author: { name: "Solo Author" },
  summary: "Paper with single author",
  id: "https://arxiv.org/abs/test.456",
};
Deno.test("parseArxivEntry handles multiple authors correctly", () => {
  const result = parseArxivEntry(mockXMLFeedEntry);
  assertEquals(result, {
    title: "Test Paper Title",
    authors: "John Doe, Jane Smith",
    summary: "This is a test paper summary",
    link: "https://arxiv.org/abs/test.123",
  });
});
Deno.test("parseArxivEntry handles single author correctly", () => {
  const result = parseArxivEntry(mockXMLFeedSingleAuthor);
  assertEquals(result, {
    title: "Single Author Paper",
    authors: "Solo Author",
    summary: "Paper with single author",
    link: "https://arxiv.org/abs/test.456",
  });
});
Deno.test("SearchArxivParamsZod validates category correctly", () => {
  const validResult = SearchArxivParamsZod.safeParse({
    category: "cs.AI",
    max_results: 5,
  });
  assertEquals(validResult.success, true);
});
Deno.test("SearchArxivParamsZod validates max_results range", () => {
  // Test minimum value
  const tooSmall = SearchArxivParamsZod.safeParse({
    category: "cs.AI",
    max_results: 0,
  });
  assertEquals(tooSmall.success, false);
  // Test maximum value
  const tooLarge = SearchArxivParamsZod.safeParse({
    category: "cs.AI",
    max_results: 101,
  });
  assertEquals(tooLarge.success, false);
  // Test valid range
  const validRange = SearchArxivParamsZod.safeParse({
    category: "cs.AI",
    max_results: 100,
  });
  assertEquals(validRange.success, true);
});
Deno.test("SearchArxivParamsZod provides default max_results", () => {
  const result = SearchArxivParamsZod.parse({
    category: "cs.AI",
  });
  assertEquals(result.max_results, 5);
});
// Integration test with mocked fetch
Deno.test("search_arxiv tool handles API errors gracefully", async () => {
  const originalFetch = globalThis.fetch;
  globalThis.fetch = async () => {
    throw new Error("Network error");
  };
  try {
    const result = await search_arxiv_execute({
      category: "cs.AI",
      max_results: 5,
    });
    assertMatch(result as string, /Error during search: Network error/);
  } finally {
    globalThis.fetch = originalFetch;
  }
});
```
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
```typescript
/**
 * arXiv Search MCP Server
 */
import { FastMCP } from "fastmcp";
import { z } from "zod";
import { XMLParser } from "fast-xml-parser";
// Type definitions for XML parsing
export interface XMLFeedEntry {
  title: string;
  author: { name: string }[] | { name: string };
  summary: string;
  id: string;
}
interface XMLResponse {
  feed: {
    entry: XMLFeedEntry[];
  };
}
// Constants
const ARXIV_API_BASE = "https://export.arxiv.org/api/query?";
const USER_AGENT = "arxiv-mcp/1.0";
// Schema definition
export const SearchArxivParamsZod = z.object({
  category: z.string().describe("arXiv category (e.g., cs.LG, astro-ph)"),
  max_results: z.number().min(1).max(100).default(5).describe(
    "Number of papers to fetch (1-100)",
  ),
});
export interface ArxivEntry {
  title: string;
  authors: string;
  summary: string;
  link: string;
}
// Helper function for parsing XML entries
export function parseArxivEntry(entry: XMLFeedEntry): ArxivEntry {
  const authors = Array.isArray(entry.author)
    ? entry.author.map((a) => a.name).join(", ")
    : entry.author.name;
  return {
    title: entry.title,
    authors,
    summary: entry.summary,
    link: entry.id,
  };
}
// Tool execution function
export async function search_arxiv_execute(args: z.infer<typeof SearchArxivParamsZod>) {
  try {
    // Build API request
    const query = new URLSearchParams({
      search_query: `cat:${args.category}`,
      sortBy: "submittedDate",
      sortOrder: "descending",
      max_results: args.max_results.toString(),
    });
    const response = await fetch(`${ARXIV_API_BASE}${query}`, {
      headers: {
        "User-Agent": USER_AGENT,
        "Accept": "application/xml",
      },
    });
    if (!response.ok) {
      throw new Error(`API request failed: ${response.status}`);
    }
    const xmlText = await response.text();
    // Debug output
    console.error("Raw XML:", xmlText);
    const parser = new XMLParser({
      ignoreAttributes: false,
      attributeNamePrefix: "@_",
    });
    const xmlDoc = parser.parse(xmlText) as XMLResponse;
    // Debug output
    console.error("Parsed XML:", JSON.stringify(xmlDoc, null, 2));
    if (!xmlDoc || !xmlDoc.feed) {
      throw new Error("Failed to parse XML response");
    }
    // Parse entries
    const entries = Array.isArray(xmlDoc.feed.entry)
      ? xmlDoc.feed.entry.map(parseArxivEntry)
      : xmlDoc.feed.entry
      ? [parseArxivEntry(xmlDoc.feed.entry)]
      : [];
    if (entries.length === 0) {
      return "No papers found for the specified category.";
    }
    // Format results
    const formattedPapers = entries.map((paper: ArxivEntry) =>
      `Title: ${paper.title}\nAuthors: ${paper.authors}\nSummary: ${paper.summary}\nLink: ${paper.link}`
    );
    return formattedPapers.join("\n\n---\n\n");
  } catch (error: unknown) {
    console.error("Error in search_arxiv:", error);
    const errorMessage = error instanceof Error
      ? error.message
      : "An unknown error occurred";
    return `Error during search: ${errorMessage}`;
  }
}
const server = new FastMCP({
  name: "arXiv-Search",
  version: "1.0.0",
});
server.addTool({
  name: "search_arxiv",
  description: "Search latest papers from a specific arXiv category",
  parameters: SearchArxivParamsZod,
  execute: search_arxiv_execute,
});
server.start({
  transportType: "stdio",
});
```