Establish websocket connection between plugin and MCP server

This commit is contained in:
Dominik Jain 2025-09-10 16:33:53 +02:00
parent 740750fbd8
commit 7faca70aa7
6 changed files with 193 additions and 2 deletions

52
README.md Normal file
View File

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

View File

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

View File

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

View File

@ -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<string, Tool>;
private readonly wsServer: WebSocketServer;
private readonly connectedClients: Set<WebSocket> = new Set();
/**
* Creates a new Penpot MCP server instance.
@ -30,7 +33,9 @@ class PenpotMcpServer {
});
this.tools = new Map<string, Tool>();
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");
}
}

View File

@ -20,6 +20,14 @@
Create text
</button>
<button type="button" data-appearance="secondary" data-handler="connect-mcp">
Connect to MCP server
</button>
<div id="connection-status" style="margin-top: 10px; font-size: 12px; color: #666;">
Not connected
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -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") {