@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
JavaScript
/**
* @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