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

```
├── .gitignore
├── create-test-image.js
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── index.ts
├── test-images
│   └── test-image.jpg
└── tsconfig.json
```

# Files

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

```
node_modules/
build/
*.log
.env*
```

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

```markdown
# image-reader MCP Server

image reader

This is a TypeScript-based MCP server that implements a simple notes system. It demonstrates core MCP concepts by providing:

- Resources representing text notes with URIs and metadata
- Tools for creating new notes
- Prompts for generating summaries of notes

## Features

### Resources
- List and access notes via `note://` URIs
- Each note has a title, content and metadata
- Plain text mime type for simple content access

### Tools
- `create_note` - Create new text notes
  - Takes title and content as required parameters
  - Stores note in server state

### Prompts
- `summarize_notes` - Generate a summary of all stored notes
  - Includes all note contents as embedded resources
  - Returns structured prompt for LLM summarization

## Development

Install dependencies:
```bash
npm install
```

Build the server:
```bash
npm run build
```

For development with auto-rebuild:
```bash
npm run watch
```

## Installation

To use with Claude Desktop, add the server config:

On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "image-reader": {
      "command": "/path/to/image-reader/build/index.js"
    }
  }
}
```

### Debugging

Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:

```bash
npm run inspector
```

The Inspector will provide a URL to access debugging tools in your browser.

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/create-test-image.js:
--------------------------------------------------------------------------------

```javascript
import sharp from 'sharp';

// Create a simple test image
const width = 300;
const height = 200;
const backgroundColor = { r: 255, g: 0, b: 0, alpha: 1 };

async function createTestImage() {
  try {
    // Create a solid red image
    await sharp({
      create: {
        width,
        height,
        channels: 4,
        background: backgroundColor
      }
    })
    .jpeg()
    .toFile('test-images/test-image.jpg');
    
    console.log('Test image created successfully at test-images/test-image.jpg');
  } catch (error) {
    console.error('Error creating test image:', error);
  }
}

createTestImage();

```

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

```json
{
  "name": "image-reader",
  "version": "0.1.0",
  "description": "image reader",
  "private": true,
  "type": "module",
  "bin": {
    "image-reader": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "0.6.0",
    "@types/fs-extra": "^11.0.4",
    "@types/sharp": "^0.31.1",
    "fs-extra": "^11.3.0",
    "sharp": "^0.33.5"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "typescript": "^5.3.3"
  }
}

```

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

```typescript
#!/usr/bin/env node

/**
 * MCP server for image processing and analysis.
 * This server provides tools and resources for working with images:
 * - List images in a directory as resources
 * - Read image metadata and thumbnails
 * - Analyze images (dimensions, format, size)
 * - Process images (resize, convert)
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ErrorCode,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  McpError,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from 'fs';
import * as path from 'path';
import * as fsExtra from 'fs-extra';
import sharp from 'sharp';

// Default directory to scan for images if not provided
const DEFAULT_IMAGE_DIR = process.env.IMAGE_DIR || process.cwd();

// Supported image formats
const SUPPORTED_FORMATS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.svg', '.bmp'];

// Cache for image metadata to avoid repeated processing
const imageMetadataCache = new Map<string, ImageMetadata>();

// Type for image metadata
interface ImageMetadata {
  path: string;
  filename: string;
  format: string;
  width?: number;
  height?: number;
  size: number;
  created: Date;
  modified: Date;
}

/**
 * Get all image files in a directory
 */
async function getImageFiles(directory: string): Promise<string[]> {
  try {
    // Use fs.promises.readdir instead of fsExtra.readdir
    const files = await fs.promises.readdir(directory);
    return files
      .filter(file => {
        const ext = path.extname(file).toLowerCase();
        return SUPPORTED_FORMATS.includes(ext);
      })
      .map(file => path.join(directory, file));
  } catch (error) {
    console.error(`Error reading directory ${directory}:`, error);
    return [];
  }
}

/**
 * Get metadata for an image file
 */
