This is page 3 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/plugin/code.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/plugin/utils/colorUtils.ts 2 | function hexToRgb(hex) { 3 | hex = hex.replace("#", ""); 4 | const r = parseInt(hex.substring(0, 2), 16) / 255; 5 | const g = parseInt(hex.substring(2, 4), 16) / 255; 6 | const b = parseInt(hex.substring(4, 6), 16) / 255; 7 | return { r, g, b }; 8 | } 9 | function createSolidPaint(color) { 10 | if (typeof color === "string") { 11 | return { type: "SOLID", color: hexToRgb(color) }; 12 | } 13 | return { type: "SOLID", color }; 14 | } 15 | 16 | // src/plugin/utils/nodeUtils.ts 17 | function applyCommonProperties(node, data) { 18 | if (data.x !== undefined) 19 | node.x = data.x; 20 | if (data.y !== undefined) 21 | node.y = data.y; 22 | if (data.name) 23 | node.name = data.name; 24 | if (data.opacity !== undefined && "opacity" in node) { 25 | node.opacity = data.opacity; 26 | } 27 | if (data.blendMode && "blendMode" in node) { 28 | node.blendMode = data.blendMode; 29 | } 30 | if (data.effects && "effects" in node) { 31 | node.effects = data.effects; 32 | } 33 | if (data.constraints && "constraints" in node) { 34 | node.constraints = data.constraints; 35 | } 36 | if (data.isMask !== undefined && "isMask" in node) { 37 | node.isMask = data.isMask; 38 | } 39 | if (data.visible !== undefined) 40 | node.visible = data.visible; 41 | if (data.locked !== undefined) 42 | node.locked = data.locked; 43 | } 44 | function selectAndFocusNodes(nodes) { 45 | const nodesToFocus = Array.isArray(nodes) ? nodes : [nodes]; 46 | figma.currentPage.selection = nodesToFocus; 47 | figma.viewport.scrollAndZoomIntoView(nodesToFocus); 48 | } 49 | function buildResultObject(result) { 50 | let resultObject = {}; 51 | if (!result) 52 | return resultObject; 53 | if (Array.isArray(result)) { 54 | resultObject.count = result.length; 55 | if (result.length > 0) { 56 | resultObject.items = result.map((node) => ({ 57 | id: node.id, 58 | type: node.type, 59 | name: node.name 60 | })); 61 | } 62 | } else { 63 | const node = result; 64 | resultObject.id = node.id; 65 | resultObject.type = node.type; 66 | resultObject.name = node.name; 67 | } 68 | return resultObject; 69 | } 70 | 71 | // src/plugin/creators/shapeCreators.ts 72 | function createRectangleFromData(data) { 73 | const rect = figma.createRectangle(); 74 | rect.resize(data.width || 100, data.height || 100); 75 | if (data.fills) { 76 | rect.fills = data.fills; 77 | } else if (data.fill) { 78 | if (typeof data.fill === "string") { 79 | rect.fills = [createSolidPaint(data.fill)]; 80 | } else { 81 | rect.fills = [data.fill]; 82 | } 83 | } 84 | if (data.strokes) 85 | rect.strokes = data.strokes; 86 | if (data.strokeWeight !== undefined) 87 | rect.strokeWeight = data.strokeWeight; 88 | if (data.strokeAlign) 89 | rect.strokeAlign = data.strokeAlign; 90 | if (data.strokeCap) 91 | rect.strokeCap = data.strokeCap; 92 | if (data.strokeJoin) 93 | rect.strokeJoin = data.strokeJoin; 94 | if (data.dashPattern) 95 | rect.dashPattern = data.dashPattern; 96 | if (data.cornerRadius !== undefined) 97 | rect.cornerRadius = data.cornerRadius; 98 | if (data.topLeftRadius !== undefined) 99 | rect.topLeftRadius = data.topLeftRadius; 100 | if (data.topRightRadius !== undefined) 101 | rect.topRightRadius = data.topRightRadius; 102 | if (data.bottomLeftRadius !== undefined) 103 | rect.bottomLeftRadius = data.bottomLeftRadius; 104 | if (data.bottomRightRadius !== undefined) 105 | rect.bottomRightRadius = data.bottomRightRadius; 106 | return rect; 107 | } 108 | function createEllipseFromData(data) { 109 | const ellipse = figma.createEllipse(); 110 | ellipse.resize(data.width || 100, data.height || 100); 111 | if (data.fills) { 112 | ellipse.fills = data.fills; 113 | } else if (data.fill) { 114 | if (typeof data.fill === "string") { 115 | ellipse.fills = [createSolidPaint(data.fill)]; 116 | } else { 117 | ellipse.fills = [data.fill]; 118 | } 119 | } 120 | if (data.arcData) { 121 | ellipse.arcData = { 122 | startingAngle: data.arcData.startingAngle !== undefined ? data.arcData.startingAngle : 0, 123 | endingAngle: data.arcData.endingAngle !== undefined ? data.arcData.endingAngle : 360, 124 | innerRadius: data.arcData.innerRadius !== undefined ? data.arcData.innerRadius : 0 125 | }; 126 | } 127 | if (data.strokes) 128 | ellipse.strokes = data.strokes; 129 | if (data.strokeWeight !== undefined) 130 | ellipse.strokeWeight = data.strokeWeight; 131 | if (data.strokeAlign) 132 | ellipse.strokeAlign = data.strokeAlign; 133 | if (data.strokeCap) 134 | ellipse.strokeCap = data.strokeCap; 135 | if (data.strokeJoin) 136 | ellipse.strokeJoin = data.strokeJoin; 137 | if (data.dashPattern) 138 | ellipse.dashPattern = data.dashPattern; 139 | return ellipse; 140 | } 141 | function createPolygonFromData(data) { 142 | const polygon = figma.createPolygon(); 143 | polygon.resize(data.width || 100, data.height || 100); 144 | if (data.pointCount) 145 | polygon.pointCount = data.pointCount; 146 | if (data.fills) { 147 | polygon.fills = data.fills; 148 | } else if (data.fill) { 149 | if (typeof data.fill === "string") { 150 | polygon.fills = [createSolidPaint(data.fill)]; 151 | } else { 152 | polygon.fills = [data.fill]; 153 | } 154 | } else if (data.color) { 155 | polygon.fills = [createSolidPaint(data.color)]; 156 | } 157 | if (data.strokes) 158 | polygon.strokes = data.strokes; 159 | if (data.strokeWeight !== undefined) 160 | polygon.strokeWeight = data.strokeWeight; 161 | if (data.strokeAlign) 162 | polygon.strokeAlign = data.strokeAlign; 163 | if (data.strokeCap) 164 | polygon.strokeCap = data.strokeCap; 165 | if (data.strokeJoin) 166 | polygon.strokeJoin = data.strokeJoin; 167 | if (data.dashPattern) 168 | polygon.dashPattern = data.dashPattern; 169 | return polygon; 170 | } 171 | function createStarFromData(data) { 172 | const star = figma.createStar(); 173 | star.resize(data.width || 100, data.height || 100); 174 | if (data.pointCount) 175 | star.pointCount = data.pointCount; 176 | if (data.innerRadius) 177 | star.innerRadius = data.innerRadius; 178 | if (data.fills) { 179 | star.fills = data.fills; 180 | } else if (data.fill) { 181 | if (typeof data.fill === "string") { 182 | star.fills = [createSolidPaint(data.fill)]; 183 | } else { 184 | star.fills = [data.fill]; 185 | } 186 | } 187 | return star; 188 | } 189 | function createLineFromData(data) { 190 | const line = figma.createLine(); 191 | line.resize(data.width || 100, 0); 192 | if (data.rotation !== undefined) 193 | line.rotation = data.rotation; 194 | if (data.strokeWeight) 195 | line.strokeWeight = data.strokeWeight; 196 | if (data.strokeAlign) 197 | line.strokeAlign = data.strokeAlign; 198 | if (data.strokeCap) 199 | line.strokeCap = data.strokeCap; 200 | if (data.strokeJoin) 201 | line.strokeJoin = data.strokeJoin; 202 | if (data.dashPattern) 203 | line.dashPattern = data.dashPattern; 204 | if (data.strokes) { 205 | line.strokes = data.strokes; 206 | } else if (data.stroke) { 207 | if (typeof data.stroke === "string") { 208 | line.strokes = [createSolidPaint(data.stroke)]; 209 | } else { 210 | line.strokes = [data.stroke]; 211 | } 212 | } else if (data.color) { 213 | line.strokes = [createSolidPaint(data.color)]; 214 | } 215 | return line; 216 | } 217 | function createVectorFromData(data) { 218 | const vector = figma.createVector(); 219 | try { 220 | vector.resize(data.width || 100, data.height || 100); 221 | if (data.vectorNetwork) { 222 | vector.vectorNetwork = data.vectorNetwork; 223 | } 224 | if (data.vectorPaths) { 225 | vector.vectorPaths = data.vectorPaths; 226 | } 227 | if (data.handleMirroring) { 228 | vector.handleMirroring = data.handleMirroring; 229 | } 230 | if (data.fills) { 231 | vector.fills = data.fills; 232 | } else if (data.fill) { 233 | if (typeof data.fill === "string") { 234 | vector.fills = [createSolidPaint(data.fill)]; 235 | } else { 236 | vector.fills = [data.fill]; 237 | } 238 | } else if (data.color) { 239 | vector.fills = [createSolidPaint(data.color)]; 240 | } 241 | if (data.strokes) 242 | vector.strokes = data.strokes; 243 | if (data.strokeWeight !== undefined) 244 | vector.strokeWeight = data.strokeWeight; 245 | if (data.strokeAlign) 246 | vector.strokeAlign = data.strokeAlign; 247 | if (data.strokeCap) 248 | vector.strokeCap = data.strokeCap; 249 | if (data.strokeJoin) 250 | vector.strokeJoin = data.strokeJoin; 251 | if (data.dashPattern) 252 | vector.dashPattern = data.dashPattern; 253 | if (data.strokeMiterLimit) 254 | vector.strokeMiterLimit = data.strokeMiterLimit; 255 | if (data.cornerRadius !== undefined) 256 | vector.cornerRadius = data.cornerRadius; 257 | if (data.cornerSmoothing !== undefined) 258 | vector.cornerSmoothing = data.cornerSmoothing; 259 | if (data.opacity !== undefined) 260 | vector.opacity = data.opacity; 261 | if (data.blendMode) 262 | vector.blendMode = data.blendMode; 263 | if (data.isMask !== undefined) 264 | vector.isMask = data.isMask; 265 | if (data.effects) 266 | vector.effects = data.effects; 267 | if (data.constraints) 268 | vector.constraints = data.constraints; 269 | if (data.layoutAlign) 270 | vector.layoutAlign = data.layoutAlign; 271 | if (data.layoutGrow !== undefined) 272 | vector.layoutGrow = data.layoutGrow; 273 | if (data.layoutPositioning) 274 | vector.layoutPositioning = data.layoutPositioning; 275 | if (data.rotation !== undefined) 276 | vector.rotation = data.rotation; 277 | if (data.layoutSizingHorizontal) 278 | vector.layoutSizingHorizontal = data.layoutSizingHorizontal; 279 | if (data.layoutSizingVertical) 280 | vector.layoutSizingVertical = data.layoutSizingVertical; 281 | console.log("Vector created successfully:", vector); 282 | } catch (error) { 283 | console.error("Error creating vector:", error); 284 | } 285 | return vector; 286 | } 287 | 288 | // src/plugin/creators/containerCreators.ts 289 | function createFrameFromData(data) { 290 | const frame = figma.createFrame(); 291 | frame.resize(data.width || 100, data.height || 100); 292 | if (data.fills) { 293 | frame.fills = data.fills; 294 | } else if (data.fill) { 295 | if (typeof data.fill === "string") { 296 | frame.fills = [createSolidPaint(data.fill)]; 297 | } else { 298 | frame.fills = [data.fill]; 299 | } 300 | } 301 | if (data.layoutMode) 302 | frame.layoutMode = data.layoutMode; 303 | if (data.primaryAxisSizingMode) 304 | frame.primaryAxisSizingMode = data.primaryAxisSizingMode; 305 | if (data.counterAxisSizingMode) 306 | frame.counterAxisSizingMode = data.counterAxisSizingMode; 307 | if (data.primaryAxisAlignItems) 308 | frame.primaryAxisAlignItems = data.primaryAxisAlignItems; 309 | if (data.counterAxisAlignItems) 310 | frame.counterAxisAlignItems = data.counterAxisAlignItems; 311 | if (data.paddingLeft !== undefined) 312 | frame.paddingLeft = data.paddingLeft; 313 | if (data.paddingRight !== undefined) 314 | frame.paddingRight = data.paddingRight; 315 | if (data.paddingTop !== undefined) 316 | frame.paddingTop = data.paddingTop; 317 | if (data.paddingBottom !== undefined) 318 | frame.paddingBottom = data.paddingBottom; 319 | if (data.itemSpacing !== undefined) 320 | frame.itemSpacing = data.itemSpacing; 321 | if (data.cornerRadius !== undefined) 322 | frame.cornerRadius = data.cornerRadius; 323 | if (data.topLeftRadius !== undefined) 324 | frame.topLeftRadius = data.topLeftRadius; 325 | if (data.topRightRadius !== undefined) 326 | frame.topRightRadius = data.topRightRadius; 327 | if (data.bottomLeftRadius !== undefined) 328 | frame.bottomLeftRadius = data.bottomLeftRadius; 329 | if (data.bottomRightRadius !== undefined) 330 | frame.bottomRightRadius = data.bottomRightRadius; 331 | return frame; 332 | } 333 | function createComponentFromData(data) { 334 | const component = figma.createComponent(); 335 | component.resize(data.width || 100, data.height || 100); 336 | if (data.fills) { 337 | component.fills = data.fills; 338 | } else if (data.fill) { 339 | if (typeof data.fill === "string") { 340 | component.fills = [createSolidPaint(data.fill)]; 341 | } else { 342 | component.fills = [data.fill]; 343 | } 344 | } 345 | if (data.layoutMode) 346 | component.layoutMode = data.layoutMode; 347 | if (data.primaryAxisSizingMode) 348 | component.primaryAxisSizingMode = data.primaryAxisSizingMode; 349 | if (data.counterAxisSizingMode) 350 | component.counterAxisSizingMode = data.counterAxisSizingMode; 351 | if (data.primaryAxisAlignItems) 352 | component.primaryAxisAlignItems = data.primaryAxisAlignItems; 353 | if (data.counterAxisAlignItems) 354 | component.counterAxisAlignItems = data.counterAxisAlignItems; 355 | if (data.paddingLeft !== undefined) 356 | component.paddingLeft = data.paddingLeft; 357 | if (data.paddingRight !== undefined) 358 | component.paddingRight = data.paddingRight; 359 | if (data.paddingTop !== undefined) 360 | component.paddingTop = data.paddingTop; 361 | if (data.paddingBottom !== undefined) 362 | component.paddingBottom = data.paddingBottom; 363 | if (data.itemSpacing !== undefined) 364 | component.itemSpacing = data.itemSpacing; 365 | if (data.description) 366 | component.description = data.description; 367 | return component; 368 | } 369 | function createGroupFromData(data, children) { 370 | const group = figma.group(children, figma.currentPage); 371 | applyCommonProperties(group, data); 372 | return group; 373 | } 374 | function createInstanceFromData(data) { 375 | if (!data.componentId) { 376 | console.error("Cannot create instance: componentId is required"); 377 | return null; 378 | } 379 | const component = figma.getNodeById(data.componentId); 380 | if (!component || component.type !== "COMPONENT") { 381 | console.error(`Cannot create instance: component with id ${data.componentId} not found`); 382 | return null; 383 | } 384 | const instance = component.createInstance(); 385 | applyCommonProperties(instance, data); 386 | if (data.componentProperties) { 387 | for (const [key, value] of Object.entries(data.componentProperties)) { 388 | if (key in instance.componentProperties) { 389 | const prop = instance.componentProperties[key]; 390 | if (prop.type === "BOOLEAN") { 391 | instance.setProperties({ [key]: !!value }); 392 | } else if (prop.type === "TEXT") { 393 | instance.setProperties({ [key]: String(value) }); 394 | } else if (prop.type === "INSTANCE_SWAP") { 395 | instance.setProperties({ [key]: String(value) }); 396 | } else if (prop.type === "VARIANT") { 397 | instance.setProperties({ [key]: String(value) }); 398 | } 399 | } 400 | } 401 | } 402 | return instance; 403 | } 404 | function createSectionFromData(data) { 405 | const section = figma.createSection(); 406 | if (data.name) 407 | section.name = data.name; 408 | if (data.sectionContentsHidden !== undefined) 409 | section.sectionContentsHidden = data.sectionContentsHidden; 410 | if (data.x !== undefined) 411 | section.x = data.x; 412 | if (data.y !== undefined) 413 | section.y = data.y; 414 | return section; 415 | } 416 | 417 | // src/plugin/creators/textCreator.ts 418 | async function createTextFromData(data) { 419 | const text = figma.createText(); 420 | const fontFamily = data.fontFamily || data.fontName.family || "Inter"; 421 | const fontStyle = data.fontStyle || data.fontName.style || "Regular"; 422 | try { 423 | await figma.loadFontAsync({ family: fontFamily, style: fontStyle }); 424 | } catch (error) { 425 | console.warn(`Failed to load font ${fontFamily} ${fontStyle}. Falling back to Inter Regular.`); 426 | await figma.loadFontAsync({ family: "Inter", style: "Regular" }); 427 | } 428 | text.characters = data.text || data.characters || "Text"; 429 | if (data.x !== undefined) 430 | text.x = data.x; 431 | if (data.y !== undefined) 432 | text.y = data.y; 433 | if (data.fontSize) 434 | text.fontSize = data.fontSize; 435 | if (data.width) 436 | text.resize(data.width, text.height); 437 | if (data.fontName) 438 | text.fontName = data.fontName; 439 | if (data.textAlignHorizontal) 440 | text.textAlignHorizontal = data.textAlignHorizontal; 441 | if (data.textAlignVertical) 442 | text.textAlignVertical = data.textAlignVertical; 443 | if (data.textAutoResize) 444 | text.textAutoResize = data.textAutoResize; 445 | if (data.textTruncation) 446 | text.textTruncation = data.textTruncation; 447 | if (data.maxLines !== undefined) 448 | text.maxLines = data.maxLines; 449 | if (data.paragraphIndent) 450 | text.paragraphIndent = data.paragraphIndent; 451 | if (data.paragraphSpacing) 452 | text.paragraphSpacing = data.paragraphSpacing; 453 | if (data.listSpacing) 454 | text.listSpacing = data.listSpacing; 455 | if (data.hangingPunctuation !== undefined) 456 | text.hangingPunctuation = data.hangingPunctuation; 457 | if (data.hangingList !== undefined) 458 | text.hangingList = data.hangingList; 459 | if (data.autoRename !== undefined) 460 | text.autoRename = data.autoRename; 461 | if (data.letterSpacing) 462 | text.letterSpacing = data.letterSpacing; 463 | if (data.lineHeight) 464 | text.lineHeight = data.lineHeight; 465 | if (data.leadingTrim) 466 | text.leadingTrim = data.leadingTrim; 467 | if (data.textCase) 468 | text.textCase = data.textCase; 469 | if (data.textDecoration) 470 | text.textDecoration = data.textDecoration; 471 | if (data.textStyleId) 472 | text.textStyleId = data.textStyleId; 473 | if (data.textDecorationStyle) 474 | text.textDecorationStyle = data.textDecorationStyle; 475 | if (data.textDecorationOffset) 476 | text.textDecorationOffset = data.textDecorationOffset; 477 | if (data.textDecorationThickness) 478 | text.textDecorationThickness = data.textDecorationThickness; 479 | if (data.textDecorationColor) 480 | text.textDecorationColor = data.textDecorationColor; 481 | if (data.textDecorationSkipInk !== undefined) 482 | text.textDecorationSkipInk = data.textDecorationSkipInk; 483 | if (data.fills) { 484 | text.fills = data.fills; 485 | } else if (data.fill) { 486 | if (typeof data.fill === "string") { 487 | text.fills = [createSolidPaint(data.fill)]; 488 | } else { 489 | text.fills = [data.fill]; 490 | } 491 | } 492 | if (data.hyperlink) { 493 | text.hyperlink = data.hyperlink; 494 | } 495 | if (data.layoutAlign) 496 | text.layoutAlign = data.layoutAlign; 497 | if (data.layoutGrow !== undefined) 498 | text.layoutGrow = data.layoutGrow; 499 | if (data.layoutSizingHorizontal) 500 | text.layoutSizingHorizontal = data.layoutSizingHorizontal; 501 | if (data.layoutSizingVertical) 502 | text.layoutSizingVertical = data.layoutSizingVertical; 503 | if (data.rangeStyles && Array.isArray(data.rangeStyles)) { 504 | applyTextRangeStyles(text, data.rangeStyles); 505 | } 506 | if (data.name) 507 | text.name = data.name; 508 | if (data.visible !== undefined) 509 | text.visible = data.visible; 510 | if (data.locked !== undefined) 511 | text.locked = data.locked; 512 | if (data.opacity !== undefined) 513 | text.opacity = data.opacity; 514 | if (data.blendMode) 515 | text.blendMode = data.blendMode; 516 | if (data.effects) 517 | text.effects = data.effects; 518 | if (data.effectStyleId) 519 | text.effectStyleId = data.effectStyleId; 520 | if (data.exportSettings) 521 | text.exportSettings = data.exportSettings; 522 | if (data.constraints) 523 | text.constraints = data.constraints; 524 | return text; 525 | } 526 | function applyTextRangeStyles(textNode, ranges) { 527 | for (const range of ranges) { 528 | for (const [property, value] of Object.entries(range.style)) { 529 | if (property === "fills") { 530 | textNode.setRangeFills(range.start, range.end, value); 531 | } else if (property === "fillStyleId") { 532 | textNode.setRangeFillStyleId(range.start, range.end, value); 533 | } else if (property === "fontName") { 534 | textNode.setRangeFontName(range.start, range.end, value); 535 | } else if (property === "fontSize") { 536 | textNode.setRangeFontSize(range.start, range.end, value); 537 | } else if (property === "textCase") { 538 | textNode.setRangeTextCase(range.start, range.end, value); 539 | } else if (property === "textDecoration") { 540 | textNode.setRangeTextDecoration(range.start, range.end, value); 541 | } else if (property === "textDecorationStyle") { 542 | textNode.setRangeTextDecorationStyle(range.start, range.end, value); 543 | } else if (property === "textDecorationOffset") { 544 | textNode.setRangeTextDecorationOffset(range.start, range.end, value); 545 | } else if (property === "textDecorationThickness") { 546 | textNode.setRangeTextDecorationThickness(range.start, range.end, value); 547 | } else if (property === "textDecorationColor") { 548 | textNode.setRangeTextDecorationColor(range.start, range.end, value); 549 | } else if (property === "textDecorationSkipInk") { 550 | textNode.setRangeTextDecorationSkipInk(range.start, range.end, value); 551 | } else if (property === "letterSpacing") { 552 | textNode.setRangeLetterSpacing(range.start, range.end, value); 553 | } else if (property === "lineHeight") { 554 | textNode.setRangeLineHeight(range.start, range.end, value); 555 | } else if (property === "hyperlink") { 556 | textNode.setRangeHyperlink(range.start, range.end, value); 557 | } else if (property === "textStyleId") { 558 | textNode.setRangeTextStyleId(range.start, range.end, value); 559 | } else if (property === "indentation") { 560 | textNode.setRangeIndentation(range.start, range.end, value); 561 | } else if (property === "paragraphIndent") { 562 | textNode.setRangeParagraphIndent(range.start, range.end, value); 563 | } else if (property === "paragraphSpacing") { 564 | textNode.setRangeParagraphSpacing(range.start, range.end, value); 565 | } else if (property === "listOptions") { 566 | textNode.setRangeListOptions(range.start, range.end, value); 567 | } else if (property === "listSpacing") { 568 | textNode.setRangeListSpacing(range.start, range.end, value); 569 | } 570 | } 571 | } 572 | } 573 | 574 | // src/plugin/creators/specialCreators.ts 575 | function createBooleanOperationFromData(data) { 576 | if (!data.children || !Array.isArray(data.children) || data.children.length < 2) { 577 | console.error("Boolean operation requires at least 2 child nodes"); 578 | return null; 579 | } 580 | let childNodes = []; 581 | try { 582 | for (const childData of data.children) { 583 | const node = figma.createRectangle(); 584 | childNodes.push(node); 585 | } 586 | const booleanOperation = figma.createBooleanOperation(); 587 | if (data.booleanOperation) { 588 | booleanOperation.booleanOperation = data.booleanOperation; 589 | } 590 | applyCommonProperties(booleanOperation, data); 591 | return booleanOperation; 592 | } catch (error) { 593 | console.error("Failed to create boolean operation:", error); 594 | childNodes.forEach((node) => node.remove()); 595 | return null; 596 | } 597 | } 598 | function createConnectorFromData(data) { 599 | const connector = figma.createConnector(); 600 | if (data.connectorStart) 601 | connector.connectorStart = data.connectorStart; 602 | if (data.connectorEnd) 603 | connector.connectorEnd = data.connectorEnd; 604 | if (data.connectorStartStrokeCap) 605 | connector.connectorStartStrokeCap = data.connectorStartStrokeCap; 606 | if (data.connectorEndStrokeCap) 607 | connector.connectorEndStrokeCap = data.connectorEndStrokeCap; 608 | if (data.connectorLineType) 609 | connector.connectorLineType = data.connectorLineType; 610 | if (data.strokes) 611 | connector.strokes = data.strokes; 612 | if (data.strokeWeight) 613 | connector.strokeWeight = data.strokeWeight; 614 | applyCommonProperties(connector, data); 615 | return connector; 616 | } 617 | function createShapeWithTextFromData(data) { 618 | if (!("createShapeWithText" in figma)) { 619 | console.error("ShapeWithText creation is not supported in this Figma version"); 620 | return null; 621 | } 622 | try { 623 | const shapeWithText = figma.createShapeWithText(); 624 | if (data.shapeType) 625 | shapeWithText.shapeType = data.shapeType; 626 | if (data.text || data.characters) { 627 | shapeWithText.text.characters = data.text || data.characters; 628 | } 629 | try { 630 | if (data.fontSize) 631 | shapeWithText.text.fontSize = data.fontSize; 632 | if (data.fontName) 633 | shapeWithText.text.fontName = data.fontName; 634 | if (data.textAlignHorizontal && "textAlignHorizontal" in shapeWithText.text) { 635 | shapeWithText.text.textAlignHorizontal = data.textAlignHorizontal; 636 | } 637 | if (data.textAlignVertical && "textAlignVertical" in shapeWithText.text) { 638 | shapeWithText.text.textAlignVertical = data.textAlignVertical; 639 | } 640 | } catch (e) { 641 | console.warn("Some text properties could not be set on ShapeWithText:", e); 642 | } 643 | if (data.fills) 644 | shapeWithText.fills = data.fills; 645 | if (data.strokes) 646 | shapeWithText.strokes = data.strokes; 647 | applyCommonProperties(shapeWithText, data); 648 | return shapeWithText; 649 | } catch (error) { 650 | console.error("Failed to create shape with text:", error); 651 | return null; 652 | } 653 | } 654 | function createCodeBlockFromData(data) { 655 | const codeBlock = figma.createCodeBlock(); 656 | if (data.code) 657 | codeBlock.code = data.code; 658 | if (data.codeLanguage) 659 | codeBlock.codeLanguage = data.codeLanguage; 660 | applyCommonProperties(codeBlock, data); 661 | return codeBlock; 662 | } 663 | function createTableFromData(data) { 664 | const table = figma.createTable(data.numRows || 2, data.numColumns || 2); 665 | if (data.fills && "fills" in table) { 666 | table.fills = data.fills; 667 | } 668 | if (data.cells && Array.isArray(data.cells)) { 669 | for (const cellData of data.cells) { 670 | if (cellData.rowIndex !== undefined && cellData.columnIndex !== undefined) { 671 | try { 672 | let cell; 673 | if ("cellAt" in table) { 674 | cell = table.cellAt(cellData.rowIndex, cellData.columnIndex); 675 | } else if ("getCellAt" in table) { 676 | cell = table.getCellAt(cellData.rowIndex, cellData.columnIndex); 677 | } 678 | if (cell) { 679 | if (cellData.text && cell.text) 680 | cell.text.characters = cellData.text; 681 | if (cellData.fills && "fills" in cell) 682 | cell.fills = cellData.fills; 683 | if (cellData.rowSpan && "rowSpan" in cell) 684 | cell.rowSpan = cellData.rowSpan; 685 | if (cellData.columnSpan && "columnSpan" in cell) 686 | cell.columnSpan = cellData.columnSpan; 687 | } 688 | } catch (e) { 689 | console.warn(`Could not set properties for cell at ${cellData.rowIndex}, ${cellData.columnIndex}:`, e); 690 | } 691 | } 692 | } 693 | } 694 | applyCommonProperties(table, data); 695 | return table; 696 | } 697 | function createWidgetFromData(data) { 698 | if (!("createWidget" in figma)) { 699 | console.error("Widget creation is not supported in this Figma version"); 700 | return null; 701 | } 702 | if (!data.widgetId) { 703 | console.error("Widget creation requires a widgetId"); 704 | return null; 705 | } 706 | try { 707 | const widget = figma.createWidget(data.widgetId); 708 | if (data.widgetData) 709 | widget.widgetData = JSON.stringify(data.widgetData); 710 | if (data.width && data.height && "resize" in widget) 711 | widget.resize(data.width, data.height); 712 | applyCommonProperties(widget, data); 713 | return widget; 714 | } catch (error) { 715 | console.error("Failed to create widget:", error); 716 | return null; 717 | } 718 | } 719 | function createMediaFromData(data) { 720 | if (!("createMedia" in figma)) { 721 | console.error("Media creation is not supported in this Figma version"); 722 | return null; 723 | } 724 | if (!data.hash) { 725 | console.error("Media creation requires a valid media hash"); 726 | return null; 727 | } 728 | try { 729 | const media = figma.createMedia(data.hash); 730 | applyCommonProperties(media, data); 731 | return media; 732 | } catch (error) { 733 | console.error("Failed to create media:", error); 734 | return null; 735 | } 736 | } 737 | 738 | // src/plugin/creators/imageCreators.ts 739 | function createImageFromData(data) { 740 | try { 741 | if (!data.hash) { 742 | console.error("Image creation requires an image hash"); 743 | return null; 744 | } 745 | const image = figma.createImage(data.hash); 746 | const rect = figma.createRectangle(); 747 | if (data.width && data.height) { 748 | rect.resize(data.width, data.height); 749 | } 750 | rect.fills = [{ 751 | type: "IMAGE", 752 | scaleMode: data.scaleMode || "FILL", 753 | imageHash: image.hash 754 | }]; 755 | applyCommonProperties(rect, data); 756 | return rect; 757 | } catch (error) { 758 | console.error("Failed to create image:", error); 759 | return null; 760 | } 761 | } 762 | async function createImageFromBytesAsync(data) { 763 | try { 764 | if (!data.bytes && !data.file) { 765 | console.error("Image creation requires image bytes or file"); 766 | return null; 767 | } 768 | let image; 769 | if (data.bytes) { 770 | image = await figma.createImageAsync(data.bytes); 771 | } else if (data.file) { 772 | image = await figma.createImageAsync(data.file); 773 | } else { 774 | return null; 775 | } 776 | const rect = figma.createRectangle(); 777 | if (data.width && data.height) { 778 | rect.resize(data.width, data.height); 779 | } 780 | rect.fills = [{ 781 | type: "IMAGE", 782 | scaleMode: data.scaleMode || "FILL", 783 | imageHash: image.hash 784 | }]; 785 | applyCommonProperties(rect, data); 786 | return rect; 787 | } catch (error) { 788 | console.error("Failed to create image asynchronously:", error); 789 | return null; 790 | } 791 | } 792 | function createGifFromData(data) { 793 | console.error("createGif API is not directly available or implemented"); 794 | return null; 795 | } 796 | async function createVideoFromDataAsync(data) { 797 | if (!("createVideoAsync" in figma)) { 798 | console.error("Video creation is not supported in this Figma version"); 799 | return null; 800 | } 801 | try { 802 | if (!data.bytes) { 803 | console.error("Video creation requires video bytes"); 804 | return null; 805 | } 806 | const video = await figma.createVideoAsync(data.bytes); 807 | applyCommonProperties(video, data); 808 | return video; 809 | } catch (error) { 810 | console.error("Failed to create video:", error); 811 | return null; 812 | } 813 | } 814 | async function createLinkPreviewFromDataAsync(data) { 815 | if (!("createLinkPreviewAsync" in figma)) { 816 | console.error("Link preview creation is not supported in this Figma version"); 817 | return null; 818 | } 819 | try { 820 | if (!data.url) { 821 | console.error("Link preview creation requires a URL"); 822 | return null; 823 | } 824 | const linkPreview = await figma.createLinkPreviewAsync(data.url); 825 | applyCommonProperties(linkPreview, data); 826 | return linkPreview; 827 | } catch (error) { 828 | console.error("Failed to create link preview:", error); 829 | return null; 830 | } 831 | } 832 | 833 | // src/plugin/creators/sliceCreators.ts 834 | function createSliceFromData(data) { 835 | const slice = figma.createSlice(); 836 | if (data.width && data.height) { 837 | slice.resize(data.width, data.height); 838 | } 839 | if (data.x !== undefined) 840 | slice.x = data.x; 841 | if (data.y !== undefined) 842 | slice.y = data.y; 843 | if (data.exportSettings && Array.isArray(data.exportSettings)) { 844 | slice.exportSettings = data.exportSettings; 845 | } 846 | if (data.name) 847 | slice.name = data.name; 848 | if (data.visible !== undefined) 849 | slice.visible = data.visible; 850 | return slice; 851 | } 852 | function createPageFromData(data) { 853 | const page = figma.createPage(); 854 | if (data.name) 855 | page.name = data.name; 856 | if (data.backgrounds) 857 | page.backgrounds = data.backgrounds; 858 | return page; 859 | } 860 | function createPageDividerFromData(data) { 861 | if (!("createPageDivider" in figma)) { 862 | console.error("createPageDivider is not supported in this Figma version"); 863 | return null; 864 | } 865 | try { 866 | const pageDivider = figma.createPageDivider(); 867 | if (data.name) 868 | pageDivider.name = data.name; 869 | return pageDivider; 870 | } catch (error) { 871 | console.error("Failed to create page divider:", error); 872 | return null; 873 | } 874 | } 875 | function createSlideFromData(data) { 876 | if (!("createSlide" in figma)) { 877 | console.error("createSlide is not supported in this Figma version"); 878 | return null; 879 | } 880 | try { 881 | const slide = figma.createSlide(); 882 | if (data.name) 883 | slide.name = data.name; 884 | applyCommonProperties(slide, data); 885 | return slide; 886 | } catch (error) { 887 | console.error("Failed to create slide:", error); 888 | return null; 889 | } 890 | } 891 | function createSlideRowFromData(data) { 892 | if (!("createSlideRow" in figma)) { 893 | console.error("createSlideRow is not supported in this Figma version"); 894 | return null; 895 | } 896 | try { 897 | const slideRow = figma.createSlideRow(); 898 | if (data.name) 899 | slideRow.name = data.name; 900 | applyCommonProperties(slideRow, data); 901 | return slideRow; 902 | } catch (error) { 903 | console.error("Failed to create slide row:", error); 904 | return null; 905 | } 906 | } 907 | 908 | // src/plugin/creators/componentCreators.ts 909 | function createComponentFromNodeData(data) { 910 | if (!data.sourceNode) { 911 | console.error("createComponentFromNode requires a sourceNode"); 912 | return null; 913 | } 914 | try { 915 | let sourceNode; 916 | if (typeof data.sourceNode === "string") { 917 | sourceNode = figma.getNodeById(data.sourceNode); 918 | if (!sourceNode || !("type" in sourceNode)) { 919 | console.error(`Node with ID ${data.sourceNode} not found or is not a valid node`); 920 | return null; 921 | } 922 | } else { 923 | sourceNode = data.sourceNode; 924 | } 925 | const component = figma.createComponentFromNode(sourceNode); 926 | if (data.description) 927 | component.description = data.description; 928 | applyCommonProperties(component, data); 929 | return component; 930 | } catch (error) { 931 | console.error("Failed to create component from node:", error); 932 | return null; 933 | } 934 | } 935 | function createComponentSetFromData(data) { 936 | try { 937 | if (!data.components || !Array.isArray(data.components) || data.components.length === 0) { 938 | console.error("Component set creation requires component nodes"); 939 | return null; 940 | } 941 | const componentNodes = []; 942 | for (const component of data.components) { 943 | let node; 944 | if (typeof component === "string") { 945 | node = figma.getNodeById(component); 946 | } else { 947 | node = component; 948 | } 949 | if (node && node.type === "COMPONENT") { 950 | componentNodes.push(node); 951 | } 952 | } 953 | if (componentNodes.length === 0) { 954 | console.error("No valid component nodes provided"); 955 | return null; 956 | } 957 | const componentSet = figma.combineAsVariants(componentNodes, figma.currentPage); 958 | if (data.name) 959 | componentSet.name = data.name; 960 | applyCommonProperties(componentSet, data); 961 | return componentSet; 962 | } catch (error) { 963 | console.error("Failed to create component set:", error); 964 | return null; 965 | } 966 | } 967 | 968 | // src/plugin/creators/elementCreator.ts 969 | async function createElementFromData(data) { 970 | if (!data || !data.type) { 971 | console.error("Invalid element data: missing type"); 972 | return null; 973 | } 974 | let element = null; 975 | try { 976 | switch (data.type.toLowerCase()) { 977 | case "rectangle": 978 | element = createRectangleFromData(data); 979 | break; 980 | case "ellipse": 981 | case "circle": 982 | element = createEllipseFromData(data); 983 | break; 984 | case "polygon": 985 | element = createPolygonFromData(data); 986 | break; 987 | case "star": 988 | element = createStarFromData(data); 989 | break; 990 | case "line": 991 | element = createLineFromData(data); 992 | break; 993 | case "vector": 994 | element = createVectorFromData(data); 995 | break; 996 | case "frame": 997 | element = createFrameFromData(data); 998 | break; 999 | case "component": 1000 | element = createComponentFromData(data); 1001 | break; 1002 | case "componentfromnode": 1003 | element = createComponentFromNodeData(data); 1004 | break; 1005 | case "componentset": 1006 | element = createComponentSetFromData(data); 1007 | break; 1008 | case "instance": 1009 | element = createInstanceFromData(data); 1010 | break; 1011 | case "section": 1012 | element = createSectionFromData(data); 1013 | break; 1014 | case "text": 1015 | element = await createTextFromData(data); 1016 | break; 1017 | case "boolean": 1018 | case "booleanoperation": 1019 | element = createBooleanOperationFromData(data); 1020 | break; 1021 | case "connector": 1022 | element = createConnectorFromData(data); 1023 | break; 1024 | case "shapewithtext": 1025 | element = createShapeWithTextFromData(data); 1026 | break; 1027 | case "codeblock": 1028 | element = createCodeBlockFromData(data); 1029 | break; 1030 | case "table": 1031 | element = createTableFromData(data); 1032 | break; 1033 | case "widget": 1034 | element = createWidgetFromData(data); 1035 | break; 1036 | case "media": 1037 | element = createMediaFromData(data); 1038 | break; 1039 | case "image": 1040 | if (data.bytes || data.file) { 1041 | element = await createImageFromBytesAsync(data); 1042 | } else { 1043 | element = createImageFromData(data); 1044 | } 1045 | break; 1046 | case "gif": 1047 | element = createGifFromData(data); 1048 | break; 1049 | case "video": 1050 | element = await createVideoFromDataAsync(data); 1051 | break; 1052 | case "linkpreview": 1053 | element = await createLinkPreviewFromDataAsync(data); 1054 | break; 1055 | case "slice": 1056 | element = createSliceFromData(data); 1057 | break; 1058 | case "page": 1059 | const page = createPageFromData(data); 1060 | console.log(`Created page: ${page.name}`); 1061 | return null; 1062 | case "pagedivider": 1063 | element = createPageDividerFromData(data); 1064 | break; 1065 | case "slide": 1066 | element = createSlideFromData(data); 1067 | break; 1068 | case "sliderow": 1069 | element = createSlideRowFromData(data); 1070 | break; 1071 | case "group": 1072 | if (!data.children || !Array.isArray(data.children) || data.children.length < 1) { 1073 | console.error("Cannot create group: children array is required"); 1074 | return null; 1075 | } 1076 | const childNodes = []; 1077 | for (const childData of data.children) { 1078 | const child = await createElementFromData(childData); 1079 | if (child) 1080 | childNodes.push(child); 1081 | } 1082 | if (childNodes.length > 0) { 1083 | element = createGroupFromData(data, childNodes); 1084 | } else { 1085 | console.error("Cannot create group: no valid children were created"); 1086 | return null; 1087 | } 1088 | break; 1089 | default: 1090 | console.error(`Unsupported element type: ${data.type}`); 1091 | return null; 1092 | } 1093 | if (element) { 1094 | applyCommonProperties(element, data); 1095 | if (data.select !== false) { 1096 | selectAndFocusNodes(element); 1097 | } 1098 | } 1099 | return element; 1100 | } catch (error) { 1101 | console.error(`Error creating element: ${error instanceof Error ? error.message : "Unknown error"}`); 1102 | return null; 1103 | } 1104 | } 1105 | async function createElementsFromDataArray(dataArray) { 1106 | const createdNodes = []; 1107 | for (const data of dataArray) { 1108 | const node = await createElementFromData(data); 1109 | if (node) 1110 | createdNodes.push(node); 1111 | } 1112 | if (createdNodes.length > 0) { 1113 | selectAndFocusNodes(createdNodes); 1114 | } 1115 | return createdNodes; 1116 | } 1117 | 1118 | // src/plugin/code.ts 1119 | figma.showUI(__html__, { width: 320, height: 500 }); 1120 | console.log("Figma MCP Plugin loaded"); 1121 | var elementCreators = { 1122 | "create-rectangle": createRectangleFromData, 1123 | "create-circle": createEllipseFromData, 1124 | "create-ellipse": createEllipseFromData, 1125 | "create-polygon": createPolygonFromData, 1126 | "create-line": createLineFromData, 1127 | "create-text": createTextFromData, 1128 | "create-star": createStarFromData, 1129 | "create-vector": createVectorFromData, 1130 | "create-arc": (params) => { 1131 | const ellipse = createEllipseFromData(params); 1132 | if (params.arcData || params.startAngle !== undefined && params.endAngle !== undefined) { 1133 | ellipse.arcData = { 1134 | startingAngle: params.startAngle || params.arcData.startingAngle || 0, 1135 | endingAngle: params.endAngle || params.arcData.endingAngle || 360, 1136 | innerRadius: params.innerRadius || params.arcData.innerRadius || 0 1137 | }; 1138 | } 1139 | return ellipse; 1140 | } 1141 | }; 1142 | async function createElement(type, params) { 1143 | console.log(`Creating ${type} with params:`, params); 1144 | const creator = elementCreators[type]; 1145 | if (!creator) { 1146 | console.error(`Unknown element type: ${type}`); 1147 | return null; 1148 | } 1149 | try { 1150 | const element = await Promise.resolve(creator(params)); 1151 | if (element && params) { 1152 | if (params.x !== undefined) 1153 | element.x = params.x; 1154 | if (params.y !== undefined) 1155 | element.y = params.y; 1156 | } 1157 | if (element) { 1158 | selectAndFocusNodes(element); 1159 | } 1160 | return element; 1161 | } catch (error) { 1162 | console.error(`Error creating ${type}:`, error); 1163 | return null; 1164 | } 1165 | } 1166 | figma.ui.onmessage = async function(msg) { 1167 | console.log("Received message from UI:", msg); 1168 | if (elementCreators[msg.type]) { 1169 | await createElement(msg.type, msg); 1170 | } else if (msg.type === "create-element") { 1171 | console.log("Creating element with data:", msg.data); 1172 | createElementFromData(msg.data); 1173 | } else if (msg.type === "create-elements") { 1174 | console.log("Creating multiple elements with data:", msg.data); 1175 | createElementsFromDataArray(msg.data); 1176 | } else if (msg.type === "mcp-command") { 1177 | console.log("Received MCP command:", msg.command, "with params:", msg.params); 1178 | handleMcpCommand(msg.command, msg.params); 1179 | } else if (msg.type === "cancel") { 1180 | console.log("Closing plugin"); 1181 | figma.closePlugin(); 1182 | } else { 1183 | console.log("Unknown message type:", msg.type); 1184 | } 1185 | }; 1186 | async function handleMcpCommand(command, params) { 1187 | let result = null; 1188 | try { 1189 | const pluginCommand = command.replace(/_/g, "-"); 1190 | switch (pluginCommand) { 1191 | case "create-rectangle": 1192 | case "create-circle": 1193 | case "create-polygon": 1194 | case "create-line": 1195 | case "create-arc": 1196 | case "create-vector": 1197 | console.log(`MCP command: Creating ${pluginCommand.substring(7)} with params:`, params); 1198 | result = await createElement(pluginCommand, params); 1199 | break; 1200 | case "create-text": 1201 | console.log("MCP command: Creating text with params:", params); 1202 | result = await createElement(pluginCommand, params); 1203 | break; 1204 | case "create-element": 1205 | console.log("MCP command: Creating element with params:", params); 1206 | result = await createElementFromData(params); 1207 | break; 1208 | case "create-elements": 1209 | console.log("MCP command: Creating multiple elements with params:", params); 1210 | result = await createElementsFromDataArray(params); 1211 | break; 1212 | case "get-selection": 1213 | console.log("MCP command: Getting current selection"); 1214 | result = figma.currentPage.selection; 1215 | break; 1216 | case "get-elements": 1217 | console.log("MCP command: Getting elements with params:", params); 1218 | const page = params.page_id ? figma.getNodeById(params.page_id) : figma.currentPage; 1219 | if (!page || page.type !== "PAGE") { 1220 | throw new Error("Invalid page ID or node is not a page"); 1221 | } 1222 | const nodeType = params.type || "ALL"; 1223 | const limit = params.limit || 100; 1224 | const includeHidden = params.include_hidden || false; 1225 | if (nodeType === "ALL") { 1226 | result = includeHidden ? page.children.slice(0, limit) : page.children.filter((node2) => node2.visible).slice(0, limit); 1227 | } else { 1228 | result = page.findAll((node2) => { 1229 | const typeMatch = node2.type === nodeType; 1230 | const visibilityMatch = includeHidden || node2.visible; 1231 | return typeMatch && visibilityMatch; 1232 | }).slice(0, limit); 1233 | } 1234 | break; 1235 | case "get-element": 1236 | console.log("MCP command: Getting element with ID:", params.node_id); 1237 | const node = figma.getNodeById(params.node_id); 1238 | if (!node) { 1239 | throw new Error("Element not found with ID: " + params.node_id); 1240 | } 1241 | if (!["DOCUMENT", "PAGE"].includes(node.type)) { 1242 | if (params.include_children && "children" in node) { 1243 | result = [node, ...node.children || []]; 1244 | } else { 1245 | result = node; 1246 | } 1247 | } else if (node.type === "PAGE") { 1248 | result = node; 1249 | } else { 1250 | throw new Error("Unsupported node type: " + node.type); 1251 | } 1252 | break; 1253 | case "get-pages": 1254 | console.log("MCP command: Getting all pages"); 1255 | result = figma.root.children; 1256 | break; 1257 | case "get-page": 1258 | console.log("MCP command: Getting page with ID:", params.page_id); 1259 | if (!params.page_id) { 1260 | console.log("No page_id provided, using current page"); 1261 | result = figma.currentPage; 1262 | } else { 1263 | const pageNode = figma.getNodeById(params.page_id); 1264 | if (!pageNode || pageNode.type !== "PAGE") 1265 | throw new Error("Invalid page ID or node is not a page"); 1266 | result = pageNode; 1267 | } 1268 | break; 1269 | case "create-page": 1270 | console.log("MCP command: Creating new page with name:", params.name); 1271 | const newPage = figma.createPage(); 1272 | newPage.name = params.name || "New Page"; 1273 | result = newPage; 1274 | break; 1275 | case "switch-page": 1276 | console.log("MCP command: Switching to page with ID:", params.id); 1277 | if (!params.id) 1278 | throw new Error("Page ID is required"); 1279 | const switchPageNode = figma.getNodeById(params.id); 1280 | if (!switchPageNode || switchPageNode.type !== "PAGE") 1281 | throw new Error("Invalid page ID"); 1282 | figma.currentPage = switchPageNode; 1283 | result = switchPageNode; 1284 | break; 1285 | case "modify-rectangle": 1286 | console.log("MCP command: Modifying rectangle with ID:", params.id); 1287 | if (!params.id) 1288 | throw new Error("Rectangle ID is required"); 1289 | const modifyNode = figma.getNodeById(params.id); 1290 | if (!modifyNode || modifyNode.type !== "RECTANGLE") 1291 | throw new Error("Invalid rectangle ID"); 1292 | const rect = modifyNode; 1293 | if (params.x !== undefined) 1294 | rect.x = params.x; 1295 | if (params.y !== undefined) 1296 | rect.y = params.y; 1297 | if (params.width !== undefined && params.height !== undefined) 1298 | rect.resize(params.width, params.height); 1299 | if (params.cornerRadius !== undefined) 1300 | rect.cornerRadius = params.cornerRadius; 1301 | if (params.color) 1302 | rect.fills = [{ type: "SOLID", color: hexToRgb(params.color) }]; 1303 | result = rect; 1304 | break; 1305 | default: 1306 | console.log("Unknown MCP command:", command); 1307 | throw new Error("Unknown command: " + command); 1308 | } 1309 | let resultForBuilder = null; 1310 | if (result === null) { 1311 | resultForBuilder = null; 1312 | } else if (Array.isArray(result)) { 1313 | resultForBuilder = result; 1314 | } else if ("type" in result && result.type === "PAGE") { 1315 | resultForBuilder = result; 1316 | } else { 1317 | resultForBuilder = result; 1318 | } 1319 | const resultObject = buildResultObject(resultForBuilder); 1320 | console.log("Command result:", resultObject); 1321 | figma.ui.postMessage({ 1322 | type: "mcp-response", 1323 | success: true, 1324 | command, 1325 | result: resultObject 1326 | }); 1327 | console.log("Response sent to UI"); 1328 | return resultObject; 1329 | } catch (error) { 1330 | console.error("Error handling MCP command:", error); 1331 | figma.ui.postMessage({ 1332 | type: "mcp-response", 1333 | success: false, 1334 | command, 1335 | error: error instanceof Error ? error.message : "Unknown error" 1336 | }); 1337 | console.log("Error response sent to UI"); 1338 | throw error; 1339 | } 1340 | } 1341 | ```