mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-05-19 23:13:47 +00:00
Improve error handling in PluginTask execution
This commit is contained in:
parent
736c25ecc2
commit
23d2270df0
@ -3,21 +3,11 @@
|
||||
*
|
||||
* Contains the outcome status of a task and any additional result data.
|
||||
*/
|
||||
export interface PluginTaskResult {
|
||||
/**
|
||||
* Whether the task completed successfully.
|
||||
*/
|
||||
success: boolean;
|
||||
|
||||
/**
|
||||
* Optional error message if the task failed.
|
||||
*/
|
||||
error?: string;
|
||||
|
||||
export interface PluginTaskResult<T> {
|
||||
/**
|
||||
* Optional result data from the task execution.
|
||||
*/
|
||||
data?: any;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,16 +37,26 @@ export interface PluginTaskRequest {
|
||||
*
|
||||
* Contains the original request ID and the execution result.
|
||||
*/
|
||||
export interface PluginTaskResponse {
|
||||
export interface PluginTaskResponse<T> {
|
||||
/**
|
||||
* Unique identifier matching the original request.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the task completed successfully.
|
||||
*/
|
||||
success: boolean;
|
||||
|
||||
/**
|
||||
* Optional error message if the task failed.
|
||||
*/
|
||||
error?: string;
|
||||
|
||||
/**
|
||||
* The result of the task execution.
|
||||
*/
|
||||
result: PluginTaskResult;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -11,7 +11,10 @@ export class PluginBridge {
|
||||
private readonly pendingTasks: Map<string, PluginTask<any, any>> = new Map();
|
||||
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||
|
||||
constructor(port: number) {
|
||||
constructor(
|
||||
port: number,
|
||||
private taskTimeoutSecs: number = 30
|
||||
) {
|
||||
this.wsServer = new WebSocketServer({ port: port });
|
||||
this.setupWebSocketHandlers();
|
||||
}
|
||||
@ -30,7 +33,7 @@ export class PluginBridge {
|
||||
ws.on("message", (data: Buffer) => {
|
||||
console.error("Received WebSocket message:", data.toString());
|
||||
try {
|
||||
const response: PluginTaskResponse = JSON.parse(data.toString());
|
||||
const response: PluginTaskResponse<any> = JSON.parse(data.toString());
|
||||
this.handlePluginTaskResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse WebSocket message:", error);
|
||||
@ -59,7 +62,7 @@ export class PluginBridge {
|
||||
*
|
||||
* @param response - The plugin task response containing ID and result
|
||||
*/
|
||||
private handlePluginTaskResponse(response: PluginTaskResponse): void {
|
||||
private handlePluginTaskResponse(response: PluginTaskResponse<any>): void {
|
||||
const task = this.pendingTasks.get(response.id);
|
||||
if (!task) {
|
||||
console.error(`Received response for unknown task ID: ${response.id}`);
|
||||
@ -75,14 +78,14 @@ export class PluginBridge {
|
||||
this.pendingTasks.delete(response.id);
|
||||
|
||||
// Resolve or reject the task's promise based on the result
|
||||
if (response.result.success) {
|
||||
task.resolveWithResult(response.result);
|
||||
if (response.success) {
|
||||
task.resolveWithResult({ data: response.data });
|
||||
} else {
|
||||
const error = new Error(response.result.error || "Task execution failed");
|
||||
const error = new Error(response.error || "Task execution failed (details not provided)");
|
||||
task.rejectWithError(error);
|
||||
}
|
||||
|
||||
console.error(`Task ${response.id} completed with success: ${response.result.success}`);
|
||||
console.error(`Task ${response.id} completed: success=${response.success}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,7 +97,7 @@ export class PluginBridge {
|
||||
* @param task - The plugin task to execute
|
||||
* @throws Error if no plugin instances are connected or available
|
||||
*/
|
||||
public async executePluginTask<TResult extends PluginTaskResult>(task: PluginTask<any, TResult>): Promise<void> {
|
||||
public async executePluginTask<TResult>(task: PluginTask<any, TResult>): Promise<TResult> {
|
||||
// Check if there are connected clients
|
||||
if (this.connectedClients.size === 0) {
|
||||
throw new Error(
|
||||
@ -105,7 +108,7 @@ export class PluginBridge {
|
||||
// Register the task for result correlation
|
||||
this.pendingTasks.set(task.id, task);
|
||||
|
||||
// Send task to all connected clients using the new request format
|
||||
// Send task to all connected clients
|
||||
const requestMessage = JSON.stringify(task.toRequest());
|
||||
let sentCount = 0;
|
||||
this.connectedClients.forEach((client) => {
|
||||
@ -133,11 +136,15 @@ export class PluginBridge {
|
||||
if (pendingTask) {
|
||||
this.pendingTasks.delete(task.id);
|
||||
this.taskTimeouts.delete(task.id);
|
||||
pendingTask.rejectWithError(new Error(`Task ${task.id} timed out after 30 seconds`));
|
||||
pendingTask.rejectWithError(
|
||||
new Error(`Task ${task.id} timed out after ${this.taskTimeoutSecs} seconds`)
|
||||
);
|
||||
}
|
||||
}, 30000);
|
||||
}, this.taskTimeoutSecs * 1000);
|
||||
|
||||
this.taskTimeouts.set(task.id, timeoutHandle);
|
||||
console.error(`Sent task ${task.id} to ${sentCount} connected clients`);
|
||||
|
||||
return await task.getResultPromise();
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export abstract class PluginTask<TParams = any, TResult extends PluginTaskResult
|
||||
/**
|
||||
* Promise that resolves when the task execution completes.
|
||||
*/
|
||||
private result?: Promise<TResult>;
|
||||
private readonly result: Promise<TResult>;
|
||||
|
||||
/**
|
||||
* Resolver function for the result promise.
|
||||
@ -59,16 +59,6 @@ export abstract class PluginTask<TParams = any, TResult extends PluginTaskResult
|
||||
this.id = randomUUID();
|
||||
this.task = task;
|
||||
this.params = params;
|
||||
this.setupResultPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the result promise and its resolvers.
|
||||
*
|
||||
* Creates a promise that can be resolved externally when
|
||||
* the task result is received from the plugin.
|
||||
*/
|
||||
private setupResultPromise(): void {
|
||||
this.result = new Promise<TResult>((resolve, reject) => {
|
||||
this.resolveResult = resolve;
|
||||
this.rejectResult = reject;
|
||||
|
||||
@ -46,19 +46,7 @@ export class PrintTextTool extends Tool<PrintTextArgs> {
|
||||
protected async executeCore(args: PrintTextArgs): Promise<ToolResponse> {
|
||||
const taskParams: PrintTextTaskParams = { text: args.text };
|
||||
const task = new PrintTextPluginTask(taskParams);
|
||||
|
||||
try {
|
||||
await this.mcpServer.pluginBridge.executePluginTask(task);
|
||||
const result = await task.getResultPromise();
|
||||
|
||||
if (result.success) {
|
||||
return new TextResponse(`Successfully created text "${args.text}" in Penpot.`);
|
||||
} else {
|
||||
return new TextResponse(`Failed to create text in Penpot: ${result.error || "Unknown error"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
return new TextResponse(`Failed to execute text creation task: ${errorMessage}`);
|
||||
}
|
||||
await this.mcpServer.pluginBridge.executePluginTask(task);
|
||||
return new TextResponse(`Successfully created text "${args.text}" in Penpot.`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,10 +38,7 @@ function handlePluginTaskRequest(request: { id: string; task: string; params: an
|
||||
|
||||
default:
|
||||
console.warn("Unknown plugin task:", request.task);
|
||||
sendTaskResponse(request.id, {
|
||||
success: false,
|
||||
error: `Unknown task type: ${request.task}`,
|
||||
});
|
||||
sendTaskError(request.id, `Unknown task type: ${request.task}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,10 +51,7 @@ function handlePluginTaskRequest(request: { id: string; task: string; params: an
|
||||
function handlePrintTextTask(taskId: string, params: { text: string }): void {
|
||||
if (!params.text) {
|
||||
console.error("printText task requires 'text' parameter");
|
||||
sendTaskResponse(taskId, {
|
||||
success: false,
|
||||
error: "printText task requires 'text' parameter",
|
||||
});
|
||||
sendTaskError(taskId, "printText task requires 'text' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -73,39 +67,29 @@ function handlePrintTextTask(taskId: string, params: { text: string }): void {
|
||||
penpot.selection = [text];
|
||||
|
||||
console.log("Successfully created text:", params.text);
|
||||
sendTaskResponse(taskId, {
|
||||
success: true,
|
||||
data: { textId: text.id },
|
||||
});
|
||||
sendTaskSuccess(taskId, { textId: text.id });
|
||||
} else {
|
||||
console.error("Failed to create text element");
|
||||
sendTaskResponse(taskId, {
|
||||
success: false,
|
||||
error: "Failed to create text element",
|
||||
});
|
||||
sendTaskError(taskId, "Failed to create text element");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating text:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
sendTaskResponse(taskId, {
|
||||
success: false,
|
||||
error: `Error creating text: ${errorMessage}`,
|
||||
});
|
||||
sendTaskError(taskId, `Error creating text: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a task response back to the MCP server.
|
||||
*
|
||||
* @param taskId - The unique ID of the original task request
|
||||
* @param result - The task execution result
|
||||
*/
|
||||
function sendTaskResponse(taskId: string, result: { success: boolean; error?: string; data?: any }): void {
|
||||
function sendTaskResponse(taskId: string, success: boolean, data: any = undefined, error: any = undefined): void {
|
||||
const response = {
|
||||
type: "task-response",
|
||||
response: {
|
||||
id: taskId,
|
||||
result: result,
|
||||
success: success,
|
||||
data: data,
|
||||
error: error,
|
||||
},
|
||||
};
|
||||
|
||||
@ -114,6 +98,14 @@ function sendTaskResponse(taskId: string, result: { success: boolean; error?: st
|
||||
console.log("Sent task response:", response);
|
||||
}
|
||||
|
||||
function sendTaskSuccess(taskId: string, data: any = undefined): void {
|
||||
sendTaskResponse(taskId, true, data);
|
||||
}
|
||||
|
||||
function sendTaskError(taskId: string, error: string): void {
|
||||
sendTaskResponse(taskId, false, undefined, error);
|
||||
}
|
||||
|
||||
// Update the theme in the iframe
|
||||
penpot.on("themechange", (theme) => {
|
||||
penpot.ui.sendMessage({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user