async function getImageMetadata(imagePath: string): Promise<ImageMetadata> {
  // Check cache first
  if (imageMetadataCache.has(imagePath)) {
    return imageMetadataCache.get(imagePath)!;
  }

  try {
    // Use fs.promises.stat instead of fsExtra.stat
    const stats = await fs.promises.stat(imagePath);
    const metadata = await sharp(imagePath).metadata();
    
    const imageMetadata: ImageMetadata = {
      path: imagePath,
      filename: path.basename(imagePath),
      format: metadata.format || path.extname(imagePath).replace('.', ''),
      width: metadata.width,
      height: metadata.height,
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime
    };

    // Cache the metadata
    imageMetadataCache.set(imagePath, imageMetadata);
    
    return imageMetadata;
  } catch (error) {
    console.error(`Error getting metadata for ${imagePath}:`, error);
    throw new McpError(
      ErrorCode.InternalError,
      `Failed to process image: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

/**
 * Generate a base64 thumbnail of an image
 */
async function generateThumbnail(imagePath: string, maxWidth = 300): Promise<string> {
  try {
    const buffer = await sharp(imagePath)
      .resize({ width: maxWidth, withoutEnlargement: true })
      .toBuffer();
    
    return `data:image/${path.extname(imagePath).replace('.', '')};base64,${buffer.toString('base64')}`;
  } catch (error) {
    console.error(`Error generating thumbnail for ${imagePath}:`, error);
    throw new McpError(
      ErrorCode.InternalError,
      `Failed to generate thumbnail: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

/**
 * Create an MCP server with capabilities for image processing
 */
const server = new Server(
  {
    name: "image-reader",
    version: "0.1.0",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
);

/**
 * Handler for listing available images as resources
 */
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const imageDir = process.env.IMAGE_DIR || DEFAULT_IMAGE_DIR;
  const imageFiles = await getImageFiles(imageDir);
  
  return {
    resources: await Promise.all(imageFiles.map(async (imagePath) => {
      try {
        const metadata = await getImageMetadata(imagePath);
        return {
          uri: `image://${encodeURIComponent(imagePath)}`,
          mimeType: `image/${metadata.format}`,
          name: metadata.filename,
          description: `Image: ${metadata.filename} (${metadata.width}x${metadata.height}, ${(metadata.size / 1024).toFixed(2)} KB)`
        };
      } catch (error) {
        console.error(`Error processing ${imagePath}:`, error);
        return {
          uri: `image://${encodeURIComponent(imagePath)}`,
          mimeType: "image/unknown",
          name: path.basename(imagePath),
          description: `Image: ${path.basename(imagePath)} (metadata unavailable)`
        };
      }
    }))
  };
});

/**
 * Handler for reading image content and metadata
 */
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const uri = request.params.uri;
  if (!uri.startsWith('image://')) {
    throw new McpError(
      ErrorCode.InvalidRequest,
      `Invalid URI scheme: ${uri}`
    );
  }

  const imagePath = decodeURIComponent(uri.replace('image://', ''));
  
  try {
    if (!fs.existsSync(imagePath)) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Image not found: ${imagePath}`
      );
    }

    const metadata = await getImageMetadata(imagePath);
    const thumbnail = await generateThumbnail(imagePath);
    
    // Format metadata as JSON
    const metadataJson = JSON.stringify({
      filename: metadata.filename,
      format: metadata.format,
      dimensions: `${metadata.width}x${metadata.height}`,
      size: `${(metadata.size / 1024).toFixed(2)} KB`,
      created: metadata.created.toISOString(),
      modified: metadata.modified.toISOString(),
      path: metadata.path
    }, null, 2);

    return {
      contents: [
        {
          uri: request.params.uri,
          mimeType: "application/json",
          text: metadataJson
        },
        {
          uri: `${request.params.uri}#thumbnail`,
          mimeType: `image/${metadata.format}`,
          text: thumbnail
        }
      ]
    };
  } catch (error) {
    if (error instanceof McpError) {
      throw error;
    }
    throw new McpError(
      ErrorCode.InternalError,
      `Error processing image: ${error instanceof Error ? error.message : String(error)}`
    );
  }
});

