#
tokens: 37842/50000 9/63 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 3. Use http://codebase.md/sichang824/mcp-figma?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .envrc
├── .gitignore
├── .mcp.pid
├── bun.lockb
├── docs
│   ├── 01-overview.md
│   ├── 02-implementation-steps.md
│   ├── 03-components-and-features.md
│   ├── 04-usage-guide.md
│   ├── 05-project-status.md
│   ├── image.png
│   ├── README.md
│   └── widget-tools-guide.md
├── Makefile
├── manifest.json
├── package.json
├── prompt.md
├── README.md
├── README.zh.md
├── src
│   ├── config
│   │   └── env.ts
│   ├── index.ts
│   ├── plugin
│   │   ├── code.js
│   │   ├── code.ts
│   │   ├── creators
│   │   │   ├── componentCreators.ts
│   │   │   ├── containerCreators.ts
│   │   │   ├── elementCreator.ts
│   │   │   ├── imageCreators.ts
│   │   │   ├── shapeCreators.ts
│   │   │   ├── sliceCreators.ts
│   │   │   ├── specialCreators.ts
│   │   │   └── textCreator.ts
│   │   ├── manifest.json
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   ├── ui.html
│   │   └── utils
│   │       ├── colorUtils.ts
│   │       └── nodeUtils.ts
│   ├── resources.ts
│   ├── services
│   │   ├── figma-api.ts
│   │   ├── websocket.ts
│   │   └── widget-api.ts
│   ├── tools
│   │   ├── canvas.ts
│   │   ├── comment.ts
│   │   ├── component.ts
│   │   ├── file.ts
│   │   ├── frame.ts
│   │   ├── image.ts
│   │   ├── index.ts
│   │   ├── node.ts
│   │   ├── page.ts
│   │   ├── search.ts
│   │   ├── utils
│   │   │   └── widget-utils.ts
│   │   ├── version.ts
│   │   ├── widget
│   │   │   ├── analyze-widget-structure.ts
│   │   │   ├── get-widget-sync-data.ts
│   │   │   ├── get-widget.ts
│   │   │   ├── get-widgets.ts
│   │   │   ├── index.ts
│   │   │   ├── README.md
│   │   │   ├── search-widgets.ts
│   │   │   └── widget-tools.ts
│   │   └── zod-schemas.ts
│   ├── utils
│   │   ├── figma-utils.ts
│   │   └── widget-utils.ts
│   ├── utils.ts
│   ├── widget
│   │   └── utils
│   │       └── widget-tools.ts
│   └── widget-tools.ts
├── tsconfig.json
└── tsconfig.widget.json
```

# Files

--------------------------------------------------------------------------------
/src/services/figma-api.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import axios from 'axios';
  2 | import { env } from '../config/env.js';
  3 | import type {
  4 |   GetFileResponse,
  5 |   GetFileNodesResponse,
  6 |   GetImageFillsResponse,
  7 |   GetImagesResponse,
  8 |   GetCommentsResponse, 
  9 |   GetFileVersionsResponse,
 10 |   GetTeamProjectsResponse,
 11 |   GetProjectFilesResponse,
 12 |   GetTeamComponentsResponse,
 13 |   GetFileComponentsResponse,
 14 |   GetComponentResponse,
 15 |   GetTeamComponentSetsResponse,
 16 |   GetFileComponentSetsResponse,
 17 |   GetComponentSetResponse,
 18 |   GetTeamStylesResponse,
 19 |   GetFileStylesResponse,
 20 |   GetStyleResponse,
 21 |   PostCommentRequestBody,
 22 |   PostCommentResponse,
 23 | } from "@figma/rest-api-spec";
 24 | 
 25 | const FIGMA_API_BASE_URL = 'https://api.figma.com/v1';
 26 | 
 27 | /**
 28 |  * Type definition for CreateFrameOptions
 29 |  */
 30 | export interface CreateFrameOptions {
 31 |   name: string;
 32 |   width: number;
 33 |   height: number;
 34 |   x?: number;
 35 |   y?: number;
 36 |   fills?: Array<{
 37 |     type: string; 
 38 |     color: { r: number; g: number; b: number }; 
 39 |     opacity: number;
 40 |   }>;
 41 |   pageId?: string;
 42 | }
 43 | 
 44 | /**
 45 |  * Type definition for the expected response when creating a frame
 46 |  */
 47 | export interface CreateFrameResponse {
 48 |   frame: {
 49 |     id: string;
 50 |     name: string;
 51 |   };
 52 |   success: boolean;
 53 | }
 54 | 
 55 | /**
 56 |  * Service for interacting with the Figma API
 57 |  */
 58 | export class FigmaApiService {
 59 |   private readonly headers: Record<string, string>;
 60 | 
 61 |   constructor(accessToken: string = env.FIGMA_PERSONAL_ACCESS_TOKEN) {
 62 |     this.headers = {
 63 |       'X-Figma-Token': accessToken,
 64 |     };
 65 |   }
 66 | 
 67 |   /**
 68 |    * Get file by key
 69 |    */
 70 |   async getFile(fileKey: string, params: { ids?: string; depth?: number; geometry?: string; plugin_data?: string; branch_data?: boolean } = {}): Promise<GetFileResponse> {
 71 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}`, {
 72 |       headers: this.headers,
 73 |       params,
 74 |     });
 75 |     return response.data;
 76 |   }
 77 | 
 78 |   /**
 79 |    * Get file nodes by key and node IDs
 80 |    */
 81 |   async getFileNodes(fileKey: string, nodeIds: string[], params: { depth?: number; geometry?: string; plugin_data?: string } = {}): Promise<GetFileNodesResponse> {
 82 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/nodes`, {
 83 |       headers: this.headers,
 84 |       params: {
 85 |         ...params,
 86 |         ids: nodeIds.join(','),
 87 |       },
 88 |     });
 89 |     return response.data;
 90 |   }
 91 | 
 92 |   /**
 93 |    * Get images for file nodes
 94 |    */
 95 |   async getImages(fileKey: string, nodeIds: string[], params: { 
 96 |     scale?: number; 
 97 |     format?: string; 
 98 |     svg_include_id?: boolean;
 99 |     svg_include_node_id?: boolean;
100 |     svg_simplify_stroke?: boolean;
101 |     use_absolute_bounds?: boolean;
102 |     version?: string;
103 |   } = {}): Promise<GetImagesResponse> {
104 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/images/${fileKey}`, {
105 |       headers: this.headers,
106 |       params: {
107 |         ...params,
108 |         ids: nodeIds.join(','),
109 |       },
110 |     });
111 |     return response.data;
112 |   }
113 | 
114 |   /**
115 |    * Get image fills for a file
116 |    */
117 |   async getImageFills(fileKey: string): Promise<GetImageFillsResponse> {
118 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/images`, {
119 |       headers: this.headers,
120 |     });
121 |     return response.data;
122 |   }
123 | 
124 |   /**
125 |    * Get comments for a file
126 |    */
127 |   async getComments(fileKey: string, params: { as_md?: boolean } = {}): Promise<GetCommentsResponse> {
128 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/comments`, {
129 |       headers: this.headers,
130 |       params,
131 |     });
132 |     return response.data;
133 |   }
134 | 
135 |   /**
136 |    * Post a comment to a file
137 |    */
138 |   async postComment(fileKey: string, data: PostCommentRequestBody): Promise<PostCommentResponse> {
139 |     const response = await axios.post(
140 |       `${FIGMA_API_BASE_URL}/files/${fileKey}/comments`,
141 |       data,
142 |       { headers: this.headers }
143 |     );
144 |     return response.data;
145 |   }
146 | 
147 |   /**
148 |    * Create a new frame in a Figma file
149 |    * Note: This uses the Figma Plugin API which requires appropriate permissions
150 |    */
151 |   async createFrame(fileKey: string, options: CreateFrameOptions): Promise<CreateFrameResponse> {
152 |     // Build the frame creation request payload
153 |     const payload = {
154 |       node: {
155 |         type: "FRAME",
156 |         name: options.name,
157 |         size: {
158 |           width: options.width,
159 |           height: options.height,
160 |         },
161 |         position: {
162 |           x: options.x || 0,
163 |           y: options.y || 0,
164 |         },
165 |         fills: options.fills || [],
166 |       },
167 |       pageId: options.pageId,
168 |     };
169 |     
170 |     const response = await axios.post(
171 |       `${FIGMA_API_BASE_URL}/files/${fileKey}/nodes`,
172 |       payload,
173 |       { headers: this.headers }
174 |     );
175 |     
176 |     return {
177 |       frame: {
178 |         id: response.data.node.id,
179 |         name: response.data.node.name,
180 |       },
181 |       success: true
182 |     };
183 |   }
184 | 
185 |   /**
186 |    * Get file versions
187 |    */
188 |   async getFileVersions(fileKey: string): Promise<GetFileVersionsResponse> {
189 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/versions`, {
190 |       headers: this.headers,
191 |     });
192 |     return response.data;
193 |   }
194 | 
195 |   /**
196 |    * Get team projects
197 |    */
198 |   async getTeamProjects(teamId: string): Promise<GetTeamProjectsResponse> {
199 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/projects`, {
200 |       headers: this.headers,
201 |     });
202 |     return response.data;
203 |   }
204 | 
205 |   /**
206 |    * Get project files
207 |    */
208 |   async getProjectFiles(projectId: string, params: { branch_data?: boolean } = {}): Promise<GetProjectFilesResponse> {
209 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/projects/${projectId}/files`, {
210 |       headers: this.headers,
211 |       params,
212 |     });
213 |     return response.data;
214 |   }
215 | 
216 |   /**
217 |    * Get team components
218 |    */
219 |   async getTeamComponents(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamComponentsResponse> {
220 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/components`, {
221 |       headers: this.headers,
222 |       params,
223 |     });
224 |     return response.data;
225 |   }
226 | 
227 |   /**
228 |    * Get file components
229 |    */
230 |   async getFileComponents(fileKey: string): Promise<GetFileComponentsResponse> {
231 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/components`, {
232 |       headers: this.headers,
233 |     });
234 |     return response.data;
235 |   }
236 | 
237 |   /**
238 |    * Get component by key
239 |    */
240 |   async getComponent(key: string): Promise<GetComponentResponse> {
241 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/components/${key}`, {
242 |       headers: this.headers,
243 |     });
244 |     return response.data;
245 |   }
246 | 
247 |   /**
248 |    * Get team component sets
249 |    */
250 |   async getTeamComponentSets(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamComponentSetsResponse> {
251 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/component_sets`, {
252 |       headers: this.headers,
253 |       params,
254 |     });
255 |     return response.data;
256 |   }
257 | 
258 |   /**
259 |    * Get file component sets
260 |    */
261 |   async getFileComponentSets(fileKey: string): Promise<GetFileComponentSetsResponse> {
262 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/component_sets`, {
263 |       headers: this.headers,
264 |     });
265 |     return response.data;
266 |   }
267 | 
268 |   /**
269 |    * Get component set by key
270 |    */
271 |   async getComponentSet(key: string): Promise<GetComponentSetResponse> {
272 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/component_sets/${key}`, {
273 |       headers: this.headers,
274 |     });
275 |     return response.data;
276 |   }
277 | 
278 |   /**
279 |    * Get team styles
280 |    */
281 |   async getTeamStyles(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamStylesResponse> {
282 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/styles`, {
283 |       headers: this.headers,
284 |       params,
285 |     });
286 |     return response.data;
287 |   }
288 | 
289 |   /**
290 |    * Get file styles
291 |    */
292 |   async getFileStyles(fileKey: string): Promise<GetFileStylesResponse> {
293 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/styles`, {
294 |       headers: this.headers,
295 |     });
296 |     return response.data;
297 |   }
298 | 
299 |   /**
300 |    * Get style by key
301 |    */
302 |   async getStyle(key: string): Promise<GetStyleResponse> {
303 |     const response = await axios.get(`${FIGMA_API_BASE_URL}/styles/${key}`, {
304 |       headers: this.headers,
305 |     });
306 |     return response.data;
307 |   }
308 | }
309 | 
310 | export default new FigmaApiService();
311 | 
```

--------------------------------------------------------------------------------
/src/plugin/creators/specialCreators.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Special element creation functions for Figma plugin
  3 |  * Handles more specialized node types like boolean operations, connectors, etc.
  4 |  */
  5 | 
  6 | import { createSolidPaint } from '../utils/colorUtils';
  7 | import { applyCommonProperties } from '../utils/nodeUtils';
  8 | 
  9 | /**
 10 |  * Create a boolean operation from data
 11 |  * @param data Boolean operation configuration data
 12 |  * @returns Created boolean operation node
 13 |  */
 14 | export function createBooleanOperationFromData(data: any): BooleanOperationNode | null {
 15 |   // Boolean operations require child nodes
 16 |   if (!data.children || !Array.isArray(data.children) || data.children.length < 2) {
 17 |     console.error('Boolean operation requires at least 2 child nodes');
 18 |     return null;
 19 |   }
 20 | 
 21 |   // First we need to create the child nodes and ensure they're on the page
 22 |   let childNodes: SceneNode[] = [];
 23 |   try {
 24 |     for (const childData of data.children) {
 25 |       const node = figma.createRectangle(); // Placeholder, would need actual createElement logic here
 26 |       // In actual use, you'll need to create the proper node type and apply properties
 27 |       childNodes.push(node);
 28 |     }
 29 | 
 30 |     // Now create the boolean operation with the child nodes
 31 |     const booleanOperation = figma.createBooleanOperation();
 32 |     
 33 |     // Set the operation type
 34 |     if (data.booleanOperation) {
 35 |       booleanOperation.booleanOperation = data.booleanOperation;
 36 |     }
 37 |     
 38 |     // Apply common properties
 39 |     applyCommonProperties(booleanOperation, data);
 40 |     
 41 |     return booleanOperation;
 42 |   } catch (error) {
 43 |     console.error('Failed to create boolean operation:', error);
 44 |     // Clean up any created nodes to avoid leaving orphans
 45 |     childNodes.forEach(node => node.remove());
 46 |     return null;
 47 |   }
 48 | }
 49 | 
 50 | /**
 51 |  * Create a connector node from data
 52 |  * @param data Connector configuration data
 53 |  * @returns Created connector node
 54 |  */
 55 | export function createConnectorFromData(data: any): ConnectorNode {
 56 |   const connector = figma.createConnector();
 57 |   
 58 |   // Set connector specific properties
 59 |   if (data.connectorStart) connector.connectorStart = data.connectorStart;
 60 |   if (data.connectorEnd) connector.connectorEnd = data.connectorEnd;
 61 |   if (data.connectorStartStrokeCap) connector.connectorStartStrokeCap = data.connectorStartStrokeCap;
 62 |   if (data.connectorEndStrokeCap) connector.connectorEndStrokeCap = data.connectorEndStrokeCap;
 63 |   if (data.connectorLineType) connector.connectorLineType = data.connectorLineType;
 64 |   
 65 |   // Set stroke properties
 66 |   if (data.strokes) connector.strokes = data.strokes;
 67 |   if (data.strokeWeight) connector.strokeWeight = data.strokeWeight;
 68 |   
 69 |   // Apply common properties
 70 |   applyCommonProperties(connector, data);
 71 |   
 72 |   return connector;
 73 | }
 74 | 
 75 | /**
 76 |  * Create a shape with text node (used in FigJam)
 77 |  * This function might not work in all Figma versions
 78 |  * 
 79 |  * @param data Shape with text configuration data
 80 |  * @returns Created shape with text node
 81 |  */
 82 | export function createShapeWithTextFromData(data: any): ShapeWithTextNode | null {
 83 |   // Check if this node type is supported
 84 |   if (!('createShapeWithText' in figma)) {
 85 |     console.error('ShapeWithText creation is not supported in this Figma version');
 86 |     return null;
 87 |   }
 88 |   
 89 |   try {
 90 |     const shapeWithText = figma.createShapeWithText();
 91 |     
 92 |     // Set shape specific properties
 93 |     if (data.shapeType) shapeWithText.shapeType = data.shapeType;
 94 |     
 95 |     // Text content
 96 |     if (data.text || data.characters) {
 97 |       shapeWithText.text.characters = data.text || data.characters;
 98 |     }
 99 |     
100 |     // Text styling - these properties may not be directly accessible on all versions
101 |     try {
102 |       if (data.fontSize) shapeWithText.text.fontSize = data.fontSize;
103 |       if (data.fontName) shapeWithText.text.fontName = data.fontName;
104 |       // These properties may not exist directly on TextSublayerNode depending on Figma version
105 |       if (data.textAlignHorizontal && 'textAlignHorizontal' in shapeWithText.text) {
106 |         (shapeWithText.text as any).textAlignHorizontal = data.textAlignHorizontal;
107 |       }
108 |       if (data.textAlignVertical && 'textAlignVertical' in shapeWithText.text) {
109 |         (shapeWithText.text as any).textAlignVertical = data.textAlignVertical;
110 |       }
111 |     } catch (e) {
112 |       console.warn('Some text properties could not be set on ShapeWithText:', e);
113 |     }
114 |     
115 |     // Fill and stroke
116 |     if (data.fills) shapeWithText.fills = data.fills;
117 |     if (data.strokes) shapeWithText.strokes = data.strokes;
118 |     
119 |     // Apply common properties
120 |     applyCommonProperties(shapeWithText, data);
121 |     
122 |     return shapeWithText;
123 |   } catch (error) {
124 |     console.error('Failed to create shape with text:', error);
125 |     return null;
126 |   }
127 | }
128 | 
129 | /**
130 |  * Create a code block node
131 |  * @param data Code block configuration data
132 |  * @returns Created code block node
133 |  */
134 | export function createCodeBlockFromData(data: any): CodeBlockNode {
135 |   const codeBlock = figma.createCodeBlock();
136 |   
137 |   // Code content
138 |   if (data.code) codeBlock.code = data.code;
139 |   if (data.codeLanguage) codeBlock.codeLanguage = data.codeLanguage;
140 |   
141 |   // Apply common properties
142 |   applyCommonProperties(codeBlock, data);
143 |   
144 |   return codeBlock;
145 | }
146 | 
147 | /**
148 |  * Create a table node
149 |  * @param data Table configuration data
150 |  * @returns Created table node
151 |  */
152 | export function createTableFromData(data: any): TableNode {
153 |   // Create table with specified rows and columns (defaults to 2x2)
154 |   const table = figma.createTable(
155 |     data.numRows || 2,
156 |     data.numColumns || 2
157 |   );
158 |   
159 |   // Applying table styling
160 |   // Note: Some properties may not be directly available depending on Figma version
161 |   if (data.fills && 'fills' in table) {
162 |     (table as any).fills = data.fills;
163 |   }
164 |   
165 |   // Process cell data if provided
166 |   if (data.cells && Array.isArray(data.cells)) {
167 |     for (const cellData of data.cells) {
168 |       if (cellData.rowIndex !== undefined && cellData.columnIndex !== undefined) {
169 |         try {
170 |           // Different Figma versions may have different API for accessing cells
171 |           let cell;
172 |           if ('cellAt' in table) {
173 |             cell = table.cellAt(cellData.rowIndex, cellData.columnIndex);
174 |           } else if ('getCellAt' in table) {
175 |             cell = (table as any).getCellAt(cellData.rowIndex, cellData.columnIndex);
176 |           }
177 |           
178 |           if (cell) {
179 |             // Apply cell properties
180 |             if (cellData.text && cell.text) cell.text.characters = cellData.text;
181 |             if (cellData.fills && 'fills' in cell) cell.fills = cellData.fills;
182 |             if (cellData.rowSpan && 'rowSpan' in cell) cell.rowSpan = cellData.rowSpan;
183 |             if (cellData.columnSpan && 'columnSpan' in cell) cell.columnSpan = cellData.columnSpan;
184 |           }
185 |         } catch (e) {
186 |           console.warn(`Could not set properties for cell at ${cellData.rowIndex}, ${cellData.columnIndex}:`, e);
187 |         }
188 |       }
189 |     }
190 |   }
191 |   
192 |   // Apply common properties
193 |   applyCommonProperties(table, data);
194 |   
195 |   return table;
196 | }
197 | 
198 | /**
199 |  * Create a widget node (if supported in current Figma version)
200 |  * @param data Widget configuration data
201 |  * @returns Created widget node or null
202 |  */
203 | export function createWidgetFromData(data: any): WidgetNode | null {
204 |   // Check if widget creation is supported
205 |   if (!('createWidget' in figma)) {
206 |     console.error('Widget creation is not supported in this Figma version');
207 |     return null;
208 |   }
209 |   
210 |   // Widgets require a package ID
211 |   if (!data.widgetId) {
212 |     console.error('Widget creation requires a widgetId');
213 |     return null;
214 |   }
215 |   
216 |   try {
217 |     // Using type assertion since createWidget may not be recognized by TypeScript
218 |     const widget = (figma as any).createWidget(data.widgetId);
219 |     
220 |     // Set widget properties
221 |     if (data.widgetData) widget.widgetData = JSON.stringify(data.widgetData);
222 |     if (data.width && data.height && 'resize' in widget) widget.resize(data.width, data.height);
223 |     
224 |     // Apply common properties
225 |     applyCommonProperties(widget, data);
226 |     
227 |     return widget;
228 |   } catch (error) {
229 |     console.error('Failed to create widget:', error);
230 |     return null;
231 |   }
232 | }
233 | 
234 | /**
235 |  * Create a media node (if supported in current Figma version)
236 |  * @param data Media configuration data
237 |  * @returns Created media node or null
238 |  */
239 | export function createMediaFromData(data: any): MediaNode | null {
240 |   // Check if media creation is supported
241 |   if (!('createMedia' in figma)) {
242 |     console.error('Media creation is not supported in this Figma version');
243 |     return null;
244 |   }
245 |   
246 |   // Media requires a hash
247 |   if (!data.hash) {
248 |     console.error('Media creation requires a valid media hash');
249 |     return null;
250 |   }
251 |   
252 |   try {
253 |     // Using type assertion since createMedia may not be recognized by TypeScript
254 |     const media = (figma as any).createMedia(data.hash);
255 |     
256 |     // Apply common properties
257 |     applyCommonProperties(media, data);
258 |     
259 |     return media;
260 |   } catch (error) {
261 |     console.error('Failed to create media:', error);
262 |     return null;
263 |   }
264 | } 
```

--------------------------------------------------------------------------------
/src/plugin/code.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Figma MCP Plugin
  3 |  * Allows manipulating elements on canvas through MCP tools
  4 |  */
  5 | 
  6 | // Import modules
  7 | import {
  8 |   createElementFromData,
  9 |   createElementsFromDataArray,
 10 | } from "./creators/elementCreator";
 11 | import {
 12 |   createEllipseFromData,
 13 |   createLineFromData,
 14 |   createPolygonFromData,
 15 |   createRectangleFromData,
 16 |   createStarFromData,
 17 |   createVectorFromData
 18 | } from "./creators/shapeCreators";
 19 | import { createTextFromData } from "./creators/textCreator";
 20 | import { hexToRgb } from "./utils/colorUtils";
 21 | import { buildResultObject, selectAndFocusNodes } from "./utils/nodeUtils";
 22 | 
 23 | // Show plugin UI
 24 | figma.showUI(__html__, { width: 320, height: 500 });
 25 | 
 26 | // Log that the plugin has loaded
 27 | console.log("Figma MCP Plugin loaded");
 28 | 
 29 | // Element creator mapping
 30 | type ElementCreator = (params: any) => SceneNode | Promise<SceneNode>;
 31 | 
 32 | const elementCreators: Record<string, ElementCreator> = {
 33 |   "create-rectangle": createRectangleFromData,
 34 |   "create-circle": createEllipseFromData,
 35 |   "create-ellipse": createEllipseFromData,
 36 |   "create-polygon": createPolygonFromData,
 37 |   "create-line": createLineFromData,
 38 |   "create-text": createTextFromData,
 39 |   "create-star": createStarFromData,
 40 |   "create-vector": createVectorFromData,
 41 |   "create-arc": (params: any) => {
 42 |     const ellipse = createEllipseFromData(params);
 43 |     if (params.arcData || (params.startAngle !== undefined && params.endAngle !== undefined)) {
 44 |       ellipse.arcData = {
 45 |         startingAngle: params.startAngle || params.arcData.startingAngle || 0,
 46 |         endingAngle: params.endAngle || params.arcData.endingAngle || 360,
 47 |         innerRadius: params.innerRadius || params.arcData.innerRadius || 0
 48 |       };
 49 |     }
 50 |     return ellipse;
 51 |   }
 52 | };
 53 | 
 54 | // Generic element creation function
 55 | async function createElement(type: string, params: any): Promise<SceneNode | null> {
 56 |   console.log(`Creating ${type} with params:`, params);
 57 |   
 58 |   // Get the creator function
 59 |   const creator = elementCreators[type];
 60 |   if (!creator) {
 61 |     console.error(`Unknown element type: ${type}`);
 62 |     return null;
 63 |   }
 64 |   
 65 |   try {
 66 |     // Create the element (handle both synchronous and asynchronous creators)
 67 |     const element = await Promise.resolve(creator(params));
 68 |     
 69 |     // Set position if provided
 70 |     if (element && params) {
 71 |       if (params.x !== undefined) element.x = params.x;
 72 |       if (params.y !== undefined) element.y = params.y;
 73 |     }
 74 |     
 75 |     // Select and focus the element
 76 |     if (element) {
 77 |       selectAndFocusNodes(element);
 78 |     }
 79 |     
 80 |     return element;
 81 |   } catch (error) {
 82 |     console.error(`Error creating ${type}:`, error);
 83 |     return null;
 84 |   }
 85 | }
 86 | 
 87 | // Handle messages from UI
 88 | figma.ui.onmessage = async function (msg) {
 89 |   console.log("Received message from UI:", msg);
 90 | 
 91 |   // Handle different types of messages
 92 |   if (elementCreators[msg.type]) {
 93 |     // Element creation messages
 94 |     await createElement(msg.type, msg);
 95 |   } else if (msg.type === "create-element") {
 96 |     // Unified create element method
 97 |     console.log("Creating element with data:", msg.data);
 98 |     createElementFromData(msg.data);
 99 |   } else if (msg.type === "create-elements") {
100 |     // Create multiple elements at once
101 |     console.log("Creating multiple elements with data:", msg.data);
102 |     createElementsFromDataArray(msg.data);
103 |   } else if (msg.type === "mcp-command") {
104 |     // Handle commands from MCP tool via UI
105 |     console.log(
106 |       "Received MCP command:",
107 |       msg.command,
108 |       "with params:",
109 |       msg.params
110 |     );
111 |     handleMcpCommand(msg.command, msg.params);
112 |   } else if (msg.type === "cancel") {
113 |     console.log("Closing plugin");
114 |     figma.closePlugin();
115 |   } else {
116 |     console.log("Unknown message type:", msg.type);
117 |   }
118 | };
119 | 
120 | // Handle MCP commands
121 | async function handleMcpCommand(command: string, params: any) {
122 |   let result:
123 |     | SceneNode
124 |     | PageNode
125 |     | readonly SceneNode[]
126 |     | readonly PageNode[]
127 |     | null = null;
128 | 
129 |   try {
130 |     // Convert command format from mcp (create_rectangle) to plugin (create-rectangle)
131 |     const pluginCommand = command.replace(/_/g, '-');
132 |     
133 |     switch (pluginCommand) {
134 |       case "create-rectangle":
135 |       case "create-circle":
136 |       case "create-polygon":
137 |       case "create-line":
138 |       case "create-arc":
139 |       case "create-vector":
140 |         console.log(`MCP command: Creating ${pluginCommand.substring(7)} with params:`, params);
141 |         result = await createElement(pluginCommand, params);
142 |         break;
143 | 
144 |       case "create-text":
145 |         console.log("MCP command: Creating text with params:", params);
146 |         result = await createElement(pluginCommand, params);
147 |         break;
148 | 
149 |       case "create-element":
150 |         console.log("MCP command: Creating element with params:", params);
151 |         result = await createElementFromData(params);
152 |         break;
153 | 
154 |       case "create-elements":
155 |         console.log(
156 |           "MCP command: Creating multiple elements with params:",
157 |           params
158 |         );
159 |         result = await createElementsFromDataArray(params);
160 |         break;
161 | 
162 |       case "get-selection":
163 |         console.log("MCP command: Getting current selection");
164 |         result = figma.currentPage.selection;
165 |         break;
166 | 
167 |       case "get-elements":
168 |         console.log("MCP command: Getting elements with params:", params);
169 |         const page = params.page_id 
170 |           ? (figma.getNodeById(params.page_id) as PageNode) 
171 |           : figma.currentPage;
172 |           
173 |         if (!page || page.type !== "PAGE") {
174 |           throw new Error("Invalid page ID or node is not a page");
175 |         }
176 |         
177 |         const nodeType = params.type || "ALL";
178 |         const limit = params.limit || 100;
179 |         const includeHidden = params.include_hidden || false;
180 |         
181 |         if (nodeType === "ALL") {
182 |           // Get all nodes, filtered by visibility if needed
183 |           result = includeHidden 
184 |             ? page.children.slice(0, limit) 
185 |             : page.children.filter(node => node.visible).slice(0, limit);
186 |         } else {
187 |           // Filter by node type and visibility
188 |           result = page.findAll(node => {
189 |             const typeMatch = node.type === nodeType;
190 |             const visibilityMatch = includeHidden || node.visible;
191 |             return typeMatch && visibilityMatch;
192 |           }).slice(0, limit);
193 |         }
194 |         break;
195 |         
196 |       case "get-element":
197 |         console.log("MCP command: Getting element with ID:", params.node_id);
198 |         const node = figma.getNodeById(params.node_id);
199 |         
200 |         if (!node) {
201 |           throw new Error("Element not found with ID: " + params.node_id);
202 |         }
203 |         
204 |         // Check if the node is a valid type for our result
205 |         if (!['DOCUMENT', 'PAGE'].includes(node.type)) {
206 |           // For scene nodes with children, include children if requested
207 |           if (params.include_children && 'children' in node) {
208 |             result = [node as SceneNode, ...((node as any).children || [])];
209 |           } else {
210 |             result = node as SceneNode;
211 |           }
212 |         } else if (node.type === 'PAGE') {
213 |           // Handle page nodes specially
214 |           result = node as PageNode;
215 |         } else {
216 |           // For document or other unsupported node types
217 |           throw new Error("Unsupported node type: " + node.type);
218 |         }
219 |         break;
220 | 
221 |       case "get-pages":
222 |         console.log("MCP command: Getting all pages");
223 |         result = figma.root.children;
224 |         break;
225 | 
226 |       case "get-page":
227 |         console.log("MCP command: Getting page with ID:", params.page_id);
228 |         if (!params.page_id) {
229 |           // If no page_id is provided, use the current page
230 |           console.log("No page_id provided, using current page");
231 |           result = figma.currentPage;
232 |         } else {
233 |           // If page_id is provided, find the page by ID
234 |           const pageNode = figma.getNodeById(params.page_id);
235 |           if (!pageNode || pageNode.type !== "PAGE")
236 |             throw new Error("Invalid page ID or node is not a page");
237 |           result = pageNode;
238 |         }
239 |         break;
240 | 
241 |       case "create-page":
242 |         console.log("MCP command: Creating new page with name:", params.name);
243 |         const newPage = figma.createPage();
244 |         newPage.name = params.name || "New Page";
245 |         result = newPage;
246 |         break;
247 | 
248 |       case "switch-page":
249 |         console.log("MCP command: Switching to page with ID:", params.id);
250 |         if (!params.id) throw new Error("Page ID is required");
251 |         const switchPageNode = figma.getNodeById(params.id);
252 |         if (!switchPageNode || switchPageNode.type !== "PAGE")
253 |           throw new Error("Invalid page ID");
254 | 
255 |         figma.currentPage = switchPageNode as PageNode;
256 |         result = switchPageNode;
257 |         break;
258 | 
259 |       case "modify-rectangle":
260 |         console.log("MCP command: Modifying rectangle with ID:", params.id);
261 |         if (!params.id) throw new Error("Rectangle ID is required");
262 |         const modifyNode = figma.getNodeById(params.id);
263 |         if (!modifyNode || modifyNode.type !== "RECTANGLE")
264 |           throw new Error("Invalid rectangle ID");
265 | 
266 |         const rect = modifyNode as RectangleNode;
267 |         if (params.x !== undefined) rect.x = params.x;
268 |         if (params.y !== undefined) rect.y = params.y;
269 |         if (params.width !== undefined && params.height !== undefined)
270 |           rect.resize(params.width, params.height);
271 |         if (params.cornerRadius !== undefined)
272 |           rect.cornerRadius = params.cornerRadius;
273 |         if (params.color)
274 |           rect.fills = [{ type: "SOLID", color: hexToRgb(params.color) }];
275 | 
276 |         result = rect;
277 |         break;
278 | 
279 |       default:
280 |         console.log("Unknown MCP command:", command);
281 |         throw new Error("Unknown command: " + command);
282 |     }
283 | 
284 |     // Convert PageNode to a compatible format for buildResultObject if needed
285 |     let resultForBuilder: SceneNode | readonly SceneNode[] | null = null;
286 | 
287 |     if (result === null) {
288 |       resultForBuilder = null;
289 |     } else if (Array.isArray(result)) {
290 |       // For arrays, we rely on duck typing - both PageNode[] and SceneNode[] have id, name, type
291 |       resultForBuilder = result as unknown as readonly SceneNode[];
292 |     } else if ("type" in result && result.type === "PAGE") {
293 |       // For individual PageNode, we rely on duck typing - PageNode has id, name, type like SceneNode
294 |       resultForBuilder = result as unknown as SceneNode;
295 |     } else {
296 |       resultForBuilder = result as SceneNode;
297 |     }
298 | 
299 |     // Build result object, avoiding possible null values
300 |     const resultObject = buildResultObject(resultForBuilder);
301 |     console.log("Command result:", resultObject);
302 | 
303 |     // Send success response to UI
304 |     figma.ui.postMessage({
305 |       type: "mcp-response",
306 |       success: true,
307 |       command: command,
308 |       result: resultObject,
309 |     });
310 |     console.log("Response sent to UI");
311 | 
312 |     return resultObject;
313 |   } catch (error) {
314 |     console.error("Error handling MCP command:", error);
315 | 
316 |     // Send error response to UI
317 |     figma.ui.postMessage({
318 |       type: "mcp-response",
319 |       success: false,
320 |       command: command,
321 |       error: error instanceof Error ? error.message : "Unknown error",
322 |     });
323 |     console.log("Error response sent to UI");
324 | 
325 |     throw error;
326 |   }
327 | }
328 | 
```

--------------------------------------------------------------------------------
/src/tools/widget/widget-tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Widget Tools - MCP server tools for interacting with Figma widgets
  3 |  */
  4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import { z } from "zod";
  6 | import figmaApi from "../../services/figma-api.js";
  7 | import { FigmaUtils } from "../../utils/figma-utils.js";
  8 | 
  9 | /**
 10 |  * Register widget-related tools with the MCP server
 11 |  * @param server The MCP server instance
 12 |  */
 13 | export function registerWidgetTools(server: McpServer) {
 14 |   // Get all widget nodes in a file
 15 |   server.tool(
 16 |     "get_widgets",
 17 |     {
 18 |       file_key: z.string().min(1).describe("The Figma file key to retrieve widgets from")
 19 |     },
 20 |     async ({ file_key }) => {
 21 |       try {
 22 |         const file = await figmaApi.getFile(file_key);
 23 |         
 24 |         // Find all widget nodes in the file
 25 |         const widgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET');
 26 |         
 27 |         if (widgetNodes.length === 0) {
 28 |           return {
 29 |             content: [
 30 |               { type: "text", text: `No widgets found in file ${file_key}` }
 31 |             ]
 32 |           };
 33 |         }
 34 |         
 35 |         const widgetsList = widgetNodes.map((node, index) => {
 36 |           const widgetSyncData = node.widgetSync ? 
 37 |             `\n   - Widget Sync Data: Available` : 
 38 |             `\n   - Widget Sync Data: None`;
 39 |             
 40 |           return `${index + 1}. **${node.name}** (ID: ${node.id})
 41 |    - Widget ID: ${node.widgetId || 'Unknown'}${widgetSyncData}`;
 42 |         }).join('\n\n');
 43 |         
 44 |         return {
 45 |           content: [
 46 |             { type: "text", text: `# Widgets in file ${file_key}` },
 47 |             { type: "text", text: `Found ${widgetNodes.length} widgets:` },
 48 |             { type: "text", text: widgetsList }
 49 |           ]
 50 |         };
 51 |       } catch (error) {
 52 |         console.error('Error fetching widgets:', error);
 53 |         return {
 54 |           content: [
 55 |             { type: "text", text: `Error getting widgets: ${(error as Error).message}` }
 56 |           ]
 57 |         };
 58 |       }
 59 |     }
 60 |   );
 61 | 
 62 |   // Get a specific widget node
 63 |   server.tool(
 64 |     "get_widget",
 65 |     {
 66 |       file_key: z.string().min(1).describe("The Figma file key"),
 67 |       node_id: z.string().min(1).describe("The ID of the widget node")
 68 |     },
 69 |     async ({ file_key, node_id }) => {
 70 |       try {
 71 |         const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]);
 72 |         const nodeData = fileNodes.nodes[node_id];
 73 |         
 74 |         if (!nodeData || nodeData.document.type !== 'WIDGET') {
 75 |           return {
 76 |             content: [
 77 |               { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` }
 78 |             ]
 79 |           };
 80 |         }
 81 |         
 82 |         const widgetNode = nodeData.document;
 83 |         
 84 |         // Get the sync data if available
 85 |         let syncDataContent = '';
 86 |         if (widgetNode.widgetSync) {
 87 |           try {
 88 |             const syncData = JSON.parse(widgetNode.widgetSync);
 89 |             syncDataContent = `\n\n## Widget Sync Data\n\`\`\`json\n${JSON.stringify(syncData, null, 2)}\n\`\`\``;
 90 |           } catch (error) {
 91 |             syncDataContent = '\n\n## Widget Sync Data\nError parsing widget sync data';
 92 |           }
 93 |         }
 94 |         
 95 |         return {
 96 |           content: [
 97 |             { type: "text", text: `# Widget: ${widgetNode.name}` },
 98 |             { type: "text", text: `ID: ${widgetNode.id}` },
 99 |             { type: "text", text: `Widget ID: ${widgetNode.widgetId || 'Unknown'}` },
100 |             { type: "text", text: `Has Sync Data: ${widgetNode.widgetSync ? 'Yes' : 'No'}${syncDataContent}` }
101 |           ]
102 |         };
103 |       } catch (error) {
104 |         console.error('Error fetching widget node:', error);
105 |         return {
106 |           content: [
107 |             { type: "text", text: `Error getting widget: ${(error as Error).message}` }
108 |           ]
109 |         };
110 |       }
111 |     }
112 |   );
113 | 
114 |   // Get widget sync data
115 |   server.tool(
116 |     "get_widget_sync_data",
117 |     {
118 |       file_key: z.string().min(1).describe("The Figma file key"),
119 |       node_id: z.string().min(1).describe("The ID of the widget node")
120 |     },
121 |     async ({ file_key, node_id }) => {
122 |       try {
123 |         const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]);
124 |         const nodeData = fileNodes.nodes[node_id];
125 |         
126 |         if (!nodeData || nodeData.document.type !== 'WIDGET') {
127 |           return {
128 |             content: [
129 |               { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` }
130 |             ]
131 |           };
132 |         }
133 |         
134 |         const widgetNode = nodeData.document;
135 |         
136 |         if (!widgetNode.widgetSync) {
137 |           return {
138 |             content: [
139 |               { type: "text", text: `Widget ${node_id} does not have any sync data` }
140 |             ]
141 |           };
142 |         }
143 |         
144 |         try {
145 |           const syncData = JSON.parse(widgetNode.widgetSync);
146 |           
147 |           return {
148 |             content: [
149 |               { type: "text", text: `# Widget Sync Data for "${widgetNode.name}"` },
150 |               { type: "text", text: `Widget ID: ${widgetNode.id}` },
151 |               { type: "text", text: "```json\n" + JSON.stringify(syncData, null, 2) + "\n```" }
152 |             ]
153 |           };
154 |         } catch (error) {
155 |           console.error('Error parsing widget sync data:', error);
156 |           return {
157 |             content: [
158 |               { type: "text", text: `Error parsing widget sync data: ${(error as Error).message}` }
159 |             ]
160 |           };
161 |         }
162 |       } catch (error) {
163 |         console.error('Error fetching widget sync data:', error);
164 |         return {
165 |           content: [
166 |             { type: "text", text: `Error getting widget sync data: ${(error as Error).message}` }
167 |           ]
168 |         };
169 |       }
170 |     }
171 |   );
172 | 
173 |   // Search widgets by property values
174 |   server.tool(
175 |     "search_widgets",
176 |     {
177 |       file_key: z.string().min(1).describe("The Figma file key"),
178 |       property_key: z.string().min(1).describe("The sync data property key to search for"),
179 |       property_value: z.string().optional().describe("Optional property value to match (if not provided, returns all widgets with the property)")
180 |     },
181 |     async ({ file_key, property_key, property_value }) => {
182 |       try {
183 |         const file = await figmaApi.getFile(file_key);
184 |         
185 |         // Find all widget nodes
186 |         const allWidgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET');
187 |         
188 |         // Filter widgets that have the specified property
189 |         const matchingWidgets = allWidgetNodes.filter(node => {
190 |           if (!node.widgetSync) return false;
191 |           
192 |           try {
193 |             const syncData = JSON.parse(node.widgetSync);
194 |             
195 |             // If property_value is provided, check for exact match
196 |             if (property_value !== undefined) {
197 |               // Handle different types of values (string, number, boolean)
198 |               const propValue = syncData[property_key];
199 |               
200 |               if (typeof propValue === 'string') {
201 |                 return propValue === property_value;
202 |               } else if (typeof propValue === 'number') {
203 |                 return propValue.toString() === property_value;
204 |               } else if (typeof propValue === 'boolean') {
205 |                 return propValue.toString() === property_value;
206 |               } else if (propValue !== null && typeof propValue === 'object') {
207 |                 return JSON.stringify(propValue) === property_value;
208 |               }
209 |               
210 |               return false;
211 |             }
212 |             
213 |             // If no value provided, just check if the property exists
214 |             return property_key in syncData;
215 |           } catch (error) {
216 |             return false;
217 |           }
218 |         });
219 |         
220 |         if (matchingWidgets.length === 0) {
221 |           return {
222 |             content: [
223 |               { type: "text", text: property_value ? 
224 |                 `No widgets found with property "${property_key}" = "${property_value}"` : 
225 |                 `No widgets found with property "${property_key}"` 
226 |               }
227 |             ]
228 |           };
229 |         }
230 |         
231 |         const widgetsList = matchingWidgets.map((node, index) => {
232 |           let syncDataValue = '';
233 |           try {
234 |             const syncData = JSON.parse(node.widgetSync!);
235 |             const value = syncData[property_key];
236 |             syncDataValue = typeof value === 'object' ? 
237 |               JSON.stringify(value) : 
238 |               String(value);
239 |           } catch (error) {
240 |             syncDataValue = 'Error parsing sync data';
241 |           }
242 |           
243 |           return `${index + 1}. **${node.name}** (ID: ${node.id})
244 |    - Property "${property_key}": ${syncDataValue}`;
245 |         }).join('\n\n');
246 |         
247 |         return {
248 |           content: [
249 |             { type: "text", text: property_value ? 
250 |               `# Widgets with property "${property_key}" = "${property_value}"` : 
251 |               `# Widgets with property "${property_key}"` 
252 |             },
253 |             { type: "text", text: `Found ${matchingWidgets.length} matching widgets:` },
254 |             { type: "text", text: widgetsList }
255 |           ]
256 |         };
257 |       } catch (error) {
258 |         console.error('Error searching widgets:', error);
259 |         return {
260 |           content: [
261 |             { type: "text", text: `Error searching widgets: ${(error as Error).message}` }
262 |           ]
263 |         };
264 |       }
265 |     }
266 |   );
267 | 
268 |   // Get widget properties for modification
269 |   server.tool(
270 |     "analyze_widget_structure",
271 |     {
272 |       file_key: z.string().min(1).describe("The Figma file key"),
273 |       node_id: z.string().min(1).describe("The ID of the widget node")
274 |     },
275 |     async ({ file_key, node_id }) => {
276 |       try {
277 |         const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]);
278 |         const nodeData = fileNodes.nodes[node_id];
279 |         
280 |         if (!nodeData || nodeData.document.type !== 'WIDGET') {
281 |           return {
282 |             content: [
283 |               { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` }
284 |             ]
285 |           };
286 |         }
287 |         
288 |         const widgetNode = nodeData.document;
289 |         
290 |         // Create a full analysis of the widget
291 |         const widgetAnalysis = {
292 |           basic: {
293 |             id: widgetNode.id,
294 |             name: widgetNode.name,
295 |             type: widgetNode.type,
296 |             widgetId: widgetNode.widgetId || 'Unknown'
297 |           },
298 |           placement: {
299 |             x: widgetNode.x || 0,
300 |             y: widgetNode.y || 0,
301 |             width: widgetNode.width || 0,
302 |             height: widgetNode.height || 0,
303 |             rotation: widgetNode.rotation || 0
304 |           },
305 |           syncData: null as any
306 |         };
307 |         
308 |         // Parse the widget sync data if available
309 |         if (widgetNode.widgetSync) {
310 |           try {
311 |             widgetAnalysis.syncData = JSON.parse(widgetNode.widgetSync);
312 |           } catch (error) {
313 |             widgetAnalysis.syncData = { error: 'Invalid sync data format' };
314 |           }
315 |         }
316 |         
317 |         return {
318 |           content: [
319 |             { type: "text", text: `# Widget Analysis: ${widgetNode.name}` },
320 |             { type: "text", text: `## Basic Information` },
321 |             { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.basic, null, 2) + "\n```" },
322 |             { type: "text", text: `## Placement` },
323 |             { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.placement, null, 2) + "\n```" },
324 |             { type: "text", text: `## Sync Data` },
325 |             { type: "text", text: widgetAnalysis.syncData ? 
326 |               "```json\n" + JSON.stringify(widgetAnalysis.syncData, null, 2) + "\n```" : 
327 |               "No sync data available" 
328 |             }
329 |           ]
330 |         };
331 |       } catch (error) {
332 |         console.error('Error analyzing widget:', error);
333 |         return {
334 |           content: [
335 |             { type: "text", text: `Error analyzing widget: ${(error as Error).message}` }
336 |           ]
337 |         };
338 |       }
339 |     }
340 |   );
341 | }
342 | 
```

--------------------------------------------------------------------------------
/src/tools/frame.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Frame Tools - MCP server tools for working with Figma Frame components
  3 |  */
  4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import { z } from "zod";
  6 | import figmaApi from "../services/figma-api.js";
  7 | import { FigmaUtils } from "../utils/figma-utils.js";
  8 | 
  9 | /**
 10 |  * Register frame-related tools with the MCP server
 11 |  * @param server The MCP server instance
 12 |  */
 13 | export function registerFrameTools(server: McpServer) {
 14 |   // Get Frame component documentation
 15 |   server.tool("get_frame_documentation", {}, async () => {
 16 |     try {
 17 |       // Frame component documentation based on the provided text
 18 |       return {
 19 |         content: [
 20 |           { type: "text", text: "# Frame Component Documentation" },
 21 |           {
 22 |             type: "text",
 23 |             text: "Frame acts exactly like a non-autolayout Frame within Figma, where children are positioned using x and y constraints. This component is useful to define a layout hierarchy.",
 24 |           },
 25 |           {
 26 |             type: "text",
 27 |             text: "If you want to use autolayout, use AutoLayout instead.",
 28 |           },
 29 |           { type: "text", text: "## BaseProps" },
 30 |           {
 31 |             type: "text",
 32 |             text: "- **name**: string - The name of the component",
 33 |           },
 34 |           {
 35 |             type: "text",
 36 |             text: "- **hidden**: boolean - Toggles whether to show the component",
 37 |           },
 38 |           {
 39 |             type: "text",
 40 |             text: "- **onClick**: (event: WidgetClickEvent) => Promise<any> | void - Attach a click handler",
 41 |           },
 42 |           {
 43 |             type: "text",
 44 |             text: "- **key**: string | number - The key of the component",
 45 |           },
 46 |           {
 47 |             type: "text",
 48 |             text: "- **hoverStyle**: HoverStyle - The style to be applied when hovering",
 49 |           },
 50 |           {
 51 |             type: "text",
 52 |             text: "- **tooltip**: string - The tooltip shown when hovering",
 53 |           },
 54 |           {
 55 |             type: "text",
 56 |             text: "- **positioning**: 'auto' | 'absolute' - How to position the node inside an AutoLayout parent",
 57 |           },
 58 |           { type: "text", text: "## BlendProps" },
 59 |           {
 60 |             type: "text",
 61 |             text: "- **blendMode**: BlendMode - The blendMode of the component",
 62 |           },
 63 |           {
 64 |             type: "text",
 65 |             text: "- **opacity**: number - The opacity of the component",
 66 |           },
 67 |           {
 68 |             type: "text",
 69 |             text: "- **effect**: Effect | Effect[] - The effect of the component",
 70 |           },
 71 |           { type: "text", text: "## ConstraintProps" },
 72 |           {
 73 |             type: "text",
 74 |             text: "- **x**: number | HorizontalConstraint - The x position of the node",
 75 |           },
 76 |           {
 77 |             type: "text",
 78 |             text: "- **y**: number | VerticalConstraint - The y position of the node",
 79 |           },
 80 |           {
 81 |             type: "text",
 82 |             text: "- **overflow**: 'visible' | 'hidden' | 'scroll' - The overflow behavior",
 83 |           },
 84 |           { type: "text", text: "## SizeProps (Required)" },
 85 |           {
 86 |             type: "text",
 87 |             text: "- **width**: Size - The width of the component (required)",
 88 |           },
 89 |           {
 90 |             type: "text",
 91 |             text: "- **height**: Size - The height of the component (required)",
 92 |           },
 93 |           { type: "text", text: "- **minWidth**: number - The minimum width" },
 94 |           { type: "text", text: "- **maxWidth**: number - The maximum width" },
 95 |           {
 96 |             type: "text",
 97 |             text: "- **minHeight**: number - The minimum height",
 98 |           },
 99 |           {
100 |             type: "text",
101 |             text: "- **maxHeight**: number - The maximum height",
102 |           },
103 |           {
104 |             type: "text",
105 |             text: "- **rotation**: number - The rotation in degrees (-180 to 180)",
106 |           },
107 |           { type: "text", text: "## CornerProps" },
108 |           {
109 |             type: "text",
110 |             text: "- **cornerRadius**: CornerRadius - The corner radius in pixels",
111 |           },
112 |           { type: "text", text: "## GeometryProps" },
113 |           {
114 |             type: "text",
115 |             text: "- **fill**: HexCode | Color | Paint | (SolidPaint | GradientPaint)[] - The fill paints",
116 |           },
117 |           {
118 |             type: "text",
119 |             text: "- **stroke**: HexCode | Color | SolidPaint | GradientPaint | (SolidPaint | GradientPaint)[] - The stroke paints",
120 |           },
121 |           {
122 |             type: "text",
123 |             text: "- **strokeWidth**: number - The stroke thickness in pixels",
124 |           },
125 |           {
126 |             type: "text",
127 |             text: "- **strokeAlign**: StrokeAlign - The stroke alignment",
128 |           },
129 |           {
130 |             type: "text",
131 |             text: "- **strokeDashPattern**: number[] - The stroke dash pattern",
132 |           },
133 |         ],
134 |       };
135 |     } catch (error) {
136 |       console.error("Error retrieving Frame documentation:", error);
137 |       return {
138 |         content: [
139 |           {
140 |             type: "text",
141 |             text: `Error getting Frame documentation: ${
142 |               (error as Error).message
143 |             }`,
144 |           },
145 |         ],
146 |       };
147 |     }
148 |   });
149 | 
150 |   // Create a new frame widget
151 |   server.tool(
152 |     "create_frame_widget",
153 |     {
154 |       name: z
155 |         .string()
156 |         .min(1)
157 |         .describe("The name for the widget containing frames"),
158 |       width: z.number().min(1).describe("The width of the frame"),
159 |       height: z.number().min(1).describe("The height of the frame"),
160 |       fill: z.string().optional().describe("The fill color (hex code)"),
161 |     },
162 |     async ({ name, width, height, fill }) => {
163 |       try {
164 |         // Create a sample widget code with Frame component
165 |         const widgetCode = `
166 | // ${name} - Figma Widget with Frame Component
167 | const { widget } = figma;
168 | const { Frame, Text } = widget;
169 | 
170 | function ${name.replace(/\\s+/g, "")}Widget() {
171 |   return (
172 |     <Frame
173 |       name="${name}"
174 |       width={${width}}
175 |       height={${height}}
176 |       fill={${fill ? `"${fill}"` : "[]"}}
177 |       stroke="#E0E0E0"
178 |       strokeWidth={1}
179 |       cornerRadius={8}
180 |     >
181 |       <Text
182 |         x={20}
183 |         y={20}
184 |         width={${width - 40}}
185 |         horizontalAlignText="center"
186 |         fill="#000000"
187 |       >
188 |         Frame Widget Example
189 |       </Text>
190 |     </Frame>
191 |   );
192 | }
193 | 
194 | widget.register(${name.replace(/\\s+/g, "")}Widget);
195 | `;
196 | 
197 |         return {
198 |           content: [
199 |             { type: "text", text: `# Frame Widget Code` },
200 |             {
201 |               type: "text",
202 |               text: `The following code creates a widget using the Frame component:`,
203 |             },
204 |             { type: "text", text: "```jsx\n" + widgetCode + "\n```" },
205 |             { type: "text", text: `## Instructions` },
206 |             { type: "text", text: `1. Create a new widget in Figma` },
207 |             {
208 |               type: "text",
209 |               text: `2. Copy and paste this code into the widget code editor`,
210 |             },
211 |             {
212 |               type: "text",
213 |               text: `3. Customize the content inside the Frame as needed`,
214 |             },
215 |           ],
216 |         };
217 |       } catch (error) {
218 |         console.error("Error generating frame widget code:", error);
219 |         return {
220 |           content: [
221 |             {
222 |               type: "text",
223 |               text: `Error generating frame widget code: ${
224 |                 (error as Error).message
225 |               }`,
226 |             },
227 |           ],
228 |         };
229 |       }
230 |     }
231 |   );
232 | 
233 |   // Create a frame directly in Figma file
234 |   server.tool(
235 |     "create_frame_in_figma",
236 |     {
237 |       file_key: z
238 |         .string()
239 |         .min(1)
240 |         .describe("The Figma file key where the frame will be created"),
241 |       page_id: z
242 |         .string()
243 |         .optional()
244 |         .describe("The page ID where the frame will be created (optional)"),
245 |       name: z
246 |         .string()
247 |         .min(1)
248 |         .describe("The name for the new frame"),
249 |       width: z.number().min(1).describe("The width of the frame in pixels"),
250 |       height: z.number().min(1).describe("The height of the frame in pixels"),
251 |       x: z.number().default(0).describe("The X position of the frame (default: 0)"),
252 |       y: z.number().default(0).describe("The Y position of the frame (default: 0)"),
253 |       fill_color: z.string().optional().describe("The fill color (hex code)"),
254 |     },
255 |     async ({ file_key, page_id, name, width, height, x, y, fill_color }) => {
256 |       try {
257 |         // Create a frame in Figma using the Figma API
258 |         const createFrameResponse = await figmaApi.createFrame(file_key, {
259 |           name,
260 |           width,
261 |           height, 
262 |           x,
263 |           y,
264 |           fills: fill_color ? [{ type: "SOLID", color: hexToRgb(fill_color), opacity: 1 }] : [],
265 |           pageId: page_id
266 |         });
267 |         
268 |         return {
269 |           content: [
270 |             { type: "text", text: `# Frame Created Successfully` },
271 |             { 
272 |               type: "text", 
273 |               text: `A new frame named "${name}" has been created in your Figma file.` 
274 |             },
275 |             {
276 |               type: "text",
277 |               text: `- Width: ${width}px\n- Height: ${height}px\n- Position: (${x}, ${y})`
278 |             },
279 |             {
280 |               type: "text",
281 |               text: `Frame ID: ${createFrameResponse.frame.id}`
282 |             },
283 |             { 
284 |               type: "text", 
285 |               text: `You can now view and edit this frame in your Figma file.` 
286 |             }
287 |           ],
288 |         };
289 |       } catch (error) {
290 |         console.error("Error creating frame in Figma:", error);
291 |         return {
292 |           content: [
293 |             {
294 |               type: "text",
295 |               text: `Error creating frame in Figma: ${
296 |                 (error as Error).message
297 |               }`,
298 |             },
299 |             {
300 |               type: "text",
301 |               text: "Please make sure you have write access to the file and the file key is correct."
302 |             }
303 |           ],
304 |         };
305 |       }
306 |     }
307 |   );
308 | 
309 |   // Get all frames in a file
310 |   server.tool(
311 |     "get_frames",
312 |     {
313 |       file_key: z
314 |         .string()
315 |         .min(1)
316 |         .describe("The Figma file key to retrieve frames from"),
317 |     },
318 |     async ({ file_key }) => {
319 |       try {
320 |         const file = await figmaApi.getFile(file_key);
321 | 
322 |         // Find all frame nodes in the file
323 |         const frameNodes = FigmaUtils.getNodesByType(file, "FRAME");
324 | 
325 |         if (frameNodes.length === 0) {
326 |           return {
327 |             content: [
328 |               { type: "text", text: `No frames found in file ${file_key}` },
329 |             ],
330 |           };
331 |         }
332 | 
333 |         const framesList = frameNodes
334 |           .map((node, index) => {
335 |             // Add type assertion for frame nodes
336 |             const frameNode = node as {
337 |               id: string;
338 |               name: string; 
339 |               width?: number;
340 |               height?: number;
341 |               children?: Array<any>;
342 |             };
343 |             
344 |             return `${index + 1}. **${frameNode.name}** (ID: ${frameNode.id})
345 |    - Width: ${frameNode.width || "Unknown"}, Height: ${frameNode.height || "Unknown"}
346 |    - Children: ${frameNode.children?.length || 0}`;
347 |           })
348 |           .join("\n\n");
349 | 
350 |         return {
351 |           content: [
352 |             { type: "text", text: `# Frames in file ${file_key}` },
353 |             { type: "text", text: `Found ${frameNodes.length} frames:` },
354 |             { type: "text", text: framesList },
355 |           ],
356 |         };
357 |       } catch (error) {
358 |         console.error("Error fetching frames:", error);
359 |         return {
360 |           content: [
361 |             {
362 |               type: "text",
363 |               text: `Error getting frames: ${(error as Error).message}`,
364 |             },
365 |           ],
366 |         };
367 |       }
368 |     }
369 |   );
370 | }
371 | 
372 | /**
373 |  * Convert hex color code to RGB values
374 |  * @param hex Hex color code (e.g., #RRGGBB or #RGB)
375 |  * @returns RGB color object with r, g, b values between 0 and 1
376 |  */
377 | function hexToRgb(hex: string) {
378 |   // Remove # if present
379 |   hex = hex.replace(/^#/, '');
380 |   
381 |   // Parse hex values
382 |   let r, g, b;
383 |   if (hex.length === 3) {
384 |     // Convert 3-digit hex to 6-digit
385 |     r = parseInt(hex.charAt(0) + hex.charAt(0), 16) / 255;
386 |     g = parseInt(hex.charAt(1) + hex.charAt(1), 16) / 255;
387 |     b = parseInt(hex.charAt(2) + hex.charAt(2), 16) / 255;
388 |   } else if (hex.length === 6) {
389 |     r = parseInt(hex.substring(0, 2), 16) / 255;
390 |     g = parseInt(hex.substring(2, 4), 16) / 255;
391 |     b = parseInt(hex.substring(4, 6), 16) / 255;
392 |   } else {
393 |     throw new Error(`Invalid hex color: ${hex}`);
394 |   }
395 |   
396 |   return { r, g, b };
397 | }
398 | 
```

--------------------------------------------------------------------------------
/src/plugin/creators/shapeCreators.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Shape creation functions for Figma plugin
  3 |  */
  4 | 
  5 | import { createSolidPaint } from "../utils/colorUtils";
  6 | import { selectAndFocusNodes } from "../utils/nodeUtils";
  7 | 
  8 | /**
  9 |  * Create a rectangle from data
 10 |  * @param data Rectangle configuration data
 11 |  * @returns Created rectangle node
 12 |  */
 13 | export function createRectangleFromData(data: any): RectangleNode {
 14 |   const rect = figma.createRectangle();
 15 | 
 16 |   // Size
 17 |   rect.resize(data.width || 100, data.height || 100);
 18 | 
 19 |   // Fill
 20 |   if (data.fills) {
 21 |     rect.fills = data.fills;
 22 |   } else if (data.fill) {
 23 |     if (typeof data.fill === "string") {
 24 |       // Handle hex color
 25 |       rect.fills = [createSolidPaint(data.fill)];
 26 |     } else {
 27 |       // Handle fill object
 28 |       rect.fills = [data.fill];
 29 |     }
 30 |   }
 31 | 
 32 |   // Stroke
 33 |   if (data.strokes) rect.strokes = data.strokes;
 34 |   if (data.strokeWeight !== undefined) rect.strokeWeight = data.strokeWeight;
 35 |   if (data.strokeAlign) rect.strokeAlign = data.strokeAlign;
 36 |   if (data.strokeCap) rect.strokeCap = data.strokeCap;
 37 |   if (data.strokeJoin) rect.strokeJoin = data.strokeJoin;
 38 |   if (data.dashPattern) rect.dashPattern = data.dashPattern;
 39 | 
 40 |   // Corner radius
 41 |   if (data.cornerRadius !== undefined) rect.cornerRadius = data.cornerRadius;
 42 |   if (data.topLeftRadius !== undefined) rect.topLeftRadius = data.topLeftRadius;
 43 |   if (data.topRightRadius !== undefined)
 44 |     rect.topRightRadius = data.topRightRadius;
 45 |   if (data.bottomLeftRadius !== undefined)
 46 |     rect.bottomLeftRadius = data.bottomLeftRadius;
 47 |   if (data.bottomRightRadius !== undefined)
 48 |     rect.bottomRightRadius = data.bottomRightRadius;
 49 | 
 50 |   return rect;
 51 | }
 52 | 
 53 | /**
 54 |  * Create a simple rectangle
 55 |  * Convenient function for basic rectangle creation
 56 |  *
 57 |  * @param x X coordinate
 58 |  * @param y Y coordinate
 59 |  * @param width Width of rectangle
 60 |  * @param height Height of rectangle
 61 |  * @param color Fill color as hex string
 62 |  * @returns Created rectangle node
 63 |  */
 64 | export function createRectangle(
 65 |   x: number,
 66 |   y: number,
 67 |   width: number,
 68 |   height: number,
 69 |   color: string
 70 | ): RectangleNode {
 71 |   // Use the new data-driven function
 72 |   const rect = createRectangleFromData({
 73 |     width,
 74 |     height,
 75 |     fill: color,
 76 |   });
 77 | 
 78 |   // Set position
 79 |   rect.x = x;
 80 |   rect.y = y;
 81 | 
 82 |   // Select and focus
 83 |   selectAndFocusNodes(rect);
 84 | 
 85 |   return rect;
 86 | }
 87 | 
 88 | /**
 89 |  * Create an ellipse/circle from data
 90 |  * @param data Ellipse configuration data
 91 |  * @returns Created ellipse node
 92 |  */
 93 | export function createEllipseFromData(data: any): EllipseNode {
 94 |   const ellipse = figma.createEllipse();
 95 | 
 96 |   // Size
 97 |   ellipse.resize(data.width || 100, data.height || 100);
 98 | 
 99 |   // Fill
100 |   if (data.fills) {
101 |     ellipse.fills = data.fills;
102 |   } else if (data.fill) {
103 |     if (typeof data.fill === "string") {
104 |       // Handle hex color
105 |       ellipse.fills = [createSolidPaint(data.fill)];
106 |     } else {
107 |       // Handle fill object
108 |       ellipse.fills = [data.fill];
109 |     }
110 |   }
111 | 
112 |   // Arc data for partial ellipses (arcs/donuts)
113 |   if (data.arcData) {
114 |     ellipse.arcData = {
115 |       startingAngle: data.arcData.startingAngle !== undefined ? data.arcData.startingAngle : 0,
116 |       endingAngle: data.arcData.endingAngle !== undefined ? data.arcData.endingAngle : 360,
117 |       innerRadius: data.arcData.innerRadius !== undefined ? data.arcData.innerRadius : 0
118 |     };
119 |   }
120 | 
121 |   // Stroke
122 |   if (data.strokes) ellipse.strokes = data.strokes;
123 |   if (data.strokeWeight !== undefined) ellipse.strokeWeight = data.strokeWeight;
124 |   if (data.strokeAlign) ellipse.strokeAlign = data.strokeAlign;
125 |   if (data.strokeCap) ellipse.strokeCap = data.strokeCap;
126 |   if (data.strokeJoin) ellipse.strokeJoin = data.strokeJoin;
127 |   if (data.dashPattern) ellipse.dashPattern = data.dashPattern;
128 | 
129 |   return ellipse;
130 | }
131 | 
132 | /**
133 |  * Create a simple circle or ellipse
134 |  * @param x X coordinate
135 |  * @param y Y coordinate
136 |  * @param width Width of ellipse
137 |  * @param height Height of ellipse
138 |  * @param color Fill color as hex string
139 |  * @returns Created ellipse node
140 |  */
141 | export function createCircle(
142 |   x: number,
143 |   y: number,
144 |   width: number,
145 |   height: number,
146 |   color: string
147 | ): EllipseNode {
148 |   // Use the data-driven function
149 |   const ellipse = createEllipseFromData({
150 |     width,
151 |     height,
152 |     fill: color,
153 |   });
154 | 
155 |   // Set position
156 |   ellipse.x = x;
157 |   ellipse.y = y;
158 | 
159 |   // Select and focus
160 |   selectAndFocusNodes(ellipse);
161 | 
162 |   return ellipse;
163 | }
164 | 
165 | /**
166 |  * Create a polygon from data
167 |  * @param data Polygon configuration data
168 |  * @returns Created polygon node
169 |  */
170 | export function createPolygonFromData(data: any): PolygonNode {
171 |   const polygon = figma.createPolygon();
172 |   polygon.resize(data.width || 100, data.height || 100);
173 | 
174 |   // Set number of sides
175 |   if (data.pointCount) polygon.pointCount = data.pointCount;
176 | 
177 |   // Fill
178 |   if (data.fills) {
179 |     polygon.fills = data.fills;
180 |   } else if (data.fill) {
181 |     if (typeof data.fill === "string") {
182 |       polygon.fills = [createSolidPaint(data.fill)];
183 |     } else {
184 |       polygon.fills = [data.fill];
185 |     }
186 |   } else if (data.color) {
187 |     // For consistency with other shape creation functions
188 |     polygon.fills = [createSolidPaint(data.color)];
189 |   }
190 | 
191 |   // Stroke
192 |   if (data.strokes) polygon.strokes = data.strokes;
193 |   if (data.strokeWeight !== undefined) polygon.strokeWeight = data.strokeWeight;
194 |   if (data.strokeAlign) polygon.strokeAlign = data.strokeAlign;
195 |   if (data.strokeCap) polygon.strokeCap = data.strokeCap;
196 |   if (data.strokeJoin) polygon.strokeJoin = data.strokeJoin;
197 |   if (data.dashPattern) polygon.dashPattern = data.dashPattern;
198 | 
199 |   return polygon;
200 | }
201 | 
202 | /**
203 |  * Create a simple polygon
204 |  * @param x X coordinate
205 |  * @param y Y coordinate
206 |  * @param width Width of polygon
207 |  * @param height Height of polygon
208 |  * @param sides Number of sides (≥ 3)
209 |  * @param color Fill color as hex string
210 |  * @returns Created polygon node
211 |  */
212 | export function createPolygon(
213 |   x: number,
214 |   y: number,
215 |   width: number,
216 |   height: number,
217 |   sides: number = 3,
218 |   color: string
219 | ): PolygonNode {
220 |   // Use the data-driven function
221 |   const polygon = createPolygonFromData({
222 |     width,
223 |     height,
224 |     pointCount: sides,
225 |     fill: color
226 |   });
227 | 
228 |   // Set position
229 |   polygon.x = x;
230 |   polygon.y = y;
231 | 
232 |   // Select and focus
233 |   selectAndFocusNodes(polygon);
234 | 
235 |   return polygon;
236 | }
237 | 
238 | /**
239 |  * Create a star from data
240 |  * @param data Star configuration data
241 |  * @returns Created star node
242 |  */
243 | export function createStarFromData(data: any): StarNode {
244 |   const star = figma.createStar();
245 |   star.resize(data.width || 100, data.height || 100);
246 | 
247 |   // Star specific properties
248 |   if (data.pointCount) star.pointCount = data.pointCount;
249 |   if (data.innerRadius) star.innerRadius = data.innerRadius;
250 | 
251 |   // Fill
252 |   if (data.fills) {
253 |     star.fills = data.fills;
254 |   } else if (data.fill) {
255 |     if (typeof data.fill === "string") {
256 |       star.fills = [createSolidPaint(data.fill)];
257 |     } else {
258 |       star.fills = [data.fill];
259 |     }
260 |   }
261 | 
262 |   return star;
263 | }
264 | 
265 | /**
266 |  * Create a line from data
267 |  * @param data Line configuration data
268 |  * @returns Created line node
269 |  */
270 | export function createLineFromData(data: any): LineNode {
271 |   const line = figma.createLine();
272 |   
273 |   // Set line length (width)
274 |   line.resize(data.width || 100, 0); // Line height is always 0
275 |   
276 |   // Set rotation if provided
277 |   if (data.rotation !== undefined) line.rotation = data.rotation;
278 | 
279 |   // Stroke properties
280 |   if (data.strokeWeight) line.strokeWeight = data.strokeWeight;
281 |   if (data.strokeAlign) line.strokeAlign = data.strokeAlign;
282 |   if (data.strokeCap) line.strokeCap = data.strokeCap;
283 |   if (data.strokeJoin) line.strokeJoin = data.strokeJoin;
284 |   if (data.dashPattern) line.dashPattern = data.dashPattern;
285 | 
286 |   // Stroke color
287 |   if (data.strokes) {
288 |     line.strokes = data.strokes;
289 |   } else if (data.stroke) {
290 |     if (typeof data.stroke === "string") {
291 |       line.strokes = [createSolidPaint(data.stroke)];
292 |     } else {
293 |       line.strokes = [data.stroke];
294 |     }
295 |   } else if (data.color) {
296 |     // For consistency with other shape creation functions
297 |     line.strokes = [createSolidPaint(data.color)];
298 |   }
299 | 
300 |   return line;
301 | }
302 | 
303 | /**
304 |  * Create a simple line
305 |  * @param x X coordinate
306 |  * @param y Y coordinate
307 |  * @param length Length of the line (width)
308 |  * @param color Stroke color as hex string
309 |  * @param rotation Rotation in degrees
310 |  * @param strokeWeight Stroke thickness
311 |  * @returns Created line node
312 |  */
313 | export function createLine(
314 |   x: number,
315 |   y: number,
316 |   length: number,
317 |   color: string,
318 |   rotation: number = 0,
319 |   strokeWeight: number = 1
320 | ): LineNode {
321 |   // Use the data-driven function
322 |   const line = createLineFromData({
323 |     width: length,
324 |     stroke: color,
325 |     strokeWeight: strokeWeight,
326 |     rotation: rotation
327 |   });
328 | 
329 |   // Set position
330 |   line.x = x;
331 |   line.y = y;
332 | 
333 |   // Select and focus
334 |   selectAndFocusNodes(line);
335 | 
336 |   return line;
337 | }
338 | 
339 | /**
340 |  * Create a simple arc (partial ellipse)
341 |  * @param x X coordinate
342 |  * @param y Y coordinate
343 |  * @param width Width of ellipse
344 |  * @param height Height of ellipse
345 |  * @param startAngle Starting angle in degrees
346 |  * @param endAngle Ending angle in degrees
347 |  * @param innerRadius Inner radius ratio (0-1) for donut shapes
348 |  * @param color Fill color as hex string
349 |  * @returns Created ellipse node as an arc
350 |  */
351 | export function createArc(
352 |   x: number,
353 |   y: number,
354 |   width: number,
355 |   height: number,
356 |   startAngle: number,
357 |   endAngle: number,
358 |   innerRadius: number = 0,
359 |   color: string
360 | ): EllipseNode {
361 |   // Use the data-driven function
362 |   const arc = createEllipseFromData({
363 |     width,
364 |     height,
365 |     fill: color,
366 |     arcData: {
367 |       startingAngle: startAngle,
368 |       endingAngle: endAngle,
369 |       innerRadius: innerRadius
370 |     }
371 |   });
372 | 
373 |   // Set position
374 |   arc.x = x;
375 |   arc.y = y;
376 | 
377 |   // Select and focus
378 |   selectAndFocusNodes(arc);
379 | 
380 |   return arc;
381 | }
382 | 
383 | /**
384 |  * Create a vector from data
385 |  * @param data Vector configuration data
386 |  * @returns Created vector node
387 |  */
388 | export function createVectorFromData(data: any): VectorNode {
389 |   const vector = figma.createVector();
390 |   
391 |   try {
392 |     // Resize the vector
393 |     vector.resize(data.width || 100, data.height || 100);
394 | 
395 |     // Set vector-specific properties
396 |     if (data.vectorNetwork) {
397 |       vector.vectorNetwork = data.vectorNetwork;
398 |     }
399 | 
400 |     if (data.vectorPaths) {
401 |       vector.vectorPaths = data.vectorPaths;
402 |     }
403 | 
404 |     if (data.handleMirroring) {
405 |       vector.handleMirroring = data.handleMirroring;
406 |     }
407 | 
408 |     // Fill
409 |     if (data.fills) {
410 |       vector.fills = data.fills;
411 |     } else if (data.fill) {
412 |       if (typeof data.fill === "string") {
413 |         vector.fills = [createSolidPaint(data.fill)];
414 |       } else {
415 |         vector.fills = [data.fill];
416 |       }
417 |     } else if (data.color) {
418 |       // For consistency with other shape creation functions
419 |       vector.fills = [createSolidPaint(data.color)];
420 |     }
421 | 
422 |     // Stroke
423 |     if (data.strokes) vector.strokes = data.strokes;
424 |     if (data.strokeWeight !== undefined) vector.strokeWeight = data.strokeWeight;
425 |     if (data.strokeAlign) vector.strokeAlign = data.strokeAlign;
426 |     if (data.strokeCap) vector.strokeCap = data.strokeCap;
427 |     if (data.strokeJoin) vector.strokeJoin = data.strokeJoin;
428 |     if (data.dashPattern) vector.dashPattern = data.dashPattern;
429 |     if (data.strokeMiterLimit) vector.strokeMiterLimit = data.strokeMiterLimit;
430 | 
431 |     // Corner properties
432 |     if (data.cornerRadius !== undefined) vector.cornerRadius = data.cornerRadius;
433 |     if (data.cornerSmoothing !== undefined) vector.cornerSmoothing = data.cornerSmoothing;
434 | 
435 |     // Blend properties
436 |     if (data.opacity !== undefined) vector.opacity = data.opacity;
437 |     if (data.blendMode) vector.blendMode = data.blendMode;
438 |     if (data.isMask !== undefined) vector.isMask = data.isMask;
439 |     if (data.effects) vector.effects = data.effects;
440 | 
441 |     // Layout properties
442 |     if (data.constraints) vector.constraints = data.constraints;
443 |     if (data.layoutAlign) vector.layoutAlign = data.layoutAlign;
444 |     if (data.layoutGrow !== undefined) vector.layoutGrow = data.layoutGrow;
445 |     if (data.layoutPositioning) vector.layoutPositioning = data.layoutPositioning;
446 |     if (data.rotation !== undefined) vector.rotation = data.rotation;
447 |     if (data.layoutSizingHorizontal) vector.layoutSizingHorizontal = data.layoutSizingHorizontal;
448 |     if (data.layoutSizingVertical) vector.layoutSizingVertical = data.layoutSizingVertical;
449 | 
450 |     console.log("Vector created successfully:", vector);
451 |   } catch (error) {
452 |     console.error("Error creating vector:", error);
453 |   }
454 | 
455 |   return vector;
456 | }
457 | 
458 | /**
459 |  * Create a simple vector
460 |  * @param x X coordinate
461 |  * @param y Y coordinate
462 |  * @param width Width of vector
463 |  * @param height Height of vector
464 |  * @param color Fill color as hex string
465 |  * @returns Created vector node
466 |  */
467 | export function createVector(
468 |   x: number,
469 |   y: number,
470 |   width: number,
471 |   height: number,
472 |   color: string
473 | ): VectorNode {
474 |   // Use the data-driven function
475 |   const vector = createVectorFromData({
476 |     width,
477 |     height,
478 |     fill: color
479 |   });
480 | 
481 |   // Set position
482 |   vector.x = x;
483 |   vector.y = y;
484 | 
485 |   // Select and focus
486 |   selectAndFocusNodes(vector);
487 | 
488 |   return vector;
489 | }
490 | 
```

--------------------------------------------------------------------------------
/src/plugin/ui.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html>
  3 |   <head>
  4 |     <style>
  5 |       body {
  6 |         font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
  7 |           Helvetica, Arial, sans-serif;
  8 |         margin: 0;
  9 |         padding: 0;
 10 |       }
 11 |       .container {
 12 |         padding: 20px;
 13 |       }
 14 |       h2 {
 15 |         font-size: 16px;
 16 |         margin-bottom: 15px;
 17 |       }
 18 |       .control-group {
 19 |         margin-bottom: 15px;
 20 |       }
 21 |       label {
 22 |         display: block;
 23 |         margin-bottom: 5px;
 24 |         font-size: 12px;
 25 |       }
 26 |       input {
 27 |         width: 100%;
 28 |         padding: 6px;
 29 |         margin-bottom: 10px;
 30 |         box-sizing: border-box;
 31 |         border: 1px solid #ccc;
 32 |         border-radius: 4px;
 33 |       }
 34 |       .button-group {
 35 |         display: flex;
 36 |         gap: 8px;
 37 |         margin-top: 15px;
 38 |       }
 39 |       button {
 40 |         background-color: #18a0fb;
 41 |         color: white;
 42 |         border: none;
 43 |         padding: 8px 12px;
 44 |         border-radius: 6px;
 45 |         cursor: pointer;
 46 |         flex: 1;
 47 |         font-size: 12px;
 48 |       }
 49 |       button:hover {
 50 |         background-color: #0d8ee3;
 51 |       }
 52 |       button.cancel {
 53 |         background-color: #f24822;
 54 |       }
 55 |       button.cancel:hover {
 56 |         background-color: #d83b17;
 57 |       }
 58 |       .connection-status {
 59 |         margin-top: 15px;
 60 |         padding: 8px;
 61 |         border-radius: 4px;
 62 |         font-size: 12px;
 63 |         text-align: center;
 64 |       }
 65 |       .status-connected {
 66 |         background-color: #ecfdf5;
 67 |         color: #047857;
 68 |       }
 69 |       .status-disconnected {
 70 |         background-color: #fef2f2;
 71 |         color: #b91c1c;
 72 |       }
 73 |       .status-connecting {
 74 |         background-color: #fef3c7;
 75 |         color: #92400e;
 76 |       }
 77 |       .mcp-section {
 78 |         margin-top: 20px;
 79 |         padding-top: 15px;
 80 |         border-top: 1px solid #e5e7eb;
 81 |       }
 82 |       .log-area {
 83 |         margin-top: 10px;
 84 |         height: 100px;
 85 |         overflow-y: auto;
 86 |         border: 1px solid #ccc;
 87 |         border-radius: 4px;
 88 |         padding: 8px;
 89 |         font-size: 11px;
 90 |         font-family: monospace;
 91 |         background-color: #f9fafb;
 92 |       }
 93 |       .log-item {
 94 |         margin-bottom: 4px;
 95 |       }
 96 |       .server-input {
 97 |         display: flex;
 98 |         gap: 8px;
 99 |         margin-bottom: 10px;
100 |       }
101 |       .server-input input {
102 |         flex: 1;
103 |       }
104 |       .checkbox-group {
105 |         display: flex;
106 |         align-items: center;
107 |         margin-bottom: 10px;
108 |       }
109 |       .checkbox-group input[type="checkbox"] {
110 |         width: auto;
111 |         margin-right: 8px;
112 |       }
113 |       .checkbox-group label {
114 |         display: inline;
115 |         font-size: 12px;
116 |       }
117 |     </style>
118 |   </head>
119 |   <body>
120 |     <div class="container">
121 |       <h2>Figma MCP 画布操作工具</h2>
122 | 
123 |       <div class="control-group">
124 |         <label for="x">X 位置:</label>
125 |         <input type="number" id="x" value="100" />
126 | 
127 |         <label for="y">Y 位置:</label>
128 |         <input type="number" id="y" value="100" />
129 |       </div>
130 | 
131 |       <div class="control-group">
132 |         <label for="width">宽度:</label>
133 |         <input type="number" id="width" value="150" />
134 | 
135 |         <label for="height">高度:</label>
136 |         <input type="number" id="height" value="150" />
137 |       </div>
138 | 
139 |       <div class="control-group">
140 |         <label for="color">颜色:</label>
141 |         <input type="color" id="color" value="#ff0000" />
142 |       </div>
143 | 
144 |       <div class="control-group">
145 |         <label for="text">文本:</label>
146 |         <input type="text" id="text" value="Hello Figma!" />
147 | 
148 |         <label for="fontSize">字体大小:</label>
149 |         <input type="number" id="fontSize" value="24" />
150 |       </div>
151 | 
152 |       <div class="button-group">
153 |         <button id="create-rectangle">矩形</button>
154 |         <button id="create-circle">圆形</button>
155 |         <button id="create-text">文本</button>
156 |       </div>
157 | 
158 |       <div class="mcp-section">
159 |         <h2>MCP 连接</h2>
160 |         <div class="server-input">
161 |           <input
162 |             type="text"
163 |             id="server-url"
164 |             value="ws://localhost:3001/ws"
165 |             placeholder="输入 MCP 服务器 WebSocket URL"
166 |           />
167 |           <button id="connect-button">连接</button>
168 |         </div>
169 | 
170 |         <div class="checkbox-group">
171 |           <input type="checkbox" id="auto-reconnect" checked />
172 |           <label for="auto-reconnect">自动重连</label>
173 |         </div>
174 | 
175 |         <div
176 |           id="connection-status"
177 |           class="connection-status status-disconnected"
178 |         >
179 |           未连接到 MCP
180 |         </div>
181 | 
182 |         <div class="log-area" id="log-area">
183 |           <div class="log-item">等待 MCP 连接和命令...</div>
184 |         </div>
185 |       </div>
186 | 
187 |       <div class="button-group">
188 |         <button class="cancel" id="cancel">关闭</button>
189 |       </div>
190 |     </div>
191 | 
192 |     <script>
193 |       // 获取所有输入元素
194 |       const xInput = document.getElementById("x");
195 |       const yInput = document.getElementById("y");
196 |       const widthInput = document.getElementById("width");
197 |       const heightInput = document.getElementById("height");
198 |       const colorInput = document.getElementById("color");
199 |       const textInput = document.getElementById("text");
200 |       const fontSizeInput = document.getElementById("fontSize");
201 |       const connectionStatus = document.getElementById("connection-status");
202 |       const logArea = document.getElementById("log-area");
203 |       const serverUrlInput = document.getElementById("server-url");
204 |       const connectButton = document.getElementById("connect-button");
205 |       const autoReconnectCheckbox = document.getElementById("auto-reconnect");
206 | 
207 |       // MCP 连接状态和 WebSocket 对象
208 |       let mcpConnected = false;
209 |       let ws = null;
210 |       let isConnecting = false;
211 |       let isManualDisconnect = false;
212 |       let retryCount = 0;
213 |       let maxRetries = 10;
214 |       let reconnectTimer = null;
215 | 
216 |       // 添加按钮事件监听器
217 |       document.getElementById("create-rectangle").onclick = () => {
218 |         parent.postMessage(
219 |           {
220 |             pluginMessage: {
221 |               type: "create-rectangle",
222 |               x: parseInt(xInput.value),
223 |               y: parseInt(yInput.value),
224 |               width: parseInt(widthInput.value),
225 |               height: parseInt(heightInput.value),
226 |               color: colorInput.value,
227 |             },
228 |           },
229 |           "*"
230 |         );
231 |       };
232 | 
233 |       document.getElementById("create-circle").onclick = () => {
234 |         parent.postMessage(
235 |           {
236 |             pluginMessage: {
237 |               type: "create-circle",
238 |               x: parseInt(xInput.value),
239 |               y: parseInt(yInput.value),
240 |               width: parseInt(widthInput.value),
241 |               height: parseInt(heightInput.value),
242 |               color: colorInput.value,
243 |             },
244 |           },
245 |           "*"
246 |         );
247 |       };
248 | 
249 |       document.getElementById("create-text").onclick = () => {
250 |         parent.postMessage(
251 |           {
252 |             pluginMessage: {
253 |               type: "create-text",
254 |               x: parseInt(xInput.value),
255 |               y: parseInt(yInput.value),
256 |               text: textInput.value,
257 |               fontSize: parseInt(fontSizeInput.value),
258 |             },
259 |           },
260 |           "*"
261 |         );
262 |       };
263 | 
264 |       document.getElementById("cancel").onclick = () => {
265 |         parent.postMessage(
266 |           {
267 |             pluginMessage: { type: "cancel" },
268 |           },
269 |           "*"
270 |         );
271 |       };
272 | 
273 |       // 添加日志条目
274 |       function addLogEntry(message) {
275 |         const logItem = document.createElement("div");
276 |         logItem.classList.add("log-item");
277 |         logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
278 |         logArea.appendChild(logItem);
279 |         logArea.scrollTop = logArea.scrollHeight;
280 |       }
281 | 
282 |       // 设置 MCP 连接状态
283 |       function setMcpConnectionStatus(status) {
284 |         if (status === "connected") {
285 |           mcpConnected = true;
286 |           isConnecting = false;
287 |           connectionStatus.className = "connection-status status-connected";
288 |           connectionStatus.textContent = "已连接到 MCP";
289 |           connectButton.textContent = "断开";
290 |           addLogEntry("MCP 已连接");
291 |           retryCount = 0;
292 |         } else if (status === "connecting") {
293 |           mcpConnected = false;
294 |           isConnecting = true;
295 |           connectionStatus.className = "connection-status status-connecting";
296 |           connectionStatus.textContent = `正在连接 MCP (尝试 ${
297 |             retryCount + 1
298 |           }/${maxRetries})`;
299 |           connectButton.textContent = "取消";
300 |           addLogEntry(`尝试连接 MCP (${retryCount + 1}/${maxRetries})...`);
301 |         } else {
302 |           // disconnected
303 |           mcpConnected = false;
304 |           isConnecting = false;
305 |           connectionStatus.className = "connection-status status-disconnected";
306 |           connectionStatus.textContent = "未连接到 MCP";
307 |           connectButton.textContent = "连接";
308 |           addLogEntry("MCP 已断开连接");
309 |         }
310 |       }
311 | 
312 |       // 计算重连延迟,使用指数退避策略
313 |       function getReconnectDelay() {
314 |         // 1秒, 2秒, 4秒, 8秒...
315 |         return Math.min(1000 * Math.pow(2, retryCount), 30000);
316 |       }
317 | 
318 |       // 清除所有重连定时器
319 |       function clearReconnectTimer() {
320 |         if (reconnectTimer) {
321 |           clearTimeout(reconnectTimer);
322 |           reconnectTimer = null;
323 |         }
324 |       }
325 | 
326 |       // 重置连接状态
327 |       function resetConnectionState() {
328 |         isConnecting = false;
329 |         isManualDisconnect = false;
330 |         clearReconnectTimer();
331 | 
332 |         if (ws) {
333 |           try {
334 |             ws.close();
335 |           } catch (e) {
336 |             // 忽略关闭错误
337 |           }
338 |           ws = null;
339 |         }
340 |       }
341 | 
342 |       // 尝试重连
343 |       function attemptReconnect() {
344 |         if (
345 |           isManualDisconnect ||
346 |           !autoReconnectCheckbox.checked ||
347 |           retryCount >= maxRetries
348 |         ) {
349 |           if (retryCount >= maxRetries) {
350 |             addLogEntry(`已达到最大重试次数 (${maxRetries}),停止重连`);
351 |           }
352 |           setMcpConnectionStatus("disconnected");
353 |           retryCount = 0;
354 |           return;
355 |         }
356 | 
357 |         retryCount++;
358 |         setMcpConnectionStatus("connecting");
359 | 
360 |         const delay = getReconnectDelay();
361 |         addLogEntry(`将在 ${delay / 1000}秒后重新连接...`);
362 | 
363 |         clearReconnectTimer();
364 |         reconnectTimer = setTimeout(() => {
365 |           connectToMcp();
366 |         }, delay);
367 |       }
368 | 
369 |       // 连接到 MCP 服务器
370 |       function connectToMcp() {
371 |         // 如果已经连接或正在连接,返回
372 |         if (mcpConnected) {
373 |           // 如果已连接,则断开
374 |           isManualDisconnect = true;
375 |           resetConnectionState();
376 |           setMcpConnectionStatus("disconnected");
377 |           return;
378 |         }
379 | 
380 |         // 如果正在尝试连接,则取消
381 |         if (isConnecting) {
382 |           isManualDisconnect = true;
383 |           resetConnectionState();
384 |           setMcpConnectionStatus("disconnected");
385 |           return;
386 |         }
387 | 
388 |         // 清除之前的连接
389 |         resetConnectionState();
390 |         isManualDisconnect = false;
391 | 
392 |         const serverUrl = serverUrlInput.value.trim();
393 |         if (!serverUrl) {
394 |           addLogEntry("错误: 服务器 URL 不能为空");
395 |           return;
396 |         }
397 | 
398 |         try {
399 |           setMcpConnectionStatus("connecting");
400 | 
401 |           // 创建 WebSocket 连接
402 |           ws = new WebSocket(serverUrl);
403 | 
404 |           ws.onopen = function () {
405 |             setMcpConnectionStatus("connected");
406 | 
407 |             // 发送初始化消息
408 |             ws.send(
409 |               JSON.stringify({
410 |                 type: "figma-plugin-connected",
411 |                 pluginId: "figma-mcp-canvas-tools",
412 |               })
413 |             );
414 |           };
415 | 
416 |           ws.onmessage = function (event) {
417 |             try {
418 |               const message = JSON.parse(event.data);
419 |               addLogEntry(`收到 MCP 命令: ${message.command}`);
420 | 
421 |               // 转发给插件代码
422 |               parent.postMessage(
423 |                 {
424 |                   pluginMessage: {
425 |                     type: "mcp-command",
426 |                     command: message.command,
427 |                     params: message.params || {},
428 |                   },
429 |                 },
430 |                 "*"
431 |               );
432 |             } catch (error) {
433 |               addLogEntry(`解析消息错误: ${error.message}`);
434 |             }
435 |           };
436 | 
437 |           ws.onclose = function () {
438 |             // 只有在不是手动断开连接的情况下才尝试重连
439 |             if (!isManualDisconnect) {
440 |               addLogEntry("与 MCP 服务器的连接已关闭");
441 |               attemptReconnect();
442 |             } else {
443 |               setMcpConnectionStatus("disconnected");
444 |             }
445 |             ws = null;
446 |           };
447 | 
448 |           ws.onerror = function (error) {
449 |             addLogEntry(`WebSocket 错误: ${error.message || "未知错误"}`);
450 |             // 错误会触发关闭事件,关闭事件会处理重连
451 |           };
452 |         } catch (error) {
453 |           addLogEntry(`连接错误: ${error.message}`);
454 |           attemptReconnect();
455 |         }
456 |       }
457 | 
458 |       // 连接按钮点击事件
459 |       connectButton.addEventListener("click", connectToMcp);
460 | 
461 |       // 自动重连选项变更
462 |       autoReconnectCheckbox.addEventListener("change", function () {
463 |         if (this.checked) {
464 |           addLogEntry("自动重连已启用");
465 |           // 如果目前未连接并且不是手动断开,尝试立即连接
466 |           if (!mcpConnected && !isManualDisconnect && !isConnecting) {
467 |             retryCount = 0;
468 |             connectToMcp();
469 |           }
470 |         } else {
471 |           addLogEntry("自动重连已禁用");
472 |           isManualDisconnect = true;
473 |           clearReconnectTimer();
474 |         }
475 |       });
476 | 
477 |       // 监听来自插件代码的消息
478 |       window.addEventListener("message", (event) => {
479 |         const message = event.data.pluginMessage;
480 |         console.log("Received message from plugin:", message);
481 |         // 处理来自插件代码的消息
482 |         if (message && message.type === "mcp-response") {
483 |           addLogEntry(
484 |             `命令 ${message.command} ${
485 |               message.success ? "成功执行" : "执行失败"
486 |             }`
487 |           );
488 | 
489 |           // 如果连接了 MCP 服务器,则将响应发送给服务器
490 |           if (ws && ws.readyState === WebSocket.OPEN) {
491 |             ws.send(
492 |               JSON.stringify({
493 |                 type: "figma-plugin-response",
494 |                 success: message.success,
495 |                 command: message.command,
496 |                 result: message.result,
497 |                 error: message.error,
498 |               })
499 |             );
500 |           }
501 |         }
502 |       });
503 | 
504 |       // 页面加载后自动尝试连接
505 |       window.addEventListener("load", () => {
506 |         if (autoReconnectCheckbox.checked) {
507 |           // 小延迟后开始连接,给 UI 渲染一些时间
508 |           setTimeout(() => {
509 |             connectToMcp();
510 |           }, 1000);
511 |         }
512 |       });
513 |     </script>
514 |   </body>
515 | </html>
516 | 
```

--------------------------------------------------------------------------------
/src/tools/canvas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Canvas Tools - MCP server tools for interacting with Figma canvas elements
  3 |  */
  4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import {
  6 |   isPluginConnected,
  7 |   sendCommandToPlugin,
  8 | } from "../services/websocket.js";
  9 | import { logError } from "../utils.js";
 10 | import {
 11 |   arcParams,
 12 |   elementParams,
 13 |   elementsParams,
 14 |   ellipseParams,
 15 |   lineParams,
 16 |   polygonParams,
 17 |   rectangleParams,
 18 |   starParams,
 19 |   textParams,
 20 |   vectorParams,
 21 | } from "./zod-schemas.js";
 22 | 
 23 | /**
 24 |  * Register canvas-related tools with the MCP server
 25 |  * @param server The MCP server instance
 26 |  */
 27 | export function registerCanvasTools(server: McpServer) {
 28 |   // Create a rectangle in Figma
 29 |   server.tool("create_rectangle", rectangleParams, async (params) => {
 30 |     try {
 31 |       // Send command to Figma plugin
 32 |       const response = await sendCommandToPlugin(
 33 |         "create-rectangle",
 34 |         params
 35 |       ).catch((error: Error) => {
 36 |         throw error;
 37 |       });
 38 | 
 39 |       if (!response.success) {
 40 |         throw new Error(response.error || "Unknown error");
 41 |       }
 42 | 
 43 |       return {
 44 |         content: [
 45 |           { type: "text", text: `# Rectangle Created Successfully` },
 46 |           {
 47 |             type: "text",
 48 |             text: `A new rectangle has been created in your Figma canvas.`,
 49 |           },
 50 |           {
 51 |             type: "text",
 52 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`,
 53 |           },
 54 |           {
 55 |             type: "text",
 56 |             text:
 57 |               response.result && response.result.id
 58 |                 ? `Node ID: ${response.result.id}`
 59 |                 : `Creation successful`,
 60 |           },
 61 |         ],
 62 |       };
 63 |     } catch (error: unknown) {
 64 |       logError("Error creating rectangle in Figma", error);
 65 |       return {
 66 |         content: [
 67 |           {
 68 |             type: "text",
 69 |             text: `Error creating rectangle: ${
 70 |               error instanceof Error ? error.message : "Unknown error"
 71 |             }`,
 72 |           },
 73 |           {
 74 |             type: "text",
 75 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
 76 |           },
 77 |         ],
 78 |       };
 79 |     }
 80 |   });
 81 | 
 82 |   // Create a circle in Figma
 83 |   server.tool("create_circle", ellipseParams, async (params) => {
 84 |     try {
 85 |       // Send command to Figma plugin
 86 |       const response = await sendCommandToPlugin("create-circle", params).catch(
 87 |         (error: Error) => {
 88 |           throw error;
 89 |         }
 90 |       );
 91 | 
 92 |       if (!response.success) {
 93 |         throw new Error(response.error || "Unknown error");
 94 |       }
 95 | 
 96 |       return {
 97 |         content: [
 98 |           { type: "text", text: `# Circle Created Successfully` },
 99 |           {
100 |             type: "text",
101 |             text: `A new circle has been created in your Figma canvas.`,
102 |           },
103 |           {
104 |             type: "text",
105 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`,
106 |           },
107 |           {
108 |             type: "text",
109 |             text:
110 |               response.result && response.result.id
111 |                 ? `Node ID: ${response.result.id}`
112 |                 : `Creation successful`,
113 |           },
114 |         ],
115 |       };
116 |     } catch (error: unknown) {
117 |       logError("Error creating circle in Figma", error);
118 |       return {
119 |         content: [
120 |           {
121 |             type: "text",
122 |             text: `Error creating circle: ${
123 |               error instanceof Error ? error.message : "Unknown error"
124 |             }`,
125 |           },
126 |           {
127 |             type: "text",
128 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
129 |           },
130 |         ],
131 |       };
132 |     }
133 |   });
134 | 
135 |   // Create an arc (partial ellipse) in Figma
136 |   server.tool("create_arc", arcParams, async (params) => {
137 |     try {
138 |       // Prepare parameters for the plugin
139 |       const arcParams = {
140 |         ...params,
141 |         startAngle: params.startAngle,
142 |         endAngle: params.endAngle,
143 |         innerRadius: params.innerRadius,
144 |       };
145 | 
146 |       // Send command to Figma plugin
147 |       const response = await sendCommandToPlugin("create-arc", arcParams).catch(
148 |         (error: Error) => {
149 |           throw error;
150 |         }
151 |       );
152 | 
153 |       if (!response.success) {
154 |         throw new Error(response.error || "Unknown error");
155 |       }
156 | 
157 |       return {
158 |         content: [
159 |           { type: "text", text: `# Arc Created Successfully` },
160 |           {
161 |             type: "text",
162 |             text: `A new arc has been created in your Figma canvas.`,
163 |           },
164 |           {
165 |             type: "text",
166 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Angles: ${params.startAngle}° to ${params.endAngle}°\n- Inner radius: ${params.innerRadius}\n- Color: ${params.color}`,
167 |           },
168 |           {
169 |             type: "text",
170 |             text:
171 |               response.result && response.result.id
172 |                 ? `Node ID: ${response.result.id}`
173 |                 : `Creation successful`,
174 |           },
175 |         ],
176 |       };
177 |     } catch (error: unknown) {
178 |       logError("Error creating arc in Figma", error);
179 |       return {
180 |         content: [
181 |           {
182 |             type: "text",
183 |             text: `Error creating arc: ${
184 |               error instanceof Error ? error.message : "Unknown error"
185 |             }`,
186 |           },
187 |           {
188 |             type: "text",
189 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
190 |           },
191 |         ],
192 |       };
193 |     }
194 |   });
195 | 
196 |   // Create a polygon in Figma
197 |   server.tool("create_polygon", polygonParams, async (params) => {
198 |     try {
199 |       // Prepare parameters for the plugin
200 |       const polygonParams = {
201 |         ...params,
202 |         pointCount: params.pointCount,
203 |       };
204 | 
205 |       // Send command to Figma plugin
206 |       const response = await sendCommandToPlugin(
207 |         "create-polygon",
208 |         polygonParams
209 |       ).catch((error: Error) => {
210 |         throw error;
211 |       });
212 | 
213 |       if (!response.success) {
214 |         throw new Error(response.error || "Unknown error");
215 |       }
216 | 
217 |       return {
218 |         content: [
219 |           { type: "text", text: `# Polygon Created Successfully` },
220 |           {
221 |             type: "text",
222 |             text: `A new polygon has been created in your Figma canvas.`,
223 |           },
224 |           {
225 |             type: "text",
226 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Sides: ${params.pointCount}\n- Color: ${params.color}`,
227 |           },
228 |           {
229 |             type: "text",
230 |             text:
231 |               response.result && response.result.id
232 |                 ? `Node ID: ${response.result.id}`
233 |                 : `Creation successful`,
234 |           },
235 |         ],
236 |       };
237 |     } catch (error: unknown) {
238 |       logError("Error creating polygon in Figma", error);
239 |       return {
240 |         content: [
241 |           {
242 |             type: "text",
243 |             text: `Error creating polygon: ${
244 |               error instanceof Error ? error.message : "Unknown error"
245 |             }`,
246 |           },
247 |           {
248 |             type: "text",
249 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
250 |           },
251 |         ],
252 |       };
253 |     }
254 |   });
255 | 
256 |   // Create a star in Figma
257 |   server.tool("create_star", starParams, async (params) => {
258 |     try {
259 |       // Prepare parameters for the plugin
260 |       const starParams = {
261 |         ...params,
262 |         pointCount: params.pointCount,
263 |         innerRadius: params.innerRadius,
264 |       };
265 | 
266 |       // Send command to Figma plugin
267 |       const response = await sendCommandToPlugin(
268 |         "create-star",
269 |         starParams
270 |       ).catch((error: Error) => {
271 |         throw error;
272 |       });
273 | 
274 |       if (!response.success) {
275 |         throw new Error(response.error || "Unknown error");
276 |       }
277 | 
278 |       return {
279 |         content: [
280 |           { type: "text", text: `# Star Created Successfully` },
281 |           {
282 |             type: "text",
283 |             text: `A new star has been created in your Figma canvas.`,
284 |           },
285 |           {
286 |             type: "text",
287 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Points: ${params.pointCount}\n- Inner Radius: ${params.innerRadius}\n- Color: ${params.color}`,
288 |           },
289 |           {
290 |             type: "text",
291 |             text:
292 |               response.result && response.result.id
293 |                 ? `Node ID: ${response.result.id}`
294 |                 : `Creation successful`,
295 |           },
296 |         ],
297 |       };
298 |     } catch (error: unknown) {
299 |       logError("Error creating star in Figma", error);
300 |       return {
301 |         content: [
302 |           {
303 |             type: "text",
304 |             text: `Error creating star: ${
305 |               error instanceof Error ? error.message : "Unknown error"
306 |             }`,
307 |           },
308 |           {
309 |             type: "text",
310 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
311 |           },
312 |         ],
313 |       };
314 |     }
315 |   });
316 | 
317 |   // Create a vector in Figma
318 |   server.tool("create_vector", vectorParams, async (params) => {
319 |     try {
320 |       // Prepare parameters for the plugin
321 |       const vectorParams = {
322 |         ...params,
323 |         vectorNetwork: params.vectorNetwork,
324 |         vectorPaths: params.vectorPaths,
325 |         handleMirroring: params.handleMirroring,
326 |       };
327 | 
328 |       // Send command to Figma plugin
329 |       const response = await sendCommandToPlugin(
330 |         "create-vector",
331 |         vectorParams
332 |       ).catch((error: Error) => {
333 |         throw error;
334 |       });
335 | 
336 |       if (!response.success) {
337 |         throw new Error(response.error || "Unknown error");
338 |       }
339 | 
340 |       return {
341 |         content: [
342 |           { type: "text", text: `# Vector Created Successfully` },
343 |           {
344 |             type: "text",
345 |             text: `A new vector has been created in your Figma canvas.`,
346 |           },
347 |           {
348 |             type: "text",
349 |             text: `- Position: (${params.x}, ${params.y})\n- Size: ${params.width}×${params.height}px\n- Color: ${params.color}`,
350 |           },
351 |           {
352 |             type: "text",
353 |             text:
354 |               response.result && response.result.id
355 |                 ? `Node ID: ${response.result.id}`
356 |                 : `Creation successful`,
357 |           },
358 |         ],
359 |       };
360 |     } catch (error: unknown) {
361 |       logError("Error creating vector in Figma", error);
362 |       return {
363 |         content: [
364 |           {
365 |             type: "text",
366 |             text: `Error creating vector: ${
367 |               error instanceof Error ? error.message : "Unknown error"
368 |             }`,
369 |           },
370 |           {
371 |             type: "text",
372 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
373 |           },
374 |         ],
375 |       };
376 |     }
377 |   });
378 | 
379 |   // Create a line in Figma
380 |   server.tool("create_line", lineParams, async (params) => {
381 |     try {
382 |       // Send command to Figma plugin
383 |       const response = await sendCommandToPlugin("create-line", params).catch(
384 |         (error: Error) => {
385 |           throw error;
386 |         }
387 |       );
388 | 
389 |       if (!response.success) {
390 |         throw new Error(response.error || "Unknown error");
391 |       }
392 | 
393 |       return {
394 |         content: [
395 |           { type: "text", text: `# Line Created Successfully` },
396 |           {
397 |             type: "text",
398 |             text: `A new line has been created in your Figma canvas.`,
399 |           },
400 |           {
401 |             type: "text",
402 |             text: `- Position: (${params.x}, ${params.y})\n- Length: ${params.width}px\n- Color: ${params.color}`,
403 |           },
404 |           {
405 |             type: "text",
406 |             text: `- Rotation: ${params.rotation || 0}°`,
407 |           },
408 |           {
409 |             type: "text",
410 |             text:
411 |               response.result && response.result.id
412 |                 ? `Node ID: ${response.result.id}`
413 |                 : `Creation successful`,
414 |           },
415 |         ],
416 |       };
417 |     } catch (error: unknown) {
418 |       logError("Error creating line in Figma", error);
419 |       return {
420 |         content: [
421 |           {
422 |             type: "text",
423 |             text: `Error creating line: ${
424 |               error instanceof Error ? error.message : "Unknown error"
425 |             }`,
426 |           },
427 |           {
428 |             type: "text",
429 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
430 |           },
431 |         ],
432 |       };
433 |     }
434 |   });
435 | 
436 |   // Create text in Figma
437 |   server.tool("create_text", textParams, async (params) => {
438 |     try {
439 |       // Send command to Figma plugin
440 |       const response = await sendCommandToPlugin("create-text", params).catch(
441 |         (error: Error) => {
442 |           throw error;
443 |         }
444 |       );
445 | 
446 |       if (!response.success) {
447 |         throw new Error(response.error || "Unknown error");
448 |       }
449 | 
450 |       return {
451 |         content: [
452 |           { type: "text", text: `# Text Created Successfully` },
453 |           {
454 |             type: "text",
455 |             text: `New text has been created in your Figma canvas.`,
456 |           },
457 |           {
458 |             type: "text",
459 |             text: `- Position: (${params.x}, ${params.y})\n- Font Size: ${params.fontSize}px\n- Content: "${params.text}"`,
460 |           },
461 |           {
462 |             type: "text",
463 |             text:
464 |               response.result && response.result.id
465 |                 ? `Node ID: ${response.result.id}`
466 |                 : `Creation successful`,
467 |           },
468 |         ],
469 |       };
470 |     } catch (error: unknown) {
471 |       logError("Error creating text in Figma", error);
472 |       return {
473 |         content: [
474 |           {
475 |             type: "text",
476 |             text: `Error creating text: ${
477 |               error instanceof Error ? error.message : "Unknown error"
478 |             }`,
479 |           },
480 |           {
481 |             type: "text",
482 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
483 |           },
484 |         ],
485 |       };
486 |     }
487 |   });
488 | 
489 |   // Get current selection in Figma
490 |   server.tool("get_selection", {}, async () => {
491 |     try {
492 |       // Send command to Figma plugin
493 |       const response = await sendCommandToPlugin("get-selection", {}).catch(
494 |         (error: Error) => {
495 |           throw error;
496 |         }
497 |       );
498 | 
499 |       if (!response.success) {
500 |         throw new Error(response.error || "Unknown error");
501 |       }
502 | 
503 |       return {
504 |         content: [
505 |           { type: "text", text: `# Current Selection` },
506 |           {
507 |             type: "text",
508 |             text: `Information about currently selected elements in Figma:`,
509 |           },
510 |           {
511 |             type: "text",
512 |             text: response.result
513 |               ? JSON.stringify(response.result, null, 2)
514 |               : "No selection information available",
515 |           },
516 |         ],
517 |       };
518 |     } catch (error: unknown) {
519 |       logError("Error getting selection in Figma", error);
520 |       return {
521 |         content: [
522 |           {
523 |             type: "text",
524 |             text: `Error getting selection: ${
525 |               error instanceof Error ? error.message : "Unknown error"
526 |             }`,
527 |           },
528 |           {
529 |             type: "text",
530 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
531 |           },
532 |         ],
533 |       };
534 |     }
535 |   });
536 | 
537 |   // Check plugin connection status
538 |   server.tool("check_connection", {}, async () => {
539 |     return {
540 |       content: [
541 |         { type: "text", text: `# Figma Plugin Connection Status` },
542 |         {
543 |           type: "text",
544 |           text: isPluginConnected()
545 |             ? `✅ Figma plugin is connected to MCP server`
546 |             : `❌ No Figma plugin is currently connected`,
547 |         },
548 |         {
549 |           type: "text",
550 |           text: isPluginConnected()
551 |             ? `You can now use MCP tools to interact with the Figma canvas.`
552 |             : `Please make sure the Figma plugin is running and connected to the MCP server.`,
553 |         },
554 |       ],
555 |     };
556 |   });
557 | 
558 |   // Get all elements from current page or specified page
559 |   server.tool("get_elements", elementsParams, async (params) => {
560 |     try {
561 |       // Send command to Figma plugin
562 |       const response = await sendCommandToPlugin("get-elements", params).catch(
563 |         (error: Error) => {
564 |           throw error;
565 |         }
566 |       );
567 | 
568 |       if (!response.success) {
569 |         throw new Error(response.error || "Unknown error");
570 |       }
571 | 
572 |       const elements = response.result;
573 |       const count = Array.isArray(elements) ? elements.length : 0;
574 |       const typeValue = params.type || "ALL";
575 |       const pageName = params.page_id ? `specified page` : "current page";
576 | 
577 |       return {
578 |         content: [
579 |           { type: "text", text: `# Elements Retrieved` },
580 |           {
581 |             type: "text",
582 |             text: `Found ${count} element${
583 |               count !== 1 ? "s" : ""
584 |             } of type ${typeValue} on ${pageName}.`,
585 |           },
586 |           {
587 |             type: "text",
588 |             text:
589 |               count > 0
590 |                 ? `Element information: ${JSON.stringify(elements, null, 2)}`
591 |                 : "No elements matched your criteria.",
592 |           },
593 |         ],
594 |       };
595 |     } catch (error: unknown) {
596 |       logError("Error getting elements from Figma", error);
597 |       return {
598 |         content: [
599 |           {
600 |             type: "text",
601 |             text: `Error retrieving elements: ${
602 |               error instanceof Error ? error.message : "Unknown error"
603 |             }`,
604 |           },
605 |           {
606 |             type: "text",
607 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
608 |           },
609 |         ],
610 |       };
611 |     }
612 |   });
613 | 
614 |   // Get a specific element by ID
615 |   server.tool("get_element", elementParams, async (params) => {
616 |     try {
617 |       // Send command to Figma plugin
618 |       const response = await sendCommandToPlugin("get-element", params).catch(
619 |         (error: Error) => {
620 |           throw error;
621 |         }
622 |       );
623 | 
624 |       if (!response.success) {
625 |         throw new Error(response.error || "Unknown error");
626 |       }
627 | 
628 |       const element = response.result;
629 |       const isArray = Array.isArray(element);
630 |       const hasChildren = isArray && element.length > 1;
631 | 
632 |       return {
633 |         content: [
634 |           { type: "text", text: `# Element Retrieved` },
635 |           {
636 |             type: "text",
637 |             text: `Successfully retrieved element with ID: ${params.node_id}`,
638 |           },
639 |           {
640 |             type: "text",
641 |             text: hasChildren
642 |               ? `Element and ${element.length - 1} children retrieved.`
643 |               : `Element information:`,
644 |           },
645 |           {
646 |             type: "text",
647 |             text: JSON.stringify(element, null, 2),
648 |           },
649 |         ],
650 |       };
651 |     } catch (error: unknown) {
652 |       logError("Error getting element from Figma", error);
653 |       return {
654 |         content: [
655 |           {
656 |             type: "text",
657 |             text: `Error retrieving element: ${
658 |               error instanceof Error ? error.message : "Unknown error"
659 |             }`,
660 |           },
661 |           {
662 |             type: "text",
663 |             text: `Make sure the Figma plugin is running and connected to the MCP server.`,
664 |           },
665 |         ],
666 |       };
667 |     }
668 |   });
669 | }
670 | 
```

--------------------------------------------------------------------------------
/src/tools/zod-schemas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Figma Zod Schemas
  3 |  * Zod schemas for validating Figma API parameters
  4 |  */
  5 | 
  6 | import { z } from "zod";
  7 | 
  8 | // Base types
  9 | export const colorSchema = z.object({
 10 |   r: z.number().min(0).max(1).describe("Red channel (0-1)"),
 11 |   g: z.number().min(0).max(1).describe("Green channel (0-1)"),
 12 |   b: z.number().min(0).max(1).describe("Blue channel (0-1)"),
 13 | });
 14 | 
 15 | // Position and size base params
 16 | const positionParams = {
 17 |   x: z.number().default(0).describe("X position of the element"),
 18 |   y: z.number().default(0).describe("Y position of the element"),
 19 | };
 20 | 
 21 | const sizeParams = {
 22 |   width: z.number().min(1).default(100).describe("Width of the element in pixels"),
 23 |   height: z.number().min(1).default(100).describe("Height of the element in pixels"),
 24 | };
 25 | 
 26 | // Base node properties
 27 | const baseNodeParams = {
 28 |   name: z.string().optional().describe("Name of the node"),
 29 | };
 30 | 
 31 | // Scene node properties
 32 | const sceneNodeParams = {
 33 |   visible: z.boolean().optional().describe("Whether the node is visible"),
 34 |   locked: z.boolean().optional().describe("Whether the node is locked"),
 35 | };
 36 | 
 37 | // Blend-related properties
 38 | const blendParams = {
 39 |   opacity: z.number().min(0).max(1).optional().describe("Opacity of the node (0-1)"),
 40 |   blendMode: z.enum([
 41 |     "NORMAL", "DARKEN", "MULTIPLY", "LINEAR_BURN", "COLOR_BURN", 
 42 |     "LIGHTEN", "SCREEN", "LINEAR_DODGE", "COLOR_DODGE", 
 43 |     "OVERLAY", "SOFT_LIGHT", "HARD_LIGHT", 
 44 |     "DIFFERENCE", "EXCLUSION", "SUBTRACT", "DIVIDE", 
 45 |     "HUE", "SATURATION", "COLOR", "LUMINOSITY",
 46 |     "PASS_THROUGH"
 47 |   ]).optional().describe("Blend mode of the node"),
 48 |   isMask: z.boolean().optional().describe("Whether this node is a mask"),
 49 |   maskType: z.enum(["ALPHA", "LUMINANCE"]).optional().describe("Type of masking to use if this node is a mask"),
 50 |   effects: z.array(z.any()).optional().describe("Array of effects"),
 51 |   effectStyleId: z.string().optional().describe("The id of the EffectStyle object"),
 52 | };
 53 | 
 54 | // Corner-related properties
 55 | const cornerParams = {
 56 |   cornerRadius: z.number().min(0).optional().describe("Rounds all corners by this amount"),
 57 |   cornerSmoothing: z.number().min(0).max(1).optional().describe("Corner smoothing between 0 and 1"),
 58 |   topLeftRadius: z.number().min(0).optional().describe("Top left corner radius override"),
 59 |   topRightRadius: z.number().min(0).optional().describe("Top right corner radius override"),
 60 |   bottomLeftRadius: z.number().min(0).optional().describe("Bottom left corner radius override"),
 61 |   bottomRightRadius: z.number().min(0).optional().describe("Bottom right corner radius override"),
 62 | };
 63 | 
 64 | // Geometry-related properties
 65 | const geometryParams = {
 66 |   fills: z.array(z.any()).optional().describe("The paints used to fill the area of the shape"),
 67 |   fillStyleId: z.string().optional().describe("The id of the PaintStyle object linked to fills"),
 68 |   strokes: z.array(z.any()).optional().describe("The paints used to fill the area of the shape's strokes"),
 69 |   strokeStyleId: z.string().optional().describe("The id of the PaintStyle object linked to strokes"),
 70 |   strokeWeight: z.number().min(0).optional().describe("The thickness of the stroke, in pixels"),
 71 |   strokeJoin: z.enum(["MITER", "BEVEL", "ROUND"]).optional().describe("The decoration applied to vertices"),
 72 |   strokeAlign: z.enum(["CENTER", "INSIDE", "OUTSIDE"]).optional().describe("The alignment of the stroke"),
 73 |   dashPattern: z.array(z.number().min(0)).optional().describe("Array of numbers for dash pattern"),
 74 |   strokeCap: z.enum(["NONE", "ROUND", "SQUARE", "ARROW_LINES", "ARROW_EQUILATERAL"]).optional().describe("The decoration applied to vertices"),
 75 |   strokeMiterLimit: z.number().min(0).optional().describe("The miter limit on the stroke"),
 76 |   color: z.string().regex(/^#([0-9A-F]{6}|[0-9A-F]{8})$/i).default("#ff0000").describe("Fill color as hex code (#RRGGBB or #RRGGBBAA)"),
 77 | };
 78 | 
 79 | // Individual strokes-related properties
 80 | const rectangleStrokeParams = {
 81 |   strokeTopWeight: z.number().min(0).optional().describe("Top stroke weight"),
 82 |   strokeBottomWeight: z.number().min(0).optional().describe("Bottom stroke weight"),
 83 |   strokeLeftWeight: z.number().min(0).optional().describe("Left stroke weight"),
 84 |   strokeRightWeight: z.number().min(0).optional().describe("Right stroke weight"),
 85 | };
 86 | 
 87 | // Layout-related properties
 88 | const layoutParams = {
 89 |   minWidth: z.number().nullable().optional().describe("Minimum width constraint"),
 90 |   maxWidth: z.number().nullable().optional().describe("Maximum width constraint"),
 91 |   minHeight: z.number().nullable().optional().describe("Minimum height constraint"),
 92 |   maxHeight: z.number().nullable().optional().describe("Maximum height constraint"),
 93 |   layoutAlign: z.enum(["MIN", "CENTER", "MAX", "STRETCH", "INHERIT"]).optional().describe("Alignment within parent"),
 94 |   layoutGrow: z.number().min(0).default(0).optional().describe("Stretch along parent's primary axis"),
 95 |   layoutPositioning: z.enum(["AUTO", "ABSOLUTE"]).optional().describe("Layout positioning mode"),
 96 |   constrainProportions: z.boolean().optional().describe("Whether to keep proportions when resizing"),
 97 |   rotation: z.number().min(-180).max(180).optional().describe("Rotation in degrees (-180 to 180)"),
 98 |   layoutSizingHorizontal: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing mode"),
 99 |   layoutSizingVertical: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing mode"),
100 |   constraints: z.any().optional().describe("Constraints relative to containing frame"),
101 | };
102 | 
103 | // Common properties for all export settings
104 | const commonExportSettingsProps = {
105 |   contentsOnly: z.boolean().optional().describe("Whether only the contents of the node are exported. Defaults to true."),
106 |   useAbsoluteBounds: z.boolean().optional().describe("Use full dimensions regardless of cropping. Defaults to false."),
107 |   suffix: z.string().optional().describe("Suffix appended to the file name when exporting."),
108 |   colorProfile: z.enum(["DOCUMENT", "SRGB", "DISPLAY_P3_V4"]).optional().describe("Color profile of the export."),
109 | };
110 | 
111 | // Common SVG export properties
112 | const commonSvgExportProps = {
113 |   ...commonExportSettingsProps,
114 |   svgOutlineText: z.boolean().optional().describe("Whether text elements are rendered as outlines. Defaults to true."),
115 |   svgIdAttribute: z.boolean().optional().describe("Whether to include layer names as ID attributes. Defaults to false."),
116 |   svgSimplifyStroke: z.boolean().optional().describe("Whether to simplify inside and outside strokes. Defaults to true."),
117 | };
118 | 
119 | // Export constraints
120 | const exportConstraintsSchema = z.object({
121 |   type: z.enum(["SCALE", "WIDTH", "HEIGHT"]).describe("Type of constraint for the export"),
122 |   value: z.number().positive().describe("Value for the constraint")
123 | });
124 | 
125 | // Export Settings Image (JPG/PNG)
126 | const exportSettingsImageSchema = z.object({
127 |   format: z.enum(["JPG", "PNG"]).describe("The export format (JPG or PNG)"),
128 |   constraint: exportConstraintsSchema.optional().describe("Constraint on the image size when exporting"),
129 |   ...commonExportSettingsProps
130 | });
131 | 
132 | // Export Settings SVG
133 | const exportSettingsSvgSchema = z.object({
134 |   format: z.literal("SVG").describe("The export format (SVG)"),
135 |   ...commonSvgExportProps
136 | });
137 | 
138 | // Export Settings SVG String (for exportAsync only)
139 | const exportSettingsSvgStringSchema = z.object({
140 |   format: z.literal("SVG_STRING").describe("The export format (SVG_STRING)"),
141 |   ...commonSvgExportProps
142 | });
143 | 
144 | // Export Settings PDF
145 | const exportSettingsPdfSchema = z.object({
146 |   format: z.literal("PDF").describe("The export format (PDF)"),
147 |   ...commonExportSettingsProps
148 | });
149 | 
150 | // Export Settings REST
151 | const exportSettingsRestSchema = z.object({
152 |   format: z.literal("JSON_REST_V1").describe("The export format (JSON_REST_V1)"),
153 |   ...commonExportSettingsProps
154 | });
155 | 
156 | // Combined Export Settings type
157 | const exportSettingsSchema = z.discriminatedUnion("format", [
158 |   exportSettingsImageSchema,
159 |   exportSettingsSvgSchema,
160 |   exportSettingsSvgStringSchema,
161 |   exportSettingsPdfSchema,
162 |   exportSettingsRestSchema
163 | ]);
164 | 
165 | // Export-related properties
166 | const exportParams = {
167 |   exportSettings: z.array(exportSettingsSchema).optional().describe("Export settings stored on the node"),
168 | };
169 | 
170 | // Prototyping - Trigger and Action types
171 | const triggerSchema = z.enum([
172 |   "ON_CLICK",
173 |   "ON_HOVER",
174 |   "ON_PRESS",
175 |   "ON_DRAG",
176 |   "AFTER_TIMEOUT",
177 |   "MOUSE_ENTER",
178 |   "MOUSE_LEAVE",
179 |   "MOUSE_UP",
180 |   "MOUSE_DOWN",
181 |   "ON_KEY_DOWN"
182 | ]).nullable().describe("The trigger that initiates the prototype interaction");
183 | 
184 | // Action represents what happens when a trigger is activated
185 | const actionSchema = z.object({
186 |   type: z.enum([
187 |     "BACK",
188 |     "CLOSE",
189 |     "URL",
190 |     "NODE",
191 |     "SWAP",
192 |     "OVERLAY",
193 |     "SCROLL_TO",
194 |     "OPEN_LINK"
195 |   ]).describe("The type of action to perform"),
196 |   url: z.string().optional().describe("URL to navigate to if action type is URL"),
197 |   nodeID: z.string().optional().describe("ID of the node if action type is NODE"),
198 |   destinationID: z.string().optional().describe("Destination node ID"),
199 |   navigation: z.enum(["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO"]).optional().describe("Navigation type"),
200 |   transitionNode: z.string().optional().describe("ID of the node to use for transition"),
201 |   preserveScrollPosition: z.boolean().optional().describe("Whether to preserve scroll position"),
202 |   overlayRelativePosition: z.object({
203 |     x: z.number(),
204 |     y: z.number()
205 |   }).optional().describe("Relative position for overlay"),
206 |   // Additional properties can be added as needed based on Figma API
207 | });
208 | 
209 | // Reaction combines a trigger with actions for prototyping
210 | const reactionSchema = z.object({
211 |   action: actionSchema.optional().describe("DEPRECATED: The action triggered by this reaction"),
212 |   actions: z.array(actionSchema).optional().describe("The actions triggered by this reaction"),
213 |   trigger: triggerSchema.describe("The trigger that initiates this reaction")
214 | });
215 | 
216 | // Reaction properties
217 | const reactionParams = {
218 |   reactions: z.array(reactionSchema).optional().describe("List of reactions for prototyping"),
219 | };
220 | 
221 | // Annotation properties
222 | const annotationPropertySchema = z.object({
223 |   type: z.enum([
224 |     "width",
225 |     "height",
226 |     "fills",
227 |     "strokes",
228 |     "strokeWeight",
229 |     "cornerRadius",
230 |     "opacity",
231 |     "blendMode",
232 |     "effects",
233 |     "layoutConstraints",
234 |     "padding",
235 |     "itemSpacing",
236 |     "layoutMode",
237 |     "primaryAxisAlignment",
238 |     "counterAxisAlignment",
239 |     "fontName",
240 |     "fontSize",
241 |     "letterSpacing",
242 |     "lineHeight",
243 |     "textCase",
244 |     "textDecoration",
245 |     "textAlignHorizontal",
246 |     "textAlignVertical",
247 |     "characters"
248 |   ]).describe("The type of property being annotated"),
249 |   value: z.any().optional().describe("The value of the property (if applicable)")
250 | });
251 | 
252 | // Annotation schema
253 | const annotationSchema = z.object({
254 |   label: z.string().optional().describe("Text label for the annotation"),
255 |   labelMarkdown: z.string().optional().describe("Markdown-formatted text label"),
256 |   properties: z.array(annotationPropertySchema).optional().describe("Properties pinned in this annotation"),
257 |   categoryId: z.string().optional().describe("ID of the annotation category")
258 | });
259 | 
260 | // Annotation properties
261 | const annotationParams = {
262 |   annotations: z.array(annotationSchema).optional().describe("Annotations on the node"),
263 | };
264 | 
265 | // Line parameters (width represents length, height is always 0)
266 | export const lineParams = {
267 |   ...positionParams,
268 |   width: z.number().min(1).default(100).describe("Length of the line in pixels"),
269 |   ...baseNodeParams,
270 |   ...sceneNodeParams,
271 |   ...blendParams,
272 |   ...geometryParams,
273 |   ...layoutParams,
274 |   ...exportParams,
275 |   ...reactionParams,
276 |   ...annotationParams,
277 | };
278 | 
279 | // Combined parameters for rectangles
280 | export const rectangleParams = {
281 |   ...positionParams,
282 |   ...sizeParams,
283 |   ...baseNodeParams,
284 |   ...sceneNodeParams,
285 |   ...blendParams,
286 |   ...cornerParams,
287 |   ...geometryParams,
288 |   ...rectangleStrokeParams,
289 |   ...layoutParams,
290 |   ...exportParams,
291 |   ...reactionParams,
292 |   ...annotationParams,
293 | };
294 | 
295 | // Ellipse Arc data for creating arcs and donuts
296 | const arcDataSchema = z.object({
297 |   startingAngle: z.number().describe("Starting angle in degrees from 0 to 360"),
298 |   endingAngle: z.number().describe("Ending angle in degrees from 0 to 360"),
299 |   innerRadius: z.number().min(0).max(1).describe("Inner radius ratio from 0 to 1")
300 | });
301 | 
302 | // Circle/Ellipse parameters
303 | export const ellipseParams = {
304 |   ...positionParams,
305 |   ...sizeParams,
306 |   ...baseNodeParams,
307 |   ...sceneNodeParams,
308 |   ...blendParams,
309 |   ...geometryParams,
310 |   ...layoutParams,
311 |   ...exportParams,
312 |   ...reactionParams,
313 |   ...annotationParams,
314 |   arcData: arcDataSchema.optional().describe("Arc data for creating partial ellipses and donuts")
315 | };
316 | 
317 | // Text parameters
318 | export const textParams = {
319 |   ...positionParams,
320 |   ...baseNodeParams,
321 |   ...sceneNodeParams,
322 |   ...blendParams,
323 |   ...layoutParams,
324 |   ...exportParams,
325 |   ...reactionParams,
326 |   ...annotationParams,
327 |   text: z.string().default("Hello Figma!").describe("The text content"),
328 |   characters: z.string().optional().describe("Alternative for text content"),
329 |   fontSize: z.number().min(1).default(24).describe("The font size in pixels"),
330 |   fontFamily: z.string().optional().describe("Font family name"),
331 |   fontStyle: z.string().optional().describe("Font style (e.g., 'Regular', 'Bold')"),
332 |   fontName: z.object({
333 |     family: z.string().optional().describe("Font family name"),
334 |     style: z.string().optional().describe("Font style (e.g., 'Regular', 'Bold')"),
335 |   }).optional().describe("Font family and style"),
336 |   textAlignHorizontal: z.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]).optional().describe("Horizontal text alignment"),
337 |   textAlignVertical: z.enum(["TOP", "CENTER", "BOTTOM"]).optional().describe("Vertical text alignment"),
338 |   textAutoResize: z.enum(["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"]).optional().describe("How text box adjusts to fit characters"),
339 |   textTruncation: z.enum(["DISABLED", "ENDING"]).optional().describe("Whether text will truncate with ellipsis"),
340 |   maxLines: z.number().nullable().optional().describe("Max number of lines before truncation"),
341 |   paragraphIndent: z.number().optional().describe("Indentation of paragraphs"),
342 |   paragraphSpacing: z.number().optional().describe("Vertical distance between paragraphs"),
343 |   listSpacing: z.number().optional().describe("Vertical distance between lines of a list"),
344 |   hangingPunctuation: z.boolean().optional().describe("Whether punctuation hangs outside the text box"),
345 |   hangingList: z.boolean().optional().describe("Whether list counters/bullets hang outside the text box"),
346 |   autoRename: z.boolean().optional().describe("Whether to update node name based on text content"),
347 |   letterSpacing: z.union([
348 |     z.number(),
349 |     z.object({
350 |       value: z.number(),
351 |       unit: z.enum(["PIXELS", "PERCENT"])
352 |     })
353 |   ]).optional().describe("Letter spacing between characters"),
354 |   lineHeight: z.union([
355 |     z.number(),
356 |     z.object({
357 |       value: z.number(),
358 |       unit: z.enum(["PIXELS", "PERCENT"])
359 |     })
360 |   ]).optional().describe("Line height"),
361 |   leadingTrim: z.enum(["NONE", "CAP_HEIGHT", "BOTH"]).optional().describe("Removal of vertical space above/below text glyphs"),
362 |   textCase: z.enum(["ORIGINAL", "UPPER", "LOWER", "TITLE"]).optional().describe("Text case transformation"),
363 |   textDecoration: z.enum(["NONE", "UNDERLINE", "STRIKETHROUGH"]).optional().describe("Text decoration"),
364 |   textDecorationStyle: z.enum(["SOLID", "DASHED", "DOTTED", "WAVY", "DOUBLE"]).optional().describe("Text decoration style"),
365 |   textDecorationOffset: z.union([
366 |     z.number(),
367 |     z.object({
368 |       value: z.number(),
369 |       unit: z.enum(["PIXELS", "PERCENT"])
370 |     })
371 |   ]).optional().describe("Text decoration offset"),
372 |   textDecorationThickness: z.union([
373 |     z.number(),
374 |     z.object({
375 |       value: z.number(),
376 |       unit: z.enum(["PIXELS", "PERCENT"])
377 |     })
378 |   ]).optional().describe("Text decoration thickness"),
379 |   textDecorationColor: z.union([
380 |     z.object({
381 |       r: z.number().min(0).max(1),
382 |       g: z.number().min(0).max(1),
383 |       b: z.number().min(0).max(1),
384 |       a: z.number().min(0).max(1).optional()
385 |     }),
386 |     z.string()
387 |   ]).optional().describe("Text decoration color"),
388 |   textDecorationSkipInk: z.boolean().optional().describe("Whether text decoration skips descenders"),
389 |   textStyleId: z.string().optional().describe("ID of linked TextStyle object"),
390 |   hyperlink: z.object({
391 |     type: z.enum(["URL", "NODE"]),
392 |     url: z.string().optional(),
393 |     nodeID: z.string().optional()
394 |   }).nullable().optional().describe("Hyperlink target"),
395 |   fill: z.string().optional().describe("Fill color as hex code (shorthand for fills)"),
396 |   rangeStyles: z.array(
397 |     z.object({
398 |       start: z.number().describe("Start index (inclusive)"),
399 |       end: z.number().describe("End index (exclusive)"),
400 |       style: z.object({}).passthrough().describe("Style properties to apply to range")
401 |     })
402 |   ).optional().describe("Character-level styling for text ranges"),
403 |   width: z.number().optional().describe("Width of the text box")
404 | };
405 | 
406 | // Frame parameters
407 | export const frameParams = {
408 |   ...positionParams,
409 |   ...sizeParams,
410 |   ...baseNodeParams,
411 |   ...sceneNodeParams,
412 |   ...blendParams,
413 |   ...cornerParams,
414 |   ...geometryParams,
415 |   ...layoutParams,
416 |   ...exportParams,
417 |   ...reactionParams,
418 |   ...annotationParams,
419 |   itemSpacing: z.number().min(0).optional().describe("Space between children in auto-layout"),
420 |   layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction"),
421 |   primaryAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().describe("How frame sizes along primary axis"),
422 |   counterAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().describe("How frame sizes along counter axis"),
423 |   primaryAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]).optional().describe("Alignment along primary axis"),
424 |   counterAxisAlignItems: z.enum(["MIN", "CENTER", "MAX"]).optional().describe("Alignment along counter axis"),
425 |   paddingLeft: z.number().min(0).optional().describe("Padding on left side"),
426 |   paddingRight: z.number().min(0).optional().describe("Padding on right side"),
427 |   paddingTop: z.number().min(0).optional().describe("Padding on top side"),
428 |   paddingBottom: z.number().min(0).optional().describe("Padding on bottom side"),
429 | };
430 | 
431 | // Arc parameters (based on ellipse parameters with added angle parameters)
432 | export const arcParams = {
433 |   ...ellipseParams,
434 |   startAngle: z.number().default(0).describe("Starting angle in degrees"),
435 |   endAngle: z.number().default(180).describe("Ending angle in degrees"),
436 |   innerRadius: z.number().min(0).max(1).default(0).describe("Inner radius ratio (0-1) for donut shapes")
437 | };
438 | 
439 | // Polygon parameters
440 | export const polygonParams = {
441 |   ...positionParams,
442 |   ...sizeParams,
443 |   ...baseNodeParams,
444 |   ...sceneNodeParams,
445 |   ...blendParams,
446 |   ...cornerParams,
447 |   ...geometryParams,
448 |   ...layoutParams,
449 |   ...exportParams,
450 |   ...reactionParams,
451 |   ...annotationParams,
452 |   pointCount: z.number().int().min(3).default(3).describe("Number of sides of the polygon. Must be an integer >= 3.")
453 | };
454 | 
455 | // Star parameters
456 | export const starParams = {
457 |   ...positionParams,
458 |   ...sizeParams,
459 |   ...baseNodeParams,
460 |   ...sceneNodeParams,
461 |   ...blendParams,
462 |   ...geometryParams,
463 |   ...layoutParams,
464 |   ...exportParams,
465 |   ...reactionParams,
466 |   ...annotationParams,
467 |   pointCount: z.number().int().min(3).default(5).describe("Number of points on the star. Must be an integer >= 3."),
468 |   innerRadius: z.number().min(0).max(1).default(0.5).describe("Ratio of inner radius to outer radius (0-1).")
469 | };
470 | 
471 | // Vector parameters for Vector Node
472 | export const vectorParams = {
473 |   ...positionParams,
474 |   ...sizeParams,
475 |   ...baseNodeParams,
476 |   ...sceneNodeParams,
477 |   ...blendParams,
478 |   ...cornerParams,
479 |   ...geometryParams,
480 |   ...layoutParams,
481 |   ...exportParams,
482 |   ...reactionParams,
483 |   ...annotationParams,
484 |   // Vector specific parameters
485 |   vectorNetwork: z.any().optional().describe("Complete representation of vectors as a network of edges between vertices"),
486 |   vectorPaths: z.any().optional().describe("Simple representation of vectors as paths"),
487 |   handleMirroring: z.enum(["NONE", "ANGLE", "ANGLE_AND_LENGTH"]).optional().describe("Whether the vector handles are mirrored or independent")
488 | };
489 | 
490 | export const elementParams = {
491 |   node_id: z.string().describe("ID of the element to retrieve"),
492 |   include_children: z.boolean().optional().default(false).describe("Whether to include children of the element"),
493 | }
494 | 
495 | export const elementsParams = {
496 |   type: z.enum([
497 |     "ALL", "RECTANGLE", "ELLIPSE", "POLYGON", "STAR", "VECTOR", 
498 |     "TEXT", "FRAME", "COMPONENT", "INSTANCE", "BOOLEAN_OPERATION", 
499 |     "GROUP", "SECTION", "SLICE", "LINE", "CONNECTOR", "SHAPE_WITH_TEXT", 
500 |     "CODE_BLOCK", "STAMP", "WIDGET", "STICKY", "TABLE", "SECTION", "HIGHLIGHT"
501 |   ]).optional().default("ALL").describe("Type of elements to filter (default: ALL)"),
502 |   page_id: z.string().optional().describe("ID of page to get elements from (default: current page)"),
503 |   limit: z.number().int().min(1).max(1000).optional().default(100).describe("Maximum number of elements to return"),
504 |   include_hidden: z.boolean().optional().default(false).describe("Whether to include hidden elements"),
505 | };
506 | 
```
Page 2/3FirstPrevNextLast