mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Improve ExportShapeTool
* Add support for SVG * Add support for shape identifiers * Add support for writing result to file
This commit is contained in:
parent
eda3f855b4
commit
649506cc9e
@ -26,7 +26,7 @@ class ImageContent implements ImageItem {
|
||||
/**
|
||||
* @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array)
|
||||
*/
|
||||
protected static byteData(data: Uint8Array | object): Uint8Array {
|
||||
public 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[]);
|
||||
@ -36,7 +36,7 @@ class ImageContent implements ImageItem {
|
||||
}
|
||||
}
|
||||
|
||||
class PNGImageContent extends ImageContent {
|
||||
export class PNGImageContent extends ImageContent {
|
||||
/**
|
||||
* @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array)
|
||||
*/
|
||||
|
||||
@ -12,7 +12,10 @@ import { ExecuteCodeTaskParams } from "@penpot-mcp/common";
|
||||
*/
|
||||
export class ExecuteCodeArgs {
|
||||
static schema = {
|
||||
code: z.string().min(1, "Code cannot be empty"),
|
||||
code: z
|
||||
.string()
|
||||
.min(1, "Code cannot be empty")
|
||||
.describe("The JavaScript code to execute in the plugin context."),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,24 +1,38 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import { PNGResponse, ToolResponse } from "../ToolResponse";
|
||||
import { PNGImageContent, PNGResponse, TextResponse, ToolResponse } from "../ToolResponse";
|
||||
import "reflect-metadata";
|
||||
import { PenpotMcpServer } from "../PenpotMcpServer";
|
||||
import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask";
|
||||
import { ExecuteCodeTaskParams } from "@penpot-mcp/common";
|
||||
import { FileUtils } from "../utils/FileUtils";
|
||||
|
||||
/**
|
||||
* Arguments class for ExecuteCodeTool
|
||||
*/
|
||||
export class ExportShapeArgs {
|
||||
static schema = {
|
||||
shapeId: z.string().min(1, "shapeId cannot be empty"),
|
||||
shapeId: z
|
||||
.string()
|
||||
.min(1, "shapeId cannot be empty")
|
||||
.describe(
|
||||
"Identifier of the shape to export. Use the special identifier 'selection' to " +
|
||||
"export the first shape currently selected by the user."
|
||||
),
|
||||
format: z.enum(["svg", "png"]).default("png").describe("The output format, either PNG (default) or SVG."),
|
||||
filePath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional file path to save the exported image to. If not provided, " +
|
||||
"the image data is returned directly for you to see."
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Identifier of the shape to export.
|
||||
* The special identifier "selection" can be used to refer to the (first) currently selected shape.
|
||||
*/
|
||||
shapeId!: string;
|
||||
|
||||
format: "svg" | "png" = "png";
|
||||
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,29 +49,53 @@ export class ExportShapeTool extends Tool<ExportShapeArgs> {
|
||||
}
|
||||
|
||||
public getToolName(): string {
|
||||
return "export_shapes";
|
||||
return "export_shape";
|
||||
}
|
||||
|
||||
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."
|
||||
"Exports a shape from the Penpot design to a PNG or SVG image, " +
|
||||
"such that you can get an impression of what the shape looks like.\n" +
|
||||
"Alternatively, you can save it to a file."
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeCore(args: ExportShapeArgs): Promise<ToolResponse> {
|
||||
// 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");
|
||||
// check arguments
|
||||
if (args.filePath) {
|
||||
FileUtils.checkPathIsAbsolute(args.filePath);
|
||||
}
|
||||
|
||||
// execute the code and return the response
|
||||
const task = new ExecuteCodePluginTask(taskParams);
|
||||
// create code for exporting the shape
|
||||
let shapeCode: string;
|
||||
if (args.shapeId === "selection") {
|
||||
shapeCode = `penpot.selection[0]`;
|
||||
} else {
|
||||
shapeCode = `penpotUtils.findShapeById("${args.shapeId}")`;
|
||||
}
|
||||
const code = `return ${shapeCode}.export({"type": "${args.format}"});`;
|
||||
|
||||
// execute the code and obtain the image data
|
||||
const task = new ExecuteCodePluginTask({ code: code });
|
||||
const result = await this.mcpServer.pluginBridge.executePluginTask(task);
|
||||
return new PNGResponse(result.data!.result);
|
||||
const imageData = result.data!.result;
|
||||
|
||||
// handle output and return response
|
||||
if (!args.filePath) {
|
||||
// return image data directly (for the LLM to "see" it)
|
||||
if (args.format === "png") {
|
||||
return new PNGResponse(imageData);
|
||||
} else {
|
||||
return new TextResponse(imageData);
|
||||
}
|
||||
} else {
|
||||
// save image to file
|
||||
if (args.format === "png") {
|
||||
FileUtils.writeBinaryFile(args.filePath, PNGImageContent.byteData(imageData));
|
||||
} else {
|
||||
FileUtils.writeTextFile(args.filePath, imageData);
|
||||
}
|
||||
return new TextResponse(`The shape has been exported to ${args.filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
mcp-server/src/utils/FileUtils.ts
Normal file
42
mcp-server/src/utils/FileUtils.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export class FileUtils {
|
||||
/**
|
||||
* Checks whether the given file path is absolute and raises an error if not.
|
||||
*
|
||||
* @param filePath - The file path to check
|
||||
*/
|
||||
public static checkPathIsAbsolute(filePath: string): void {
|
||||
if (!require("path").isAbsolute(filePath)) {
|
||||
throw new Error(`The specified file path must be absolute: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static createParentDirectories(filePath: string): void {
|
||||
const path = require("path");
|
||||
const dir = path.dirname(filePath);
|
||||
if (!require("fs").existsSync(dir)) {
|
||||
require("fs").mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes binary data to a file at the specified path, creating the parent directories if necessary.
|
||||
*
|
||||
* @param filePath - The absolute path to the file where data should be written
|
||||
* @param bytes - The binary data to write to the file
|
||||
*/
|
||||
public static writeBinaryFile(filePath: string, bytes: Uint8Array): void {
|
||||
this.createParentDirectories(filePath);
|
||||
require("fs").writeFileSync(filePath, Buffer.from(bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes text data to a file at the specified path, creating the parent directories if necessary.
|
||||
*
|
||||
* @param filePath - The absolute path to the file where data should be written
|
||||
* @param text - The text data to write to the file
|
||||
*/
|
||||
public static writeTextFile(filePath: string, text: string): void {
|
||||
this.createParentDirectories(filePath);
|
||||
require("fs").writeFileSync(filePath, text, { encoding: "utf-8" });
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user