#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createCanvas } from "canvas";
import fs from "fs";
import path from "path";
// Create server instance
const server = new McpServer({
name: "generate-placeholder",
version: "1.0.0",
capabilities: {
tools: {},
},
});
// Helper function to validate color
function isValidColor(color: string): boolean {
// Simple validation for hex colors, rgb colors, and common named colors
const hexPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const rgbPattern = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
const rgbaPattern = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([01]|0?\.\d+)\s*\)$/;
const namedColors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink', 'brown', 'black', 'white', 'gray', 'grey'];
return hexPattern.test(color) ||
rgbPattern.test(color) ||
rgbaPattern.test(color) ||
namedColors.includes(color.toLowerCase());
}
// Helper function to get contrasting text color
function getContrastingTextColor(backgroundColor: string): string {
// Simple logic: if background is dark, use white text; if light, use black text
const darkColors = ['black', 'navy', 'darkblue', 'darkgreen', 'purple', 'darkred', 'brown'];
if (backgroundColor.startsWith('#')) {
// For hex colors, calculate brightness
const hex = backgroundColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128 ? 'black' : 'white';
} else if (backgroundColor.startsWith('rgb')) {
// For RGB colors, extract values and calculate brightness
const match = backgroundColor.match(/\d+/g);
if (match && match.length >= 3) {
const r = parseInt(match[0]);
const g = parseInt(match[1]);
const b = parseInt(match[2]);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128 ? 'black' : 'white';
}
}
// For named colors
return darkColors.includes(backgroundColor.toLowerCase()) ? 'white' : 'black';
}
// Register the generate placeholder image tool
server.tool(
"generate-placeholder-image",
"Generate a placeholder image with specified dimensions, color, and text",
{
filename: z.string().describe("The filename for the generated image (including extension)"),
width: z.number().int().min(1).max(4096).describe("Width of the image in pixels"),
height: z.number().int().min(1).max(4096).describe("Height of the image in pixels"),
color: z.string().describe("Background color (hex, rgb, or named color)"),
text: z.string().describe("Text to display on the placeholder image"),
},
async (params: { filename: string; width: number; height: number; color: string; text: string }) => {
const { filename, width, height, color, text } = params;
try {
// Validate inputs
if (!isValidColor(color)) {
return {
content: [
{
type: "text",
text: `Error: Invalid color format '${color}'. Please use hex (#RRGGBB), rgb(r,g,b), or named colors.`,
},
],
};
}
// Ensure filename has a supported extension
const supportedExtensions = ['.png', '.jpg', '.jpeg'];
const ext = path.extname(filename).toLowerCase();
if (!supportedExtensions.includes(ext)) {
return {
content: [
{
type: "text",
text: `Error: Unsupported file extension '${ext}'. Supported extensions: ${supportedExtensions.join(', ')}`,
},
],
};
}
// Create canvas
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// Set background color
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
// Calculate font size based on image dimensions
const fontSize = Math.min(width, height) / 10;
ctx.font = `${fontSize}px Arial, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Set text color (contrasting with background)
ctx.fillStyle = getContrastingTextColor(color);
// Draw text in the center
const centerX = width / 2;
const centerY = height / 2;
// Handle multi-line text if it's too long
const maxWidth = width * 0.8;
const words = text.split(' ');
const lines: string[] = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine + (currentLine ? ' ' : '') + word;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
lines.push(currentLine);
// Draw each line
const lineHeight = fontSize * 1.2;
const startY = centerY - ((lines.length - 1) * lineHeight) / 2;
lines.forEach((line, index) => {
ctx.fillText(line, centerX, startY + index * lineHeight);
});
// Add dimensions text at the bottom
const dimensionsText = `${width} × ${height}`;
const smallFontSize = Math.min(fontSize * 0.4, 24);
ctx.font = `${smallFontSize}px Arial, sans-serif`;
ctx.fillText(dimensionsText, centerX, height - smallFontSize);
// Save the image
const buffer = ext === '.png' ? canvas.toBuffer('image/png') : canvas.toBuffer('image/jpeg');
fs.writeFileSync(filename, buffer);
return {
content: [
{
type: "text",
text: `Successfully generated placeholder image: ${filename} (${width}×${height}) with background color '${color}' and text '${text}'`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error generating placeholder image: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Main function to run the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Generate Placeholder Image MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});