@boomlinkai/image-resize-mcp
Version:
MCP server for image resizing
320 lines (319 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const types_js = require("@modelcontextprotocol/sdk/types.js");
const fetch = require("node-fetch");
const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
const fs = require("fs");
const sharp = require("sharp");
const zod = require("zod");
const SUPPORTED_INPUT_FORMATS = ["jpeg", "jpg", "png", "webp", "avif", "tiff", "gif"];
const SUPPORTED_OUTPUT_FORMATS = ["jpeg", "png", "webp", "avif"];
const DEFAULT_QUALITY = 80;
const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 600;
const VERSION = "0.0.1";
function isValidInputFormat(format) {
return SUPPORTED_INPUT_FORMATS.includes(format.toLowerCase());
}
function isValidOutputFormat(format) {
return SUPPORTED_OUTPUT_FORMATS.includes(format.toLowerCase());
}
function isValidDimensions(width, height) {
if (width !== void 0 && (width <= 0 || width > 1e4)) {
return false;
}
if (height !== void 0 && (height <= 0 || height > 1e4)) {
return false;
}
return true;
}
function isValidQuality(quality) {
if (quality !== void 0 && (quality < 1 || quality > 100)) {
return false;
}
return true;
}
function getFileExtension(filename) {
const parts = filename.split(".");
return parts.length > 1 ? parts.pop().toLowerCase() : "";
}
function base64ToBuffer(base64) {
const base64Data = base64.replace(/^data:image\/\w+;base64,/, "");
return Buffer.from(base64Data, "base64");
}
function bufferToBase64(buffer, mimeType) {
return `data:${mimeType};base64,${buffer.toString("base64")}`;
}
function normalizeFilePath(filePath) {
let normalizedPath = filePath.replace(/\\ /g, " ");
normalizedPath = normalizedPath.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\{/g, "{").replace(/\\\}/g, "}");
return normalizedPath;
}
async function fetchImageFromUrl(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new types_js.McpError(
types_js.ErrorCode.InvalidParams,
`Failed to fetch image from URL: ${url}, status code: ${response.status}`
);
}
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.startsWith("image/")) {
throw new types_js.McpError(
types_js.ErrorCode.InvalidParams,
`URL does not point to an image: ${url}, content-type: ${contentType}`
);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (error) {
if (error instanceof types_js.McpError) {
throw error;
}
throw new types_js.McpError(
types_js.ErrorCode.InternalError,
`Error fetching image from URL: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const resizeImageSchema = {
imagePath: zod.z.string({
description: "Path to image"
}).optional(),
imageUrl: zod.z.string({
description: "URL to image"
}).optional(),
base64Image: zod.z.string({
description: "Base64-encoded image data (with or without data URL prefix)"
}).optional(),
format: zod.z.enum(SUPPORTED_OUTPUT_FORMATS).optional(),
width: zod.z.number().min(1).max(1e4).optional(),
height: zod.z.number().min(1).max(1e4).optional(),
quality: zod.z.number().min(1).max(100).optional(),
fit: zod.z.enum(["cover", "contain", "fill", "inside", "outside"]).optional(),
position: zod.z.enum(["centre", "center", "north", "east", "south", "west", "northeast", "southeast", "southwest", "northwest"]).optional(),
background: zod.z.string().optional(),
withoutEnlargement: zod.z.boolean().optional(),
withoutReduction: zod.z.boolean().optional(),
rotate: zod.z.number().optional(),
flip: zod.z.boolean().optional(),
flop: zod.z.boolean().optional(),
grayscale: zod.z.boolean().optional(),
blur: zod.z.number().min(0.3).max(1e3).optional(),
sharpen: zod.z.number().min(0.3).max(1e3).optional(),
gamma: zod.z.number().min(1).max(3).optional(),
negate: zod.z.boolean().optional(),
normalize: zod.z.boolean().optional(),
threshold: zod.z.number().min(0).max(255).optional(),
trim: zod.z.boolean().optional(),
outputPath: zod.z.string({
description: "Path to save the resized image (if not provided, image will only be returned as base64)"
}).optional()
};
class ImageResizeMcpServer {
constructor() {
this.server = new mcp_js.McpServer({
name: "image-resize-mcp-server",
version: VERSION
});
this.setupToolHandlers();
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
setupToolHandlers() {
this.server.tool("resize_image", "Resize and transform images", resizeImageSchema, async (validatedArgs) => {
try {
if (!validatedArgs.imagePath && !validatedArgs.imageUrl && !validatedArgs.base64Image) {
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, "One of imagePath, imageUrl, or base64Image must be provided");
}
let inputBuffer;
if (validatedArgs.imagePath) {
try {
const normalizedPath = normalizeFilePath(validatedArgs.imagePath);
inputBuffer = fs.readFileSync(normalizedPath);
} catch (error) {
throw new types_js.McpError(
types_js.ErrorCode.InvalidParams,
`Failed to read image from path: ${validatedArgs.imagePath}. ${error instanceof Error ? error.message : String(error)}`
);
}
} else if (validatedArgs.imageUrl) {
inputBuffer = await fetchImageFromUrl(validatedArgs.imageUrl);
} else if (validatedArgs.base64Image) {
inputBuffer = base64ToBuffer(validatedArgs.base64Image);
} else {
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, "One of imagePath, imageUrl, or base64Image must be provided");
}
let image = sharp(inputBuffer);
const metadata = await image.metadata();
const inputFormat = metadata.format;
if (!inputFormat || !isValidInputFormat(inputFormat)) {
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, `Unsupported input format: ${inputFormat}`);
}
let width = validatedArgs.width;
let height = validatedArgs.height;
let fit = validatedArgs.fit;
if (width && !height) {
height = void 0;
} else if (!width && height) {
width = void 0;
} else {
width = width || DEFAULT_WIDTH;
height = height || DEFAULT_HEIGHT;
if (!fit && width && height) {
fit = "contain";
}
}
image = image.resize({
width,
height,
fit,
position: validatedArgs.position,
background: validatedArgs.background,
withoutEnlargement: validatedArgs.withoutEnlargement,
withoutReduction: validatedArgs.withoutReduction
});
if (validatedArgs.rotate) {
image = image.rotate(validatedArgs.rotate);
}
if (validatedArgs.flip) {
image = image.flip();
}
if (validatedArgs.flop) {
image = image.flop();
}
if (validatedArgs.grayscale) {
image = image.grayscale();
}
if (validatedArgs.blur) {
image = image.blur(validatedArgs.blur);
}
if (validatedArgs.sharpen) {
image = image.sharpen(validatedArgs.sharpen);
}
if (validatedArgs.gamma) {
image = image.gamma(validatedArgs.gamma);
}
if (validatedArgs.negate) {
image = image.negate();
}
if (validatedArgs.normalize) {
image = image.normalize();
}
if (validatedArgs.threshold) {
image = image.threshold(validatedArgs.threshold);
}
if (validatedArgs.trim) {
image = image.trim();
}
const outputFormat = validatedArgs.format || inputFormat;
const quality = validatedArgs.quality || DEFAULT_QUALITY;
let outputBuffer;
let mimeType;
switch (outputFormat) {
case "jpeg":
case "jpg":
outputBuffer = await image.jpeg({ quality }).toBuffer();
mimeType = "image/jpeg";
break;
case "png":
outputBuffer = await image.png({ quality }).toBuffer();
mimeType = "image/png";
break;
case "webp":
outputBuffer = await image.webp({ quality }).toBuffer();
mimeType = "image/webp";
break;
case "avif":
outputBuffer = await image.avif({ quality }).toBuffer();
mimeType = "image/avif";
break;
default:
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, `Unsupported output format: ${outputFormat}`);
}
if (validatedArgs.outputPath) {
try {
const normalizedOutputPath = normalizeFilePath(validatedArgs.outputPath);
fs.writeFileSync(normalizedOutputPath, outputBuffer);
} catch (writeError) {
throw new types_js.McpError(
types_js.ErrorCode.InternalError,
`Failed to save image to ${validatedArgs.outputPath}: ${writeError instanceof Error ? writeError.message : String(writeError)}`
);
}
}
const outputBase64 = bufferToBase64(outputBuffer, mimeType);
const finalMetadata = await sharp(outputBuffer).metadata();
return {
content: [
{
type: "text",
text: JSON.stringify(
{
image: outputBase64,
format: outputFormat,
width: finalMetadata.width,
height: finalMetadata.height,
size: outputBuffer.length,
savedTo: validatedArgs.outputPath || null,
source: validatedArgs.imagePath ? "file" : validatedArgs.imageUrl ? "url" : "base64"
},
null,
2
)
}
]
};
} catch (error) {
if (error instanceof zod.z.ZodError) {
return {
content: [
{
type: "text",
text: `Validation error: ${JSON.stringify(error.format(), null, 2)}`
}
],
isError: true
};
}
if (error instanceof types_js.McpError) {
throw error;
}
return {
content: [
{
type: "text",
text: `Error processing image: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
});
}
async run() {
const transport = new stdio_js.StdioServerTransport();
await this.server.connect(transport);
console.error("Image Resize MCP server running on stdio");
}
}
exports.DEFAULT_HEIGHT = DEFAULT_HEIGHT;
exports.DEFAULT_QUALITY = DEFAULT_QUALITY;
exports.DEFAULT_WIDTH = DEFAULT_WIDTH;
exports.ImageResizeMcpServer = ImageResizeMcpServer;
exports.SUPPORTED_INPUT_FORMATS = SUPPORTED_INPUT_FORMATS;
exports.SUPPORTED_OUTPUT_FORMATS = SUPPORTED_OUTPUT_FORMATS;
exports.VERSION = VERSION;
exports.base64ToBuffer = base64ToBuffer;
exports.bufferToBase64 = bufferToBase64;
exports.fetchImageFromUrl = fetchImageFromUrl;
exports.getFileExtension = getFileExtension;
exports.isValidDimensions = isValidDimensions;
exports.isValidInputFormat = isValidInputFormat;
exports.isValidOutputFormat = isValidOutputFormat;
exports.isValidQuality = isValidQuality;
exports.normalizeFilePath = normalizeFilePath;