Skip to main content
Glama
ncukondo

PubMed MCP Server

by ncukondo
pubmed-api.test.ts24.2 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createPubMedAPI, type PubMedAPI, type PubMedOptions } from '../pubmed-api.js'; // Mock fetch globally global.fetch = vi.fn(); const mockFetch = global.fetch as any; describe('PubMed API', () => { let api: PubMedAPI; const mockOptions: PubMedOptions = { email: 'test@example.com', apiKey: 'test-api-key' }; beforeEach(() => { vi.clearAllMocks(); api = createPubMedAPI(mockOptions); }); describe('createPubMedAPI', () => { it('should create API instance with email only', () => { const apiWithoutKey = createPubMedAPI({ email: 'test@example.com' }); expect(apiWithoutKey).toBeDefined(); expect(apiWithoutKey.search).toBeInstanceOf(Function); expect(apiWithoutKey.fetchArticles).toBeInstanceOf(Function); expect(apiWithoutKey.searchAndFetch).toBeInstanceOf(Function); expect(apiWithoutKey.checkFullTextAvailability).toBeInstanceOf(Function); expect(apiWithoutKey.getFullText).toBeInstanceOf(Function); }); it('should create API instance with email and API key', () => { expect(api).toBeDefined(); expect(api.search).toBeInstanceOf(Function); expect(api.fetchArticles).toBeInstanceOf(Function); expect(api.searchAndFetch).toBeInstanceOf(Function); expect(api.checkFullTextAvailability).toBeInstanceOf(Function); expect(api.getFullText).toBeInstanceOf(Function); }); }); describe('search', () => { it('should perform basic search and return results', async () => { const mockSearchResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult> <Count>2</Count> <IdList> <Id>12345678</Id> <Id>87654321</Id> </IdList> </eSearchResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSearchResponse) }); const result = await api.search('covid-19'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('esearch.fcgi') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('email=test%40example.com') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('api_key=test-api-key') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('term=covid-19') ); expect(result).toEqual({ idList: ['12345678', '87654321'], count: 2, retMax: 20, retStart: 0 }); }); it('should handle search options correctly', async () => { const mockSearchResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult> <Count>5</Count> <IdList> <Id>11111111</Id> <Id>22222222</Id> <Id>33333333</Id> <Id>44444444</Id> <Id>55555555</Id> </IdList> </eSearchResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSearchResponse) }); const result = await api.search('machine learning', { retMax: 5, retStart: 10, sort: 'pub_date', dateFrom: '2023/01/01', dateTo: '2023/12/31' }); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('retmax=5') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('retstart=10') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('sort=pub_date') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('machine+learning+AND+%28%222023%2F01%2F01%22%5BDate+-+Publication%5D+%3A+%222023%2F12%2F31%22%5BDate+-+Publication%5D%29') ); expect(result.idList).toHaveLength(5); expect(result.count).toBe(5); }); it('should handle empty search results', async () => { const mockSearchResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult> <Count>0</Count> <IdList></IdList> </eSearchResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSearchResponse) }); const result = await api.search('nonexistent query'); expect(result).toEqual({ idList: [], count: 0, retMax: 20, retStart: 0 }); }); it('should handle HTTP errors', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(api.search('test query')).rejects.toThrow('HTTP error! status: 500'); }); }); describe('fetchArticles', () => { it('should fetch article details', async () => { const mockFetchResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>12345678</PMID> <Article> <ArticleTitle>Test Article Title</ArticleTitle> <Abstract> <AbstractText>This is a test abstract.</AbstractText> </Abstract> <AuthorList> <Author> <LastName>Smith</LastName> <ForeName>John</ForeName> </Author> <Author> <LastName>Doe</LastName> <ForeName>Jane</ForeName> </Author> </AuthorList> <Journal> <Title>Test Journal</Title> <JournalIssue> <PubDate> <Year>2023</Year> <Month>Jan</Month> <Day>15</Day> </PubDate> </JournalIssue> </Journal> </Article> <ELocationID EIdType="doi">10.1234/test.doi</ELocationID> </MedlineCitation> <PubmedData> <ArticleIdList> <ArticleId IdType="pmc">PMC123456</ArticleId> </ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockFetchResponse) }); const result = await api.fetchArticles(['12345678']); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('efetch.fcgi') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('id=12345678') ); expect(result).toHaveLength(1); expect(result[0]).toEqual({ pmid: '12345678', title: 'Test Article Title', authors: ['Smith, John', 'Doe, Jane'], abstract: 'This is a test abstract.', journal: 'Test Journal', pubDate: '2023-Jan-15', doi: '10.1234/test.doi', pmcId: '123456' }); }); it('should handle empty PMID list', async () => { const result = await api.fetchArticles([]); expect(result).toEqual([]); expect(mockFetch).not.toHaveBeenCalled(); }); it('should handle multiple articles', async () => { const mockFetchResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>11111111</PMID> <Article> <ArticleTitle>First Article</ArticleTitle> <Journal><Title>Journal One</Title></Journal> </Article> </MedlineCitation> </PubmedArticle> <PubmedArticle> <MedlineCitation> <PMID>22222222</PMID> <Article> <ArticleTitle>Second Article</ArticleTitle> <Journal><Title>Journal Two</Title></Journal> </Article> </MedlineCitation> </PubmedArticle> </PubmedArticleSet>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockFetchResponse) }); const result = await api.fetchArticles(['11111111', '22222222']); expect(result).toHaveLength(2); expect(result[0].pmid).toBe('11111111'); expect(result[0].title).toBe('First Article'); expect(result[1].pmid).toBe('22222222'); expect(result[1].title).toBe('Second Article'); }); }); describe('searchAndFetch', () => { it('should search and fetch articles in one call', async () => { const mockSearchResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult> <Count>1</Count> <IdList> <Id>12345678</Id> </IdList> </eSearchResult>`; const mockFetchResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>12345678</PMID> <Article> <ArticleTitle>Combined Search and Fetch Test</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> </Article> </MedlineCitation> </PubmedArticle> </PubmedArticleSet>`; mockFetch .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSearchResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockFetchResponse) }); const result = await api.searchAndFetch('test query', { maxResults: 5 }); expect(mockFetch).toHaveBeenCalledTimes(2); expect(result).toHaveLength(1); expect(result[0].pmid).toBe('12345678'); expect(result[0].title).toBe('Combined Search and Fetch Test'); }); }); describe('checkFullTextAvailability', () => { it('should check if full text is available and return PMC ID', async () => { const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>33333333</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> </Article> </MedlineCitation> <PubmedData> <ArticleIdList> <ArticleId IdType="pmc">PMC12345</ArticleId> </ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet>`; const mockElinkResponse = JSON.stringify({ linksets: [{ dbfrom: 'pubmed', ids: ['33333333'], idurlset: { idurl: { url: 'https://example.com/article' } } }] }); // Mock fetchArticles call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }); // Mock elink API call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const results = await api.checkFullTextAvailability(['33333333']); expect(Array.isArray(results)).toBe(true); expect(results.length).toBeGreaterThan(0); const [pmid, availability] = results[0]; expect(pmid).toBe('33333333'); expect(availability.pmcId).toBe('12345'); expect(Array.isArray(availability.links)).toBe(true); expect(availability.links).toContain('https://example.com/article'); }); it('should return empty links when no full text is available', async () => { const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>44444444</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> </Article> </MedlineCitation> </PubmedArticle> </PubmedArticleSet>`; const mockIdConverterResponse = JSON.stringify({ records: [] }); const mockElinkResponse = JSON.stringify({ linksets: [] }); // Mock fetchArticles call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }); // Mock PMC ID Converter call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }); // Mock elink API call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const results = await api.checkFullTextAvailability(['44444444']); expect(Array.isArray(results)).toBe(true); // Empty results or results with empty links are both acceptable }); it('should handle elink API errors gracefully', async () => { const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>55555555</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> </Article> </MedlineCitation> </PubmedArticle> </PubmedArticleSet>`; const mockIdConverterResponse = JSON.stringify({ records: [] }); // Mock fetchArticles call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }); // Mock PMC ID Converter call mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }); // Mock elink API call with error mockFetch.mockResolvedValueOnce({ ok: false, status: 500 }); const results = await api.checkFullTextAvailability(['55555555']); // Should return empty array or handle error gracefully expect(Array.isArray(results)).toBe(true); }); }); describe('getFullText', () => { it('should fetch full text when available', async () => { // Mock fetchArticles (efetch) response - no PMC ID in article data const mockFetchResponse = `<?xml version="1.0" encoding="UTF-8"?> <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>66666666</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal> <Title>Test Journal</Title> <JournalIssue> <PubDate> <Year>2023</Year> </PubDate> </JournalIssue> </Journal> </Article> </MedlineCitation> <PubmedData> <ArticleIdList> </ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet>`; // Mock getPmcIdFromIdConverter (ID Converter) response const mockIdConverterResponse = JSON.stringify({ records: [{ pmid: "66666666", pmcid: "PMC12345" }] }); // Mock getLinksFromId (elink) response - JSON format const mockElinkResponse = JSON.stringify({ linksets: [{ ids: ["66666666"], idurlset: { idurl: [{ url: "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC12345/" }] } }] }); const mockPmcResponse = `<?xml version="1.0" encoding="UTF-8"?> <pmc_articleset> <article> <front> <article-meta> <title-group> <article-title>Test Full Text Article</article-title> </title-group> <abstract> <p>This is the abstract of the test article.</p> </abstract> </article-meta> </front> <body> <sec> <title>Introduction</title> <p>This is the introduction section.</p> </sec> <sec> <title>Methods</title> <p>This is the methods section.</p> </sec> </body> </article> </pmc_articleset>`; mockFetch .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockFetchResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockPmcResponse) }); const results = await api.getFullText(['66666666']); const result = results[0]; expect(mockFetch).toHaveBeenCalledTimes(4); // efetch (pubmed) + idconv + elink + efetch (pmc) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('efetch.fcgi') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('idconv/api/v1/articles') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('elink.fcgi') ); expect(result.pmid).toBe('66666666'); // Full text should be successfully extracted with the improved implementation expect(result.fullText).toContain('Test Full Text Article'); expect(result.fullText).toContain('Introduction'); expect(result.fullText).toContain('Methods'); }); it('should return null when full text is not available', async () => { // Mock fetchArticles response const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSummaryResult> <DocSum> <Id>77777777</Id> <Item Name="PubDate" Type="Date">2023</Item> <Item Name="Title" Type="String">Test Article</Item> </DocSum> </eSummaryResult>`; // Mock getPmcIdFromIdConverter response - no PMC ID found const mockIdConverterResponse = JSON.stringify({ records: [] }); // Mock getLinksFromId response - no links found const mockElinkResponse = JSON.stringify({ linksets: [] }); mockFetch .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const results = await api.getFullText(['77777777']); const result = results[0]; expect(mockFetch).toHaveBeenCalledTimes(3); // esummary + idconv + elink expect(result.pmid).toBe('77777777'); expect(result.fullText).toBeNull(); }); it('should return null when PMC article structure is invalid', async () => { // Mock fetchArticles response const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSummaryResult> <DocSum> <Id>88888888</Id> <Item Name="PubDate" Type="Date">2023</Item> <Item Name="Title" Type="String">Test Article</Item> </DocSum> </eSummaryResult>`; // Mock getPmcIdFromIdConverter response const mockIdConverterResponse = JSON.stringify({ records: [{ pmid: "88888888", pmcid: "PMC12345" }] }); // Mock getLinksFromId response - JSON format const mockElinkResponse = JSON.stringify({ linksets: [{ ids: ["88888888"], idurlset: { idurl: [{ url: "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC12345/" }] } }] }); const mockPmcResponse = `<?xml version="1.0" encoding="UTF-8"?> <pmc_articleset> <invalid_structure> </invalid_structure> </pmc_articleset>`; mockFetch .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockPmcResponse) }); const results = await api.getFullText(['88888888']); const result = results[0]; expect(result.pmid).toBe('88888888'); expect(result.fullText).toBeNull(); }); it('should handle PMC API errors gracefully', async () => { // Mock fetchArticles response const mockSummaryResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSummaryResult> <DocSum> <Id>99999999</Id> <Item Name="PubDate" Type="Date">2023</Item> <Item Name="Title" Type="String">Test Article</Item> </DocSum> </eSummaryResult>`; // Mock getPmcIdFromIdConverter response const mockIdConverterResponse = JSON.stringify({ records: [{ pmid: "99999999", pmcid: "PMC12345" }] }); // Mock getLinksFromId response - JSON format const mockElinkResponse = JSON.stringify({ linksets: [{ ids: ["99999999"], idurlset: { idurl: [{ url: "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC12345/" }] } }] }); mockFetch .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockSummaryResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockIdConverterResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }) .mockResolvedValueOnce({ ok: false, status: 500 }); const results = await api.getFullText(['99999999']); const result = results[0]; expect(result.pmid).toBe('99999999'); expect(result.fullText).toBeNull(); }); }); describe('rate limiting', () => { it('should apply rate limiting for requests without API key', async () => { const apiWithoutKey = createPubMedAPI({ email: 'test@example.com' }); const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult><Count>0</Count><IdList></IdList></eSearchResult>`; mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); const startTime = Date.now(); await apiWithoutKey.search('test1'); await apiWithoutKey.search('test2'); const endTime = Date.now(); // Should take at least 334ms for the second request (rate limiting) expect(endTime - startTime).toBeGreaterThan(300); }); it('should apply faster rate limiting with API key', async () => { const mockResponse = `<?xml version="1.0" encoding="UTF-8"?> <eSearchResult><Count>0</Count><IdList></IdList></eSearchResult>`; mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); const startTime = Date.now(); await api.search('test1'); await api.search('test2'); const endTime = Date.now(); // Should take at least 100ms for the second request (faster rate limiting with API key) expect(endTime - startTime).toBeGreaterThan(90); expect(endTime - startTime).toBeLessThan(300); // But faster than without API key }); }); });

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/ncukondo/pubmed-mcp'

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