Talk to Figma MCP

by sonnylazuardi
Verified
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Cursor MCP Plugin</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; color: #e0e0e0; background-color: #1e1e1e; } .container { display: flex; flex-direction: column; height: 100%; } h1 { font-size: 16px; font-weight: 600; margin-bottom: 10px; color: #ffffff; } h2 { font-size: 14px; font-weight: 600; margin-top: 20px; margin-bottom: 8px; color: #ffffff; } button { background-color: #18a0fb; border: none; color: white; padding: 8px 12px; border-radius: 6px; margin-top: 8px; margin-bottom: 8px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } button:hover { background-color: #0d8ee0; } button.secondary { background-color: #3d3d3d; color: #e0e0e0; } button.secondary:hover { background-color: #4d4d4d; } button:disabled { background-color: #333333; color: #666666; cursor: not-allowed; } input { border: 1px solid #444444; border-radius: 4px; padding: 8px; margin-bottom: 12px; font-size: 14px; width: 100%; box-sizing: border-box; background-color: #2d2d2d; color: #e0e0e0; } label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 500; color: #cccccc; } .status { margin-top: 16px; padding: 12px; border-radius: 6px; font-size: 14px; } .status.connected { background-color: #1a472a; color: #4ade80; } .status.disconnected { background-color: #471a1a; color: #ff9999; } .status.info { background-color: #1a3147; color: #66b3ff; } .section { margin-bottom: 24px; } .hidden { display: none; } .logo { width: 50px; height: 50px; } .header { display: flex; align-items: center; margin-bottom: 16px; } .header-text { margin-left: 12px; } .header-text h1 { margin: 0; font-size: 16px; } .header-text p { margin: 4px 0 0 0; font-size: 12px; color: #999999; } .tabs { display: flex; border-bottom: 1px solid #444444; margin-bottom: 16px; } .tab { padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500; color: #999999; } .tab.active { border-bottom: 2px solid #18a0fb; color: #18a0fb; } .tab-content { display: none; } .tab-content.active { display: block; } .link { color: #18a0fb; text-decoration: none; cursor: pointer; } .link:hover { text-decoration: underline; } .header-logo { padding: 16px; border-radius: 16px; background-color: #333; } .header-logo-image { width: 24px; height: 24px; object-fit: contain; } </style> </head> <body> <div class="container"> <div class="header"> <div class="header-logo"> <img class="header-logo-image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAEJwAABCcASbNOjQAAAB1UExURUdwTP////////////////39/f////////////////////////////7+/v////////////39/f////////////////////////////////////////////////////39/fn5+ejo6P///+rq6uXl5f////Ly8gf4a04AAAAkdFJOUwAOdkZCfz04zIgbT0pkIagnm7C9b6C2LWqSxBMyB11W2Ovsy3D12ZYAAALtSURBVEjHndcJt6ogEADgXNAUcWlxSQVN3/3/P/EBAgJpWdM9p5ue78xANE2n05vIUduffgvn1oA0bX+hvRc1DYjTPHe+tiGIoqhx4zTNq/y72lMURQtmqasuPc4dAmgwfWuZrqquiw8uNnC5BRJT3YXhIZ7Xris0oLjlmOrArz7VHpOb6wpNee0ITVMHvvd25/qgvtFwla8dpxV7xnTi7dbed7iuTY16lZoV7iXQb3cqRgjVgoviKTZSUw2719pbD2OEVu5yjnqeOpZ75lMMobVzfUcwC6lrofGJpdb3jGtj6TkkNKRWtXMsU+ciNdfQUwe+zZ7/vo1CYYgv39G/kShMS6mHL+g8F96K2Uqi52E6j3DFnsc4uR/hMwugYd9bOLoeSTvPE1yx4/sLh9B9fKbziHVM3z/G+dKb5wdKdysxsNCc4+2l/yk7EnrOVhwGBt9auqJ0t9gR13C4cl77bdil88SPuK9jxrXksHjab48Mwo+4ha3aSbZJ52JpC4GFbY7OdsVst4Lls/mKZe1y6fXTonS3RFsIN7C5dAJsO+WiI21jbd8xesFEtoUdLLjH+qGNJ9WRuj3MOOQNycaV6khvsLc0MxsD2Uq7bhcHuBZh4rFdujjT1c6GkaXtszCx3sW3MRRfNjwiI7EjGjGfFjZwUgM9CuNggqRVXz+vOGDTBOCP5UnHE73ghjK1jYNlEIma9UnHBb/qdkvq1MSQjk4yCvGk4UneQylLbWAIio3I1t26q4sNTuM01tqQe9+My5pYv9wk8Ypv92w7JpXYulGoD8aJ3C/bUUp8tW5EuTa2oXI7ZGLzahZYE0l03QqZWI8Lfh1lw+zxEoNIrF8Dm/NQT8rzgz+WP/oQmL6Ud4pud/4DZzMWPKjXZfJufOyiVzzKV4/609yelDaWiZsDc6+DSwOLxNqxeD/6Ah3zf674+Kyf3xUeDi3WDFIKzCpOv/5phB4MD+cs/OWXVdych/GBf/xJd4pL9+1i/wOElMO5v/co4wAAAABJRU5ErkJggg==" /> </div> <div class="header-text"> <h1>Cursor Talk To Figma Plugin</h1> <p>Connect Figma to Cursor AI using MCP</p> </div> </div> <div class="tabs"> <div id="tab-connection" class="tab active">Connection</div> <div id="tab-about" class="tab">About</div> </div> <div id="content-connection" class="tab-content active"> <div class="section"> <label for="port">WebSocket Server Port</label> <div style="display: flex; gap: 8px"> <input type="number" id="port" placeholder="3055" value="3055" min="1024" max="65535" /> <button id="btn-connect" class="primary">Connect</button> </div> </div> <div id="connection-status" class="status disconnected"> Not connected to Cursor MCP server </div> <div class="section"> <button id="btn-disconnect" class="secondary" disabled> Disconnect </button> </div> </div> <div id="content-about" class="tab-content"> <div class="section"> <h2>About Cursor Talk To Figma Plugin</h2> <p> This plugin allows Cursor AI to communicate with Figma, enabling AI-assisted design operations. created by <a class="link" onclick="window.open(`https://github.com/sonnylazuardi`, '_blank')" >Sonny</a > </p> <p>Version: 1.0.0</p> <h2>How to Use</h2> <ol> <li>Make sure the MCP server is running in Cursor</li> <li>Connect to the server using the port number (default: 3055)</li> <li>Once connected, you can interact with Figma through Cursor</li> </ol> </div> </div> </div> <script> // WebSocket connection state const state = { connected: false, socket: null, serverPort: 3055, pendingRequests: new Map(), channel: null, }; // UI Elements const portInput = document.getElementById("port"); const connectButton = document.getElementById("btn-connect"); const disconnectButton = document.getElementById("btn-disconnect"); const connectionStatus = document.getElementById("connection-status"); // Tabs const tabs = document.querySelectorAll(".tab"); const tabContents = document.querySelectorAll(".tab-content"); // Initialize UI function updateConnectionStatus(isConnected, message) { state.connected = isConnected; connectionStatus.innerHTML = message || (isConnected ? "Connected to Cursor MCP server" : "Not connected to Cursor MCP server"); connectionStatus.className = `status ${ isConnected ? "connected" : "disconnected" }`; connectButton.disabled = isConnected; disconnectButton.disabled = !isConnected; portInput.disabled = isConnected; } // Connect to WebSocket server async function connectToServer(port) { try { if (state.connected && state.socket) { updateConnectionStatus(true, "Already connected to server"); return; } state.serverPort = port; state.socket = new WebSocket(`ws://localhost:${port}`); state.socket.onopen = () => { // Generate random channel name const channelName = generateChannelName(); console.log("Joining channel:", channelName); state.channel = channelName; // Join the channel using the same format as App.tsx state.socket.send( JSON.stringify({ type: "join", channel: channelName.trim(), }) ); }; state.socket.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log("Received message:", data); if (data.type === "system") { // Successfully joined channel if (data.message && data.message.result) { state.connected = true; const channelName = data.channel; updateConnectionStatus( true, `Connected to server on port ${port} in channel: <strong>${channelName}</strong>` ); // Notify the plugin code parent.postMessage( { pluginMessage: { type: "notify", message: `Connected to Cursor MCP server on port ${port} in channel: ${channelName}`, }, }, "*" ); } } else if (data.type === "error") { console.error("Error:", data.message); updateConnectionStatus(false, `Error: ${data.message}`); state.socket.close(); } handleSocketMessage(data); } catch (error) { console.error("Error parsing message:", error); } }; state.socket.onclose = () => { state.connected = false; state.socket = null; updateConnectionStatus(false, "Disconnected from server"); }; state.socket.onerror = (error) => { console.error("WebSocket error:", error); updateConnectionStatus(false, "Connection error"); state.connected = false; state.socket = null; }; } catch (error) { console.error("Connection error:", error); updateConnectionStatus( false, `Connection error: ${error.message || "Unknown error"}` ); } } // Disconnect from websocket server function disconnectFromServer() { if (state.socket) { state.socket.close(); state.socket = null; state.connected = false; updateConnectionStatus(false, "Disconnected from server"); } } // Handle messages from the WebSocket async function handleSocketMessage(payload) { const data = payload.message; console.log("handleSocketMessage", data); // If it's a response to a previous request if (data.id && state.pendingRequests.has(data.id)) { const { resolve, reject } = state.pendingRequests.get(data.id); state.pendingRequests.delete(data.id); if (data.error) { reject(new Error(data.error)); } else { resolve(data.result); } return; } // If it's a new command if (data.command) { try { // Send the command to the plugin code parent.postMessage( { pluginMessage: { type: "execute-command", id: data.id, command: data.command, params: data.params, }, }, "*" ); } catch (error) { // Send error back to WebSocket sendErrorResponse( data.id, error.message || "Error executing command" ); } } } // Send a command to the WebSocket server async function sendCommand(command, params) { return new Promise((resolve, reject) => { if (!state.connected || !state.socket) { reject(new Error("Not connected to server")); return; } const id = generateId(); state.pendingRequests.set(id, { resolve, reject }); state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, command, params, }, }) ); // Set timeout to reject the promise after 30 seconds setTimeout(() => { if (state.pendingRequests.has(id)) { state.pendingRequests.delete(id); reject(new Error("Request timed out")); } }, 30000); }); } // Send success response back to WebSocket function sendSuccessResponse(id, result) { if (!state.connected || !state.socket) { console.error("Cannot send response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, result, }, }) ); } // Send error response back to WebSocket function sendErrorResponse(id, errorMessage) { if (!state.connected || !state.socket) { console.error("Cannot send error response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, error: errorMessage, }) ); } // Helper to generate unique IDs function generateId() { return ( Date.now().toString(36) + Math.random().toString(36).substr(2, 5) ); } // Add this function after the generateId() function function generateChannelName() { const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < 8; i++) { result += characters.charAt( Math.floor(Math.random() * characters.length) ); } return result; } // Tab switching tabs.forEach((tab) => { tab.addEventListener("click", () => { tabs.forEach((t) => t.classList.remove("active")); tabContents.forEach((c) => c.classList.remove("active")); tab.classList.add("active"); const contentId = "content-" + tab.id.split("-")[1]; document.getElementById(contentId).classList.add("active"); }); }); // Connect to server connectButton.addEventListener("click", () => { const port = parseInt(portInput.value, 10) || 3055; updateConnectionStatus(false, "Connecting..."); connectionStatus.className = "status info"; connectToServer(port); }); // Disconnect from server disconnectButton.addEventListener("click", () => { updateConnectionStatus(false, "Disconnecting..."); connectionStatus.className = "status info"; disconnectFromServer(); }); // Listen for messages from the plugin code window.onmessage = (event) => { const message = event.data.pluginMessage; if (!message) return; switch (message.type) { case "connection-status": updateConnectionStatus(message.connected, message.message); break; case "auto-connect": connectButton.click(); break; case "auto-disconnect": disconnectButton.click(); break; case "command-result": // Forward the result from plugin code back to WebSocket sendSuccessResponse(message.id, message.result); break; case "command-error": // Forward the error from plugin code back to WebSocket sendErrorResponse(message.id, message.error); break; } }; </script> </body> </html>