placeholder-image-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # placeholder-image-mcp
2
+
3
+ Generates PNG placeholder images with colored background and text. Support batch generate.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ git clone https://github.com/RainbowCockroach/placeholder-image-mcp.git
9
+ cd placeholder-image-mcp
10
+ npm install
11
+ npm run build
12
+ ```
13
+
14
+ Then register it as an MCP server in your agent's config (check the documentation in of your AI agent). Or just ask it to install this MCP for you. It's good at doing that.
15
+
16
+ ## Usage
17
+
18
+ Once connected, just ask your agent to generate placeholder images. It will call the tool automatically.
19
+
20
+ **Examples:**
21
+
22
+ - "Create 64x64 image and save it to desktop"
23
+ - "Create three placeholder banners at 100x100 with text "frog", save to {some folder}"
24
+ - "Create 200x200 pink image, show its dimension"
25
+
26
+ Two calling modes:
27
+
28
+ ### Individual images
29
+
30
+ Each image has its own size, text, color, and output path:
31
+
32
+ ```json
33
+ {
34
+ "images": [
35
+ { "width": 800, "height": 600, "text": "ss", "path": "hero.png" },
36
+ {
37
+ "width": 400,
38
+ "height": 300,
39
+ "text": "Frog",
40
+ "color": "#1a874f",
41
+ "path": "frog.png"
42
+ }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ ### Batch (same config, multiple files)
48
+
49
+ One config applied to multiple paths — each file gets a different random color:
50
+
51
+ ```json
52
+ {
53
+ "config": { "width": 600, "height": 400, "text": "Yayaya" },
54
+ "paths": ["is-cucumber.png", "fruit.png", "or-veggie.png"]
55
+ }
56
+ ```
57
+
58
+ ## Parameters
59
+
60
+ | Parameter | Type | Required | Description |
61
+ | --------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------- |
62
+ | `width` | number | yes | Width in pixels (1–8192) |
63
+ | `height` | number | yes | Height in pixels (1–8192) |
64
+ | `text` | string | no | Centered text. Use `"ss"` to show dimensions (e.g. `800×600`). Blank by default. |
65
+ | `color` | string | no | Hex background color (e.g. `#A8D8EA`). Omit for random. |
66
+ | `path` | string | yes | Output path. Relative paths resolve against the `CLAUDE_CWD` env var if set, otherwise the process working directory. |
67
+
68
+ ## Color palette
69
+
70
+ 30 soft pastel colors chosen to look good as backgrounds:
71
+
72
+ ```
73
+ #A8D8EA #AA96DA #FCBAD3 #FFFFD2 #B5EAD7 #E2F0CB #C7CEEA
74
+ #FFB7B2 #FFDAC1 #F0E6EF #95B8D1 #DDA0DD #98D8C8 #F7DC6F
75
+ #AED6F1 #D5AAFF #85E3FF #BAFFC9 #FFE156 #FF9AA2 #D4A5A5
76
+ #A0CED9 #FFC75F #C3B1E1 #B4F8C8 #FFE5B4 #E0BBE4 #957DAD
77
+ #D291BC #FEC8D8
78
+ ```
79
+
80
+ Text is automatically black or white based on WCAG 2.0 contrast ratio.
@@ -0,0 +1,22 @@
1
+ export declare const PALETTE: readonly ["#A8D8EA", "#AA96DA", "#FCBAD3", "#FFFFD2", "#B5EAD7", "#E2F0CB", "#C7CEEA", "#FFB7B2", "#FFDAC1", "#F0E6EF", "#95B8D1", "#DDA0DD", "#98D8C8", "#F7DC6F", "#AED6F1", "#D5AAFF", "#85E3FF", "#BAFFC9", "#FFE156", "#FF9AA2", "#D4A5A5", "#A0CED9", "#FFC75F", "#C3B1E1", "#B4F8C8", "#FFE5B4", "#E0BBE4", "#957DAD", "#D291BC", "#FEC8D8"];
2
+ /**
3
+ * Parse a hex color string to RGB components.
4
+ */
5
+ export declare function hexToRgb(hex: string): {
6
+ r: number;
7
+ g: number;
8
+ b: number;
9
+ };
10
+ /**
11
+ * Compute relative luminance per WCAG 2.0.
12
+ * Returns a value between 0 (black) and 1 (white).
13
+ */
14
+ export declare function relativeLuminance(hex: string): number;
15
+ /**
16
+ * Pick a high-contrast text color (black or white) for the given background.
17
+ */
18
+ export declare function contrastTextColor(bgHex: string): string;
19
+ /**
20
+ * Pick a random color from the palette, optionally excluding certain colors.
21
+ */
22
+ export declare function randomColor(exclude?: Set<string>): string;
package/dist/colors.js ADDED
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PALETTE = void 0;
4
+ exports.hexToRgb = hexToRgb;
5
+ exports.relativeLuminance = relativeLuminance;
6
+ exports.contrastTextColor = contrastTextColor;
7
+ exports.randomColor = randomColor;
8
+ // Curated palette of easy-on-the-eyes colors
9
+ // Muted pastels and soft tones that work well as placeholder backgrounds
10
+ exports.PALETTE = [
11
+ "#A8D8EA", // soft sky blue
12
+ "#AA96DA", // lavender
13
+ "#FCBAD3", // soft pink
14
+ "#FFFFD2", // cream yellow
15
+ "#B5EAD7", // mint green
16
+ "#E2F0CB", // light lime
17
+ "#C7CEEA", // periwinkle
18
+ "#FFB7B2", // salmon pink
19
+ "#FFDAC1", // peach
20
+ "#F0E6EF", // pale mauve
21
+ "#95B8D1", // steel blue
22
+ "#DDA0DD", // plum
23
+ "#98D8C8", // seafoam
24
+ "#F7DC6F", // soft gold
25
+ "#AED6F1", // light cornflower
26
+ "#D5AAFF", // soft violet
27
+ "#85E3FF", // baby blue
28
+ "#BAFFC9", // light mint
29
+ "#FFE156", // warm yellow
30
+ "#FF9AA2", // rose
31
+ "#D4A5A5", // dusty rose
32
+ "#A0CED9", // powder blue
33
+ "#FFC75F", // mango
34
+ "#C3B1E1", // wisteria
35
+ "#B4F8C8", // spring green
36
+ "#FFE5B4", // bisque
37
+ "#E0BBE4", // thistle
38
+ "#957DAD", // muted purple
39
+ "#D291BC", // orchid pink
40
+ "#FEC8D8", // blush
41
+ ];
42
+ /**
43
+ * Parse a hex color string to RGB components.
44
+ */
45
+ function hexToRgb(hex) {
46
+ const clean = hex.replace("#", "");
47
+ return {
48
+ r: parseInt(clean.substring(0, 2), 16),
49
+ g: parseInt(clean.substring(2, 4), 16),
50
+ b: parseInt(clean.substring(4, 6), 16),
51
+ };
52
+ }
53
+ /**
54
+ * Compute relative luminance per WCAG 2.0.
55
+ * Returns a value between 0 (black) and 1 (white).
56
+ */
57
+ function relativeLuminance(hex) {
58
+ const { r, g, b } = hexToRgb(hex);
59
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
60
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
61
+ }
62
+ /**
63
+ * Pick a high-contrast text color (black or white) for the given background.
64
+ */
65
+ function contrastTextColor(bgHex) {
66
+ return relativeLuminance(bgHex) > 0.4 ? "#222222" : "#FFFFFF";
67
+ }
68
+ /**
69
+ * Pick a random color from the palette, optionally excluding certain colors.
70
+ */
71
+ function randomColor(exclude) {
72
+ const available = exclude
73
+ ? exports.PALETTE.filter((c) => !exclude.has(c))
74
+ : [...exports.PALETTE];
75
+ if (available.length === 0) {
76
+ // All colors used — reset and allow duplicates
77
+ return exports.PALETTE[Math.floor(Math.random() * exports.PALETTE.length)];
78
+ }
79
+ return available[Math.floor(Math.random() * available.length)];
80
+ }
@@ -0,0 +1,33 @@
1
+ export interface ImageConfig {
2
+ width: number;
3
+ height: number;
4
+ text?: string;
5
+ color?: string;
6
+ path: string;
7
+ }
8
+ /**
9
+ * Generate a single placeholder image and save it to disk.
10
+ */
11
+ export declare function generateImage(config: ImageConfig, assignedColor?: string): Promise<{
12
+ path: string;
13
+ color: string;
14
+ width: number;
15
+ height: number;
16
+ }>;
17
+ /**
18
+ * Generate multiple placeholder images.
19
+ * - If `images` is provided: each entry is a full config.
20
+ * - If `config` + `count` + `paths` is provided: generate `count` images
21
+ * from one config template, each with a different random color.
22
+ */
23
+ export declare function generateImages(params: {
24
+ images: ImageConfig[];
25
+ } | {
26
+ config: Omit<ImageConfig, "path">;
27
+ paths: string[];
28
+ }): Promise<{
29
+ path: string;
30
+ color: string;
31
+ width: number;
32
+ height: number;
33
+ }[]>;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateImage = generateImage;
7
+ exports.generateImages = generateImages;
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const colors_js_1 = require("./colors.js");
12
+ /**
13
+ * Resolve a file path relative to the Claude Code project directory.
14
+ * - If the path is absolute, use it as-is
15
+ * - If relative, resolve it relative to CLAUDE_CWD env var (if set) or process.cwd()
16
+ */
17
+ function resolvePath(filePath) {
18
+ if (path_1.default.isAbsolute(filePath)) {
19
+ return filePath;
20
+ }
21
+ // Use CLAUDE_CWD if set (for multi-folder workspaces or future enhancements)
22
+ // Otherwise use process.cwd()
23
+ const baseDir = process.env.CLAUDE_CWD || process.cwd();
24
+ return path_1.default.resolve(baseDir, filePath);
25
+ }
26
+ /**
27
+ * Escape special XML characters for safe SVG embedding.
28
+ */
29
+ function escapeXml(str) {
30
+ return str
31
+ .replace(/&/g, "&amp;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;")
35
+ .replace(/'/g, "&apos;");
36
+ }
37
+ /**
38
+ * Build an SVG string for the placeholder image.
39
+ */
40
+ function buildSvg(width, height, bgColor, text) {
41
+ const textColor = (0, colors_js_1.contrastTextColor)(bgColor);
42
+ // Scale font size relative to image dimensions
43
+ const minDim = Math.min(width, height);
44
+ let fontSize = Math.max(12, Math.floor(minDim / 8));
45
+ // For long text, shrink font to fit within ~80% of width
46
+ if (text.length > 0) {
47
+ const maxTextWidth = width * 0.8;
48
+ const estimatedCharWidth = fontSize * 0.6;
49
+ const estimatedTextWidth = text.length * estimatedCharWidth;
50
+ if (estimatedTextWidth > maxTextWidth) {
51
+ fontSize = Math.max(10, Math.floor(maxTextWidth / (text.length * 0.6)));
52
+ }
53
+ }
54
+ let textElement = "";
55
+ if (text.length > 0) {
56
+ textElement = `
57
+ <text
58
+ x="50%" y="50%"
59
+ dominant-baseline="central"
60
+ text-anchor="middle"
61
+ font-family="Arial, Helvetica, sans-serif"
62
+ font-size="${fontSize}"
63
+ font-weight="600"
64
+ fill="${textColor}"
65
+ >${escapeXml(text)}</text>`;
66
+ }
67
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
68
+ <rect width="100%" height="100%" fill="${bgColor}"/>
69
+ ${textElement}
70
+ </svg>`;
71
+ }
72
+ /**
73
+ * Generate a single placeholder image and save it to disk.
74
+ */
75
+ async function generateImage(config, assignedColor) {
76
+ const bgColor = config.color || assignedColor || (0, colors_js_1.randomColor)();
77
+ // Resolve text
78
+ let displayText = "";
79
+ if (config.text === "ss") {
80
+ displayText = `${config.width}\u00D7${config.height}`;
81
+ }
82
+ else if (config.text !== undefined && config.text !== "") {
83
+ displayText = config.text;
84
+ }
85
+ const svg = buildSvg(config.width, config.height, bgColor, displayText);
86
+ // Ensure output directory exists
87
+ const outputPath = resolvePath(config.path);
88
+ const dir = path_1.default.dirname(outputPath);
89
+ fs_1.default.mkdirSync(dir, { recursive: true });
90
+ await (0, sharp_1.default)(Buffer.from(svg)).png().toFile(outputPath);
91
+ return {
92
+ path: outputPath,
93
+ color: bgColor,
94
+ width: config.width,
95
+ height: config.height,
96
+ };
97
+ }
98
+ /**
99
+ * Generate multiple placeholder images.
100
+ * - If `images` is provided: each entry is a full config.
101
+ * - If `config` + `count` + `paths` is provided: generate `count` images
102
+ * from one config template, each with a different random color.
103
+ */
104
+ async function generateImages(params) {
105
+ const usedColors = new Set();
106
+ if ("images" in params) {
107
+ const results = await Promise.all(params.images.map((img) => {
108
+ const color = img.color || (0, colors_js_1.randomColor)(usedColors);
109
+ usedColors.add(color);
110
+ return generateImage(img, color);
111
+ }));
112
+ return results;
113
+ }
114
+ // Batch mode: one config, multiple paths
115
+ const { config, paths } = params;
116
+ const results = await Promise.all(paths.map((p) => {
117
+ const color = config.color || (0, colors_js_1.randomColor)(usedColors);
118
+ usedColors.add(color);
119
+ return generateImage({ ...config, path: p }, color);
120
+ }));
121
+ return results;
122
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const zod_1 = require("zod");
7
+ const generate_js_1 = require("./generate.js");
8
+ const ImageConfigSchema = zod_1.z.object({
9
+ width: zod_1.z.number().int().min(1).max(8192).describe("Image width in pixels"),
10
+ height: zod_1.z.number().int().min(1).max(8192).describe("Image height in pixels"),
11
+ text: zod_1.z
12
+ .string()
13
+ .optional()
14
+ .describe('Text to display centered on image. Use "ss" to show dimensions (e.g. "800×600"). Omit or "" for blank.'),
15
+ color: zod_1.z
16
+ .string()
17
+ .regex(/^#[0-9a-fA-F]{6}$/)
18
+ .optional()
19
+ .describe("Background color as hex (e.g. #A8D8EA). Omit for random from curated palette."),
20
+ path: zod_1.z.string().describe("Output file path for the PNG"),
21
+ });
22
+ const server = new mcp_js_1.McpServer({
23
+ name: "placeholder-image",
24
+ version: "1.0.0",
25
+ });
26
+ server.tool("generate_placeholder", "Generate one or more placeholder PNG images with colored backgrounds and optional centered text. Supports two modes: (1) provide an array of individual image configs, or (2) provide a single config template with multiple output paths to generate batch images with different random colors.", {
27
+ images: zod_1.z
28
+ .array(ImageConfigSchema)
29
+ .optional()
30
+ .describe("Array of image configurations. Each gets its own size, text, color, and output path."),
31
+ config: zod_1.z
32
+ .object({
33
+ width: zod_1.z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(8192)
38
+ .describe("Image width in pixels"),
39
+ height: zod_1.z
40
+ .number()
41
+ .int()
42
+ .min(1)
43
+ .max(8192)
44
+ .describe("Image height in pixels"),
45
+ text: zod_1.z
46
+ .string()
47
+ .optional()
48
+ .describe('Text to display centered on image. Use "ss" to show dimensions. Omit or "" for blank.'),
49
+ color: zod_1.z
50
+ .string()
51
+ .regex(/^#[0-9a-fA-F]{6}$/)
52
+ .optional()
53
+ .describe("Background color as hex. Omit for random (each image gets a different color)."),
54
+ })
55
+ .optional()
56
+ .describe("Single config template for batch mode. Use with `paths` to generate multiple images."),
57
+ paths: zod_1.z
58
+ .array(zod_1.z.string())
59
+ .optional()
60
+ .describe("Output file paths for batch mode. Each path produces one image with a different random color."),
61
+ }, async (params) => {
62
+ try {
63
+ // Validate: must provide either `images` or `config` + `paths`
64
+ if (params.images && params.images.length > 0) {
65
+ const results = await (0, generate_js_1.generateImages)({
66
+ images: params.images,
67
+ });
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Generated ${results.length} image(s):\n${results
73
+ .map((r) => ` - ${r.path} (${r.width}x${r.height}, ${r.color})`)
74
+ .join("\n")}`,
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ if (params.config && params.paths && params.paths.length > 0) {
80
+ const results = await (0, generate_js_1.generateImages)({
81
+ config: params.config,
82
+ paths: params.paths,
83
+ });
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: `Generated ${results.length} image(s):\n${results
89
+ .map((r) => ` - ${r.path} (${r.width}x${r.height}, ${r.color})`)
90
+ .join("\n")}`,
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: "Error: Provide either `images` (array of configs) or `config` + `paths` (batch mode).",
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+ catch (error) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: `Error generating images: ${error instanceof Error ? error.message : String(error)}`,
111
+ },
112
+ ],
113
+ isError: true,
114
+ };
115
+ }
116
+ });
117
+ async function main() {
118
+ const transport = new stdio_js_1.StdioServerTransport();
119
+ await server.connect(transport);
120
+ }
121
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "placeholder-image-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for generating placeholder PNG images",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "placeholder-image-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepare": "npm run build",
15
+ "start": "node dist/index.js"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/RainbowCockroach/placeholder-image-mcp.git"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "placeholder",
24
+ "image",
25
+ "png"
26
+ ],
27
+ "author": "RGB Cockroach",
28
+ "license": "ISC",
29
+ "type": "commonjs",
30
+ "bugs": {
31
+ "url": "https://github.com/RainbowCockroach/placeholder-image-mcp/issues"
32
+ },
33
+ "homepage": "https://github.com/RainbowCockroach/placeholder-image-mcp#readme",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.27.1",
36
+ "sharp": "^0.34.5",
37
+ "zod": "^4.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.5.0",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }