mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Establish return channel when executing plugin tasks
and package 'common' for representations used in both subprojects
This commit is contained in:
parent
283f01b0ac
commit
b7d1171654
@ -1,31 +1,70 @@
|
|||||||
# Penpot MCP Project Overview
|
# Penpot MCP Project Overview - Updated
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
This project is a Model Context Protocol (MCP) server for Penpot integration. It provides a TypeScript-based server that can be used to extend Penpot's functionality through custom tools.
|
This project is a Model Context Protocol (MCP) server for Penpot integration. It provides a TypeScript-based server that can be used to extend Penpot's functionality through custom tools with bidirectional WebSocket communication.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
- **Runtime**: Node.js
|
- **Runtime**: Node.js
|
||||||
- **Framework**: MCP SDK (@modelcontextprotocol/sdk)
|
- **Framework**: MCP SDK (@modelcontextprotocol/sdk)
|
||||||
- **Build Tool**: TypeScript Compiler (tsc)
|
- **Build Tool**: TypeScript Compiler (tsc) + esbuild
|
||||||
- **Package Manager**: npm
|
- **Package Manager**: npm
|
||||||
|
- **WebSocket**: ws library for real-time communication
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
```
|
```
|
||||||
penpot-mcp/
|
penpot-mcp/
|
||||||
├── mcp-server/ # Main MCP server implementation
|
├── common/ # NEW: Shared type definitions
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── index.ts # Exports for shared types
|
||||||
|
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
||||||
|
│ └── package.json # @penpot-mcp/common package
|
||||||
|
├── mcp-server/ # Main MCP server implementation
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── index.ts # Main server entry point
|
│ │ ├── index.ts # Main server entry point
|
||||||
│ │ ├── interfaces/ # Type definitions and contracts
|
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
|
||||||
│ │ │ └── Tool.ts # Tool interface definition
|
│ │ ├── PluginTask.ts # Now supports result promises
|
||||||
|
│ │ ├── tasks/ # Task implementations
|
||||||
|
│ │ │ └── PrintTextPluginTask.ts # Uses shared types
|
||||||
│ │ └── tools/ # Tool implementations
|
│ │ └── tools/ # Tool implementations
|
||||||
│ │ └── HelloWorldTool.ts
|
│ │ ├── HelloWorldTool.ts
|
||||||
│ ├── package.json # Dependencies and scripts
|
│ │ └── PrintTextTool.ts # Now waits for task completion
|
||||||
│ └── tsconfig.json # TypeScript configuration
|
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||||
└── penpot-plugin/ # Penpot plugin (currently empty)
|
└── penpot-plugin/ # Penpot plugin with response capability
|
||||||
|
├── src/
|
||||||
|
│ ├── main.ts # Enhanced WebSocket handling with response forwarding
|
||||||
|
│ └── plugin.ts # Now sends task responses back to server
|
||||||
|
└── package.json # Includes @penpot-mcp/common dependency
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Components
|
## Key Components - Updated
|
||||||
- **PenpotMcpServer**: Main server class that manages tool registration and MCP protocol handling
|
|
||||||
- **Tool Interface**: Abstraction for all tool implementations
|
### Enhanced WebSocket Protocol
|
||||||
- **HelloWorldTool**: Example tool implementation demonstrating the pattern
|
- **Request Format**: `{id: string, task: string, params: any}`
|
||||||
|
- **Response Format**: `{id: string, result: {success: boolean, error?: string, data?: any}}`
|
||||||
|
- **Request/Response Correlation**: Using unique UUIDs for task tracking
|
||||||
|
- **Timeout Handling**: 30-second timeout with automatic cleanup
|
||||||
|
- **Type Safety**: Shared definitions via @penpot-mcp/common package
|
||||||
|
|
||||||
|
### Core Classes
|
||||||
|
- **PenpotMcpServer**: Enhanced with pending task tracking and response handling
|
||||||
|
- **PluginTask**: Now creates result promises that resolve when plugin responds
|
||||||
|
- **Tool implementations**: Now properly await task completion and report results
|
||||||
|
- **Plugin handlers**: Send structured responses back to server
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
1. **Bidirectional Communication**: Plugin now responds with success/failure status
|
||||||
|
2. **Task Result Promises**: Every executePluginTask() sets and returns a promise
|
||||||
|
3. **Error Reporting**: Failed tasks properly report error messages to tools
|
||||||
|
4. **Shared Type Safety**: Common package ensures consistency across projects
|
||||||
|
5. **Timeout Protection**: Tasks don't hang indefinitely (30s limit)
|
||||||
|
6. **Request Correlation**: Unique IDs match requests to responses
|
||||||
|
|
||||||
|
## Protocol Flow
|
||||||
|
```
|
||||||
|
LLM Tool Call → MCP Server → WebSocket (Request) → Plugin → Penpot API
|
||||||
|
↑ ↓
|
||||||
|
Tool Response ← MCP Server ← WebSocket (Response) ← Plugin Result
|
||||||
|
```
|
||||||
|
|
||||||
|
All components now properly handle the full request/response lifecycle with comprehensive error handling and type safety.
|
||||||
|
|||||||
59
README.md
59
README.md
@ -1,16 +1,57 @@
|
|||||||
# The Penpot MCP Server
|
# The Penpot MCP Server
|
||||||
|
|
||||||
The system consists of two main components:
|
This system enables LLMs to interact with Penpot design projects through a Model Context Protocol (MCP) server and plugin architecture.
|
||||||
|
|
||||||
1. **MCP Server** (`mcp-server/`):
|
## Architecture
|
||||||
- Runs the MCP server providing tools to an LLM for Penpot project interaction
|
|
||||||
- Runs a WebSocket server which accepts connections from the Penpot MCP Plugin,
|
|
||||||
establishing a communication channel between the plugin and the MCP server
|
|
||||||
|
|
||||||
2. **Penpot MCP Plugin** (`penpot-plugin/`):
|
The system consists of three main components:
|
||||||
- Establishes WebSocket connection to the MCP server
|
|
||||||
- Receives tasks from the MCP server, which it executes in the Penpot project, making
|
1. **Common Types** (`common/`):
|
||||||
use of the Penpot Plugin API
|
- Shared TypeScript definitions for request/response protocol
|
||||||
|
- Ensures type safety across server and plugin components
|
||||||
|
- Defines `PluginTaskResult`, request/response interfaces, and task parameters
|
||||||
|
|
||||||
|
2. **MCP Server** (`mcp-server/`):
|
||||||
|
- Provides MCP tools to LLMs for Penpot interaction
|
||||||
|
- Runs WebSocket server accepting connections from Penpot plugins
|
||||||
|
- Implements request/response correlation with unique task IDs
|
||||||
|
- Handles task timeouts and proper error reporting
|
||||||
|
|
||||||
|
3. **Penpot Plugin** (`penpot-plugin/`):
|
||||||
|
- Connects to MCP server via WebSocket
|
||||||
|
- Executes tasks in Penpot using the Plugin API
|
||||||
|
- Sends structured responses back to server with success/failure status
|
||||||
|
|
||||||
|
## Protocol Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
LLM → MCP Server → WebSocket → Penpot Plugin → Penpot API
|
||||||
|
↓ ↓ ↓
|
||||||
|
Tool Call Task Request Execute Action
|
||||||
|
↑ ↑ ↑
|
||||||
|
LLM ← MCP Server ← WebSocket ← Penpot Plugin ← Result
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Format
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string, // Unique UUID for correlation
|
||||||
|
task: string, // Task type (e.g., "printText")
|
||||||
|
params: object // Task-specific parameters
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string, // Matching request ID
|
||||||
|
result: {
|
||||||
|
success: boolean, // Task completion status
|
||||||
|
error?: string, // Error message if failed
|
||||||
|
data?: any // Optional result data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Testing the Connection
|
## Testing the Connection
|
||||||
|
|
||||||
|
|||||||
29
common/package-lock.json
generated
Normal file
29
common/package-lock.json
generated
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@penpot-mcp/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@penpot-mcp/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
common/package.json
Normal file
17
common/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@penpot-mcp/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
common/src/index.ts
Normal file
1
common/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './types';
|
||||||
70
common/src/types.ts
Normal file
70
common/src/types.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Result of a plugin task execution.
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional result data from the task execution.
|
||||||
|
*/
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request message sent from server to plugin.
|
||||||
|
*
|
||||||
|
* Contains a unique identifier, task name, and parameters for execution.
|
||||||
|
*/
|
||||||
|
export interface PluginTaskRequest {
|
||||||
|
/**
|
||||||
|
* Unique identifier for request/response correlation.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the task to execute.
|
||||||
|
*/
|
||||||
|
task: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameters for task execution.
|
||||||
|
*/
|
||||||
|
params: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response message sent from plugin back to server.
|
||||||
|
*
|
||||||
|
* Contains the original request ID and the execution result.
|
||||||
|
*/
|
||||||
|
export interface PluginTaskResponse {
|
||||||
|
/**
|
||||||
|
* Unique identifier matching the original request.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of the task execution.
|
||||||
|
*/
|
||||||
|
result: PluginTaskResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for the printText task.
|
||||||
|
*/
|
||||||
|
export interface PrintTextTaskParams {
|
||||||
|
/**
|
||||||
|
* The text to be displayed in Penpot.
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
19
common/tsconfig.json
Normal file
19
common/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
1
common/tsconfig.tsbuildinfo
Normal file
1
common/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
@ -111,3 +111,19 @@ For MCP clients that support HTTP transport directly, use:
|
|||||||
- Streamable HTTP for modern clients: `http://localhost:4401/mcp`
|
- Streamable HTTP for modern clients: `http://localhost:4401/mcp`
|
||||||
- SSE for legacy clients: `http://localhost:4401/sse`
|
- SSE for legacy clients: `http://localhost:4401/sse`
|
||||||
|
|
||||||
|
## Plugin Communication
|
||||||
|
|
||||||
|
The server also runs a WebSocket server on port 8080 for communication with Penpot plugins:
|
||||||
|
|
||||||
|
- **WebSocket endpoint**: `ws://localhost:8080`
|
||||||
|
- **Protocol**: Request/response with unique ID correlation
|
||||||
|
- **Timeout**: 30 seconds for task completion
|
||||||
|
- **Shared Types**: Uses `@penpot-mcp/common` package for type safety
|
||||||
|
|
||||||
|
### WebSocket Protocol Features
|
||||||
|
|
||||||
|
- **Request Correlation**: Each task has a unique UUID for matching responses
|
||||||
|
- **Structured Results**: Tasks return `{success: boolean, error?: string, data?: any}`
|
||||||
|
- **Timeout Handling**: Prevents hanging tasks with automatic cleanup
|
||||||
|
- **Type Safety**: Shared TypeScript definitions across server and plugin
|
||||||
|
|
||||||
|
|||||||
12
mcp-server/package-lock.json
generated
12
mcp-server/package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
|
"@penpot-mcp/common": "file:../common",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
@ -26,6 +27,13 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../common": {
|
||||||
|
"name": "@penpot-mcp/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@ -751,6 +759,10 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@penpot-mcp/common": {
|
||||||
|
"resolved": "../common",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
|
"@penpot-mcp/common": "file:../common",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { ToolInterface } from "./Tool";
|
|||||||
import { HelloWorldTool } from "./tools/HelloWorldTool";
|
import { HelloWorldTool } from "./tools/HelloWorldTool";
|
||||||
import { PrintTextTool } from "./tools/PrintTextTool";
|
import { PrintTextTool } from "./tools/PrintTextTool";
|
||||||
import { PluginTask } from "./PluginTask";
|
import { PluginTask } from "./PluginTask";
|
||||||
|
import { PluginTaskResponse, PluginTaskResult } from '@penpot-mcp/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Penpot MCP server implementation with HTTP and SSE Transport Support
|
* Penpot MCP server implementation with HTTP and SSE Transport Support
|
||||||
@ -17,6 +18,8 @@ export class PenpotMcpServer {
|
|||||||
private readonly tools: Map<string, ToolInterface>;
|
private readonly tools: Map<string, ToolInterface>;
|
||||||
private readonly wsServer: WebSocketServer;
|
private readonly wsServer: WebSocketServer;
|
||||||
private readonly connectedClients: Set<WebSocket> = new Set();
|
private readonly connectedClients: Set<WebSocket> = new Set();
|
||||||
|
private readonly pendingTasks: Map<string, PluginTask<any, any>> = new Map();
|
||||||
|
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
private app: any; // Express app
|
private app: any; // Express app
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
|
|
||||||
@ -228,7 +231,12 @@ export class PenpotMcpServer {
|
|||||||
|
|
||||||
ws.on("message", (data: Buffer) => {
|
ws.on("message", (data: Buffer) => {
|
||||||
console.error("Received WebSocket message:", data.toString());
|
console.error("Received WebSocket message:", data.toString());
|
||||||
// Protocol will be defined later
|
try {
|
||||||
|
const response: PluginTaskResponse = JSON.parse(data.toString());
|
||||||
|
this.handlePluginTaskResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse WebSocket message:", error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
@ -245,7 +253,52 @@ export class PenpotMcpServer {
|
|||||||
console.error("WebSocket server started on port 8080");
|
console.error("WebSocket server started on port 8080");
|
||||||
}
|
}
|
||||||
|
|
||||||
public executePluginTask(task: PluginTask) {
|
/**
|
||||||
|
* Handles responses from the plugin for completed tasks.
|
||||||
|
*
|
||||||
|
* Finds the pending task by ID and resolves or rejects its promise
|
||||||
|
* based on the execution result.
|
||||||
|
*
|
||||||
|
* @param response - The plugin task response containing ID and result
|
||||||
|
*/
|
||||||
|
private handlePluginTaskResponse(response: PluginTaskResponse): void {
|
||||||
|
const task = this.pendingTasks.get(response.id);
|
||||||
|
if (!task) {
|
||||||
|
console.error(`Received response for unknown task ID: ${response.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the timeout and remove the task from pending tasks
|
||||||
|
const timeoutHandle = this.taskTimeouts.get(response.id);
|
||||||
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
this.taskTimeouts.delete(response.id);
|
||||||
|
}
|
||||||
|
this.pendingTasks.delete(response.id);
|
||||||
|
|
||||||
|
// Resolve or reject the task's promise based on the result
|
||||||
|
if (response.result.success) {
|
||||||
|
task.resolveWithResult(response.result);
|
||||||
|
} else {
|
||||||
|
const error = new Error(response.result.error || 'Task execution failed');
|
||||||
|
task.rejectWithError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Task ${response.id} completed with success: ${response.result.success}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a plugin task by sending it to connected clients.
|
||||||
|
*
|
||||||
|
* Registers the task for result correlation and returns a promise
|
||||||
|
* that resolves when the plugin responds with the execution result.
|
||||||
|
*
|
||||||
|
* @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> {
|
||||||
// Check if there are connected clients
|
// Check if there are connected clients
|
||||||
if (this.connectedClients.size === 0) {
|
if (this.connectedClients.size === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -253,19 +306,43 @@ export class PenpotMcpServer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send task to all connected clients
|
// Register the task for result correlation
|
||||||
const taskMessage = JSON.stringify(task.toJSON());
|
this.pendingTasks.set(task.id, task);
|
||||||
|
|
||||||
|
// Send task to all connected clients using the new request format
|
||||||
|
const requestMessage = JSON.stringify(task.toRequest());
|
||||||
let sentCount = 0;
|
let sentCount = 0;
|
||||||
this.connectedClients.forEach((client) => {
|
this.connectedClients.forEach((client) => {
|
||||||
if (client.readyState === 1) {
|
if (client.readyState === 1) {
|
||||||
// WebSocket.OPEN
|
// WebSocket.OPEN
|
||||||
client.send(taskMessage);
|
client.send(requestMessage);
|
||||||
sentCount++;
|
sentCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sentCount === 0) {
|
if (sentCount === 0) {
|
||||||
throw new Error(`All connected plugin instances appear to be disconnected. No text was created.`);
|
// Clean up the pending task and timeout since we couldn't send it
|
||||||
|
this.pendingTasks.delete(task.id);
|
||||||
|
const timeoutHandle = this.taskTimeouts.get(task.id);
|
||||||
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
this.taskTimeouts.delete(task.id);
|
||||||
|
}
|
||||||
|
throw new Error(`All connected plugin instances appear to be disconnected. Task could not be sent.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up a timeout to reject the task if no response is received
|
||||||
|
const timeoutHandle = setTimeout(() => {
|
||||||
|
const pendingTask = this.pendingTasks.get(task.id);
|
||||||
|
if (pendingTask) {
|
||||||
|
this.pendingTasks.delete(task.id);
|
||||||
|
this.taskTimeouts.delete(task.id);
|
||||||
|
pendingTask.rejectWithError(new Error(`Task ${task.id} timed out after 30 seconds`));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
|
||||||
|
this.taskTimeouts.set(task.id, timeoutHandle);
|
||||||
|
console.error(`Sent task ${task.id} to ${sentCount} connected clients`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,7 +6,24 @@
|
|||||||
*
|
*
|
||||||
* @template TParams - The strongly-typed parameters for this task
|
* @template TParams - The strongly-typed parameters for this task
|
||||||
*/
|
*/
|
||||||
export abstract class PluginTask<TParams = any, TResult = any> {
|
import { PluginTaskRequest, PluginTaskResult } from '@penpot-mcp/common';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for plugin tasks that are sent over WebSocket.
|
||||||
|
*
|
||||||
|
* Each task defines a specific operation for the plugin to execute
|
||||||
|
* along with strongly-typed parameters and request/response correlation.
|
||||||
|
*
|
||||||
|
* @template TParams - The strongly-typed parameters for this task
|
||||||
|
* @template TResult - The expected result type from task execution
|
||||||
|
*/
|
||||||
|
export abstract class PluginTask<TParams = any, TResult extends PluginTaskResult = PluginTaskResult> {
|
||||||
|
/**
|
||||||
|
* Unique identifier for request/response correlation.
|
||||||
|
*/
|
||||||
|
public readonly id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the task to execute on the plugin side.
|
* The name of the task to execute on the plugin side.
|
||||||
*/
|
*/
|
||||||
@ -17,7 +34,20 @@ export abstract class PluginTask<TParams = any, TResult = any> {
|
|||||||
*/
|
*/
|
||||||
public readonly params: TParams;
|
public readonly params: TParams;
|
||||||
|
|
||||||
private result?: Promise<TResult> = undefined;
|
/**
|
||||||
|
* Promise that resolves when the task execution completes.
|
||||||
|
*/
|
||||||
|
private result?: Promise<TResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolver function for the result promise.
|
||||||
|
*/
|
||||||
|
private resolveResult?: (result: TResult) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejector function for the result promise.
|
||||||
|
*/
|
||||||
|
private rejectResult?: (error: Error) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new plugin task instance.
|
* Creates a new plugin task instance.
|
||||||
@ -26,26 +56,75 @@ export abstract class PluginTask<TParams = any, TResult = any> {
|
|||||||
* @param params - The parameters for task execution
|
* @param params - The parameters for task execution
|
||||||
*/
|
*/
|
||||||
constructor(task: string, params: TParams) {
|
constructor(task: string, params: TParams) {
|
||||||
|
this.id = randomUUID();
|
||||||
this.task = task;
|
this.task = task;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
this.setupResultPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the result promise for this task.
|
* Sets up the result promise and its resolvers.
|
||||||
*
|
*
|
||||||
* This can be used to track the outcome of the task execution.
|
* Creates a promise that can be resolved externally when
|
||||||
*
|
* the task result is received from the plugin.
|
||||||
* @param resultPromise - A promise that resolves to the task result
|
|
||||||
*/
|
*/
|
||||||
setResult(resultPromise: Promise<TResult>): void {
|
private setupResultPromise(): void {
|
||||||
this.result = resultPromise;
|
this.result = new Promise<TResult>((resolve, reject) => {
|
||||||
|
this.resolveResult = resolve;
|
||||||
|
this.rejectResult = reject;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the task to JSON for WebSocket transmission.
|
* Gets the result promise for this task.
|
||||||
|
*
|
||||||
|
* @returns Promise that resolves when the task execution completes
|
||||||
*/
|
*/
|
||||||
toJSON(): { task: string; params: TParams } {
|
getResultPromise(): Promise<TResult> {
|
||||||
|
if (!this.result) {
|
||||||
|
throw new Error('Result promise not initialized');
|
||||||
|
}
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the task with the given result.
|
||||||
|
*
|
||||||
|
* This method should be called when a task response is received
|
||||||
|
* from the plugin with matching ID.
|
||||||
|
*
|
||||||
|
* @param result - The task execution result
|
||||||
|
*/
|
||||||
|
resolveWithResult(result: TResult): void {
|
||||||
|
if (!this.resolveResult) {
|
||||||
|
throw new Error('Result promise not initialized');
|
||||||
|
}
|
||||||
|
this.resolveResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejects the task with the given error.
|
||||||
|
*
|
||||||
|
* This method should be called when task execution fails
|
||||||
|
* or times out.
|
||||||
|
*
|
||||||
|
* @param error - The error that occurred during task execution
|
||||||
|
*/
|
||||||
|
rejectWithError(error: Error): void {
|
||||||
|
if (!this.rejectResult) {
|
||||||
|
throw new Error('Result promise not initialized');
|
||||||
|
}
|
||||||
|
this.rejectResult(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the task to a request message for WebSocket transmission.
|
||||||
|
*
|
||||||
|
* @returns The request message containing ID, task name, and parameters
|
||||||
|
*/
|
||||||
|
toRequest(): PluginTaskRequest {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
task: this.task,
|
task: this.task,
|
||||||
params: this.params,
|
params: this.params,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,18 +1,5 @@
|
|||||||
import { PluginTask } from "../PluginTask";
|
import { PluginTask } from "../PluginTask";
|
||||||
|
import { PrintTextTaskParams, PluginTaskResult } from '@penpot-mcp/common';
|
||||||
/**
|
|
||||||
* Parameters for the printText task.
|
|
||||||
*/
|
|
||||||
export class PrintTextPluginTaskParams {
|
|
||||||
/**
|
|
||||||
* The text to be displayed in Penpot.
|
|
||||||
*/
|
|
||||||
public readonly text: string;
|
|
||||||
|
|
||||||
constructor(text: string) {
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task for printing/creating text in Penpot.
|
* Task for printing/creating text in Penpot.
|
||||||
@ -20,13 +7,13 @@ export class PrintTextPluginTaskParams {
|
|||||||
* This task instructs the plugin to create a text element
|
* This task instructs the plugin to create a text element
|
||||||
* at the viewport center and select it.
|
* at the viewport center and select it.
|
||||||
*/
|
*/
|
||||||
export class PrintTextPluginTask extends PluginTask<PrintTextPluginTaskParams> {
|
export class PrintTextPluginTask extends PluginTask<PrintTextTaskParams, PluginTaskResult> {
|
||||||
/**
|
/**
|
||||||
* Creates a new print text task.
|
* Creates a new print text task.
|
||||||
*
|
*
|
||||||
* @param params - The parameters containing the text to print
|
* @param params - The parameters containing the text to print
|
||||||
*/
|
*/
|
||||||
constructor(params: PrintTextPluginTaskParams) {
|
constructor(params: PrintTextTaskParams) {
|
||||||
super("printText", params);
|
super("printText", params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import type { ToolResponse } from "../ToolResponse";
|
|||||||
import { TextResponse } from "../ToolResponse";
|
import { TextResponse } from "../ToolResponse";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { PenpotMcpServer } from "../PenpotMcpServer";
|
import { PenpotMcpServer } from "../PenpotMcpServer";
|
||||||
import { PrintTextPluginTask, PrintTextPluginTaskParams } from "../tasks/PrintTextPluginTask";
|
import { PrintTextPluginTask } from "../tasks/PrintTextPluginTask";
|
||||||
|
import { PrintTextTaskParams } from '@penpot-mcp/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arguments class for the PrintText tool with validation decorators.
|
* Arguments class for the PrintText tool with validation decorators.
|
||||||
@ -43,11 +44,25 @@ export class PrintTextTool extends Tool<PrintTextArgs> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async executeCore(args: PrintTextArgs): Promise<ToolResponse> {
|
protected async executeCore(args: PrintTextArgs): Promise<ToolResponse> {
|
||||||
const taskParams = new PrintTextPluginTaskParams(args.text);
|
const taskParams: PrintTextTaskParams = { text: args.text };
|
||||||
const task = new PrintTextPluginTask(taskParams);
|
const task = new PrintTextPluginTask(taskParams);
|
||||||
this.mcpServer.executePluginTask(task);
|
|
||||||
return new TextResponse(
|
try {
|
||||||
`Successfully sent text creation task. Text "${args.text}" should now appear in Penpot.`
|
await this.mcpServer.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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
penpot-plugin/package-lock.json
generated
29
penpot-plugin/package-lock.json
generated
@ -8,15 +8,24 @@
|
|||||||
"name": "penpot-plugin-starter-template",
|
"name": "penpot-plugin-starter-template",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@penpot-mcp/common": "file:../common",
|
||||||
"@penpot/plugin-styles": "1.3.2",
|
"@penpot/plugin-styles": "1.3.2",
|
||||||
"@penpot/plugin-types": "1.3.2"
|
"@penpot/plugin-types": "1.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.0.5",
|
"vite": "^7.0.5",
|
||||||
"vite-live-preview": "^0.3.2"
|
"vite-live-preview": "^0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../common": {
|
||||||
|
"name": "@penpot-mcp/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@commander-js/extra-typings": {
|
"node_modules/@commander-js/extra-typings": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-12.1.0.tgz",
|
||||||
@ -442,6 +451,10 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@penpot-mcp/common": {
|
||||||
|
"resolved": "../common",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@penpot/plugin-styles": {
|
"node_modules/@penpot/plugin-styles": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@penpot/plugin-styles/-/plugin-styles-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@penpot/plugin-styles/-/plugin-styles-1.3.2.tgz",
|
||||||
@ -970,6 +983,22 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.45.1",
|
"version": "4.45.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@penpot/plugin-styles": "1.3.2",
|
"@penpot/plugin-styles": "1.3.2",
|
||||||
"@penpot/plugin-types": "1.3.2"
|
"@penpot/plugin-types": "1.3.2",
|
||||||
|
"@penpot-mcp/common": "file:../common"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
|
|||||||
@ -18,6 +18,20 @@ function updateConnectionStatus(status: string, isConnectedState: boolean): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a task response back to the MCP server via WebSocket.
|
||||||
|
*
|
||||||
|
* @param response - The response containing task ID and result
|
||||||
|
*/
|
||||||
|
function sendTaskResponse(response: any): void {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(response));
|
||||||
|
console.log("Sent response to MCP server:", response);
|
||||||
|
} else {
|
||||||
|
console.error("WebSocket not connected, cannot send response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establishes a WebSocket connection to the MCP server.
|
* Establishes a WebSocket connection to the MCP server.
|
||||||
*/
|
*/
|
||||||
@ -39,9 +53,9 @@ function connectToMcpServer(): void {
|
|||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
console.log("Received from MCP server:", event.data);
|
console.log("Received from MCP server:", event.data);
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const request = JSON.parse(event.data);
|
||||||
// Forward the task to the plugin for execution
|
// Forward the task request to the plugin for execution
|
||||||
parent.postMessage(message, "*");
|
parent.postMessage(request, "*");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse WebSocket message:", error);
|
console.error("Failed to parse WebSocket message:", error);
|
||||||
}
|
}
|
||||||
@ -77,5 +91,8 @@ document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click"
|
|||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.data.source === "penpot") {
|
if (event.data.source === "penpot") {
|
||||||
document.body.dataset.theme = event.data.theme;
|
document.body.dataset.theme = event.data.theme;
|
||||||
|
} else if (event.data.type === "task-response") {
|
||||||
|
// Forward task response back to MCP server
|
||||||
|
sendTaskResponse(event.data.response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`);
|
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`);
|
||||||
|
|
||||||
// Handle both legacy string messages and new task-based messages
|
// Handle both legacy string messages and new request-based messages
|
||||||
penpot.ui.onMessage<string | { task: string; params: any }>((message) => {
|
penpot.ui.onMessage<string | { id: string; task: string; params: any }>((message) => {
|
||||||
// Legacy string-based message handling
|
// Legacy string-based message handling
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
if (message === "create-text") {
|
if (message === "create-text") {
|
||||||
@ -17,38 +17,47 @@ penpot.ui.onMessage<string | { task: string; params: any }>((message) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New task-based message handling
|
// New request-based message handling
|
||||||
if (typeof message === "object" && message.task) {
|
if (typeof message === "object" && message.task && message.id) {
|
||||||
handlePluginTask(message);
|
handlePluginTaskRequest(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles plugin tasks received from the MCP server via WebSocket.
|
* Handles plugin task requests received from the MCP server via WebSocket.
|
||||||
*
|
*
|
||||||
* @param taskMessage - The task message containing task type and parameters
|
* @param request - The task request containing ID, task type and parameters
|
||||||
*/
|
*/
|
||||||
function handlePluginTask(taskMessage: { task: string; params: any }): void {
|
function handlePluginTaskRequest(request: { id: string; task: string; params: any }): void {
|
||||||
console.log("Executing plugin task:", taskMessage.task, taskMessage.params);
|
console.log("Executing plugin task:", request.task, request.params);
|
||||||
|
|
||||||
switch (taskMessage.task) {
|
switch (request.task) {
|
||||||
case "printText":
|
case "printText":
|
||||||
handlePrintTextTask(taskMessage.params);
|
handlePrintTextTask(request.id, request.params);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn("Unknown plugin task:", taskMessage.task);
|
console.warn("Unknown plugin task:", request.task);
|
||||||
|
sendTaskResponse(request.id, {
|
||||||
|
success: false,
|
||||||
|
error: `Unknown task type: ${request.task}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the printText task by creating text in Penpot.
|
* Handles the printText task by creating text in Penpot.
|
||||||
*
|
*
|
||||||
|
* @param taskId - The unique ID of the task request
|
||||||
* @param params - The parameters containing the text to create
|
* @param params - The parameters containing the text to create
|
||||||
*/
|
*/
|
||||||
function handlePrintTextTask(params: { text: string }): void {
|
function handlePrintTextTask(taskId: string, params: { text: string }): void {
|
||||||
if (!params.text) {
|
if (!params.text) {
|
||||||
console.error("printText task requires 'text' parameter");
|
console.error("printText task requires 'text' parameter");
|
||||||
|
sendTaskResponse(taskId, {
|
||||||
|
success: false,
|
||||||
|
error: "printText task requires 'text' parameter",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,14 +73,47 @@ function handlePrintTextTask(params: { text: string }): void {
|
|||||||
penpot.selection = [text];
|
penpot.selection = [text];
|
||||||
|
|
||||||
console.log("Successfully created text:", params.text);
|
console.log("Successfully created text:", params.text);
|
||||||
|
sendTaskResponse(taskId, {
|
||||||
|
success: true,
|
||||||
|
data: { textId: text.id },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to create text element");
|
console.error("Failed to create text element");
|
||||||
|
sendTaskResponse(taskId, {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to create text element",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating text:", 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}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
const response = {
|
||||||
|
type: "task-response",
|
||||||
|
response: {
|
||||||
|
id: taskId,
|
||||||
|
result: result,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send to main.ts which will forward to MCP server via WebSocket
|
||||||
|
penpot.ui.sendMessage(response);
|
||||||
|
console.log("Sent task response:", response);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the theme in the iframe
|
// Update the theme in the iframe
|
||||||
penpot.on("themechange", (theme) => {
|
penpot.on("themechange", (theme) => {
|
||||||
penpot.ui.sendMessage({
|
penpot.ui.sendMessage({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user