/**
 * Handler that lists available tools for image processing
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "analyze_image",
        description: "Get detailed metadata about an image",
        inputSchema: {
          type: "object",
          properties: {
            path: {
              type: "string",
              description: "Path to the image file"
            }
          },
          required: ["path"]
        }
      },
      {
        name: "resize_image",
        description: "Resize an image and save to a new file",
        inputSchema: {
          type: "object",
          properties: {
            input: {
              type: "string",
              description: "Path to the input image file"
            },
            output: {
              type: "string",
              description: "Path to save the resized image"
            },
            width: {
              type: "number",
              description: "Target width in pixels"
            },
            height: {
              type: "number",
              description: "Target height in pixels (optional)"
            },
            fit: {
              type: "string",
              description: "Fit method: cover, contain, fill, inside, outside",
              enum: ["cover", "contain", "fill", "inside", "outside"]
            }
          },
          required: ["input", "output", "width"]
        }
      },
      {
        name: "convert_format",
        description: "Convert an image to a different format",
        inputSchema: {
          type: "object",
          properties: {
            input: {
              type: "string",
              description: "Path to the input image file"
            },
            output: {
              type: "string",
              description: "Path to save the converted image"
            },
            format: {
              type: "string",
              description: "Target format: jpeg, png, webp, avif, tiff, etc.",
              enum: ["jpeg", "png", "webp", "avif", "tiff", "gif"]
            },
            quality: {
              type: "number",
              description: "Quality level (1-100, for formats that support it)"
            }
          },
          required: ["input", "output", "format"]
        }
      },
      {
        name: "scan_directory",
        description: "Scan a directory for images and return metadata",
        inputSchema: {
          type: "object",
          properties: {
            directory: {
              type: "string",
              description: "Directory path to scan for images"
            },
            recursive: {
              type: "boolean",
              description: "Whether to scan subdirectories recursively"
            }
          },
          required: ["directory"]
        }
      }
    ]
  };
});

/**
 * Handler for image processing tools
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case "analyze_image": {
      const { path: imagePath } = request.params.arguments as { path: string };
      
      if (!imagePath) {
        throw new McpError(ErrorCode.InvalidParams, "Image path is required");
      }
      
      if (!fs.existsSync(imagePath)) {
        throw new McpError(ErrorCode.InvalidRequest, `Image not found: ${imagePath}`);
      }
      
      try {
        const metadata = await getImageMetadata(imagePath);
        const thumbnail = await generateThumbnail(imagePath);
        
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                filename: metadata.filename,
                format: metadata.format,
                dimensions: `${metadata.width}x${metadata.height}`,
                size: {
                  bytes: metadata.size,
                  kilobytes: (metadata.size / 1024).toFixed(2),
                  megabytes: (metadata.size / (1024 * 1024)).toFixed(2)
                },
                created: metadata.created.toISOString(),
                modified: metadata.modified.toISOString(),
                path: metadata.path,
                thumbnail
              }, null, 2)
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error analyzing image: ${error instanceof Error ? error.message : String(error)}`
            }
          ],
          isError: true
        };
      }
    }
    
    case "resize_image": {
      const { input, output, width, height, fit = "cover" } = 
        request.params.arguments as { input: string, output: string, width: number, height?: number, fit?: string };
      
      if (!input || !output || !width) {
        throw new McpError(ErrorCode.InvalidParams, "Input path, output path, and width are required");
      }
      
      if (!fs.existsSync(input)) {
        throw new McpError(ErrorCode.InvalidRequest, `Input image not found: ${input}`);
      }
      
      try {
        // Create output directory if it doesn't exist
        await fsExtra.ensureDir(path.dirname(output));
        
        // Resize the image
        await sharp(input)
          .resize({
            width,
            height,
            fit: fit as keyof sharp.FitEnum
          })
          .toFile(output);
        
        // Get metadata of the resized image
        const metadata = await getImageMetadata(output);
        
        return {
          content: [
            {
              type: "text",
              text: `Image resized successfully:\n` +
                `- Original: ${input}\n` +
                `- Resized: ${output}\n` +
                `- New dimensions: ${metadata.width}x${metadata.height}\n` +
                `- Size: ${(metadata.size / 1024).toFixed(2)} KB`
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error resizing image: ${error instanceof Error ? error.message : String(error)}`
            }
          ],
          isError: true
        };
      }
    }
    
    case "convert_format": {
      const { input, output, format, quality = 80 } = 
        request.params.arguments as { input: string, output: string, format: string, quality?: number };
      
      if (!input || !output || !format) {
        throw new McpError(ErrorCode.InvalidParams, "Input path, output path, and format are required");
      }
      
      if (!fs.existsSync(input)) {
        throw new McpError(ErrorCode.InvalidRequest, `Input image not found: ${input}`);
      }
      
      try {
        // Create output directory if it doesn't exist
        await fsExtra.ensureDir(path.dirname(output));
        
        // Convert the image
        let processor = sharp(input);
        
        // Apply format-specific options
        switch (format.toLowerCase()) {
          case 'jpeg':
          case 'jpg':
            processor = processor.jpeg({ quality });
            break;
          case 'png':
            processor = processor.png({ quality: Math.floor(quality / 100 * 9) });
            break;
          case 'webp':
            processor = processor.webp({ quality });
            break;
          case 'avif':
            processor = processor.avif({ quality });
            break;
          case 'tiff':
            processor = processor.tiff({ quality });
            break;
          case 'gif':
            processor = processor.gif();
            break;
          default:
            throw new McpError(ErrorCode.InvalidParams, `Unsupported format: ${format}`);
        }
        
        await processor.toFile(output);
        
        // Get metadata of the converted image
        const metadata = await getImageMetadata(output);
        
        return {
          content: [
            {
              type: "text",
              text: `Image converted successfully:\n` +
                `- Original: ${input}\n` +
                `- Converted: ${output}\n` +
                `- Format: ${metadata.format}\n` +
                `- Dimensions: ${metadata.width}x${metadata.height}\n` +
                `- Size: ${(metadata.size / 1024).toFixed(2)} KB`
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error converting image: ${error instanceof Error ? error.message : String(error)}`
            }
          ],
          isError: true
        };
      }
    }
    
    case "scan_directory": {
      const { directory, recursive = false } = 
        request.params.arguments as { directory: string, recursive?: boolean };
      
      if (!directory) {
        throw new McpError(ErrorCode.InvalidParams, "Directory path is required");
      }
      
      if (!fs.existsSync(directory)) {
        throw new McpError(ErrorCode.InvalidRequest, `Directory not found: ${directory}`);
      }
      
      try {
        // Function to scan directory recursively
        async function scanDir(dir: string, results: ImageMetadata[] = []): Promise<ImageMetadata[]> {
          // Use fs instead of fsExtra for readdir
          const entries = await fs.promises.readdir(dir, { withFileTypes: true });
          
          for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            
            if (entry.isDirectory() && recursive) {
              await scanDir(fullPath, results);
            } else if (entry.isFile()) {
              const ext = path.extname(entry.name).toLowerCase();
              if (SUPPORTED_FORMATS.includes(ext)) {
                try {
                  const metadata = await getImageMetadata(fullPath);
                  results.push(metadata);
                } catch (error) {
                  console.error(`Error processing ${fullPath}:`, error);
                }
              }
            }
          }
          
          return results;
        }
        
        const images = await scanDir(directory);
        
        return {
          content: [
            {
              type: "text",
              text: `Found ${images.length} images in ${directory}${recursive ? ' (including subdirectories)' : ''}:\n\n` +
                JSON.stringify(images.map(img => ({
                  filename: img.filename,
                  path: img.path,
                  format: img.format,
                  dimensions: `${img.width}x${img.height}`,
                  size: `${(img.size / 1024).toFixed(2)} KB`
                })), null, 2)
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error scanning directory: ${error instanceof Error ? error.message : String(error)}`
            }
          ],
          isError: true
        };
      }
    }
    
    default:
      throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
  }
});

/**
 * Start the server using stdio transport
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Image Reader MCP server running on stdio');
  
  // Error handling
  process.on('uncaughtException', (error) => {
    console.error('Uncaught exception:', error);
  });
  
  process.on('unhandledRejection', (reason) => {
    console.error('Unhandled rejection:', reason);
  });
  
  process.on('SIGINT', async () => {
    await server.close();
    process.exit(0);
  });
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

```