diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index b103288..34e0749 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -20,11 +20,12 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; -import { WebSocketServer, WebSocket } from "ws"; +import { WebSocket, WebSocketServer } from "ws"; import { ToolInterface } from "./interfaces/Tool.js"; import { HelloWorldTool } from "./tools/HelloWorldTool.js"; -import { PrintTextTool } from "./tools/PrintTextTool"; +import { PrintTextTool } from "./tools/PrintTextTool.js"; +import { PluginTask } from "./interfaces/PluginTask.js"; /** * Main MCP server implementation for Penpot integration. @@ -32,7 +33,7 @@ import { PrintTextTool } from "./tools/PrintTextTool"; * This server manages tool registration and execution using a clean * abstraction pattern that allows for easy extension with new tools. */ -class PenpotMcpServer { +export class PenpotMcpServer { private readonly server: Server; private readonly tools: Map; private readonly wsServer: WebSocketServer; @@ -80,7 +81,7 @@ class PenpotMcpServer { * the internal registry for later execution. */ private registerTools(): void { - const toolInstances: ToolInterface[] = [new HelloWorldTool(), new PrintTextTool(this.connectedClients)]; + const toolInstances: ToolInterface[] = [new HelloWorldTool(this), new PrintTextTool(this)]; for (const tool of toolInstances) { this.tools.set(tool.definition.name, tool); @@ -265,6 +266,29 @@ class PenpotMcpServer { console.error("WebSocket server started on port 8080"); } + public executePluginTask(task: PluginTask) { + // Check if there are connected clients + if (this.connectedClients.size === 0) { + throw new Error( + `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` + ); + } + + // Send task to all connected clients + const taskMessage = JSON.stringify(task.toJSON()); + let sentCount = 0; + this.connectedClients.forEach((client) => { + if (client.readyState === 1) { + // WebSocket.OPEN + client.send(taskMessage); + sentCount++; + } + }); + if (sentCount === 0) { + throw new Error(`All connected plugin instances appear to be disconnected. No text was created.`); + } + } + /** * Starts the MCP server using HTTP and SSE transports. * diff --git a/mcp-server/src/interfaces/PluginTask.ts b/mcp-server/src/interfaces/PluginTask.ts index 534a394..84caa42 100644 --- a/mcp-server/src/interfaces/PluginTask.ts +++ b/mcp-server/src/interfaces/PluginTask.ts @@ -6,7 +6,7 @@ * * @template TParams - The strongly-typed parameters for this task */ -export abstract class PluginTask { +export abstract class PluginTask { /** * The name of the task to execute on the plugin side. */ @@ -17,6 +17,8 @@ export abstract class PluginTask { */ public readonly params: TParams; + private result?: Promise = undefined; + /** * Creates a new plugin task instance. * @@ -28,6 +30,17 @@ export abstract class PluginTask { this.params = params; } + /** + * Sets the result promise for this task. + * + * This can be used to track the outcome of the task execution. + * + * @param resultPromise - A promise that resolves to the task result + */ + setResult(resultPromise: Promise): void { + this.result = resultPromise; + } + /** * Serializes the task to JSON for WebSocket transmission. */ diff --git a/mcp-server/src/interfaces/Tool.ts b/mcp-server/src/interfaces/Tool.ts index 5f867c3..1b6e19a 100644 --- a/mcp-server/src/interfaces/Tool.ts +++ b/mcp-server/src/interfaces/Tool.ts @@ -2,7 +2,8 @@ import { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js"; import { validate, ValidationError } from "class-validator"; import { plainToClass } from "class-transformer"; import "reflect-metadata"; -import { ToolResponse } from "./ToolResponse"; +import { TextResponse, ToolResponse } from "./ToolResponse.js"; +import type { PenpotMcpServer } from "../index.js"; /** * Base interface for MCP tool implementations. @@ -45,7 +46,10 @@ interface PropertyMetadata { export abstract class Tool implements ToolInterface { private _definition: MCPTool | undefined; - constructor(private ArgsClass: new () => TArgs) {} + protected constructor( + protected mcpServer: PenpotMcpServer, + private ArgsClass: new () => TArgs + ) {} /** * Gets the tool definition with automatically generated JSON schema. @@ -69,24 +73,20 @@ export abstract class Tool implements ToolInterface { */ async execute(args: unknown): Promise { try { - // Transform plain object to class instance + // transform plain object to class instance const argsInstance = plainToClass(this.ArgsClass, args as object); - // Validate using class-validator decorators + // validate using class-validator decorators const errors = await validate(argsInstance); - if (errors.length > 0) { const errorMessages = this.formatValidationErrors(errors); throw new Error(`Validation failed: ${errorMessages.join(", ")}`); } - // Call the type-safe implementation + // execute the actual tool logic return await this.executeCore(argsInstance); } catch (error) { - if (error instanceof Error) { - throw error; - } - throw new Error(`Tool execution failed: ${String(error)}`); + return new TextResponse(`Tool execution failed: ${String(error)}`); } } diff --git a/mcp-server/src/tools/HelloWorldTool.ts b/mcp-server/src/tools/HelloWorldTool.ts index 59e0d95..2198b8f 100644 --- a/mcp-server/src/tools/HelloWorldTool.ts +++ b/mcp-server/src/tools/HelloWorldTool.ts @@ -3,6 +3,7 @@ import { Tool } from "../interfaces/Tool.js"; import "reflect-metadata"; import type { ToolResponse } from "../interfaces/ToolResponse.js"; import { TextResponse } from "../interfaces/ToolResponse.js"; +import { PenpotMcpServer } from "../index"; /** * Arguments class for the HelloWorld tool with validation decorators. @@ -23,8 +24,11 @@ export class HelloWorldArgs { * type safety through the protected executeTypeSafe method. */ export class HelloWorldTool extends Tool { - constructor() { - super(HelloWorldArgs); + /** + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, HelloWorldArgs); } protected getToolName(): string { diff --git a/mcp-server/src/tools/PrintTextTool.ts b/mcp-server/src/tools/PrintTextTool.ts index 2f10fab..a47c7c9 100644 --- a/mcp-server/src/tools/PrintTextTool.ts +++ b/mcp-server/src/tools/PrintTextTool.ts @@ -4,6 +4,7 @@ import { PluginTaskPrintText, PluginTaskPrintTextParams } from "../interfaces/Pl import type { ToolResponse } from "../interfaces/ToolResponse.js"; import { TextResponse } from "../interfaces/ToolResponse.js"; import "reflect-metadata"; +import { PenpotMcpServer } from "../index.js"; /** * Arguments class for the PrintText tool with validation decorators. @@ -24,16 +25,13 @@ export class PrintTextArgs { * instructing them to create and position text elements in the canvas. */ export class PrintTextTool extends Tool { - private connectedClients: Set; // WebSocket clients - /** * Creates a new PrintText tool instance. * - * @param connectedClients - Set of connected WebSocket clients + * @param mcpServer - The MCP server instance */ - constructor(connectedClients: Set) { - super(PrintTextArgs); - this.connectedClients = connectedClients; + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, PrintTextArgs); } protected getToolName(): string { @@ -44,51 +42,12 @@ export class PrintTextTool extends Tool { return "Creates text in Penpot at the viewport center and selects it"; } - /** - * Executes the print text functionality by sending a task to connected plugins. - * - * This method creates a PluginTaskPrintText and broadcasts it to all - * connected WebSocket clients for execution. - * - * @param args - The validated PrintTextArgs instance - */ protected async executeCore(args: PrintTextArgs): Promise { - try { - // Create the plugin task - const taskParams = new PluginTaskPrintTextParams(args.text); - const task = new PluginTaskPrintText(taskParams); - - // Check if there are connected clients - if (this.connectedClients.size === 0) { - return new TextResponse( - `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` - ); - } - - // Send task to all connected clients - const taskMessage = JSON.stringify(task.toJSON()); - let sentCount = 0; - - this.connectedClients.forEach((client) => { - if (client.readyState === 1) { - // WebSocket.OPEN - client.send(taskMessage); - sentCount++; - } - }); - - if (sentCount === 0) { - return new TextResponse( - `All connected plugin instances appear to be disconnected. No text was created.` - ); - } - - return new TextResponse( - `Successfully sent text creation task to ${sentCount} connected plugin instance(s). Text "${args.text}" should now appear in Penpot.` - ); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - return new TextResponse(`Failed to create text in Penpot: ${errorMessage}`); - } + const taskParams = new PluginTaskPrintTextParams(args.text); + const task = new PluginTaskPrintText(taskParams); + this.mcpServer.executePluginTask(task); + return new TextResponse( + `Successfully sent text creation task. Text "${args.text}" should now appear in Penpot.` + ); } }