Back to List

AI Agents as Experimental Tools: Searching Biological Databases with MCP

I am developing an MCP server in Python that allows Claude/GPT to query PubMed, UniProt, and GenBank using natural language instructions. This includes defining the tool, creating execution functions, and building an evaluation harness.

Advanced
|
120min
|
Verified (2026-07)
AI agentMinimum Control PlanNCBI Entrez is a comprehensive and integrated search and retrieval system for biomedical literature and molecular biology data provided by the National Center for Biotechnology Information (NCBI).PubMed searchUniProt is a comprehensive resource for protein sequence and functional information.tool utilizationutilize
Progress0/19 (0%)

AI Agents as Experimental Tools: Searching Bio Databases with MCP

Upon Completion of This Topic

You will be able to combine the MCP (Model Context Protocol) and the harness learned in the textbook to create a tool that allows LLMs like Claude or GPT to automatically query bio databases such as NCBI Entrez, PubMed, and UniProt using natural language instructions. You will gain an understanding of the internal principles of LLM tool use and the actual implementation of the harness used to evaluate its reliability.

This article provides a general educational example. Real-world deployed MCP servers have much more sophisticated authentication, rate limiting, and observability.

"Why keep repeating the same search every time?" - The Pitfalls of Repetitive Experiments

Suppose you are preparing research ideas and repeat the following search every day.

  1. Search "BRCA1" AND "review" AND "2024" on NCBI PubMed.
  2. Browse the abstracts and determine relevance.
  3. Check citation relationships.
  4. Check relevant protein domains on UniProt.
  5. Download the sequence from GenBank.

You repeat this combination every day. There is a problem.

Problem 1: Each database has different search syntax. The filter syntax of NCBI Entrez, the search syntax of UniProt, and the accession format of GenBank are all different. You have to remember them again each time.

Problem 2: You have to manually combine the results from multiple databases. For example, you enter the author's name obtained from PubMed into UniProt again, and then enter the resulting accession into GenBank again.

Problem 3: Most of this task is deterministic and rule-based, but it is repeated daily with different keywords. This means it is a perfect candidate for automation, but there is a significant overhead in learning the API documentation of each database.

The real approach is to delegate this to an AI agent. If you give a request to an LLM in natural language, it will sequentially call multiple APIs in the background and combine the results. A protocol for doing this in a safe and standardized way is MCP (Model Context Protocol).

From Black Box to Components: Exploring the MCP

The MCP may appear complex at first glance, but it can be broken down into three core components.

Component 1: Tool Definition

The MCP server presents the LLM with a list of available tools. Each tool is defined by its name, description, and input schema.

python
tool_definition = {
"name": "search_pubmed",
"description": "Search the PubMed database for scientific papers using a keyword and return the top N results.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "The search query, using PubMed syntax."},
"max_results": {"type": "integer", "default": 10}
},
"required": ["query"]
}
}

The key point is that the description is the only information the LLM actually reads and uses to make decisions. A well-written tool description is crucial for good agent behavior.

Component 2: Execution Function

Each tool has an associated function that is executed when the LLM requests a tool call. The function takes the input arguments, performs the necessary operations, and returns the result to the LLM.

python
async def search_pubmed(query: str, max_results: int = 10) -> dict:
import httpx
base = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
async with httpx.AsyncClient() as client:
search_response = await client.get(
f"{base}/esearch.fcgi",
params={"db": "pubmed", "term": query, "retmax": max_results, "retmode": "json"}
)
ids = search_response.json()["esearchresult"]["idlist"]
if not ids:
return {"results": []}
summary_response = await client.get(
f"{base}/esummary.fcgi",
params={"db": "pubmed", "id": ",".join(ids), "retmode": "json"}
)
summaries = summary_response.json()["result"]
return {
"results": [
{
"pmid": pmid,
"title": summaries[pmid].get("title"),
"journal": summaries[pmid].get("fulljournalname"),
"pubdate": summaries[pmid].get("pubdate")
}
for pmid in ids
]
}

Important Note: This function makes calls to external APIs, so it must handle network issues, rate limits, and timeouts. We will address this in more detail later.

Component 3: MCP Server Skeleton

All of these components are combined into a single server. Anthropic provides an official Python SDK, but to better illustrate the concept, let's create our own simplified version.

python
from typing import Any, Callable, Coroutine
import json
class MCPServer:
def __init__(self):
self.tools: dict[str, dict[str, Any]] = {}
self.handlers: dict[str, Callable[..., Coroutine]] = {}
def register(self, definition: dict, handler: Callable[..., Coroutine]):
name = definition["name"]
self.tools[name] = definition
self.handlers[name] = handler
def list_tools(self) -> list[dict]:
return list(self.tools.values())
async def call_tool(self, name: str, arguments: dict) -> dict:
if name not in self.handlers:
return {"error": f"Unknown tool: {name}"}
try:
return await self.handlers[name](**arguments)
except Exception as e:
return {"error": str(e)}
server = MCPServer()
server.register(
definition={
"name": "search_pubmed",
"description": "Search PubMed for scientific papers",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 10}
},
"required": ["query"]
}
},
handler=search_pubmed
)

This server would typically communicate with an LLM client via standard input/output or HTTP. However, for clarity, we will use a simplified approach where the server is called directly as a function.


