Skip to main content
Glama
index.ts11.7 kB
#!/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); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jbergant/mcp_image_processor_to_vercel_blob'

If you have feedback or need assistance with the MCP directory API, please join our Discord server