MCP Read Images

#!/usr/bin/env node import { promises as fs } from 'fs'; import { resolve, isAbsolute } from 'path'; import sharp from 'sharp'; import fetch from 'node-fetch'; // MCP Types interface ServerInfo { name: string; version: string; } interface ServerCapabilities { tools: Record<string, unknown>; } interface ToolDefinition { name: string; description: string; inputSchema: { type: string; properties: Record<string, unknown>; required: string[]; }; } interface McpRequest<T> { params: T; } interface ListToolsRequest {} interface CallToolRequest { name: string; arguments: Record<string, unknown>; } interface McpResponse<T> { content: Array<{ type: string; text: string; }>; isError?: boolean; } class McpError extends Error { constructor(public code: string, message: string) { super(message); this.name = 'McpError'; } } const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; if (!OPENROUTER_API_KEY) { throw new Error('OPENROUTER_API_KEY environment variable is required'); } async function analyzeImage(imagePath: string, question?: string, model?: string): Promise<string> { // Validate absolute path if (!isAbsolute(imagePath)) { throw new McpError( 'InvalidParams', 'Image path must be absolute' ); } try { const imageBuffer = await fs.readFile(imagePath); console.error('Successfully read image buffer of size:', imageBuffer.length); // Get image metadata const metadata = await sharp(imageBuffer).metadata(); console.error('Image metadata:', metadata); // Calculate dimensions to keep base64 size reasonable const MAX_DIMENSION = 400; const JPEG_QUALITY = 60; let resizedBuffer = imageBuffer; if (metadata.width && metadata.height) { const largerDimension = Math.max(metadata.width, metadata.height); if (largerDimension > MAX_DIMENSION) { const resizeOptions = metadata.width > metadata.height ? { width: MAX_DIMENSION } : { height: MAX_DIMENSION }; resizedBuffer = await sharp(imageBuffer) .resize(resizeOptions) .jpeg({ quality: JPEG_QUALITY }) .toBuffer(); } else { resizedBuffer = await sharp(imageBuffer) .jpeg({ quality: JPEG_QUALITY }) .toBuffer(); } } const base64Image = resizedBuffer.toString('base64'); // Analyze with OpenRouter const requestBody = { model: model || "anthropic/claude-3.5-sonnet", messages: [ { role: "user", content: [ { type: "text", text: question || "What's in this image?" }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${base64Image}` } } ] } ] }; console.error('Sending request to OpenRouter...'); const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/bendichter/read_images', 'X-Title': 'Image Analysis Tool' }, body: JSON.stringify(requestBody) }); console.error('Response status:', response.status); const responseText = await response.text(); console.error('Response text:', responseText); if (!response.ok) { throw new Error(`OpenRouter API error: ${response.statusText}\nDetails: ${responseText}`); } const analysis = JSON.parse(responseText); console.error('OpenRouter API response:', JSON.stringify(analysis, null, 2)); return analysis.choices[0].message.content; } catch (error) { console.error('Error processing image:', error); throw error; } } class ImageAnalysisServer { private info: ServerInfo; private capabilities: ServerCapabilities; private handlers: Map<string, (request: any) => Promise<any>>; constructor() { this.info = { name: 'read-images', version: '0.1.0', }; this.capabilities = { tools: {}, }; this.handlers = new Map(); this.setupHandlers(); process.on('SIGINT', () => { process.exit(0); }); } private setupHandlers(): void { // List Tools Handler this.handlers.set('list_tools', async (_request: McpRequest<ListToolsRequest>) => ({ tools: [ { name: 'analyze_image', description: 'Analyze an image using OpenRouter vision models (default: anthropic/claude-3.5-sonnet)', inputSchema: { type: 'object', properties: { image_path: { type: 'string', description: 'Path to the image file to analyze (must be absolute path)' }, question: { type: 'string', description: 'Question to ask about the image' }, model: { type: 'string', description: 'OpenRouter model to use (e.g., anthropic/claude-3-opus-20240229)' } }, required: ['image_path'] } } ] })); // Call Tool Handler this.handlers.set('call_tool', async (request: McpRequest<CallToolRequest>) => { if (request.params.name !== 'analyze_image') { throw new McpError( 'MethodNotFound', `Unknown tool: ${request.params.name}` ); } const args = request.params.arguments as { image_path: string; question?: string; model?: string; }; try { const result = await analyzeImage(args.image_path, args.question, args.model); return { content: [ { type: 'text', text: result } ] }; } catch (error) { if (error instanceof McpError) { throw error; } return { content: [ { type: 'text', text: `Error analyzing image: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); } async handleRequest(method: string, params: any): Promise<any> { const handler = this.handlers.get(method); if (!handler) { throw new McpError('MethodNotFound', `Unknown method: ${method}`); } return handler({ params }); } async run(): Promise<void> { process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', (chunk: string) => { buffer += chunk; while (true) { const newlineIndex = buffer.indexOf('\n'); if (newlineIndex === -1) break; const line = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); try { const request = JSON.parse(line); this.handleRequest(request.method, request.params) .then(result => { const response = { id: request.id, result }; process.stdout.write(JSON.stringify(response) + '\n'); }) .catch(error => { const response = { id: request.id, error: { code: error instanceof McpError ? error.code : 'InternalError', message: error.message } }; process.stdout.write(JSON.stringify(response) + '\n'); }); } catch (error) { console.error('Error processing request:', error); } } }); console.error('Image Analysis MCP server running on stdio'); } } const server = new ImageAnalysisServer(); server.run().catch(console.error);