Skip to main content
Glama

exif-mcp

BSD 2-Clause "Simplified" License
19
tools.test.ts13.1 kB
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import exifr from 'exifr'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { registerTools } from '../src/tools/index.js'; // Get current directory for referencing fixtures const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.join(__dirname, 'fixtures'); const sampleJpegPath = path.join(fixturesDir, 'sample.jpg'); // Mock exifr functions vi.mock('exifr', () => { return { parse: vi.fn(), orientation: vi.fn(), rotation: vi.fn(), gps: vi.fn(), thumbnail: vi.fn() }; }); describe('MCP Tools', () => { let server: McpServer; let tools: Record<string, any>; beforeAll(() => { // Create server instance for testing server = new McpServer({ name: 'exif-mcp-test', version: '1.0.0' }); // Register tools and get references to them tools = registerTools(server); // Ensure fixture directory exists if (!fs.existsSync(fixturesDir)) { fs.mkdirSync(fixturesDir, { recursive: true }); } // Create a simple test JPEG if it doesn't exist if (!fs.existsSync(sampleJpegPath)) { // This is a minimal valid JPEG (1x1 pixel) const minimalJpeg = Buffer.from( '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==', 'base64' ); fs.writeFileSync(sampleJpegPath, minimalJpeg); } }); afterEach(() => { // Reset mocks between tests vi.clearAllMocks(); }); describe('read-metadata tool', () => { it('should call exifr.parse with all segments enabled by default', async () => { // Setup mock return value const mockMetadata = { Make: 'Test Camera', Model: 'Test Model', xmp: { Title: 'Test Image' } }; (exifr.parse as any).mockResolvedValue(mockMetadata); // Get the tool from the registered tools const tool = tools['read-metadata']; expect(tool).toBeDefined(); // Call the tool const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); // Verify exifr.parse was called with correct options expect(exifr.parse).toHaveBeenCalledTimes(1); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: true, xmp: true, icc: true, iptc: true, jfif: true, ihdr: true }); // Verify response expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockMetadata); }); it('should call exifr.parse with specific segments when requested', async () => { // Setup mock return value const mockMetadata = { xmp: { Title: 'Test Image' } }; (exifr.parse as any).mockResolvedValue(mockMetadata); // Get the tool from the registered tools const tool = tools['read-metadata']; expect(tool).toBeDefined(); // Call the tool with specific segments const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath }, segments: ['XMP', 'ICC'] }, { request: {} } ); // Verify exifr.parse was called with correct options expect(exifr.parse).toHaveBeenCalledTimes(1); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: false, xmp: true, icc: true, iptc: false, jfif: false, ihdr: false }); // Verify response expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockMetadata); }); it('should handle error when no metadata is found', async () => { // Setup mock to return empty object (no metadata) (exifr.parse as any).mockResolvedValue({}); // Get the tool from the tools object const tool = tools['read-metadata']; // Call the tool should error const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('No metadata found'); }); it('should propagate exifr errors', async () => { // Setup mock to throw an error (exifr.parse as any).mockRejectedValue(new Error('Parse error')); // Get the tool from the tools object const tool = tools['read-metadata']; // Call the tool should error const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parse error'); }); }); describe('read-exif tool', () => { it('should call exifr.parse with EXIF options', async () => { // Setup mock return value const mockExif = { Make: 'Test Camera', Model: 'Test Model', ExposureTime: 1/250 }; (exifr.parse as any).mockResolvedValue(mockExif); // Get the tool from the tools object const tool = tools['read-exif']; expect(tool).toBeDefined(); // Call the tool const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); // Verify exifr.parse was called with correct options expect(exifr.parse).toHaveBeenCalledTimes(1); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: true, xmp: false, icc: false, iptc: false, jfif: false, ihdr: false }); // Verify response expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockExif); }); it('should support picking specific EXIF tags', async () => { // Setup mock return value const mockExif = { Make: 'Test Camera' }; (exifr.parse as any).mockResolvedValue(mockExif); // Get the tool from the tools object const tool = tools['read-exif']; // Call the tool with pick list const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath }, pick: ['Make'] }, { request: {} } ); // Verify exifr.parse was called with correct options expect(exifr.parse).toHaveBeenCalledTimes(1); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: true, xmp: false, icc: false, iptc: false, jfif: false, ihdr: false, pick: ['Make'] }); // Verify response expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockExif); }); }); describe('Segment-specific tools', () => { it('should configure read-xmp correctly', async () => { const mockXmp = { xmp: { Title: 'Test Image' } }; (exifr.parse as any).mockResolvedValue(mockXmp); const tool = tools['read-xmp']; expect(tool).toBeDefined(); // Test with extended option const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath }, extended: true }, { request: {} } ); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: false, xmp: true, icc: false, iptc: false, jfif: false, ihdr: false, multiSegment: true }); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockXmp); }); it('should configure read-icc correctly', async () => { const mockIcc = { icc: { Description: 'Test ICC Profile' } }; (exifr.parse as any).mockResolvedValue(mockIcc); const tool = tools['read-icc']; expect(tool).toBeDefined(); const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(exifr.parse).toHaveBeenCalledWith(expect.any(Buffer), { tiff: false, xmp: false, icc: true, iptc: false, jfif: false, ihdr: false }); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockIcc); }); }); describe('Specialized tools', () => { it('should call exifr.orientation correctly', async () => { (exifr.orientation as any).mockResolvedValue(6); const tool = tools['orientation']; expect(tool).toBeDefined(); const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(exifr.orientation).toHaveBeenCalledWith(expect.any(Buffer)); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual({ orientation: 6 }); }); it('should call exifr.rotation correctly', async () => { const mockRotation = { deg: 90, rad: Math.PI/2, scaleX: 1, scaleY: 1, dimensionSwapped: true }; (exifr.rotation as any).mockResolvedValue(mockRotation); const tool = tools['rotation-info']; expect(tool).toBeDefined(); const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(exifr.rotation).toHaveBeenCalledWith(expect.any(Buffer)); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockRotation); }); it('should call exifr.gps correctly', async () => { const mockGps = { latitude: 37.7749, longitude: -122.4194 }; (exifr.gps as any).mockResolvedValue(mockGps); const tool = tools['gps-coordinates']; expect(tool).toBeDefined(); const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(exifr.gps).toHaveBeenCalledWith(expect.any(Buffer)); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toEqual(mockGps); }); it('should handle null GPS data gracefully', async () => { (exifr.gps as any).mockResolvedValue(null); const tool = tools['gps-coordinates']; const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toBeNull(); }); it('should call exifr.thumbnail correctly and return base64 data URL', async () => { // Create mock thumbnail data const mockThumbnail = Buffer.from('fake-thumbnail-data'); (exifr.thumbnail as any).mockResolvedValue(mockThumbnail); const tool = tools['thumbnail']; expect(tool).toBeDefined(); const result = await tool.callback( { image: { kind: 'path', path: sampleJpegPath } }, { request: {} } ); expect(exifr.thumbnail).toHaveBeenCalledWith(expect.any(Buffer)); expect(result.content[0].type).toBe('text'); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.dataUrl).toMatch(/^data:image\/jpeg;base64,/); }); }); });

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/stass/exif-mcp'

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