#
tokens: 32094/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | .cursor/
```

--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Cursor Talk to Figma MCP
  2 | 
  3 | 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.
  4 | 
  5 | https://github.com/user-attachments/assets/129a14d2-ed73-470f-9a4c-2240b2a4885c
  6 | 
  7 | ## Project Structure
  8 | 
  9 | - `src/talk_to_figma_mcp/` - TypeScript MCP server for Figma integration
 10 | - `src/cursor_mcp_plugin/` - Figma plugin for communicating with Cursor
 11 | - `src/socket.ts` - WebSocket server that facilitates communication between the MCP server and Figma plugin
 12 | 
 13 | ## Get Started
 14 | 
 15 | 1. Install Bun if you haven't already:
 16 | 
 17 | ```bash
 18 | curl -fsSL https://bun.sh/install | bash
 19 | ```
 20 | 
 21 | 2. Run setup, this will also install MCP in your Cursor's active project
 22 | 
 23 | ```bash
 24 | bun setup
 25 | ```
 26 | 
 27 | 3. Start the Websocket server
 28 | 
 29 | ```bash
 30 | bun start
 31 | ```
 32 | 
 33 | 4. Install [Figma Plugin](#figma-plugin)
 34 | 
 35 | ## Manual Setup and Installation
 36 | 
 37 | ### MCP Server: Integration with Cursor
 38 | 
 39 | Add the server to your Cursor MCP configuration in `~/.cursor/mcp.json`:
 40 | 
 41 | ```json
 42 | {
 43 |   "mcpServers": {
 44 |     "TalkToFigma": {
 45 |       "command": "bun",
 46 |       "args": [
 47 |         "/path/to/cursor-talk-to-figma-mcp/src/talk_to_figma_mcp/server.ts"
 48 |       ]
 49 |     }
 50 |   }
 51 | }
 52 | ```
 53 | 
 54 | ### WebSocket Server
 55 | 
 56 | Start the WebSocket server:
 57 | 
 58 | ```bash
 59 | bun run src/socket.ts
 60 | ```
 61 | 
 62 | ### Figma Plugin
 63 | 
 64 | 1. In Figma, go to Plugins > Development > New Plugin
 65 | 2. Choose "Link existing plugin"
 66 | 3. Select the `src/cursor_mcp_plugin/manifest.json` file
 67 | 4. The plugin should now be available in your Figma development plugins
 68 | 
 69 | ## Usage
 70 | 
 71 | 1. Start the WebSocket server
 72 | 2. Install the MCP server in Cursor
 73 | 3. Open Figma and run the Cursor MCP Plugin
 74 | 4. Connect the plugin to the WebSocket server by joining a channel using `join_channel`
 75 | 5. Use Cursor to communicate with Figma using the MCP tools
 76 | 
 77 | ## MCP Tools
 78 | 
 79 | The MCP server provides the following tools for interacting with Figma:
 80 | 
 81 | ### Document & Selection
 82 | 
 83 | - `get_document_info` - Get information about the current Figma document
 84 | - `get_selection` - Get information about the current selection
 85 | - `get_node_info` - Get detailed information about a specific node
 86 | 
 87 | ### Creating Elements
 88 | 
 89 | - `create_rectangle` - Create a new rectangle with position, size, and optional name
 90 | - `create_frame` - Create a new frame with position, size, and optional name
 91 | - `create_text` - Create a new text node with customizable font properties
 92 | 
 93 | ### Styling
 94 | 
 95 | - `set_fill_color` - Set the fill color of a node (RGBA)
 96 | - `set_stroke_color` - Set the stroke color and weight of a node
 97 | - `set_corner_radius` - Set the corner radius of a node with optional per-corner control
 98 | 
 99 | ### Layout & Organization
100 | 
101 | - `move_node` - Move a node to a new position
102 | - `resize_node` - Resize a node with new dimensions
103 | - `delete_node` - Delete a node
104 | 
105 | ### Components & Styles
106 | 
107 | - `get_styles` - Get information about local styles
108 | - `get_local_components` - Get information about local components
109 | - `get_team_components` - Get information about team components
110 | - `create_component_instance` - Create an instance of a component
111 | 
112 | ### Export & Advanced
113 | 
114 | - `export_node_as_image` - Export a node as an image (PNG, JPG, SVG, or PDF)
115 | - `execute_figma_code` - Execute arbitrary JavaScript code in Figma (use with caution)
116 | 
117 | ### Connection Management
118 | 
119 | - `join_channel` - Join a specific channel to communicate with Figma
120 | 
121 | ## Development
122 | 
123 | ### Building the Figma Plugin
124 | 
125 | 1. Navigate to the Figma plugin directory:
126 | 
127 |    ```
128 |    cd src/cursor_mcp_plugin
129 |    ```
130 | 
131 | 2. Edit code.js and ui.html
132 | 
133 | ## Best Practices
134 | 
135 | When working with the Figma MCP:
136 | 
137 | 1. Always join a channel before sending commands
138 | 2. Get document overview using `get_document_info` first
139 | 3. Check current selection with `get_selection` before modifications
140 | 4. Use appropriate creation tools based on needs:
141 |    - `create_frame` for containers
142 |    - `create_rectangle` for basic shapes
143 |    - `create_text` for text elements
144 | 5. Verify changes using `get_node_info`
145 | 6. Use component instances when possible for consistency
146 | 7. Handle errors appropriately as all commands can throw exceptions
147 | 
148 | ## License
149 | 
150 | MIT
151 | 
```

--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Create .cursor directory if it doesn't exist
 4 | mkdir -p .cursor
 5 | 
 6 | # Get current directory path
 7 | CURRENT_DIR=$(pwd)
 8 | 
 9 | bun install
10 | 
11 | # Create mcp.json with the current directory path
12 | echo "{
13 |   \"mcpServers\": {
14 |     \"TalkToFigma\": {
15 |       \"command\": \"bun\",
16 |       \"args\": [
17 |         \"${CURRENT_DIR}/src/talk_to_figma_mcp/server.ts\"
18 |       ]
19 |     }
20 |   }
21 | }" > .cursor/mcp.json 
```

--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/manifest.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "Cursor MCP Plugin",
 3 |   "id": "cursor-mcp-plugin",
 4 |   "api": "1.0.0",
 5 |   "main": "code.js",
 6 |   "ui": "ui.html",
 7 |   "editorType": [
 8 |     "figma",
 9 |     "figjam"
10 |   ],
11 |   "permissions": [],
12 |   "networkAccess": {
13 |     "allowedDomains": [
14 |       "https://google.com"
15 |     ],
16 |     "devAllowedDomains": [
17 |       "http://localhost:3056",
18 |       "ws://localhost:3056"
19 |     ]
20 |   },
21 |   "documentAccess": "dynamic-page",
22 |   "enableProposedApi": true,
23 |   "enablePrivatePluginApi": true
24 | }
```

--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "outDir": "dist",
11 |     "rootDir": ".",
12 |     "declaration": true,
13 |     "experimentalDecorators": false,
14 |     "emitDecoratorMetadata": false,
15 |     "lib": ["ESNext", "DOM"]
16 |   },
17 |   "include": ["./**/*.ts"],
18 |   "exclude": ["node_modules", "dist"]
19 | }
20 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "cursor-talk-to-figma-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "Cursor Talk to Figma MCP",
 5 |   "main": "src/socket.ts",
 6 |   "scripts": {
 7 |     "setup": "./scripts/setup.sh",
 8 |     "start": "bun run src/socket.ts"
 9 |   },
10 |   "dependencies": {
11 |     "@modelcontextprotocol/sdk": "latest",
12 |     "@types/node": "^22.13.10",
13 |     "bun": "^1.2.5",
14 |     "uuid": "latest",
15 |     "ws": "latest",
16 |     "zod": "latest"
17 |   },
18 |   "devDependencies": {
19 |     "@types/bun": "^1.2.5",
20 |     "@types/ws": "^8.18.0"
21 |   }
22 | }
```

--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "talk-to-figma-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP server for Figma integration",
 5 |   "main": "server.ts",
 6 |   "type": "module",
 7 |   "scripts": {
 8 |     "start": "node --loader ts-node/esm server.ts",
 9 |     "build": "tsc",
10 |     "dev": "node --loader ts-node/esm --watch server.ts"
11 |   },
12 |   "keywords": [
13 |     "figma",
14 |     "mcp",
15 |     "cursor",
16 |     "ai"
17 |   ],
18 |   "dependencies": {
19 |     "@modelcontextprotocol/sdk": "^1.4.0",
20 |     "uuid": "^9.0.1",
21 |     "ws": "^8.16.0",
22 |     "zod": "^3.22.4"
23 |   },
24 |   "devDependencies": {
25 |     "@types/node": "^20.10.5",
26 |     "@types/uuid": "^9.0.7",
27 |     "@types/ws": "^8.5.10",
28 |     "ts-node": "^10.9.2",
29 |     "typescript": "^5.3.3"
30 |   }
31 | }
```

