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:
- 2 Tools:
search_papers,extract_info - 2 Resources:
papers://folders,papers://{topic} - 1 Prompt:
generate_search_prompt
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:
- View all available tools, prompts, and resources
- Test them independently
- Inspect schemas and responses
This helps separate concerns:
- Is the MCP server working? → Test in Inspector
- Is the AI agent working? → Test the client
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.