PubMed MCP Server
by rikachu225
- src
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const PUBMED_BASE_URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
const DEFAULT_TOOL = 'pubmed-api';
const DEFAULT_EMAIL = 'default@example.com';
const RATE_LIMIT_DELAY = 334;
// Define interfaces
interface Article {
pmid: string;
title: string;
authors: string[];
publicationDate: string;
journal: string;
abstract: string | null;
url: string;
}
interface PubMedResponse {
esearchresult: {
idlist: string[];
count: string;
};
}
interface PubMedSummaryResponse {
result: {
[key: string]: {
title: string;
authors?: Array<{ name: string }>;
pubdate?: string;
source?: string;
abstract?: string;
};
};
}
// Define Zod schemas for validation
const SearchArgumentsSchema = z.object({
query: z.string(),
maxResults: z.number().default(10),
filterOpenAccess: z.boolean().default(true)
});
const LatestArticlesSchema = z.object({
topic: z.string(),
days: z.number().default(30),
maxResults: z.number().default(10)
});
type SearchArgs = z.infer<typeof SearchArgumentsSchema>;
type LatestArticlesArgs = z.infer<typeof LatestArticlesSchema>;
// Create server instance
const server = new Server(
{
name: "pubmed",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search",
description: "Search PubMed for research articles",
inputSchema: SearchArgumentsSchema
},
{
name: "getLatestArticles",
description: "Get recent articles on a topic",
inputSchema: LatestArticlesSchema
}
]
};
});
let lastRequestTime = 0;
async function enforceRateLimit(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < RATE_LIMIT_DELAY) {
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY - timeSinceLastRequest));
}
lastRequestTime = Date.now();
}
async function search({ query, maxResults = 10, filterOpenAccess = true }: SearchArgs) {
await enforceRateLimit();
try {
const searchQuery = filterOpenAccess ?
`(${query}) AND ("open access"[Filter])` : query;
const searchUrl = new URL(`${PUBMED_BASE_URL}/esearch.fcgi`);
searchUrl.searchParams.append('db', 'pubmed');
searchUrl.searchParams.append('term', searchQuery);
searchUrl.searchParams.append('retmax', maxResults.toString());
searchUrl.searchParams.append('retmode', 'json');
searchUrl.searchParams.append('tool', DEFAULT_TOOL);
searchUrl.searchParams.append('email', DEFAULT_EMAIL);
const response = await fetch(searchUrl.toString());
if (!response.ok) throw new Error(`PubMed search failed: ${response.statusText}`);
const data = await response.json() as PubMedResponse;
const ids = data.esearchresult.idlist;
if (!ids.length) {
return { content: [{ type: "text", text: "No results found" }] };
}
const articles = await fetchArticleDetails(ids);
const formattedArticles = articles.map(article =>
`Title: ${article.title}\n` +
`Authors: ${article.authors.join(', ')}\n` +
`Journal: ${article.journal}\n` +
`Date: ${article.publicationDate}\n` +
`URL: ${article.url}\n` +
(article.abstract ? `Abstract: ${article.abstract}\n` : '') +
'---\n'
).join('\n');
return {
content: [{
type: "text",
text: `Found ${articles.length} articles:\n\n${formattedArticles}`
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error searching PubMed: ${errorMessage}`
}]
};
}
}
async function fetchArticleDetails(ids: string[]): Promise<Article[]> {
await enforceRateLimit();
try {
const summaryUrl = new URL(`${PUBMED_BASE_URL}/esummary.fcgi`);
summaryUrl.searchParams.append('db', 'pubmed');
summaryUrl.searchParams.append('id', ids.join(','));
summaryUrl.searchParams.append('retmode', 'json');
summaryUrl.searchParams.append('tool', DEFAULT_TOOL);
summaryUrl.searchParams.append('email', DEFAULT_EMAIL);
const response = await fetch(summaryUrl.toString());
if (!response.ok) throw new Error(`Failed to fetch article details: ${response.statusText}`);
const data = await response.json() as PubMedSummaryResponse;
return ids.map(id => {
const article = data.result[id];
return {
pmid: id,
title: article.title || 'No title',
authors: article.authors?.map(author => author.name) || [],
publicationDate: article.pubdate || 'No date',
journal: article.source || 'No journal',
abstract: article.abstract || null,
url: `https://pubmed.ncbi.nlm.nih.gov/${id}/`
};
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch article details: ${errorMessage}`);
}
}
function getDateFilter(days: number): string {
const date = new Date();
date.setDate(date.getDate() - days);
const formattedDate = date.toISOString().split('T')[0];
return `"${formattedDate}"[Date - Publication] : "3000"[Date - Publication]`;
}
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search':
const searchArgs = SearchArgumentsSchema.parse(args);
return await search(searchArgs);
case 'getLatestArticles':
const { topic, days, maxResults } = LatestArticlesSchema.parse(args);
const dateFilter = getDateFilter(days);
const query = `${topic} AND ${dateFilter}`;
return await search({ query, maxResults, filterOpenAccess: true }); // Added filterOpenAccess parameter
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("PubMed MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});