Skip to main content
Glama

PubMed MCP Server

by ncukondo
cache.test.ts14.7 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createPubMedAPI } from '../pubmed-api'; import { promises as fs } from 'fs'; import { join } from 'path'; describe('PubMed API Cache', () => { const testCacheDir = join(process.cwd(), 'test-cache-vitest'); let api: ReturnType<typeof createPubMedAPI>; beforeEach(async () => { // Clean up any existing test cache await fs.rm(testCacheDir, { recursive: true, force: true }).catch(() => {}); // Create API instance with cache enabled api = createPubMedAPI({ email: 'test@example.com', cacheDir: testCacheDir, cacheTTL: 3600 // 1 hour }); }); afterEach(async () => { // Clean up test cache directory await fs.rm(testCacheDir, { recursive: true, force: true }).catch(() => {}); }); describe('Cache Directory Creation', () => { it('should create cache directories when first writing to cache', async () => { // Mock the API response global.fetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(` <eSearchResult> <IdList><Id>12345</Id></IdList> <Count>1</Count> </eSearchResult> `) }); // Mock fetchArticles response const mockFetchResponse = ` <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>12345</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> <AuthorList> <Author> <LastName>Test</LastName> <ForeName>Author</ForeName> </Author> </AuthorList> </Article> </MedlineCitation> <PubmedData> <ArticleIdList></ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet> `; (global.fetch as any) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(`<eSearchResult><IdList><Id>12345</Id></IdList><Count>1</Count></eSearchResult>`) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockFetchResponse) }); await api.searchAndFetch('test query', { maxResults: 1 }); // Check that cache directories were created const summaryDir = join(testCacheDir, 'summary'); const fulltextDir = join(testCacheDir, 'fulltext'); const summaryExists = await fs.access(summaryDir).then(() => true).catch(() => false); const fulltextExists = await fs.access(fulltextDir).then(() => true).catch(() => false); expect(summaryExists).toBe(true); expect(fulltextExists).toBe(true); }); }); describe('Summary Cache', () => { const mockPmid = '12345'; const mockArticle = { pmid: mockPmid, title: 'Test Article Title', authors: ['Doe, John'], abstract: 'Test abstract content', journal: 'Test Journal', pubDate: '2023', doi: '10.1234/test', pmcId: undefined }; beforeEach(() => { // Mock the fetchArticles API response const mockResponse = ` <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>${mockPmid}</PMID> <Article> <ArticleTitle>${mockArticle.title}</ArticleTitle> <Journal><Title>${mockArticle.journal}</Title></Journal> <AuthorList> <Author> <LastName>Doe</LastName> <ForeName>John</ForeName> </Author> </AuthorList> <Abstract> <AbstractText>${mockArticle.abstract}</AbstractText> </Abstract> </Article> </MedlineCitation> <PubmedData> <ArticleIdList> <ArticleId IdType="doi">${mockArticle.doi}</ArticleId> </ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet> `; global.fetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); }); it('should cache article summaries and retrieve them on subsequent calls', async () => { // First call - should fetch from API and cache const result1 = await api.fetchArticles([mockPmid]); expect(result1).toHaveLength(1); expect(result1[0].pmid).toBe(mockPmid); expect(result1[0].title).toBe(mockArticle.title); // Verify cache file was created const cacheFile = join(testCacheDir, 'summary', `${mockPmid}.json`); const cacheExists = await fs.access(cacheFile).then(() => true).catch(() => false); expect(cacheExists).toBe(true); // Verify cache file content const cacheContent = await fs.readFile(cacheFile, 'utf8'); const cacheData = JSON.parse(cacheContent); expect(cacheData.data.pmid).toBe(mockPmid); expect(cacheData.data.title).toBe(mockArticle.title); expect(typeof cacheData.timestamp).toBe('number'); // Reset fetch mock to ensure second call doesn't hit API (global.fetch as any).mockClear(); // Second call - should use cache const result2 = await api.fetchArticles([mockPmid]); expect(result2).toHaveLength(1); expect(result2[0].pmid).toBe(mockPmid); expect(result2[0].title).toBe(mockArticle.title); // Verify no API calls were made expect(global.fetch).not.toHaveBeenCalled(); }); it('should handle cache expiration correctly', async () => { // Create API with very short TTL const shortTtlApi = createPubMedAPI({ email: 'test@example.com', cacheDir: testCacheDir, cacheTTL: 1 // 1 second }); // First call await shortTtlApi.fetchArticles([mockPmid]); // Wait for cache to expire await new Promise(resolve => setTimeout(resolve, 1100)); // Reset fetch mock (global.fetch as any).mockClear(); // Second call after expiration - should fetch from API again await shortTtlApi.fetchArticles([mockPmid]); // Verify API was called again expect(global.fetch).toHaveBeenCalled(); }); }); describe('Full Text Cache', () => { const mockPmid = '12345'; const mockPmcId = '67890'; beforeEach(() => { // Mock elink response for full text availability const mockElinkResponse = ` <eLinkResult> <LinkSet> <IdList><Id>${mockPmid}</Id></IdList> <LinkSetDb> <Link><Id>${mockPmcId}</Id></Link> </LinkSetDb> </LinkSet> </eLinkResult> `; // Mock PMC full text response const mockPmcResponse = ` <pmc-articleset> <article> <front> <article-meta> <title-group> <article-title>Test Article</article-title> </title-group> <abstract>Test abstract</abstract> </article-meta> </front> <body>Test content</body> </article> </pmc-articleset> `; global.fetch = vi.fn() .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockElinkResponse) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(mockPmcResponse) }); }); it('should cache full text and retrieve it on subsequent calls', async () => { // First call - should fetch from API and cache const result1 = await api.getFullText([mockPmid]); expect(result1).toHaveLength(1); expect(result1[0].pmid).toBe(mockPmid); expect(result1[0].fullText).toContain('Test Article'); // Verify cache file was created const cacheFile = join(testCacheDir, 'fulltext', `${mockPmid}.md`); const cacheExists = await fs.access(cacheFile).then(() => true).catch(() => false); expect(cacheExists).toBe(true); // Verify cache file content const cacheContent = await fs.readFile(cacheFile, 'utf8'); expect(cacheContent).toMatch(/^<!-- timestamp: \d+ -->/); expect(cacheContent).toContain('Test Article'); // Reset fetch mock (global.fetch as any).mockClear(); // Second call - should use cache const result2 = await api.getFullText([mockPmid]); expect(result2).toHaveLength(1); expect(result2[0].pmid).toBe(mockPmid); expect(result2[0].fullText).toBe(result1[0].fullText); // Verify no API calls were made expect(global.fetch).not.toHaveBeenCalled(); }); it('should handle full text cache expiration correctly', async () => { // Create API with very short TTL const shortTtlApi = createPubMedAPI({ email: 'test@example.com', cacheDir: testCacheDir, cacheTTL: 1 // 1 second }); // First call await shortTtlApi.getFullText([mockPmid]); // Wait for cache to expire await new Promise(resolve => setTimeout(resolve, 1100)); // Reset fetch mock for second call global.fetch = vi.fn() .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(` <eLinkResult> <LinkSet> <IdList><Id>${mockPmid}</Id></IdList> <LinkSetDb> <Link><Id>${mockPmcId}</Id></Link> </LinkSetDb> </LinkSet> </eLinkResult> `) }) .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(` <pmc-articleset> <article> <front> <article-meta> <title-group> <article-title>Test Article</article-title> </title-group> </article-meta> </front> <body>Test content</body> </article> </pmc-articleset> `) }); // Second call after expiration - should fetch from API again await shortTtlApi.getFullText([mockPmid]); // Verify API was called again expect(global.fetch).toHaveBeenCalled(); }); }); describe('Abstract handling', () => { it('should properly handle multiple AbstractText sections without creating [Object],[Object]', async () => { const mockPmid = '12345'; // Mock response with multiple AbstractText sections (array) const mockResponse = ` <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>${mockPmid}</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> <Abstract> <AbstractText>First section of abstract.</AbstractText> <AbstractText>Second section of abstract.</AbstractText> </Abstract> </Article> </MedlineCitation> <PubmedData> <ArticleIdList></ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet> `; global.fetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); const result = await api.fetchArticles([mockPmid]); expect(result).toHaveLength(1); expect(result[0].abstract).toBe('First section of abstract. Second section of abstract.'); expect(result[0].abstract).not.toContain('[Object]'); // Verify cache content doesn't contain [Object],[Object] const cacheFile = join(testCacheDir, 'summary', `${mockPmid}.json`); const cacheContent = await fs.readFile(cacheFile, 'utf8'); const cacheData = JSON.parse(cacheContent); expect(cacheData.data.abstract).toBe('First section of abstract. Second section of abstract.'); expect(cacheData.data.abstract).not.toContain('[Object]'); }); it('should handle single AbstractText correctly', async () => { const mockPmid = '54321'; // Mock response with single AbstractText const mockResponse = ` <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>${mockPmid}</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> <Abstract> <AbstractText>Single abstract section.</AbstractText> </Abstract> </Article> </MedlineCitation> <PubmedData> <ArticleIdList></ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet> `; global.fetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); const result = await api.fetchArticles([mockPmid]); expect(result).toHaveLength(1); expect(result[0].abstract).toBe('Single abstract section.'); expect(result[0].abstract).not.toContain('[Object]'); }); }); describe('Cache without cacheDir', () => { it('should work normally without caching when cacheDir is not provided', async () => { const noCacheApi = createPubMedAPI({ email: 'test@example.com' // No cacheDir provided }); const mockResponse = ` <PubmedArticleSet> <PubmedArticle> <MedlineCitation> <PMID>12345</PMID> <Article> <ArticleTitle>Test Article</ArticleTitle> <Journal><Title>Test Journal</Title></Journal> </Article> </MedlineCitation> <PubmedData> <ArticleIdList></ArticleIdList> </PubmedData> </PubmedArticle> </PubmedArticleSet> `; global.fetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(mockResponse) }); const result = await noCacheApi.fetchArticles(['12345']); expect(result).toHaveLength(1); expect(result[0].pmid).toBe('12345'); // Verify no cache directories were created const cacheExists = await fs.access(testCacheDir).then(() => true).catch(() => false); expect(cacheExists).toBe(false); }); }); });

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