Skip to main content
Glama

PDF Reader MCP Server

by sylphxltd
readPdf.test.ts30.8 kB
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { resolvePath } from '../../src/utils/pathUtils.js'; import * as pathUtils from '../../src/utils/pathUtils.js'; // Import the module itself for spying // Define a type for the expected structure after JSON.parse interface ExpectedResultType { results: { source: string; success: boolean; data?: object; error?: string }[]; } // --- Mocking pdfjs-dist --- const mockGetMetadata = vi.fn(); const mockGetPage = vi.fn(); const mockGetDocument = vi.fn(); const mockReadFile = vi.fn(); vi.doMock('pdfjs-dist/legacy/build/pdf.mjs', () => { return { getDocument: mockGetDocument, }; }); vi.doMock('node:fs/promises', () => { return { default: { readFile: mockReadFile, }, readFile: mockReadFile, __esModule: true, }; }); // Dynamically import the handler *once* after mocks are defined // Define a more specific type for the handler's return value content interface HandlerResultContent { type: string; text: string; } let handler: (args: unknown) => Promise<{ content: HandlerResultContent[] }>; beforeAll(async () => { // Only import the tool definition now const { readPdfToolDefinition: importedDefinition } = await import( '../../src/handlers/readPdf.js' ); handler = importedDefinition.handler; }); // Renamed describe block as it now only tests the handler describe('handleReadPdfFunc Integration Tests', () => { beforeEach(() => { vi.resetAllMocks(); // Reset mocks for pathUtils if we spy on it vi.spyOn(pathUtils, 'resolvePath').mockImplementation((p) => p); // Simple mock for resolvePath mockReadFile.mockResolvedValue(Buffer.from('mock pdf content')); const mockDocumentAPI = { numPages: 3, getMetadata: mockGetMetadata, getPage: mockGetPage, }; const mockLoadingTaskAPI = { promise: Promise.resolve(mockDocumentAPI) }; mockGetDocument.mockReturnValue(mockLoadingTaskAPI); mockGetMetadata.mockResolvedValue({ info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { _metadataMap: new Map([['dc:format', 'application/pdf']]), get(key: string) { return this._metadataMap.get(key); }, has(key: string) { return this._metadataMap.has(key); }, getAll() { return Object.fromEntries(this._metadataMap); }, }, }); // Removed unnecessary async and eslint-disable comment mockGetPage.mockImplementation((pageNum: number) => { if (pageNum > 0 && pageNum <= mockDocumentAPI.numPages) { return { getTextContent: vi .fn() .mockResolvedValueOnce({ items: [{ str: `Mock page text ${String(pageNum)}` }] }), }; } throw new Error(`Mock getPage error: Invalid page number ${String(pageNum)}`); }); }); // Removed unit tests for parsePageRanges // --- Integration Tests for handleReadPdfFunc --- it('should successfully read full text, metadata, and page count for a local file', async () => { const args = { sources: [{ path: 'test.pdf' }], include_full_text: true, include_metadata: true, include_page_count: true, }; const result = await handler(args); const expectedData = { results: [ { source: 'test.pdf', success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, num_pages: 3, full_text: 'Mock page text 1\n\nMock page text 2\n\nMock page text 3', }, }, ], }; expect(mockReadFile).toHaveBeenCalledWith(resolvePath('test.pdf')); expect(mockGetDocument).toHaveBeenCalledWith(Buffer.from('mock pdf content')); expect(mockGetMetadata).toHaveBeenCalled(); expect(mockGetPage).toHaveBeenCalledTimes(3); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(result.content[0].type).toBe('text'); expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should successfully read specific pages for a local file', async () => { const args = { sources: [{ path: 'test.pdf', pages: [1, 3] }], include_metadata: false, include_page_count: true, }; const result = await handler(args); const expectedData = { results: [ { source: 'test.pdf', success: true, data: { num_pages: 3, page_texts: [ { page: 1, text: 'Mock page text 1' }, { page: 3, text: 'Mock page text 3' }, ], }, }, ], }; expect(mockGetPage).toHaveBeenCalledTimes(2); expect(mockGetPage).toHaveBeenCalledWith(1); expect(mockGetPage).toHaveBeenCalledWith(3); expect(mockReadFile).toHaveBeenCalledWith(resolvePath('test.pdf')); expect(mockGetDocument).toHaveBeenCalledWith(Buffer.from('mock pdf content')); expect(mockGetMetadata).not.toHaveBeenCalled(); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(result.content[0].type).toBe('text'); expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should successfully read specific pages using string range', async () => { const args = { sources: [{ path: 'test.pdf', pages: '1,3-3' }], include_page_count: true, }; const result = await handler(args); const expectedData = { results: [ { source: 'test.pdf', success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, num_pages: 3, page_texts: [ { page: 1, text: 'Mock page text 1' }, { page: 3, text: 'Mock page text 3' }, ], }, }, ], }; // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should successfully read metadata only for a URL', async () => { const testUrl = 'http://example.com/test.pdf'; const args = { sources: [{ url: testUrl }], include_full_text: false, include_metadata: true, include_page_count: false, }; const result = await handler(args); const expectedData = { results: [ { source: testUrl, success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, }, }, ], }; expect(mockReadFile).not.toHaveBeenCalled(); expect(mockGetDocument).toHaveBeenCalledWith({ url: testUrl }); expect(mockGetMetadata).toHaveBeenCalled(); expect(mockGetPage).not.toHaveBeenCalled(); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(result.content[0].type).toBe('text'); expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should handle multiple sources with different options', async () => { const urlSource = 'http://example.com/another.pdf'; const args = { sources: [{ path: 'local.pdf', pages: [1] }, { url: urlSource }], include_full_text: true, include_metadata: true, include_page_count: true, }; // Setup mocks for the second source (URL) const secondMockGetPage = vi.fn().mockImplementation((pageNum: number) => { // Removed unnecessary async if (pageNum === 1) return { getTextContent: vi.fn().mockResolvedValue({ items: [{ str: 'URL Mock page text 1' }] }), }; if (pageNum === 2) return { getTextContent: vi.fn().mockResolvedValue({ items: [{ str: 'URL Mock page text 2' }] }), }; throw new Error(`Mock getPage error: Invalid page number ${String(pageNum)}`); }); const secondMockGetMetadata = vi.fn().mockResolvedValue({ // Separate metadata mock if needed info: { Title: 'URL PDF' }, metadata: { getAll: () => ({ 'dc:creator': 'URL Author' }) }, }); const secondMockDocumentAPI = { numPages: 2, getMetadata: secondMockGetMetadata, // Use separate metadata mock getPage: secondMockGetPage, }; const secondLoadingTaskAPI = { promise: Promise.resolve(secondMockDocumentAPI) }; // Reset getDocument mock before setting implementation mockGetDocument.mockReset(); // Mock getDocument based on input source mockGetDocument.mockImplementation((source: Buffer | { url: string }) => { // Check if source is not a Buffer and has the matching url property if (typeof source === 'object' && !Buffer.isBuffer(source) && source.url === urlSource) { return secondLoadingTaskAPI; } // Default mock for path-based source (local.pdf) const defaultMockDocumentAPI = { numPages: 3, getMetadata: mockGetMetadata, // Use original metadata mock getPage: mockGetPage, // Use original page mock }; return { promise: Promise.resolve(defaultMockDocumentAPI) }; }); const result = await handler(args); const expectedData = { results: [ { source: 'local.pdf', success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, num_pages: 3, page_texts: [{ page: 1, text: 'Mock page text 1' }], }, }, { source: urlSource, success: true, data: { // Use the metadata returned by secondMockGetMetadata info: { Title: 'URL PDF' }, metadata: { 'dc:creator': 'URL Author' }, num_pages: 2, full_text: 'URL Mock page text 1\n\nURL Mock page text 2', }, }, ], }; expect(mockReadFile).toHaveBeenCalledOnce(); expect(mockReadFile).toHaveBeenCalledWith(resolvePath('local.pdf')); expect(mockGetDocument).toHaveBeenCalledTimes(2); expect(mockGetDocument).toHaveBeenCalledWith(Buffer.from('mock pdf content')); expect(mockGetDocument).toHaveBeenCalledWith({ url: urlSource }); expect(mockGetPage).toHaveBeenCalledTimes(1); // Should be called once for local.pdf page 1 expect(secondMockGetPage).toHaveBeenCalledTimes(2); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); // --- Error Handling Tests --- it('should return error if local file not found', async () => { const error = new Error('Mock ENOENT') as NodeJS.ErrnoException; error.code = 'ENOENT'; mockReadFile.mockRejectedValue(error); const args = { sources: [{ path: 'nonexistent.pdf' }] }; const result = await handler(args); const expectedData = { results: [ { source: 'nonexistent.pdf', success: false, error: `MCP error -32600: File not found at 'nonexistent.pdf'.`, // Corrected expected error message }, ], }; // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should return error if pdfjs fails to load document', async () => { const loadError = new Error('Mock PDF loading failed'); const failingLoadingTask = { promise: Promise.reject(loadError) }; mockGetDocument.mockReturnValue(failingLoadingTask); const args = { sources: [{ path: 'bad.pdf' }] }; const result = await handler(args); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Check that the error message includes the source description expect(parsedResult.results[0].error).toBe( `MCP error -32600: Failed to load PDF document from bad.pdf. Reason: ${loadError.message}` ); } } else { expect.fail('result.content[0] was undefined'); } }); it('should throw McpError for invalid input arguments (Zod error)', async () => { const args = { sources: [{ path: 'test.pdf' }], include_full_text: 'yes' }; await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( /Invalid arguments: include_full_text \(Expected boolean, received string\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); // Test case for the initial Zod parse failure it('should throw McpError if top-level argument parsing fails', async () => { const invalidArgs = { invalid_prop: true }; // Completely wrong structure await expect(handler(invalidArgs)).rejects.toThrow(McpError); await expect(handler(invalidArgs)).rejects.toThrow(/Invalid arguments: sources \(Required\)/); // Example Zod error await expect(handler(invalidArgs)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); // Updated test: Expect Zod validation to throw McpError directly it('should throw McpError for invalid page specification string (Zod)', async () => { const args = { sources: [{ path: 'test.pdf', pages: '1,abc,3' }] }; await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( /Invalid arguments: sources.0.pages \(Page string must contain only numbers, commas, and hyphens.\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); // Updated test: Expect Zod validation to throw McpError directly it('should throw McpError for invalid page specification array (non-positive - Zod)', async () => { const args = { sources: [{ path: 'test.pdf', pages: [1, 0, 3] }] }; await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( /Invalid arguments: sources.0.pages.1 \(Number must be greater than 0\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); // Test case for resolvePath failure within the catch block it('should return error if resolvePath fails', async () => { const resolveError = new Error('Mock resolvePath failed'); vi.spyOn(pathUtils, 'resolvePath').mockImplementation(() => { throw resolveError; }); const args = { sources: [{ path: 'some/path' }] }; const result = await handler(args); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Error now includes MCP code and different phrasing expect(parsedResult.results[0].error).toBe( `MCP error -32600: Failed to prepare PDF source some/path. Reason: ${resolveError.message}` ); } } else { expect.fail('result.content[0] was undefined'); } }); // Test case for the final catch block with a generic error it('should handle generic errors during processing', async () => { const genericError = new Error('Something unexpected happened'); mockReadFile.mockRejectedValue(genericError); // Simulate error after path resolution const args = { sources: [{ path: 'generic/error/path' }] }; const result = await handler(args); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Error now includes MCP code and different phrasing expect(parsedResult.results[0].error).toBe( `MCP error -32600: Failed to prepare PDF source generic/error/path. Reason: ${genericError.message}` ); } } else { expect.fail('result.content[0] was undefined'); } }); // Test case for the final catch block with a non-Error object it('should handle non-Error exceptions during processing', async () => { const nonError = { message: 'Just an object', code: 'UNEXPECTED' }; mockReadFile.mockRejectedValue(nonError); // Simulate error after path resolution const args = { sources: [{ path: 'non/error/path' }] }; const result = await handler(args); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Use JSON.stringify for non-Error objects // Error now includes MCP code and different phrasing, and stringifies [object Object] expect(parsedResult.results[0].error).toBe( `MCP error -32600: Failed to prepare PDF source non/error/path. Reason: [object Object]` ); } } else { expect.fail('result.content[0] was undefined'); } }); it('should include warnings for requested pages exceeding total pages', async () => { const args = { sources: [{ path: 'test.pdf', pages: [1, 4, 5] }], include_page_count: true, }; const result = await handler(args); const expectedData = { results: [ { source: 'test.pdf', success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, num_pages: 3, page_texts: [{ page: 1, text: 'Mock page text 1' }], warnings: ['Requested page numbers 4, 5 exceed total pages (3).'], }, }, ], }; expect(mockGetPage).toHaveBeenCalledTimes(1); expect(mockGetPage).toHaveBeenCalledWith(1); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); } }); it('should handle errors during page processing gracefully when specific pages are requested', async () => { // Removed unnecessary async and eslint-disable comment mockGetPage.mockImplementation((pageNum: number) => { if (pageNum === 1) return { getTextContent: vi.fn().mockResolvedValueOnce({ items: [{ str: `Mock page text 1` }] }), }; if (pageNum === 2) throw new Error('Failed to get page 2'); if (pageNum === 3) return { getTextContent: vi.fn().mockResolvedValueOnce({ items: [{ str: `Mock page text 3` }] }), }; throw new Error(`Mock getPage error: Invalid page number ${String(pageNum)}`); }); const args = { sources: [{ path: 'test.pdf', pages: [1, 2, 3] }], }; const result = await handler(args); const expectedData = { results: [ { source: 'test.pdf', success: true, data: { info: { PDFFormatVersion: '1.7', Title: 'Mock PDF' }, metadata: { 'dc:format': 'application/pdf' }, num_pages: 3, page_texts: [ { page: 1, text: 'Mock page text 1' }, { page: 2, text: 'Error processing page: Failed to get page 2' }, { page: 3, text: 'Mock page text 3' }, ], }, }, ], }; expect(mockGetPage).toHaveBeenCalledTimes(3); // Add check for content existence and access safely expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { expect(JSON.parse(result.content[0].text) as ExpectedResultType).toEqual(expectedData); } else { expect.fail('result.content[0] was undefined'); it('should return error if pdfjs fails to load document from URL', async () => { const testUrl = 'http://example.com/bad-url.pdf'; const loadError = new Error('Mock URL PDF loading failed'); const failingLoadingTask = { promise: Promise.reject(loadError) }; // Ensure getDocument is mocked specifically for this URL mockGetDocument.mockReset(); // Reset previous mocks if necessary // Explicitly type source as unknown and use stricter type guards/assertions mockGetDocument.mockImplementation((source: unknown) => { if ( typeof source === 'object' && source !== null && Object.prototype.hasOwnProperty.call(source, 'url') && // Use safer check typeof (source as { url?: unknown }).url === 'string' && // Assert type for check (source as { url: string }).url === testUrl // Assert type for comparison ) { return failingLoadingTask; } // Fallback for other potential calls in the test suite const mockDocumentAPI = { numPages: 1, getMetadata: vi.fn(), getPage: vi.fn() }; return { promise: Promise.resolve(mockDocumentAPI) }; }); const args = { sources: [{ url: testUrl }] }; const result = await handler(args); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].source).toBe(testUrl); // Check source description (line 168) expect(parsedResult.results[0].success).toBe(false); expect(parsedResult.results[0].error).toBe( `MCP error -32600: Failed to load PDF document. Reason: ${loadError.message}` ); } } else { expect.fail('result.content[0] was undefined'); } }); } }); // --- Additional Coverage Tests --- it('should not include page count when include_page_count is false', async () => { const args = { sources: [{ path: 'test.pdf' }], include_page_count: false, // Explicitly false include_metadata: false, // Keep it simple include_full_text: false, }; const result = await handler(args); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]?.data) { expect(parsedResult.results[0].success).toBe(true); expect(parsedResult.results[0].data).not.toHaveProperty('num_pages'); expect(parsedResult.results[0].data).not.toHaveProperty('metadata'); expect(parsedResult.results[0].data).not.toHaveProperty('info'); } } else { expect.fail('result.content[0] was undefined'); } expect(mockGetMetadata).not.toHaveBeenCalled(); // Because include_metadata is false }); it('should handle ENOENT error where resolvePath also fails in catch block', async () => { const enoentError = new Error('Mock ENOENT') as NodeJS.ErrnoException; enoentError.code = 'ENOENT'; const resolveError = new Error('Mock resolvePath failed in catch'); const targetPath = 'enoent/and/resolve/fails.pdf'; // Mock resolvePath: first call succeeds, second call (in catch) fails vi.spyOn(pathUtils, 'resolvePath') .mockImplementationOnce((p) => p) // First call succeeds .mockImplementationOnce(() => { // Second call throws throw resolveError; }); mockReadFile.mockRejectedValue(enoentError); const args = { sources: [{ path: targetPath }] }; const result = await handler(args); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Check for the specific error message from lines 323-324 // Error message changed due to refactoring of the catch block expect(parsedResult.results[0].error).toBe( `MCP error -32600: File not found at '${targetPath}'.` ); } } else { expect.fail('result.content[0] was undefined'); } // Ensure readFile was called with the path that resolvePath initially returned expect(mockReadFile).toHaveBeenCalledWith(targetPath); // Ensure resolvePath was called twice (once before readFile, once in catch) expect(pathUtils.resolvePath).toHaveBeenCalledTimes(1); // Only called once before readFile attempt }); // --- Additional Error Coverage Tests --- it('should return error for invalid page range string (e.g., 5-3)', async () => { const args = { sources: [{ path: 'test.pdf', pages: '1,5-3,7' }] }; const result = await handler(args); // Expect promise to resolve // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (result.content?.[0]) { const parsedResult = JSON.parse(result.content[0].text) as ExpectedResultType; expect(parsedResult.results[0]).toBeDefined(); if (parsedResult.results[0]) { expect(parsedResult.results[0].success).toBe(false); // Error message changed slightly due to refactoring expect(parsedResult.results[0].error).toMatch( /Invalid page specification for source test.pdf: Invalid page range values: 5-3/ ); // Check the error code embedded in the message if needed, or just the message content } } else { expect.fail('result.content[0] was undefined'); } }); it('should throw McpError for invalid page number string (e.g., 1,a,3)', async () => { const args = { sources: [{ path: 'test.pdf', pages: '1,a,3' }] }; // Zod catches this first due to refine await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( // Escaped backslash for JSON /Invalid arguments: sources.0.pages \(Page string must contain only numbers, commas, and hyphens.\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); // Test Zod refinement for path/url exclusivity it('should throw McpError if source has both path and url', async () => { const args = { sources: [{ path: 'test.pdf', url: 'http://example.com' }] }; await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( // Escaped backslash for JSON /Invalid arguments: sources.0 \(Each source must have either 'path' or 'url', but not both.\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); it('should throw McpError if source has neither path nor url', async () => { const args = { sources: [{ pages: [1] }] }; // Missing path and url await expect(handler(args)).rejects.toThrow(McpError); await expect(handler(args)).rejects.toThrow( // Escaped backslash for JSON /Invalid arguments: sources.0 \(Each source must have either 'path' or 'url', but not both.\)/ ); await expect(handler(args)).rejects.toHaveProperty('code', ErrorCode.InvalidParams); }); }); // End top-level describe

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/sylphxltd/pdf-reader-mcp'

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