# Directory Structure
```
├── .gitignore
├── bun.lock
├── package-lock.json
├── package.json
├── readme.md
├── scripts
│ └── setup.sh
└── src
├── cursor_mcp_plugin
│ ├── code.js
│ ├── manifest.json
│ ├── setcharacters.js
│ └── ui.html
├── socket.ts
└── talk_to_figma_mcp
├── bun.lock
├── package.json
├── server.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
.cursor/
```
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
```markdown
# Cursor Talk to Figma MCP
This project implements a Model Context Protocol (MCP) integration between Cursor AI and Figma, allowing Cursor to communicate with Figma for reading designs and modifying them programmatically.
https://github.com/user-attachments/assets/129a14d2-ed73-470f-9a4c-2240b2a4885c
## Project Structure
- `src/talk_to_figma_mcp/` - TypeScript MCP server for Figma integration
- `src/cursor_mcp_plugin/` - Figma plugin for communicating with Cursor
- `src/socket.ts` - WebSocket server that facilitates communication between the MCP server and Figma plugin
## Get Started
1. Install Bun if you haven't already:
```bash
curl -fsSL https://bun.sh/install | bash
```
2. Run setup, this will also install MCP in your Cursor's active project
```bash
bun setup
```
3. Start the Websocket server
```bash
bun start
```
4. Install [Figma Plugin](#figma-plugin)
## Manual Setup and Installation
### MCP Server: Integration with Cursor
Add the server to your Cursor MCP configuration in `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"TalkToFigma": {
"command": "bun",
"args": [
"/path/to/cursor-talk-to-figma-mcp/src/talk_to_figma_mcp/server.ts"
]
}
}
}
```
### WebSocket Server
Start the WebSocket server:
```bash
bun run src/socket.ts
```
### Figma Plugin
1. In Figma, go to Plugins > Development > New Plugin
2. Choose "Link existing plugin"
3. Select the `src/cursor_mcp_plugin/manifest.json` file
4. The plugin should now be available in your Figma development plugins
## Usage
1. Start the WebSocket server
2. Install the MCP server in Cursor
3. Open Figma and run the Cursor MCP Plugin
4. Connect the plugin to the WebSocket server by joining a channel using `join_channel`
5. Use Cursor to communicate with Figma using the MCP tools
## MCP Tools
The MCP server provides the following tools for interacting with Figma:
### Document & Selection
- `get_document_info` - Get information about the current Figma document
- `get_selection` - Get information about the current selection
- `get_node_info` - Get detailed information about a specific node
### Creating Elements
- `create_rectangle` - Create a new rectangle with position, size, and optional name
- `create_frame` - Create a new frame with position, size, and optional name
- `create_text` - Create a new text node with customizable font properties
### Styling
- `set_fill_color` - Set the fill color of a node (RGBA)
- `set_stroke_color` - Set the stroke color and weight of a node
- `set_corner_radius` - Set the corner radius of a node with optional per-corner control
### Layout & Organization
- `move_node` - Move a node to a new position
- `resize_node` - Resize a node with new dimensions
- `delete_node` - Delete a node
### Components & Styles
- `get_styles` - Get information about local styles
- `get_local_components` - Get information about local components
- `get_team_components` - Get information about team components
- `create_component_instance` - Create an instance of a component
### Export & Advanced
- `export_node_as_image` - Export a node as an image (PNG, JPG, SVG, or PDF)
- `execute_figma_code` - Execute arbitrary JavaScript code in Figma (use with caution)
### Connection Management
- `join_channel` - Join a specific channel to communicate with Figma
## Development
### Building the Figma Plugin
1. Navigate to the Figma plugin directory:
```
cd src/cursor_mcp_plugin
```
2. Edit code.js and ui.html
## Best Practices
When working with the Figma MCP:
1. Always join a channel before sending commands
2. Get document overview using `get_document_info` first
3. Check current selection with `get_selection` before modifications
4. Use appropriate creation tools based on needs:
- `create_frame` for containers
- `create_rectangle` for basic shapes
- `create_text` for text elements
5. Verify changes using `get_node_info`
6. Use component instances when possible for consistency
7. Handle errors appropriately as all commands can throw exceptions
## License
MIT
```
--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Create .cursor directory if it doesn't exist
mkdir -p .cursor
# Get current directory path
CURRENT_DIR=$(pwd)
bun install
# Create mcp.json with the current directory path
echo "{
\"mcpServers\": {
\"TalkToFigma\": {
\"command\": \"bun\",
\"args\": [
\"${CURRENT_DIR}/src/talk_to_figma_mcp/server.ts\"
]
}
}
}" > .cursor/mcp.json
```
--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/manifest.json:
--------------------------------------------------------------------------------
```json
{
"name": "Cursor MCP Plugin",
"id": "cursor-mcp-plugin",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": [
"figma",
"figjam"
],
"permissions": [],
"networkAccess": {
"allowedDomains": [
"https://google.com"
],
"devAllowedDomains": [
"http://localhost:3056",
"ws://localhost:3056"
]
},
"documentAccess": "dynamic-page",
"enableProposedApi": true,
"enablePrivatePluginApi": true
}
```
--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"lib": ["ESNext", "DOM"]
},
"include": ["./**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "cursor-talk-to-figma-mcp",
"version": "1.0.0",
"description": "Cursor Talk to Figma MCP",
"main": "src/socket.ts",
"scripts": {
"setup": "./scripts/setup.sh",
"start": "bun run src/socket.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "latest",
"@types/node": "^22.13.10",
"bun": "^1.2.5",
"uuid": "latest",
"ws": "latest",
"zod": "latest"
},
"devDependencies": {
"@types/bun": "^1.2.5",
"@types/ws": "^8.18.0"
}
}
```
--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "talk-to-figma-mcp",
"version": "1.0.0",
"description": "MCP server for Figma integration",
"main": "server.ts",
"type": "module",
"scripts": {
"start": "node --loader ts-node/esm server.ts",
"build": "tsc",
"dev": "node --loader ts-node/esm --watch server.ts"
},
"keywords": [
"figma",
"mcp",
"cursor",
"ai"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.0",
"uuid": "^9.0.1",
"ws": "^8.16.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/socket.ts:
--------------------------------------------------------------------------------
```typescript
import { Server, ServerWebSocket } from "bun";
// Store clients by channel
const channels = new Map<string, Set<ServerWebSocket<any>>>();
function handleConnection(ws: ServerWebSocket<any>) {
// Don't add to clients immediately - wait for channel join
console.log("New client connected");
// Send welcome message to the new client
ws.send(JSON.stringify({
type: "system",
message: "Please join a channel to start chatting",
}));
ws.close = () => {
console.log("Client disconnected");
// Remove client from their channel
channels.forEach((clients, channelName) => {
if (clients.has(ws)) {
clients.delete(ws);
// Notify other clients in same channel
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: "system",
message: "A user has left the channel",
channel: channelName
}));
}
});
}
});
};
}
const server = Bun.serve({
port: 3056,
fetch(req: Request, server: Server) {
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
// Handle WebSocket upgrade
const success = server.upgrade(req, {
headers: {
"Access-Control-Allow-Origin": "*",
},
});
if (success) {
return; // Upgraded to WebSocket
}
// Return response for non-WebSocket requests
return new Response("WebSocket server running", {
headers: {
"Access-Control-Allow-Origin": "*",
},
});
},
websocket: {
open: handleConnection,
message(ws: ServerWebSocket<any>, message: string | Buffer) {
try {
console.log("Received message from client:", message);
const data = JSON.parse(message as string);
if (data.type === "join") {
const channelName = data.channel;
if (!channelName || typeof channelName !== "string") {
ws.send(JSON.stringify({
type: "error",
message: "Channel name is required"
}));
return;
}
// Create channel if it doesn't exist
if (!channels.has(channelName)) {
channels.set(channelName, new Set());
}
// Add client to channel
const channelClients = channels.get(channelName)!;
channelClients.add(ws);
// Notify client they joined successfully
ws.send(JSON.stringify({
type: "system",
message: `Joined channel: ${channelName}`,
channel: channelName
}));
console.log("Sending message to client:", data.id);
ws.send(JSON.stringify({
type: "system",
message: {
id: data.id,
result: "Connected to channel: " + channelName,
},
channel: channelName
}));
// Notify other clients in channel
channelClients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: "system",
message: "A new user has joined the channel",
channel: channelName
}));
}
});
return;
}
// Handle regular messages
if (data.type === "message") {
const channelName = data.channel;
if (!channelName || typeof channelName !== "string") {
ws.send(JSON.stringify({
type: "error",
message: "Channel name is required"
}));
return;
}
const channelClients = channels.get(channelName);
if (!channelClients || !channelClients.has(ws)) {
ws.send(JSON.stringify({
type: "error",
message: "You must join the channel first"
}));
return;
}
// Broadcast to all clients in the channel
channelClients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
console.log("Broadcasting message to client:", data.message);
client.send(JSON.stringify({
type: "broadcast",
message: data.message,
sender: client === ws ? "You" : "User",
channel: channelName
}));
}
});
}
} catch (err) {
console.error("Error handling message:", err);
}
},
close(ws: ServerWebSocket<any>) {
// Remove client from their channel
channels.forEach((clients) => {
clients.delete(ws);
});
}
}
});
console.log(`WebSocket server running on port ${server.port}`);
```
--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/setcharacters.js:
--------------------------------------------------------------------------------
```javascript
function uniqBy(arr, predicate) {
const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
return [
...arr
.reduce((map, item) => {
const key = item === null || item === undefined ? item : cb(item);
map.has(key) || map.set(key, item);
return map;
}, new Map())
.values(),
];
}
export const setCharacters = async (node, characters, options) => {
const fallbackFont = options?.fallbackFont || {
family: "Roboto",
style: "Regular",
};
try {
if (node.fontName === figma.mixed) {
if (options?.smartStrategy === "prevail") {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const charFont = node.getRangeFontName(i - 1, i);
const key = `${charFont.family}::${charFont.style}`;
fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
}
const prevailedTreeItem = Object.entries(fontHashTree).sort(
(a, b) => b[1] - a[1]
)[0];
const [family, style] = prevailedTreeItem[0].split("::");
const prevailedFont = {
family,
style,
};
await figma.loadFontAsync(prevailedFont);
node.fontName = prevailedFont;
} else if (options?.smartStrategy === "strict") {
return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
} else if (options?.smartStrategy === "experimental") {
return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
} else {
const firstCharFont = node.getRangeFontName(0, 1);
await figma.loadFontAsync(firstCharFont);
node.fontName = firstCharFont;
}
} else {
await figma.loadFontAsync({
family: node.fontName.family,
style: node.fontName.style,
});
}
} catch (err) {
console.warn(
`Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
err
);
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
}
try {
node.characters = characters;
return true;
} catch (err) {
console.warn(`Failed to set characters. Skipped.`, err);
return false;
}
};
const setCharactersWithStrictMatchFont = async (
node,
characters,
fallbackFont
) => {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const startIdx = i - 1;
const startCharFont = node.getRangeFontName(startIdx, i);
const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
while (i < node.characters.length) {
i++;
const charFont = node.getRangeFontName(i - 1, i);
if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
break;
}
}
fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
}
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
node.characters = characters;
console.log(fontHashTree);
await Promise.all(
Object.keys(fontHashTree).map(async (range) => {
console.log(range, fontHashTree[range]);
const [start, end] = range.split("_");
const [family, style] = fontHashTree[range].split("::");
const matchedFont = {
family,
style,
};
await figma.loadFontAsync(matchedFont);
return node.setRangeFontName(Number(start), Number(end), matchedFont);
})
);
return true;
};
const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
const indices = [];
let temp = startIdx;
for (let i = startIdx; i < endIdx; i++) {
if (
str[i] === delimiter &&
i + startIdx !== endIdx &&
temp !== i + startIdx
) {
indices.push([temp, i + startIdx]);
temp = i + startIdx + 1;
}
}
temp !== endIdx && indices.push([temp, endIdx]);
return indices.filter(Boolean);
};
const buildLinearOrder = (node) => {
const fontTree = [];
const newLinesPos = getDelimiterPos(node.characters, "\n");
newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
const newLinesRangeFont = node.getRangeFontName(
newLinesRangeStart,
newLinesRangeEnd
);
if (newLinesRangeFont === figma.mixed) {
const spacesPos = getDelimiterPos(
node.characters,
" ",
newLinesRangeStart,
newLinesRangeEnd
);
spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeEnd
);
if (spacesRangeFont === figma.mixed) {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeStart[0]
);
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
} else {
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
}
});
} else {
fontTree.push({
start: newLinesRangeStart,
delimiter: "\n",
family: newLinesRangeFont.family,
style: newLinesRangeFont.style,
});
}
});
return fontTree
.sort((a, b) => +a.start - +b.start)
.map(({ family, style, delimiter }) => ({ family, style, delimiter }));
};
const setCharactersWithSmartMatchFont = async (
node,
characters,
fallbackFont
) => {
const rangeTree = buildLinearOrder(node);
const fontsToLoad = uniqBy(
rangeTree,
({ family, style }) => `${family}::${style}`
).map(({ family, style }) => ({
family,
style,
}));
await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
node.fontName = fallbackFont;
node.characters = characters;
let prevPos = 0;
rangeTree.forEach(({ family, style, delimiter }) => {
if (prevPos < node.characters.length) {
const delimeterPos = node.characters.indexOf(delimiter, prevPos);
const endPos =
delimeterPos > prevPos ? delimeterPos : node.characters.length;
const matchedFont = {
family,
style,
};
node.setRangeFontName(prevPos, endPos, matchedFont);
prevPos = endPos + 1;
}
});
return true;
};
```
--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/ui.html:
--------------------------------------------------------------------------------
```html
<!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="3056"
value="3056"
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: 3056)</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: 3056,
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)) {
console.log("go inside", data);
console.log("nice", data);
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) || 3056;
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>
```
--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/code.js:
--------------------------------------------------------------------------------
```javascript
// This is the main code file for the Cursor MCP Figma plugin
// It handles Figma API commands
// Plugin state
const state = {
serverPort: 3056, // Default port
};
// Show UI
figma.showUI(__html__, { width: 350, height: 450 });
// Plugin commands from UI
figma.ui.onmessage = async (msg) => {
switch (msg.type) {
case "update-settings":
updateSettings(msg);
break;
case "notify":
figma.notify(msg.message);
break;
case "close-plugin":
figma.closePlugin();
break;
case "execute-command":
// Execute commands received from UI (which gets them from WebSocket)
try {
const result = await handleCommand(msg.command, msg.params);
// Send result back to UI
figma.ui.postMessage({
type: "command-result",
id: msg.id,
result,
});
} catch (error) {
figma.ui.postMessage({
type: "command-error",
id: msg.id,
error: error.message || "Error executing command",
});
}
break;
}
};
// Listen for plugin commands from menu
figma.on("run", ({ command }) => {
figma.ui.postMessage({ type: "auto-connect" });
});
// Update plugin settings
function updateSettings(settings) {
if (settings.serverPort) {
state.serverPort = settings.serverPort;
}
figma.clientStorage.setAsync("settings", {
serverPort: state.serverPort,
});
}
// Handle commands from UI
async function handleCommand(command, params) {
switch (command) {
case "get_document_info":
return await getDocumentInfo();
case "get_selection":
return await getSelection();
case "get_node_info":
if (!params || !params.nodeId) {
throw new Error("Missing nodeId parameter");
}
return await getNodeInfo(params.nodeId);
case "create_rectangle":
return await createRectangle(params);
case "create_frame":
return await createFrame(params);
case "create_text":
return await createText(params);
case "set_fill_color":
return await setFillColor(params);
case "set_stroke_color":
return await setStrokeColor(params);
case "move_node":
return await moveNode(params);
case "resize_node":
return await resizeNode(params);
case "delete_node":
return await deleteNode(params);
case "get_styles":
return await getStyles();
case "get_local_components":
return await getLocalComponents();
case "get_team_components":
return await getTeamComponents();
case "create_component_instance":
return await createComponentInstance(params);
case "export_node_as_image":
return await exportNodeAsImage(params);
case "execute_code":
return await executeCode(params);
case "set_corner_radius":
return await setCornerRadius(params);
default:
throw new Error(`Unknown command: ${command}`);
}
}
// Command implementations
async function getDocumentInfo() {
await figma.currentPage.loadAsync();
const page = figma.currentPage;
return {
name: page.name,
id: page.id,
type: page.type,
children: page.children.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
})),
currentPage: {
id: page.id,
name: page.name,
childCount: page.children.length,
},
pages: [
{
id: page.id,
name: page.name,
childCount: page.children.length,
},
],
};
}
async function getSelection() {
return {
selectionCount: figma.currentPage.selection.length,
selection: figma.currentPage.selection.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
visible: node.visible,
})),
};
}
async function getNodeInfo(nodeId) {
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Base node information
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
visible: node.visible,
};
// Add position and size for SceneNode
if ("x" in node && "y" in node) {
nodeInfo.x = node.x;
nodeInfo.y = node.y;
}
if ("width" in node && "height" in node) {
nodeInfo.width = node.width;
nodeInfo.height = node.height;
}
// Add fills for nodes with fills
if ("fills" in node) {
nodeInfo.fills = node.fills;
}
// Add strokes for nodes with strokes
if ("strokes" in node) {
nodeInfo.strokes = node.strokes;
if ("strokeWeight" in node) {
nodeInfo.strokeWeight = node.strokeWeight;
}
}
// Add children for parent nodes
if ("children" in node) {
nodeInfo.children = node.children.map((child) => ({
id: child.id,
name: child.name,
type: child.type,
}));
}
// Add text-specific properties
if (node.type === "TEXT") {
nodeInfo.characters = node.characters;
nodeInfo.fontSize = node.fontSize;
nodeInfo.fontName = node.fontName;
}
return nodeInfo;
}
async function createRectangle(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Rectangle",
parentId,
} = params || {};
const rect = figma.createRectangle();
rect.x = x;
rect.y = y;
rect.resize(width, height);
rect.name = name;
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(rect);
} else {
figma.currentPage.appendChild(rect);
}
return {
id: rect.id,
name: rect.name,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
parentId: rect.parent ? rect.parent.id : undefined,
};
}
async function createFrame(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Frame",
parentId,
} = params || {};
const frame = figma.createFrame();
frame.x = x;
frame.y = y;
frame.resize(width, height);
frame.name = name;
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(frame);
} else {
figma.currentPage.appendChild(frame);
}
return {
id: frame.id,
name: frame.name,
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
parentId: frame.parent ? frame.parent.id : undefined,
};
}
async function createText(params) {
const {
x = 0,
y = 0,
text = "Text",
fontSize = 14,
fontWeight = 400,
fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black
name = "Text",
parentId,
} = params || {};
// Map common font weights to Figma font styles
const getFontStyle = (weight) => {
switch (weight) {
case 100:
return "Thin";
case 200:
return "Extra Light";
case 300:
return "Light";
case 400:
return "Regular";
case 500:
return "Medium";
case 600:
return "Semi Bold";
case 700:
return "Bold";
case 800:
return "Extra Bold";
case 900:
return "Black";
default:
return "Regular";
}
};
const textNode = figma.createText();
textNode.x = x;
textNode.y = y;
textNode.name = name;
try {
await figma.loadFontAsync({
family: "Inter",
style: getFontStyle(fontWeight),
});
textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
textNode.fontSize = parseInt(fontSize);
} catch (error) {
console.error("Error setting font size", error);
}
setCharacters(textNode, text);
// Set text color
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(fontColor.r) || 0,
g: parseFloat(fontColor.g) || 0,
b: parseFloat(fontColor.b) || 0,
},
opacity: parseFloat(fontColor.a) || 1,
};
textNode.fills = [paintStyle];
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(textNode);
} else {
figma.currentPage.appendChild(textNode);
}
return {
id: textNode.id,
name: textNode.name,
x: textNode.x,
y: textNode.y,
width: textNode.width,
height: textNode.height,
characters: textNode.characters,
fontSize: textNode.fontSize,
fontWeight: fontWeight,
fontColor: fontColor,
fontName: textNode.fontName,
fills: textNode.fills,
parentId: textNode.parent ? textNode.parent.id : undefined,
};
}
async function setFillColor(params) {
console.log("setFillColor", params);
const {
nodeId,
color: { r, g, b, a },
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("fills" in node)) {
throw new Error(`Node does not support fills: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: parseFloat(r) || 0,
g: parseFloat(g) || 0,
b: parseFloat(b) || 0,
a: parseFloat(a) || 1,
};
// Set fill
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(rgbColor.r),
g: parseFloat(rgbColor.g),
b: parseFloat(rgbColor.b),
},
opacity: parseFloat(rgbColor.a),
};
console.log("paintStyle", paintStyle);
node.fills = [paintStyle];
return {
id: node.id,
name: node.name,
fills: [paintStyle],
};
}
async function setStrokeColor(params) {
const {
nodeId,
color: { r, g, b, a },
weight = 1,
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("strokes" in node)) {
throw new Error(`Node does not support strokes: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: r !== undefined ? r : 0,
g: g !== undefined ? g : 0,
b: b !== undefined ? b : 0,
a: a !== undefined ? a : 1,
};
// Set stroke
const paintStyle = {
type: "SOLID",
color: {
r: rgbColor.r,
g: rgbColor.g,
b: rgbColor.b,
},
opacity: rgbColor.a,
};
node.strokes = [paintStyle];
// Set stroke weight if available
if ("strokeWeight" in node) {
node.strokeWeight = weight;
}
return {
id: node.id,
name: node.name,
strokes: node.strokes,
strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined,
};
}
async function moveNode(params) {
const { nodeId, x, y } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (x === undefined || y === undefined) {
throw new Error("Missing x or y parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("x" in node) || !("y" in node)) {
throw new Error(`Node does not support position: ${nodeId}`);
}
node.x = x;
node.y = y;
return {
id: node.id,
name: node.name,
x: node.x,
y: node.y,
};
}
async function resizeNode(params) {
const { nodeId, width, height } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (width === undefined || height === undefined) {
throw new Error("Missing width or height parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("resize" in node)) {
throw new Error(`Node does not support resizing: ${nodeId}`);
}
node.resize(width, height);
return {
id: node.id,
name: node.name,
width: node.width,
height: node.height,
};
}
async function deleteNode(params) {
const { nodeId } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Save node info before deleting
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
};
node.remove();
return nodeInfo;
}
async function getStyles() {
const styles = {
colors: await figma.getLocalPaintStylesAsync(),
texts: await figma.getLocalTextStylesAsync(),
effects: await figma.getLocalEffectStylesAsync(),
grids: await figma.getLocalGridStylesAsync(),
};
return {
colors: styles.colors.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
paint: style.paints[0],
})),
texts: styles.texts.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
fontSize: style.fontSize,
fontName: style.fontName,
})),
effects: styles.effects.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
grids: styles.grids.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
};
}
async function getLocalComponents() {
const components = figma.root.findAllWithCriteria({
types: ["COMPONENT"],
});
return {
count: components.length,
components: components.map((component) => ({
id: component.id,
name: component.name,
key: "key" in component ? component.key : null,
})),
};
}
async function getTeamComponents() {
try {
const teamComponents =
await figma.teamLibrary.getAvailableComponentsAsync();
return {
count: teamComponents.length,
components: teamComponents.map((component) => ({
key: component.key,
name: component.name,
description: component.description,
libraryName: component.libraryName,
})),
};
} catch (error) {
throw new Error(`Error getting team components: ${error.message}`);
}
}
async function createComponentInstance(params) {
const { componentKey, x = 0, y = 0 } = params || {};
if (!componentKey) {
throw new Error("Missing componentKey parameter");
}
try {
const component = await figma.importComponentByKeyAsync(componentKey);
const instance = component.createInstance();
instance.x = x;
instance.y = y;
figma.currentPage.appendChild(instance);
return {
id: instance.id,
name: instance.name,
x: instance.x,
y: instance.y,
width: instance.width,
height: instance.height,
componentId: instance.componentId,
};
} catch (error) {
throw new Error(`Error creating component instance: ${error.message}`);
}
}
async function exportNodeAsImage(params) {
const { nodeId, format = "PNG", scale = 1 } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("exportAsync" in node)) {
throw new Error(`Node does not support exporting: ${nodeId}`);
}
try {
const settings = {
format: format,
constraint: { type: "SCALE", value: scale },
};
const bytes = await node.exportAsync(settings);
let mimeType;
switch (format) {
case "PNG":
mimeType = "image/png";
break;
case "JPG":
mimeType = "image/jpeg";
break;
case "SVG":
mimeType = "image/svg+xml";
break;
case "PDF":
mimeType = "application/pdf";
break;
default:
mimeType = "application/octet-stream";
}
// Convert to base64
const uint8Array = new Uint8Array(bytes);
let binary = "";
for (let i = 0; i < uint8Array.length; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
const base64 = btoa(binary);
const imageData = `data:${mimeType};base64,${base64}`;
return {
nodeId,
format,
scale,
mimeType,
imageData,
};
} catch (error) {
throw new Error(`Error exporting node as image: ${error.message}`);
}
}
async function executeCode(params) {
const { code } = params || {};
if (!code) {
throw new Error("Missing code parameter");
}
try {
// Execute the provided code
// Note: This is potentially unsafe, but matches the Blender MCP functionality
const executeFn = new Function(
"figma",
"selection",
`
try {
const result = (async () => {
${code}
})();
return result;
} catch (error) {
throw new Error('Error executing code: ' + error.message);
}
`
);
const result = await executeFn(figma, figma.currentPage.selection);
return { result };
} catch (error) {
throw new Error(`Error executing code: ${error.message}`);
}
}
async function setCornerRadius(params) {
const { nodeId, radius, corners } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (radius === undefined) {
throw new Error("Missing radius parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Check if node supports corner radius
if (!("cornerRadius" in node)) {
throw new Error(`Node does not support corner radius: ${nodeId}`);
}
// If corners array is provided, set individual corner radii
if (corners && Array.isArray(corners) && corners.length === 4) {
if ("topLeftRadius" in node) {
// Node supports individual corner radii
if (corners[0]) node.topLeftRadius = radius;
if (corners[1]) node.topRightRadius = radius;
if (corners[2]) node.bottomRightRadius = radius;
if (corners[3]) node.bottomLeftRadius = radius;
} else {
// Node only supports uniform corner radius
node.cornerRadius = radius;
}
} else {
// Set uniform corner radius
node.cornerRadius = radius;
}
return {
id: node.id,
name: node.name,
cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined,
topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined,
topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined,
bottomRightRadius:
"bottomRightRadius" in node ? node.bottomRightRadius : undefined,
bottomLeftRadius:
"bottomLeftRadius" in node ? node.bottomLeftRadius : undefined,
};
}
// Initialize settings on load
(async function initializePlugin() {
try {
const savedSettings = await figma.clientStorage.getAsync("settings");
if (savedSettings) {
if (savedSettings.serverPort) {
state.serverPort = savedSettings.serverPort;
}
}
// Send initial settings to UI
figma.ui.postMessage({
type: "init-settings",
settings: {
serverPort: state.serverPort,
},
});
} catch (error) {
console.error("Error loading settings:", error);
}
})();
function uniqBy(arr, predicate) {
const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
return [
...arr
.reduce((map, item) => {
const key = item === null || item === undefined ? item : cb(item);
map.has(key) || map.set(key, item);
return map;
}, new Map())
.values(),
];
}
const setCharacters = async (node, characters, options) => {
const fallbackFont = (options && options.fallbackFont) || {
family: "Inter",
style: "Regular",
};
try {
if (node.fontName === figma.mixed) {
if (options && options.smartStrategy === "prevail") {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const charFont = node.getRangeFontName(i - 1, i);
const key = `${charFont.family}::${charFont.style}`;
fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
}
const prevailedTreeItem = Object.entries(fontHashTree).sort(
(a, b) => b[1] - a[1]
)[0];
const [family, style] = prevailedTreeItem[0].split("::");
const prevailedFont = {
family,
style,
};
await figma.loadFontAsync(prevailedFont);
node.fontName = prevailedFont;
} else if (options && options.smartStrategy === "strict") {
return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
} else if (options && options.smartStrategy === "experimental") {
return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
} else {
const firstCharFont = node.getRangeFontName(0, 1);
await figma.loadFontAsync(firstCharFont);
node.fontName = firstCharFont;
}
} else {
await figma.loadFontAsync({
family: node.fontName.family,
style: node.fontName.style,
});
}
} catch (err) {
console.warn(
`Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
err
);
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
}
try {
node.characters = characters;
return true;
} catch (err) {
console.warn(`Failed to set characters. Skipped.`, err);
return false;
}
};
const setCharactersWithStrictMatchFont = async (
node,
characters,
fallbackFont
) => {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const startIdx = i - 1;
const startCharFont = node.getRangeFontName(startIdx, i);
const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
while (i < node.characters.length) {
i++;
const charFont = node.getRangeFontName(i - 1, i);
if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
break;
}
}
fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
}
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
node.characters = characters;
console.log(fontHashTree);
await Promise.all(
Object.keys(fontHashTree).map(async (range) => {
console.log(range, fontHashTree[range]);
const [start, end] = range.split("_");
const [family, style] = fontHashTree[range].split("::");
const matchedFont = {
family,
style,
};
await figma.loadFontAsync(matchedFont);
return node.setRangeFontName(Number(start), Number(end), matchedFont);
})
);
return true;
};
const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
const indices = [];
let temp = startIdx;
for (let i = startIdx; i < endIdx; i++) {
if (
str[i] === delimiter &&
i + startIdx !== endIdx &&
temp !== i + startIdx
) {
indices.push([temp, i + startIdx]);
temp = i + startIdx + 1;
}
}
temp !== endIdx && indices.push([temp, endIdx]);
return indices.filter(Boolean);
};
const buildLinearOrder = (node) => {
const fontTree = [];
const newLinesPos = getDelimiterPos(node.characters, "\n");
newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
const newLinesRangeFont = node.getRangeFontName(
newLinesRangeStart,
newLinesRangeEnd
);
if (newLinesRangeFont === figma.mixed) {
const spacesPos = getDelimiterPos(
node.characters,
" ",
newLinesRangeStart,
newLinesRangeEnd
);
spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeEnd
);
if (spacesRangeFont === figma.mixed) {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeStart[0]
);
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
} else {
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
}
});
} else {
fontTree.push({
start: newLinesRangeStart,
delimiter: "\n",
family: newLinesRangeFont.family,
style: newLinesRangeFont.style,
});
}
});
return fontTree
.sort((a, b) => +a.start - +b.start)
.map(({ family, style, delimiter }) => ({ family, style, delimiter }));
};
const setCharactersWithSmartMatchFont = async (
node,
characters,
fallbackFont
) => {
const rangeTree = buildLinearOrder(node);
const fontsToLoad = uniqBy(
rangeTree,
({ family, style }) => `${family}::${style}`
).map(({ family, style }) => ({
family,
style,
}));
await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
node.fontName = fallbackFont;
node.characters = characters;
let prevPos = 0;
rangeTree.forEach(({ family, style, delimiter }) => {
if (prevPos < node.characters.length) {
const delimeterPos = node.characters.indexOf(delimiter, prevPos);
const endPos =
delimeterPos > prevPos ? delimeterPos : node.characters.length;
const matchedFont = {
family,
style,
};
node.setRangeFontName(prevPos, endPos, matchedFont);
prevPos = endPos + 1;
}
});
return true;
};
```
--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid';
// Define TypeScript interfaces for Figma responses
interface FigmaResponse {
id: string;
result?: any;
error?: string;
}
// WebSocket connection and request tracking
let ws: WebSocket | null = null;
const pendingRequests = new Map<string, {
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
timeout: NodeJS.Timeout;
}>();
// Track which channel each client is in
let currentChannel: string | null = null;
// Create MCP server
const server = new McpServer({
name: "TalkToFigmaMCP",
version: "1.0.0",
});
// Document Info Tool
server.tool(
"get_document_info",
"Get detailed information about the current Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma('get_document_info');
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting document info: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Selection Tool
server.tool(
"get_selection",
"Get information about the current selection in Figma",
{},
async () => {
try {
const result = await sendCommandToFigma('get_selection');
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting selection: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Node Info Tool
server.tool(
"get_node_info",
"Get detailed information about a specific node in Figma",
{
nodeId: z.string().describe("The ID of the node to get information about")
},
async ({ nodeId }) => {
try {
const result = await sendCommandToFigma('get_node_info', { nodeId });
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting node info: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Create Rectangle Tool
server.tool(
"create_rectangle",
"Create a new rectangle in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
width: z.number().describe("Width of the rectangle"),
height: z.number().describe("Height of the rectangle"),
name: z.string().optional().describe("Optional name for the rectangle"),
parentId: z.string().optional().describe("Optional parent node ID to append the rectangle to")
},
async ({ x, y, width, height, name, parentId }) => {
try {
const result = await sendCommandToFigma('create_rectangle', {
x, y, width, height, name: name || 'Rectangle', parentId
});
return {
content: [
{
type: "text",
text: `Created rectangle "${JSON.stringify(result)}"`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating rectangle: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Create Frame Tool
server.tool(
"create_frame",
"Create a new frame in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
width: z.number().describe("Width of the frame"),
height: z.number().describe("Height of the frame"),
name: z.string().optional().describe("Optional name for the frame"),
parentId: z.string().optional().describe("Optional parent node ID to append the frame to"),
fillColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Fill color in RGBA format"),
strokeColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Stroke color in RGBA format"),
strokeWeight: z.number().positive().optional().describe("Stroke weight")
},
async ({ x, y, width, height, name, parentId, fillColor, strokeColor, strokeWeight }) => {
try {
const result = await sendCommandToFigma('create_frame', {
x, y, width, height, name: name || 'Frame', parentId,
fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 },
strokeColor: strokeColor,
strokeWeight: strokeWeight
});
const typedResult = result as { name: string, id: string };
return {
content: [
{
type: "text",
text: `Created frame "${typedResult.name}" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating frame: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Create Text Tool
server.tool(
"create_text",
"Create a new text element in Figma",
{
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
text: z.string().describe("Text content"),
fontSize: z.number().optional().describe("Font size (default: 14)"),
fontWeight: z.number().optional().describe("Font weight (e.g., 400 for Regular, 700 for Bold)"),
fontColor: z.object({
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
}).optional().describe("Font color in RGBA format"),
name: z.string().optional().describe("Optional name for the text node by default following text"),
parentId: z.string().optional().describe("Optional parent node ID to append the text to")
},
async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }) => {
try {
const result = await sendCommandToFigma('create_text', {
x, y, text,
fontSize: fontSize || 14,
fontWeight: fontWeight || 400,
fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 },
name: name || 'Text',
parentId
});
const typedResult = result as { name: string, id: string };
return {
content: [
{
type: "text",
text: `Created text "${typedResult.name}" with ID: ${typedResult.id}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating text: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Set Fill Color Tool
server.tool(
"set_fill_color",
"Set the fill color of a node in Figma can be TextNode or FrameNode",
{
nodeId: z.string().describe("The ID of the node to modify"),
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
},
async ({ nodeId, r, g, b, a }) => {
try {
const result = await sendCommandToFigma('set_fill_color', {
nodeId,
color: { r, g, b, a: a || 1 }
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Set fill color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1})`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting fill color: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Set Stroke Color Tool
server.tool(
"set_stroke_color",
"Set the stroke color of a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
r: z.number().min(0).max(1).describe("Red component (0-1)"),
g: z.number().min(0).max(1).describe("Green component (0-1)"),
b: z.number().min(0).max(1).describe("Blue component (0-1)"),
a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"),
weight: z.number().positive().optional().describe("Stroke weight")
},
async ({ nodeId, r, g, b, a, weight }) => {
try {
const result = await sendCommandToFigma('set_stroke_color', {
nodeId,
color: { r, g, b, a: a || 1 },
weight: weight || 1
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Set stroke color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting stroke color: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Move Node Tool
server.tool(
"move_node",
"Move a node to a new position in Figma",
{
nodeId: z.string().describe("The ID of the node to move"),
x: z.number().describe("New X position"),
y: z.number().describe("New Y position")
},
async ({ nodeId, x, y }) => {
try {
const result = await sendCommandToFigma('move_node', { nodeId, x, y });
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Moved node "${typedResult.name}" to position (${x}, ${y})`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error moving node: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Resize Node Tool
server.tool(
"resize_node",
"Resize a node in Figma",
{
nodeId: z.string().describe("The ID of the node to resize"),
width: z.number().positive().describe("New width"),
height: z.number().positive().describe("New height")
},
async ({ nodeId, width, height }) => {
try {
const result = await sendCommandToFigma('resize_node', { nodeId, width, height });
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error resizing node: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Delete Node Tool
server.tool(
"delete_node",
"Delete a node from Figma",
{
nodeId: z.string().describe("The ID of the node to delete")
},
async ({ nodeId }) => {
try {
await sendCommandToFigma('delete_node', { nodeId });
return {
content: [
{
type: "text",
text: `Deleted node with ID: ${nodeId}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting node: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Get Styles Tool
server.tool(
"get_styles",
"Get all styles from the current Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma('get_styles');
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting styles: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Get Local Components Tool
server.tool(
"get_local_components",
"Get all local components from the Figma document",
{},
async () => {
try {
const result = await sendCommandToFigma('get_local_components');
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting local components: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Get Team Components Tool
// server.tool(
// "get_team_components",
// "Get all team library components available in Figma",
// {},
// async () => {
// try {
// const result = await sendCommandToFigma('get_team_components');
// return {
// content: [
// {
// type: "text",
// text: JSON.stringify(result, null, 2)
// }
// ]
// };
// } catch (error) {
// return {
// content: [
// {
// type: "text",
// text: `Error getting team components: ${error instanceof Error ? error.message : String(error)}`
// }
// ]
// };
// }
// }
// );
// Create Component Instance Tool
server.tool(
"create_component_instance",
"Create an instance of a component in Figma",
{
componentKey: z.string().describe("Key of the component to instantiate"),
x: z.number().describe("X position"),
y: z.number().describe("Y position")
},
async ({ componentKey, x, y }) => {
try {
const result = await sendCommandToFigma('create_component_instance', { componentKey, x, y });
const typedResult = result as { name: string, id: string };
return {
content: [
{
type: "text",
text: `Created component instance "${typedResult.name}" with ID: ${typedResult.id}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating component instance: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Export Node as Image Tool
server.tool(
"export_node_as_image",
"Export a node as an image from Figma",
{
nodeId: z.string().describe("The ID of the node to export"),
format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
scale: z.number().positive().optional().describe("Export scale")
},
async ({ nodeId, format, scale }) => {
try {
const result = await sendCommandToFigma('export_node_as_image', {
nodeId,
format: format || 'PNG',
scale: scale || 1
});
const typedResult = result as { imageData: string, mimeType: string };
return {
content: [
{
type: "image",
data: typedResult.imageData,
mimeType: typedResult.mimeType || "image/png"
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error exporting node as image: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Execute Figma Code Tool
// server.tool(
// "execute_figma_code",
// "Execute arbitrary JavaScript code in Figma (use with caution)",
// {
// code: z.string().describe("JavaScript code to execute in Figma")
// },
// async ({ code }) => {
// try {
// const result = await sendCommandToFigma('execute_code', { code });
// return {
// content: [
// {
// type: "text",
// text: `Code executed successfully: ${JSON.stringify(result, null, 2)}`
// }
// ]
// };
// } catch (error) {
// return {
// content: [
// {
// type: "text",
// text: `Error executing code: ${error instanceof Error ? error.message : String(error)}`
// }
// ]
// };
// }
// }
// );
// Set Corner Radius Tool
server.tool(
"set_corner_radius",
"Set the corner radius of a node in Figma",
{
nodeId: z.string().describe("The ID of the node to modify"),
radius: z.number().min(0).describe("Corner radius value"),
corners: z.array(z.boolean()).length(4).optional().describe("Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]")
},
async ({ nodeId, radius, corners }) => {
try {
const result = await sendCommandToFigma('set_corner_radius', {
nodeId,
radius,
corners: corners || [true, true, true, true]
});
const typedResult = result as { name: string };
return {
content: [
{
type: "text",
text: `Set corner radius of node "${typedResult.name}" to ${radius}px`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error setting corner radius: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Define design strategy prompt
server.prompt(
"design_strategy",
"Best practices for working with Figma designs",
(extra) => {
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: `When working with Figma designs, follow these best practices:
1. Start with Document Structure:
- First use get_document_info() to understand the current document
- Plan your layout hierarchy before creating elements
- Create a main container frame for each screen/section
2. Naming Conventions:
- Use descriptive, semantic names for all elements
- Follow a consistent naming pattern (e.g., "Login Screen", "Logo Container", "Email Input")
- Group related elements with meaningful names
3. Layout Hierarchy:
- Create parent frames first, then add child elements
- For forms/login screens:
* Start with the main screen container frame
* Create a logo container at the top
* Group input fields in their own containers
* Place action buttons (login, submit) after inputs
* Add secondary elements (forgot password, signup links) last
4. Input Fields Structure:
- Create a container frame for each input field
- Include a label text above or inside the input
- Group related inputs (e.g., username/password) together
5. Element Creation:
- Use create_frame() for containers and input fields
- Use create_text() for labels, buttons text, and links
- Set appropriate colors and styles:
* Use fillColor for backgrounds
* Use strokeColor for borders
* Set proper fontWeight for different text elements
6. Visual Hierarchy:
- Position elements in logical reading order (top to bottom)
- Maintain consistent spacing between elements
- Use appropriate font sizes for different text types:
* Larger for headings/welcome text
* Medium for input labels
* Standard for button text
* Smaller for helper text/links
7. Best Practices:
- Verify each creation with get_node_info()
- Use parentId to maintain proper hierarchy
- Group related elements together in frames
- Keep consistent spacing and alignment
Example Login Screen Structure:
- Login Screen (main frame)
- Logo Container (frame)
- Logo (image/text)
- Welcome Text (text)
- Input Container (frame)
- Email Input (frame)
- Email Label (text)
- Email Field (frame)
- Password Input (frame)
- Password Label (text)
- Password Field (frame)
- Login Button (frame)
- Button Text (text)
- Helper Links (frame)
- Forgot Password (text)
- Don't have account (text)`
}
}
],
description: "Best practices for working with Figma designs"
};
}
);
// Define command types and parameters
type FigmaCommand =
| 'get_document_info'
| 'get_selection'
| 'get_node_info'
| 'create_rectangle'
| 'create_frame'
| 'create_text'
| 'set_fill_color'
| 'set_stroke_color'
| 'move_node'
| 'resize_node'
| 'delete_node'
| 'get_styles'
| 'get_local_components'
| 'get_team_components'
| 'create_component_instance'
| 'export_node_as_image'
| 'execute_code'
| 'join'
| 'set_corner_radius';
// Helper function to process Figma node responses
function processFigmaNodeResponse(result: unknown): any {
if (!result || typeof result !== 'object') {
return result;
}
// Check if this looks like a node response
const resultObj = result as Record<string, unknown>;
if ('id' in resultObj && typeof resultObj.id === 'string') {
// It appears to be a node response, log the details
console.info(`Processed Figma node: ${resultObj.name || 'Unknown'} (ID: ${resultObj.id})`);
if ('x' in resultObj && 'y' in resultObj) {
console.debug(`Node position: (${resultObj.x}, ${resultObj.y})`);
}
if ('width' in resultObj && 'height' in resultObj) {
console.debug(`Node dimensions: ${resultObj.width}×${resultObj.height}`);
}
}
return result;
}
// Simple function to connect to Figma WebSocket server
function connectToFigma(port: number = 3056) {
// If already connected, do nothing
if (ws && ws.readyState === WebSocket.OPEN) {
console.info('Already connected to Figma');
return;
}
console.info(`Connecting to Figma socket server on port ${port}...`);
ws = new WebSocket(`ws://localhost:${port}`);
ws.on('open', () => {
console.info('Connected to Figma socket server');
// Reset channel on new connection
currentChannel = null;
});
ws.on('message', (data: any) => {
try {
const json = JSON.parse(data) as { message: FigmaResponse };
const myResponse = json.message;
console.debug(`Received message: ${JSON.stringify(myResponse)}`);
console.log('myResponse', myResponse);
// Handle response to a request
if (myResponse.id && pendingRequests.has(myResponse.id) && myResponse.result) {
const request = pendingRequests.get(myResponse.id)!;
clearTimeout(request.timeout);
if (myResponse.error) {
console.error(`Error from Figma: ${myResponse.error}`);
request.reject(new Error(myResponse.error));
} else {
if (myResponse.result) {
request.resolve(myResponse.result);
}
}
pendingRequests.delete(myResponse.id);
} else {
// Handle broadcast messages or events
console.info(`Received broadcast message: ${JSON.stringify(myResponse)}`);
}
} catch (error) {
console.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`);
}
});
ws.on('error', (error) => {
console.error(`Socket error: ${error}`);
});
ws.on('close', () => {
console.info('Disconnected from Figma socket server');
ws = null;
// Reject all pending requests
for (const [id, request] of pendingRequests.entries()) {
clearTimeout(request.timeout);
request.reject(new Error('Connection closed'));
pendingRequests.delete(id);
}
// Attempt to reconnect
console.info('Attempting to reconnect in 2 seconds...');
setTimeout(() => connectToFigma(port), 2000);
});
}
// Function to join a channel
async function joinChannel(channelName: string): Promise<void> {
if (!ws || ws.readyState !== WebSocket.OPEN) {
throw new Error('Not connected to Figma');
}
try {
await sendCommandToFigma('join', { channel: channelName });
currentChannel = channelName;
console.info(`Joined channel: ${channelName}`);
} catch (error) {
console.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
// Function to send commands to Figma
function sendCommandToFigma(command: FigmaCommand, params: unknown = {}): Promise<unknown> {
return new Promise((resolve, reject) => {
// If not connected, try to connect first
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectToFigma();
reject(new Error('Not connected to Figma. Attempting to connect...'));
return;
}
// Check if we need a channel for this command
const requiresChannel = command !== 'join';
if (requiresChannel && !currentChannel) {
reject(new Error('Must join a channel before sending commands'));
return;
}
const id = uuidv4();
const request = {
id,
type: command === 'join' ? 'join' : 'message',
...(command === 'join' ? { channel: (params as any).channel } : { channel: currentChannel }),
message: {
id,
command,
params: {
...(params as any),
}
}
};
// Set timeout for request
const timeout = setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id);
console.error(`Request ${id} to Figma timed out after 30 seconds`);
reject(new Error('Request to Figma timed out'));
}
}, 30000); // 30 second timeout
// Store the promise callbacks to resolve/reject later
pendingRequests.set(id, { resolve, reject, timeout });
// Send the request
console.info(`Sending command to Figma: ${command}`);
console.debug(`Request details: ${JSON.stringify(request)}`);
ws.send(JSON.stringify(request));
});
}
// Update the join_channel tool
server.tool(
"join_channel",
"Join a specific channel to communicate with Figma",
{
channel: z.string().describe("The name of the channel to join").default("")
},
async ({ channel }) => {
try {
if (!channel) {
// If no channel provided, ask the user for input
return {
content: [
{
type: "text",
text: "Please provide a channel name to join:"
}
],
followUp: {
tool: "join_channel",
description: "Join the specified channel"
}
};
}
await joinChannel(channel);
return {
content: [
{
type: "text",
text: `Successfully joined channel: ${channel}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error joining channel: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Start the server
async function main() {
try {
// Try to connect to Figma socket server
connectToFigma();
} catch (error) {
console.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`);
console.warn('Will try to connect when the first command is sent');
}
// Start the MCP server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.info('FigmaMCP server running on stdio');
}
// Run the server
main().catch(error => {
console.error(`Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
```