UNPKG

@cyanheads/pubmed-mcp-server

Version:

A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.

290 lines (289 loc) 13.3 kB
/** * @fileoverview Handles citation formatting for the getPubMedArticleConnections tool. * Fetches article details using EFetch and formats them into various citation styles. * @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter */ import { getNcbiService } from "../../../../services/NCBI/ncbiService.js"; import { logger, requestContextService, } from "../../../../utils/index.js"; import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../utils/parsing/ncbi-parsing/index.js"; // Main handler for citation formats export async function handleCitationFormats(input, outputData, context) { const eFetchParams = { db: "pubmed", id: input.sourcePmid, retmode: "xml", // Omitting rettype to hopefully get the fullest XML record by default }; const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"; const searchParamsString = new URLSearchParams(eFetchParams).toString(); outputData.eUtilityUrl = `${eFetchBaseUrl}?${searchParamsString}`; const ncbiService = getNcbiService(); const eFetchResult = await ncbiService.eFetch(eFetchParams, context); if (!eFetchResult?.PubmedArticleSet?.PubmedArticle?.[0]) { outputData.message = "Could not retrieve article details for citation formatting."; logger.warning(outputData.message, context); return; } const article = eFetchResult.PubmedArticleSet.PubmedArticle[0]; if (input.citationStyles?.includes("ris")) { outputData.citations.ris = formatAsRIS(article, context); } if (input.citationStyles?.includes("bibtex")) { outputData.citations.bibtex = formatAsBibTeX(article, context); } if (input.citationStyles?.includes("apa_string")) { outputData.citations.apa_string = formatAsAPA(article, context); } if (input.citationStyles?.includes("mla_string")) { outputData.citations.mla_string = formatAsMLA(article, context); } outputData.retrievedCount = 1; } // --- Citation Formatting Helper Functions --- function formatAsRIS(article, context) { const medlineCitation = article.MedlineCitation; const articleDetails = medlineCitation?.Article; const pmid = extractPmid(medlineCitation); logger.debug("Formatting RIS for article", requestContextService.createRequestContext({ ...context, pmid })); if (!articleDetails) return "TY - ERROR\nTI - Article details not found\nER - \n"; const authors = extractAuthors(articleDetails.AuthorList); const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation); const title = getText(articleDetails.ArticleTitle); const doi = extractDoi(articleDetails); let risString = "TY - JOUR\n"; authors.forEach((author) => { if (author.lastName && author.firstName) { risString += `AU - ${author.lastName}, ${author.firstName}\n`; } else if (author.lastName) { // Handle collective name or incomplete author risString += `AU - ${author.lastName}\n`; } }); risString += `TI - ${title || "N/A"}\n`; risString += `JO - ${journalInfo?.title || "N/A"}\n`; risString += `VL - ${journalInfo?.volume || ""}\n`; risString += `IS - ${journalInfo?.issue || ""}\n`; if (journalInfo?.pages) { const pages = journalInfo.pages.split("-"); risString += `SP - ${pages[0] || ""}\n`; if (pages.length > 1) risString += `EP - ${pages[1] || ""}\n`; } risString += `PY - ${journalInfo?.publicationDate?.year || ""}\n`; if (doi) risString += `DO - ${doi}\n`; if (pmid) risString += `UR - https://pubmed.ncbi.nlm.nih.gov/${pmid}\n`; risString += "ER - \n"; return risString; } function formatAsBibTeX(article, context) { const medlineCitation = article.MedlineCitation; const articleDetails = medlineCitation?.Article; const pmid = extractPmid(medlineCitation); logger.debug("Formatting BibTeX for article", requestContextService.createRequestContext({ ...context, pmid })); if (!articleDetails) return `@article{ERROR_ArticleNotFound${pmid || ""},\n title = {Article details not found}\n}\n`; const parsedAuthors = extractAuthors(articleDetails.AuthorList); const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation); const title = getText(articleDetails.ArticleTitle) || "N/A"; const doi = extractDoi(articleDetails); const authorsBibtex = parsedAuthors .map((author) => { if (author.lastName && author.firstName) return `${author.lastName}, ${author.firstName}`; if (author.lastName) return `{${author.lastName}}`; // For collective names or single names return ""; }) .filter(Boolean) .join(" and "); const year = journalInfo?.publicationDate?.year || "ND"; const firstAuthorLastName = parsedAuthors[0]?.lastName?.replace(/\s+/g, "") || "Unknown"; const bibKey = `${firstAuthorLastName}${year}`; let bibtexString = `@article{${bibKey},\n`; if (authorsBibtex) bibtexString += ` author = {${authorsBibtex}},\n`; bibtexString += ` title = {${title}},\n`; bibtexString += ` journal = {${journalInfo?.title || "N/A"}},\n`; if (journalInfo?.publicationDate?.year) bibtexString += ` year = {${journalInfo.publicationDate.year}},\n`; if (journalInfo?.volume) bibtexString += ` volume = {${journalInfo.volume}},\n`; if (journalInfo?.issue) bibtexString += ` number = {${journalInfo.issue}},\n`; if (journalInfo?.pages) bibtexString += ` pages = {${journalInfo.pages.replace("-", "--")}},\n`; if (journalInfo?.publicationDate?.month) bibtexString += ` month = {${journalInfo.publicationDate.month.toLowerCase()}},\n`; if (doi) bibtexString += ` doi = {${doi}},\n`; if (pmid) bibtexString += ` pmid = {${pmid}}\n`; bibtexString += `}\n`; return bibtexString; } function formatAsAPA(article, context) { const medlineCitation = article.MedlineCitation; const articleDetails = medlineCitation?.Article; const pmid = extractPmid(medlineCitation); logger.debug("Formatting APA for article", requestContextService.createRequestContext({ ...context, pmid })); if (!articleDetails) return "Article details not found."; const parsedAuthors = extractAuthors(articleDetails.AuthorList); const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation); const titleText = getText(articleDetails.ArticleTitle) || "N/A"; const doi = extractDoi(articleDetails); let authorsString = "N/A"; if (parsedAuthors.length > 0) { if (parsedAuthors.length <= 20) { authorsString = parsedAuthors .map((author) => { if (author.lastName && author.firstName) { const initials = author.firstName .split(/\s+|-/) .map((namePart) => namePart.charAt(0).toUpperCase() + ".") .join(""); return `${author.lastName}, ${initials}`; } if (author.lastName) return author.lastName; // Collective name return ""; }) .filter(Boolean) .join(", "); if (parsedAuthors.length > 1) { const lastCommaIndex = authorsString.lastIndexOf(", "); if (lastCommaIndex !== -1) { authorsString = authorsString.substring(0, lastCommaIndex) + " & " + authorsString.substring(lastCommaIndex + 2); } } } else { const first19 = parsedAuthors .slice(0, 19) .map((author) => { if (author.lastName && author.firstName) { const initials = author.firstName .split(/\s+|-/) .map((namePart) => namePart.charAt(0).toUpperCase() + ".") .join(""); return `${author.lastName}, ${initials}`; } if (author.lastName) return author.lastName; return ""; }) .filter(Boolean) .join(", "); const lastAuthor = parsedAuthors[parsedAuthors.length - 1]; let lastAuthorString = ""; if (lastAuthor.lastName && lastAuthor.firstName) { const initials = lastAuthor.firstName .split(/\s+|-/) .map((namePart) => namePart.charAt(0).toUpperCase() + ".") .join(""); lastAuthorString = `${lastAuthor.lastName}, ${initials}`; } else if (lastAuthor.lastName) { lastAuthorString = lastAuthor.lastName; } authorsString = `${first19}, ..., ${lastAuthorString}`; } } const year = journalInfo?.publicationDate?.year || "n.d."; const apaTitle = titleText.charAt(0).toUpperCase() + titleText.slice(1); // APA typically sentence case for article titles. const journal = journalInfo?.title || "N/A"; const volume = journalInfo?.volume || ""; const issue = journalInfo?.issue ? `(${journalInfo.issue})` : ""; const pages = journalInfo?.pages || ""; const doiLink = doi ? ` https://doi.org/${doi}` : ""; let apaString = `${authorsString}. (${year}). ${apaTitle}. ${journal}`; if (volume) apaString += `, ${volume}`; if (issue) apaString += issue; if (pages) apaString += `, ${pages}`; apaString += `.${doiLink}`; return apaString; } function formatAsMLA(article, context) { const medlineCitation = article.MedlineCitation; const articleDetails = medlineCitation?.Article; const pmid = extractPmid(medlineCitation); logger.debug("Formatting MLA for article", requestContextService.createRequestContext({ ...context, pmid })); if (!articleDetails) return "Article details not found."; const parsedAuthors = extractAuthors(articleDetails.AuthorList); const journalInfo = extractJournalInfo(articleDetails.Journal, medlineCitation); const titleText = getText(articleDetails.ArticleTitle); const doi = extractDoi(articleDetails); let authorsString = "N/A"; if (parsedAuthors.length > 0) { if (parsedAuthors.length <= 2) { authorsString = parsedAuthors .map((author, index) => { if (author.lastName && author.firstName) { return index === 0 ? `${author.lastName}, ${author.firstName}` : `${author.firstName} ${author.lastName}`; } if (author.lastName) return author.lastName; return ""; }) .filter(Boolean) .join(" and "); } else { const firstAuthor = parsedAuthors[0]; if (firstAuthor.lastName && firstAuthor.firstName) { authorsString = `${firstAuthor.lastName}, ${firstAuthor.firstName}, et al`; } else if (firstAuthor.lastName) { authorsString = `${firstAuthor.lastName}, et al`; } } } const title = titleText ? `"${titleText}."` : "N/A."; const journal = journalInfo?.title || "N/A"; let publicationDateString = journalInfo?.publicationDate?.year || ""; if (journalInfo?.publicationDate?.month && journalInfo.publicationDate.year) { const month = journalInfo.publicationDate.month.substring(0, 3) + "."; // Abbreviate month publicationDateString = `${month} ${journalInfo.publicationDate.year}`; if (journalInfo.publicationDate.day) { publicationDateString = `${journalInfo.publicationDate.day} ${month} ${journalInfo.publicationDate.year}`; } } else if (journalInfo?.publicationDate?.medlineDate) { publicationDateString = journalInfo.publicationDate.medlineDate; } const volume = journalInfo?.volume ? `vol. ${journalInfo.volume}` : ""; const issue = journalInfo?.issue ? `no. ${journalInfo.issue}` : ""; const pages = journalInfo?.pages ? `pp. ${journalInfo.pages.replace("-", "–")}` : ""; // en-dash for MLA const accessUrl = doi ? `doi:${doi}` : pmid ? `https://pubmed.ncbi.nlm.nih.gov/${pmid}` : ""; let mlaString = `${authorsString}. ${title} ${journal}`; if (volume) mlaString += `, ${volume}`; if (issue) mlaString += `, ${issue}`; if (publicationDateString) mlaString += `, ${publicationDateString}`; if (pages) mlaString += `, ${pages}`; mlaString += `. PubMed Central, ${accessUrl}.`; // Assuming PubMed Central or just PubMed return mlaString; }