Improve error handling in PluginTask execution

This commit is contained in:
Dominik Jain 2025-09-12 18:36:13 +02:00
parent 736c25ecc2
commit 23d2270df0
6 changed files with 54 additions and 77 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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({