From 5ffaabd728154de7f37fd61dfb1d5aa221f1784c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 19 Sep 2025 19:35:08 +0200 Subject: [PATCH] Add code execution tool --- .gitignore | 3 + common/src/types.ts | 10 +++ mcp-server/src/PenpotMcpServer.ts | 3 +- mcp-server/src/tasks/ExecuteCodePluginTask.ts | 19 ++++++ mcp-server/src/tools/ExecuteCodeTool.ts | 61 +++++++++++++++++++ penpot-plugin/src/plugin.ts | 6 +- .../task-handlers/ExecuteCodeTaskHandler.ts | 44 +++++++++++++ 7 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 mcp-server/src/tasks/ExecuteCodePluginTask.ts create mode 100644 mcp-server/src/tools/ExecuteCodeTool.ts create mode 100644 penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts diff --git a/.gitignore b/.gitignore index b61bc19..137cd90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .idea node_modules dist +*.bak +*.orig temp +*.tsbuildinfo diff --git a/common/src/types.ts b/common/src/types.ts index 0e3a931..27a2c0f 100644 --- a/common/src/types.ts +++ b/common/src/types.ts @@ -68,3 +68,13 @@ export interface PrintTextTaskParams { */ text: string; } + +/** + * Parameters for the executeCode task. + */ +export interface ExecuteCodeTaskParams { + /** + * The JavaScript code to be executed. + */ + code: string; +} diff --git a/mcp-server/src/PenpotMcpServer.ts b/mcp-server/src/PenpotMcpServer.ts index d9ef53e..3f0cb8a 100644 --- a/mcp-server/src/PenpotMcpServer.ts +++ b/mcp-server/src/PenpotMcpServer.ts @@ -4,6 +4,7 @@ import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema } from "@ import { ToolInterface } from "./Tool"; import { HelloWorldTool } from "./tools/HelloWorldTool"; import { PrintTextTool } from "./tools/PrintTextTool"; +import { ExecuteCodeTool } from "./tools/ExecuteCodeTool"; import { PluginBridge } from "./PluginBridge"; import { createLogger } from "./logger"; @@ -58,7 +59,7 @@ export class PenpotMcpServer { * the internal registry for later execution. */ private registerTools(): void { - const toolInstances: ToolInterface[] = [new HelloWorldTool(this), new PrintTextTool(this)]; + const toolInstances: ToolInterface[] = [new HelloWorldTool(this), new PrintTextTool(this), new ExecuteCodeTool(this)]; for (const tool of toolInstances) { this.tools.set(tool.definition.name, tool); diff --git a/mcp-server/src/tasks/ExecuteCodePluginTask.ts b/mcp-server/src/tasks/ExecuteCodePluginTask.ts new file mode 100644 index 0000000..5c107c5 --- /dev/null +++ b/mcp-server/src/tasks/ExecuteCodePluginTask.ts @@ -0,0 +1,19 @@ +import { PluginTask } from "../PluginTask"; +import { ExecuteCodeTaskParams, PluginTaskResult } from "@penpot-mcp/common"; + +/** + * Task for executing JavaScript code in the plugin context. + * + * This task instructs the plugin to execute arbitrary JavaScript code + * and return the result of execution. + */ +export class ExecuteCodePluginTask extends PluginTask> { + /** + * Creates a new execute code task. + * + * @param params - The parameters containing the code to execute + */ + constructor(params: ExecuteCodeTaskParams) { + super("executeCode", params); + } +} diff --git a/mcp-server/src/tools/ExecuteCodeTool.ts b/mcp-server/src/tools/ExecuteCodeTool.ts new file mode 100644 index 0000000..f3f6ee7 --- /dev/null +++ b/mcp-server/src/tools/ExecuteCodeTool.ts @@ -0,0 +1,61 @@ +import { IsNotEmpty, IsString } from "class-validator"; +import { Tool } from "../Tool"; +import type { ToolResponse } from "../ToolResponse"; +import { TextResponse } 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 ExecuteCodeArgs { + /** + * The JavaScript code to execute in the plugin context. + */ + @IsString({ message: "Code must be a string" }) + @IsNotEmpty({ message: "Code cannot be empty" }) + code!: string; +} + +/** + * Tool for executing JavaScript code in the Penpot plugin context + */ +export class ExecuteCodeTool extends Tool { + /** + * Creates a new ExecuteCode tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, ExecuteCodeArgs); + } + + protected getToolName(): string { + return "execute_code"; + } + + protected getToolDescription(): string { + return ( + "Executes JavaScript code in the Penpot plugin context. " + + "Two objects are available: `penpot` (the Penpot API) and `storage` (an object in which arbitrary " + + "data can be stored, simply by adding a new attribute; stored attributes can be referenced in future calls " + + "to this tool, so any intermediate results that could come in handy later should be stored in `storage` " + + "instead of just a fleeting variable).\n" + + "The tool call returns the value of the concluding return statement, if any." + ); + } + + protected async executeCore(args: ExecuteCodeArgs): Promise { + const taskParams: ExecuteCodeTaskParams = { code: args.code }; + const task = new ExecuteCodePluginTask(taskParams); + const result = await this.mcpServer.pluginBridge.executePluginTask(task); + + if (result.data !== undefined) { + return new TextResponse(`Code executed successfully. Result: ${JSON.stringify(result.data, null, 2)}`); + } else { + return new TextResponse("Code executed successfully with no return value."); + } + } +} diff --git a/penpot-plugin/src/plugin.ts b/penpot-plugin/src/plugin.ts index 3a560c4..aa5d220 100644 --- a/penpot-plugin/src/plugin.ts +++ b/penpot-plugin/src/plugin.ts @@ -1,4 +1,5 @@ import {PrintTextTaskHandler} from "./task-handlers/PrintTextTaskHandler"; +import {ExecuteCodeTaskHandler} from "./task-handlers/ExecuteCodeTaskHandler"; import {Task, TaskHandler} from "./TaskHandler"; /** @@ -6,6 +7,7 @@ import {Task, TaskHandler} from "./TaskHandler"; */ const taskHandlers: TaskHandler[] = [ new PrintTextTaskHandler(), + new ExecuteCodeTaskHandler(), ]; penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`); @@ -52,12 +54,12 @@ function handlePluginTaskRequest(request: { id: string; task: string; params: an handler.handle(task); console.log("Task handled successfully:", task); } catch (error) { - console.error("Error creating text:", error); + console.error("Error handling task:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error"; task.sendError(`Error handling task: ${errorMessage}`); } } else { - console.warn("Unknown plugin task:", request.task); + console.error("Unknown plugin task:", request.task); task.sendError(`Unknown task type: ${request.task}`); } } diff --git a/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts new file mode 100644 index 0000000..f97a2db --- /dev/null +++ b/penpot-plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -0,0 +1,44 @@ +import {Task, TaskHandler} from "../TaskHandler"; +import {ExecuteCodeTaskParams} from "../../../common/src"; + +/** + * Task handler for executing JavaScript code in the plugin context. + * + * Maintains a persistent context object that preserves state between code executions. + */ +export class ExecuteCodeTaskHandler extends TaskHandler { + readonly taskType = "executeCode"; + + /** + * Persistent context object that maintains state between code executions. + * Contains the penpot API and any variables defined in executed code. + */ + private readonly context: any; + + constructor() { + super(); + + // initialize context, making penpot object available + this.context = { + penpot: penpot, + storage: {} + }; + } + + handle(task: Task): void { + if (!task.params.code) { + task.sendError("executeCode task requires 'code' parameter"); + return; + } + + const context = this.context; + const code = task.params.code; + + const result = (function (ctx) { + return Function(...Object.keys(ctx), code)(...Object.values(ctx)); + })(context); + + console.log("Code execution result:", result); + task.sendSuccess(result); + } +}