# 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);
});
```