Skip to main content
Glama

pubmed_article_connections

Identify related PubMed articles or format citations based on a source PMID. Find similar articles, citing references, or referenced works, or generate citations in RIS, BibTeX, APA, or MLA styles.

Instructions

Finds articles related to a source PubMed ID (PMID) or retrieves formatted citations for it. Supports finding similar articles, articles that cite the source, articles referenced by the source (via NCBI ELink), or fetching data to generate citations in various styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting). Returns a JSON object detailing the connections or formatted citations.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
citationStylesNoAn array of citation styles to format the source article into when 'relationshipType' is 'citation_formats'. Supported styles: 'ris', 'bibtex', 'apa_string', 'mla_string'. Default is ['ris'].
maxRelatedResultsNoMaximum number of related articles to retrieve for relationship-based searches. Default is 5, max is 50.
relationshipTypeNoSpecifies the type of connection or action: 'pubmed_similar_articles' (finds similar articles), 'pubmed_citedin' (finds citing articles), 'pubmed_references' (finds referenced articles), or 'citation_formats' (retrieves formatted citations).pubmed_similar_articles
sourcePmidYesThe PubMed Unique Identifier (PMID) of the source article for which to find connections or format citations. This PMID must be a valid number string.

Implementation Reference

  • Main handler function that orchestrates the tool logic: routes to ELink handlers for article relationships or citation formatter based on relationshipType, initializes output, handles errors, and logs execution.
    export async function handlePubMedArticleConnections( input: PubMedArticleConnectionsInput, context: RequestContext, ): Promise<ToolOutputData> { const toolLogicContext = requestContextService.createRequestContext({ parentRequestId: context.requestId, operation: "handlePubMedArticleConnections", toolName: "pubmed_article_connections", input: sanitizeInputForLogging(input), }); logger.info("Executing pubmed_article_connections tool", toolLogicContext); const outputData: ToolOutputData = { sourcePmid: input.sourcePmid, relationshipType: input.relationshipType, relatedArticles: [], citations: {}, retrievedCount: 0, eUtilityUrl: undefined, message: undefined, }; switch (input.relationshipType) { case "pubmed_similar_articles": case "pubmed_citedin": case "pubmed_references": await handleELinkRelationships(input, outputData, toolLogicContext); break; case "citation_formats": await handleCitationFormats(input, outputData, toolLogicContext); break; default: throw new McpError( BaseErrorCode.VALIDATION_ERROR, `Unsupported relationshipType: ${input.relationshipType}`, { ...toolLogicContext, receivedType: input.relationshipType }, ); } if ( outputData.retrievedCount === 0 && !outputData.message && (input.relationshipType !== "citation_formats" || Object.keys(outputData.citations).length === 0) ) { outputData.message = "No results found for the given parameters."; } logger.notice("Successfully executed pubmed_article_connections tool.", { ...toolLogicContext, relationshipType: input.relationshipType, retrievedCount: outputData.retrievedCount, citationsGenerated: Object.keys(outputData.citations).length, }); return outputData; }
  • Zod input schema defining parameters: sourcePmid (required PMID), relationshipType (enum for connection types or citations), maxRelatedResults (optional, default 5), citationStyles (optional array for formats).
    export const PubMedArticleConnectionsInputSchema = z.object({ sourcePmid: z .string() .regex(/^\d+$/) .describe( "The PubMed Unique Identifier (PMID) of the source article for which to find connections or format citations. This PMID must be a valid number string.", ), relationshipType: z .enum([ "pubmed_similar_articles", "pubmed_citedin", "pubmed_references", "citation_formats", ]) .default("pubmed_similar_articles") .describe( "Specifies the type of connection or action: 'pubmed_similar_articles' (finds similar articles), 'pubmed_citedin' (finds citing articles), 'pubmed_references' (finds referenced articles), or 'citation_formats' (retrieves formatted citations).", ), maxRelatedResults: z .number() .int() .positive() .max(50, "Maximum 50 related results can be requested.") .optional() .default(5) .describe( "Maximum number of related articles to retrieve for relationship-based searches. Default is 5, max is 50.", ), citationStyles: z .array(z.enum(["ris", "bibtex", "apa_string", "mla_string"])) .optional() .default(["ris"]) .describe( "An array of citation styles to format the source article into when 'relationshipType' is 'citation_formats'. Supported styles: 'ris', 'bibtex', 'apa_string', 'mla_string'. Default is ['ris'].", ), });
  • Registers the 'pubmed_article_connections' tool on the MCP server using server.tool(), provides description and input schema, wraps the handlePubMedArticleConnections function with error handling and JSON response formatting.
    export async function registerPubMedArticleConnectionsTool( server: McpServer, ): Promise<void> { const operation = "registerPubMedArticleConnectionsTool"; const toolName = "pubmed_article_connections"; const toolDescription = "Finds articles related to a source PubMed ID (PMID) or retrieves formatted citations for it. Supports finding similar articles, articles that cite the source, articles referenced by the source (via NCBI ELink), or fetching data to generate citations in various styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting). Returns a JSON object detailing the connections or formatted citations."; const context = requestContextService.createRequestContext({ operation }); await ErrorHandler.tryCatch( async () => { server.tool( toolName, toolDescription, PubMedArticleConnectionsInputSchema.shape, async ( input: PubMedArticleConnectionsInput, mcpProvidedContext: unknown, ): Promise<CallToolResult> => { const richContext: RequestContext = requestContextService.createRequestContext({ parentRequestId: context.requestId, operation: "pubMedArticleConnectionsToolHandler", mcpToolContext: mcpProvidedContext, input, }); try { const result = await handlePubMedArticleConnections( input, richContext, ); return { content: [ { type: "text", text: JSON.stringify(result, null, 2) }, ], isError: false, }; } catch (error) { const handledError = ErrorHandler.handleError(error, { operation: "pubMedArticleConnectionsToolHandler", context: richContext, input, rethrow: false, }); const mcpError = handledError instanceof McpError ? handledError : new McpError( BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while getting PubMed article connections.", { originalErrorName: handledError.name, originalErrorMessage: handledError.message, }, ); return { content: [ { type: "text", text: JSON.stringify({ error: { code: mcpError.code, message: mcpError.message, details: mcpError.details, }, }), }, ], isError: true, }; } }, ); logger.notice(`Tool '${toolName}' registered.`, context); }, { operation, context, errorCode: BaseErrorCode.INITIALIZATION_FAILED, critical: true, }, ); }
  • Helper function handling NCBI ELink, ESearch, ESummary for finding related articles (similar, cited-by, references), parsing results, enriching with summaries, and populating output.
    export async function handleELinkRelationships( input: PubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext, ): Promise<void> { const eLinkParams: Record<string, string> = { dbfrom: "pubmed", db: "pubmed", id: input.sourcePmid, retmode: "xml", // cmd and linkname will be set below based on relationshipType }; switch (input.relationshipType) { case "pubmed_citedin": eLinkParams.cmd = "neighbor_history"; eLinkParams.linkname = "pubmed_pubmed_citedin"; break; case "pubmed_references": eLinkParams.cmd = "neighbor_history"; eLinkParams.linkname = "pubmed_pubmed_refs"; break; case "pubmed_similar_articles": default: // Default to similar articles eLinkParams.cmd = "neighbor_score"; // No linkname is explicitly needed for neighbor_score when dbfrom and db are pubmed break; } const tempUrl = new URL( "https://dummy.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi", ); Object.keys(eLinkParams).forEach((key) => tempUrl.searchParams.append(key, String(eLinkParams[key])), ); outputData.eUtilityUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?${tempUrl.search.substring(1)}`; const ncbiService = getNcbiService(); const eLinkResult: ELinkResult = (await ncbiService.eLink( eLinkParams, context, )) as ELinkResult; // Log the full eLinkResult for debugging logger.debug("Raw eLinkResult from ncbiService:", { ...context, eLinkResultString: JSON.stringify(eLinkResult, null, 2), }); // Use ensureArray for robust handling of potentially single or array eLinkResult const eLinkResultsArray = ensureArray(eLinkResult?.eLinkResult); const firstELinkResult = eLinkResultsArray[0]; // Use ensureArray for LinkSet as well const linkSetsArray = ensureArray(firstELinkResult?.LinkSet); const linkSet = linkSetsArray[0]; let foundPmids: { pmid: string; score?: number }[] = []; if (firstELinkResult?.ERROR) { const errorMsg = typeof firstELinkResult.ERROR === "string" ? firstELinkResult.ERROR : JSON.stringify(firstELinkResult.ERROR); logger.warning(`ELink returned an error: ${errorMsg}`, context); outputData.message = `ELink error: ${errorMsg}`; outputData.retrievedCount = 0; return; } if (linkSet?.LinkSetDbHistory) { // Handle cmd=neighbor_history response (citedin, references) const history = Array.isArray(linkSet.LinkSetDbHistory) ? linkSet.LinkSetDbHistory[0] : linkSet.LinkSetDbHistory; if (history?.QueryKey && firstELinkResult?.LinkSet?.WebEnv) { const eSearchParams = { db: "pubmed", query_key: history.QueryKey, WebEnv: firstELinkResult.LinkSet.WebEnv, retmode: "xml", retmax: input.maxRelatedResults * 2, // Fetch a bit more to allow filtering sourcePmid }; const eSearchResult: { eSearchResult?: { IdList?: { Id?: unknown } } } = (await ncbiService.eSearch(eSearchParams, context)) as { eSearchResult?: { IdList?: { Id?: unknown } }; }; if (eSearchResult?.eSearchResult?.IdList?.Id) { const ids = ensureArray(eSearchResult.eSearchResult.IdList.Id); foundPmids = ids .map((idNode: string | number | { "#text"?: string | number }) => { // Allow number for idNode let pmidVal: string | number | undefined; if (typeof idNode === "object" && idNode !== null) { pmidVal = idNode["#text"]; } else { pmidVal = idNode; } return { pmid: pmidVal !== undefined ? String(pmidVal) : "", // No scores from this ESearch path }; }) .filter( (item: { pmid: string }) => item.pmid && item.pmid !== input.sourcePmid && item.pmid !== "0", ); } } } else if (linkSet?.LinkSetDb) { // Handle cmd=neighbor_score response (similar_articles) const linkSetDbArray = Array.isArray(linkSet.LinkSetDb) ? linkSet.LinkSetDb : [linkSet.LinkSetDb]; const targetLinkSetDbEntry = linkSetDbArray.find( (db) => db.LinkName === "pubmed_pubmed", ); if (targetLinkSetDbEntry?.Link) { const links = ensureArray(targetLinkSetDbEntry.Link); // Use ensureArray here too foundPmids = links .map((link: XmlELinkItem) => { let pmidValue: string | number | undefined; if (typeof link.Id === "object" && link.Id !== null) { pmidValue = link.Id["#text"]; } else if (link.Id !== undefined) { pmidValue = link.Id; } let scoreValue: string | number | undefined; if (typeof link.Score === "object" && link.Score !== null) { scoreValue = link.Score["#text"]; } else if (link.Score !== undefined) { scoreValue = link.Score; } const pmidString = pmidValue !== undefined ? String(pmidValue) : ""; return { pmid: pmidString, score: scoreValue !== undefined ? Number(scoreValue) : undefined, }; }) .filter( (item: { pmid: string; score?: number }) => item.pmid && item.pmid !== input.sourcePmid && item.pmid !== "0", ); } } if (foundPmids.length === 0) { logger.warning( "No related PMIDs found after ELink/ESearch processing.", context, ); outputData.message = "No related articles found or ELink error."; // Generic message if no PMIDs outputData.retrievedCount = 0; return; } logger.debug( "Found PMIDs after initial parsing and filtering (before sort):", { ...context, foundPmidsCount: foundPmids.length, firstFewFoundPmids: foundPmids.slice(0, 3), }, ); if (foundPmids.every((p) => p.score !== undefined)) { foundPmids.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); } logger.debug("Found PMIDs after sorting:", { ...context, sortedFoundPmidsCount: foundPmids.length, firstFewSortedFoundPmids: foundPmids.slice(0, 3), }); const pmidsToEnrich = foundPmids .slice(0, input.maxRelatedResults) .map((p) => p.pmid); logger.debug("PMIDs to enrich with ESummary:", { ...context, pmidsToEnrichCount: pmidsToEnrich.length, pmidsToEnrichList: pmidsToEnrich, }); if (pmidsToEnrich.length > 0) { try { const summaryParams = { db: "pubmed", id: pmidsToEnrich.join(","), version: "2.0", retmode: "xml", }; const summaryResultContainer: { eSummaryResult?: ESummaryResult; result?: ESummaryResult; } = (await ncbiService.eSummary(summaryParams, context)) as { eSummaryResult?: ESummaryResult; result?: ESummaryResult; }; const summaryResult: ESummaryResult | undefined = summaryResultContainer?.eSummaryResult || summaryResultContainer?.result || summaryResultContainer; if (summaryResult) { const briefSummaries: ParsedBriefSummary[] = await extractBriefSummaries(summaryResult, context); const pmidDetailsMap = new Map<string, ParsedBriefSummary>(); briefSummaries.forEach((bs) => pmidDetailsMap.set(bs.pmid, bs)); outputData.relatedArticles = foundPmids .filter((p) => pmidsToEnrich.includes(p.pmid)) .map((p) => { const details = pmidDetailsMap.get(p.pmid); return { pmid: p.pmid, title: details?.title, authors: details?.authors, score: p.score, linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`, }; }) .slice(0, input.maxRelatedResults); } else { logger.warning( "ESummary did not return usable data for enrichment.", context, ); outputData.relatedArticles = foundPmids .slice(0, input.maxRelatedResults) .map((p) => ({ pmid: p.pmid, score: p.score, linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`, })); } } catch (summaryError: unknown) { logger.error( "Failed to enrich related articles with summaries", summaryError instanceof Error ? summaryError : new Error(String(summaryError)), context, ); outputData.relatedArticles = foundPmids .slice(0, input.maxRelatedResults) .map((p) => ({ pmid: p.pmid, score: p.score, linkUrl: `https://pubmed.ncbi.nlm.nih.gov/${p.pmid}/`, })); } } outputData.retrievedCount = outputData.relatedArticles.length; }
  • Helper function for citation formatting: fetches PubMed article via EFetch, converts to CSL-JSON, uses citation-js library to generate requested formats (RIS, BibTeX, APA, MLA), populates output.
    export async function handleCitationFormats( input: PubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext, ): Promise<void> { const eFetchParams = { db: "pubmed", id: input.sourcePmid, retmode: "xml" as const, }; const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"; const searchParamsString = new URLSearchParams( eFetchParams as Record<string, string>, ).toString(); outputData.eUtilityUrl = `${eFetchBaseUrl}?${searchParamsString}`; const ncbiService = getNcbiService(); const eFetchResult: { PubmedArticleSet?: XmlPubmedArticleSet } = (await ncbiService.eFetch(eFetchParams, context)) as { PubmedArticleSet?: XmlPubmedArticleSet; }; const pubmedArticles = ensureArray<XmlPubmedArticle>( eFetchResult?.PubmedArticleSet?.PubmedArticle as | XmlPubmedArticle | XmlPubmedArticle[] | undefined, ); if (pubmedArticles.length === 0) { outputData.message = "Could not retrieve article details for citation formatting."; logger.warning(outputData.message, context); return; } const article: XmlPubmedArticle = pubmedArticles[0]!; const csl = pubmedArticleToCsl(article, context); const cite = new Cite(csl); if (input.citationStyles?.includes("ris")) { outputData.citations.ris = cite.format("ris"); } if (input.citationStyles?.includes("bibtex")) { outputData.citations.bibtex = cite.format("bibtex"); } if (input.citationStyles?.includes("apa_string")) { outputData.citations.apa_string = cite.format("bibliography", { format: "text", template: "apa", }); } if (input.citationStyles?.includes("mla_string")) { outputData.citations.mla_string = cite.format("bibliography", { format: "text", template: "mla", }); } outputData.retrievedCount = 1; }

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/pubmed-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server