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

```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package.json
├── README.ja.md
├── README.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

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

```markdown
# image-mcp-server

[日本語の README](README.ja.md)

<a href="https://glama.ai/mcp/servers/@champierre/image-mcp-server">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@champierre/image-mcp-server/badge" alt="Image Analysis MCP Server" />
</a>

[![smithery badge](https://smithery.ai/badge/@champierre/image-mcp-server)](https://smithery.ai/server/@champierre/image-mcp-server)
An MCP server that receives image URLs or local file paths and analyzes image content using the GPT-4o-mini model.

## Features

- Receives image URLs or local file paths as input and provides detailed analysis of the image content
- High-precision image recognition and description using the GPT-4o-mini model
- Image URL validity checking
- Image loading from local files and Base64 encoding

## Installation

### Installing via Smithery

To install Image Analysis Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@champierre/image-mcp-server):

```bash
npx -y @smithery/cli install @champierre/image-mcp-server --client claude
```

### Manual Installation

```bash
# Clone the repository
git clone https://github.com/champierre/image-mcp-server.git # or your forked repository
cd image-mcp-server

# Install dependencies
npm install

# Compile TypeScript
npm run build
```

## Configuration

To use this server, you need an OpenAI API key. Set the following environment variable:

```
OPENAI_API_KEY=your_openai_api_key
```

## MCP Server Configuration

To use with tools like Cline, add the following settings to your MCP server configuration file:

### For Cline

Add the following to `cline_mcp_settings.json`:

```json
{
  "mcpServers": {
    "image-analysis": {
      "command": "node",
      "args": ["/path/to/image-mcp-server/dist/index.js"],
      "env": {
        "OPENAI_API_KEY": "your_openai_api_key"
      }
    }
  }
}
```

### For Claude Desktop App

Add the following to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "image-analysis": {
      "command": "node",
      "args": ["/path/to/image-mcp-server/dist/index.js"],
      "env": {
        "OPENAI_API_KEY": "your_openai_api_key"
      }
    }
  }
}
```

## Usage

Once the MCP server is configured, the following tools become available:

- `analyze_image`: Receives an image URL and analyzes its content.
- `analyze_image_from_path`: Receives a local file path and analyzes its content.

### Usage Examples

**Analyzing from URL:**

```
Please analyze this image URL: https://example.com/image.jpg
```

**Analyzing from local file path:**

```
Please analyze this image: /path/to/your/image.jpg
```

### Note: Specifying Local File Paths

When using the `analyze_image_from_path` tool, the AI assistant (client) must specify a **valid file path in the environment where this server is running**.

- **If the server is running on WSL:**
  - If the AI assistant has a Windows path (e.g., `C:\...`), it needs to convert it to a WSL path (e.g., `/mnt/c/...`) before passing it to the tool.
  - If the AI assistant has a WSL path, it can pass it as is.
- **If the server is running on Windows:**
  - If the AI assistant has a WSL path (e.g., `/home/user/...`), it needs to convert it to a UNC path (e.g., `\\wsl$\Distro\...`) before passing it to the tool.
  - If the AI assistant has a Windows path, it can pass it as is.

**Path conversion is the responsibility of the AI assistant (or its execution environment).** The server will try to interpret the received path as is.

### Note: Type Errors During Build

When running `npm run build`, you may see an error (TS7016) about missing TypeScript type definitions for the `mime-types` module.

```
src/index.ts:16:23 - error TS7016: Could not find a declaration file for module 'mime-types'. ...
```

This is a type checking error, and since the JavaScript compilation itself succeeds, it **does not affect the server's execution**. If you want to resolve this error, install the type definition file as a development dependency.

```bash
npm install --save-dev @types/mime-types
# or
yarn add --dev @types/mime-types
```

## Development

```bash
# Run in development mode
npm run dev
```

## License

MIT

```

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

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package.json package-lock.json* ./

# Install dependencies without running scripts
RUN npm install --ignore-scripts

