Skip to content
← Back to Blog

Building the MCP Research Server with FastMCP and arXiv

2026-09-18

Building the MCP Research Server

This guide walks through building the MCP server that powers our research assistant. We'll use FastMCP for the server, arxiv.py for paper searches, and expose tools, prompts, and resources.

Previous: Understanding MCP: Tools, Prompts, and Resources ←

Project Structure

learn_mcp/
├── docker/
│   ├── Dockerfile.dev
│   ├── Dockerfile.prod
│   └── docker-compose.yml
├── src/
│   ├── llm_provider.py
│   ├── mcp_chatbot.py
│   └── research_server.py  ← Our MCP server
├── papers/                 ← Cached paper data
├── pyproject.toml
├── server_config.json
└── uv.lock

Step 1: Set Up the Project

Using uv for fast Python dependency management:

# Clone the repository
git clone https://github.com/nosadaniel/learn_ai.git
cd learn_ai/learn_mcp

# Install dependencies
uv sync

# Configure environment
cp .env.example .env

The project supports multiple LLM providers:

LLM_PROVIDER=ollama    # Default
LLM_MODEL=llama3.1

# Or use cloud providers:
# LLM_PROVIDER=anthropic
# LLM_MODEL=claude-3-7-sonnet-20250219
# ANTHROPIC_API_KEY=...

# LLM_PROVIDER=google_genai
# LLM_MODEL=gemini-2.0-flash
# GOOGLE_API_KEY=...

Step 2: Create the MCP Server

Initialize the Server

# src/research_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("research", host="0.0.0.0", port=8080)

Run with Streamable HTTP transport:

if __name__ == "__main__":
    mcp.run(transport='streamable-http')

This exposes the MCP endpoint at: http://localhost:8080/mcp

Step 3: Add MCP Tools

Tools let the AI perform actions.

Tool 1: Search Papers

@mcp.tool()
def search_papers(topic: str, max_results: int = 5) -> List[str]:
    """Search for papers on arXiv based on a topic and store their information."""
    
    client = arxiv.Client(page_size=min(max_results, 100))
    search = arxiv.Search(
        query=topic,
        max_results=max_results,
        sort_by=arxiv.SortCriterion.Relevance
    )
    
    papers = client.results(search)
    paper_ids = []
    
    for paper in papers:
        paper_id = paper.get_short_id()
        paper_ids.append(paper_id)
        
        # Save metadata locally
        # (title, authors, summary, pdf_url, published)
        # in papers/<topic>/papers_info.json
    
    return paper_ids

Tool 2: Extract Paper Info

@mcp.tool()
def extract_info(paper_id: str) -> str:
    """Retrieve cached information for a specific paper."""
    
    # Search across all saved topics
    for topic_dir in os.listdir("papers"):
        file_path = os.path.join("papers", topic_dir, "papers_info.json")
        if os.path.exists(file_path):
            papers_info = load_papers_info(file_path)
            if paper_id in papers_info:
                return papers_info[paper_id].model_dump_json(indent=2)
    
    return f"No information found for paper {paper_id}."

How They Work Together

User Query
  │
  ▼
search_papers()
  │
  ▼
paper IDs
  │
  ▼
extract_info()
  │
  ▼
paper metadata

The server provides a small research workflow, not just an API wrapper.

Step 4: Add MCP Resources

Resources let the AI browse or read information.

Resource 1: List Available Topics

@mcp.resource("papers://folders")
def get_available_folders() -> str:
    """List all available research topics."""
    
    folders = []
    if os.path.exists("papers"):
        for topic_dir in os.listdir("papers"):
            if os.path.isdir(os.path.join("papers", topic_dir)):
                folders.append(topic_dir)
    
    content = "# Available Topics\n\n"
    for folder in folders:
        content += f"- {folder}\n"
    
    return content

Access via URI: papers://folders

Resource 2: Read Papers for a Topic

@mcp.resource("papers://{topic}")
def get_topic_papers(topic: str) -> str:
    """Get detailed information about papers on a specific topic."""
    
    topic_dir = topic.lower().replace(" ", "_")
    papers_file = os.path.join("papers", topic_dir, "papers_info.json")
    
    if not os.path.exists(papers_file):
        return f"# No papers found for topic: {topic}"
    
    papers_data = load_papers_info(papers_file)
    
    content = f"# Papers on {topic.replace('_', ' ').title()}\n\n"
    content += f"Total papers: {len(papers_data)}\n\n"
    
    for paper_id, paper_info in papers_data.items():
        content += f"## {paper_info.title}\n"
        content += f"- **ID**: {paper_id}\n"
        content += f"- **Authors**: {', '.join(paper_info.authors)}\n"
        content += f"- **Published**: {paper_info.published}\n"
        content += f"- **PDF**: [{paper_info.pdf_url}]({paper_info.pdf_url})\n\n"
        content += f"### Summary\n{paper_info.summary[:500]}...\n\n"
        content += "---\n\n"
    
    return content

Access via URI: papers://machine_learning

Step 5: Add Reusable MCP Prompts

Prompts provide reusable workflows that tell the AI how to perform tasks.

Prompt 1: Research a Topic

@mcp.prompt()
def generate_search_prompt(topic: str, num_papers: int = 5) -> str:
    """Generate a prompt for researching a topic."""
    return f"""
    Search for {num_papers} academic papers about '{topic}' using the search_papers tool.
    
    For each paper found:
    1. Extract: title, authors, publication date, summary
    2. Identify: main contributions, methodologies, relevance to '{topic}'
    
    Then provide:
    - Overview of current research in '{topic}'
    - Common themes and trends
    - Key research gaps
    - Most impactful papers
    
    Organize findings with headings and bullet points.
    """

Step 6: Run the Server

uv run src/research_server.py

The server is now available at http://localhost:8080/mcp with:

Step 7: Test with MCP Inspector

Before connecting an AI agent, verify the server:

npx @modelcontextprotocol/inspector

Connect to: http://localhost:8080/mcp

The Inspector lets you:

This helps separate concerns:

Step 8: Connect an AI Client

The project uses LangChain with its MCP adapter:

# src/mcp_chatbot.py
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

# Configure servers in server_config.json
client = MultiServerMCPClient({"research": {"url": "http://localhost:8080/mcp", "transport": "http"}})

# Discover capabilities
tools = await client.get_tools()

# Create agent with LLM
agent = create_agent("ollama:llama3.2", tools)

# Ask a question
result = await agent.ainvoke({"messages": [
    {"role": "user", "content": "Find papers on AI security"}
]})

Run the chatbot:

uv run src/mcp_chatbot.py

The Agentic Tool Loop

When you ask: "Find papers on AI security"

User
  │
  │ "Find papers on AI security"
  ▼
AI Agent
  │
  │ decides research tool is useful
  ▼
MCP Client
  │
  │ MCP request
  ▼
Research MCP Server
  │
  │ search_papers()
  ▼
arXiv
  │
  │ results
  ▼
Research MCP Server
  │
  │ tool result
  ▼
MCP Client
  │
  ▼
AI Agent
  │
  ▼
Final answer

The model decides when a tool is appropriate, the MCP client executes the request, and the result returns to the model.


Next: Deploying the MCP Research Assistant →