import express from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import sharp from 'sharp'; import { z } from 'zod'; const app = express(); app.use(express.json({ limit: '50mb' })); const PORT = process.env.PORT || 3000; async function urlToBuffer(url) { const res = await fetch(url); return Buffer.from(await res.arrayBuffer()); } app.get('/', (req, res) => { res.json({ status: 'ok', service: 'sharp-remote-mcp', tools: 30 }); }); app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); app.all('/mcp', async (req, res) => { const server = new McpServer({ name: 'sharp-mcp', version: '1.0.0' }); // ===== FORMAT CONVERSION (৬টা) ===== server.tool('to_png', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).png().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('to_jpeg', { imageUrl: z.string(), quality: z.number().optional() }, async ({ imageUrl, quality = 85 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).jpeg({ quality }).toBuffer(); return { content: [{ type: 'text', text: `data:image/jpeg;base64,${out.toString('base64')}` }] }; }); server.tool('to_webp', { imageUrl: z.string(), quality: z.number().optional() }, async ({ imageUrl, quality = 85 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).webp({ quality }).toBuffer(); return { content: [{ type: 'text', text: `data:image/webp;base64,${out.toString('base64')}` }] }; }); server.tool('to_avif', { imageUrl: z.string(), quality: z.number().optional() }, async ({ imageUrl, quality = 50 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).avif({ quality }).toBuffer(); return { content: [{ type: 'text', text: `data:image/avif;base64,${out.toString('base64')}` }] }; }); server.tool('to_tiff', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).tiff().toBuffer(); return { content: [{ type: 'text', text: `data:image/tiff;base64,${out.toString('base64')}` }] }; }); server.tool('to_gif', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).gif().toBuffer(); return { content: [{ type: 'text', text: `data:image/gif;base64,${out.toString('base64')}` }] }; }); // ===== RESIZE & CROP (৪টা) ===== server.tool('resize_image', { imageUrl: z.string(), width: z.number(), height: z.number() }, async ({ imageUrl, width, height }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).resize(width, height, { fit: 'inside' }).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('crop_image', { imageUrl: z.string(), left: z.number(), top: z.number(), width: z.number(), height: z.number() }, async ({ imageUrl, left, top, width, height }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).extract({ left, top, width, height }).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('extend_image', { imageUrl: z.string(), top: z.number().optional(), bottom: z.number().optional(), left: z.number().optional(), right: z.number().optional() }, async ({ imageUrl, top = 0, bottom = 0, left = 0, right = 0 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).extend({ top, bottom, left, right, background: { r: 255, g: 255, b: 255, alpha: 1 } }).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('trim_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).trim().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); // ===== ROTATION & FLIP (৪টা) ===== server.tool('rotate_image', { imageUrl: z.string(), angle: z.number() }, async ({ imageUrl, angle }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).rotate(angle).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('flip_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).flip().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('flop_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).flop().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('auto_rotate', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).rotate().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); // ===== COLOR & EFFECTS (৮টা) ===== server.tool('grayscale', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).grayscale().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('tint_image', { imageUrl: z.string(), r: z.number(), g: z.number(), b: z.number() }, async ({ imageUrl, r, g, b }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).tint({ r, g, b }).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('blur_image', { imageUrl: z.string(), sigma: z.number().optional() }, async ({ imageUrl, sigma = 3 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).blur(sigma).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('sharpen_image', { imageUrl: z.string(), sigma: z.number().optional() }, async ({ imageUrl, sigma = 1 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).sharpen({ sigma }).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('brightness_contrast', { imageUrl: z.string(), brightness: z.number().optional(), contrast: z.number().optional() }, async ({ imageUrl, brightness = 1, contrast = 1 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).modulate({ brightness }).linear(contrast, 0).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('gamma_correction', { imageUrl: z.string(), gamma: z.number().optional() }, async ({ imageUrl, gamma = 2.2 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).gamma(gamma).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('negate_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).negate().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('normalize_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).normalize().toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); // ===== WATERMARK & COMPOSITE (৩টা) ===== server.tool('add_watermark', { imageUrl: z.string(), watermarkUrl: z.string(), gravity: z.enum(['centre', 'north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest']).optional() }, async ({ imageUrl, watermarkUrl, gravity = 'southeast' }) => { const buf = await urlToBuffer(imageUrl); const watermarkBuf = await urlToBuffer(watermarkUrl); const out = await sharp(buf).composite([{ input: watermarkBuf, gravity }]).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('composite_images', { baseUrl: z.string(), overlayUrl: z.string(), left: z.number().optional(), top: z.number().optional() }, async ({ baseUrl, overlayUrl, left = 0, top = 0 }) => { const buf = await urlToBuffer(baseUrl); const overlayBuf = await urlToBuffer(overlayUrl); const out = await sharp(buf).composite([{ input: overlayBuf, left, top }]).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); server.tool('add_text', { imageUrl: z.string(), text: z.string(), fontSize: z.number().optional() }, async ({ imageUrl, text, fontSize = 32 }) => { const buf = await urlToBuffer(imageUrl); const svgText = `${text}`; const out = await sharp(buf).composite([{ input: Buffer.from(svgText), gravity: 'southwest' }]).toBuffer(); return { content: [{ type: 'text', text: `data:image/png;base64,${out.toString('base64')}` }] }; }); // ===== INFO & METADATA (৩টা) ===== server.tool('get_image_info', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const info = await sharp(buf).metadata(); return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] }; }); server.tool('get_metadata', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const metadata = await sharp(buf).metadata(); return { content: [{ type: 'text', text: JSON.stringify({ width: metadata.width, height: metadata.height, format: metadata.format, size: metadata.size, channels: metadata.channels }, null, 2) }] }; }); server.tool('get_stats', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const stats = await sharp(buf).stats(); return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }] }; }); // ===== COMPRESSION (২টা) ===== server.tool('compress_image', { imageUrl: z.string(), quality: z.number().optional() }, async ({ imageUrl, quality = 60 }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).webp({ quality }).toBuffer(); return { content: [{ type: 'text', text: `data:image/webp;base64,${out.toString('base64')}` }] }; }); server.tool('optimize_image', { imageUrl: z.string() }, async ({ imageUrl }) => { const buf = await urlToBuffer(imageUrl); const out = await sharp(buf).webp({ quality: 80, effort: 6 }).toBuffer(); return { content: [{ type: 'text', text: `data:image/webp;base64,${out.toString('base64')}` }] }; }); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(PORT, () => { console.log(`Sharp Remote MCP running on port ${PORT} — 30 tools ready!`); });