# Copy rest of the source code
COPY . .

# Build the project (TypeScript compilation)
RUN npm run build

# Expose port if necessary (not specified in MCP, so optional)
# EXPOSE 3000

# Start the MCP server
CMD ["npm", "run", "start"]

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - openaiApiKey
    properties:
      openaiApiKey:
        type: string
        description: Your OpenAI API key required for image analysis.
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'node',
      args: ['dist/index.js'],
      env: { OPENAI_API_KEY: config.openaiApiKey }
    })
  exampleConfig:
    openaiApiKey: your_openai_api_key_here

```

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

```json
{
  "name": "image-mcp-server",
  "version": "1.0.0",
  "description": "MCP server for image analysis using GPT-4o-mini",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node --esm src/index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/champierre/image-mcp-server.git"
  },
  "keywords": [],
  "author": "Junya Ishihara",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/champierre/image-mcp-server/issues"
  },
  "homepage": "https://github.com/champierre/image-mcp-server#readme",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "@types/node": "^22.13.13",
    "axios": "^1.8.4",
    "dotenv": "^16.4.7",
    "mime-types": "^3.0.1",
    "openai": "^4.89.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.8.2"
  },
  "devDependencies": {
    "@types/mime-types": "^2.1.4"
  }
}

```

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

```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { OpenAI } from 'openai';
import axios from 'axios';
import * as dotenv from 'dotenv';
import * as fs from 'fs'; // Import fs for file reading
import * as path from 'path'; // Import path for path operations
import * as os from 'os'; // Import os module
import * as mime from 'mime-types'; // Revert to import statement

// Load environment variables from .env file
dotenv.config();

// Get OpenAI API key from environment variables
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (!OPENAI_API_KEY) {
  throw new Error('OPENAI_API_KEY environment variable is required');
}

// Initialize OpenAI client
const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

// --- Argument Type Guards ---
const isValidAnalyzeImageArgs = (
  args: any
): args is { imageUrl: string } =>
  typeof args === 'object' &&
  args !== null &&
  typeof args.imageUrl === 'string';

const isValidAnalyzeImagePathArgs = (
  args: any
): args is { imagePath: string } => // New type guard for path tool
  typeof args === 'object' &&
  args !== null &&
  typeof args.imagePath === 'string';
// --- End Argument Type Guards ---

class ImageAnalysisServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'image-analysis-server',
        version: '1.1.0', // Version bump
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupToolHandlers();

    // Error handling
    this.server.onerror = (error) => console.error('[MCP Error]', error);
    process.on('SIGINT', async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private setupToolHandlers() {
    // Define tool list
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'analyze_image',
          description: 'Receives an image URL and analyzes the image content using GPT-4o-mini',
          inputSchema: {
            type: 'object',
            properties: {
              imageUrl: {
                type: 'string',
                description: 'URL of the image to analyze',
              },
            },
            required: ['imageUrl'],
          },
        },
        // --- New Tool Definition ---
        {
          name: 'analyze_image_from_path',
          description: 'Loads an image from a local file path and analyzes its content using GPT-4o-mini. AI assistants need to provide a valid path for the server execution environment (e.g., Linux path if the server is running on WSL).',
          inputSchema: {
            type: 'object',
            properties: {
              imagePath: {
                type: 'string',
                description: 'Local file path of the image to analyze (must be accessible from the server execution environment)',
              },
            },
            required: ['imagePath'],
          },
        },
        // --- End New Tool Definition ---
      ],
    }));

    // Tool execution handler
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const toolName = request.params.name;
      const args = request.params.arguments;

      try {
        let analysis: string;

        if (toolName === 'analyze_image') {
          if (!isValidAnalyzeImageArgs(args)) {
            throw new McpError(
              ErrorCode.InvalidParams,
              'Invalid arguments for analyze_image: imageUrl (string) is required'
            );
          }
          const imageUrl = args.imageUrl;
          await this.validateImageUrl(imageUrl); // Validate URL accessibility
          analysis = await this.analyzeImageWithGpt4({ type: 'url', data: imageUrl });

        } else if (toolName === 'analyze_image_from_path') {
          if (!isValidAnalyzeImagePathArgs(args)) {
            throw new McpError(
              ErrorCode.InvalidParams,
              'Invalid arguments for analyze_image_from_path: imagePath (string) is required'
            );
          }
          const imagePath = args.imagePath;
          // Basic security check: prevent absolute paths trying to escape common roots (adjust as needed)
          // This is a VERY basic check and might need refinement based on security requirements.
          if (path.isAbsolute(imagePath) && !imagePath.startsWith(process.cwd()) && !imagePath.startsWith(os.homedir()) && !imagePath.startsWith('/mnt/')) {
             // Allow relative paths, paths within cwd, home, or WSL mounts. Adjust if needed.
             console.warn(`Potential unsafe path access attempt blocked: ${imagePath}`);
             throw new McpError(ErrorCode.InvalidParams, 'Invalid or potentially unsafe imagePath provided.');
          }

          const resolvedPath = path.resolve(imagePath); // Resolve relative paths
          if (!fs.existsSync(resolvedPath)) {
            throw new McpError(ErrorCode.InvalidParams, `File not found at path: ${resolvedPath}`);
          }
          const imageDataBuffer = fs.readFileSync(resolvedPath);
          const base64String = imageDataBuffer.toString('base64');
          const mimeType = mime.lookup(resolvedPath) || 'application/octet-stream'; // Detect MIME type or default

          if (!mimeType.startsWith('image/')) {
             throw new McpError(ErrorCode.InvalidParams, `File is not an image: ${mimeType}`);
          }

          analysis = await this.analyzeImageWithGpt4({ type: 'base64', data: base64String, mimeType: mimeType });

        } else {
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Unknown tool: ${toolName}`
          );
        }

        // Return successful analysis
        return {
          content: [
            {
              type: 'text',
              text: analysis,
            },
          ],
        };

      } catch (error) {
        console.error(`Error calling tool ${toolName}:`, error);
        // Return error content
        return {
          content: [
            {
              type: 'text',
              text: `Tool execution error (${toolName}): ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  // Method to check if the image URL is valid (existing)
  private async validateImageUrl(url: string): Promise<void> {
    try {
      const response = await axios.head(url);
      const contentType = response.headers['content-type'];
      if (!contentType || !contentType.startsWith('image/')) {
        throw new Error(`URL is not an image: ${contentType}`);
      }
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw new Error(`Cannot access image URL: ${error.message}`);
      }
      throw error;
    }
  }

  // Method to analyze images with GPT-4o-mini (modified: accepts URL or Base64)
  private async analyzeImageWithGpt4(
     imageData: { type: 'url', data: string } | { type: 'base64', data: string, mimeType: string }
   ): Promise<string> {
    try {
      let imageInput: any;
      if (imageData.type === 'url') {
        imageInput = { type: 'image_url', image_url: { url: imageData.data } };
      } else {
        // Construct data URI for OpenAI API
        imageInput = { type: 'image_url', image_url: { url: `data:${imageData.mimeType};base64,${imageData.data}` } };
      }

      const response = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [
          {
            role: 'system',
            content: 'Analyze the image content in detail and provide an explanation in English.',
          },
          {
            role: 'user',
            content: [
              { type: 'text', text: 'Please analyze the following image and explain its content in detail.' },
              imageInput, // Use the constructed image input
            ],
          },
        ],
        max_tokens: 1000,
      });

      return response.choices[0]?.message?.content || 'Could not retrieve analysis results.';
    } catch (error) {
      console.error('OpenAI API error:', error);
      throw new Error(`OpenAI API error: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Image Analysis MCP server (v1.1.0) running on stdio'); // Updated version
  }
}

const server = new ImageAnalysisServer();
server.run().catch(console.error);

```