pubmed_mcp_server2
Version:
Advanced Model Context Protocol server for PubMed database access with MeSH optimization and citation analysis
884 lines (882 loc) • 39.1 kB
JavaScript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { searchAndFetchArticles, getFullAbstract, getFullText, searchPubMed, getArticleDetails, exportRIS, getCitationCounts, findSimilarArticles, batchProcess, countResults, convertIds } from "./pubmed-api.js";
// Create MCP server
const server = new McpServer({
name: "pubmed-mcp-server",
version: "1.1.3"
});
// Tool: Search PubMed articles
server.registerTool("search_pubmed", {
title: "Search PubMed",
description: "Search PubMed database for biomedical literature. Supports pagination, sorting, output modes, and structured filters.",
inputSchema: {
query: z.string().describe("Search query for PubMed database"),
maxResults: z.number().optional().default(10).describe("Maximum number of results to return (default: 10, max: 100)"),
retstart: z.number().optional().default(0).describe("Starting index for pagination (0-indexed). Use with maxResults for paging through results."),
sort: z.enum(["relevance", "pub_date", "first_author"]).optional().default("relevance").describe("Sort order: relevance (Best Match), pub_date (Most Recent), first_author"),
output_mode: z.enum(["compact", "full"]).optional().default("full").describe("Output format: compact (essential fields), full (detailed with abstracts). Use count_results tool for count-only queries."),
filters: z.object({
publication_types: z.array(z.string()).optional().describe("Filter by publication types: Clinical Trial, Review, Meta-Analysis, etc."),
date_range: z.object({
start: z.string().optional().describe("Start date (YYYY or YYYY/MM/DD)"),
end: z.string().optional().describe("End date (YYYY or YYYY/MM/DD)")
}).optional().describe("Filter by publication date range"),
languages: z.array(z.string()).optional().describe("Filter by language codes: eng, jpn, ger, etc."),
has_abstract: z.boolean().optional().describe("Only include articles with abstracts"),
free_full_text: z.boolean().optional().describe("Only include free full text articles"),
humans_only: z.boolean().optional().describe("Only include human studies")
}).optional().describe("Structured filters for search refinement")
}
}, async ({ query, maxResults = 10, retstart = 0, sort = "relevance", output_mode = "full", filters }) => {
try {
// Limit maxResults to prevent abuse
const limitedMax = Math.min(maxResults, 100);
// First get search results to show total hit count
const searchResult = await searchPubMed(query, limitedMax, retstart, sort, filters);
if (searchResult.idList.length === 0) {
return {
content: [{
type: "text",
text: JSON.stringify({
total: searchResult.count,
showing: 0,
retstart: retstart,
message: `No articles found for query: "${query}"`,
queryTranslation: searchResult.queryTranslation
}, null, 2)
}]
};
}
// Get detailed article information
const articles = await getArticleDetails(searchResult.idList);
// Handle compact output mode
if (output_mode === "compact") {
const compactResults = articles.map(article => {
const firstAuthor = article.authors.length > 0
? article.authors[0] + (article.authors.length > 1 ? " et al." : "")
: "Unknown";
// Extract year from publication date
const yearMatch = article.publicationDate.match(/\d{4}/);
const year = yearMatch ? yearMatch[0] : "Unknown";
return {
pmid: article.pmid,
title: article.title,
authors: firstAuthor,
journal: article.journal,
year: year,
pmcid: article.pmcId,
doi: article.doi
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
total: searchResult.count,
showing: compactResults.length,
retstart: searchResult.retStart,
sort: sort,
results: compactResults,
next_start: searchResult.retStart + compactResults.length < searchResult.count
? searchResult.retStart + compactResults.length
: undefined
}, null, 2)
}]
};
}
// Full output mode (default) - original detailed format
const formattedResults = articles.map((article, index) => {
const authorsText = article.authors.length > 0
? article.authors.slice(0, 3).join(", ") + (article.authors.length > 3 ? ", et al." : "")
: "Unknown authors";
let result = `**${retstart + index + 1}. ${article.title}**\n`;
result += `Authors: ${authorsText}\n`;
result += `Journal: ${article.journal}\n`;
result += `Publication Date: ${article.publicationDate}\n`;
result += `PMID: ${article.pmid}\n`;
if (article.doi) {
result += `DOI: ${article.doi}\n`;
}
if (article.pmcId) {
result += `PMC ID: ${article.pmcId}\n`;
}
result += `URL: ${article.url}\n`;
if (article.abstract) {
const truncatedAbstract = article.abstract.length > 500
? article.abstract.substring(0, 500) + "..."
: article.abstract;
result += `\nAbstract: ${truncatedAbstract}\n`;
}
return result;
}).join("\n\n");
// Extract PMIDs for easy reference
const pmids = articles.map(a => a.pmid);
// Create search summary
let searchSummary = `Search Results Summary\n`;
searchSummary += `Query: "${query}"\n`;
searchSummary += `Total: ${searchResult.count.toLocaleString()} | Showing: ${retstart + 1}-${retstart + articles.length} | Sort: ${sort}\n`;
if (searchResult.queryTranslation) {
searchSummary += `Query translation: ${searchResult.queryTranslation}\n`;
}
searchSummary += `PMIDs: ${pmids.join(", ")}\n`;
// Add pagination info
if (searchResult.retStart + articles.length < searchResult.count) {
searchSummary += `\nNext page: retstart=${searchResult.retStart + articles.length}\n`;
}
return {
content: [{
type: "text",
text: `${searchSummary}\n\n${formattedResults}`
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error searching PubMed: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Get full abstract
server.registerTool("get_full_abstract", {
title: "Get Full Abstract",
description: "Get complete, untruncated abstracts for specific PubMed articles by their PMID(s). Useful when search results show truncated abstracts.",
inputSchema: {
pmids: z.array(z.string()).describe("Array of PubMed IDs (PMIDs) to get full abstracts for")
}
}, async ({ pmids }) => {
try {
if (pmids.length === 0) {
return {
content: [{
type: "text",
text: "No PMIDs provided"
}],
isError: true
};
}
// Limit to prevent abuse
const limitedPmids = pmids.slice(0, 20);
const abstracts = await getFullAbstract(limitedPmids);
if (abstracts.length === 0) {
return {
content: [{
type: "text",
text: `No abstracts found for PMIDs: ${limitedPmids.join(", ")}`
}]
};
}
// Format abstracts for display
const formattedResults = abstracts.map((article, index) => {
const authorsText = article.authors.length > 0
? article.authors.join(", ")
: "Unknown authors";
let result = `**${index + 1}. ${article.title}**\n`;
result += `Authors: ${authorsText}\n`;
result += `Journal: ${article.journal}\n`;
result += `Publication Date: ${article.publicationDate}\n`;
result += `PMID: ${article.pmid}\n`;
if (article.doi) {
result += `DOI: ${article.doi}\n`;
}
if (article.pmcId) {
result += `PMC ID: ${article.pmcId}\n`;
}
if (article.fullAbstract) {
result += `\n**Full Abstract:**\n${article.fullAbstract}\n`;
}
else {
result += `\nNo abstract available for this article.\n`;
}
return result;
}).join("\n\n");
return {
content: [{
type: "text",
text: `Full abstracts for PMIDs: ${limitedPmids.join(", ")}\n\n${formattedResults}`
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error fetching full abstracts: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Get full text from PMC
server.registerTool("get_full_text", {
title: "Get Full Text",
description: "Get full text of articles from PubMed Central (PMC) by PMC ID. Supports section filtering to reduce context usage.",
inputSchema: {
pmcIds: z.array(z.string()).describe("Array of PMC IDs (e.g., 'PMC1234567' or '1234567') to get full text for"),
sections: z.array(z.enum(["abstract", "introduction", "methods", "results", "discussion", "conclusions", "references", "all"]))
.optional()
.default(["all"])
.describe("Sections to extract: abstract, introduction, methods, results, discussion, conclusions, references, or all (default)")
}
}, async ({ pmcIds, sections = ["all"] }) => {
try {
if (pmcIds.length === 0) {
return {
content: [{
type: "text",
text: "No PMC IDs provided"
}],
isError: true
};
}
// Limit to prevent abuse
const limitedPmcIds = pmcIds.slice(0, 10);
const fullTexts = await getFullText(limitedPmcIds, sections);
if (fullTexts.length === 0) {
return {
content: [{
type: "text",
text: `No full texts found for PMC IDs: ${limitedPmcIds.join(", ")}\n\nNote: Full text is only available for open access PMC articles.`
}]
};
}
// Format full texts for display
const formattedResults = fullTexts.map((article, index) => {
let result = `**${index + 1}. ${article.title}**\n`;
result += `PMID: ${article.pmid}\n`;
result += `PMC ID: ${article.pmcId}\n`;
if (!sections.includes("all")) {
result += `Sections requested: ${sections.join(", ")}\n`;
}
result += `\n`;
// Show sections if available
if (article.sections.length > 0) {
article.sections.forEach((section) => {
result += `\n**${section.title}**\n${section.content}\n`;
});
}
else if (article.fullText) {
// Fallback to full text if sections are not properly parsed
result += `**Full Text:**\n${article.fullText}\n`;
}
else {
result += `No content extracted for requested sections.\n`;
}
return result;
}).join("\n\n");
return {
content: [{
type: "text",
text: `Full text for PMC IDs: ${limitedPmcIds.join(", ")}\n\n${formattedResults}`
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error fetching full texts: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Export citations in RIS format
server.registerTool("export_ris", {
title: "Export RIS Format",
description: "Export PubMed citations in RIS format for use with reference management software (Zotero, Mendeley, EndNote, etc.). Uses NCBI Literature Citation Exporter API.",
inputSchema: {
pmids: z.string().describe("Comma-separated list of PubMed IDs (PMIDs) to export in RIS format (e.g., '36038128, 30105375')")
}
}, async ({ pmids }) => {
try {
if (!pmids || pmids.trim().length === 0) {
return {
content: [{
type: "text",
text: "No PMIDs provided for RIS export"
}],
isError: true
};
}
// Parse comma-separated PMIDs
const pmidArray = pmids.split(',').map(id => id.trim()).filter(id => id.length > 0);
if (pmidArray.length === 0) {
return {
content: [{
type: "text",
text: "No valid PMIDs found in input"
}],
isError: true
};
}
// Limit to prevent abuse and respect API rate limits
const limitedPmids = pmidArray.slice(0, 50);
if (pmidArray.length > 50) {
console.warn(`Requested ${pmidArray.length} PMIDs, limiting to 50 for RIS export`);
}
const result = await exportRIS(limitedPmids);
if (result.successCount === 0) {
return {
content: [{
type: "text",
text: `Failed to export any citations in RIS format for PMIDs: ${limitedPmids.join(", ")}\n\nErrors:\n${result.errors.join("\n")}`
}],
isError: true
};
}
// Format the response
let responseText = `**RIS Export Results**\n\n`;
responseText += `Total PMIDs requested: ${limitedPmids.length}\n`;
responseText += `Successfully exported: ${result.successCount}\n`;
if (result.errorCount > 0) {
responseText += `Failed: ${result.errorCount}\n`;
responseText += `\nErrors:\n${result.errors.join("\n")}\n`;
}
responseText += `\n`;
responseText += `**RIS FORMAT DATA**\n`;
responseText += `\n`;
responseText += result.risData;
responseText += `\n`;
responseText += `**Usage Instructions:**\n`;
responseText += `1. Copy the RIS format data above\n`;
responseText += `2. Save as a .ris file (e.g., "citations.ris")\n`;
responseText += `3. Import into your reference manager:\n`;
responseText += ` - Zotero: File → Import\n`;
responseText += ` - Mendeley: File → Import → RIS\n`;
responseText += ` - EndNote: File → Import → File\n`;
responseText += ` - Papers: Import → From File\n`;
responseText += `\nInput format: comma-separated PMIDs (e.g., "36038128, 30105375")\n`;
return {
content: [{
type: "text",
text: responseText
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error exporting RIS format: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Get citation counts
server.registerTool("get_citation_counts", {
title: "Get Citation Counts",
description: "Get citation counts for specific PubMed articles using NCBI elink API. Shows how many times each article has been cited by other PubMed articles.",
inputSchema: {
pmids: z.string().describe("Comma-separated list of PubMed IDs (PMIDs) to get citation counts for (e.g., '36038128, 30105375')")
}
}, async ({ pmids }) => {
try {
if (!pmids || pmids.trim().length === 0) {
return {
content: [{
type: "text",
text: "No PMIDs provided for citation count analysis"
}],
isError: true
};
}
// Parse comma-separated PMIDs
const pmidArray = pmids.split(',').map(id => id.trim()).filter(id => id.length > 0);
if (pmidArray.length === 0) {
return {
content: [{
type: "text",
text: "No valid PMIDs found in input"
}],
isError: true
};
}
// Limit to prevent abuse and respect API rate limits
const limitedPmids = pmidArray.slice(0, 20);
if (pmidArray.length > 20) {
console.warn(`Requested ${pmidArray.length} PMIDs, limiting to 20 for citation count analysis`);
}
const results = await getCitationCounts(limitedPmids);
if (results.length === 0) {
return {
content: [{
type: "text",
text: `No citation data found for PMIDs: ${limitedPmids.join(", ")}`
}]
};
}
// Format the response
let responseText = `**Citation Count Analysis**\n\n`;
responseText += `Analyzed PMIDs: ${limitedPmids.length}\n`;
// Calculate total citations
const totalCitations = results.reduce((sum, result) => sum + result.citationCount, 0);
responseText += `Total citations found: ${totalCitations}\n`;
responseText += `\n`;
// Format individual results
const formattedResults = results.map((result, index) => {
let text = `**${index + 1}. ${result.title}**\n`;
text += `PMID: ${result.pmid}\n`;
text += `Citations: **${result.citationCount}**\n`;
if (result.error) {
text += `Error: ${result.error}\n`;
}
else if (result.citationCount > 0) {
text += `\nTop citing PMIDs (first 10):\n`;
const topCitingPmids = result.citingPmids.slice(0, 10);
topCitingPmids.forEach((citingPmid, citingIndex) => {
text += ` ${citingIndex + 1}. PMID: ${citingPmid}\n`;
});
if (result.citingPmids.length > 10) {
text += ` ... and ${result.citingPmids.length - 10} more citing articles\n`;
}
}
else {
text += `No citations found in PubMed database\n`;
}
return text;
}).join("\n\n");
responseText += formattedResults;
responseText += `\n`;
responseText += `**Notes:**\n`;
responseText += `• Citation counts are based on PubMed database only\n`;
responseText += `• May not include all citations from other databases\n`;
responseText += `• Data is updated periodically by NCBI\n`;
responseText += `• Analysis limited to 20 PMIDs per request\n`;
responseText += `\nInput format: comma-separated PMIDs (e.g., "36038128, 30105375")\n`;
return {
content: [{
type: "text",
text: responseText
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error getting citation counts: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Find similar articles
server.registerTool("find_similar_articles", {
title: "Find Similar Articles",
description: "Find articles similar to a given PubMed article using NCBI's similarity algorithm. Returns articles ranked by relevance with similarity scores.",
inputSchema: {
pmid: z.string().describe("PubMed ID (PMID) of the reference article"),
maxResults: z.number().optional().default(10).describe("Maximum number of similar articles to return (default: 10, max: 50)")
}
}, async ({ pmid, maxResults = 10 }) => {
try {
if (!pmid || pmid.trim().length === 0) {
return {
content: [{
type: "text",
text: "No PMID provided for similarity search"
}],
isError: true
};
}
// Limit maxResults to prevent abuse
const limitedMax = Math.min(maxResults, 50);
// Get similar articles
const similarArticles = await findSimilarArticles(pmid.trim(), limitedMax);
if (similarArticles.length === 0) {
return {
content: [{
type: "text",
text: `No similar articles found for PMID: ${pmid}\n\nNote: This could be due to:\n1. Invalid PMID\n2. Very recent article not yet indexed\n3. Article type not suitable for similarity matching`
}]
};
}
// First, get the original article details for reference
const originalArticle = await getArticleDetails([pmid]);
// Format the response
let responseText = `**Similar Articles Analysis**\n\n`;
if (originalArticle.length > 0) {
responseText += `**Reference Article:**\n`;
responseText += `Title: ${originalArticle[0].title}\n`;
responseText += `Authors: ${originalArticle[0].authors.slice(0, 3).join(", ")}${originalArticle[0].authors.length > 3 ? ", et al." : ""}\n`;
responseText += `PMID: ${pmid}\n`;
responseText += `\n\n`;
}
responseText += `**Found ${similarArticles.length} similar articles:**\n\n`;
// Format each similar article
const formattedResults = similarArticles.map((article, index) => {
const authorsText = article.authors.length > 0
? article.authors.slice(0, 3).join(", ") + (article.authors.length > 3 ? ", et al." : "")
: "Unknown authors";
let result = `**${index + 1}. ${article.title}**\n`;
if (article.similarityScore !== undefined && article.similarityScore !== null) {
result += `Similarity Score: ${article.similarityScore.toFixed(2)}\n`;
}
result += `Authors: ${authorsText}\n`;
result += `Journal: ${article.journal}\n`;
result += `Publication Date: ${article.publicationDate}\n`;
result += `PMID: ${article.pmid}\n`;
if (article.doi) {
result += `DOI: ${article.doi}\n`;
}
if (article.pmcId) {
result += `PMC ID: ${article.pmcId}\n`;
}
result += `URL: https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/\n`;
if (article.abstract) {
const truncatedAbstract = article.abstract.length > 300
? article.abstract.substring(0, 300) + "... (Use get_full_abstract for complete abstract)"
: article.abstract;
result += `\nAbstract: ${truncatedAbstract}\n`;
}
return result;
}).join("\n\n");
responseText += formattedResults;
// Extract PMIDs for easy reference
const pmids = similarArticles.map(a => a.pmid);
responseText += `\n`;
responseText += `**Quick Reference:**\n`;
responseText += `PMIDs of similar articles: ${pmids.join(", ")}\n`;
responseText += `\n**Tips:**\n`;
responseText += `• Use get_full_abstract with PMIDs for complete abstracts\n`;
responseText += `• Use search_pubmed to explore specific topics from these articles\n`;
responseText += `• Higher similarity scores indicate stronger relevance\n`;
return {
content: [{
type: "text",
text: responseText
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error finding similar articles: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Count search results (lightweight)
server.registerTool("count_results", {
title: "Count Search Results",
description: "Get only the count of search results for a query without fetching article details. Useful for validating search queries before running full searches.",
inputSchema: {
query: z.string().describe("PubMed search query to count results for")
}
}, async ({ query }) => {
try {
const result = await countResults(query);
return {
content: [{
type: "text",
text: JSON.stringify({
query: result.query,
queryTranslation: result.queryTranslation,
count: result.count
}, null, 2)
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error counting results: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Convert IDs between PMID, PMCID, and DOI
server.registerTool("convert_ids", {
title: "Convert Article IDs",
description: "Convert between PMID, PMCID, and DOI identifiers using NCBI ID Converter API.",
inputSchema: {
ids: z.array(z.string()).describe("Array of IDs to convert"),
from_type: z.enum(["pmid", "pmcid", "doi"]).describe("Type of input IDs"),
to_type: z.enum(["pmid", "pmcid", "doi"]).describe("Type of output IDs")
}
}, async ({ ids, from_type, to_type }) => {
try {
if (ids.length === 0) {
return {
content: [{
type: "text",
text: "No IDs provided for conversion"
}],
isError: true
};
}
// Limit to prevent abuse
const limitedIds = ids.slice(0, 100);
const result = await convertIds(limitedIds, from_type, to_type);
return {
content: [{
type: "text",
text: JSON.stringify({
requested: limitedIds.length,
converted: result.conversions.length,
failed: result.failed.length,
conversions: result.conversions,
failedIds: result.failed
}, null, 2)
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error converting IDs: ${errorMessage}`
}],
isError: true
};
}
});
// Tool: Batch Processing
server.registerTool("batch_process", {
title: "Batch Process",
description: "Process multiple PMIDs with multiple operations efficiently. Supports abstracts, citations, similar articles, RIS export, and full text.",
inputSchema: {
pmids: z.union([
z.array(z.string()),
z.string()
]).describe("Array of PubMed IDs (PMIDs) to process, or space/comma-separated string (e.g., ['123', '456'] or '123 456 789' or '123,456,789')"),
operations: z.array(z.enum(["abstract", "citations", "similar", "ris_export", "full_text"])).describe("Operations to perform on each PMID"),
maxConcurrency: z.number().optional().default(3).describe("Maximum concurrent operations (default: 3)")
}
}, async ({ pmids, operations, maxConcurrency = 3 }) => {
try {
// Parse PMIDs from different input formats
let pmidArray = [];
if (typeof pmids === 'string') {
// Handle space or comma-separated string
pmidArray = pmids
.split(/[\s,]+/) // Split by spaces or commas
.map(pmid => pmid.trim())
.filter(pmid => pmid.length > 0);
}
else if (Array.isArray(pmids)) {
pmidArray = pmids.filter(pmid => pmid && pmid.trim().length > 0);
}
if (pmidArray.length === 0) {
return {
content: [{
type: "text",
text: "No valid PMIDs provided for batch processing"
}],
isError: true
};
}
if (!operations || operations.length === 0) {
return {
content: [{
type: "text",
text: "No operations specified for batch processing"
}],
isError: true
};
}
// Limit to prevent abuse
const limitedPmids = pmidArray.slice(0, 50); // Max 50 PMIDs
const limitedConcurrency = Math.min(maxConcurrency, 5); // Max 5 concurrent
if (pmidArray.length > 50) {
console.warn(`Requested ${pmidArray.length} PMIDs, limiting to 50 for batch processing`);
}
// Start batch processing
const result = await batchProcess(limitedPmids, operations, limitedConcurrency);
// Format the response
let responseText = `**Batch Processing Results**\n\n`;
responseText += `**Task ID**: ${result.taskId}\n`;
responseText += `**PMIDs processed**: ${limitedPmids.length}\n`;
responseText += `**Operations**: ${operations.join(", ")}\n\n`;
// Summary
responseText += `**Summary**:\n`;
responseText += `• Total operations: ${result.summary.total}\n`;
responseText += `• Completed: ${result.summary.completed}\n`;
responseText += `• Failed: ${result.summary.failed}\n`;
responseText += `• Success rate: ${((result.summary.completed / result.summary.total) * 100).toFixed(1)}%\n\n`;
responseText += `\n`;
// Results by operation type
if (result.results.abstracts && result.results.abstracts.length > 0) {
responseText += `**Abstracts Retrieved**: ${result.results.abstracts.length}\n`;
result.results.abstracts.slice(0, 3).forEach((abstract, index) => {
responseText += `${index + 1}. ${abstract.title} (PMID: ${abstract.pmid})\n`;
});
if (result.results.abstracts.length > 3) {
responseText += `... and ${result.results.abstracts.length - 3} more abstracts\n`;
}
responseText += `\n`;
}
if (result.results.citations && result.results.citations.length > 0) {
responseText += `**Citation Analysis**: ${result.results.citations.length} articles\n`;
const totalCitations = result.results.citations.reduce((sum, c) => sum + c.citationCount, 0);
responseText += `Total citations found: ${totalCitations}\n`;
const topCited = result.results.citations
.sort((a, b) => b.citationCount - a.citationCount)
.slice(0, 3);
topCited.forEach((citation, index) => {
responseText += `${index + 1}. ${citation.title}: ${citation.citationCount} citations\n`;
});
responseText += `\n`;
}
if (result.results.similar && Object.keys(result.results.similar).length > 0) {
responseText += `**Similar Articles**: Found for ${Object.keys(result.results.similar).length} PMIDs\n`;
Object.entries(result.results.similar).slice(0, 2).forEach(([pmid, similars]) => {
responseText += `PMID ${pmid}: ${similars.length} similar articles\n`;
});
responseText += `\n`;
}
if (result.results.risExports) {
responseText += `**RIS Export**: Generated for all processed PMIDs\n`;
responseText += `RIS data length: ${result.results.risExports.length} characters\n\n`;
}
if (result.results.fullTexts && result.results.fullTexts.length > 0) {
responseText += `**Full Texts Retrieved**: ${result.results.fullTexts.length}\n`;
result.results.fullTexts.forEach((fullText, index) => {
responseText += `${index + 1}. ${fullText.title} (PMC: ${fullText.pmcId})\n`;
});
responseText += `\n`;
}
// Error details if any
if (result.summary.failed > 0) {
responseText += `**Failed Operations**:\n`;
const failedOps = result.operations.filter(op => op.status === 'error');
failedOps.slice(0, 5).forEach((op, index) => {
responseText += `${index + 1}. PMID ${op.pmid} (${op.operation}): ${op.error}\n`;
});
if (failedOps.length > 5) {
responseText += `... and ${failedOps.length - 5} more errors\n`;
}
responseText += `\n`;
}
responseText += `\n`;
responseText += `**Tips:**\n`;
responseText += `• Use individual tools for detailed analysis of specific results\n`;
responseText += `• Check failed operations and retry with valid PMIDs\n`;
responseText += `• For large datasets, consider processing in smaller batches\n`;
return {
content: [{
type: "text",
text: responseText
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error in batch processing: ${errorMessage}`
}],
isError: true
};
}
});
// Resource: PubMed search results
server.registerResource("search-results", new ResourceTemplate("pubmed://search/{query}", { list: undefined }), {
title: "PubMed Search Results",
description: "Search results from PubMed database in JSON format",
mimeType: "application/json"
}, async (uri, params) => {
const { query } = params;
try {
const decodedQuery = decodeURIComponent(query);
const articles = await searchAndFetchArticles(decodedQuery, 10);
return {
contents: [{
uri: uri.href,
text: JSON.stringify(articles, null, 2),
mimeType: "application/json"
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
contents: [{
uri: uri.href,
text: JSON.stringify({ error: errorMessage }, null, 2),
mimeType: "application/json"
}]
};
}
});
// Prompt: Generate PubMed search query
server.registerPrompt("generate_search_query", {
title: "Generate PubMed Search Query",
description: "Help generate an effective PubMed search query based on research topic",
argsSchema: {
topic: z.string().describe("Research topic or question")
}
}, ({ topic }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Help me create an effective PubMed search query for the following research topic: "${topic}".
Please suggest:
1. Key search terms and medical subject headings (MeSH terms)
2. Boolean operators (AND, OR, NOT) to combine terms
3. Field tags if applicable (e.g., [ti] for title, [au] for author)
4. Filters that might be useful (e.g., publication date, article type)
Format the final query as a ready-to-use PubMed search string.`
}
}]
}));
// Main function to start the server
async function main() {
try {
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("PubMed MCP Server v1.1.3 is running...");
console.error("Available tools:");
console.error("- search_pubmed: Search PubMed (supports pagination, sorting, output modes, filters)");
console.error("- get_full_abstract: Get complete abstracts by PMID");
console.error("- get_full_text: Get full text from PMC (supports section filtering)");
console.error("- export_ris: Export citations in RIS format");
console.error("- get_citation_counts: Get citation counts for specific PMIDs");
console.error("- find_similar_articles: Find similar articles");
console.error("- count_results: Count search results (lightweight)");
console.error("- convert_ids: Convert between PMID, PMCID, and DOI");
console.error("- batch_process: Process multiple PMIDs with multiple operations");
}
catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
// Handle process termination gracefully
process.on('SIGINT', () => {
console.error("\nShutting down server...");
process.exit(0);
});
process.on('SIGTERM', () => {
console.error("\nShutting down server...");
process.exit(0);
});
// Start the server
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
//# sourceMappingURL=index.js.map