mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Refactoring
* Handle plugin task execution centrally in MCP server * Centrally handle exceptions in Tool base class, returning an error message
This commit is contained in:
parent
472dcd7890
commit
6636544f88
@ -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<string, ToolInterface>;
|
||||
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.
|
||||
*
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*
|
||||
* @template TParams - The strongly-typed parameters for this task
|
||||
*/
|
||||
export abstract class PluginTask<TParams = any> {
|
||||
export abstract class PluginTask<TParams = any, TResult = any> {
|
||||
/**
|
||||
* The name of the task to execute on the plugin side.
|
||||
*/
|
||||
@ -17,6 +17,8 @@ export abstract class PluginTask<TParams = any> {
|
||||
*/
|
||||
public readonly params: TParams;
|
||||
|
||||
private result?: Promise<TResult> = undefined;
|
||||
|
||||
/**
|
||||
* Creates a new plugin task instance.
|
||||
*
|
||||
@ -28,6 +30,17 @@ export abstract class PluginTask<TParams = any> {
|
||||
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<TResult>): void {
|
||||
this.result = resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the task to JSON for WebSocket transmission.
|
||||
*/
|
||||
|
||||
@ -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<TArgs extends object> 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<TArgs extends object> implements ToolInterface {
|
||||
*/
|
||||
async execute(args: unknown): Promise<ToolResponse> {
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<HelloWorldArgs> {
|
||||
constructor() {
|
||||
super(HelloWorldArgs);
|
||||
/**
|
||||
* @param mcpServer - The MCP server instance
|
||||
*/
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, HelloWorldArgs);
|
||||
}
|
||||
|
||||
protected getToolName(): string {
|
||||
|
||||
@ -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<PrintTextArgs> {
|
||||
private connectedClients: Set<any>; // WebSocket clients
|
||||
|
||||
/**
|
||||
* Creates a new PrintText tool instance.
|
||||
*
|
||||
* @param connectedClients - Set of connected WebSocket clients
|
||||
* @param mcpServer - The MCP server instance
|
||||
*/
|
||||
constructor(connectedClients: Set<any>) {
|
||||
super(PrintTextArgs);
|
||||
this.connectedClients = connectedClients;
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, PrintTextArgs);
|
||||
}
|
||||
|
||||
protected getToolName(): string {
|
||||
@ -44,51 +42,12 @@ export class PrintTextTool extends Tool<PrintTextArgs> {
|
||||
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<ToolResponse> {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user