#!/usr/bin/env node
/**
* MCP server for image processing and Vercel Blob upload
* This server provides tools to:
* - Optimize and resize images
* - Convert images to WebP format
* - Upload both versions to Vercel Blob
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError
} from "@modelcontextprotocol/sdk/types.js";
import sharp from "sharp";
import { put, list } from "@vercel/blob";
import fs from "fs-extra";
import path from "path";
import os from "os";
import axios from "axios";
// Vercel Blob token from environment variables
const BLOB_READ_WRITE_TOKEN = process.env.BLOB_READ_WRITE_TOKEN;
if (!BLOB_READ_WRITE_TOKEN) {
throw new Error("BLOB_READ_WRITE_TOKEN environment variable is required");
}
/**
* Optimizes and resizes an image to the specified dimensions
* @param imagePath Path to the image file
* @param width Width to resize to (default: 550)
* @param height Height to resize to (default: 300)
* @returns Promise<Buffer> Optimized and resized image buffer
*/
async function optimiseAndResize(
imagePath: string,
width: number = 550,
height: number = 300
): Promise<Buffer> {
const optimizedBuffer = await sharp(imagePath)
.resize(width, height)
.png({
quality: 80, // Adjust quality as needed (0-100)
effort: 6 // Maximum compression effort
})
.toBuffer();
return optimizedBuffer;
}
/**
* Converts an image to WebP format with optimization
* @param imagePath Path to the image file
* @returns Promise<Buffer> WebP-optimized image buffer
*/
async function optimiseChangeToWebp(imagePath: string): Promise<Buffer> {
const optimizedBuffer = await sharp(imagePath)
.webp({
quality: 80, // Adjust quality as needed (0-100)
effort: 6 // Maximum compression effort
})
.toBuffer();
return optimizedBuffer;
}
/**
* Create an MCP server with capabilities for image processing tools
*/
const server = new Server(
{
name: "image-processor-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
/**
* Handler that lists available tools.
* Exposes tools for processing and uploading images.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "process_and_upload_image",
description: "Process a local image file (optimize, resize, convert to WebP) and upload to Vercel Blob",
inputSchema: {
type: "object",
properties: {
imagePath: {
type: "string",
description: "Path to the image file to process"
},
newName: {
type: "string",
description: "New name for the processed image (without extension)"
},
width: {
type: "number",
description: "Width to resize the image to (default: 550)"
},
height: {
type: "number",
description: "Height to resize the image to (default: 300)"
}
},
required: ["imagePath", "newName"]
}
},
{
name: "process_and_upload_image_from_url",
description: "Process an image from a URL (optimize, resize, convert to WebP) and upload to Vercel Blob",
inputSchema: {
type: "object",
properties: {
imageUrl: {
type: "string",
description: "URL of the image to process"
},
newName: {
type: "string",
description: "New name for the processed image (without extension)"
},
width: {
type: "number",
description: "Width to resize the image to (default: 550)"
},
height: {
type: "number",
description: "Height to resize the image to (default: 300)"
}
},
required: ["imageUrl", "newName"]
}
}
]
};
});
/**
* Downloads an image from a URL and returns the buffer
* @param url URL of the image to download
* @returns Promise<Buffer> Image buffer
*/
async function downloadImage(url: string): Promise<Buffer> {
try {
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
return Buffer.from(response.data, 'binary');
} catch (error) {
console.error(`Failed to download image from URL: ${url}`, error);
throw new Error(`Failed to download image from URL: ${url}`);
}
}
/**
* Creates a temporary directory for processed images
* @returns Promise<string> Path to the temporary directory
*/
async function createTempDir(): Promise<string> {
let tempDir = path.resolve(process.cwd(), "temp");
console.error(`Creating temp directory at: ${tempDir}`);
try {
await fs.ensureDir(tempDir);
console.error(`Successfully created temp directory at: ${tempDir}`);
return tempDir;
} catch (dirError) {
console.error(`Failed to create temp directory: ${dirError}`);
// Fallback to using the os.tmpdir() if we can't create in cwd
const tempDirFallback = path.join(os.tmpdir(), "image-processor-temp");
console.error(`Trying fallback temp directory: ${tempDirFallback}`);
await fs.ensureDir(tempDirFallback);
console.error(`Using fallback temp directory: ${tempDirFallback}`);
return tempDirFallback;
}
}
/**
* Processes an image buffer and uploads both PNG and WebP versions to Vercel Blob
* @param imageBuffer Buffer containing the image data
* @param newName New name for the processed image (without extension)
* @param width Width to resize to (default: 550)
* @param height Height to resize to (default: 300)
* @returns Promise<object> Results of the processing and upload
*/
async function processAndUploadImageBuffer(
imageBuffer: Buffer,
newName: string,
width: number = 550,
height: number = 300
): Promise<any> {
// Process results
const results = {
png: { localPath: "", blobUrl: "" },
webp: { localPath: "", blobUrl: "" }
};
// Create temporary directory for processed images
const tempDir = await createTempDir();
// Process PNG version
const optimizedBuffer = await sharp(imageBuffer)
.resize(width, height)
.png({
quality: 80,
effort: 6
})
.toBuffer();
const smallFileName = `${newName}_small.png`;
const smallFilePath = path.join(tempDir, smallFileName);
await fs.writeFile(smallFilePath, optimizedBuffer);
results.png.localPath = smallFilePath;
// Check if PNG already exists in Vercel Blob storage
let pngUrl = "";
try {
const existingUrl = await list({ prefix: smallFileName });
if (existingUrl.blobs.length > 0) {
pngUrl = existingUrl.blobs[0].url;
} else {
// Upload if not found
const { url } = await put(smallFileName, optimizedBuffer, {
access: "public",
contentType: "image/png"
});
pngUrl = url;
}
} catch (error) {
console.error(`Failed to check/upload PNG blob: ${error}`);
throw new McpError(
ErrorCode.InternalError,
`Failed to upload PNG to Vercel Blob: ${error}`
);
}
results.png.blobUrl = pngUrl;
// Process WebP version
const optimizedBufferWebp = await sharp(imageBuffer)
.webp({
quality: 80,
effort: 6
})
.toBuffer();
const webpFileName = `${newName}.webp`;
const webpFilePath = path.join(tempDir, webpFileName);
await fs.writeFile(webpFilePath, optimizedBufferWebp);
results.webp.localPath = webpFilePath;
// Check if WebP already exists in Vercel Blob storage
let webpUrl = "";
try {
const existingUrlWebp = await list({ prefix: webpFileName });
if (existingUrlWebp.blobs.length > 0) {
webpUrl = existingUrlWebp.blobs[0].url;
} else {
// Upload if not found
const { url } = await put(webpFileName, optimizedBufferWebp, {
access: "public",
contentType: "image/webp"
});
webpUrl = url;
}
} catch (error) {
console.error(`Failed to check/upload WebP blob: ${error}`);
throw new McpError(
ErrorCode.InternalError,
`Failed to upload WebP to Vercel Blob: ${error}`
);
}
results.webp.blobUrl = webpUrl;
return {
success: true,
message: `Successfully processed and uploaded image: ${newName}`,
results: {
png: {
fileName: smallFileName,
localPath: results.png.localPath,
blobUrl: results.png.blobUrl
},
webp: {
fileName: webpFileName,
localPath: results.webp.localPath,
blobUrl: results.webp.blobUrl
}
}
};
}
/**
* Handler for the image processing tools.
* Processes an image and uploads both PNG and WebP versions to Vercel Blob.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
if (toolName !== "process_and_upload_image" && toolName !== "process_and_upload_image_from_url") {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${toolName}`
);
}
const args = request.params.arguments;
if (!args || typeof args !== "object") {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid arguments"
);
}
try {
let result;
if (toolName === "process_and_upload_image") {
// Process local image file
const { imagePath, newName, width = 550, height = 300 } = args as {
imagePath: string;
newName: string;
width?: number;
height?: number;
};
if (!imagePath || !newName) {
throw new McpError(
ErrorCode.InvalidParams,
"imagePath and newName are required"
);
}
if (!fs.existsSync(imagePath)) {
throw new McpError(
ErrorCode.InvalidParams,
`Image file not found: ${imagePath}`
);
}
// Read the image file
const imageBuffer = await fs.readFile(imagePath);
// Process and upload the image
result = await processAndUploadImageBuffer(imageBuffer, newName, width, height);
}
else if (toolName === "process_and_upload_image_from_url") {
// Process image from URL
const { imageUrl, newName, width = 550, height = 300 } = args as {
imageUrl: string;
newName: string;
width?: number;
height?: number;
};
if (!imageUrl || !newName) {
throw new McpError(
ErrorCode.InvalidParams,
"imageUrl and newName are required"
);
}
// Download the image from the URL
const imageBuffer = await downloadImage(imageUrl);
// Process and upload the image
result = await processAndUploadImageBuffer(imageBuffer, newName, width, height);
}
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
} catch (error) {
console.error(`Failed to process/upload image: ${error}`);
throw new McpError(
ErrorCode.InternalError,
`Failed to process/upload image: ${error}`
);
}
});
/**
* Start the server using stdio transport.
* This allows the server to communicate via standard input/output streams.
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Image Processor MCP server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});