Talk to Figma MCP
by sonnylazuardi
Verified
- cursor-talk-to-figma-mcp
- src
- cursor_mcp_plugin
<!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>