Skip to main content
Glama

PubMed MCP Server

by ncukondo
pubmed-api.test.ts17.9 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: 'PMC123456' }); }); 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 mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> <LinkSetDb> <DbTo>pmc</DbTo> <LinkName>pubmed_pmc</LinkName> <Link> <Id>12345</Id> </Link> </LinkSetDb> </LinkSet> </eLinkResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const result = await api.checkFullTextAvailability('33333333'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('elink.fcgi') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('dbfrom=pubmed') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('db=pmc') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('id=33333333') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('linkname=pubmed_pmc') ); expect(result).toEqual({ hasFullText: true, pmcId: '12345' }); }); it('should return false when no full text is available', async () => { const mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> </LinkSet> </eLinkResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const result = await api.checkFullTextAvailability('44444444'); expect(result).toEqual({ hasFullText: false }); }); it('should handle elink API errors gracefully', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500 }); const result = await api.checkFullTextAvailability('55555555'); expect(result).toEqual({ hasFullText: false }); }); }); describe('getFullText', () => { it('should fetch full text when available', async () => { const mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> <LinkSetDb> <Link> <Id>12345</Id> </Link> </LinkSetDb> </LinkSet> </eLinkResult>`; 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(mockElinkResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockPmcResponse) }); const results = await api.getFullText(['66666666']); const result = results[0]; expect(mockFetch).toHaveBeenCalledTimes(2); // elink call + efetch call for PMC content expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('elink.fcgi') ); // Only elink call is made in current implementation 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 () => { const mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> </LinkSet> </eLinkResult>`; mockFetch.mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }); const results = await api.getFullText(['77777777']); const result = results[0]; expect(mockFetch).toHaveBeenCalledTimes(1); expect(result.pmid).toBe('77777777'); expect(result.fullText).toBeNull(); }); it('should return null when PMC article structure is invalid', async () => { const mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> <LinkSetDb> <Link> <Id>12345</Id> </Link> </LinkSetDb> </LinkSet> </eLinkResult>`; const mockPmcResponse = `<?xml version="1.0" encoding="UTF-8"?> <pmc_articleset> <invalid_structure> </invalid_structure> </pmc_articleset>`; mockFetch .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 () => { const mockElinkResponse = `<?xml version="1.0" encoding="UTF-8"?> <eLinkResult> <LinkSet> <LinkSetDb> <Link> <Id>12345</Id> </Link> </LinkSetDb> </LinkSet> </eLinkResult>`; mockFetch .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 }); }); });

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