diff --git a/README.md b/README.md new file mode 100644 index 0000000..c26f0b9 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# WebSocket Connection Setup + +This document explains how to test the basic WebSocket connection between the MCP server and the Penpot plugin. + +## Architecture Overview + +The system consists of two main components: + +1. **MCP Server** (`mcp-server/`): + - Runs as a traditional MCP server (stdio transport) + - Also runs a WebSocket server on port 8080 + - Basic WebSocket connection handling (protocol to be defined later) + +2. **Penpot Plugin** (`penpot-plugin/`): + - Contains a "Connect to MCP server" button in the UI + - Establishes WebSocket connection to `ws://localhost:8080` + - Basic connection status feedback + +## Testing the Connection + +### Step 1: Start the MCP Server +```bash +cd mcp-server +npm run build +npm start +``` + +The server will output: +``` +Penpot MCP Server started successfully +WebSocket server is listening on ws://localhost:8080 +``` + +### Step 2: Build and Run the Plugin +```bash +cd penpot-plugin +npm run build +npm run dev +``` + +### Step 3: Load Plugin in Penpot +1. Open Penpot in your browser +2. Navigate to a design file +3. Go to Plugins menu +4. Load the plugin using the development URL (typically `http://localhost:4400/manifest.json`) + +### Step 4: Test the Connection +1. In the plugin UI, click "Connect to MCP server" +2. The connection status should change from "Not connected" to "Connected to MCP server" +3. Check the browser's developer console for WebSocket connection logs +4. Check the MCP server terminal for WebSocket connection messages + diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index 31b6923..84e44b5 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -12,10 +12,12 @@ "@modelcontextprotocol/sdk": "^0.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", "prettier": "^3.0.0", "ts-node": "^10.9.0", "typescript": "^5.0.0" @@ -107,6 +109,16 @@ "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -387,6 +399,27 @@ "node": ">= 0.10" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index 5129517..9abdbca 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -22,10 +22,12 @@ "@modelcontextprotocol/sdk": "^0.4.0", "class-validator": "^0.14.0", "class-transformer": "^0.5.1", - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", "prettier": "^3.0.0", "ts-node": "^10.9.0", "typescript": "^5.0.0" diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 21d9442..715f8a5 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { WebSocketServer, WebSocket } from "ws"; import { Tool } from "./interfaces/Tool.js"; import { HelloWorldTool } from "./tools/HelloWorldTool.js"; @@ -16,6 +17,8 @@ import { HelloWorldTool } from "./tools/HelloWorldTool.js"; class PenpotMcpServer { private readonly server: Server; private readonly tools: Map; + private readonly wsServer: WebSocketServer; + private readonly connectedClients: Set = new Set(); /** * Creates a new Penpot MCP server instance. @@ -30,7 +33,9 @@ class PenpotMcpServer { }); this.tools = new Map(); + this.wsServer = new WebSocketServer({ port: 8080 }); this.setupHandlers(); + this.setupWebSocketHandlers(); this.registerTools(); } @@ -80,6 +85,37 @@ class PenpotMcpServer { }); } + /** + * Sets up WebSocket connection handlers for plugin communication. + * + * Manages client connections and provides bidirectional communication + * channel between the MCP server and Penpot plugin instances. + */ + private setupWebSocketHandlers(): void { + this.wsServer.on("connection", (ws: WebSocket) => { + console.error("New WebSocket connection established"); + this.connectedClients.add(ws); + + ws.on("message", (data: Buffer) => { + console.error("Received WebSocket message:", data.toString()); + // Protocol will be defined later + }); + + ws.on("close", () => { + console.error("WebSocket connection closed"); + this.connectedClients.delete(ws); + }); + + ws.on("error", (error) => { + console.error("WebSocket connection error:", error); + this.connectedClients.delete(ws); + }); + }); + + console.error("WebSocket server started on port 8080"); + } + + /** * Starts the MCP server using stdio transport. * @@ -90,6 +126,7 @@ class PenpotMcpServer { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Penpot MCP Server started successfully"); + console.error("WebSocket server is listening on ws://localhost:8080"); } } diff --git a/penpot-plugin/index.html b/penpot-plugin/index.html index b2f0a74..7b75cfd 100644 --- a/penpot-plugin/index.html +++ b/penpot-plugin/index.html @@ -20,6 +20,14 @@ Create text + + +
+ Not connected +
+ diff --git a/penpot-plugin/src/main.ts b/penpot-plugin/src/main.ts index b0aa5df..3983206 100644 --- a/penpot-plugin/src/main.ts +++ b/penpot-plugin/src/main.ts @@ -4,11 +4,70 @@ import "./style.css"; const searchParams = new URLSearchParams(window.location.search); document.body.dataset.theme = searchParams.get("theme") ?? "light"; +// WebSocket connection management +let ws: WebSocket | null = null; +const statusElement = document.getElementById("connection-status"); + +/** + * Updates the connection status display element. + */ +function updateConnectionStatus(status: string, isConnectedState: boolean): void { + if (statusElement) { + statusElement.textContent = status; + statusElement.style.color = isConnectedState ? "#4CAF50" : "#666"; + } +} + +/** + * Establishes a WebSocket connection to the MCP server. + */ +function connectToMcpServer(): void { + if (ws?.readyState === WebSocket.OPEN) { + updateConnectionStatus("Already connected", true); + return; + } + + try { + ws = new WebSocket("ws://localhost:8080"); + updateConnectionStatus("Connecting...", false); + + ws.onopen = () => { + console.log("Connected to MCP server"); + updateConnectionStatus("Connected to MCP server", true); + }; + + ws.onmessage = (event) => { + console.log("Received from MCP server:", event.data); + // Protocol will be defined later + }; + + ws.onclose = () => { + console.log("Disconnected from MCP server"); + updateConnectionStatus("Disconnected", false); + ws = null; + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + updateConnectionStatus("Connection error", false); + }; + } catch (error) { + console.error("Failed to connect to MCP server:", error); + updateConnectionStatus("Connection failed", false); + } +} + + +// Event handlers document.querySelector("[data-handler='create-text']")?.addEventListener("click", () => { // send message to plugin.ts parent.postMessage("create-text", "*"); }); +document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => { + connectToMcpServer(); +}); + // Listen plugin.ts messages window.addEventListener("message", (event) => { if (event.data.source === "penpot") {