From ad771ee92fbbf36b2d3b515e1a16e551a39cc986 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 27 Sep 2025 16:09:13 +0200 Subject: [PATCH] Add ExportShapeTool --- mcp-server/src/PenpotMcpServer.ts | 2 + mcp-server/src/ToolResponse.ts | 29 +++++++++++- mcp-server/src/tools/ExportShapeTool.ts | 63 +++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 mcp-server/src/tools/ExportShapeTool.ts diff --git a/mcp-server/src/PenpotMcpServer.ts b/mcp-server/src/PenpotMcpServer.ts index 9ecbd1b..464cb86 100644 --- a/mcp-server/src/PenpotMcpServer.ts +++ b/mcp-server/src/PenpotMcpServer.ts @@ -11,6 +11,7 @@ import { createLogger } from "./logger"; import { Tool } from "./Tool"; import { HighLevelOverviewTool } from "./tools/HighLevelOverviewTool"; import { PenpotApiInfoTool } from "./tools/PenpotApiInfoTool"; +import { ExportShapeTool } from "./tools/ExportShapeTool"; export class PenpotMcpServer { private readonly logger = createLogger("PenpotMcpServer"); @@ -58,6 +59,7 @@ export class PenpotMcpServer { new ExecuteCodeTool(this), new HighLevelOverviewTool(this), new PenpotApiInfoTool(this), + new ExportShapeTool(this), ]; for (const tool of toolInstances) { diff --git a/mcp-server/src/ToolResponse.ts b/mcp-server/src/ToolResponse.ts index b25c89a..33073d6 100644 --- a/mcp-server/src/ToolResponse.ts +++ b/mcp-server/src/ToolResponse.ts @@ -22,11 +22,27 @@ class ImageContent implements ImageItem { public data: string, public mimeType: string ) {} + + /** + * @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array) + */ + protected static byteData(data: Uint8Array | object): Uint8Array { + if (typeof data === "object") { + // convert object (as obtained from JSON conversion of Uint8Array) back to Uint8Array + return new Uint8Array(Object.values(data) as number[]); + } else { + return data; + } + } } class PNGImageContent extends ImageContent { - constructor(data: Uint8Array) { - super(Buffer.from(data).toString("base64"), "image/png"); + /** + * @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array) + */ + constructor(data: Uint8Array | object) { + let array = ImageContent.byteData(data); + super(Buffer.from(array).toString("base64"), "image/png"); } } @@ -43,3 +59,12 @@ export class TextResponse extends ToolResponse { super([new TextContent(text)]); } } + +export class PNGResponse extends ToolResponse { + /** + * @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array) + */ + constructor(data: Uint8Array | object) { + super([new PNGImageContent(data)]); + } +} diff --git a/mcp-server/src/tools/ExportShapeTool.ts b/mcp-server/src/tools/ExportShapeTool.ts new file mode 100644 index 0000000..54b72d0 --- /dev/null +++ b/mcp-server/src/tools/ExportShapeTool.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import { PNGResponse, ToolResponse } from "../ToolResponse"; +import "reflect-metadata"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask"; +import { ExecuteCodeTaskParams } from "@penpot-mcp/common"; + +/** + * Arguments class for ExecuteCodeTool + */ +export class ExportShapeArgs { + static schema = { + shapeId: z.string().min(1, "shapeId cannot be empty"), + }; + + /** + * Identifier of the shape to export. + * The special identifier "selection" can be used to refer to the (first) currently selected shape. + */ + shapeId!: string; +} + +/** + * Tool for executing JavaScript code in the Penpot plugin context + */ +export class ExportShapeTool extends Tool { + /** + * Creates a new ExecuteCode tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, ExportShapeArgs.schema); + } + + public getToolName(): string { + return "export_shapes"; + } + + public getToolDescription(): string { + return ( + "Exports a shape from the Penpot design to a PNG image, such that you can get an impression of what the shape looks like.\n" + + "Parameter `shapeId`: identifier of the shapes to export. Use the special identifier 'selection' to " + + "export the first shape currently selected by the user." + ); + } + + protected async executeCore(args: ExportShapeArgs): Promise { + // create code for exporting the shape + let taskParams: ExecuteCodeTaskParams; + if (args.shapeId === "selection") { + taskParams = { code: 'return penpot.selection[0].export({"type": "png"});' }; + } else { + throw new Error("Identifiers other than 'selection' are not yet supported"); + } + + // execute the code and return the response + const task = new ExecuteCodePluginTask(taskParams); + const result = await this.mcpServer.pluginBridge.executePluginTask(task); + return new PNGResponse(result.data!.result); + } +}