Skip to main content
Glama
analyze-image-tool.test.ts13.3 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { registerAnalyzeImageTool } from '../../src/tools/analyze-image.js'; import { Config } from '../../src/config/index.js'; import { OpenRouterClient } from '../../src/utils/openrouter-client.js'; import { ImageProcessor } from '../../src/utils/image-processor.js'; import { Logger } from '../../src/utils/logger.js'; import type { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; // Mock dependencies vi.mock('../../src/config/index.js'); vi.mock('../../src/utils/openrouter-client.js'); vi.mock('../../src/utils/image-processor.js'); vi.mock('../../src/utils/logger.js'); const MockedConfig = vi.mocked(Config); const MockedOpenRouterClient = vi.mocked(OpenRouterClient); const MockedImageProcessor = vi.mocked(ImageProcessor); const MockedLogger = vi.mocked(Logger); describe('registerAnalyzeImageTool', () => { let mockServer: any; let mockConfig: any; let mockOpenRouterClient: any; let mockImageProcessor: any; let mockLogger: any; beforeEach(() => { vi.clearAllMocks(); // Create mocks mockServer = { setRequestHandler: vi.fn(), }; mockConfig = { getOpenRouterConfig: vi.fn(() => ({ apiKey: 'test-api-key', model: 'test-model', baseUrl: 'https://test.api.com', })), getServerConfig: vi.fn(() => ({ maxImageSize: 10485760, })), }; mockOpenRouterClient = { analyzeImage: vi.fn(), }; mockImageProcessor = { processImage: vi.fn(), isValidImageType: vi.fn(() => true), }; mockLogger = { getInstance: vi.fn(() => ({ info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), })), }; // Setup mock returns MockedConfig.getInstance = vi.fn(() => mockConfig); MockedOpenRouterClient.getInstance = vi.fn(() => mockOpenRouterClient); MockedImageProcessor.getInstance = vi.fn(() => mockImageProcessor); MockedLogger.getInstance = mockLogger.getInstance; // Register the tool registerAnalyzeImageTool(mockServer); }); afterEach(() => { vi.resetAllMocks(); }); describe('tool registration', () => { it('should register the tool handler', () => { expect(mockServer.setRequestHandler).toHaveBeenCalledWith( expect.anything(), expect.any(Function) ); }); }); describe('tool handler', () => { let toolHandler: any; beforeEach(() => { // Get the registered handler const handlerCall = mockServer.setRequestHandler.mock.calls[0]; toolHandler = handlerCall[1]; }); it('should reject unknown tool names', async () => { const request = { params: { name: 'unknown_tool', arguments: {}, }, }; await expect(toolHandler(request)).rejects.toThrow('Unknown tool: unknown_tool'); }); it('should reject requests without arguments', async () => { const request = { params: { name: 'analyze_image', }, }; await expect(toolHandler(request)).rejects.toThrow('Arguments are required'); }); it('should successfully analyze image with base64 input', async () => { const mockProcessedImage = { data: 'base64imagedata', mimeType: 'image/png', size: 1000, }; const mockAnalysisResult = { success: true, analysis: 'This is a detailed image analysis', model: 'test-model', usage: { promptTokens: 50, completionTokens: 100, totalTokens: 150, }, }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', mimeType: 'image/png', prompt: 'Analyze this image', format: 'text', maxTokens: 2000, temperature: 0.5, }, }, }; const result = await toolHandler(request); expect(mockImageProcessor.processImage).toHaveBeenCalledWith({ type: 'base64', data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', mimeType: 'image/png', }); expect(mockOpenRouterClient.analyzeImage).toHaveBeenCalledWith( 'base64imagedata', 'image/png', 'Analyze this image', { format: 'text', maxTokens: 2000, temperature: 0.5, prompt: 'Analyze this image', } ); expect(result).toEqual({ content: [ { type: 'text', text: 'This is a detailed image analysis', }, ], }); }); it('should successfully analyze image with file input', async () => { const mockProcessedImage = { data: 'base64fromfile', mimeType: 'image/jpeg', size: 2000, }; const mockAnalysisResult = { success: true, analysis: 'File image analysis', model: 'test-model', }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'file', data: '/path/to/image.jpg', }, }, }; const result = await toolHandler(request); expect(mockImageProcessor.processImage).toHaveBeenCalledWith({ type: 'file', data: '/path/to/image.jpg', mimeType: undefined, }); expect(mockOpenRouterClient.analyzeImage).toHaveBeenCalledWith( 'base64fromfile', 'image/jpeg', 'Analyze this image in detail. Describe what you see, including objects, people, text, and any notable features.', {} ); expect(result).toEqual({ content: [ { type: 'text', text: 'File image analysis', }, ], }); }); it('should successfully analyze image with URL input', async () => { const mockProcessedImage = { data: 'base64fromurl', mimeType: 'image/webp', size: 3000, }; const mockAnalysisResult = { success: true, analysis: 'URL image analysis', model: 'test-model', }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'url', data: 'https://example.com/image.webp', format: 'json', }, }, }; const result = await toolHandler(request); expect(mockImageProcessor.processImage).toHaveBeenCalledWith({ type: 'url', data: 'https://example.com/image.webp', mimeType: undefined, }); expect(mockOpenRouterClient.analyzeImage).toHaveBeenCalledWith( 'base64fromurl', 'image/webp', 'Analyze this image in detail. Describe what you see, including objects, people, text, and any notable features.', { format: 'json' } ); expect(result).toEqual({ content: [ { type: 'text', text: 'URL image analysis', }, ], }); }); it('should reject unsupported image types', async () => { const mockProcessedImage = { data: 'basedata', mimeType: 'application/pdf', size: 1000, }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockImageProcessor.isValidImageType.mockReturnValue(false); const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'basedata', mimeType: 'application/pdf', }, }, }; const result = await toolHandler(request); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Unsupported image type: application/pdf', }, ], isError: true, }); }); it('should reject images that exceed maximum size', async () => { const mockProcessedImage = { data: 'largedata', mimeType: 'image/png', size: 20971520, // 20MB }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockConfig.getServerConfig.mockReturnValue({ maxImageSize: 10485760 }); // 10MB const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'largedata', }, }, }; const result = await toolHandler(request); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Image size 20971520 exceeds maximum allowed size 10485760', }, ], isError: true, }); }); it('should handle image processing errors', async () => { const processingError = new Error('Failed to process image'); mockImageProcessor.processImage.mockRejectedValue(processingError); const request = { params: { name: 'analyze_image', arguments: { type: 'file', data: '/nonexistent/file.jpg', }, }, }; const result = await toolHandler(request); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to process image', }, ], isError: true, }); // Verify that the error was handled (logger may not be called in test due to mock setup) expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to process image', }, ], isError: true, }); }); it('should handle OpenRouter API errors', async () => { const mockProcessedImage = { data: 'basedata', mimeType: 'image/png', size: 1000, }; const mockAnalysisResult = { success: false, error: 'API rate limit exceeded', }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'basedata', }, }, }; const result = await toolHandler(request); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: API rate limit exceeded', }, ], isError: true, }); }); it('should handle cases where analysis result has no content', async () => { const mockProcessedImage = { data: 'basedata', mimeType: 'image/png', size: 1000, }; const mockAnalysisResult = { success: true, analysis: undefined, }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'basedata', }, }, }; const result = await toolHandler(request); expect(result).toEqual({ content: [ { type: 'text', text: 'No analysis available', }, ], }); }); it('should use default options when not provided', async () => { const mockProcessedImage = { data: 'basedata', mimeType: 'image/png', size: 1000, }; const mockAnalysisResult = { success: true, analysis: 'Analysis with defaults', model: 'test-model', }; mockImageProcessor.processImage.mockResolvedValue(mockProcessedImage); mockOpenRouterClient.analyzeImage.mockResolvedValue(mockAnalysisResult); const request = { params: { name: 'analyze_image', arguments: { type: 'base64', data: 'basedata', }, }, }; await toolHandler(request); expect(mockOpenRouterClient.analyzeImage).toHaveBeenCalledWith( 'basedata', 'image/png', 'Analyze this image in detail. Describe what you see, including objects, people, text, and any notable features.', {} ); }); }); });

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/JonathanJude/openrouter-image-mcp'

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