penpot/mcp/packages/server/src/tools/ImportImageTool.ts
Dominik Jain 880b9b61c4 🎉 Integrate mcp repository
Original repository: https://github.com/penpot/penpot-mcp
Imported commit: fcfa67e908fc54e23a3a3543dee432472dc90c5d
2026-02-04 12:22:36 +01:00

124 lines
4.5 KiB
TypeScript

import { z } from "zod";
import { Tool } from "../Tool";
import { TextResponse, ToolResponse } from "../ToolResponse";
import "reflect-metadata";
import { PenpotMcpServer } from "../PenpotMcpServer";
import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask";
import { FileUtils } from "../utils/FileUtils";
import * as fs from "fs";
import * as path from "path";
/**
* Arguments class for ImportImageTool
*/
export class ImportImageArgs {
static schema = {
filePath: z.string().min(1, "filePath cannot be empty").describe("Absolute path to the image file to import."),
x: z.number().optional().describe("Optional X coordinate for the rectangle's position."),
y: z.number().optional().describe("Optional Y coordinate for the rectangle's position."),
width: z
.number()
.positive("width must be positive")
.optional()
.describe(
"Optional width for the rectangle. If only width is provided, height is calculated to maintain aspect ratio."
),
height: z
.number()
.positive("height must be positive")
.optional()
.describe(
"Optional height for the rectangle. If only height is provided, width is calculated to maintain aspect ratio."
),
};
filePath!: string;
x?: number;
y?: number;
width?: number;
height?: number;
}
/**
* Tool for importing a raster image from the local file system into Penpot
*/
export class ImportImageTool extends Tool<ImportImageArgs> {
/**
* Maps file extensions to MIME types.
*/
protected static readonly MIME_TYPES: { [key: string]: string } = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
/**
* Creates a new ImportImage tool instance.
*
* @param mcpServer - The MCP server instance
*/
constructor(mcpServer: PenpotMcpServer) {
super(mcpServer, ImportImageArgs.schema);
}
public getToolName(): string {
return "import_image";
}
public getToolDescription(): string {
return (
"Imports a pixel image from the local file system into Penpot by creating a Rectangle instance " +
"that uses the image as a fill. The rectangle has the image's original proportions by default. " +
"Optionally accepts position (x, y) and dimensions (width, height) parameters. " +
"If only one dimension is provided, the other is calculated to maintain the image's aspect ratio. " +
"Supported formats: JPEG, PNG, GIF, WEBP."
);
}
protected async executeCore(args: ImportImageArgs): Promise<ToolResponse> {
// check that file path is absolute
FileUtils.checkPathIsAbsolute(args.filePath);
// check that file exists
if (!fs.existsSync(args.filePath)) {
throw new Error(`File not found: ${args.filePath}`);
}
// read the file as binary data
const fileData = fs.readFileSync(args.filePath);
const base64Data = fileData.toString("base64");
// determine mime type from file extension
const ext = path.extname(args.filePath).toLowerCase();
const mimeType = ImportImageTool.MIME_TYPES[ext];
if (!mimeType) {
const supportedExtensions = Object.keys(ImportImageTool.MIME_TYPES).join(", ");
throw new Error(
`Unsupported image format: ${ext}. Supported formats (file extensions): ${supportedExtensions}`
);
}
// generate and execute JavaScript code to import the image
const fileName = path.basename(args.filePath);
const escapedBase64 = base64Data.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const escapedFileName = fileName.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const code = `
const rectangle = await penpotUtils.importImage(
'${escapedBase64}', '${mimeType}', '${escapedFileName}',
${args.x ?? "undefined"}, ${args.y ?? "undefined"},
${args.width ?? "undefined"}, ${args.height ?? "undefined"});
return { shapeId: rectangle.id };
`;
const task = new ExecuteCodePluginTask({ code: code });
const executionResult = await this.mcpServer.pluginBridge.executePluginTask(task);
return new TextResponse(JSON.stringify(executionResult.data?.result, null, 2));
}
}