Add code execution tool

This commit is contained in:
Dominik Jain 2025-09-19 19:35:08 +02:00 committed by Dominik Jain
parent 9fb3ccc2e2
commit 5ffaabd728
7 changed files with 143 additions and 3 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
.idea
node_modules
dist
*.bak
*.orig
temp
*.tsbuildinfo

View File

@ -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;
}

View File

@ -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);

View File

@ -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<ExecuteCodeTaskParams, PluginTaskResult<any>> {
/**
* Creates a new execute code task.
*
* @param params - The parameters containing the code to execute
*/
constructor(params: ExecuteCodeTaskParams) {
super("executeCode", params);
}
}

View File

@ -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<ExecuteCodeArgs> {
/**
* 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<ToolResponse> {
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.");
}
}
}

View File

@ -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}`);
}
}

View File

@ -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<ExecuteCodeTaskParams> {
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<ExecuteCodeTaskParams>): 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);
}
}