search-medical-literature
Find medical research articles in PubMed by entering a medical topic or condition to access relevant scientific literature for clinical or academic purposes.
Instructions
Search for medical research articles in PubMed
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Medical topic or condition to search for | |
| max_results | No | Maximum number of articles to return (max 20) |
Implementation Reference
- src/index.ts:117-139 (registration)Registers the 'search-medical-literature' MCP tool, including inline handler, input schema (Zod validation for query and max_results), and description. The handler delegates to searchPubMedArticles utility.server.tool( "search-medical-literature", "Search for medical research articles in PubMed", { query: z.string().describe("Medical topic or condition to search for"), max_results: z .number() .int() .min(1) .max(20) .optional() .default(10) .describe("Maximum number of articles to return (max 20)"), }, async ({ query, max_results }) => { try { const articles = await searchPubMedArticles(query, max_results); return formatPubMedArticles(articles, query); } catch (error: any) { return createErrorResponse("searching medical literature", error); } }, );
- src/utils.ts:1374-1432 (handler)Core implementation of the tool logic: Queries PubMed ESearch for article IDs matching the query, fetches XML details via EFetch, parses into structured PubMedArticle objects using parsePubMedXML, and retrieves full text for open-access (PMC) articles.export async function searchPubMedArticles( query: string, maxResults: number = 10, ): Promise<PubMedArticle[]> { try { // First, search for article IDs const searchRes = await superagent .get(`${PUBMED_API_BASE}/esearch.fcgi`) .query({ db: "pubmed", term: query, retmode: "json", retmax: maxResults, }) .set("User-Agent", USER_AGENT); const idList = searchRes.body.esearchresult?.idlist || []; if (idList.length === 0) return []; // Then, fetch article details const fetchRes = await superagent .get(`${PUBMED_API_BASE}/efetch.fcgi`) .query({ db: "pubmed", id: idList.join(","), retmode: "xml", }) .set("User-Agent", USER_AGENT); const articles = parsePubMedXML(fetchRes.text); // Fetch full text for articles with PMC ID (limit to first 3 to avoid rate limiting) const articlesWithFullText = await Promise.all( articles.slice(0, 3).map(async (article) => { if (article.pmc_id) { try { const fullText = await fetchFullTextFromPMC(article.pmc_id); if (fullText) { article.full_text = fullText; } } catch (error) { console.error( `Error fetching full text for PMID ${article.pmid}:`, error, ); } } return article; }), ); // Combine articles with full text and those without return [...articlesWithFullText, ...articles.slice(3)]; } catch (error) { console.error("Error searching PubMed:", error); return []; } }
- src/utils.ts:486-521 (helper)Helper function to format PubMed article search results into a structured, readable text response for MCP, including titles, authors, abstracts, links, and full-text previews.export function formatPubMedArticles(articles: any[], query: string) { if (articles.length === 0) { return createMCPResponse( `No medical articles found for "${query}". Try different search terms or check the spelling.`, ); } let result = `**Medical Literature Search: "${query}"**\n\n`; result += `Found ${articles.length} article(s)\n\n`; articles.forEach((article, index) => { result += `${index + 1}. **${article.title}**\n`; result += ` Authors: ${article.authors.join(", ")}\n`; result += ` Journal: ${article.journal}\n`; result += ` Publication Date: ${article.publication_date}\n`; result += ` PMID: ${article.pmid}\n`; if (article.pmc_id) { result += ` PMC ID: ${article.pmc_id} (Full text available)\n`; } if (article.abstract) { result += ` Abstract: ${article.abstract.substring(0, 300)}${article.abstract.length > 300 ? "..." : ""}\n`; } if (article.full_text) { result += ` **Full Text Available**\n`; result += ` Full Text (first 1000 chars): ${article.full_text.substring(0, 1000)}${article.full_text.length > 1000 ? "..." : ""}\n`; result += ` [Full text truncated for display. Use get-article-details for complete text.]\n`; } result += ` URL: https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/\n`; if (article.pmc_id) { result += ` Full Text: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC${article.pmc_id}/\n`; } result += "\n"; }); return createMCPResponse(result); }
- src/utils.ts:1434-1555 (helper)Helper function that parses raw PubMed EFetch XML response into structured PubMedArticle objects, extracting PMID, title, abstract, authors, journal, publication date, DOI, and PMC ID.export function parsePubMedXML(xmlText: string): PubMedArticle[] { const articles: PubMedArticle[] = []; // Split by article boundaries const articleMatches = xmlText.match( /<PubmedArticle>[\s\S]*?<\/PubmedArticle>/g, ); if (!articleMatches) return articles; for (const articleXml of articleMatches) { try { // Extract PMID const pmidMatch = articleXml.match(/<PMID[^>]*>(\d+)<\/PMID>/); const pmid = pmidMatch?.[1]; if (!pmid) continue; // Extract title const titleMatch = articleXml.match( /<ArticleTitle[^>]*>([^<]+)<\/ArticleTitle>/, ); const title = titleMatch?.[1]?.trim() || "No title available"; // Extract abstract let abstract = "No abstract available"; const abstractMatch = articleXml.match( /<AbstractText[^>]*>([\s\S]*?)<\/AbstractText>/, ); if (abstractMatch) { abstract = abstractMatch[1] .replace(/<[^>]*>/g, "") // Remove HTML tags .replace(/\s+/g, " ") // Normalize whitespace .trim(); } // Extract authors const authors: string[] = []; const authorMatches = articleXml.match(/<Author[\s\S]*?<\/Author>/g); if (authorMatches) { for (const authorXml of authorMatches) { const lastNameMatch = authorXml.match( /<LastName>([^<]+)<\/LastName>/, ); const firstNameMatch = authorXml.match( /<ForeName>([^<]+)<\/ForeName>/, ); const collectiveNameMatch = authorXml.match( /<CollectiveName>([^<]+)<\/CollectiveName>/, ); if (collectiveNameMatch) { authors.push(collectiveNameMatch[1].trim()); } else if (lastNameMatch && firstNameMatch) { authors.push( `${firstNameMatch[1].trim()} ${lastNameMatch[1].trim()}`, ); } else if (lastNameMatch) { authors.push(lastNameMatch[1].trim()); } } } // Extract journal information let journal = "Journal information not available"; const journalMatch = articleXml.match(/<Title>([^<]+)<\/Title>/); if (journalMatch) { journal = journalMatch[1].trim(); } // Extract publication date let publicationDate = "Date not available"; const yearMatch = articleXml.match(/<Year>(\d{4})<\/Year>/); const monthMatch = articleXml.match(/<Month>(\d{1,2})<\/Month>/); const dayMatch = articleXml.match(/<Day>(\d{1,2})<\/Day>/); if (yearMatch) { const year = yearMatch[1]; const month = monthMatch?.[1]?.padStart(2, "0") || "01"; const day = dayMatch?.[1]?.padStart(2, "0") || "01"; publicationDate = `${year}-${month}-${day}`; } // Extract DOI let doi: string | undefined; const doiMatch = articleXml.match( /<ELocationID[^>]*EIdType="doi"[^>]*>([^<]+)<\/ELocationID>/, ); if (doiMatch) { doi = doiMatch[1].trim(); } // Extract PMC ID let pmc_id: string | undefined; const pmcIdPatterns = [ /<ArticleId[^>]*IdType="pmc"[^>]*>PMC(\d+)<\/ArticleId>/i, /<ArticleId[^>]*IdType="pmc"[^>]*>(\d+)<\/ArticleId>/i, ]; for (const pattern of pmcIdPatterns) { const pmcMatch = articleXml.match(pattern); if (pmcMatch) { pmc_id = pmcMatch[1].trim(); break; } } articles.push({ pmid, title, abstract, authors, journal, publication_date: publicationDate, doi, pmc_id, }); } catch (error) { console.error("Error parsing individual article:", error); } } return articles; }
- src/types.ts:47-57 (schema)TypeScript interface defining the structure of PubMed articles returned by the search, used for type safety in handler and formatting functions.export type PubMedArticle = { pmid: string; title: string; abstract: string; authors: string[]; journal: string; publication_date: string; doi?: string; pmc_id?: string; full_text?: string; };