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.

106 lines 5.24 kB
/** * @fileoverview Article ID conversion tool. Converts between DOI, PMID, and PMCID * using the NCBI PMC ID Converter API for deterministic, batch-friendly resolution. * @module src/mcp-server/tools/definitions/convert-ids.tool */ import { tool, z } from '@cyanheads/mcp-ts-core'; import { NCBI_SERVICE_ERRORS } from '../../../services/error-contracts.js'; import { getNcbiService } from '../../../services/ncbi/ncbi-service.js'; import { conceptMeta, EDAM_ACCESSION, EDAM_ID_MAPPING } from './_concepts.js'; /** * NCBI's PMC ID Converter returns this exact wording for any non-PMC ID — even * articles that exist in PubMed and have a recoverable DOI. Rewrite to point * the caller at the recovery path; the original is logged at debug. */ const PMC_NOT_FOUND_RE = /^identifier not found in pmc$/i; const PMC_NOT_FOUND_REWRITE = 'Not in PMC ID Converter. Article may still exist in PubMed — try pubmed_fetch_articles (PMID → DOI) or pubmed_search_articles.'; export const convertIdsTool = tool('pubmed_convert_ids', { description: `Convert between article identifiers (DOI, PMID, PMCID). Accepts up to 50 IDs of a single type per request. Only resolves articles indexed in PubMed Central — for articles not in PMC, use pubmed_search_articles instead.`, annotations: { readOnlyHint: true, openWorldHint: true }, _meta: conceptMeta([EDAM_ID_MAPPING, EDAM_ACCESSION]), sourceUrl: 'https://github.com/cyanheads/pubmed-mcp-server/blob/main/src/mcp-server/tools/definitions/convert-ids.tool.ts', errors: [...NCBI_SERVICE_ERRORS], input: z.object({ ids: z .array(z.string().min(1)) .min(1) .max(50) .describe('Article identifiers to convert. All IDs must be the same type. DOIs: "10.1093/nar/gks1195", PMIDs: "23193287", PMCIDs: "PMC3531190".'), idType: z .enum(['pmcid', 'pmid', 'doi']) .describe('The type of IDs being submitted. Required so the API can unambiguously resolve them.'), }), output: z.object({ records: z .array(z .object({ requestedId: z.string().describe('The ID that was submitted'), pmid: z.string().optional().describe('PubMed ID; absent if no mapping was found'), pmcid: z .string() .optional() .describe('PubMed Central ID; absent if the article has no PMC copy'), doi: z .string() .optional() .describe('Digital Object Identifier; absent if no DOI is on record'), errmsg: z .string() .optional() .describe('Error message if conversion failed. Presence of `errmsg` is the failure signal; absence means the conversion succeeded.'), }) .describe('Per-ID conversion record')) .describe('Conversion results, one per input ID'), totalConverted: z.number().describe('Number of IDs successfully converted'), totalSubmitted: z.number().describe('Number of IDs submitted'), }), async handler(input, ctx) { ctx.log.info('Executing pubmed_convert_ids', { count: input.ids.length, idType: input.idType, }); const raw = await getNcbiService().idConvert(input.ids, input.idType, { signal: ctx.signal }); // NCBI returns pmid as a number in JSON — coerce all ID fields to strings const records = raw.map((r) => { const requestedId = String(r['requested-id']); let errmsg; if (r.errmsg !== undefined) { const original = String(r.errmsg); if (PMC_NOT_FOUND_RE.test(original)) { ctx.log.debug('Rewriting PMC-not-found errmsg', { requestedId, original }); errmsg = PMC_NOT_FOUND_REWRITE; } else { errmsg = original; } } return { requestedId, ...(r.pmid !== undefined && { pmid: String(r.pmid) }), ...(r.pmcid !== undefined && { pmcid: String(r.pmcid) }), ...(r.doi !== undefined && { doi: String(r.doi) }), ...(errmsg !== undefined && { errmsg }), }; }); const totalConverted = records.filter((r) => !r.errmsg).length; ctx.log.info('pubmed_convert_ids completed', { totalConverted, totalSubmitted: input.ids.length, }); return { records, totalConverted, totalSubmitted: input.ids.length }; }, format: (result) => { const lines = [ `## ID Conversion Results`, `**Converted:** ${result.totalConverted}/${result.totalSubmitted}`, '', '| Requested ID | PMID | PMCID | DOI | Error |', '|:---|:---|:---|:---|:---|', ]; for (const r of result.records) { lines.push(`| ${r.requestedId} | ${r.pmid ?? '-'} | ${r.pmcid ?? '-'} | ${r.doi ?? '-'} | ${r.errmsg ?? '-'} |`); } return [{ type: 'text', text: lines.join('\n') }]; }, }); //# sourceMappingURL=convert-ids.tool.js.map