#
tokens: 3336/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── examples
│   ├── analyze.js
│   └── sample.jpg
├── LICENSE
├── package.json
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/
package-lock.json

# Build output
build/
dist/

# Environment variables
.env
.env.*

# IDE files
.vscode/
.idea/
*.code-workspace

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Test files
coverage/
.nyc_output/

# Temporary files
*.swp
*.swo
.tmp/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP Read Images

An MCP server for analyzing images using OpenRouter vision models. This server provides a simple interface to analyze images using various vision models like Claude-3.5-sonnet and Claude-3-opus through the OpenRouter API.

## Installation

```bash
npm install @catalystneuro/mcp_read_images
```

## Configuration

The server requires an OpenRouter API key. You can get one from [OpenRouter](https://openrouter.ai/keys).

Add the server to your MCP settings file (usually located at `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` for VSCode):

```json
{
  "mcpServers": {
    "read_images": {
      "command": "read_images",
      "env": {
        "OPENROUTER_API_KEY": "your-api-key-here",
        "OPENROUTER_MODEL": "anthropic/claude-3.5-sonnet"  // optional, defaults to claude-3.5-sonnet
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
```

## Usage

The server provides a single tool `analyze_image` that can be used to analyze images:

```typescript
// Basic usage with default model
use_mcp_tool({
  server_name: "read_images",
  tool_name: "analyze_image",
  arguments: {
    image_path: "/path/to/image.jpg",
    question: "What do you see in this image?"  // optional
  }
});

// Using a specific model for this call
use_mcp_tool({
  server_name: "read_images",
  tool_name: "analyze_image",
  arguments: {
    image_path: "/path/to/image.jpg",
    question: "What do you see in this image?",
    model: "anthropic/claude-3-opus-20240229"  // overrides default and settings
  }
});
```

### Model Selection

The model is selected in the following order of precedence:
1. Model specified in the tool call (`model` argument)
2. Model specified in MCP settings (`OPENROUTER_MODEL` environment variable)
3. Default model (anthropic/claude-3.5-sonnet)

### Supported Models

The following OpenRouter models have been tested:
- anthropic/claude-3.5-sonnet
- anthropic/claude-3-opus-20240229

## Features

- Automatic image resizing and optimization
- Configurable model selection
- Support for custom questions about images
- Detailed error messages
- Automatic JPEG conversion and quality optimization

## Error Handling

The server handles various error cases:
- Invalid image paths
- Missing API keys
- Network errors
- Invalid model selections
- Image processing errors

Each error will return a descriptive message to help diagnose the issue.

## Development

To build from source:

```bash
git clone https://github.com/catalystneuro/mcp_read_images.git
cd mcp_read_images
npm install
npm run build
```

## License

MIT License. See [LICENSE](LICENSE) for details.

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "build",
    "skipLibCheck": true,
    "allowJs": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

```

--------------------------------------------------------------------------------
/examples/analyze.js:
--------------------------------------------------------------------------------

```javascript
// Example script demonstrating how to use the read_images MCP server
import { use_mcp_tool } from '@modelcontextprotocol/sdk';

// Basic usage with default model
const result1 = await use_mcp_tool({
  server_name: "read_images",
  tool_name: "analyze_image",
  arguments: {
    image_path: "./sample.jpg",
    question: "What do you see in this image?"
  }
});

console.log('Analysis with default model:');
console.log(result1);

// Using a specific model
const result2 = await use_mcp_tool({
  server_name: "read_images",
  tool_name: "analyze_image",
  arguments: {
    image_path: "./sample.jpg",
    question: "What colors are most prominent in this image?",
    model: "anthropic/claude-3-opus-20240229"
  }
});

console.log('\nAnalysis with Claude-3-opus:');
console.log(result2);

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@catalystneuro/mcp_read_images",
  "version": "0.1.0",
  "description": "MCP server for analyzing images using OpenRouter vision models",
  "type": "module",
  "main": "build/index.js",
  "bin": {
    "read_images": "./build/index.js"
  },
  "files": [
    "build",
    "README.md",
    "LICENSE",
    "examples"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
    "prepare": "npm run build",
    "start": "node build/index.js",
    "example": "node examples/analyze.js"
  },
  "dependencies": {
    "node-fetch": "^2.6.1",
    "sharp": "^0.32.1"
  },
  "peerDependencies": {
    "@modelcontextprotocol/sdk": "^0.1.0"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "@types/node-fetch": "^2.6.1",
    "@types/sharp": "^0.31.1",
    "typescript": "^4.9.5"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "vision",
    "image-analysis",
    "claude",
    "openrouter"
  ],
  "author": "CatalystNeuro",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/catalystneuro/mcp_read_images.git"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/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);

```