UNPKG

@cyanheads/pubmed-mcp-server

Version:

Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.

104 lines 5.32 kB
/** * @fileoverview PubMed citation tool — generates formatted citations (APA, MLA, * BibTeX, RIS) for one or more PubMed articles. * @module src/mcp-server/tools/definitions/format-citations.tool */ import { tool, z } from '@cyanheads/mcp-ts-core'; import { NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js'; import { formatCitations, } from '../../../services/ncbi/formatting/citation-formatter.js'; import { getNcbiService } from '../../../services/ncbi/ncbi-service.js'; import { parseFullArticle } from '../../../services/ncbi/parsing/article-parser.js'; import { ensureArray } from '../../../services/ncbi/parsing/xml-helpers.js'; import { conceptMeta, EDAM_DATA_FORMATTING, SCHEMA_CREATIVE_WORK } from './_concepts.js'; import { pmidStringSchema } from './_schemas.js'; const CitationStyleEnum = z.enum(['apa', 'mla', 'bibtex', 'ris']); export const formatCitationsTool = tool('pubmed_format_citations', { description: 'Get formatted citations for PubMed articles in one or more styles (APA, MLA, BibTeX, RIS). Pass a single style as a string or multiple as an array.', annotations: { readOnlyHint: true, openWorldHint: true }, _meta: conceptMeta([SCHEMA_CREATIVE_WORK, EDAM_DATA_FORMATTING]), sourceUrl: 'https://github.com/cyanheads/pubmed-mcp-server/blob/main/src/mcp-server/tools/definitions/format-citations.tool.ts', errors: [...NCBI_SERVICE_ERRORS], input: z.object({ pmids: z.array(pmidStringSchema).min(1).max(50).describe('PubMed IDs to cite'), format: z .union([ CitationStyleEnum.describe('Single citation style. One of: apa, mla, bibtex, ris.'), z .array(CitationStyleEnum) .min(1) .describe('Multiple citation styles to generate. Each entry: apa, mla, bibtex, or ris.'), ]) .default('apa') .describe('Citation format(s) to generate — single style as a string or multiple as an array. Allowed values: apa, mla, bibtex, ris.'), }), output: z.object({ citations: z .array(z .object({ pmid: z.string().describe('PubMed ID'), title: z.string().optional().describe('Article title'), citations: z.record(z.string(), z.string()).describe('Citations keyed by style'), }) .describe('Citations for a single article')) .describe('Citations per article'), totalSubmitted: z.number().describe('Number of PMIDs submitted for citation formatting'), totalFormatted: z.number().describe('Number of PMIDs successfully formatted'), unavailablePmids: z .array(z.string()) .optional() .describe('Requested PMIDs that did not return article metadata'), }), async handler(input, ctx) { const formats = Array.isArray(input.format) ? input.format : [input.format]; ctx.log.debug('Fetching articles for citation generation', { pmids: input.pmids, formats, }); const raw = await getNcbiService().eFetch({ db: 'pubmed', id: input.pmids.join(','), retmode: 'xml' }, { retmode: 'xml', usePost: input.pmids.length >= 25, signal: ctx.signal }); const xmlArticles = ensureArray(raw?.PubmedArticleSet?.PubmedArticle); const citations = xmlArticles.map((xmlArticle) => { const parsed = parseFullArticle(xmlArticle); return { pmid: parsed.pmid, title: parsed.title, citations: formatCitations(parsed, formats), }; }); const returnedPmids = new Set(citations.map((entry) => entry.pmid)); const unavailablePmids = input.pmids.filter((pmid) => !returnedPmids.has(pmid)); return { citations, totalSubmitted: input.pmids.length, totalFormatted: citations.length, ...(unavailablePmids.length > 0 && { unavailablePmids }), }; }, format: (result) => { const lines = [ '# PubMed Citations', `**Formatted:** ${result.totalFormatted}/${result.totalSubmitted}`, ]; if (result.unavailablePmids?.length) { lines.push(`**Unavailable PMIDs:** ${result.unavailablePmids.join(', ')}`); } if (result.totalFormatted === 0) { lines.push(`\n> No articles were returned for the submitted PMIDs. They may be invalid, unpublished, or withdrawn. Try \`pubmed_search_articles\` to discover valid PMIDs, or \`pubmed_spell_check\` if these came from a noisy source.`); } for (const entry of result.citations) { lines.push(`\n## PMID ${entry.pmid}`); if (entry.title) lines.push(`**${entry.title}**`); for (const [style, citation] of Object.entries(entry.citations)) { lines.push(`\n### ${style.toUpperCase()}`); if (style === 'bibtex' || style === 'ris') { lines.push(`\`\`\`${style}\n${citation}\n\`\`\``); } else { lines.push(citation); } } } return [{ type: 'text', text: lines.join('\n') }]; }, }); //# sourceMappingURL=format-citations.tool.js.map