--------------------------------------------------------------------------------
/src/socket.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server, ServerWebSocket } from "bun";
  2 | 
  3 | // Store clients by channel
  4 | const channels = new Map<string, Set<ServerWebSocket<any>>>();
  5 | 
  6 | function handleConnection(ws: ServerWebSocket<any>) {
  7 |   // Don't add to clients immediately - wait for channel join
  8 |   console.log("New client connected");
  9 | 
 10 |   // Send welcome message to the new client
 11 |   ws.send(JSON.stringify({
 12 |     type: "system",
 13 |     message: "Please join a channel to start chatting",
 14 |   }));
 15 | 
 16 |   ws.close = () => {
 17 |     console.log("Client disconnected");
 18 | 
 19 |     // Remove client from their channel
 20 |     channels.forEach((clients, channelName) => {
 21 |       if (clients.has(ws)) {
 22 |         clients.delete(ws);
 23 | 
 24 |         // Notify other clients in same channel
 25 |         clients.forEach((client) => {
 26 |           if (client.readyState === WebSocket.OPEN) {
 27 |             client.send(JSON.stringify({
 28 |               type: "system",
 29 |               message: "A user has left the channel",
 30 |               channel: channelName
 31 |             }));
 32 |           }
 33 |         });
 34 |       }
 35 |     });
 36 |   };
 37 | }
 38 | 
 39 | const server = Bun.serve({
 40 |   port: 3056,
 41 |   fetch(req: Request, server: Server) {
 42 |     // Handle CORS preflight
 43 |     if (req.method === "OPTIONS") {
 44 |       return new Response(null, {
 45 |         headers: {
 46 |           "Access-Control-Allow-Origin": "*",
 47 |           "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
 48 |           "Access-Control-Allow-Headers": "Content-Type, Authorization",
 49 |         },
 50 |       });
 51 |     }
 52 | 
 53 |     // Handle WebSocket upgrade
 54 |     const success = server.upgrade(req, {
 55 |       headers: {
 56 |         "Access-Control-Allow-Origin": "*",
 57 |       },
 58 |     });
 59 | 
 60 |     if (success) {
 61 |       return; // Upgraded to WebSocket
 62 |     }
 63 | 
 64 |     // Return response for non-WebSocket requests
 65 |     return new Response("WebSocket server running", {
 66 |       headers: {
 67 |         "Access-Control-Allow-Origin": "*",
 68 |       },
 69 |     });
 70 |   },
 71 |   websocket: {
 72 |     open: handleConnection,
 73 |     message(ws: ServerWebSocket<any>, message: string | Buffer) {
 74 |       try {
 75 |         console.log("Received message from client:", message);
 76 |         const data = JSON.parse(message as string);
 77 | 
 78 |         if (data.type === "join") {
 79 |           const channelName = data.channel;
 80 |           if (!channelName || typeof channelName !== "string") {
 81 |             ws.send(JSON.stringify({
 82 |               type: "error",
 83 |               message: "Channel name is required"
 84 |             }));
 85 |             return;
 86 |           }
 87 | 
 88 |           // Create channel if it doesn't exist
 89 |           if (!channels.has(channelName)) {
 90 |             channels.set(channelName, new Set());
 91 |           }
 92 | 
 93 |           // Add client to channel
 94 |           const channelClients = channels.get(channelName)!;
 95 |           channelClients.add(ws);
 96 | 
 97 |           // Notify client they joined successfully
 98 |           ws.send(JSON.stringify({
 99 |             type: "system",
100 |             message: `Joined channel: ${channelName}`,
101 |             channel: channelName
102 |           }));
103 | 
104 |           console.log("Sending message to client:", data.id);
105 | 
106 |           ws.send(JSON.stringify({
107 |             type: "system",
108 |             message: {
109 |               id: data.id,
110 |               result: "Connected to channel: " + channelName,
111 |             },
112 |             channel: channelName
113 |           }));
114 | 
115 |           // Notify other clients in channel
116 |           channelClients.forEach((client) => {
117 |             if (client !== ws && client.readyState === WebSocket.OPEN) {
118 |               client.send(JSON.stringify({
119 |                 type: "system",
120 |                 message: "A new user has joined the channel",
121 |                 channel: channelName
122 |               }));
123 |             }
124 |           });
125 |           return;
126 |         }
127 | 
128 |         // Handle regular messages
129 |         if (data.type === "message") {
130 |           const channelName = data.channel;
131 |           if (!channelName || typeof channelName !== "string") {
132 |             ws.send(JSON.stringify({
133 |               type: "error",
134 |               message: "Channel name is required"
135 |             }));
136 |             return;
137 |           }
138 | 
139 |           const channelClients = channels.get(channelName);
140 |           if (!channelClients || !channelClients.has(ws)) {
141 |             ws.send(JSON.stringify({
142 |               type: "error",
143 |               message: "You must join the channel first"
144 |             }));
145 |             return;
146 |           }
147 | 
148 |           // Broadcast to all clients in the channel
149 |           channelClients.forEach((client) => {
150 |             if (client.readyState === WebSocket.OPEN) {
151 |               console.log("Broadcasting message to client:", data.message);
152 |               client.send(JSON.stringify({
153 |                 type: "broadcast",
154 |                 message: data.message,
155 |                 sender: client === ws ? "You" : "User",
156 |                 channel: channelName
157 |               }));
158 |             }
159 |           });
160 |         }
161 |       } catch (err) {
162 |         console.error("Error handling message:", err);
163 |       }
164 |     },
165 |     close(ws: ServerWebSocket<any>) {
166 |       // Remove client from their channel
167 |       channels.forEach((clients) => {
168 |         clients.delete(ws);
169 |       });
170 |     }
171 |   }
172 | });
173 | 
174 | console.log(`WebSocket server running on port ${server.port}`);
```

--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/setcharacters.js:
--------------------------------------------------------------------------------

```javascript
  1 | function uniqBy(arr, predicate) {
  2 |   const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
  3 |   return [
  4 |     ...arr
  5 |       .reduce((map, item) => {
  6 |         const key = item === null || item === undefined ? item : cb(item);
  7 | 
  8 |         map.has(key) || map.set(key, item);
  9 | 
 10 |         return map;
 11 |       }, new Map())
 12 |       .values(),
 13 |   ];
 14 | }
 15 | export const setCharacters = async (node, characters, options) => {
 16 |   const fallbackFont = options?.fallbackFont || {
 17 |     family: "Roboto",
 18 |     style: "Regular",
 19 |   };
 20 |   try {
 21 |     if (node.fontName === figma.mixed) {
 22 |       if (options?.smartStrategy === "prevail") {
 23 |         const fontHashTree = {};
 24 |         for (let i = 1; i < node.characters.length; i++) {
 25 |           const charFont = node.getRangeFontName(i - 1, i);
 26 |           const key = `${charFont.family}::${charFont.style}`;
 27 |           fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
 28 |         }
 29 |         const prevailedTreeItem = Object.entries(fontHashTree).sort(
 30 |           (a, b) => b[1] - a[1]
 31 |         )[0];
 32 |         const [family, style] = prevailedTreeItem[0].split("::");
 33 |         const prevailedFont = {
 34 |           family,
 35 |           style,
 36 |         };
 37 |         await figma.loadFontAsync(prevailedFont);
 38 |         node.fontName = prevailedFont;
 39 |       } else if (options?.smartStrategy === "strict") {
 40 |         return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
 41 |       } else if (options?.smartStrategy === "experimental") {
 42 |         return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
 43 |       } else {
 44 |         const firstCharFont = node.getRangeFontName(0, 1);
 45 |         await figma.loadFontAsync(firstCharFont);
 46 |         node.fontName = firstCharFont;
 47 |       }
 48 |     } else {
 49 |       await figma.loadFontAsync({
 50 |         family: node.fontName.family,
 51 |         style: node.fontName.style,
 52 |       });
 53 |     }
 54 |   } catch (err) {
 55 |     console.warn(
 56 |       `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
 57 |       err
 58 |     );
 59 |     await figma.loadFontAsync(fallbackFont);
 60 |     node.fontName = fallbackFont;
 61 |   }
 62 |   try {
 63 |     node.characters = characters;
 64 |     return true;
 65 |   } catch (err) {
 66 |     console.warn(`Failed to set characters. Skipped.`, err);
 67 |     return false;
 68 |   }
 69 | };
 70 | 
 71 | const setCharactersWithStrictMatchFont = async (
 72 |   node,
 73 |   characters,
 74 |   fallbackFont
 75 | ) => {
 76 |   const fontHashTree = {};
 77 |   for (let i = 1; i < node.characters.length; i++) {
 78 |     const startIdx = i - 1;
 79 |     const startCharFont = node.getRangeFontName(startIdx, i);
 80 |     const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
 81 |     while (i < node.characters.length) {
 82 |       i++;
 83 |       const charFont = node.getRangeFontName(i - 1, i);
 84 |       if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
 85 |         break;
 86 |       }
 87 |     }
 88 |     fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
 89 |   }
 90 |   await figma.loadFontAsync(fallbackFont);
 91 |   node.fontName = fallbackFont;
 92 |   node.characters = characters;
 93 |   console.log(fontHashTree);
 94 |   await Promise.all(
 95 |     Object.keys(fontHashTree).map(async (range) => {
 96 |       console.log(range, fontHashTree[range]);
 97 |       const [start, end] = range.split("_");
 98 |       const [family, style] = fontHashTree[range].split("::");
 99 |       const matchedFont = {
100 |         family,
101 |         style,
102 |       };
103 |       await figma.loadFontAsync(matchedFont);
104 |       return node.setRangeFontName(Number(start), Number(end), matchedFont);
105 |     })
106 |   );
107 |   return true;
108 | };
109 | 
110 | const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
111 |   const indices = [];
112 |   let temp = startIdx;
113 |   for (let i = startIdx; i < endIdx; i++) {
114 |     if (
115 |       str[i] === delimiter &&
116 |       i + startIdx !== endIdx &&
117 |       temp !== i + startIdx
118 |     ) {
119 |       indices.push([temp, i + startIdx]);
120 |       temp = i + startIdx + 1;
121 |     }
122 |   }
123 |   temp !== endIdx && indices.push([temp, endIdx]);
124 |   return indices.filter(Boolean);
125 | };
126 | 
127 | const buildLinearOrder = (node) => {
128 |   const fontTree = [];
129 |   const newLinesPos = getDelimiterPos(node.characters, "\n");
130 |   newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
131 |     const newLinesRangeFont = node.getRangeFontName(
132 |       newLinesRangeStart,
133 |       newLinesRangeEnd
134 |     );
135 |     if (newLinesRangeFont === figma.mixed) {
136 |       const spacesPos = getDelimiterPos(
137 |         node.characters,
138 |         " ",
139 |         newLinesRangeStart,
140 |         newLinesRangeEnd
141 |       );
142 |       spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
143 |         const spacesRangeFont = node.getRangeFontName(
144 |           spacesRangeStart,
145 |           spacesRangeEnd
146 |         );
147 |         if (spacesRangeFont === figma.mixed) {
148 |           const spacesRangeFont = node.getRangeFontName(
149 |             spacesRangeStart,
150 |             spacesRangeStart[0]
151 |           );
152 |           fontTree.push({
153 |             start: spacesRangeStart,
154 |             delimiter: " ",
155 |             family: spacesRangeFont.family,
156 |             style: spacesRangeFont.style,
157 |           });
158 |         } else {
159 |           fontTree.push({
160 |             start: spacesRangeStart,
161 |             delimiter: " ",
162 |             family: spacesRangeFont.family,
163 |             style: spacesRangeFont.style,
164 |           });
165 |         }
166 |       });
167 |     } else {
168 |       fontTree.push({
169 |         start: newLinesRangeStart,
170 |         delimiter: "\n",
171 |         family: newLinesRangeFont.family,
172 |         style: newLinesRangeFont.style,
173 |       });
174 |     }
175 |   });
176 |   return fontTree
177 |     .sort((a, b) => +a.start - +b.start)
178 |     .map(({ family, style, delimiter }) => ({ family, style, delimiter }));
179 | };
180 | 
181 | const setCharactersWithSmartMatchFont = async (
182 |   node,
183 |   characters,
184 |   fallbackFont
185 | ) => {
186 |   const rangeTree = buildLinearOrder(node);
187 |   const fontsToLoad = uniqBy(
188 |     rangeTree,
189 |     ({ family, style }) => `${family}::${style}`
190 |   ).map(({ family, style }) => ({
191 |     family,
192 |     style,
193 |   }));
194 | 
195 |   await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
196 | 
197 |   node.fontName = fallbackFont;
198 |   node.characters = characters;
199 | 
200 |   let prevPos = 0;
201 |   rangeTree.forEach(({ family, style, delimiter }) => {
202 |     if (prevPos < node.characters.length) {
203 |       const delimeterPos = node.characters.indexOf(delimiter, prevPos);
204 |       const endPos =
205 |         delimeterPos > prevPos ? delimeterPos : node.characters.length;
206 |       const matchedFont = {
207 |         family,
208 |         style,
209 |       };
210 |       node.setRangeFontName(prevPos, endPos, matchedFont);
211 |       prevPos = endPos + 1;
212 |     }
213 |   });
214 |   return true;
215 | };
216 | 
```

--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/ui.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html>
  3 |   <head>
  4 |     <meta charset="utf-8" />
  5 |     <title>Cursor MCP Plugin</title>
  6 |     <style>
  7 |       body {
  8 |         font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
  9 |           Helvetica, Arial, sans-serif;
 10 |         margin: 0;
 11 |         padding: 20px;
 12 |         color: #e0e0e0;
 13 |         background-color: #1e1e1e;
 14 |       }
 15 |       .container {
 16 |         display: flex;
 17 |         flex-direction: column;
 18 |         height: 100%;
 19 |       }
 20 |       h1 {
 21 |         font-size: 16px;
 22 |         font-weight: 600;
 23 |         margin-bottom: 10px;
 24 |         color: #ffffff;
 25 |       }
 26 |       h2 {
 27 |         font-size: 14px;
 28 |         font-weight: 600;
 29 |         margin-top: 20px;
 30 |         margin-bottom: 8px;
 31 |         color: #ffffff;
 32 |       }
 33 |       button {
 34 |         background-color: #18a0fb;
 35 |         border: none;
 36 |         color: white;
 37 |         padding: 8px 12px;
 38 |         border-radius: 6px;
 39 |         margin-top: 8px;
 40 |         margin-bottom: 8px;
 41 |         cursor: pointer;
 42 |         font-size: 14px;
 43 |         transition: background-color 0.2s;
 44 |       }
 45 |       button:hover {
 46 |         background-color: #0d8ee0;
 47 |       }
 48 |       button.secondary {
 49 |         background-color: #3d3d3d;
 50 |         color: #e0e0e0;
 51 |       }
 52 |       button.secondary:hover {
 53 |         background-color: #4d4d4d;
 54 |       }
 55 |       button:disabled {
 56 |         background-color: #333333;
 57 |         color: #666666;
 58 |         cursor: not-allowed;
 59 |       }
 60 |       input {
 61 |         border: 1px solid #444444;
 62 |         border-radius: 4px;
 63 |         padding: 8px;
 64 |         margin-bottom: 12px;
 65 |         font-size: 14px;
 66 |         width: 100%;
 67 |         box-sizing: border-box;
 68 |         background-color: #2d2d2d;
 69 |         color: #e0e0e0;
 70 |       }
 71 |       label {
 72 |         display: block;
 73 |         margin-bottom: 4px;
 74 |         font-size: 12px;
 75 |         font-weight: 500;
 76 |         color: #cccccc;
 77 |       }
 78 |       .status {
 79 |         margin-top: 16px;
 80 |         padding: 12px;
 81 |         border-radius: 6px;
 82 |         font-size: 14px;
 83 |       }
 84 |       .status.connected {
 85 |         background-color: #1a472a;
 86 |         color: #4ade80;
 87 |       }
 88 |       .status.disconnected {
 89 |         background-color: #471a1a;
 90 |         color: #ff9999;
 91 |       }
 92 |       .status.info {
 93 |         background-color: #1a3147;
 94 |         color: #66b3ff;
 95 |       }
 96 |       .section {
 97 |         margin-bottom: 24px;
 98 |       }
 99 |       .hidden {
100 |         display: none;
101 |       }
102 |       .logo {
103 |         width: 50px;
104 |         height: 50px;
105 |       }
106 |       .header {
107 |         display: flex;
108 |         align-items: center;
109 |         margin-bottom: 16px;
110 |       }
111 |       .header-text {
112 |         margin-left: 12px;
113 |       }
114 |       .header-text h1 {
115 |         margin: 0;
116 |         font-size: 16px;
117 |       }
118 |       .header-text p {
119 |         margin: 4px 0 0 0;
120 |         font-size: 12px;
121 |         color: #999999;
122 |       }
123 |       .tabs {
124 |         display: flex;
125 |         border-bottom: 1px solid #444444;
126 |         margin-bottom: 16px;
127 |       }
128 |       .tab {
129 |         padding: 8px 16px;
130 |         cursor: pointer;
131 |         font-size: 14px;
132 |         font-weight: 500;
133 |         color: #999999;
134 |       }
135 |       .tab.active {
136 |         border-bottom: 2px solid #18a0fb;
137 |         color: #18a0fb;
138 |       }
139 |       .tab-content {
140 |         display: none;
141 |       }
142 |       .tab-content.active {
143 |         display: block;
144 |       }
145 |       .link {
146 |         color: #18a0fb;
147 |         text-decoration: none;
148 |         cursor: pointer;
149 |       }
150 |       .link:hover {
151 |         text-decoration: underline;
152 |       }
153 |       .header-logo {
154 |         padding: 16px;
155 |         border-radius: 16px;
156 |         background-color: #333;
157 |       }
158 |       .header-logo-image {
159 |         width: 24px;
160 |         height: 24px;
161 |         object-fit: contain;
162 |       }
163 |     </style>
164 |   </head>
165 |   <body>
166 |     <div class="container">
167 |       <div class="header">
168 |         <div class="header-logo">
169 |           <img
170 |             class="header-logo-image"
171 |             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=="
172 |           />
173 |         </div>
174 |         <div class="header-text">
175 |           <h1>Cursor Talk To Figma Plugin</h1>
176 |           <p>Connect Figma to Cursor AI using MCP</p>
177 |         </div>
178 |       </div>
179 | 
180 |       <div class="tabs">
181 |         <div id="tab-connection" class="tab active">Connection</div>
182 |         <div id="tab-about" class="tab">About</div>
183 |       </div>
184 | 
185 |       <div id="content-connection" class="tab-content active">
186 |         <div class="section">
187 |           <label for="port">WebSocket Server Port</label>
188 |           <div style="display: flex; gap: 8px">
189 |             <input
190 |               type="number"
191 |               id="port"
192 |               placeholder="3056"
193 |               value="3056"
194 |               min="1024"
195 |               max="65535"
196 |             />
197 |             <button id="btn-connect" class="primary">Connect</button>
198 |           </div>
199 |         </div>
200 | 
201 |         <div id="connection-status" class="status disconnected">
202 |           Not connected to Cursor MCP server
203 |         </div>
204 | 
205 |         <div class="section">
206 |           <button id="btn-disconnect" class="secondary" disabled>
207 |             Disconnect
208 |           </button>
209 |         </div>
210 |       </div>
211 | 
212 |       <div id="content-about" class="tab-content">
213 |         <div class="section">
214 |           <h2>About Cursor Talk To Figma Plugin</h2>
215 |           <p>
216 |             This plugin allows Cursor AI to communicate with Figma, enabling
217 |             AI-assisted design operations. created by
218 |             <a
219 |               class="link"
220 |               onclick="window.open(`https://github.com/sonnylazuardi`, '_blank')"
221 |             >
222 |               Sonny
223 |             </a>
224 |           </p>
225 |           <p>Version: 1.0.0</p>
226 | 
227 |           <h2>How to Use</h2>
228 |           <ol>
229 |             <li>Make sure the MCP server is running in Cursor</li>
230 |             <li>Connect to the server using the port number (default: 3056)</li>
231 |             <li>Once connected, you can interact with Figma through Cursor</li>
232 |           </ol>
233 |         </div>
234 |       </div>
235 |     </div>
236 | 
237 |     <script>
238 |       // WebSocket connection state
239 |       const state = {
240 |         connected: false,
241 |         socket: null,
242 |         serverPort: 3056,
243 |         pendingRequests: new Map(),
244 |         channel: null,
245 |       };
246 | 
247 |       // UI Elements
248 |       const portInput = document.getElementById("port");
249 |       const connectButton = document.getElementById("btn-connect");
250 |       const disconnectButton = document.getElementById("btn-disconnect");
251 |       const connectionStatus = document.getElementById("connection-status");
252 | 
253 |       // Tabs
254 |       const tabs = document.querySelectorAll(".tab");
255 |       const tabContents = document.querySelectorAll(".tab-content");
256 | 
257 |       // Initialize UI
258 |       function updateConnectionStatus(isConnected, message) {
259 |         state.connected = isConnected;
260 |         connectionStatus.innerHTML =
261 |           message ||
262 |           (isConnected
263 |             ? "Connected to Cursor MCP server"
264 |             : "Not connected to Cursor MCP server");
265 |         connectionStatus.className = `status ${
266 |           isConnected ? "connected" : "disconnected"
267 |         }`;
268 | 
269 |         connectButton.disabled = isConnected;
270 |         disconnectButton.disabled = !isConnected;
271 |         portInput.disabled = isConnected;
272 |       }
273 | 
274 |       // Connect to WebSocket server
275 |       async function connectToServer(port) {
276 |         try {
277 |           if (state.connected && state.socket) {
278 |             updateConnectionStatus(true, "Already connected to server");
279 |             return;
280 |           }
281 | 
282 |           state.serverPort = port;
283 |           state.socket = new WebSocket(`ws://localhost:${port}`);
284 | 
285 |           state.socket.onopen = () => {
286 |             // Generate random channel name
287 |             const channelName = generateChannelName();
288 |             console.log("Joining channel:", channelName);
289 |             state.channel = channelName;
290 | 
291 |             // Join the channel using the same format as App.tsx
292 |             state.socket.send(
293 |               JSON.stringify({
294 |                 type: "join",
295 |                 channel: channelName.trim(),
296 |               })
297 |             );
298 |           };
299 | 
300 |           state.socket.onmessage = (event) => {
301 |             try {
302 |               const data = JSON.parse(event.data);
303 |               console.log("Received message:", data);
304 | 
305 |               if (data.type === "system") {
306 |                 // Successfully joined channel
307 |                 if (data.message && data.message.result) {
308 |                   state.connected = true;
309 |                   const channelName = data.channel;
310 |                   updateConnectionStatus(
311 |                     true,
312 |                     `Connected to server on port ${port} in channel: <strong>${channelName}</strong>`
313 |                   );
314 | 
315 |                   // Notify the plugin code
316 |                   parent.postMessage(
317 |                     {
318 |                       pluginMessage: {
319 |                         type: "notify",
320 |                         message: `Connected to Cursor MCP server on port ${port} in channel: ${channelName}`,
321 |                       },
322 |                     },
323 |                     "*"
324 |                   );
325 |                 }
326 |               } else if (data.type === "error") {
327 |                 console.error("Error:", data.message);
328 |                 updateConnectionStatus(false, `Error: ${data.message}`);
329 |                 state.socket.close();
330 |               }
331 | 
332 |               handleSocketMessage(data);
333 |             } catch (error) {
334 |               console.error("Error parsing message:", error);
335 |             }
336 |           };
337 | 
338 |           state.socket.onclose = () => {
339 |             state.connected = false;
340 |             state.socket = null;
341 |             updateConnectionStatus(false, "Disconnected from server");
342 |           };
343 | 
344 |           state.socket.onerror = (error) => {
345 |             console.error("WebSocket error:", error);
346 |             updateConnectionStatus(false, "Connection error");
347 |             state.connected = false;
348 |             state.socket = null;
349 |           };
350 |         } catch (error) {
351 |           console.error("Connection error:", error);
352 |           updateConnectionStatus(
353 |             false,
354 |             `Connection error: ${error.message || "Unknown error"}`
355 |           );
356 |         }
357 |       }
358 | 
359 |       // Disconnect from websocket server
360 |       function disconnectFromServer() {
361 |         if (state.socket) {
362 |           state.socket.close();
363 |           state.socket = null;
364 |           state.connected = false;
365 |           updateConnectionStatus(false, "Disconnected from server");
366 |         }
367 |       }
368 | 
369 |       // Handle messages from the WebSocket
370 |       async function handleSocketMessage(payload) {
371 |         const data = payload.message;
372 |         console.log("handleSocketMessage", data);
373 | 
374 |         // If it's a response to a previous request
375 |         if (data.id && state.pendingRequests.has(data.id)) {
376 |           console.log("go inside", data);
377 | 
378 |           console.log("nice", data);
379 |           const { resolve, reject } = state.pendingRequests.get(data.id);
380 |           state.pendingRequests.delete(data.id);
381 | 
382 |           if (data.error) {
383 |             reject(new Error(data.error));
384 |           } else {
385 |             resolve(data.result);
386 |           }
387 |           return;
388 |         }
389 | 
390 |         // If it's a new command
391 |         if (data.command) {
392 |           try {
393 |             // Send the command to the plugin code
394 |             parent.postMessage(
395 |               {
396 |                 pluginMessage: {
397 |                   type: "execute-command",
398 |                   id: data.id,
399 |                   command: data.command,
400 |                   params: data.params,
401 |                 },
402 |               },
403 |               "*"
404 |             );
405 |           } catch (error) {
406 |             // Send error back to WebSocket
407 |             sendErrorResponse(
408 |               data.id,
409 |               error.message || "Error executing command"
410 |             );
411 |           }
412 |         }
413 |       }
414 | 
415 |       // Send a command to the WebSocket server
416 |       async function sendCommand(command, params) {
417 |         return new Promise((resolve, reject) => {
418 |           if (!state.connected || !state.socket) {
419 |             reject(new Error("Not connected to server"));
420 |             return;
421 |           }
422 | 
423 |           const id = generateId();
424 |           state.pendingRequests.set(id, { resolve, reject });
425 | 
426 |           state.socket.send(
427 |             JSON.stringify({
428 |               id,
429 |               type: "message",
430 |               channel: state.channel,
431 |               message: {
432 |                 id,
433 |                 command,
434 |                 params,
435 |               },
436 |             })
437 |           );
438 | 
439 |           // Set timeout to reject the promise after 30 seconds
440 |           setTimeout(() => {
441 |             if (state.pendingRequests.has(id)) {
442 |               state.pendingRequests.delete(id);
443 |               reject(new Error("Request timed out"));
444 |             }
445 |           }, 30000);
446 |         });
447 |       }
448 | 
449 |       // Send success response back to WebSocket
450 |       function sendSuccessResponse(id, result) {
451 |         if (!state.connected || !state.socket) {
452 |           console.error("Cannot send response: socket not connected");
453 |           return;
454 |         }
455 | 
456 |         state.socket.send(
457 |           JSON.stringify({
458 |             id,
459 |             type: "message",
460 |             channel: state.channel,
461 |             message: {
462 |               id,
463 |               result,
464 |             },
465 |           })
466 |         );
467 |       }
468 | 
469 |       // Send error response back to WebSocket
470 |       function sendErrorResponse(id, errorMessage) {
471 |         if (!state.connected || !state.socket) {
472 |           console.error("Cannot send error response: socket not connected");
473 |           return;
474 |         }
475 | 
476 |         state.socket.send(
477 |           JSON.stringify({
478 |             id,
479 |             error: errorMessage,
480 |           })
481 |         );
482 |       }
483 | 
484 |       // Helper to generate unique IDs
485 |       function generateId() {
486 |         return (
487 |           Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
488 |         );
489 |       }
490 | 
491 |       // Add this function after the generateId() function
492 |       function generateChannelName() {
493 |         const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
494 |         let result = "";
495 |         for (let i = 0; i < 8; i++) {
496 |           result += characters.charAt(
497 |             Math.floor(Math.random() * characters.length)
498 |           );
499 |         }
500 |         return result;
501 |       }
502 | 
503 |       // Tab switching
504 |       tabs.forEach((tab) => {
505 |         tab.addEventListener("click", () => {
506 |           tabs.forEach((t) => t.classList.remove("active"));
507 |           tabContents.forEach((c) => c.classList.remove("active"));
508 | 
509 |           tab.classList.add("active");
510 |           const contentId = "content-" + tab.id.split("-")[1];
511 |           document.getElementById(contentId).classList.add("active");
512 |         });
513 |       });
514 | 
515 |       // Connect to server
516 |       connectButton.addEventListener("click", () => {
517 |         const port = parseInt(portInput.value, 10) || 3056;
518 |         updateConnectionStatus(false, "Connecting...");
519 |         connectionStatus.className = "status info";
520 |         connectToServer(port);
521 |       });
522 | 
523 |       // Disconnect from server
524 |       disconnectButton.addEventListener("click", () => {
525 |         updateConnectionStatus(false, "Disconnecting...");
526 |         connectionStatus.className = "status info";
527 |         disconnectFromServer();
528 |       });
529 | 
530 |       // Listen for messages from the plugin code
531 |       window.onmessage = (event) => {
532 |         const message = event.data.pluginMessage;
533 |         if (!message) return;
534 | 
535 |         switch (message.type) {
536 |           case "connection-status":
537 |             updateConnectionStatus(message.connected, message.message);
538 |             break;
539 |           case "auto-connect":
540 |             connectButton.click();
541 |             break;
542 |           case "auto-disconnect":
543 |             disconnectButton.click();
544 |             break;
545 |           case "command-result":
546 |             // Forward the result from plugin code back to WebSocket
547 |             sendSuccessResponse(message.id, message.result);
548 |             break;
549 |           case "command-error":
550 |             // Forward the error from plugin code back to WebSocket
551 |             sendErrorResponse(message.id, message.error);
552 |             break;
553 |         }
554 |       };
555 |     </script>
556 |   </body>
557 | </html>
558 | 
```

--------------------------------------------------------------------------------
/src/cursor_mcp_plugin/code.js:
--------------------------------------------------------------------------------

```javascript
   1 | // This is the main code file for the Cursor MCP Figma plugin
   2 | // It handles Figma API commands
   3 | 
   4 | // Plugin state
   5 | const state = {
   6 |   serverPort: 3056, // Default port
   7 | };
   8 | 
   9 | // Show UI
  10 | figma.showUI(__html__, { width: 350, height: 450 });
  11 | 
  12 | // Plugin commands from UI
  13 | figma.ui.onmessage = async (msg) => {
  14 |   switch (msg.type) {
  15 |     case "update-settings":
  16 |       updateSettings(msg);
  17 |       break;
  18 |     case "notify":
  19 |       figma.notify(msg.message);
  20 |       break;
  21 |     case "close-plugin":
  22 |       figma.closePlugin();
  23 |       break;
  24 |     case "execute-command":
  25 |       // Execute commands received from UI (which gets them from WebSocket)
  26 |       try {
  27 |         const result = await handleCommand(msg.command, msg.params);
  28 |         // Send result back to UI
  29 |         figma.ui.postMessage({
  30 |           type: "command-result",
  31 |           id: msg.id,
  32 |           result,
  33 |         });
  34 |       } catch (error) {
  35 |         figma.ui.postMessage({
  36 |           type: "command-error",
  37 |           id: msg.id,
  38 |           error: error.message || "Error executing command",
  39 |         });
  40 |       }
  41 |       break;
  42 |   }
  43 | };
  44 | 
  45 | // Listen for plugin commands from menu
  46 | figma.on("run", ({ command }) => {
  47 |   figma.ui.postMessage({ type: "auto-connect" });
  48 | });
  49 | 
  50 | // Update plugin settings
  51 | function updateSettings(settings) {
  52 |   if (settings.serverPort) {
  53 |     state.serverPort = settings.serverPort;
  54 |   }
  55 | 
  56 |   figma.clientStorage.setAsync("settings", {
  57 |     serverPort: state.serverPort,
  58 |   });
  59 | }
  60 | 
  61 | // Handle commands from UI
  62 | async function handleCommand(command, params) {
  63 |   switch (command) {
  64 |     case "get_document_info":
  65 |       return await getDocumentInfo();
  66 |     case "get_selection":
  67 |       return await getSelection();
  68 |     case "get_node_info":
  69 |       if (!params || !params.nodeId) {
  70 |         throw new Error("Missing nodeId parameter");
  71 |       }
  72 |       return await getNodeInfo(params.nodeId);
  73 |     case "create_rectangle":
  74 |       return await createRectangle(params);
  75 |     case "create_frame":
  76 |       return await createFrame(params);
  77 |     case "create_text":
  78 |       return await createText(params);
  79 |     case "set_fill_color":
  80 |       return await setFillColor(params);
  81 |     case "set_stroke_color":
  82 |       return await setStrokeColor(params);
  83 |     case "move_node":
  84 |       return await moveNode(params);
  85 |     case "resize_node":
  86 |       return await resizeNode(params);
  87 |     case "delete_node":
  88 |       return await deleteNode(params);
  89 |     case "get_styles":
  90 |       return await getStyles();
  91 |     case "get_local_components":
  92 |       return await getLocalComponents();
  93 |     case "get_team_components":
  94 |       return await getTeamComponents();
  95 |     case "create_component_instance":
  96 |       return await createComponentInstance(params);
  97 |     case "export_node_as_image":
  98 |       return await exportNodeAsImage(params);
  99 |     case "execute_code":
 100 |       return await executeCode(params);
 101 |     case "set_corner_radius":
 102 |       return await setCornerRadius(params);
 103 |     default:
 104 |       throw new Error(`Unknown command: ${command}`);
 105 |   }
 106 | }
 107 | 
 108 | // Command implementations
 109 | 
 110 | async function getDocumentInfo() {
 111 |   await figma.currentPage.loadAsync();
 112 |   const page = figma.currentPage;
 113 |   return {
 114 |     name: page.name,
 115 |     id: page.id,
 116 |     type: page.type,
 117 |     children: page.children.map((node) => ({
 118 |       id: node.id,
 119 |       name: node.name,
 120 |       type: node.type,
 121 |     })),
 122 |     currentPage: {
 123 |       id: page.id,
 124 |       name: page.name,
 125 |       childCount: page.children.length,
 126 |     },
 127 |     pages: [
 128 |       {
 129 |         id: page.id,
 130 |         name: page.name,
 131 |         childCount: page.children.length,
 132 |       },
 133 |     ],
 134 |   };
 135 | }
 136 | 
 137 | async function getSelection() {
 138 |   return {
 139 |     selectionCount: figma.currentPage.selection.length,
 140 |     selection: figma.currentPage.selection.map((node) => ({
 141 |       id: node.id,
 142 |       name: node.name,
 143 |       type: node.type,
 144 |       visible: node.visible,
 145 |     })),
 146 |   };
 147 | }
 148 | 
 149 | async function getNodeInfo(nodeId) {
 150 |   const node = await figma.getNodeByIdAsync(nodeId);
 151 | 
 152 |   if (!node) {
 153 |     throw new Error(`Node not found with ID: ${nodeId}`);
 154 |   }
 155 | 
 156 |   // Base node information
 157 |   const nodeInfo = {
 158 |     id: node.id,
 159 |     name: node.name,
 160 |     type: node.type,
 161 |     visible: node.visible,
 162 |   };
 163 | 
 164 |   // Add position and size for SceneNode
 165 |   if ("x" in node && "y" in node) {
 166 |     nodeInfo.x = node.x;
 167 |     nodeInfo.y = node.y;
 168 |   }
 169 | 
 170 |   if ("width" in node && "height" in node) {
 171 |     nodeInfo.width = node.width;
 172 |     nodeInfo.height = node.height;
 173 |   }
 174 | 
 175 |   // Add fills for nodes with fills
 176 |   if ("fills" in node) {
 177 |     nodeInfo.fills = node.fills;
 178 |   }
 179 | 
 180 |   // Add strokes for nodes with strokes
 181 |   if ("strokes" in node) {
 182 |     nodeInfo.strokes = node.strokes;
 183 |     if ("strokeWeight" in node) {
 184 |       nodeInfo.strokeWeight = node.strokeWeight;
 185 |     }
 186 |   }
 187 | 
 188 |   // Add children for parent nodes
 189 |   if ("children" in node) {
 190 |     nodeInfo.children = node.children.map((child) => ({
 191 |       id: child.id,
 192 |       name: child.name,
 193 |       type: child.type,
 194 |     }));
 195 |   }
 196 | 
 197 |   // Add text-specific properties
 198 |   if (node.type === "TEXT") {
 199 |     nodeInfo.characters = node.characters;
 200 |     nodeInfo.fontSize = node.fontSize;
 201 |     nodeInfo.fontName = node.fontName;
 202 |   }
 203 | 
 204 |   return nodeInfo;
 205 | }
 206 | 
 207 | async function createRectangle(params) {
 208 |   const {
 209 |     x = 0,
 210 |     y = 0,
 211 |     width = 100,
 212 |     height = 100,
 213 |     name = "Rectangle",
 214 |     parentId,
 215 |   } = params || {};
 216 | 
 217 |   const rect = figma.createRectangle();
 218 |   rect.x = x;
 219 |   rect.y = y;
 220 |   rect.resize(width, height);
 221 |   rect.name = name;
 222 | 
 223 |   // If parentId is provided, append to that node, otherwise append to current page
 224 |   if (parentId) {
 225 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 226 |     if (!parentNode) {
 227 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 228 |     }
 229 |     if (!("appendChild" in parentNode)) {
 230 |       throw new Error(`Parent node does not support children: ${parentId}`);
 231 |     }
 232 |     parentNode.appendChild(rect);
 233 |   } else {
 234 |     figma.currentPage.appendChild(rect);
 235 |   }
 236 | 
 237 |   return {
 238 |     id: rect.id,
 239 |     name: rect.name,
 240 |     x: rect.x,
 241 |     y: rect.y,
 242 |     width: rect.width,
 243 |     height: rect.height,
 244 |     parentId: rect.parent ? rect.parent.id : undefined,
 245 |   };
 246 | }
 247 | 
 248 | async function createFrame(params) {
 249 |   const {
 250 |     x = 0,
 251 |     y = 0,
 252 |     width = 100,
 253 |     height = 100,
 254 |     name = "Frame",
 255 |     parentId,
 256 |   } = params || {};
 257 | 
 258 |   const frame = figma.createFrame();
 259 |   frame.x = x;
 260 |   frame.y = y;
 261 |   frame.resize(width, height);
 262 |   frame.name = name;
 263 | 
 264 |   // If parentId is provided, append to that node, otherwise append to current page
 265 |   if (parentId) {
 266 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 267 |     if (!parentNode) {
 268 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 269 |     }
 270 |     if (!("appendChild" in parentNode)) {
 271 |       throw new Error(`Parent node does not support children: ${parentId}`);
 272 |     }
 273 |     parentNode.appendChild(frame);
 274 |   } else {
 275 |     figma.currentPage.appendChild(frame);
 276 |   }
 277 | 
 278 |   return {
 279 |     id: frame.id,
 280 |     name: frame.name,
 281 |     x: frame.x,
 282 |     y: frame.y,
 283 |     width: frame.width,
 284 |     height: frame.height,
 285 |     parentId: frame.parent ? frame.parent.id : undefined,
 286 |   };
 287 | }
 288 | 
 289 | async function createText(params) {
 290 |   const {
 291 |     x = 0,
 292 |     y = 0,
 293 |     text = "Text",
 294 |     fontSize = 14,
 295 |     fontWeight = 400,
 296 |     fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black
 297 |     name = "Text",
 298 |     parentId,
 299 |   } = params || {};
 300 | 
 301 |   // Map common font weights to Figma font styles
 302 |   const getFontStyle = (weight) => {
 303 |     switch (weight) {
 304 |       case 100:
 305 |         return "Thin";
 306 |       case 200:
 307 |         return "Extra Light";
 308 |       case 300:
 309 |         return "Light";
 310 |       case 400:
 311 |         return "Regular";
 312 |       case 500:
 313 |         return "Medium";
 314 |       case 600:
 315 |         return "Semi Bold";
 316 |       case 700:
 317 |         return "Bold";
 318 |       case 800:
 319 |         return "Extra Bold";
 320 |       case 900:
 321 |         return "Black";
 322 |       default:
 323 |         return "Regular";
 324 |     }
 325 |   };
 326 | 
 327 |   const textNode = figma.createText();
 328 |   textNode.x = x;
 329 |   textNode.y = y;
 330 |   textNode.name = name;
 331 |   try {
 332 |     await figma.loadFontAsync({
 333 |       family: "Inter",
 334 |       style: getFontStyle(fontWeight),
 335 |     });
 336 |     textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
 337 |     textNode.fontSize = parseInt(fontSize);
 338 |   } catch (error) {
 339 |     console.error("Error setting font size", error);
 340 |   }
 341 |   setCharacters(textNode, text);
 342 | 
 343 |   // Set text color
 344 |   const paintStyle = {
 345 |     type: "SOLID",
 346 |     color: {
 347 |       r: parseFloat(fontColor.r) || 0,
 348 |       g: parseFloat(fontColor.g) || 0,
 349 |       b: parseFloat(fontColor.b) || 0,
 350 |     },
 351 |     opacity: parseFloat(fontColor.a) || 1,
 352 |   };
 353 |   textNode.fills = [paintStyle];
 354 | 
 355 |   // If parentId is provided, append to that node, otherwise append to current page
 356 |   if (parentId) {
 357 |     const parentNode = await figma.getNodeByIdAsync(parentId);
 358 |     if (!parentNode) {
 359 |       throw new Error(`Parent node not found with ID: ${parentId}`);
 360 |     }
 361 |     if (!("appendChild" in parentNode)) {
 362 |       throw new Error(`Parent node does not support children: ${parentId}`);
 363 |     }
 364 |     parentNode.appendChild(textNode);
 365 |   } else {
 366 |     figma.currentPage.appendChild(textNode);
 367 |   }
 368 | 
 369 |   return {
 370 |     id: textNode.id,
 371 |     name: textNode.name,
 372 |     x: textNode.x,
 373 |     y: textNode.y,
 374 |     width: textNode.width,
 375 |     height: textNode.height,
 376 |     characters: textNode.characters,
 377 |     fontSize: textNode.fontSize,
 378 |     fontWeight: fontWeight,
 379 |     fontColor: fontColor,
 380 |     fontName: textNode.fontName,
 381 |     fills: textNode.fills,
 382 |     parentId: textNode.parent ? textNode.parent.id : undefined,
 383 |   };
 384 | }
 385 | 
 386 | async function setFillColor(params) {
 387 |   console.log("setFillColor", params);
 388 |   const {
 389 |     nodeId,
 390 |     color: { r, g, b, a },
 391 |   } = params || {};
 392 | 
 393 |   if (!nodeId) {
 394 |     throw new Error("Missing nodeId parameter");
 395 |   }
 396 | 
 397 |   const node = await figma.getNodeByIdAsync(nodeId);
 398 |   if (!node) {
 399 |     throw new Error(`Node not found with ID: ${nodeId}`);
 400 |   }
 401 | 
 402 |   if (!("fills" in node)) {
 403 |     throw new Error(`Node does not support fills: ${nodeId}`);
 404 |   }
 405 | 
 406 |   // Create RGBA color
 407 |   const rgbColor = {
 408 |     r: parseFloat(r) || 0,
 409 |     g: parseFloat(g) || 0,
 410 |     b: parseFloat(b) || 0,
 411 |     a: parseFloat(a) || 1,
 412 |   };
 413 | 
 414 |   // Set fill
 415 |   const paintStyle = {
 416 |     type: "SOLID",
 417 |     color: {
 418 |       r: parseFloat(rgbColor.r),
 419 |       g: parseFloat(rgbColor.g),
 420 |       b: parseFloat(rgbColor.b),
 421 |     },
 422 |     opacity: parseFloat(rgbColor.a),
 423 |   };
 424 | 
 425 |   console.log("paintStyle", paintStyle);
 426 | 
 427 |   node.fills = [paintStyle];
 428 | 
 429 |   return {
 430 |     id: node.id,
 431 |     name: node.name,
 432 |     fills: [paintStyle],
 433 |   };
 434 | }
 435 | 
 436 | async function setStrokeColor(params) {
 437 |   const {
 438 |     nodeId,
 439 |     color: { r, g, b, a },
 440 |     weight = 1,
 441 |   } = params || {};
 442 | 
 443 |   if (!nodeId) {
 444 |     throw new Error("Missing nodeId parameter");
 445 |   }
 446 | 
 447 |   const node = await figma.getNodeByIdAsync(nodeId);
 448 |   if (!node) {
 449 |     throw new Error(`Node not found with ID: ${nodeId}`);
 450 |   }
 451 | 
 452 |   if (!("strokes" in node)) {
 453 |     throw new Error(`Node does not support strokes: ${nodeId}`);
 454 |   }
 455 | 
 456 |   // Create RGBA color
 457 |   const rgbColor = {
 458 |     r: r !== undefined ? r : 0,
 459 |     g: g !== undefined ? g : 0,
 460 |     b: b !== undefined ? b : 0,
 461 |     a: a !== undefined ? a : 1,
 462 |   };
 463 | 
 464 |   // Set stroke
 465 |   const paintStyle = {
 466 |     type: "SOLID",
 467 |     color: {
 468 |       r: rgbColor.r,
 469 |       g: rgbColor.g,
 470 |       b: rgbColor.b,
 471 |     },
 472 |     opacity: rgbColor.a,
 473 |   };
 474 | 
 475 |   node.strokes = [paintStyle];
 476 | 
 477 |   // Set stroke weight if available
 478 |   if ("strokeWeight" in node) {
 479 |     node.strokeWeight = weight;
 480 |   }
 481 | 
 482 |   return {
 483 |     id: node.id,
 484 |     name: node.name,
 485 |     strokes: node.strokes,
 486 |     strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined,
 487 |   };
 488 | }
 489 | 
 490 | async function moveNode(params) {
 491 |   const { nodeId, x, y } = params || {};
 492 | 
 493 |   if (!nodeId) {
 494 |     throw new Error("Missing nodeId parameter");
 495 |   }
 496 | 
 497 |   if (x === undefined || y === undefined) {
 498 |     throw new Error("Missing x or y parameters");
 499 |   }
 500 | 
 501 |   const node = await figma.getNodeByIdAsync(nodeId);
 502 |   if (!node) {
 503 |     throw new Error(`Node not found with ID: ${nodeId}`);
 504 |   }
 505 | 
 506 |   if (!("x" in node) || !("y" in node)) {
 507 |     throw new Error(`Node does not support position: ${nodeId}`);
 508 |   }
 509 | 
 510 |   node.x = x;
 511 |   node.y = y;
 512 | 
 513 |   return {
 514 |     id: node.id,
 515 |     name: node.name,
 516 |     x: node.x,
 517 |     y: node.y,
 518 |   };
 519 | }
 520 | 
 521 | async function resizeNode(params) {
 522 |   const { nodeId, width, height } = params || {};
 523 | 
 524 |   if (!nodeId) {
 525 |     throw new Error("Missing nodeId parameter");
 526 |   }
 527 | 
 528 |   if (width === undefined || height === undefined) {
 529 |     throw new Error("Missing width or height parameters");
 530 |   }
 531 | 
 532 |   const node = await figma.getNodeByIdAsync(nodeId);
 533 |   if (!node) {
 534 |     throw new Error(`Node not found with ID: ${nodeId}`);
 535 |   }
 536 | 
 537 |   if (!("resize" in node)) {
 538 |     throw new Error(`Node does not support resizing: ${nodeId}`);
 539 |   }
 540 | 
 541 |   node.resize(width, height);
 542 | 
 543 |   return {
 544 |     id: node.id,
 545 |     name: node.name,
 546 |     width: node.width,
 547 |     height: node.height,
 548 |   };
 549 | }
 550 | 
 551 | async function deleteNode(params) {
 552 |   const { nodeId } = params || {};
 553 | 
 554 |   if (!nodeId) {
 555 |     throw new Error("Missing nodeId parameter");
 556 |   }
 557 | 
 558 |   const node = await figma.getNodeByIdAsync(nodeId);
 559 |   if (!node) {
 560 |     throw new Error(`Node not found with ID: ${nodeId}`);
 561 |   }
 562 | 
 563 |   // Save node info before deleting
 564 |   const nodeInfo = {
 565 |     id: node.id,
 566 |     name: node.name,
 567 |     type: node.type,
 568 |   };
 569 | 
 570 |   node.remove();
 571 | 
 572 |   return nodeInfo;
 573 | }
 574 | 
 575 | async function getStyles() {
 576 |   const styles = {
 577 |     colors: await figma.getLocalPaintStylesAsync(),
 578 |     texts: await figma.getLocalTextStylesAsync(),
 579 |     effects: await figma.getLocalEffectStylesAsync(),
 580 |     grids: await figma.getLocalGridStylesAsync(),
 581 |   };
 582 | 
 583 |   return {
 584 |     colors: styles.colors.map((style) => ({
 585 |       id: style.id,
 586 |       name: style.name,
 587 |       key: style.key,
 588 |       paint: style.paints[0],
 589 |     })),
 590 |     texts: styles.texts.map((style) => ({
 591 |       id: style.id,
 592 |       name: style.name,
 593 |       key: style.key,
 594 |       fontSize: style.fontSize,
 595 |       fontName: style.fontName,
 596 |     })),
 597 |     effects: styles.effects.map((style) => ({
 598 |       id: style.id,
 599 |       name: style.name,
 600 |       key: style.key,
 601 |     })),
 602 |     grids: styles.grids.map((style) => ({
 603 |       id: style.id,
 604 |       name: style.name,
 605 |       key: style.key,
 606 |     })),
 607 |   };
 608 | }
 609 | 
 610 | async function getLocalComponents() {
 611 |   const components = figma.root.findAllWithCriteria({
 612 |     types: ["COMPONENT"],
 613 |   });
 614 | 
 615 |   return {
 616 |     count: components.length,
 617 |     components: components.map((component) => ({
 618 |       id: component.id,
 619 |       name: component.name,
 620 |       key: "key" in component ? component.key : null,
 621 |     })),
 622 |   };
 623 | }
 624 | 
 625 | async function getTeamComponents() {
 626 |   try {
 627 |     const teamComponents =
 628 |       await figma.teamLibrary.getAvailableComponentsAsync();
 629 | 
 630 |     return {
 631 |       count: teamComponents.length,
 632 |       components: teamComponents.map((component) => ({
 633 |         key: component.key,
 634 |         name: component.name,
 635 |         description: component.description,
 636 |         libraryName: component.libraryName,
 637 |       })),
 638 |     };
 639 |   } catch (error) {
 640 |     throw new Error(`Error getting team components: ${error.message}`);
 641 |   }
 642 | }
 643 | 
 644 | async function createComponentInstance(params) {
 645 |   const { componentKey, x = 0, y = 0 } = params || {};
 646 | 
 647 |   if (!componentKey) {
 648 |     throw new Error("Missing componentKey parameter");
 649 |   }
 650 | 
 651 |   try {
 652 |     const component = await figma.importComponentByKeyAsync(componentKey);
 653 |     const instance = component.createInstance();
 654 | 
 655 |     instance.x = x;
 656 |     instance.y = y;
 657 | 
 658 |     figma.currentPage.appendChild(instance);
 659 | 
 660 |     return {
 661 |       id: instance.id,
 662 |       name: instance.name,
 663 |       x: instance.x,
 664 |       y: instance.y,
 665 |       width: instance.width,
 666 |       height: instance.height,
 667 |       componentId: instance.componentId,
 668 |     };
 669 |   } catch (error) {
 670 |     throw new Error(`Error creating component instance: ${error.message}`);
 671 |   }
 672 | }
 673 | 
 674 | async function exportNodeAsImage(params) {
 675 |   const { nodeId, format = "PNG", scale = 1 } = params || {};
 676 | 
 677 |   if (!nodeId) {
 678 |     throw new Error("Missing nodeId parameter");
 679 |   }
 680 | 
 681 |   const node = await figma.getNodeByIdAsync(nodeId);
 682 |   if (!node) {
 683 |     throw new Error(`Node not found with ID: ${nodeId}`);
 684 |   }
 685 | 
 686 |   if (!("exportAsync" in node)) {
 687 |     throw new Error(`Node does not support exporting: ${nodeId}`);
 688 |   }
 689 | 
 690 |   try {
 691 |     const settings = {
 692 |       format: format,
 693 |       constraint: { type: "SCALE", value: scale },
 694 |     };
 695 | 
 696 |     const bytes = await node.exportAsync(settings);
 697 | 
 698 |     let mimeType;
 699 |     switch (format) {
 700 |       case "PNG":
 701 |         mimeType = "image/png";
 702 |         break;
 703 |       case "JPG":
 704 |         mimeType = "image/jpeg";
 705 |         break;
 706 |       case "SVG":
 707 |         mimeType = "image/svg+xml";
 708 |         break;
 709 |       case "PDF":
 710 |         mimeType = "application/pdf";
 711 |         break;
 712 |       default:
 713 |         mimeType = "application/octet-stream";
 714 |     }
 715 | 
 716 |     // Convert to base64
 717 |     const uint8Array = new Uint8Array(bytes);
 718 |     let binary = "";
 719 |     for (let i = 0; i < uint8Array.length; i++) {
 720 |       binary += String.fromCharCode(uint8Array[i]);
 721 |     }
 722 |     const base64 = btoa(binary);
 723 |     const imageData = `data:${mimeType};base64,${base64}`;
 724 | 
 725 |     return {
 726 |       nodeId,
 727 |       format,
 728 |       scale,
 729 |       mimeType,
 730 |       imageData,
 731 |     };
 732 |   } catch (error) {
 733 |     throw new Error(`Error exporting node as image: ${error.message}`);
 734 |   }
 735 | }
 736 | 
 737 | async function executeCode(params) {
 738 |   const { code } = params || {};
 739 | 
 740 |   if (!code) {
 741 |     throw new Error("Missing code parameter");
 742 |   }
 743 | 
 744 |   try {
 745 |     // Execute the provided code
 746 |     // Note: This is potentially unsafe, but matches the Blender MCP functionality
 747 |     const executeFn = new Function(
 748 |       "figma",
 749 |       "selection",
 750 |       `
 751 |       try {
 752 |         const result = (async () => {
 753 |           ${code}
 754 |         })();
 755 |         return result;
 756 |       } catch (error) {
 757 |         throw new Error('Error executing code: ' + error.message);
 758 |       }
 759 |     `
 760 |     );
 761 | 
 762 |     const result = await executeFn(figma, figma.currentPage.selection);
 763 |     return { result };
 764 |   } catch (error) {
 765 |     throw new Error(`Error executing code: ${error.message}`);
 766 |   }
 767 | }
 768 | 
 769 | async function setCornerRadius(params) {
 770 |   const { nodeId, radius, corners } = params || {};
 771 | 
 772 |   if (!nodeId) {
 773 |     throw new Error("Missing nodeId parameter");
 774 |   }
 775 | 
 776 |   if (radius === undefined) {
 777 |     throw new Error("Missing radius parameter");
 778 |   }
 779 | 
 780 |   const node = await figma.getNodeByIdAsync(nodeId);
 781 |   if (!node) {
 782 |     throw new Error(`Node not found with ID: ${nodeId}`);
 783 |   }
 784 | 
 785 |   // Check if node supports corner radius
 786 |   if (!("cornerRadius" in node)) {
 787 |     throw new Error(`Node does not support corner radius: ${nodeId}`);
 788 |   }
 789 | 
 790 |   // If corners array is provided, set individual corner radii
 791 |   if (corners && Array.isArray(corners) && corners.length === 4) {
 792 |     if ("topLeftRadius" in node) {
 793 |       // Node supports individual corner radii
 794 |       if (corners[0]) node.topLeftRadius = radius;
 795 |       if (corners[1]) node.topRightRadius = radius;
 796 |       if (corners[2]) node.bottomRightRadius = radius;
 797 |       if (corners[3]) node.bottomLeftRadius = radius;
 798 |     } else {
 799 |       // Node only supports uniform corner radius
 800 |       node.cornerRadius = radius;
 801 |     }
 802 |   } else {
 803 |     // Set uniform corner radius
 804 |     node.cornerRadius = radius;
 805 |   }
 806 | 
 807 |   return {
 808 |     id: node.id,
 809 |     name: node.name,
 810 |     cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined,
 811 |     topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined,
 812 |     topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined,
 813 |     bottomRightRadius:
 814 |       "bottomRightRadius" in node ? node.bottomRightRadius : undefined,
 815 |     bottomLeftRadius:
 816 |       "bottomLeftRadius" in node ? node.bottomLeftRadius : undefined,
 817 |   };
 818 | }
 819 | 
 820 | // Initialize settings on load
 821 | (async function initializePlugin() {
 822 |   try {
 823 |     const savedSettings = await figma.clientStorage.getAsync("settings");
 824 |     if (savedSettings) {
 825 |       if (savedSettings.serverPort) {
 826 |         state.serverPort = savedSettings.serverPort;
 827 |       }
 828 |     }
 829 | 
 830 |     // Send initial settings to UI
 831 |     figma.ui.postMessage({
 832 |       type: "init-settings",
 833 |       settings: {
 834 |         serverPort: state.serverPort,
 835 |       },
 836 |     });
 837 |   } catch (error) {
 838 |     console.error("Error loading settings:", error);
 839 |   }
 840 | })();
 841 | 
 842 | function uniqBy(arr, predicate) {
 843 |   const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
 844 |   return [
 845 |     ...arr
 846 |       .reduce((map, item) => {
 847 |         const key = item === null || item === undefined ? item : cb(item);
 848 | 
 849 |         map.has(key) || map.set(key, item);
 850 | 
 851 |         return map;
 852 |       }, new Map())
 853 |       .values(),
 854 |   ];
 855 | }
 856 | const setCharacters = async (node, characters, options) => {
 857 |   const fallbackFont = (options && options.fallbackFont) || {
 858 |     family: "Inter",
 859 |     style: "Regular",
 860 |   };
 861 |   try {
 862 |     if (node.fontName === figma.mixed) {
 863 |       if (options && options.smartStrategy === "prevail") {
 864 |         const fontHashTree = {};
 865 |         for (let i = 1; i < node.characters.length; i++) {
 866 |           const charFont = node.getRangeFontName(i - 1, i);
 867 |           const key = `${charFont.family}::${charFont.style}`;
 868 |           fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
 869 |         }
 870 |         const prevailedTreeItem = Object.entries(fontHashTree).sort(
 871 |           (a, b) => b[1] - a[1]
 872 |         )[0];
 873 |         const [family, style] = prevailedTreeItem[0].split("::");
 874 |         const prevailedFont = {
 875 |           family,
 876 |           style,
 877 |         };
 878 |         await figma.loadFontAsync(prevailedFont);
 879 |         node.fontName = prevailedFont;
 880 |       } else if (options && options.smartStrategy === "strict") {
 881 |         return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
 882 |       } else if (options && options.smartStrategy === "experimental") {
 883 |         return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
 884 |       } else {
 885 |         const firstCharFont = node.getRangeFontName(0, 1);
 886 |         await figma.loadFontAsync(firstCharFont);
 887 |         node.fontName = firstCharFont;
 888 |       }
 889 |     } else {
 890 |       await figma.loadFontAsync({
 891 |         family: node.fontName.family,
 892 |         style: node.fontName.style,
 893 |       });
 894 |     }
 895 |   } catch (err) {
 896 |     console.warn(
 897 |       `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
 898 |       err
 899 |     );
 900 |     await figma.loadFontAsync(fallbackFont);
 901 |     node.fontName = fallbackFont;
 902 |   }
 903 |   try {
 904 |     node.characters = characters;
 905 |     return true;
 906 |   } catch (err) {
 907 |     console.warn(`Failed to set characters. Skipped.`, err);
 908 |     return false;
 909 |   }
 910 | };
 911 | 
 912 | const setCharactersWithStrictMatchFont = async (
 913 |   node,
 914 |   characters,
 915 |   fallbackFont
 916 | ) => {
 917 |   const fontHashTree = {};
 918 |   for (let i = 1; i < node.characters.length; i++) {
 919 |     const startIdx = i - 1;
 920 |     const startCharFont = node.getRangeFontName(startIdx, i);
 921 |     const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
 922 |     while (i < node.characters.length) {
 923 |       i++;
 924 |       const charFont = node.getRangeFontName(i - 1, i);
 925 |       if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
 926 |         break;
 927 |       }
 928 |     }
 929 |     fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
 930 |   }
 931 |   await figma.loadFontAsync(fallbackFont);
 932 |   node.fontName = fallbackFont;
 933 |   node.characters = characters;
 934 |   console.log(fontHashTree);
 935 |   await Promise.all(
 936 |     Object.keys(fontHashTree).map(async (range) => {
 937 |       console.log(range, fontHashTree[range]);
 938 |       const [start, end] = range.split("_");
 939 |       const [family, style] = fontHashTree[range].split("::");
 940 |       const matchedFont = {
 941 |         family,
 942 |         style,
 943 |       };
 944 |       await figma.loadFontAsync(matchedFont);
 945 |       return node.setRangeFontName(Number(start), Number(end), matchedFont);
 946 |     })
 947 |   );
 948 |   return true;
 949 | };
 950 | 
 951 | const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
 952 |   const indices = [];
 953 |   let temp = startIdx;
 954 |   for (let i = startIdx; i < endIdx; i++) {
 955 |     if (
 956 |       str[i] === delimiter &&
 957 |       i + startIdx !== endIdx &&
 958 |       temp !== i + startIdx
 959 |     ) {
 960 |       indices.push([temp, i + startIdx]);
 961 |       temp = i + startIdx + 1;
 962 |     }
 963 |   }
 964 |   temp !== endIdx && indices.push([temp, endIdx]);
 965 |   return indices.filter(Boolean);
 966 | };
 967 | 
 968 | const buildLinearOrder = (node) => {
 969 |   const fontTree = [];
 970 |   const newLinesPos = getDelimiterPos(node.characters, "\n");
 971 |   newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
 972 |     const newLinesRangeFont = node.getRangeFontName(
 973 |       newLinesRangeStart,
 974 |       newLinesRangeEnd
 975 |     );
 976 |     if (newLinesRangeFont === figma.mixed) {
 977 |       const spacesPos = getDelimiterPos(
 978 |         node.characters,
 979 |         " ",
 980 |         newLinesRangeStart,
 981 |         newLinesRangeEnd
 982 |       );
 983 |       spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
 984 |         const spacesRangeFont = node.getRangeFontName(
 985 |           spacesRangeStart,
 986 |           spacesRangeEnd
 987 |         );
 988 |         if (spacesRangeFont === figma.mixed) {
 989 |           const spacesRangeFont = node.getRangeFontName(
 990 |             spacesRangeStart,
 991 |             spacesRangeStart[0]
 992 |           );
 993 |           fontTree.push({
 994 |             start: spacesRangeStart,
 995 |             delimiter: " ",
 996 |             family: spacesRangeFont.family,
 997 |             style: spacesRangeFont.style,
 998 |           });
 999 |         } else {
1000 |           fontTree.push({
1001 |             start: spacesRangeStart,
1002 |             delimiter: " ",
1003 |             family: spacesRangeFont.family,
1004 |             style: spacesRangeFont.style,
1005 |           });
1006 |         }
1007 |       });
1008 |     } else {
1009 |       fontTree.push({
1010 |         start: newLinesRangeStart,
1011 |         delimiter: "\n",
1012 |         family: newLinesRangeFont.family,
1013 |         style: newLinesRangeFont.style,
1014 |       });
1015 |     }
1016 |   });
1017 |   return fontTree
1018 |     .sort((a, b) => +a.start - +b.start)
1019 |     .map(({ family, style, delimiter }) => ({ family, style, delimiter }));
1020 | };
1021 | 
1022 | const setCharactersWithSmartMatchFont = async (
1023 |   node,
1024 |   characters,
1025 |   fallbackFont
1026 | ) => {
1027 |   const rangeTree = buildLinearOrder(node);
1028 |   const fontsToLoad = uniqBy(
1029 |     rangeTree,
1030 |     ({ family, style }) => `${family}::${style}`
1031 |   ).map(({ family, style }) => ({
1032 |     family,
1033 |     style,
1034 |   }));
1035 | 
1036 |   await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
1037 | 
1038 |   node.fontName = fallbackFont;
1039 |   node.characters = characters;
1040 | 
1041 |   let prevPos = 0;
1042 |   rangeTree.forEach(({ family, style, delimiter }) => {
1043 |     if (prevPos < node.characters.length) {
1044 |       const delimeterPos = node.characters.indexOf(delimiter, prevPos);
1045 |       const endPos =
1046 |         delimeterPos > prevPos ? delimeterPos : node.characters.length;
1047 |       const matchedFont = {
1048 |         family,
1049 |         style,
1050 |       };
1051 |       node.setRangeFontName(prevPos, endPos, matchedFont);
1052 |       prevPos = endPos + 1;
1053 |     }
1054 |   });
1055 |   return true;
1056 | };
1057 | 
```

--------------------------------------------------------------------------------
/src/talk_to_figma_mcp/server.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
   2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   3 | import { z } from "zod";
   4 | import WebSocket from 'ws';
   5 | import { v4 as uuidv4 } from 'uuid';
   6 | 
   7 | // Define TypeScript interfaces for Figma responses
   8 | interface FigmaResponse {
   9 |   id: string;
  10 |   result?: any;
  11 |   error?: string;
  12 | }
  13 | 
  14 | // WebSocket connection and request tracking
  15 | let ws: WebSocket | null = null;
  16 | const pendingRequests = new Map<string, {
  17 |   resolve: (value: unknown) => void;
  18 |   reject: (reason: unknown) => void;
  19 |   timeout: NodeJS.Timeout;
  20 | }>();
  21 | 
  22 | // Track which channel each client is in
  23 | let currentChannel: string | null = null;
  24 | 
  25 | // Create MCP server
  26 | const server = new McpServer({
  27 |   name: "TalkToFigmaMCP",
  28 |   version: "1.0.0",
  29 | });
  30 | 
  31 | // Document Info Tool
  32 | server.tool(
  33 |   "get_document_info",
  34 |   "Get detailed information about the current Figma document",
  35 |   {},
  36 |   async () => {
  37 |     try {
  38 |       const result = await sendCommandToFigma('get_document_info');
  39 |       return {
  40 |         content: [
  41 |           {
  42 |             type: "text",
  43 |             text: JSON.stringify(result, null, 2)
  44 |           }
  45 |         ]
  46 |       };
  47 |     } catch (error) {
  48 |       return {
  49 |         content: [
  50 |           {
  51 |             type: "text",
  52 |             text: `Error getting document info: ${error instanceof Error ? error.message : String(error)}`
  53 |           }
  54 |         ]
  55 |       };
  56 |     }
  57 |   }
  58 | );
  59 | 
  60 | // Selection Tool
  61 | server.tool(
  62 |   "get_selection",
  63 |   "Get information about the current selection in Figma",
  64 |   {},
  65 |   async () => {
  66 |     try {
  67 |       const result = await sendCommandToFigma('get_selection');
  68 |       return {
  69 |         content: [
  70 |           {
  71 |             type: "text",
  72 |             text: JSON.stringify(result, null, 2)
  73 |           }
  74 |         ]
  75 |       };
  76 |     } catch (error) {
  77 |       return {
  78 |         content: [
  79 |           {
  80 |             type: "text",
  81 |             text: `Error getting selection: ${error instanceof Error ? error.message : String(error)}`
  82 |           }
  83 |         ]
  84 |       };
  85 |     }
  86 |   }
  87 | );
  88 | 
  89 | // Node Info Tool
  90 | server.tool(
  91 |   "get_node_info",
  92 |   "Get detailed information about a specific node in Figma",
  93 |   {
  94 |     nodeId: z.string().describe("The ID of the node to get information about")
  95 |   },
  96 |   async ({ nodeId }) => {
  97 |     try {
  98 |       const result = await sendCommandToFigma('get_node_info', { nodeId });
  99 |       return {
 100 |         content: [
 101 |           {
 102 |             type: "text",
 103 |             text: JSON.stringify(result, null, 2)
 104 |           }
 105 |         ]
 106 |       };
 107 |     } catch (error) {
 108 |       return {
 109 |         content: [
 110 |           {
 111 |             type: "text",
 112 |             text: `Error getting node info: ${error instanceof Error ? error.message : String(error)}`
 113 |           }
 114 |         ]
 115 |       };
 116 |     }
 117 |   }
 118 | );
 119 | 
 120 | // Create Rectangle Tool
 121 | server.tool(
 122 |   "create_rectangle",
 123 |   "Create a new rectangle in Figma",
 124 |   {
 125 |     x: z.number().describe("X position"),
 126 |     y: z.number().describe("Y position"),
 127 |     width: z.number().describe("Width of the rectangle"),
 128 |     height: z.number().describe("Height of the rectangle"),
 129 |     name: z.string().optional().describe("Optional name for the rectangle"),
 130 |     parentId: z.string().optional().describe("Optional parent node ID to append the rectangle to")
 131 |   },
 132 |   async ({ x, y, width, height, name, parentId }) => {
 133 |     try {
 134 |       const result = await sendCommandToFigma('create_rectangle', {
 135 |         x, y, width, height, name: name || 'Rectangle', parentId
 136 |       });
 137 |       return {
 138 |         content: [
 139 |           {
 140 |             type: "text",
 141 |             text: `Created rectangle "${JSON.stringify(result)}"`
 142 |           }
 143 |         ]
 144 |       }
 145 |     } catch (error) {
 146 |       return {
 147 |         content: [
 148 |           {
 149 |             type: "text",
 150 |             text: `Error creating rectangle: ${error instanceof Error ? error.message : String(error)}`
 151 |           }
 152 |         ]
 153 |       };
 154 |     }
 155 |   }
 156 | );
 157 | 
 158 | // Create Frame Tool
 159 | server.tool(
 160 |   "create_frame",
 161 |   "Create a new frame in Figma",
 162 |   {
 163 |     x: z.number().describe("X position"),
 164 |     y: z.number().describe("Y position"),
 165 |     width: z.number().describe("Width of the frame"),
 166 |     height: z.number().describe("Height of the frame"),
 167 |     name: z.string().optional().describe("Optional name for the frame"),
 168 |     parentId: z.string().optional().describe("Optional parent node ID to append the frame to"),
 169 |     fillColor: z.object({
 170 |       r: z.number().min(0).max(1).describe("Red component (0-1)"),
 171 |       g: z.number().min(0).max(1).describe("Green component (0-1)"),
 172 |       b: z.number().min(0).max(1).describe("Blue component (0-1)"),
 173 |       a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
 174 |     }).optional().describe("Fill color in RGBA format"),
 175 |     strokeColor: z.object({
 176 |       r: z.number().min(0).max(1).describe("Red component (0-1)"),
 177 |       g: z.number().min(0).max(1).describe("Green component (0-1)"),
 178 |       b: z.number().min(0).max(1).describe("Blue component (0-1)"),
 179 |       a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
 180 |     }).optional().describe("Stroke color in RGBA format"),
 181 |     strokeWeight: z.number().positive().optional().describe("Stroke weight")
 182 |   },
 183 |   async ({ x, y, width, height, name, parentId, fillColor, strokeColor, strokeWeight }) => {
 184 |     try {
 185 |       const result = await sendCommandToFigma('create_frame', {
 186 |         x, y, width, height, name: name || 'Frame', parentId,
 187 |         fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 },
 188 |         strokeColor: strokeColor,
 189 |         strokeWeight: strokeWeight
 190 |       });
 191 |       const typedResult = result as { name: string, id: string };
 192 |       return {
 193 |         content: [
 194 |           {
 195 |             type: "text",
 196 |             text: `Created frame "${typedResult.name}" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`
 197 |           }
 198 |         ]
 199 |       };
 200 |     } catch (error) {
 201 |       return {
 202 |         content: [
 203 |           {
 204 |             type: "text",
 205 |             text: `Error creating frame: ${error instanceof Error ? error.message : String(error)}`
 206 |           }
 207 |         ]
 208 |       };
 209 |     }
 210 |   }
 211 | );
 212 | 
 213 | // Create Text Tool
 214 | server.tool(
 215 |   "create_text",
 216 |   "Create a new text element in Figma",
 217 |   {
 218 |     x: z.number().describe("X position"),
 219 |     y: z.number().describe("Y position"),
 220 |     text: z.string().describe("Text content"),
 221 |     fontSize: z.number().optional().describe("Font size (default: 14)"),
 222 |     fontWeight: z.number().optional().describe("Font weight (e.g., 400 for Regular, 700 for Bold)"),
 223 |     fontColor: z.object({
 224 |       r: z.number().min(0).max(1).describe("Red component (0-1)"),
 225 |       g: z.number().min(0).max(1).describe("Green component (0-1)"),
 226 |       b: z.number().min(0).max(1).describe("Blue component (0-1)"),
 227 |       a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
 228 |     }).optional().describe("Font color in RGBA format"),
 229 |     name: z.string().optional().describe("Optional name for the text node by default following text"),
 230 |     parentId: z.string().optional().describe("Optional parent node ID to append the text to")
 231 |   },
 232 |   async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }) => {
 233 |     try {
 234 |       const result = await sendCommandToFigma('create_text', {
 235 |         x, y, text,
 236 |         fontSize: fontSize || 14,
 237 |         fontWeight: fontWeight || 400,
 238 |         fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 },
 239 |         name: name || 'Text',
 240 |         parentId
 241 |       });
 242 |       const typedResult = result as { name: string, id: string };
 243 |       return {
 244 |         content: [
 245 |           {
 246 |             type: "text",
 247 |             text: `Created text "${typedResult.name}" with ID: ${typedResult.id}`
 248 |           }
 249 |         ]
 250 |       };
 251 |     } catch (error) {
 252 |       return {
 253 |         content: [
 254 |           {
 255 |             type: "text",
 256 |             text: `Error creating text: ${error instanceof Error ? error.message : String(error)}`
 257 |           }
 258 |         ]
 259 |       };
 260 |     }
 261 |   }
 262 | );
 263 | 
 264 | // Set Fill Color Tool
 265 | server.tool(
 266 |   "set_fill_color",
 267 |   "Set the fill color of a node in Figma can be TextNode or FrameNode",
 268 |   {
 269 |     nodeId: z.string().describe("The ID of the node to modify"),
 270 |     r: z.number().min(0).max(1).describe("Red component (0-1)"),
 271 |     g: z.number().min(0).max(1).describe("Green component (0-1)"),
 272 |     b: z.number().min(0).max(1).describe("Blue component (0-1)"),
 273 |     a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)")
 274 |   },
 275 |   async ({ nodeId, r, g, b, a }) => {
 276 |     try {
 277 |       const result = await sendCommandToFigma('set_fill_color', {
 278 |         nodeId,
 279 |         color: { r, g, b, a: a || 1 }
 280 |       });
 281 |       const typedResult = result as { name: string };
 282 |       return {
 283 |         content: [
 284 |           {
 285 |             type: "text",
 286 |             text: `Set fill color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1})`
 287 |           }
 288 |         ]
 289 |       };
 290 |     } catch (error) {
 291 |       return {
 292 |         content: [
 293 |           {
 294 |             type: "text",
 295 |             text: `Error setting fill color: ${error instanceof Error ? error.message : String(error)}`
 296 |           }
 297 |         ]
 298 |       };
 299 |     }
 300 |   }
 301 | );
 302 | 
 303 | // Set Stroke Color Tool
 304 | server.tool(
 305 |   "set_stroke_color",
 306 |   "Set the stroke color of a node in Figma",
 307 |   {
 308 |     nodeId: z.string().describe("The ID of the node to modify"),
 309 |     r: z.number().min(0).max(1).describe("Red component (0-1)"),
 310 |     g: z.number().min(0).max(1).describe("Green component (0-1)"),
 311 |     b: z.number().min(0).max(1).describe("Blue component (0-1)"),
 312 |     a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"),
 313 |     weight: z.number().positive().optional().describe("Stroke weight")
 314 |   },
 315 |   async ({ nodeId, r, g, b, a, weight }) => {
 316 |     try {
 317 |       const result = await sendCommandToFigma('set_stroke_color', {
 318 |         nodeId,
 319 |         color: { r, g, b, a: a || 1 },
 320 |         weight: weight || 1
 321 |       });
 322 |       const typedResult = result as { name: string };
 323 |       return {
 324 |         content: [
 325 |           {
 326 |             type: "text",
 327 |             text: `Set stroke color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`
 328 |           }
 329 |         ]
 330 |       };
 331 |     } catch (error) {
 332 |       return {
 333 |         content: [
 334 |           {
 335 |             type: "text",
 336 |             text: `Error setting stroke color: ${error instanceof Error ? error.message : String(error)}`
 337 |           }
 338 |         ]
 339 |       };
 340 |     }
 341 |   }
 342 | );
 343 | 
 344 | // Move Node Tool
 345 | server.tool(
 346 |   "move_node",
 347 |   "Move a node to a new position in Figma",
 348 |   {
 349 |     nodeId: z.string().describe("The ID of the node to move"),
 350 |     x: z.number().describe("New X position"),
 351 |     y: z.number().describe("New Y position")
 352 |   },
 353 |   async ({ nodeId, x, y }) => {
 354 |     try {
 355 |       const result = await sendCommandToFigma('move_node', { nodeId, x, y });
 356 |       const typedResult = result as { name: string };
 357 |       return {
 358 |         content: [
 359 |           {
 360 |             type: "text",
 361 |             text: `Moved node "${typedResult.name}" to position (${x}, ${y})`
 362 |           }
 363 |         ]
 364 |       };
 365 |     } catch (error) {
 366 |       return {
 367 |         content: [
 368 |           {
 369 |             type: "text",
 370 |             text: `Error moving node: ${error instanceof Error ? error.message : String(error)}`
 371 |           }
 372 |         ]
 373 |       };
 374 |     }
 375 |   }
 376 | );
 377 | 
 378 | // Resize Node Tool
 379 | server.tool(
 380 |   "resize_node",
 381 |   "Resize a node in Figma",
 382 |   {
 383 |     nodeId: z.string().describe("The ID of the node to resize"),
 384 |     width: z.number().positive().describe("New width"),
 385 |     height: z.number().positive().describe("New height")
 386 |   },
 387 |   async ({ nodeId, width, height }) => {
 388 |     try {
 389 |       const result = await sendCommandToFigma('resize_node', { nodeId, width, height });
 390 |       const typedResult = result as { name: string };
 391 |       return {
 392 |         content: [
 393 |           {
 394 |             type: "text",
 395 |             text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`
 396 |           }
 397 |         ]
 398 |       };
 399 |     } catch (error) {
 400 |       return {
 401 |         content: [
 402 |           {
 403 |             type: "text",
 404 |             text: `Error resizing node: ${error instanceof Error ? error.message : String(error)}`
 405 |           }
 406 |         ]
 407 |       };
 408 |     }
 409 |   }
 410 | );
 411 | 
 412 | // Delete Node Tool
 413 | server.tool(
 414 |   "delete_node",
 415 |   "Delete a node from Figma",
 416 |   {
 417 |     nodeId: z.string().describe("The ID of the node to delete")
 418 |   },
 419 |   async ({ nodeId }) => {
 420 |     try {
 421 |       await sendCommandToFigma('delete_node', { nodeId });
 422 |       return {
 423 |         content: [
 424 |           {
 425 |             type: "text",
 426 |             text: `Deleted node with ID: ${nodeId}`
 427 |           }
 428 |         ]
 429 |       };
 430 |     } catch (error) {
 431 |       return {
 432 |         content: [
 433 |           {
 434 |             type: "text",
 435 |             text: `Error deleting node: ${error instanceof Error ? error.message : String(error)}`
 436 |           }
 437 |         ]
 438 |       };
 439 |     }
 440 |   }
 441 | );
 442 | 
 443 | // Get Styles Tool
 444 | server.tool(
 445 |   "get_styles",
 446 |   "Get all styles from the current Figma document",
 447 |   {},
 448 |   async () => {
 449 |     try {
 450 |       const result = await sendCommandToFigma('get_styles');
 451 |       return {
 452 |         content: [
 453 |           {
 454 |             type: "text",
 455 |             text: JSON.stringify(result, null, 2)
 456 |           }
 457 |         ]
 458 |       };
 459 |     } catch (error) {
 460 |       return {
 461 |         content: [
 462 |           {
 463 |             type: "text",
 464 |             text: `Error getting styles: ${error instanceof Error ? error.message : String(error)}`
 465 |           }
 466 |         ]
 467 |       };
 468 |     }
 469 |   }
 470 | );
 471 | 
 472 | // Get Local Components Tool
 473 | server.tool(
 474 |   "get_local_components",
 475 |   "Get all local components from the Figma document",
 476 |   {},
 477 |   async () => {
 478 |     try {
 479 |       const result = await sendCommandToFigma('get_local_components');
 480 |       return {
 481 |         content: [
 482 |           {
 483 |             type: "text",
 484 |             text: JSON.stringify(result, null, 2)
 485 |           }
 486 |         ]
 487 |       };
 488 |     } catch (error) {
 489 |       return {
 490 |         content: [
 491 |           {
 492 |             type: "text",
 493 |             text: `Error getting local components: ${error instanceof Error ? error.message : String(error)}`
 494 |           }
 495 |         ]
 496 |       };
 497 |     }
 498 |   }
 499 | );
 500 | 
 501 | // Get Team Components Tool
 502 | // server.tool(
 503 | //   "get_team_components",
 504 | //   "Get all team library components available in Figma",
 505 | //   {},
 506 | //   async () => {
 507 | //     try {
 508 | //       const result = await sendCommandToFigma('get_team_components');
 509 | //       return {
 510 | //         content: [
 511 | //           {
 512 | //             type: "text",
 513 | //             text: JSON.stringify(result, null, 2)
 514 | //           }
 515 | //         ]
 516 | //       };
 517 | //     } catch (error) {
 518 | //       return {
 519 | //         content: [
 520 | //           {
 521 | //             type: "text",
 522 | //             text: `Error getting team components: ${error instanceof Error ? error.message : String(error)}`
 523 | //           }
 524 | //         ]
 525 | //       };
 526 | //     }
 527 | //   }
 528 | // );
 529 | 
 530 | // Create Component Instance Tool
 531 | server.tool(
 532 |   "create_component_instance",
 533 |   "Create an instance of a component in Figma",
 534 |   {
 535 |     componentKey: z.string().describe("Key of the component to instantiate"),
 536 |     x: z.number().describe("X position"),
 537 |     y: z.number().describe("Y position")
 538 |   },
 539 |   async ({ componentKey, x, y }) => {
 540 |     try {
 541 |       const result = await sendCommandToFigma('create_component_instance', { componentKey, x, y });
 542 |       const typedResult = result as { name: string, id: string };
 543 |       return {
 544 |         content: [
 545 |           {
 546 |             type: "text",
 547 |             text: `Created component instance "${typedResult.name}" with ID: ${typedResult.id}`
 548 |           }
 549 |         ]
 550 |       };
 551 |     } catch (error) {
 552 |       return {
 553 |         content: [
 554 |           {
 555 |             type: "text",
 556 |             text: `Error creating component instance: ${error instanceof Error ? error.message : String(error)}`
 557 |           }
 558 |         ]
 559 |       };
 560 |     }
 561 |   }
 562 | );
 563 | 
 564 | // Export Node as Image Tool
 565 | server.tool(
 566 |   "export_node_as_image",
 567 |   "Export a node as an image from Figma",
 568 |   {
 569 |     nodeId: z.string().describe("The ID of the node to export"),
 570 |     format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
 571 |     scale: z.number().positive().optional().describe("Export scale")
 572 |   },
 573 |   async ({ nodeId, format, scale }) => {
 574 |     try {
 575 |       const result = await sendCommandToFigma('export_node_as_image', {
 576 |         nodeId,
 577 |         format: format || 'PNG',
 578 |         scale: scale || 1
 579 |       });
 580 |       const typedResult = result as { imageData: string, mimeType: string };
 581 | 
 582 |       return {
 583 |         content: [
 584 |           {
 585 |             type: "image",
 586 |             data: typedResult.imageData,
 587 |             mimeType: typedResult.mimeType || "image/png"
 588 |           }
 589 |         ]
 590 |       };
 591 |     } catch (error) {
 592 |       return {
 593 |         content: [
 594 |           {
 595 |             type: "text",
 596 |             text: `Error exporting node as image: ${error instanceof Error ? error.message : String(error)}`
 597 |           }
 598 |         ]
 599 |       };
 600 |     }
 601 |   }
 602 | );
 603 | 
 604 | // Execute Figma Code Tool
 605 | // server.tool(
 606 | //   "execute_figma_code",
 607 | //   "Execute arbitrary JavaScript code in Figma (use with caution)",
 608 | //   {
 609 | //     code: z.string().describe("JavaScript code to execute in Figma")
 610 | //   },
 611 | //   async ({ code }) => {
 612 | //     try {
 613 | //       const result = await sendCommandToFigma('execute_code', { code });
 614 | //       return {
 615 | //         content: [
 616 | //           {
 617 | //             type: "text",
 618 | //             text: `Code executed successfully: ${JSON.stringify(result, null, 2)}`
 619 | //           }
 620 | //         ]
 621 | //       };
 622 | //     } catch (error) {
 623 | //       return {
 624 | //         content: [
 625 | //           {
 626 | //             type: "text",
 627 | //             text: `Error executing code: ${error instanceof Error ? error.message : String(error)}`
 628 | //           }
 629 | //         ]
 630 | //       };
 631 | //     }
 632 | //   }
 633 | // );
 634 | 
 635 | // Set Corner Radius Tool
 636 | server.tool(
 637 |   "set_corner_radius",
 638 |   "Set the corner radius of a node in Figma",
 639 |   {
 640 |     nodeId: z.string().describe("The ID of the node to modify"),
 641 |     radius: z.number().min(0).describe("Corner radius value"),
 642 |     corners: z.array(z.boolean()).length(4).optional().describe("Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]")
 643 |   },
 644 |   async ({ nodeId, radius, corners }) => {
 645 |     try {
 646 |       const result = await sendCommandToFigma('set_corner_radius', {
 647 |         nodeId,
 648 |         radius,
 649 |         corners: corners || [true, true, true, true]
 650 |       });
 651 |       const typedResult = result as { name: string };
 652 |       return {
 653 |         content: [
 654 |           {
 655 |             type: "text",
 656 |             text: `Set corner radius of node "${typedResult.name}" to ${radius}px`
 657 |           }
 658 |         ]
 659 |       };
 660 |     } catch (error) {
 661 |       return {
 662 |         content: [
 663 |           {
 664 |             type: "text",
 665 |             text: `Error setting corner radius: ${error instanceof Error ? error.message : String(error)}`
 666 |           }
 667 |         ]
 668 |       };
 669 |     }
 670 |   }
 671 | );
 672 | 
 673 | // Define design strategy prompt
 674 | server.prompt(
 675 |   "design_strategy",
 676 |   "Best practices for working with Figma designs",
 677 |   (extra) => {
 678 |     return {
 679 |       messages: [
 680 |         {
 681 |           role: "assistant",
 682 |           content: {
 683 |             type: "text",
 684 |             text: `When working with Figma designs, follow these best practices:
 685 | 
 686 | 1. Start with Document Structure:
 687 |    - First use get_document_info() to understand the current document
 688 |    - Plan your layout hierarchy before creating elements
 689 |    - Create a main container frame for each screen/section
 690 | 
 691 | 2. Naming Conventions:
 692 |    - Use descriptive, semantic names for all elements
 693 |    - Follow a consistent naming pattern (e.g., "Login Screen", "Logo Container", "Email Input")
 694 |    - Group related elements with meaningful names
 695 | 
 696 | 3. Layout Hierarchy:
 697 |    - Create parent frames first, then add child elements
 698 |    - For forms/login screens:
 699 |      * Start with the main screen container frame
 700 |      * Create a logo container at the top
 701 |      * Group input fields in their own containers
 702 |      * Place action buttons (login, submit) after inputs
 703 |      * Add secondary elements (forgot password, signup links) last
 704 | 
 705 | 4. Input Fields Structure:
 706 |    - Create a container frame for each input field
 707 |    - Include a label text above or inside the input
 708 |    - Group related inputs (e.g., username/password) together
 709 | 
 710 | 5. Element Creation:
 711 |    - Use create_frame() for containers and input fields
 712 |    - Use create_text() for labels, buttons text, and links
 713 |    - Set appropriate colors and styles:
 714 |      * Use fillColor for backgrounds
 715 |      * Use strokeColor for borders
 716 |      * Set proper fontWeight for different text elements
 717 | 
 718 | 6. Visual Hierarchy:
 719 |    - Position elements in logical reading order (top to bottom)
 720 |    - Maintain consistent spacing between elements
 721 |    - Use appropriate font sizes for different text types:
 722 |      * Larger for headings/welcome text
 723 |      * Medium for input labels
 724 |      * Standard for button text
 725 |      * Smaller for helper text/links
 726 | 
 727 | 7. Best Practices:
 728 |    - Verify each creation with get_node_info()
 729 |    - Use parentId to maintain proper hierarchy
 730 |    - Group related elements together in frames
 731 |    - Keep consistent spacing and alignment
 732 | 
 733 | Example Login Screen Structure:
 734 | - Login Screen (main frame)
 735 |   - Logo Container (frame)
 736 |     - Logo (image/text)
 737 |   - Welcome Text (text)
 738 |   - Input Container (frame)
 739 |     - Email Input (frame)
 740 |       - Email Label (text)
 741 |       - Email Field (frame)
 742 |     - Password Input (frame)
 743 |       - Password Label (text)
 744 |       - Password Field (frame)
 745 |   - Login Button (frame)
 746 |     - Button Text (text)
 747 |   - Helper Links (frame)
 748 |     - Forgot Password (text)
 749 |     - Don't have account (text)`
 750 |           }
 751 |         }
 752 |       ],
 753 |       description: "Best practices for working with Figma designs"
 754 |     };
 755 |   }
 756 | );
 757 | 
 758 | // Define command types and parameters
 759 | type FigmaCommand =
 760 |   | 'get_document_info'
 761 |   | 'get_selection'
 762 |   | 'get_node_info'
 763 |   | 'create_rectangle'
 764 |   | 'create_frame'
 765 |   | 'create_text'
 766 |   | 'set_fill_color'
 767 |   | 'set_stroke_color'
 768 |   | 'move_node'
 769 |   | 'resize_node'
 770 |   | 'delete_node'
 771 |   | 'get_styles'
 772 |   | 'get_local_components'
 773 |   | 'get_team_components'
 774 |   | 'create_component_instance'
 775 |   | 'export_node_as_image'
 776 |   | 'execute_code'
 777 |   | 'join'
 778 |   | 'set_corner_radius';
 779 | 
 780 | // Helper function to process Figma node responses
 781 | function processFigmaNodeResponse(result: unknown): any {
 782 |   if (!result || typeof result !== 'object') {
 783 |     return result;
 784 |   }
 785 | 
 786 |   // Check if this looks like a node response
 787 |   const resultObj = result as Record<string, unknown>;
 788 |   if ('id' in resultObj && typeof resultObj.id === 'string') {
 789 |     // It appears to be a node response, log the details
 790 |     console.info(`Processed Figma node: ${resultObj.name || 'Unknown'} (ID: ${resultObj.id})`);
 791 | 
 792 |     if ('x' in resultObj && 'y' in resultObj) {
 793 |       console.debug(`Node position: (${resultObj.x}, ${resultObj.y})`);
 794 |     }
 795 | 
 796 |     if ('width' in resultObj && 'height' in resultObj) {
 797 |       console.debug(`Node dimensions: ${resultObj.width}×${resultObj.height}`);
 798 |     }
 799 |   }
 800 | 
 801 |   return result;
 802 | }
 803 | 
 804 | // Simple function to connect to Figma WebSocket server
 805 | function connectToFigma(port: number = 3056) {
 806 |   // If already connected, do nothing
 807 |   if (ws && ws.readyState === WebSocket.OPEN) {
 808 |     console.info('Already connected to Figma');
 809 |     return;
 810 |   }
 811 | 
 812 |   console.info(`Connecting to Figma socket server on port ${port}...`);
 813 |   ws = new WebSocket(`ws://localhost:${port}`);
 814 | 
 815 |   ws.on('open', () => {
 816 |     console.info('Connected to Figma socket server');
 817 |     // Reset channel on new connection
 818 |     currentChannel = null;
 819 |   });
 820 | 
 821 |   ws.on('message', (data: any) => {
 822 |     try {
 823 |       const json = JSON.parse(data) as { message: FigmaResponse };
 824 |       const myResponse = json.message;
 825 |       console.debug(`Received message: ${JSON.stringify(myResponse)}`);
 826 |       console.log('myResponse', myResponse);
 827 | 
 828 |       // Handle response to a request
 829 |       if (myResponse.id && pendingRequests.has(myResponse.id) && myResponse.result) {
 830 |         const request = pendingRequests.get(myResponse.id)!;
 831 |         clearTimeout(request.timeout);
 832 | 
 833 |         if (myResponse.error) {
 834 |           console.error(`Error from Figma: ${myResponse.error}`);
 835 |           request.reject(new Error(myResponse.error));
 836 |         } else {
 837 |           if (myResponse.result) {
 838 |             request.resolve(myResponse.result);
 839 |           }
 840 |         }
 841 | 
 842 |         pendingRequests.delete(myResponse.id);
 843 |       } else {
 844 |         // Handle broadcast messages or events
 845 |         console.info(`Received broadcast message: ${JSON.stringify(myResponse)}`);
 846 |       }
 847 |     } catch (error) {
 848 |       console.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`);
 849 |     }
 850 |   });
 851 | 
 852 |   ws.on('error', (error) => {
 853 |     console.error(`Socket error: ${error}`);
 854 |   });
 855 | 
 856 |   ws.on('close', () => {
 857 |     console.info('Disconnected from Figma socket server');
 858 |     ws = null;
 859 | 
 860 |     // Reject all pending requests
 861 |     for (const [id, request] of pendingRequests.entries()) {
 862 |       clearTimeout(request.timeout);
 863 |       request.reject(new Error('Connection closed'));
 864 |       pendingRequests.delete(id);
 865 |     }
 866 | 
 867 |     // Attempt to reconnect
 868 |     console.info('Attempting to reconnect in 2 seconds...');
 869 |     setTimeout(() => connectToFigma(port), 2000);
 870 |   });
 871 | }
 872 | 
 873 | // Function to join a channel
 874 | async function joinChannel(channelName: string): Promise<void> {
 875 |   if (!ws || ws.readyState !== WebSocket.OPEN) {
 876 |     throw new Error('Not connected to Figma');
 877 |   }
 878 | 
 879 |   try {
 880 |     await sendCommandToFigma('join', { channel: channelName });
 881 |     currentChannel = channelName;
 882 |     console.info(`Joined channel: ${channelName}`);
 883 |   } catch (error) {
 884 |     console.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`);
 885 |     throw error;
 886 |   }
 887 | }
 888 | 
 889 | // Function to send commands to Figma
 890 | function sendCommandToFigma(command: FigmaCommand, params: unknown = {}): Promise<unknown> {
 891 |   return new Promise((resolve, reject) => {
 892 |     // If not connected, try to connect first
 893 |     if (!ws || ws.readyState !== WebSocket.OPEN) {
 894 |       connectToFigma();
 895 |       reject(new Error('Not connected to Figma. Attempting to connect...'));
 896 |       return;
 897 |     }
 898 | 
 899 |     // Check if we need a channel for this command
 900 |     const requiresChannel = command !== 'join';
 901 |     if (requiresChannel && !currentChannel) {
 902 |       reject(new Error('Must join a channel before sending commands'));
 903 |       return;
 904 |     }
 905 | 
 906 |     const id = uuidv4();
 907 |     const request = {
 908 |       id,
 909 |       type: command === 'join' ? 'join' : 'message',
 910 |       ...(command === 'join' ? { channel: (params as any).channel } : { channel: currentChannel }),
 911 |       message: {
 912 |         id,
 913 |         command,
 914 |         params: {
 915 |           ...(params as any),
 916 |         }
 917 |       }
 918 |     };
 919 | 
 920 |     // Set timeout for request
 921 |     const timeout = setTimeout(() => {
 922 |       if (pendingRequests.has(id)) {
 923 |         pendingRequests.delete(id);
 924 |         console.error(`Request ${id} to Figma timed out after 30 seconds`);
 925 |         reject(new Error('Request to Figma timed out'));
 926 |       }
 927 |     }, 30000); // 30 second timeout
 928 | 
 929 |     // Store the promise callbacks to resolve/reject later
 930 |     pendingRequests.set(id, { resolve, reject, timeout });
 931 | 
 932 |     // Send the request
 933 |     console.info(`Sending command to Figma: ${command}`);
 934 |     console.debug(`Request details: ${JSON.stringify(request)}`);
 935 |     ws.send(JSON.stringify(request));
 936 |   });
 937 | }
 938 | 
 939 | // Update the join_channel tool
 940 | server.tool(
 941 |   "join_channel",
 942 |   "Join a specific channel to communicate with Figma",
 943 |   {
 944 |     channel: z.string().describe("The name of the channel to join").default("")
 945 |   },
 946 |   async ({ channel }) => {
 947 |     try {
 948 |       if (!channel) {
 949 |         // If no channel provided, ask the user for input
 950 |         return {
 951 |           content: [
 952 |             {
 953 |               type: "text",
 954 |               text: "Please provide a channel name to join:"
 955 |             }
 956 |           ],
 957 |           followUp: {
 958 |             tool: "join_channel",
 959 |             description: "Join the specified channel"
 960 |           }
 961 |         };
 962 |       }
 963 | 
 964 |       await joinChannel(channel);
 965 |       return {
 966 |         content: [
 967 |           {
 968 |             type: "text",
 969 |             text: `Successfully joined channel: ${channel}`
 970 |           }
 971 |         ]
 972 |       };
 973 |     } catch (error) {
 974 |       return {
 975 |         content: [
 976 |           {
 977 |             type: "text",
 978 |             text: `Error joining channel: ${error instanceof Error ? error.message : String(error)}`
 979 |           }
 980 |         ]
 981 |       };
 982 |     }
 983 |   }
 984 | );
 985 | 
 986 | // Start the server
 987 | async function main() {
 988 |   try {
 989 |     // Try to connect to Figma socket server
 990 |     connectToFigma();
 991 |   } catch (error) {
 992 |     console.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`);
 993 |     console.warn('Will try to connect when the first command is sent');
 994 |   }
 995 | 
 996 |   // Start the MCP server with stdio transport
 997 |   const transport = new StdioServerTransport();
 998 |   await server.connect(transport);
 999 |   console.info('FigmaMCP server running on stdio');
1000 | }
1001 | 
1002 | // Run the server
1003 | main().catch(error => {
1004 |   console.error(`Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`);
1005 |   process.exit(1);
1006 | });
```