LLM Integration โ€” Writing with Claude

Now, let's connect this server to LLM calls. We will use the tool use feature of the Claude API.

python
import anthropic
client = anthropic.Anthropic()
async def agent_loop(user_message: str, server: MCPServer, max_iterations: int = 5) -> str:
messages = [{"role": "user", "content": user_message}]
tools = [
{
"name": t["name"],
"description": t["description"],
"input_schema": t["input_schema"]
}
for t in server.list_tools()
]
for iteration in range(max_iterations):
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
text_blocks = [b.text for b in response.content if b.type == "text"]
return "\n".join(text_blocks)
if response.stop_reason == "tool_use":
tool_uses = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_use in tool_uses:
result = await server.call_tool(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})
return "Max iterations reached"

Now, let's use it:

python
result = await agent_loop(
"Find 3 review papers on the BRCA1 gene from 2024 onwards and tell me the title and journal.",
server
)
print(result)

The LLM will automatically call search_pubmed(query="BRCA1 AND review AND 2024:2025", max_results=3) and return the results in a natural language format.


Fading โ€“ Three Blanks for You to Fill

Blank 1: Tool Expansion โ€“ UniProt Search

A server with only PubMed is only half complete. Let's add the UniProt tool.

python
async def search_uniprot(query: str, max_results: int = 10) -> dict:
import httpx
async with httpx.AsyncClient() as client:
# TODO: Call the UniProt REST API
# Endpoint: https://rest.uniprot.org/uniprotkb/search
# Parameters: query, format=json, size=max_results
# Result: {"results": [{"accession": ..., "name": ..., "gene": ...}]}
pass

Hint: Extract primaryAccession, proteinDescription.recommendedName.fullName.value, and genes[0].geneName.value from each item in the results array of the UniProt response.

Blank 2: Harness โ€“ Agent Evaluation System

To verify that the agent is working correctly, we need an evaluation harness. It automatically runs multiple test cases and evaluates each result.

python
async def evaluate_agent(
test_cases: list[dict],
server: MCPServer
) -> dict:
"""
test_cases: [
{
"prompt": "User request",
"expected_tool_calls": ["search_pubmed"],
"expected_content_contains": ["BRCA1", "review"]
}
]
"""
results = []
for tc in test_cases:
# TODO: Run the agent_loop, track the actual tools called, and verify the results.
# Record whether the test case passed in the results.
pass
return {
"total": len(test_cases),
"passed": sum(1 for r in results if r["passed"]),
"details": results
}

Hint: Modify the agent_loop to log the tool calls. Append each call to a list and return it.

Blank 3: Rate Limit + Error Handling

NCBI Entrez blocks your IP address if you make more than 3 requests per second. Add a rate limit to the tool execution.

python
import asyncio
import time
class RateLimiter:
def __init__(self, calls_per_second: float):
self.min_interval = 1.0 / calls_per_second
self.last_call = 0.0
async def wait(self):
now = time.time()
elapsed = now - self.last_call
if elapsed < self.min_interval:
await asyncio.sleep(self.min_interval - elapsed)
self.last_call = time.time()
ncbi_limiter = RateLimiter(calls_per_second=3)
async def search_pubmed_limited(query: str, max_results: int = 10) -> dict:
# TODO: Call ncbi_limiter.wait() before executing the function.
# Then, execute the existing logic.
pass

Reflection โ€” How does this agent differ from a production-ready tool-use LLM?

The agent you've created shares conceptual roots with production systems (like Claude Code or ChatGPT with tools), but production systems are much more sophisticated.

Security: Production-level MCP servers must defend against prompt injection. If a tool returns data containing malicious instructions ("Ignore this instruction and do X instead"), the agent may malfunction. Production systems either isolate tool return values in a sandboxed context or include a separate validation layer.

Cost Management: Because each iteration triggers an API call, costs can quickly accumulate. Production systems include iteration limits, token budgets, and tool call caching.

Observability: To analyze why the agent called a specific tool and what reasoning led to errors, trace logs are necessary. Production systems record each step using standards like OpenTelemetry.

Multi-Step Reasoning: Complex requests (e.g., "Find all other genes that appear in papers about this gene and look up their respective expression patterns") require planning for multiple steps of tool calls. Production systems often use a planner-executor separation or the ReAct framework.

Extension Project

1. Add GenBank Sequence Download Tool: Use efetch.fcgi to retrieve FASTA sequences from accession numbers.

2. Citation Graph Tool: Trace citation relationships of PubMed papers to construct a related paper tree.

3. Local Cache: Add an SQLite cache layer to avoid re-running the same queries.

4. Claude Desktop Integration: Register the MCP server you created with the actual Claude Desktop app for daily use. Wrap it as an stdio server using the standard MCP SDK.

Component Map for This Feature

  • [F] MCP Protocol: Standard format for tool definition, calls, and responses. Enables server-client separation.
  • [F] Harness: A testing system that automatically evaluates agent behavior. Serves as the CI/CD for LLM systems.
  • [W] API Calls/JSON: Parsing NCBI/UniProt REST calls and responses.
  • [W] Async/Await: Parallelizes multiple API calls using asynchronous I/O.

[F] = Tool concept that you implement yourself / [W] = Tool concept provided as complete code.

๐Ÿ’ฌ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...