Skip to main content
Glama

Readwise MCP Server

by IAmAlexander
get-highlights.test.ts7.54 kB
import { GetHighlightsTool } from '../../src/tools/get-highlights.js'; import { ReadwiseAPI } from '../../src/api/readwise-api.js'; import { Logger } from '../../src/utils/logger.js'; import { Highlight, PaginatedResponse } from '../../src/types/index.js'; import { validateNumberRange } from '../../src/types/validation.js'; // Get mocked instances of validateNumberRange // This will make our tests match what the implementation actually does jest.mock('../../src/types/validation', () => { return { validateNumberRange: jest.fn().mockImplementation((params, field, min, max, message) => { // Mock implementation that ensures it handles validation correctly // Skip validation if field is not present if (params[field] === undefined || params[field] === null) { return { success: true }; } const value = Number(params[field]); if (isNaN(value)) { return { success: false, errors: [{ field, message: message || `${field} must be a number` }] }; } if (min !== undefined && value < min) { return { success: false, errors: [{ field, message: message || `${field} must be at least ${min}` }] }; } if (max !== undefined && value > max) { return { success: false, errors: [{ field, message: message || `${field} must be at most ${max}` }] }; } return { success: true }; }), validationSuccess: jest.fn().mockReturnValue({ success: true }), validationError: jest.fn().mockImplementation((field, message) => { return { success: false, errors: [{ field, message }] }; }) }; }); // Mocking ReadwiseAPI and Logger jest.mock('../../src/api/readwise-api'); jest.mock('../../src/utils/logger'); describe('GetHighlightsTool', () => { let tool: GetHighlightsTool; let mockApi: jest.Mocked<ReadwiseAPI>; let mockLogger: jest.Mocked<Logger>; beforeEach(() => { jest.clearAllMocks(); // Create mock API mockApi = { getHighlights: jest.fn() } as unknown as jest.Mocked<ReadwiseAPI>; // Create mock logger mockLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } as unknown as jest.Mocked<Logger>; // Create the tool tool = new GetHighlightsTool(mockApi, mockLogger); }); it('should have the correct name and description', () => { expect(tool.name).toBe('get_highlights'); expect(tool.description).toBe('Retrieve highlights from your Readwise library'); expect(tool.parameters).toHaveProperty('properties.page'); expect(tool.parameters).toHaveProperty('properties.page_size'); }); it('should validate parameters correctly', () => { // Reset mock before we start (validateNumberRange as jest.Mock).mockReset(); // Test valid parameters expect(tool.validate({})).toEqual(expect.objectContaining({ success: true })); // Test that validate calls validateNumberRange with expected parameters // Test each validation case individually // Test page parameter validations (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page: 1 }); expect(validateNumberRange).toHaveBeenCalledWith( { page: 1 }, 'page', 1, undefined, 'Page must be a positive number' ); // For page: 0, the validation won't occur due to JavaScript treating 0 as falsy // in the !params.page condition in the validate method. (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page: 0 }); expect(validateNumberRange).not.toHaveBeenCalled(); (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page: -1 }); expect(validateNumberRange).toHaveBeenCalledWith( { page: -1 }, 'page', 1, undefined, 'Page must be a positive number' ); // Test page_size parameter validations (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page_size: 10 }); expect(validateNumberRange).toHaveBeenCalledWith( { page_size: 10 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100' ); // For page_size: 0, the validation won't occur due to JavaScript treating 0 as falsy // in the !params.page_size condition in the validate method. (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page_size: 0 }); expect(validateNumberRange).not.toHaveBeenCalled(); (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page_size: -10 }); expect(validateNumberRange).toHaveBeenCalledWith( { page_size: -10 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100' ); (validateNumberRange as jest.Mock).mockReset(); tool.validate({ page_size: 101 }); expect(validateNumberRange).toHaveBeenCalledWith( { page_size: 101 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100' ); }); it('should execute and return highlights', async () => { // Set up mock response const mockResponse: PaginatedResponse<Highlight> = { count: 2, next: null, previous: null, results: [ { id: '1', text: 'Test Highlight 1', book_id: '123', location: 42, location_type: 'page', highlighted_at: '2023-01-01T12:00:00Z', updated_at: '2023-01-01T12:00:00Z', created_at: '2023-01-01T12:00:00Z', color: 'yellow', note: '', tags: ['test'], url: 'https://example.com' }, { id: '2', text: 'Test Highlight 2', book_id: '123', location: 84, location_type: 'page', highlighted_at: '2023-01-02T12:00:00Z', updated_at: '2023-01-02T12:00:00Z', created_at: '2023-01-02T12:00:00Z', color: 'blue', note: 'This is a note', tags: ['test', 'important'], url: 'https://example.com' } ] }; mockApi.getHighlights.mockResolvedValue(mockResponse); // Execute the tool const result = await tool.execute({}); // Verify the result expect(result).toEqual({ result: mockResponse }); expect(mockApi.getHighlights).toHaveBeenCalledWith({}); expect(mockLogger.debug).toHaveBeenCalled(); }); it('should handle API errors correctly', async () => { // Set up mock error const apiError = { type: 'api', details: { status: 401, code: 'api_error', message: 'Invalid token' } }; mockApi.getHighlights.mockRejectedValue(apiError); // Execute the tool and expect it to reject await expect(tool.execute({})).rejects.toEqual(apiError); expect(mockLogger.error).toHaveBeenCalled(); }); it('should handle unexpected errors correctly', async () => { // Spy on the error logger mockLogger.error = jest.fn(); // Set up the API mock to throw an unexpected error mockApi.getHighlights.mockImplementation(() => { throw new Error('Unexpected error'); }); // Execute the tool and expect it to not throw but return error in the result const result = await tool.execute({}); expect(result).toEqual({ result: { count: 0, next: null, previous: null, results: [] }, success: false, error: 'An unexpected error occurred while fetching highlights' }); expect(mockLogger.error).toHaveBeenCalled(); }); });

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/IAmAlexander/readwise-mcp'

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