This is page 1 of 2. Use http://codebase.md/sichang824/mcp-figma?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 -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- ``` dotenv ``` -------------------------------------------------------------------------------- /.mcp.pid: -------------------------------------------------------------------------------- ``` 11949 ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Figma API credentials FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token_here # Server configuration PORT=3001 NODE_ENV=development ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Caches .cache # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store .env.test # Environment files .env.* !.env.example # Dependencies node_modules/ .npm npm-debug.log* yarn-debug.log* yarn-error.log* # Build output dist/ build/ # OS specific files .DS_Store Thumbs.db # IDE specific files .idea/ .vscode/ *.sublime-* .history/ ``` -------------------------------------------------------------------------------- /src/tools/widget/README.md: -------------------------------------------------------------------------------- ```markdown # Widget Tools for Figma MCP This directory contains tools for interacting with Figma widgets through the MCP server. ## Directory Structure Each tool is organized in its own directory: - `get-widgets`: Lists all widgets in a file - `get-widget`: Gets detailed information about a specific widget - `get-widget-sync-data`: Retrieves a widget's synchronized state data - `search-widgets`: Searches for widgets with specific properties - `analyze-widget-structure`: Provides detailed analysis of a widget's structure ## Adding New Widget Tools To add a new widget tool: 1. Create a new directory for your tool under `src/tools/widget/` 2. Create an `index.ts` file with your tool implementation 3. Update the main `index.ts` to import and register your tool ## Using Shared Utilities Common widget utilities can be found in `src/tools/utils/widget-utils.ts`. ## Widget Tool Pattern Each widget tool follows this pattern: ```typescript export const yourToolName = (server: McpServer) => { server.tool( "tool_name", { // Parameter schema using zod param1: z.string().describe("Description"), param2: z.number().describe("Description") }, async ({ param1, param2 }) => { try { // Tool implementation return { content: [ { type: "text", text: "Response content" } ] }; } catch (error) { console.error('Error message:', error); return { content: [ { type: "text", text: `Error: ${(error as Error).message}` } ] }; } } ); }; ``` For more information, see the [Widget Tools Guide](/docs/widget-tools-guide.md). ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- ```markdown # Figma MCP Server Documentation Welcome to the documentation for the Figma MCP (Model Context Protocol) Server. This documentation provides comprehensive information about the project's purpose, implementation, features, and usage. ## Table of Contents 1. [Project Overview](./01-overview.md) - Introduction and purpose - Core features - Technology stack - Project structure - Integration with AI systems 2. [Implementation Steps](./02-implementation-steps.md) - Project setup - Configuration - Figma API integration - MCP server implementation - Build system - Documentation - Testing and verification 3. [Components and Features](./03-components-and-features.md) - Core components - MCP tools - Resource templates - Error handling - Response formatting 4. [Usage Guide](./04-usage-guide.md) - Setup instructions - Running the server - Using MCP tools (with examples) - Using resource templates - Error handling examples - Tips and best practices 5. [Project Status and Roadmap](./05-project-status.md) - Current status - Next steps - Version history - Known issues - Contribution guidelines - Support and feedback ## Quick Start To get started with the Figma MCP server: 1. Install dependencies: ```bash make install ``` 2. Configure your Figma API token in `.env` 3. Start the server: ```bash make mcp ``` For more detailed instructions, see the [Usage Guide](./04-usage-guide.md). ## Project Status The project is currently in its initial release version with core functionality implemented. See [Project Status and Roadmap](./05-project-status.md) for more details on current status and future plans. ## Contributing Contributions are welcome! See the [Contribution Guidelines](./05-project-status.md#5-contribution-guidelines) for more information on how to contribute to the project. --- Last updated: April 13, 2025 ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown [](https://mseep.ai/app/sichang824-mcp-figma) # Figma MCP Server A Figma API server implementation based on Model Context Protocol (MCP), supporting Figma plugin and widget integration. ## Features - Interact with Figma API through MCP - WebSocket server for Figma plugin communication - Support for Figma widget development - Environment variable configuration via command line arguments - Rich set of Figma operation tools ## Installation 1. Clone the repository: ```bash git clone <repository-url> cd figma-mcp ``` 2. Install dependencies: ```bash bun install ``` ## Configuration ### Environment Variables Create a `.env` file and set the following environment variables: ``` FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token PORT=3001 NODE_ENV=development ``` ### Getting a Figma Access Token 1. Log in to [Figma](https://www.figma.com/) 2. Go to Account Settings > Personal Access Tokens 3. Create a new access token 4. Copy the token to your `.env` file or pass it via command line arguments ## Usage ### Build the Project ```bash bun run build ``` ### Run the Development Server ```bash bun run dev ``` ### Using Command Line Arguments Support for setting environment variables via the `-e` parameter: ```bash bun --watch src/index.ts -e FIGMA_PERSONAL_ACCESS_TOKEN=your_token -e PORT=6000 ``` You can also use a dedicated token parameter: ```bash bun --watch src/index.ts --token your_token ``` Or its shorthand: ```bash bun --watch src/index.ts -t your_token ``` ## Configuring MCP in Cursor Add to the `.cursor/mcp.json` file: ```json { "Figma MCP": { "command": "bun", "args": [ "--watch", "/path/to/figma-mcp/src/index.ts", "-e", "FIGMA_PERSONAL_ACCESS_TOKEN=your_token_here", "-e", "PORT=6000" ] } } ``` ## Available Tools The server provides the following Figma operation tools: - File operations: Get files, versions, etc. - Node operations: Get and manipulate Figma nodes - Comment operations: Manage comments in Figma files - Image operations: Export Figma elements as images - Search functionality: Search content in Figma files - Component operations: Manage Figma components - Canvas operations: Create rectangles, circles, text, etc. - Widget operations: Manage Figma widgets ## Figma Plugin Development ### Plugin Introduction Figma plugins are customized tools that extend Figma's functionality, enabling workflow automation, adding new features, or integrating with external services. This MCP server provides a convenient way to develop, test, and deploy Figma plugins. ### Building and Testing Build the plugin: ```bash bun run build:plugin ``` Run in development mode: ```bash bun run dev:plugin ``` ### Loading the Plugin in Figma  1. Right-click in Figma to open the menu -> Plugins -> Development -> Import plugin from manifest... 2. Select the plugin's `manifest.json` file 3. Your plugin will now appear in Figma's plugin menu ### Plugin Interaction with MCP Server Plugins can communicate with the MCP server via WebSocket to achieve: - Complex data processing - External API integration - Cross-session data persistence - AI functionality integration ## Development ### Build Widget ```bash bun run build:widget ``` ### Build Plugin ```bash bun run build:plugin ``` ### Development Mode ```bash bun run dev:widget # Widget development mode bun run dev:plugin # Plugin development mode ``` ## License MIT ``` -------------------------------------------------------------------------------- /src/widget-tools.ts: -------------------------------------------------------------------------------- ```typescript ``` -------------------------------------------------------------------------------- /src/plugin/manifest.json: -------------------------------------------------------------------------------- ```json { "name": "Figma MCP Canvas Operation Tool", "id": "figma-mcp-canvas-tools", "api": "1.0.0", "main": "code.js", "ui": "ui.html", "editorType": ["figma"], "permissions": [] } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "name": "MCP Figma Widget", "id": "mcp-figma-widget", "api": "1.0.0", "main": "dist/widget-code.js", "capabilities": ["network-access"], "editorType": ["figma"], "containsWidget": true, "widgetApi": "1.0.0" } ``` -------------------------------------------------------------------------------- /src/plugin/tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "es6", "lib": ["es6", "dom"], "typeRoots": ["../../node_modules/@types", "../../node_modules/@figma"], "moduleResolution": "node", "strict": true }, "include": ["*.ts", "utils/**/*.ts"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /tsconfig.widget.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "jsx": "react", "jsxFactory": "figma.widget.h", "jsxFragmentFactory": "figma.widget.Fragment", "target": "es6", "strict": true, "typeRoots": [ "./node_modules/@types", "./node_modules/@figma" ] }, "include": ["src/widget/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "resolveJsonModule": true, "outDir": "dist", "declaration": true, "declarationDir": "dist/types", "baseUrl": ".", "paths": { "~/*": ["src/*"] } }, "include": ["src/**/*", "types/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- ```typescript /** * Utility functions for Figma MCP Server */ /** * Log function that writes to stderr instead of stdout * to avoid interfering with MCP stdio communication */ export function log(message: string): void { process.stderr.write(`${message}\n`); } /** * Error log function that writes to stderr */ export function logError(message: string, error?: unknown): void { const errorMessage = error instanceof Error ? error.message : error ? String(error) : 'Unknown error'; process.stderr.write(`ERROR: ${message}: ${errorMessage}\n`); } ``` -------------------------------------------------------------------------------- /src/tools/widget/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Widget Tools - Index file to export all widget-related tools */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { getWidgetsTool } from "./get-widgets.js"; import { getWidgetTool } from "./get-widget.js"; import { getWidgetSyncDataTool } from "./get-widget-sync-data.js"; import { searchWidgetsTool } from "./search-widgets.js"; import { analyzeWidgetStructureTool } from "./analyze-widget-structure.js"; /** * Registers all widget-related tools with the MCP server * @param server The MCP server instance */ export function registerWidgetTools(server: McpServer): void { // Register all widget tools getWidgetsTool(server); getWidgetTool(server); getWidgetSyncDataTool(server); searchWidgetsTool(server); analyzeWidgetStructureTool(server); } ``` -------------------------------------------------------------------------------- /src/plugin/utils/colorUtils.ts: -------------------------------------------------------------------------------- ```typescript /** * Color utility functions for Figma plugin */ /** * Convert hex color to RGB object * @param hex Hexadecimal color string (with or without #) * @returns RGB object with values between 0 and 1 */ export function hexToRgb(hex: string): RGB { hex = hex.replace('#', ''); const r = parseInt(hex.substring(0, 2), 16) / 255; const g = parseInt(hex.substring(2, 4), 16) / 255; const b = parseInt(hex.substring(4, 6), 16) / 255; return { r, g, b }; } /** * Convert RGB object to hex color string * @param rgb RGB object with values between 0 and 1 * @returns Hexadecimal color string with # */ export function rgbToHex(rgb: RGB): string { const r = Math.round(rgb.r * 255).toString(16).padStart(2, '0'); const g = Math.round(rgb.g * 255).toString(16).padStart(2, '0'); const b = Math.round(rgb.b * 255).toString(16).padStart(2, '0'); return `#${r}${g}${b}`; } /** * Create a solid color paint * @param color RGB color object or hex string * @returns Solid paint object */ export function createSolidPaint(color: RGB | string): SolidPaint { if (typeof color === 'string') { return { type: 'SOLID', color: hexToRgb(color) }; } return { type: 'SOLID', color }; } ``` -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Tools - Main index file for all MCP tools */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerWidgetTools } from "./widget/index.js"; import { registerFileTools } from "./file.js"; import { registerNodeTools } from "./node.js"; import { registerCommentTools } from "./comment.js"; import { registerImageTools } from "./image.js"; import { registerVersionTools } from "./version.js"; import { registerSearchTools } from "./search.js"; import { registerComponentTools } from "./component.js"; import { registerFrameTools } from "./frame.js"; import { registerCanvasTools } from "./canvas.js"; import { registerPageTools } from "./page.js"; /** * Registers all tools with the MCP server * @param server The MCP server instance */ export function registerAllTools(server: McpServer): void { // Register all tool categories registerFileTools(server); registerNodeTools(server); registerCommentTools(server); registerImageTools(server); registerVersionTools(server); registerSearchTools(server); registerComponentTools(server); registerWidgetTools(server); registerFrameTools(server); registerCanvasTools(server); registerPageTools(server); } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "figma-mcp-server", "version": "1.0.0", "description": "MCP server for accessing Figma API with widget support", "type": "module", "main": "dist/index.js", "scripts": { "start": "bun run dist/index.js", "dev": "bun --watch src/index.ts", "mcp": "bun --watch src/index.ts", "build": "bun build src/index.ts --outdir dist --target node", "build:mcp": "bun build src/index.ts --outdir dist --target node", "build:widget": "bun build src/widget/widget.tsx --outfile dist/widget-code.js --target browser", "dev:widget": "bun build src/widget/widget.tsx --outfile dist/widget-code.js --target browser --watch", "build:plugin": "bun build src/plugin/code.ts --outfile src/plugin/code.js --target browser", "dev:plugin": "bun build src/plugin/code.ts --outfile src/plugin/code.js --target browser --watch", "test": "bun test" }, "keywords": [ "figma", "api", "mcp", "server", "widget" ], "author": "", "license": "MIT", "dependencies": { "@create-figma-plugin/ui": "^4.0.0", "@create-figma-plugin/utilities": "^4.0.0", "@figma/rest-api-spec": "^0.27.0", "@figma/widget-typings": "^1.11.0", "@modelcontextprotocol/sdk": "^1.9.0", "@types/ws": "^8.18.1", "axios": "^1.6.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^5.1.0", "js-yaml": "^4.1.0", "ws": "^8.18.1", "zod": "^3.24.2" }, "devDependencies": { "@figma/plugin-typings": "^1.109.0", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/js-yaml": "^4.0.9", "@types/node": "^20.10.0", "typescript": "^5.3.2" } } ``` -------------------------------------------------------------------------------- /src/tools/node.ts: -------------------------------------------------------------------------------- ```typescript /** * Node tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getNodeTool = (server: McpServer) => { server.tool( "get_node", { file_key: z.string().min(1).describe("The Figma file key to retrieve from"), node_id: z.string().min(1).describe("The ID of the node to retrieve") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData) { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key}` } ] }; } return { content: [ { type: "text", text: `# Node: ${nodeData.document.name}` }, { type: "text", text: `Type: ${nodeData.document.type}` }, { type: "text", text: `ID: ${nodeData.document.id}` }, { type: "text", text: `Children: ${nodeData.document.children?.length || 0}` }, { type: "text", text: "```json\n" + JSON.stringify(nodeData.document, null, 2) + "\n```" } ] }; } catch (error) { console.error('Error fetching node:', error); return { content: [ { type: "text", text: `Error getting node: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all node-related tools with the MCP server */ export const registerNodeTools = (server: McpServer): void => { getNodeTool(server); }; ``` -------------------------------------------------------------------------------- /prompt.md: -------------------------------------------------------------------------------- ```markdown # Basic Command Line Coding Assistant You are a command line coding assistant. Help me write and manage code using these essential terminal commands: ## Basic File Operations - View files recursively: `tree -fiI ".venv|node_modules|.git|dist|<MORE_IGNORE_PATTERNS>"` - View file contents: `cat file.py` - Search in files: `grep "function" file.py` - Search recursively: `grep -r "pattern" directory/` - Find files by name: `find . -name "*.py"` - Write to file using cat: ``` cat > file.py << EOF # Add your code here EOF ``` - Move/rename files: `mv oldname.py newname.py` ## Assistant Behavior - Directly modify files without outputting code blocks - Read/Write all of docs in the project directory ./docs - Ensure code is not redundant or duplicative - Prioritize implementation logic and ask user when facing decisions - Maintain existing code style and naming conventions when modifying files - Use concise commands to execute operations efficiently - Consider performance implications when suggesting solutions - Provide clear explanation of steps taken during complex operations - Verify commands before execution, especially for destructive operations - Suggest file organization improvements when appropriate - Always write code in English, including all code, comments, and strings - After fully understanding responsibilities, respond with "Ready to start coding now" ## Project Preferences - TypeScript/Node.js: Use Bun instead of npm/node - Initialize: `bun init` - Install packages: `bun install <package>` - Run scripts: `bun run <script>` - Default Project Files: - Create Makefile - Create .envrc: - Project dir: /Users/ann/Workspace/MCP/figma - Project language: TypeScript ``` -------------------------------------------------------------------------------- /src/tools/version.ts: -------------------------------------------------------------------------------- ```typescript /** * Version tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getFileVersionsTool = (server: McpServer) => { server.tool( "get_file_versions", { file_key: z.string().min(1).describe("The Figma file key") }, async ({ file_key }) => { try { const versionsResponse = await figmaApi.getFileVersions(file_key); if (!versionsResponse.versions || versionsResponse.versions.length === 0) { return { content: [ { type: "text", text: `No versions found for file ${file_key}` } ] }; } const versionsList = versionsResponse.versions.map((version, index) => { return `${index + 1}. **${version.label || 'Unnamed version'}** - ${new Date(version.created_at).toLocaleString()} by ${version.user.handle}\n ${version.description || 'No description'}`; }).join('\n\n'); return { content: [ { type: "text", text: `# File Versions for ${file_key}` }, { type: "text", text: `Found ${versionsResponse.versions.length} versions:` }, { type: "text", text: versionsList } ] }; } catch (error) { console.error('Error fetching file versions:', error); return { content: [ { type: "text", text: `Error getting file versions: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all version-related tools with the MCP server */ export const registerVersionTools = (server: McpServer): void => { getFileVersionsTool(server); }; ``` -------------------------------------------------------------------------------- /src/tools/component.ts: -------------------------------------------------------------------------------- ```typescript /** * Component tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getComponentsTool = (server: McpServer) => { server.tool( "get_components", { file_key: z.string().min(1).describe("The Figma file key") }, async ({ file_key }) => { try { const componentsResponse = await figmaApi.getFileComponents(file_key); if (!componentsResponse.meta?.components || componentsResponse.meta.components.length === 0) { return { content: [ { type: "text", text: `No components found in file ${file_key}` } ] }; } const componentsList = componentsResponse.meta.components.map(component => { return `- **${component.name}** (Key: ${component.key})\n Description: ${component.description || 'No description'}\n ${component.remote ? '(Remote component)' : '(Local component)'}`; }).join('\n\n'); return { content: [ { type: "text", text: `# Components in file ${file_key}` }, { type: "text", text: `Found ${componentsResponse.meta.components.length} components:` }, { type: "text", text: componentsList } ] }; } catch (error) { console.error('Error fetching components:', error); return { content: [ { type: "text", text: `Error getting components: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all component-related tools with the MCP server */ export const registerComponentTools = (server: McpServer): void => { getComponentsTool(server); }; ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widgets.ts: -------------------------------------------------------------------------------- ```typescript /** * Tool: get_widgets * * Retrieves all widget nodes from a Figma file */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; import { FigmaUtils } from "../../utils/figma-utils.js"; export const getWidgetsTool = (server: McpServer) => { server.tool( "get_widgets", { file_key: z.string().min(1).describe("The Figma file key to retrieve widgets from") }, async ({ file_key }) => { try { const file = await figmaApi.getFile(file_key); // Find all widget nodes in the file const widgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); if (widgetNodes.length === 0) { return { content: [ { type: "text", text: `No widgets found in file ${file_key}` } ] }; } const widgetsList = widgetNodes.map((node, index) => { const widgetSyncData = node.widgetSync ? `\n - Widget Sync Data: Available` : `\n - Widget Sync Data: None`; return `${index + 1}. **${node.name}** (ID: ${node.id}) - Widget ID: ${node.widgetId || 'Unknown'}${widgetSyncData}`; }).join('\n\n'); return { content: [ { type: "text", text: `# Widgets in file ${file_key}` }, { type: "text", text: `Found ${widgetNodes.length} widgets:` }, { type: "text", text: widgetsList } ] }; } catch (error) { console.error('Error fetching widgets:', error); return { content: [ { type: "text", text: `Error getting widgets: ${(error as Error).message}` } ] }; } } ); }; ``` -------------------------------------------------------------------------------- /src/tools/file.ts: -------------------------------------------------------------------------------- ```typescript /** * File tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getFileTool = (server: McpServer) => { server.tool( "get_file", { file_key: z.string().min(1).describe("The Figma file key to retrieve"), return_full_file: z.boolean().default(false).describe("Whether to return the full file contents or just a summary") }, async ({ file_key, return_full_file }) => { try { const file = await figmaApi.getFile(file_key); if (return_full_file) { return { content: [ { type: "text", text: `Retrieved Figma file: ${file.name}` }, { type: "text", text: JSON.stringify(file, null, 2) } ] }; } else { return { content: [ { type: "text", text: `# Figma File: ${file.name}` }, { type: "text", text: `Last modified: ${file.lastModified}` }, { type: "text", text: `Document contains ${file.document.children?.length || 0} top-level nodes.` }, { type: "text", text: `Components: ${Object.keys(file.components).length || 0}` }, { type: "text", text: `Component sets: ${Object.keys(file.componentSets).length || 0}` }, { type: "text", text: `Styles: ${Object.keys(file.styles).length || 0}` } ] }; } } catch (error) { console.error('Error fetching file:', error); return { content: [ { type: "text", text: `Error getting Figma file: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all file-related tools with the MCP server */ export const registerFileTools = (server: McpServer): void => { getFileTool(server); }; ``` -------------------------------------------------------------------------------- /src/tools/image.ts: -------------------------------------------------------------------------------- ```typescript /** * Image tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getImagesTool = (server: McpServer) => { server.tool( "get_images", { file_key: z.string().min(1).describe("The Figma file key"), node_ids: z.array(z.string()).min(1).describe("The IDs of nodes to export as images"), format: z.enum(["jpg", "png", "svg", "pdf"]).default("png").describe("Image format to export"), scale: z.number().min(0.01).max(4).default(1).describe("Scale factor for the image (0.01 to 4)") }, async ({ file_key, node_ids, format, scale }) => { try { const imagesResponse = await figmaApi.getImages(file_key, node_ids, { format, scale }); if (imagesResponse.err) { return { content: [ { type: "text", text: `Error getting images: ${imagesResponse.err}` } ] }; } const imageUrls = Object.entries(imagesResponse.images) .map(([nodeId, url]) => { if (!url) { return `- ${nodeId}: Error generating image`; } return `- ${nodeId}: [Image URL](${url})`; }) .join('\n'); return { content: [ { type: "text", text: `# Images for file ${file_key}` }, { type: "text", text: `Format: ${format}, Scale: ${scale}` }, { type: "text", text: imageUrls } ] }; } catch (error) { console.error('Error fetching images:', error); return { content: [ { type: "text", text: `Error getting images: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all image-related tools with the MCP server */ export const registerImageTools = (server: McpServer): void => { getImagesTool(server); }; ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widget.ts: -------------------------------------------------------------------------------- ```typescript /** * Tool: get_widget * * Retrieves detailed information about a specific widget node */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; export const getWidgetTool = (server: McpServer) => { server.tool( "get_widget", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; // Get the sync data if available let syncDataContent = ''; if (widgetNode.widgetSync) { try { const syncData = JSON.parse(widgetNode.widgetSync); syncDataContent = `\n\n## Widget Sync Data\n\`\`\`json\n${JSON.stringify(syncData, null, 2)}\n\`\`\``; } catch (error) { syncDataContent = '\n\n## Widget Sync Data\nError parsing widget sync data'; } } return { content: [ { type: "text", text: `# Widget: ${widgetNode.name}` }, { type: "text", text: `ID: ${widgetNode.id}` }, { type: "text", text: `Widget ID: ${widgetNode.widgetId || 'Unknown'}` }, { type: "text", text: `Has Sync Data: ${widgetNode.widgetSync ? 'Yes' : 'No'}${syncDataContent}` } ] }; } catch (error) { console.error('Error fetching widget node:', error); return { content: [ { type: "text", text: `Error getting widget: ${(error as Error).message}` } ] }; } } ); }; ``` -------------------------------------------------------------------------------- /src/tools/search.ts: -------------------------------------------------------------------------------- ```typescript /** * Search tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; import { FigmaUtils } from "../utils/figma-utils.js"; export const searchTextTool = (server: McpServer) => { server.tool( "search_text", { file_key: z.string().min(1).describe("The Figma file key"), search_text: z.string().min(1).describe("The text to search for in the file") }, async ({ file_key, search_text }) => { try { const file = await figmaApi.getFile(file_key); // Find all TEXT nodes const textNodes = FigmaUtils.getNodesByType(file, 'TEXT'); // Filter for nodes containing the search text const matchingNodes = textNodes.filter(node => node.characters && node.characters.toLowerCase().includes(search_text.toLowerCase()) ); if (matchingNodes.length === 0) { return { content: [ { type: "text", text: `No text matching "${search_text}" found in file ${file_key}` } ] }; } const matchesList = matchingNodes.map(node => { const path = FigmaUtils.getNodePath(file, node.id); return `- **${node.name}** (ID: ${node.id})\n Path: ${path.join(' > ')}\n Text: "${node.characters}"`; }).join('\n\n'); return { content: [ { type: "text", text: `# Text Search Results for "${search_text}"` }, { type: "text", text: `Found ${matchingNodes.length} matching text nodes:` }, { type: "text", text: matchesList } ] }; } catch (error) { console.error('Error searching text:', error); return { content: [ { type: "text", text: `Error searching text: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all search-related tools with the MCP server */ export const registerSearchTools = (server: McpServer): void => { searchTextTool(server); }; ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widget-sync-data.ts: -------------------------------------------------------------------------------- ```typescript /** * Tool: get_widget_sync_data * * Retrieves the synchronized state data for a specific widget */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; export const getWidgetSyncDataTool = (server: McpServer) => { server.tool( "get_widget_sync_data", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; if (!widgetNode.widgetSync) { return { content: [ { type: "text", text: `Widget ${node_id} does not have any sync data` } ] }; } try { const syncData = JSON.parse(widgetNode.widgetSync); return { content: [ { type: "text", text: `# Widget Sync Data for "${widgetNode.name}"` }, { type: "text", text: `Widget ID: ${widgetNode.id}` }, { type: "text", text: "```json\n" + JSON.stringify(syncData, null, 2) + "\n```" } ] }; } catch (error) { console.error('Error parsing widget sync data:', error); return { content: [ { type: "text", text: `Error parsing widget sync data: ${(error as Error).message}` } ] }; } } catch (error) { console.error('Error fetching widget sync data:', error); return { content: [ { type: "text", text: `Error getting widget sync data: ${(error as Error).message}` } ] }; } } ); }; ``` -------------------------------------------------------------------------------- /docs/01-overview.md: -------------------------------------------------------------------------------- ```markdown # Figma MCP Server - Project Overview ## Introduction The Figma MCP Server is a Model Context Protocol (MCP) implementation that provides a bridge between AI assistants and the Figma API. This allows AI systems to interact with Figma files, components, and other resources through a standardized protocol, enabling rich integrations and automations with Figma designs. ## Purpose This project aims to: 1. Provide AI assistants with the ability to access and manipulate Figma files 2. Enable structured access to Figma resources through the MCP protocol 3. Bridge the gap between design tools and AI systems 4. Support design workflows with AI-assisted operations ## Core Features - **File Access**: Retrieve Figma files and inspect their contents - **Node Operations**: Access specific nodes within Figma files - **Comment Management**: Read and write comments on Figma files - **Image Export**: Export nodes as images in various formats - **Search Capabilities**: Search for text and elements within files - **Component Access**: View and work with Figma components - **Version History**: Access file version history ## Technology Stack - **TypeScript**: Type-safe implementation - **Bun**: JavaScript/TypeScript runtime and package manager - **MCP SDK**: Model Context Protocol implementation - **Figma REST API**: Official Figma API with TypeScript definitions - **Zod**: Schema validation for parameters and configurations ## Project Structure ``` /figma ├── dist/ # Compiled output ├── docs/ # Documentation ├── src/ │ ├── config/ # Configuration files │ ├── services/ # API and external service integrations │ └── utils/ # Utility functions ├── types/ # Type definitions ├── .env # Environment variables ├── Makefile # Build and run commands ├── README.md # Project README ├── package.json # Dependencies and scripts └── tsconfig.json # TypeScript configuration ``` ## Integration with AI Systems This MCP server enables AI assistants to: 1. Retrieve design information from Figma 2. Answer questions about design files 3. Generate images and assets from Figma files 4. Add comments and feedback to designs 5. Search for specific elements or text within designs 6. Track version history and changes With these capabilities, AI systems can provide more contextual and helpful responses when users ask about their Figma designs, streamlining the design workflow and enhancing collaboration between designers and stakeholders. ``` -------------------------------------------------------------------------------- /src/tools/utils/widget-utils.ts: -------------------------------------------------------------------------------- ```typescript /** * Widget Utils - Helper functions for widget tools */ import type { Node } from '@figma/rest-api-spec'; /** * Parses the widget sync data from a widget node * @param node A Figma node of type WIDGET * @returns The parsed sync data object or null if not available/invalid */ export function parseWidgetSyncData(node: Node): Record<string, any> | null { if (node.type !== 'WIDGET' || !node.widgetSync) { return null; } try { return JSON.parse(node.widgetSync); } catch (error) { console.error('Error parsing widget sync data:', error); return null; } } /** * Formats widget sync data as a string * @param syncData The widget sync data object * @returns A formatted string representation of the sync data */ export function formatWidgetSyncData(syncData: Record<string, any> | null): string { if (!syncData) { return 'No sync data available'; } return JSON.stringify(syncData, null, 2); } /** * Gets a summary of the widget's properties * @param node A Figma node of type WIDGET * @returns A summary object with key widget properties */ export function getWidgetSummary(node: Node): Record<string, any> { if (node.type !== 'WIDGET') { return { error: 'Not a widget node' }; } const summary: Record<string, any> = { id: node.id, name: node.name, type: 'WIDGET', widgetId: node.widgetId || 'Unknown', }; // If there's widget sync data, analyze it if (node.widgetSync) { try { const syncData = JSON.parse(node.widgetSync); const syncKeys = Object.keys(syncData); summary.syncDataKeys = syncKeys; summary.hasSyncData = syncKeys.length > 0; } catch (error) { summary.hasSyncData = false; summary.syncDataError = 'Invalid sync data format'; } } else { summary.hasSyncData = false; } return summary; } /** * Creates a human-readable description of the widget * @param node A Figma node of type WIDGET * @returns A detailed text description of the widget */ export function createWidgetDescription(node: Node): string { if (node.type !== 'WIDGET') { return 'Not a widget node'; } let description = `Widget "${node.name}" (ID: ${node.id})`; if (node.widgetId) { description += `\nWidget ID: ${node.widgetId}`; } if (node.widgetSync) { try { const syncData = JSON.parse(node.widgetSync); const syncKeys = Object.keys(syncData); description += `\nSync Data Keys: ${syncKeys.join(', ')}`; } catch (error) { description += '\nSync Data: [Invalid format]'; } } else { description += '\nSync Data: None'; } return description; } ``` -------------------------------------------------------------------------------- /src/plugin/utils/nodeUtils.ts: -------------------------------------------------------------------------------- ```typescript /** * Utility functions for handling Figma nodes */ /** * Apply common properties to any node type * Safely handles properties that might not be available on all node types * * @param node The target node to apply properties to * @param data Object containing the properties to apply */ export function applyCommonProperties(node: SceneNode, data: any): void { // Position if (data.x !== undefined) node.x = data.x; if (data.y !== undefined) node.y = data.y; // Name if (data.name) node.name = data.name; // Properties that aren't available on all node types // We need to check if they exist before setting them // Opacity if (data.opacity !== undefined && 'opacity' in node) { (node as any).opacity = data.opacity; } // Blend mode if (data.blendMode && 'blendMode' in node) { (node as any).blendMode = data.blendMode; } // Effects if (data.effects && 'effects' in node) { (node as any).effects = data.effects; } // Constraint if (data.constraints && 'constraints' in node) { (node as any).constraints = data.constraints; } // Is Mask if (data.isMask !== undefined && 'isMask' in node) { (node as any).isMask = data.isMask; } // Visible if (data.visible !== undefined) node.visible = data.visible; // Locked if (data.locked !== undefined) node.locked = data.locked; } /** * Select and focus on a node or set of nodes * @param nodes Node or array of nodes to focus on */ export function selectAndFocusNodes(nodes: SceneNode | SceneNode[]): void { const nodesToFocus = Array.isArray(nodes) ? nodes : [nodes]; figma.currentPage.selection = nodesToFocus; figma.viewport.scrollAndZoomIntoView(nodesToFocus); } /** * Build a result object from a node or array of nodes * @param result Node or array of nodes to create a result object from * @returns Object containing node information in a consistent format */ export function buildResultObject(result: SceneNode | readonly SceneNode[] | null): {[key: string]: any} { let resultObject: {[key: string]: any} = {}; if (!result) return resultObject; if (Array.isArray(result)) { // Handle array result (like from get-selection) resultObject.count = result.length; if (result.length > 0) { resultObject.items = result.map(node => ({ id: node.id, type: node.type, name: node.name })); } } else { // Handle single node result - we know it's a SceneNode at this point const node = result as SceneNode; resultObject.id = node.id; resultObject.type = node.type; resultObject.name = node.name; } return resultObject; } ``` -------------------------------------------------------------------------------- /src/tools/widget/analyze-widget-structure.ts: -------------------------------------------------------------------------------- ```typescript /** * Tool: analyze_widget_structure * * Provides a detailed analysis of a widget's structure and properties */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; export const analyzeWidgetStructureTool = (server: McpServer) => { server.tool( "analyze_widget_structure", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; // Create a full analysis of the widget const widgetAnalysis = { basic: { id: widgetNode.id, name: widgetNode.name, type: widgetNode.type, widgetId: widgetNode.widgetId || 'Unknown' }, placement: { x: widgetNode.x || 0, y: widgetNode.y || 0, width: widgetNode.width || 0, height: widgetNode.height || 0, rotation: widgetNode.rotation || 0 }, syncData: null as any }; // Parse the widget sync data if available if (widgetNode.widgetSync) { try { widgetAnalysis.syncData = JSON.parse(widgetNode.widgetSync); } catch (error) { widgetAnalysis.syncData = { error: 'Invalid sync data format' }; } } return { content: [ { type: "text", text: `# Widget Analysis: ${widgetNode.name}` }, { type: "text", text: `## Basic Information` }, { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.basic, null, 2) + "\n```" }, { type: "text", text: `## Placement` }, { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.placement, null, 2) + "\n```" }, { type: "text", text: `## Sync Data` }, { type: "text", text: widgetAnalysis.syncData ? "```json\n" + JSON.stringify(widgetAnalysis.syncData, null, 2) + "\n```" : "No sync data available" } ] }; } catch (error) { console.error('Error analyzing widget:', error); return { content: [ { type: "text", text: `Error analyzing widget: ${(error as Error).message}` } ] }; } } ); }; ``` -------------------------------------------------------------------------------- /src/services/websocket.ts: -------------------------------------------------------------------------------- ```typescript /** * WebSocket Service - Handles communication with Figma plugin */ import { WebSocketServer, WebSocket as WSWebSocket } from "ws"; import { log, logError } from "../utils.js"; // Store active plugin connection WebSocket let activePluginConnection: WSWebSocket | null = null; // Callbacks for handling responses const pendingCommands = new Map<string, (response: any) => void>(); interface PluginResponse { success: boolean; result?: any; error?: string; } /** * Create WebSocket server */ export function initializeWebSocketServer(port = 3001) { const wss = new WebSocketServer({ port }); log(`WebSocket server started on port ${port}`); wss.on("connection", (ws: WSWebSocket) => { log("New WebSocket connection"); ws.on("message", (message: WSWebSocket.Data) => { try { const data = JSON.parse(message.toString()); log(`Received WebSocket message: ${JSON.stringify(data)}`); if (data.type === "figma-plugin-connected") { // Store active connection activePluginConnection = ws; log(`Figma plugin connected: ${data.pluginId || "unknown"}`); } else if (data.type === "figma-plugin-response") { // Handle response from plugin const { command, success, result, error } = data; const callback = pendingCommands.get(command); if (callback) { callback({ success, result, error }); pendingCommands.delete(command); } } } catch (error) { logError("Error processing WebSocket message", error); } }); ws.on("close", () => { log("WebSocket connection closed"); if (activePluginConnection === ws) { activePluginConnection = null; } }); ws.on("error", (error: Error) => { logError("WebSocket error", error); }); }); return wss; } /** * Send command to Figma plugin */ export async function sendCommandToPlugin( command: string, params: any ): Promise<PluginResponse> { return new Promise((resolve, reject) => { if (!activePluginConnection) { reject(new Error("No active Figma plugin connection")); return; } try { // Store callback pendingCommands.set(command, resolve); // Send command activePluginConnection.send( JSON.stringify({ type: "mcp-command", command, params, }) ); // Set timeout setTimeout(() => { if (pendingCommands.has(command)) { pendingCommands.delete(command); reject(new Error(`Command ${command} timed out`)); } }, 10000); // 10 second timeout } catch (error) { pendingCommands.delete(command); reject(error); } }); } /** * Check if a Figma plugin is connected */ export function isPluginConnected(): boolean { return activePluginConnection !== null; } ``` -------------------------------------------------------------------------------- /src/utils/widget-utils.ts: -------------------------------------------------------------------------------- ```typescript /** * Utility functions for working with Figma Widgets */ import type { GetFileResponse, Node } from '@figma/rest-api-spec'; import type { WidgetNode, WidgetSyncData } from '../services/widget-api.js'; /** * Utility functions for working with Figma Widgets */ export class WidgetUtils { /** * Find all widget nodes in a file */ static findAllWidgetNodes(file: GetFileResponse): Node[] { const widgetNodes: Node[] = []; // Helper function to recursively search for widget nodes const findWidgets = (node: Node) => { if (node.type === 'WIDGET') { widgetNodes.push(node); } if (node.children) { for (const child of node.children) { findWidgets(child); } } }; findWidgets(file.document); return widgetNodes; } /** * Extract widget sync data from a widget node */ static extractWidgetSyncData(node: Node): WidgetSyncData | null { if (node.type !== 'WIDGET' || !node.widgetSync) { return null; } try { return JSON.parse(node.widgetSync); } catch (error) { console.error('Error parsing widget sync data:', error); return null; } } /** * Format widget sync data for display */ static formatWidgetSyncData(syncData: WidgetSyncData | null): string { if (!syncData) { return 'No sync data available'; } return JSON.stringify(syncData, null, 2); } /** * Get a summary of a widget node */ static getWidgetSummary(node: WidgetNode): Record<string, any> { const { id, name, widgetId } = node; const summary: Record<string, any> = { id, name, type: 'WIDGET', widgetId: widgetId || 'Unknown', }; // If there's widget sync data, add a summary if (node.widgetSync) { try { const syncData = JSON.parse(node.widgetSync); const syncKeys = Object.keys(syncData); summary.syncDataKeys = syncKeys; summary.hasSyncData = syncKeys.length > 0; } catch (error) { summary.hasSyncData = false; summary.syncDataError = 'Invalid sync data format'; } } else { summary.hasSyncData = false; } return summary; } /** * Check if a node is a widget */ static isWidgetNode(node: Node): boolean { return node.type === 'WIDGET'; } /** * Create a human-readable description of a widget */ static createWidgetDescription(widget: WidgetNode): string { let description = ; if (widget.widgetId) { description += ; } if (widget.widgetSync) { try { const syncData = JSON.parse(widget.widgetSync); const syncKeys = Object.keys(syncData); description += ; } catch (error) { description += '\nSync Data: [Invalid format]'; } } else { description += '\nSync Data: None'; } return description; } } ``` -------------------------------------------------------------------------------- /src/plugin/creators/componentCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Component-related creation functions for Figma plugin */ import { applyCommonProperties } from '../utils/nodeUtils'; /** * Create a component from another node * @param data Configuration data with sourceNode reference * @returns Created component node */ export function createComponentFromNodeData(data: any): ComponentNode | null { // We need a sourceNode to create a component from if (!data.sourceNode) { console.error('createComponentFromNode requires a sourceNode'); return null; } try { // If sourceNode is a string, try to find it by ID let sourceNode; if (typeof data.sourceNode === 'string') { sourceNode = figma.getNodeById(data.sourceNode); if (!sourceNode || !('type' in sourceNode)) { console.error(`Node with ID ${data.sourceNode} not found or is not a valid node`); return null; } } else { sourceNode = data.sourceNode; } // Create the component from the source node const component = figma.createComponentFromNode(sourceNode as SceneNode); // Apply component-specific properties if (data.description) component.description = data.description; // Apply common properties applyCommonProperties(component, data); return component; } catch (error) { console.error('Failed to create component from node:', error); return null; } } /** * Create a component set (variant container) * @param data Configuration data for component set * @returns Created component set node */ export function createComponentSetFromData(data: any): ComponentSetNode | null { try { // Create an empty component set // In practice, component sets are usually created by combining variants // using figma.combineAsVariants, not directly created // Get the components to combine if (!data.components || !Array.isArray(data.components) || data.components.length === 0) { console.error('Component set creation requires component nodes'); return null; } const componentNodes: ComponentNode[] = []; // Collect the component nodes (could be IDs or actual nodes) for (const component of data.components) { let node; if (typeof component === 'string') { // If it's a string, assume it's a node ID node = figma.getNodeById(component); } else { node = component; } if (node && node.type === 'COMPONENT') { componentNodes.push(node as ComponentNode); } } if (componentNodes.length === 0) { console.error('No valid component nodes provided'); return null; } // Combine the components as variants const componentSet = figma.combineAsVariants(componentNodes, figma.currentPage); // Apply component set properties if (data.name) componentSet.name = data.name; // Apply common properties applyCommonProperties(componentSet, data); return componentSet; } catch (error) { console.error('Failed to create component set:', error); return null; } } ``` -------------------------------------------------------------------------------- /src/utils/figma-utils.ts: -------------------------------------------------------------------------------- ```typescript import type { GetFileResponse, Node } from '@figma/rest-api-spec'; /** * Utility functions for working with Figma files and nodes */ export class FigmaUtils { /** * Find a node by ID in a Figma file */ static findNodeById(file: GetFileResponse, nodeId: string): Node | null { // If node ID is the document itself if (nodeId === file.document.id) { return file.document; } // Helper function to recursively search for node const findNode = (node: Node): Node | null => { if (node.id === nodeId) { return node; } if (node.children) { for (const child of node.children) { const found = findNode(child); if (found) return found; } } return null; }; return findNode(file.document); } /** * Get all nodes of a specific type from a Figma file */ static getNodesByType(file: GetFileResponse, type: string): Node[] { const nodes: Node[] = []; // Helper function to recursively search for nodes const findNodes = (node: Node) => { if (node.type === type) { nodes.push(node); } if (node.children) { for (const child of node.children) { findNodes(child); } } }; findNodes(file.document); return nodes; } /** * Format a node ID for display (e.g., "1:2" -> "Node 1:2") */ static formatNodeId(nodeId: string): string { return `Node ${nodeId}`; } /** * Extract text content from a TEXT node */ static extractTextFromNode(node: Node): string { return node.type === 'TEXT' ? node.characters || '' : ''; } /** * Get a simple representation of a node's properties */ static getNodeProperties(node: Node): Record<string, any> { const { id, name, type } = node; let properties: Record<string, any> = { id, name, type }; // Add type-specific properties switch (node.type) { case 'TEXT': properties.text = node.characters; break; case 'RECTANGLE': case 'ELLIPSE': case 'POLYGON': case 'STAR': case 'VECTOR': if (node.fills) { properties.fills = node.fills.map(fill => ({ type: fill.type, visible: fill.visible, })); } break; case 'FRAME': case 'GROUP': case 'INSTANCE': case 'COMPONENT': properties.childCount = node.children?.length || 0; break; } return properties; } /** * Get the path to a node in the document */ static getNodePath(file: GetFileResponse, nodeId: string): string[] { const path: string[] = []; const findPath = (node: Node, target: string): boolean => { if (node.id === target) { path.unshift(node.name); return true; } if (node.children) { for (const child of node.children) { if (findPath(child, target)) { path.unshift(node.name); return true; } } } return false; }; findPath(file.document, nodeId); return path; } } ``` -------------------------------------------------------------------------------- /docs/widget-tools-guide.md: -------------------------------------------------------------------------------- ```markdown # Figma Widget Tools Guide This guide explains how to use the MCP server's Widget Tools for interacting with Figma widgets. ## Overview Widget Tools are a set of utilities that allow AI assistants to interact with and manipulate Figma widgets through the MCP protocol. These tools provide capabilities for discovering, analyzing, and working with widgets in Figma files. ## Available Widget Tools ### 1. get_widgets Retrieves all widget nodes from a Figma file. **Parameters:** - `file_key` (string): The Figma file key to retrieve widgets from **Example:** ```json { "file_key": "abcxyz123456" } ``` **Response:** Returns a list of all widgets in the file, including their names, IDs, and whether they have sync data. ### 2. get_widget Retrieves detailed information about a specific widget node. **Parameters:** - `file_key` (string): The Figma file key - `node_id` (string): The ID of the widget node **Example:** ```json { "file_key": "abcxyz123456", "node_id": "1:123" } ``` **Response:** Returns detailed information about the specified widget, including its sync data if available. ### 3. get_widget_sync_data Retrieves the synchronized state data for a specific widget. **Parameters:** - `file_key` (string): The Figma file key - `node_id` (string): The ID of the widget node **Example:** ```json { "file_key": "abcxyz123456", "node_id": "1:123" } ``` **Response:** Returns the raw sync data (state) for the specified widget in JSON format. ### 4. search_widgets Searches for widgets that have specific sync data properties and values. **Parameters:** - `file_key` (string): The Figma file key - `property_key` (string): The sync data property key to search for - `property_value` (string, optional): Optional property value to match **Example:** ```json { "file_key": "abcxyz123456", "property_key": "count", "property_value": "5" } ``` **Response:** Returns a list of widgets that match the search criteria. ### 5. analyze_widget_structure Provides a detailed analysis of a widget's structure and properties. **Parameters:** - `file_key` (string): The Figma file key - `node_id` (string): The ID of the widget node **Example:** ```json { "file_key": "abcxyz123456", "node_id": "1:123" } ``` **Response:** Returns a comprehensive analysis of the widget's structure, including basic information, placement details, and sync data. ## Widget Integration These tools can be used to: 1. Discover widgets in Figma files 2. Analyze widget properties and state 3. Search for widgets with specific characteristics 4. Extract widget sync data for external processing 5. Generate reports about widget usage in design files ## Implementation Details The widget tools use the Figma API to access widget data and analyze their properties. They are designed to work seamlessly with the MCP protocol and provide rich, structured information about widgets that can be used by AI assistants. ## Future Enhancements Planned enhancements for widget tools include: - Widget state modification capabilities (requires special access) - Widget creation and deletion - Widget template libraries - Widget analytics and usage statistics ``` -------------------------------------------------------------------------------- /src/tools/comment.ts: -------------------------------------------------------------------------------- ```typescript /** * Comment tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; export const getCommentsTool = (server: McpServer) => { server.tool( "get_comments", { file_key: z.string().min(1).describe("The Figma file key to retrieve comments from") }, async ({ file_key }) => { try { const commentsResponse = await figmaApi.getComments(file_key, { as_md: true }); if (!commentsResponse.comments || commentsResponse.comments.length === 0) { return { content: [ { type: "text", text: `No comments found in file ${file_key}` } ] }; } const commentsList = commentsResponse.comments.map(comment => { return `- **${comment.user.handle}** (${new Date(comment.created_at).toLocaleString()}): ${comment.message}`; }).join('\n'); return { content: [ { type: "text", text: `# Comments for file ${file_key}` }, { type: "text", text: `Found ${commentsResponse.comments.length} comments:` }, { type: "text", text: commentsList } ] }; } catch (error) { console.error('Error fetching comments:', error); return { content: [ { type: "text", text: `Error getting comments: ${(error as Error).message}` } ] }; } } ); }; export const addCommentTool = (server: McpServer) => { server.tool( "add_comment", { file_key: z.string().min(1).describe("The Figma file key"), message: z.string().min(1).describe("The comment text"), node_id: z.string().optional().describe("Optional node ID to attach the comment to") }, async ({ file_key, message, node_id }) => { try { const commentData: any = { message }; // If node_id is provided, create a client_meta object if (node_id) { // Create a frame offset client_meta commentData.client_meta = { node_id: node_id, node_offset: { x: 0, y: 0 } }; } const commentResponse = await figmaApi.postComment(file_key, commentData); return { content: [ { type: "text", text: `Comment added successfully!` }, { type: "text", text: `Comment ID: ${commentResponse.id}` }, { type: "text", text: `By user: ${commentResponse.user.handle}` }, { type: "text", text: `Added at: ${new Date(commentResponse.created_at).toLocaleString()}` } ] }; } catch (error) { console.error('Error adding comment:', error); return { content: [ { type: "text", text: `Error adding comment: ${(error as Error).message}` } ] }; } } ); }; /** * Registers all comment-related tools with the MCP server */ export const registerCommentTools = (server: McpServer): void => { getCommentsTool(server); addCommentTool(server); }; ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript /** * Figma MCP Server - Main entry point * * This server provides a Model Context Protocol (MCP) implementation * for interacting with the Figma API. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { execSync } from "child_process"; import * as dotenv from "dotenv"; import { env } from "./config/env.js"; import { registerAllResources } from "./resources.js"; import { initializeWebSocketServer } from "./services/websocket.js"; import { registerAllTools } from "./tools/index.js"; import { log } from "./utils.js"; // Load environment variables dotenv.config(); // Check for and kill any existing processes using the same port function killExistingProcesses() { try { const wsPort = env.WEBSOCKET_PORT || 3001; log(`Checking for processes using port ${wsPort}...`); // Find processes using the websocket port const findCmd = process.platform === "win32" ? `netstat -ano | findstr :${wsPort}` : `lsof -i:${wsPort} | grep LISTEN`; let output; try { output = execSync(findCmd, { encoding: "utf8" }); } catch (e) { // No process found, which is fine log("No existing processes found."); return; } // Extract PIDs and kill them if (output) { if (process.platform === "win32") { // Windows: extract PID from last column const pids = output .split("\n") .filter((line) => line.trim()) .map((line) => line.trim().split(/\s+/).pop()) .filter((pid, index, self) => pid && self.indexOf(pid) === index); pids.forEach((pid) => { if (pid && parseInt(pid) !== process.pid) { try { execSync(`taskkill /F /PID ${pid}`); log(`Killed process with PID: ${pid}`); } catch (e) { log(`Failed to kill process with PID: ${pid}`); } } }); } else { // Unix-like: extract PID from second column const pids = output .split("\n") .filter((line) => line.trim()) .map((line) => { const parts = line.trim().split(/\s+/); return parts[1]; }) .filter((pid, index, self) => pid && self.indexOf(pid) === index); pids.forEach((pid) => { if (pid && parseInt(pid) !== process.pid) { try { execSync(`kill -9 ${pid}`); log(`Killed process with PID: ${pid}`); } catch (e) { log(`Failed to kill process with PID: ${pid}`); } } }); } } } catch (error) { log(`Error checking for existing processes: ${error}`); } } // Kill any existing processes before starting killExistingProcesses(); // Create an MCP server const server = new McpServer({ name: "Figma API", version: "1.0.0", }); // Register all tools and resources registerAllTools(server); registerAllResources(server); // Initialize WebSocket server for Figma plugin communication const wsPort = env.WEBSOCKET_PORT || 3001; initializeWebSocketServer(wsPort); // Start the MCP server with stdio transport const transport = new StdioServerTransport(); server.connect(transport); // Use logger utility to avoid interfering with stdout used by MCP log("Figma MCP Server started"); export { server }; ``` -------------------------------------------------------------------------------- /src/plugin/creators/sliceCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Slice and page-related element creation functions */ import { applyCommonProperties } from "../utils/nodeUtils"; /** * Create a slice node from data * @param data Slice configuration data * @returns Created slice node */ export function createSliceFromData(data: any): SliceNode { const slice = figma.createSlice(); // Set size and position if (data.width && data.height) { slice.resize(data.width, data.height); } if (data.x !== undefined) slice.x = data.x; if (data.y !== undefined) slice.y = data.y; // Apply export settings if (data.exportSettings && Array.isArray(data.exportSettings)) { slice.exportSettings = data.exportSettings; } // Apply common properties that apply to slices if (data.name) slice.name = data.name; if (data.visible !== undefined) slice.visible = data.visible; return slice; } /** * Create a page node from data * @param data Page configuration data * @returns Created page node */ export function createPageFromData(data: any): PageNode { const page = figma.createPage(); // Set page name if (data.name) page.name = data.name; // Set background color if provided if (data.backgrounds) page.backgrounds = data.backgrounds; return page; } /** * Create a page divider (used for sections) * @param data Page divider configuration data * @returns Created page divider node */ export function createPageDividerFromData(data: any) { // Check if this method is available in the current Figma version if (!("createPageDivider" in figma)) { console.error("createPageDivider is not supported in this Figma version"); return null; } try { // Using type assertion since API might not be recognized in all Figma versions const pageDivider = (figma as any).createPageDivider(); // Set properties if (data.name) pageDivider.name = data.name; return pageDivider; } catch (error) { console.error("Failed to create page divider:", error); return null; } } /** * Create a slide node (for Figma Slides) * @param data Slide configuration data * @returns Created slide node */ export function createSlideFromData(data: any): SlideNode | null { // Check if this method is available in the current Figma version if (!("createSlide" in figma)) { console.error("createSlide is not supported in this Figma version"); return null; } try { // Using type assertion since API might not be recognized const slide = (figma as any).createSlide(); // Set slide properties if (data.name) slide.name = data.name; // Apply common properties applyCommonProperties(slide, data); return slide; } catch (error) { console.error("Failed to create slide:", error); return null; } } /** * Create a slide row node (for Figma Slides) * @param data Slide row configuration data * @returns Created slide row node */ export function createSlideRowFromData(data: any): SlideRowNode | null { // Check if this method is available in the current Figma version if (!("createSlideRow" in figma)) { console.error("createSlideRow is not supported in this Figma version"); return null; } try { // Using type assertion since API might not be recognized const slideRow = (figma as any).createSlideRow(); // Set slide row properties if (data.name) slideRow.name = data.name; // Apply common properties applyCommonProperties(slideRow, data); return slideRow; } catch (error) { console.error("Failed to create slide row:", error); return null; } } ``` -------------------------------------------------------------------------------- /src/widget/utils/widget-tools.ts: -------------------------------------------------------------------------------- ```typescript /** * Widget Tools - Utility functions for Figma widget development */ // Theme constants export const COLORS = { primary: "#0D99FF", primaryHover: "#0870B8", secondary: "#F0F0F0", secondaryHover: "#E0E0E0", text: "#333333", lightText: "#666666", background: "#FFFFFF", border: "#E6E6E6", success: "#36B37E", error: "#FF5630", warning: "#FFAB00", }; // Widget sizing helpers export const SPACING = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }; // Common shadow effects export const EFFECTS = { dropShadow: { type: "drop-shadow" as const, color: { r: 0, g: 0, b: 0, a: 0.1 }, offset: { x: 0, y: 2 }, blur: 4, }, strongShadow: { type: "drop-shadow" as const, color: { r: 0, g: 0, b: 0, a: 0.2 }, offset: { x: 0, y: 4 }, blur: 8, } }; // Formatting helpers export const formatDate = (date: Date): string => { return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; export const truncateText = (text: string, maxLength: number = 100): string => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }; // Figma widget helper functions export const createNodeId = (): string => { return 'id_' + Math.random().toString(36).substring(2, 11); }; // UI Component generators export type ButtonVariant = 'primary' | 'secondary' | 'danger'; export const buttonStyles = (variant: ButtonVariant = 'primary') => { switch (variant) { case 'primary': return { fill: COLORS.primary, hoverFill: COLORS.primaryHover, textColor: '#FFFFFF', }; case 'secondary': return { fill: COLORS.secondary, hoverFill: COLORS.secondaryHover, textColor: COLORS.text, }; case 'danger': return { fill: COLORS.error, hoverFill: '#E64C3D', textColor: '#FFFFFF', }; default: return { fill: COLORS.primary, hoverFill: COLORS.primaryHover, textColor: '#FFFFFF', }; } }; // Network request utilities for widgets export const fetchWithTimeout = async ( url: string, options: RequestInit = {}, timeout: number = 10000 ): Promise<Response> => { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(id); return response; } catch (error) { clearTimeout(id); throw error; } }; // Storage helpers export const saveToLocalStorage = (key: string, data: any): void => { try { localStorage.setItem(key, JSON.stringify(data)); } catch (error) { console.error('Error saving to localStorage:', error); } }; export const getFromLocalStorage = <T>(key: string, defaultValue: T): T => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error('Error reading from localStorage:', error); return defaultValue; } }; // Widget data utilities export interface WidgetData { id: string; name: string; createdAt: string; updatedAt: string; data: Record<string, any>; } export const createWidgetData = (name: string, data: Record<string, any> = {}): WidgetData => { const now = new Date().toISOString(); return { id: createNodeId(), name, createdAt: now, updatedAt: now, data }; }; export const updateWidgetData = (widgetData: WidgetData, newData: Partial<Record<string, any>>): WidgetData => { return { ...widgetData, updatedAt: new Date().toISOString(), data: { ...widgetData.data, ...newData } }; }; ``` -------------------------------------------------------------------------------- /src/tools/widget/search-widgets.ts: -------------------------------------------------------------------------------- ```typescript /** * Tool: search_widgets * * Searches for widgets that have specific sync data properties and values */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; import { FigmaUtils } from "../../utils/figma-utils.js"; export const searchWidgetsTool = (server: McpServer) => { server.tool( "search_widgets", { file_key: z.string().min(1).describe("The Figma file key"), property_key: z.string().min(1).describe("The sync data property key to search for"), property_value: z.string().optional().describe("Optional property value to match (if not provided, returns all widgets with the property)") }, async ({ file_key, property_key, property_value }) => { try { const file = await figmaApi.getFile(file_key); // Find all widget nodes const allWidgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); // Filter widgets that have the specified property const matchingWidgets = allWidgetNodes.filter(node => { if (!node.widgetSync) return false; try { const syncData = JSON.parse(node.widgetSync); // If property_value is provided, check for exact match if (property_value !== undefined) { // Handle different types of values (string, number, boolean) const propValue = syncData[property_key]; if (typeof propValue === 'string') { return propValue === property_value; } else if (typeof propValue === 'number') { return propValue.toString() === property_value; } else if (typeof propValue === 'boolean') { return propValue.toString() === property_value; } else if (propValue !== null && typeof propValue === 'object') { return JSON.stringify(propValue) === property_value; } return false; } // If no value provided, just check if the property exists return property_key in syncData; } catch (error) { return false; } }); if (matchingWidgets.length === 0) { return { content: [ { type: "text", text: property_value ? `No widgets found with property "${property_key}" = "${property_value}"` : `No widgets found with property "${property_key}"` } ] }; } const widgetsList = matchingWidgets.map((node, index) => { let syncDataValue = ''; try { const syncData = JSON.parse(node.widgetSync!); const value = syncData[property_key]; syncDataValue = typeof value === 'object' ? JSON.stringify(value) : String(value); } catch (error) { syncDataValue = 'Error parsing sync data'; } return `${index + 1}. **${node.name}** (ID: ${node.id}) - Property "${property_key}": ${syncDataValue}`; }).join('\n\n'); return { content: [ { type: "text", text: property_value ? `# Widgets with property "${property_key}" = "${property_value}"` : `# Widgets with property "${property_key}"` }, { type: "text", text: `Found ${matchingWidgets.length} matching widgets:` }, { type: "text", text: widgetsList } ] }; } catch (error) { console.error('Error searching widgets:', error); return { content: [ { type: "text", text: `Error searching widgets: ${(error as Error).message}` } ] }; } } ); }; ``` -------------------------------------------------------------------------------- /docs/02-implementation-steps.md: -------------------------------------------------------------------------------- ```markdown # Implementation Steps This document outlines the process followed to implement the Figma MCP server, from project setup to final testing. ## 1. Project Setup ### Initial Directory Structure The project was organized with the following structure: - `/src` for TypeScript source files - `/config` for configuration files - `/services` for API integrations - `/utils` for utility functions - `/types` for type definitions ### Dependencies Installation The project uses Bun as its package manager and runtime. Key dependencies include: - `@modelcontextprotocol/sdk` for MCP implementation - `@figma/rest-api-spec` for Figma API type definitions - `axios` for HTTP requests - `zod` for schema validation - `dotenv` for environment variable management ## 2. Configuration Setup ### Environment Variables Created a configuration system using Zod to validate environment variables: - `FIGMA_PERSONAL_ACCESS_TOKEN`: For Figma API authentication - `PORT`: Server port (default: 3001) - `NODE_ENV`: Environment (development/production) ## 3. Figma API Integration ### API Service Implementation Created a comprehensive service for interacting with the Figma API: - Used official Figma REST API specification types - Implemented methods for all required API endpoints - Added request/response handling with proper error management - Organized methods by resource type (files, nodes, comments, etc.) ### Utility Functions Implemented utility functions for common operations: - Finding nodes by ID - Getting nodes by type - Extracting text from nodes - Formatting node information - Calculating node paths in document hierarchy ## 4. MCP Server Implementation ### Server Setup Set up the MCP server using the MCP SDK: - Configured server metadata (name, version) - Connected to standard I/O for communication - Set up error handling and logging ### Tools Implementation Created tools for various Figma operations: - `get_file`: Retrieve file information - `get_node`: Access specific nodes - `get_comments`: Read file comments - `get_images`: Export node images - `get_file_versions`: Access version history - `search_text`: Search for text in files - `get_components`: Get file components - `add_comment`: Add comments to files Each tool includes: - Parameter validation using Zod - Error handling and response formatting - Proper response formatting for AI consumption ### Resource Templates Implemented resource templates for consistent access patterns: - `figma-file://{file_key}`: Access to Figma files - `figma-node://{file_key}/{node_id}`: Access to specific nodes ## 5. Build System ### Build Configuration Set up a build system with Bun: - Configured TypeScript compilation - Set up build scripts for development and production - Created a Makefile for common operations ### Scripts Implemented various scripts: - `start`: Run the server - `dev`: Development mode with auto-reload - `mcp`: MCP server with auto-reload - `build`: Build the project - `build:mcp`: Build the MCP server - `test`: Run tests - `clean`: Clean build artifacts ## 6. Documentation ### README Created a comprehensive README with: - Project description - Installation instructions - Usage examples - Available tools and resources - Development guidelines ### Code Documentation Added documentation throughout the codebase: - Function and method descriptions - Parameter documentation - Type definitions - Usage examples ## 7. Testing and Verification ### Build Verification Verified the build process: - Confirmed successful compilation - Checked for TypeScript errors - Ensured all dependencies were properly resolved ### File Structure Verification Confirmed the final directory structure: - All required files in place - Proper organization of code - Correct file permissions ## Next Steps - **Integration Testing**: Test with real AI assistants - **Performance Optimization**: Optimize for response time - **Caching**: Add caching for frequent requests - **Extended Capabilities**: Add more tools and resources - **User Documentation**: Create end-user documentation ``` -------------------------------------------------------------------------------- /docs/05-project-status.md: -------------------------------------------------------------------------------- ```markdown # Project Status and Roadmap This document tracks the current status of the Figma MCP server project and outlines future development plans. ## 1. Current Status ### Completed Tasks ✅ **Project Setup** - Created project structure - Installed dependencies - Configured TypeScript environment - Set up build system ✅ **Core Components** - Environment configuration - Figma API service - Utility functions - MCP server implementation ✅ **MCP Tools** - get_file: Retrieve Figma files - get_node: Access specific nodes - get_comments: Read file comments - get_images: Export node images - get_file_versions: Access version history - search_text: Search for text in files - get_components: Get file components - add_comment: Add comments to files ✅ **Resource Templates** - figma-file: Access to Figma files - figma-node: Access to specific nodes ✅ **Documentation** - Project overview - Implementation steps - Components and features - Usage guide - Project status and roadmap ### Current Limitations - No authentication refresh mechanism - Limited error reporting detail - No caching mechanism for frequent requests - Limited support for advanced Figma features - No pagination support for large result sets - Limited testing ## 2. Next Steps ### Short-Term Goals (Next 2-4 Weeks) - [ ] **Comprehensive Testing** - Unit tests for all components - Integration tests with Figma API - Performance testing - [ ] **Error Handling Improvements** - More detailed error messages - Better error categorization - Recovery mechanisms - [ ] **Caching System** - Implement response caching - Configure TTL for different resource types - Cache invalidation mechanisms - [ ] **Authentication Enhancements** - Token refresh mechanism - Better error handling for authentication issues - Support for OAuth authentication ### Medium-Term Goals (Next 2-3 Months) - [ ] **Additional Tools** - Team and project management - Style operations - Branch management - Widget interactions - Variable access and manipulation - [ ] **Enhanced Resource Templates** - More granular resource access - Improved filtering and searching - Resource relationships - [ ] **Performance Optimizations** - Parallel request processing - Response size optimization - Processing time improvements - [ ] **Security Enhancements** - Request validation - Rate limiting - Access control for sensitive operations ### Long-Term Goals (3+ Months) - [ ] **Advanced Feature Support** - FigJam-specific features - Prototyping capabilities - Dev mode integration - Widget creation and management - [ ] **Real-Time Updates** - Webhook integration for file changes - Live updates for collaborative editing - [ ] **Extended Integration** - Integration with other design tools - Version control system integration - CI/CD pipeline integration - [ ] **Advanced AI Features** - Design analysis capabilities - Automated design suggestions - Design consistency checking ## 3. Version History ### v1.0.0 (April 13, 2025) - Initial release - Core tools and resources - Basic documentation ## 4. Known Issues - Large files may cause performance issues - Certain complex node types may not be fully supported - Error handling in nested operations needs improvement - Some API rate limits may be encountered with frequent use ## 5. Contribution Guidelines ### Priority Areas for Contribution 1. **Testing**: Unit and integration tests 2. **Documentation**: Usage examples and API docs 3. **Feature Expansion**: Additional tools and resources 4. **Performance**: Optimizations for large files and complex operations 5. **Error Handling**: Improved error reporting and recovery ### Contribution Process 1. Select an issue or feature from the project board 2. Create a branch with a descriptive name 3. Implement the change with appropriate tests 4. Submit a pull request with a clear description 5. Address review feedback 6. Merge upon approval ## 6. Support and Feedback For support or to provide feedback, please: - Open an issue in the GitHub repository - Contact the project maintainers - Join the project discussion forum --- Last updated: April 13, 2025 ``` -------------------------------------------------------------------------------- /src/services/widget-api.ts: -------------------------------------------------------------------------------- ```typescript /** * Service for interacting with Figma Widget API */ import axios from 'axios'; import { env } from '../config/env.js'; import { z } from 'zod'; const FIGMA_API_BASE_URL = 'https://api.figma.com/v1'; // Widget data schemas export const WidgetNodeSchema = z.object({ id: z.string(), name: z.string(), type: z.literal('WIDGET'), widgetId: z.string().optional(), widgetSync: z.string().optional(), pluginData: z.record(z.unknown()).optional(), sharedPluginData: z.record(z.record(z.unknown())).optional(), }); export type WidgetNode = z.infer<typeof WidgetNodeSchema>; export const WidgetSyncDataSchema = z.record(z.unknown()); export type WidgetSyncData = z.infer<typeof WidgetSyncDataSchema>; /** * Service for interacting with Figma Widget API */ export class WidgetApiService { private readonly headers: Record<string, string>; constructor(accessToken: string = env.FIGMA_PERSONAL_ACCESS_TOKEN) { this.headers = { 'X-Figma-Token': accessToken, }; } /** * Get all widget nodes in a file */ async getWidgetNodes(fileKey: string): Promise<WidgetNode[]> { try { const response = await axios.get(, { headers: this.headers, }); const file = response.data; return this.findAllWidgetNodes(file.document); } catch (error) { console.error('Error fetching widget nodes:', error); throw error; } } /** * Get a specific widget node by ID */ async getWidgetNode(fileKey: string, nodeId: string): Promise<WidgetNode | null> { try { const response = await axios.get(, { headers: this.headers, }); const node = response.data.nodes[nodeId]?.document; if (!node || node.type !== 'WIDGET') { return null; } return WidgetNodeSchema.parse(node); } catch (error) { console.error('Error fetching widget node:', error); throw error; } } /** * Get the widget sync data (state) for a specific widget */ async getWidgetSyncData(fileKey: string, nodeId: string): Promise<WidgetSyncData | null> { try { const widgetNode = await this.getWidgetNode(fileKey, nodeId); if (!widgetNode || !widgetNode.widgetSync) { return null; } // Parse the widgetSync data string (it's stored as a JSON string) try { return JSON.parse(widgetNode.widgetSync); } catch (parseError) { console.error('Error parsing widget sync data:', parseError); return null; } } catch (error) { console.error('Error fetching widget sync data:', error); throw error; } } /** * Create a widget instance in a file (requires special access) * Note: This is only available to Figma widget developers or partners. */ async createWidget(fileKey: string, options: { name: string, widgetId: string, x: number, y: number, initialSyncData?: Record<string, any>, parentNodeId?: string, }): Promise<{ widgetNodeId: string } | null> { try { // This endpoint might not be publicly available const response = await axios.post( , options, { headers: this.headers } ); return response.data; } catch (error) { console.error('Error creating widget:', error); throw error; } } /** * Update a widget's properties (requires widget developer access) * Note: This functionality is limited to Figma widget developers. */ async updateWidgetProperties(fileKey: string, nodeId: string, properties: Record<string, any>): Promise<boolean> { try { // This endpoint might not be publicly available await axios.patch( , { properties }, { headers: this.headers } ); return true; } catch (error) { console.error('Error updating widget properties:', error); throw error; } } /** * Helper method to recursively find all widget nodes in a file */ private findAllWidgetNodes(node: any): WidgetNode[] { let widgets: WidgetNode[] = []; if (node.type === 'WIDGET') { try { widgets.push(WidgetNodeSchema.parse(node)); } catch (error) { console.error('Error parsing widget node:', error); } } if (node.children) { for (const child of node.children) { widgets = widgets.concat(this.findAllWidgetNodes(child)); } } return widgets; } } export default new WidgetApiService(); ``` -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- ```typescript /** * Resources for the Figma MCP server */ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import figmaApi from "./services/figma-api.js"; /** * Resource template for Figma files */ export const figmaFileResource = (server: McpServer): void => { server.resource( "figma-file", new ResourceTemplate("figma-file://{file_key}", { // Define listCallback instead of just providing a string for 'list' listCallback: async () => { try { // Here we would typically get a list of files // For now, return an empty list since we don't have access to "all files" return { contents: [{ uri: "figma-file://", title: "Figma Files", description: "List of Figma files you have access to", text: "# Figma Files\n\nTo access a specific file, you need to provide its file key." }] }; } catch (error) { console.error('Error listing files:', error); return { contents: [{ uri: "figma-file://", title: "Error listing files", text: `Error: ${(error as Error).message}` }] }; } } }), async (uri, { file_key }) => { try { const file = await figmaApi.getFile(file_key); return { contents: [{ uri: uri.href, title: file.name, description: `Last modified: ${file.lastModified}`, text: `# ${file.name}\n\nLast modified: ${file.lastModified}\n\nDocument contains ${file.document.children?.length || 0} top-level nodes.\nComponents: ${Object.keys(file.components).length}\nStyles: ${Object.keys(file.styles).length}` }] }; } catch (error) { console.error('Error fetching file for resource:', error); return { contents: [{ uri: uri.href, title: `File not found: ${file_key}`, text: `Error: ${(error as Error).message}` }] }; } } ); }; /** * Resource template for Figma nodes */ export const figmaNodeResource = (server: McpServer): void => { server.resource( "figma-node", new ResourceTemplate("figma-node://{file_key}/{node_id}", { // Define listCallback instead of just providing a string for 'list' listCallback: async (uri, { file_key }) => { try { // If only file_key is provided, list all top-level nodes const file = await figmaApi.getFile(file_key); return { contents: file.document.children?.map(node => ({ uri: `figma-node://${file_key}/${node.id}`, title: node.name, description: `Type: ${node.type}`, text: `# ${node.name}\n\nType: ${node.type}\nID: ${node.id}` })) || [] }; } catch (error) { console.error('Error listing nodes:', error); return { contents: [{ uri: `figma-node://${file_key}`, title: "Error listing nodes", text: `Error: ${(error as Error).message}` }] }; } } }), async (uri, { file_key, node_id }) => { try { // Get specific node const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData) { return { contents: [{ uri: uri.href, title: `Node not found: ${node_id}`, text: `Node ${node_id} not found in file ${file_key}` }] }; } return { contents: [{ uri: uri.href, title: nodeData.document.name, description: `Type: ${nodeData.document.type}`, text: `# ${nodeData.document.name}\n\nType: ${nodeData.document.type}\nID: ${nodeData.document.id}\nChildren: ${nodeData.document.children?.length || 0}` }] }; } catch (error) { console.error('Error fetching node for resource:', error); return { contents: [{ uri: uri.href, title: `Error`, text: `Error: ${(error as Error).message}` }] }; } } ); }; /** * Registers all resources with the MCP server */ export function registerAllResources(server: McpServer): void { figmaFileResource(server); figmaNodeResource(server); } ``` -------------------------------------------------------------------------------- /src/plugin/creators/imageCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Image and media element creation functions for Figma plugin */ import { applyCommonProperties } from '../utils/nodeUtils'; /** * Create an image node from data * @param data Image configuration data * @returns Created image node */ export function createImageFromData(data: any): SceneNode | null { try { // Image creation requires a hash if (!data.hash) { console.error('Image creation requires an image hash'); return null; } const image = figma.createImage(data.hash); // Create a rectangle to display the image const rect = figma.createRectangle(); // Set size if (data.width && data.height) { rect.resize(data.width, data.height); } // Apply image as fill rect.fills = [{ type: 'IMAGE', scaleMode: data.scaleMode || 'FILL', imageHash: image.hash }]; // Apply common properties applyCommonProperties(rect, data); return rect; } catch (error) { console.error('Failed to create image:', error); return null; } } /** * Create an image node asynchronously from data * @param data Image configuration data with bytes or file * @returns Promise resolving to created image node */ export async function createImageFromBytesAsync(data: any): Promise<SceneNode | null> { try { // Image creation requires bytes or a file if (!data.bytes && !data.file) { console.error('Image creation requires image bytes or file'); return null; } let image; if (data.bytes) { image = await figma.createImageAsync(data.bytes); } else if (data.file) { // Note: file would need to be provided through some UI interaction // as plugins cannot directly access the file system image = await figma.createImageAsync(data.file); } else { return null; } // Create a rectangle to display the image const rect = figma.createRectangle(); // Set size if (data.width && data.height) { rect.resize(data.width, data.height); } // Apply image as fill rect.fills = [{ type: 'IMAGE', scaleMode: data.scaleMode || 'FILL', imageHash: image.hash }]; // Apply common properties applyCommonProperties(rect, data); return rect; } catch (error) { console.error('Failed to create image asynchronously:', error); return null; } } /** * Create a GIF node from data * @param data GIF configuration data * @returns Created gif node */ export function createGifFromData(data: any): SceneNode | null { // As of my knowledge, there isn't a direct createGif API // Even though it's in the list of methods // For now, return null and log an error console.error('createGif API is not directly available or implemented'); return null; } /** * Create a video node asynchronously from data * This depends on figma.createVideoAsync which may not be available in all versions * * @param data Video configuration data * @returns Promise resolving to created video node */ export async function createVideoFromDataAsync(data: any): Promise<SceneNode | null> { // Check if video creation is supported if (!('createVideoAsync' in figma)) { console.error('Video creation is not supported in this Figma version'); return null; } try { // Video creation requires bytes if (!data.bytes) { console.error('Video creation requires video bytes'); return null; } // Using type assertion since createVideoAsync may not be recognized by TypeScript const video = await (figma as any).createVideoAsync(data.bytes); // Apply common properties applyCommonProperties(video, data); return video; } catch (error) { console.error('Failed to create video:', error); return null; } } /** * Create a link preview node asynchronously from data * This depends on figma.createLinkPreviewAsync which may not be available in all versions * * @param data Link preview configuration data * @returns Promise resolving to created link preview node */ export async function createLinkPreviewFromDataAsync(data: any): Promise<SceneNode | null> { // Check if link preview creation is supported if (!('createLinkPreviewAsync' in figma)) { console.error('Link preview creation is not supported in this Figma version'); return null; } try { // Link preview creation requires a URL if (!data.url) { console.error('Link preview creation requires a URL'); return null; } // Using type assertion since createLinkPreviewAsync may not be recognized by TypeScript const linkPreview = await (figma as any).createLinkPreviewAsync(data.url); // Apply common properties applyCommonProperties(linkPreview, data); return linkPreview; } catch (error) { console.error('Failed to create link preview:', error); return null; } } ``` -------------------------------------------------------------------------------- /docs/03-components-and-features.md: -------------------------------------------------------------------------------- ```markdown # Components and Features This document provides detailed information about the key components and features of the Figma MCP server. ## 1. Core Components ### Environment Configuration (`src/config/env.ts`) The environment configuration component: - Loads variables from `.env` file using dotenv - Validates environment variables using Zod schema - Provides type-safe access to configuration values - Ensures required variables are present - Sets sensible defaults for optional variables ```typescript // Example of environment validation const envSchema = z.object({ FIGMA_PERSONAL_ACCESS_TOKEN: z.string().min(1), PORT: z.string().default("3001").transform(Number), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), }); ``` ### Figma API Service (`src/services/figma-api.ts`) A comprehensive service for interacting with the Figma API: - Uses official Figma API TypeScript definitions - Provides methods for all relevant API endpoints - Handles authentication and request formatting - Processes responses and errors consistently - Supports all Figma resource types: - Files and nodes - Comments - Images - Components and styles - Versions - Teams and projects ```typescript // Example method for retrieving a Figma file async getFile(fileKey: string, params: { ids?: string; depth?: number; geometry?: string } = {}): Promise<GetFileResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}`, { headers: this.headers, params, }); return response.data; } ``` ### Figma Utilities (`src/utils/figma-utils.ts`) Utility functions for working with Figma data: - Node search and traversal - Text extraction - Property formatting - Path calculation - Type-specific operations ```typescript // Example utility for finding a node by ID static findNodeById(file: GetFileResponse, nodeId: string): Node | null { // Implementation details... } ``` ### MCP Server Implementation (`src/index.ts`) The main MCP server implementation: - Configures the MCP server - Defines tools and resources - Handles communication via standard I/O - Manages error handling and response formatting ```typescript // Example of MCP server configuration const server = new McpServer({ name: "Figma API", version: "1.0.0", }); ``` ## 2. MCP Tools The server provides the following tools: ### `get_file` Retrieves a Figma file by key: - Parameters: - `file_key`: The Figma file key - `return_full_file`: Whether to return the full file structure - Returns: - File name, modification date - Document structure summary - Component and style counts - Full file contents (if requested) ### `get_node` Retrieves a specific node from a Figma file: - Parameters: - `file_key`: The Figma file key - `node_id`: The ID of the node to retrieve - Returns: - Node name, type, and ID - Node properties and attributes - Child node count ### `get_comments` Retrieves comments from a Figma file: - Parameters: - `file_key`: The Figma file key - Returns: - Comment count - Comment text - Author information - Timestamps ### `get_images` Exports nodes as images: - Parameters: - `file_key`: The Figma file key - `node_ids`: Array of node IDs to export - `format`: Image format (jpg, png, svg, pdf) - `scale`: Scale factor for the image - Returns: - Image URLs for each node - Error information for failed exports ### `get_file_versions` Retrieves version history for a file: - Parameters: - `file_key`: The Figma file key - Returns: - Version list - Version labels and descriptions - Author information - Timestamps ### `search_text` Searches for text within a Figma file: - Parameters: - `file_key`: The Figma file key - `search_text`: The text to search for - Returns: - Matching text nodes - Node paths in document hierarchy - Matching text content ### `get_components` Retrieves components from a Figma file: - Parameters: - `file_key`: The Figma file key - Returns: - Component list - Component names and keys - Component descriptions - Remote status ### `add_comment` Adds a comment to a Figma file: - Parameters: - `file_key`: The Figma file key - `message`: The comment text - `node_id`: Optional node ID to attach the comment to - Returns: - Comment ID - Author information - Timestamp ## 3. Resource Templates The server provides the following resource templates: ### `figma-file://{file_key}` Provides access to Figma files: - URI format: `figma-file://{file_key}` - List URI: `figma-file://` - Returns: - File name - Last modified date - Document structure summary ### `figma-node://{file_key}/{node_id}` Provides access to nodes within Figma files: - URI format: `figma-node://{file_key}/{node_id}` - List URI: `figma-node://{file_key}` - Returns: - Node name and type - Node properties - Child node count ## 4. Error Handling The server implements comprehensive error handling: - API request errors - Authentication failures - Invalid parameters - Resource not found errors - Server errors Each error is properly formatted and returned to the client with: - Error message - Error type - Context information (when available) ## 5. Response Formatting Responses are formatted for optimal consumption by AI assistants: - Clear headings - Structured information - Formatted lists - Contextual descriptions - Links and references where appropriate ``` -------------------------------------------------------------------------------- /docs/04-usage-guide.md: -------------------------------------------------------------------------------- ```markdown # Usage Guide This document provides detailed instructions for setting up, running, and using the Figma MCP server. ## 1. Setup Instructions ### Prerequisites Before you begin, ensure you have the following: - [Bun](https://bun.sh/) v1.0.0 or higher installed - A Figma account with API access - A personal access token from Figma ### Installation 1. Clone the repository: ```bash git clone <repository-url> cd figma-mcp-server ``` 2. Install dependencies: ```bash make install ``` or ```bash bun install ``` 3. Configure environment variables: - Copy the example environment file: ```bash cp .env.example .env ``` - Edit `.env` and add your Figma personal access token: ``` FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token_here PORT=3001 NODE_ENV=development ``` ## 2. Running the Server ### Development Mode Run the server in development mode with auto-reload: ```bash make mcp ``` or ```bash bun run mcp ``` ### Production Mode 1. Build the server: ```bash make build-mcp ``` or ```bash bun run build:mcp ``` 2. Run the built server: ```bash make start ``` or ```bash bun run start ``` ## 3. Using the MCP Tools To use the MCP tools, you'll need an MCP client that can communicate with the server. This could be an AI assistant or another application that implements the MCP protocol. ### Example: Retrieving a Figma File Using the `get_file` tool: ```json { "tool": "get_file", "parameters": { "file_key": "abc123xyz789", "return_full_file": false } } ``` Expected response: ```json { "content": [ { "type": "text", "text": "# Figma File: My Design" }, { "type": "text", "text": "Last modified: 2025-04-10T15:30:45Z" }, { "type": "text", "text": "Document contains 5 top-level nodes." }, { "type": "text", "text": "Components: 12" }, { "type": "text", "text": "Component sets: 3" }, { "type": "text", "text": "Styles: 8" } ] } ``` ### Example: Searching for Text Using the `search_text` tool: ```json { "tool": "search_text", "parameters": { "file_key": "abc123xyz789", "search_text": "Welcome" } } ``` Expected response: ```json { "content": [ { "type": "text", "text": "# Text Search Results for \"Welcome\"" }, { "type": "text", "text": "Found 2 matching text nodes:" }, { "type": "text", "text": "- **Header Text** (ID: 123:456)\n Path: Page 1 > Header > Text\n Text: \"Welcome to our application\"" } ] } ``` ### Example: Adding a Comment Using the `add_comment` tool: ```json { "tool": "add_comment", "parameters": { "file_key": "abc123xyz789", "message": "This design looks great! Consider adjusting the contrast on the buttons.", "node_id": "123:456" } } ``` Expected response: ```json { "content": [ { "type": "text", "text": "Comment added successfully!" }, { "type": "text", "text": "Comment ID: 987654" }, { "type": "text", "text": "By user: John Doe" }, { "type": "text", "text": "Added at: 4/13/2025, 12:34:56 PM" } ] } ``` ## 4. Using Resource Templates Resource templates provide a consistent way to access Figma resources. ### Example: Accessing a File Resource URI: `figma-file://abc123xyz789` Expected response: ```json { "contents": [{ "uri": "figma-file://abc123xyz789", "title": "My Design", "description": "Last modified: 2025-04-10T15:30:45Z", "text": "# My Design\n\nLast modified: 2025-04-10T15:30:45Z\n\nDocument contains 5 top-level nodes.\nComponents: 12\nStyles: 8" }] } ``` ### Example: Listing Nodes in a File Resource URI: `figma-node://abc123xyz789` Expected response: ```json { "contents": [ { "uri": "figma-node://abc123xyz789/1:1", "title": "Page 1", "description": "Type: CANVAS", "text": "# Page 1\n\nType: CANVAS\nID: 1:1" }, { "uri": "figma-node://abc123xyz789/1:2", "title": "Page 2", "description": "Type: CANVAS", "text": "# Page 2\n\nType: CANVAS\nID: 1:2" } ] } ``` ### Example: Accessing a Specific Node Resource URI: `figma-node://abc123xyz789/123:456` Expected response: ```json { "contents": [{ "uri": "figma-node://abc123xyz789/123:456", "title": "Header Text", "description": "Type: TEXT", "text": "# Header Text\n\nType: TEXT\nID: 123:456\nChildren: 0" }] } ``` ## 5. Error Handling Examples ### Example: File Not Found ```json { "content": [ { "type": "text", "text": "Error getting Figma file: File not found" } ] } ``` ### Example: Node Not Found ```json { "content": [ { "type": "text", "text": "Node 123:456 not found in file abc123xyz789" } ] } ``` ### Example: Authentication Error ```json { "content": [ { "type": "text", "text": "Error getting Figma file: Authentication failed. Please check your personal access token." } ] } ``` ## 6. Tips and Best Practices 1. **File Keys**: Obtain file keys from Figma file URLs. The format is typically `https://www.figma.com/file/FILE_KEY/FILE_NAME`. 2. **Node IDs**: Node IDs can be found in Figma by right-clicking a layer and selecting "Copy/Paste as > Copy link". The node ID is the part after `?node-id=` in the URL. 3. **Performance**: For large files, use targeted queries with specific node IDs rather than retrieving the entire file. 4. **Image Export**: When exporting images, use appropriate scale factors: 1 for normal resolution, 2 for @2x, etc. 5. **Comments**: When adding comments, provide node IDs to attach comments to specific elements. 6. **Error Handling**: Always handle potential errors in your client application. 7. **Resource Caching**: Consider caching resource responses for improved performance in your client application. ``` -------------------------------------------------------------------------------- /src/plugin/creators/containerCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Container element creation functions for Figma plugin * Including Frame, Component, and other container-like nodes */ import { createSolidPaint } from '../utils/colorUtils'; import { applyCommonProperties } from '../utils/nodeUtils'; /** * Create a frame from data * @param data Frame configuration data * @returns Created frame node */ export function createFrameFromData(data: any): FrameNode { const frame = figma.createFrame(); // Size frame.resize(data.width || 100, data.height || 100); // Background if (data.fills) { frame.fills = data.fills; } else if (data.fill) { if (typeof data.fill === 'string') { frame.fills = [createSolidPaint(data.fill)]; } else { frame.fills = [data.fill]; } } // Auto layout properties if (data.layoutMode) frame.layoutMode = data.layoutMode; if (data.primaryAxisSizingMode) frame.primaryAxisSizingMode = data.primaryAxisSizingMode; if (data.counterAxisSizingMode) frame.counterAxisSizingMode = data.counterAxisSizingMode; if (data.primaryAxisAlignItems) frame.primaryAxisAlignItems = data.primaryAxisAlignItems; if (data.counterAxisAlignItems) frame.counterAxisAlignItems = data.counterAxisAlignItems; if (data.paddingLeft !== undefined) frame.paddingLeft = data.paddingLeft; if (data.paddingRight !== undefined) frame.paddingRight = data.paddingRight; if (data.paddingTop !== undefined) frame.paddingTop = data.paddingTop; if (data.paddingBottom !== undefined) frame.paddingBottom = data.paddingBottom; if (data.itemSpacing !== undefined) frame.itemSpacing = data.itemSpacing; // Corner radius if (data.cornerRadius !== undefined) frame.cornerRadius = data.cornerRadius; if (data.topLeftRadius !== undefined) frame.topLeftRadius = data.topLeftRadius; if (data.topRightRadius !== undefined) frame.topRightRadius = data.topRightRadius; if (data.bottomLeftRadius !== undefined) frame.bottomLeftRadius = data.bottomLeftRadius; if (data.bottomRightRadius !== undefined) frame.bottomRightRadius = data.bottomRightRadius; return frame; } /** * Create a component from data * @param data Component configuration data * @returns Created component node */ export function createComponentFromData(data: any): ComponentNode { const component = figma.createComponent(); // Size component.resize(data.width || 100, data.height || 100); // Background if (data.fills) { component.fills = data.fills; } else if (data.fill) { if (typeof data.fill === 'string') { component.fills = [createSolidPaint(data.fill)]; } else { component.fills = [data.fill]; } } // Auto layout properties (components support same auto layout as frames) if (data.layoutMode) component.layoutMode = data.layoutMode; if (data.primaryAxisSizingMode) component.primaryAxisSizingMode = data.primaryAxisSizingMode; if (data.counterAxisSizingMode) component.counterAxisSizingMode = data.counterAxisSizingMode; if (data.primaryAxisAlignItems) component.primaryAxisAlignItems = data.primaryAxisAlignItems; if (data.counterAxisAlignItems) component.counterAxisAlignItems = data.counterAxisAlignItems; if (data.paddingLeft !== undefined) component.paddingLeft = data.paddingLeft; if (data.paddingRight !== undefined) component.paddingRight = data.paddingRight; if (data.paddingTop !== undefined) component.paddingTop = data.paddingTop; if (data.paddingBottom !== undefined) component.paddingBottom = data.paddingBottom; if (data.itemSpacing !== undefined) component.itemSpacing = data.itemSpacing; // Component properties if (data.description) component.description = data.description; return component; } /** * Create a group from data * Note: Groups require children, so this typically needs to be used after creating child nodes * * @param data Group configuration data * @param children Child nodes to include in the group * @returns Created group node */ export function createGroupFromData(data: any, children: SceneNode[]): GroupNode { // Create group with the provided children const group = figma.group(children, figma.currentPage); // Apply common properties applyCommonProperties(group, data); return group; } /** * Create an instance from data * @param data Instance configuration data (must include componentId) * @returns Created instance node */ export function createInstanceFromData(data: any): InstanceNode | null { if (!data.componentId) { console.error('Cannot create instance: componentId is required'); return null; } // Try to find the component const component = figma.getNodeById(data.componentId) as ComponentNode; if (!component || component.type !== 'COMPONENT') { console.error(`Cannot create instance: component with id ${data.componentId} not found`); return null; } // Create instance const instance = component.createInstance(); // Apply common properties applyCommonProperties(instance, data); // Handle instance-specific properties if (data.componentProperties) { for (const [key, value] of Object.entries(data.componentProperties)) { if (key in instance.componentProperties) { // Handle different types of component properties const prop = instance.componentProperties[key]; if (prop.type === 'BOOLEAN') { instance.setProperties({ [key]: !!value }); } else if (prop.type === 'TEXT') { instance.setProperties({ [key]: String(value) }); } else if (prop.type === 'INSTANCE_SWAP') { instance.setProperties({ [key]: String(value) }); } else if (prop.type === 'VARIANT') { instance.setProperties({ [key]: String(value) }); } } } } return instance; } /** * Create a section from data * Sections are a special type of node used to organize frames in Figma * * @param data Section configuration data * @returns Created section node */ export function createSectionFromData(data: any): SectionNode { const section = figma.createSection(); // Section-specific properties if (data.name) section.name = data.name; if (data.sectionContentsHidden !== undefined) section.sectionContentsHidden = data.sectionContentsHidden; // Apply common properties that apply to sections if (data.x !== undefined) section.x = data.x; if (data.y !== undefined) section.y = data.y; return section; } ``` -------------------------------------------------------------------------------- /src/plugin/creators/elementCreator.ts: -------------------------------------------------------------------------------- ```typescript /** * Universal element creator for Figma plugin * Acts as a central entry point for creating any type of Figma element */ import { createRectangleFromData, createEllipseFromData, createPolygonFromData, createStarFromData, createLineFromData, createVectorFromData } from './shapeCreators'; import { createFrameFromData, createComponentFromData, createInstanceFromData, createGroupFromData, createSectionFromData } from './containerCreators'; import { createTextFromData } from './textCreator'; import { createBooleanOperationFromData, createConnectorFromData, createShapeWithTextFromData, createCodeBlockFromData, createTableFromData, createWidgetFromData, createMediaFromData } from './specialCreators'; import { createImageFromData, createImageFromBytesAsync, createGifFromData, createVideoFromDataAsync, createLinkPreviewFromDataAsync } from './imageCreators'; import { createSliceFromData, createPageFromData, createPageDividerFromData, createSlideFromData, createSlideRowFromData } from './sliceCreators'; import { createComponentFromNodeData, createComponentSetFromData } from './componentCreators'; import { applyCommonProperties, selectAndFocusNodes } from '../utils/nodeUtils'; /** * Unified create element function that works with structured data * Detects the type of element to create based on the data.type property * * @param data Configuration data with type and other properties * @returns Created Figma node or null if creation failed */ export async function createElementFromData(data: any): Promise<SceneNode | null> { if (!data || !data.type) { console.error('Invalid element data: missing type'); return null; } let element: SceneNode | null = null; try { // Create the element based on its type switch (data.type.toLowerCase()) { // Basic shapes case 'rectangle': element = createRectangleFromData(data); break; case 'ellipse': case 'circle': element = createEllipseFromData(data); break; case 'polygon': element = createPolygonFromData(data); break; case 'star': element = createStarFromData(data); break; case 'line': element = createLineFromData(data); break; case 'vector': element = createVectorFromData(data); break; // Container elements case 'frame': element = createFrameFromData(data); break; case 'component': element = createComponentFromData(data); break; case 'componentfromnode': element = createComponentFromNodeData(data); break; case 'componentset': element = createComponentSetFromData(data); break; case 'instance': element = createInstanceFromData(data); break; case 'section': element = createSectionFromData(data); break; // Text case 'text': element = await createTextFromData(data); break; // Special types case 'boolean': case 'booleanoperation': element = createBooleanOperationFromData(data); break; case 'connector': element = createConnectorFromData(data); break; case 'shapewithtext': element = createShapeWithTextFromData(data); break; case 'codeblock': element = createCodeBlockFromData(data); break; case 'table': element = createTableFromData(data); break; case 'widget': element = createWidgetFromData(data); break; case 'media': element = createMediaFromData(data); break; // Image and media types case 'image': if (data.bytes || data.file) { element = await createImageFromBytesAsync(data); } else { element = createImageFromData(data); } break; case 'gif': element = createGifFromData(data); break; case 'video': element = await createVideoFromDataAsync(data); break; case 'linkpreview': element = await createLinkPreviewFromDataAsync(data); break; // Page and slice types case 'slice': element = createSliceFromData(data); break; case 'page': // PageNode is not a SceneNode in Figma's type system // So we create it but don't return it through the same path const page = createPageFromData(data); console.log(`Created page: ${page.name}`); // We return null as we can't return a PageNode as SceneNode return null; case 'pagedivider': element = createPageDividerFromData(data); break; case 'slide': element = createSlideFromData(data); break; case 'sliderow': element = createSlideRowFromData(data); break; // Special cases case 'group': if (!data.children || !Array.isArray(data.children) || data.children.length < 1) { console.error('Cannot create group: children array is required'); return null; } // Create all child elements first const childNodes: SceneNode[] = []; for (const childData of data.children) { const child = await createElementFromData(childData); if (child) childNodes.push(child); } if (childNodes.length > 0) { element = createGroupFromData(data, childNodes); } else { console.error('Cannot create group: no valid children were created'); return null; } break; default: console.error(`Unsupported element type: ${data.type}`); return null; } // Apply common properties if element was created if (element) { applyCommonProperties(element, data); // Select and focus on the element if requested if (data.select !== false) { selectAndFocusNodes(element); } } return element; } catch (error) { console.error(`Error creating element: ${error instanceof Error ? error.message : 'Unknown error'}`); return null; } } /** * Create multiple elements from an array of data objects * @param dataArray Array of element configuration data * @returns Array of created nodes */ export async function createElementsFromDataArray(dataArray: any[]): Promise<SceneNode[]> { const createdNodes: SceneNode[] = []; for (const data of dataArray) { const node = await createElementFromData(data); if (node) createdNodes.push(node); } // If there are created nodes, select them all at the end if (createdNodes.length > 0) { selectAndFocusNodes(createdNodes); } return createdNodes; } ``` -------------------------------------------------------------------------------- /src/tools/page.ts: -------------------------------------------------------------------------------- ```typescript /** * Page tools for the Figma MCP server */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { isPluginConnected, sendCommandToPlugin, } from "../services/websocket.js"; // Define interfaces for page data interface PageData { id: string; name: string; children?: any[]; [key: string]: any; } export const getPagesTool = (server: McpServer) => { server.tool("get_pages", {}, async () => { try { // Get pages using WebSocket only if plugin is connected if (!isPluginConnected()) { return { content: [ { type: "text", text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", }, ], }; } const response = await sendCommandToPlugin("get-pages", {}); if (!response.success) { throw new Error(response.error || "Failed to get pages"); } // Process the response to handle different result formats const result = response.result || {}; const pages = result.items || []; const pagesCount = result.count || 0; // Check if we have pages to display if (pagesCount === 0 && !pages.length) { return { content: [ { type: "text", text: `# Pages in Figma File` }, { type: "text", text: `No pages found in the current Figma file.` }, ], }; } return { content: [ { type: "text", text: `# Pages in Figma File` }, { type: "text", text: `Found ${pagesCount || pages.length} pages:` }, { type: "text", text: pages .map((page: PageData) => `- ${page.name} (ID: ${page.id})`) .join("\n"), }, ], }; } catch (error) { console.error("Error fetching pages:", error); return { content: [ { type: "text", text: `Error getting pages: ${(error as Error).message}`, }, ], }; } }); }; export const getPageTool = (server: McpServer) => { server.tool( "get_page", { page_id: z.string().min(1).describe("The ID of the page to retrieve").optional(), }, async ({ page_id }) => { try { // Get page using WebSocket only if plugin is connected if (!isPluginConnected()) { return { content: [ { type: "text", text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", }, ], }; } // If page_id is not provided, get the current page const response = await sendCommandToPlugin("get-page", { page_id, }); if (!response.success) { throw new Error(response.error || "Failed to get page"); } const pageNode = response.result; return { content: [ { type: "text", text: `# Page: ${pageNode.name}` }, { type: "text", text: `ID: ${pageNode.id}` }, { type: "text", text: `Type: ${pageNode.type}` }, { type: "text", text: `Elements: ${pageNode.children?.length || 0}`, }, { type: "text", text: "```json\n" + JSON.stringify(pageNode, null, 2) + "\n```", }, ], }; } catch (error) { console.error("Error fetching page:", error); return { content: [ { type: "text", text: `Error getting page: ${(error as Error).message}`, }, ], }; } } ); }; export const createPageTool = (server: McpServer) => { server.tool( "create_page", { page_name: z.string().min(1).describe("Name for the new page"), }, async ({ page_name }) => { try { if (!isPluginConnected()) { return { content: [ { type: "text", text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", }, ], }; } // Use WebSocket to send command to plugin const response = await sendCommandToPlugin("create-page", { name: page_name, }); if (!response.success) { throw new Error(response.error || "Failed to create page"); } return { content: [ { type: "text", text: `# Page Created Successfully`, }, { type: "text", text: `A new page named "${page_name}" has been created.`, }, { type: "text", text: response.result && response.result.id ? `Page ID: ${response.result.id}` : `Creation successful`, }, ], }; } catch (error) { console.error("Error creating page:", error); return { content: [ { type: "text", text: `Error creating page: ${(error as Error).message}`, }, ], }; } } ); }; export const switchPageTool = (server: McpServer) => { server.tool( "switch_page", { page_id: z.string().min(1).describe("The ID of the page to switch to"), }, async ({ page_id }) => { try { if (!isPluginConnected()) { return { content: [ { type: "text", text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", }, ], }; } // Use WebSocket to send command to plugin const response = await sendCommandToPlugin("switch-page", { id: page_id, // Note: plugin expects 'id', not 'page_id' }); if (!response.success) { throw new Error(response.error || "Failed to switch page"); } return { content: [ { type: "text", text: `# Page Switched Successfully`, }, { type: "text", text: `Successfully switched to page with ID: ${page_id}`, }, { type: "text", text: response.result && response.result.name ? `Current page: ${response.result.name}` : `Switch successful`, }, ], }; } catch (error) { console.error("Error switching page:", error); return { content: [ { type: "text", text: `Error switching page: ${(error as Error).message}`, }, ], }; } } ); }; /** * Registers all page-related tools with the MCP server */ export const registerPageTools = (server: McpServer): void => { getPagesTool(server); getPageTool(server); createPageTool(server); switchPageTool(server); }; ``` -------------------------------------------------------------------------------- /src/plugin/creators/textCreator.ts: -------------------------------------------------------------------------------- ```typescript /** * Text element creation functions for Figma plugin */ import { createSolidPaint } from '../utils/colorUtils'; import { selectAndFocusNodes } from '../utils/nodeUtils'; /** * Create a text node from data * @param data Text configuration data * @returns Created text node */ export async function createTextFromData(data: any): Promise<TextNode> { const text = figma.createText(); // Load font - default to Inter if not specified const fontFamily = data.fontFamily || (data.fontName.family) || "Inter"; const fontStyle = data.fontStyle || (data.fontName.style) || "Regular"; // Load the font before setting text try { await figma.loadFontAsync({ family: fontFamily, style: fontStyle }); } catch (error) { console.warn(`Failed to load font ${fontFamily} ${fontStyle}. Falling back to Inter Regular.`); await figma.loadFontAsync({ family: "Inter", style: "Regular" }); } // Set basic text content text.characters = data.text || data.characters || "Text"; // Position and size if (data.x !== undefined) text.x = data.x; if (data.y !== undefined) text.y = data.y; // Text size and dimensions if (data.fontSize) text.fontSize = data.fontSize; if (data.width) text.resize(data.width, text.height); // Text style and alignment if (data.fontName) text.fontName = data.fontName; if (data.textAlignHorizontal) text.textAlignHorizontal = data.textAlignHorizontal; if (data.textAlignVertical) text.textAlignVertical = data.textAlignVertical; if (data.textAutoResize) text.textAutoResize = data.textAutoResize; if (data.textTruncation) text.textTruncation = data.textTruncation; if (data.maxLines !== undefined) text.maxLines = data.maxLines; // Paragraph styling if (data.paragraphIndent) text.paragraphIndent = data.paragraphIndent; if (data.paragraphSpacing) text.paragraphSpacing = data.paragraphSpacing; if (data.listSpacing) text.listSpacing = data.listSpacing; if (data.hangingPunctuation !== undefined) text.hangingPunctuation = data.hangingPunctuation; if (data.hangingList !== undefined) text.hangingList = data.hangingList; if (data.autoRename !== undefined) text.autoRename = data.autoRename; // Text styling if (data.letterSpacing) text.letterSpacing = data.letterSpacing; if (data.lineHeight) text.lineHeight = data.lineHeight; if (data.leadingTrim) text.leadingTrim = data.leadingTrim; if (data.textCase) text.textCase = data.textCase; if (data.textDecoration) text.textDecoration = data.textDecoration; if (data.textStyleId) text.textStyleId = data.textStyleId; // Text decoration details if (data.textDecorationStyle) text.textDecorationStyle = data.textDecorationStyle; if (data.textDecorationOffset) text.textDecorationOffset = data.textDecorationOffset; if (data.textDecorationThickness) text.textDecorationThickness = data.textDecorationThickness; if (data.textDecorationColor) text.textDecorationColor = data.textDecorationColor; if (data.textDecorationSkipInk !== undefined) text.textDecorationSkipInk = data.textDecorationSkipInk; // Text fill if (data.fills) { text.fills = data.fills; } else if (data.fill) { if (typeof data.fill === 'string') { text.fills = [createSolidPaint(data.fill)]; } else { text.fills = [data.fill]; } } // Text hyperlink if (data.hyperlink) { text.hyperlink = data.hyperlink; } // Layout properties if (data.layoutAlign) text.layoutAlign = data.layoutAlign; if (data.layoutGrow !== undefined) text.layoutGrow = data.layoutGrow; if (data.layoutSizingHorizontal) text.layoutSizingHorizontal = data.layoutSizingHorizontal; if (data.layoutSizingVertical) text.layoutSizingVertical = data.layoutSizingVertical; // Apply text range styles if provided if (data.rangeStyles && Array.isArray(data.rangeStyles)) { applyTextRangeStyles(text, data.rangeStyles); } // Apply common base properties if (data.name) text.name = data.name; if (data.visible !== undefined) text.visible = data.visible; if (data.locked !== undefined) text.locked = data.locked; if (data.opacity !== undefined) text.opacity = data.opacity; if (data.blendMode) text.blendMode = data.blendMode; if (data.effects) text.effects = data.effects; if (data.effectStyleId) text.effectStyleId = data.effectStyleId; if (data.exportSettings) text.exportSettings = data.exportSettings; if (data.constraints) text.constraints = data.constraints; return text; } /** * Create a simple text node with basic properties * @param x X coordinate * @param y Y coordinate * @param content Text content * @param fontSize Font size * @returns Created text node */ export async function createText(x: number, y: number, content: string, fontSize: number): Promise<TextNode> { // Use the data-driven function const text = await createTextFromData({ text: content, fontSize, x, y }); // Select and focus selectAndFocusNodes(text); return text; } /** * Apply character-level styling to text ranges in a text node * @param textNode Text node to style * @param ranges Array of range objects with start, end, and style properties */ export function applyTextRangeStyles(textNode: TextNode, ranges: Array<{start: number, end: number, style: any}>): void { for (const range of ranges) { // Apply individual style properties to the range for (const [property, value] of Object.entries(range.style)) { if (property === 'fills') { textNode.setRangeFills(range.start, range.end, value as Paint[]); } else if (property === 'fillStyleId') { textNode.setRangeFillStyleId(range.start, range.end, value as string); } else if (property === 'fontName') { textNode.setRangeFontName(range.start, range.end, value as FontName); } else if (property === 'fontSize') { textNode.setRangeFontSize(range.start, range.end, value as number); } else if (property === 'textCase') { textNode.setRangeTextCase(range.start, range.end, value as TextCase); } else if (property === 'textDecoration') { textNode.setRangeTextDecoration(range.start, range.end, value as TextDecoration); } else if (property === 'textDecorationStyle') { textNode.setRangeTextDecorationStyle(range.start, range.end, value as TextDecorationStyle); } else if (property === 'textDecorationOffset') { textNode.setRangeTextDecorationOffset(range.start, range.end, value as TextDecorationOffset); } else if (property === 'textDecorationThickness') { textNode.setRangeTextDecorationThickness(range.start, range.end, value as TextDecorationThickness); } else if (property === 'textDecorationColor') { textNode.setRangeTextDecorationColor(range.start, range.end, value as TextDecorationColor); } else if (property === 'textDecorationSkipInk') { textNode.setRangeTextDecorationSkipInk(range.start, range.end, value as boolean); } else if (property === 'letterSpacing') { textNode.setRangeLetterSpacing(range.start, range.end, value as LetterSpacing); } else if (property === 'lineHeight') { textNode.setRangeLineHeight(range.start, range.end, value as LineHeight); } else if (property === 'hyperlink') { textNode.setRangeHyperlink(range.start, range.end, value as HyperlinkTarget); } else if (property === 'textStyleId') { textNode.setRangeTextStyleId(range.start, range.end, value as string); } else if (property === 'indentation') { textNode.setRangeIndentation(range.start, range.end, value as number); } else if (property === 'paragraphIndent') { textNode.setRangeParagraphIndent(range.start, range.end, value as number); } else if (property === 'paragraphSpacing') { textNode.setRangeParagraphSpacing(range.start, range.end, value as number); } else if (property === 'listOptions') { textNode.setRangeListOptions(range.start, range.end, value as TextListOptions); } else if (property === 'listSpacing') { textNode.setRangeListSpacing(range.start, range.end, value as number); } } } } ``` -------------------------------------------------------------------------------- /src/services/figma-api.ts: -------------------------------------------------------------------------------- ```typescript import axios from 'axios'; import { env } from '../config/env.js'; import type { GetFileResponse, GetFileNodesResponse, GetImageFillsResponse, GetImagesResponse, GetCommentsResponse, GetFileVersionsResponse, GetTeamProjectsResponse, GetProjectFilesResponse, GetTeamComponentsResponse, GetFileComponentsResponse, GetComponentResponse, GetTeamComponentSetsResponse, GetFileComponentSetsResponse, GetComponentSetResponse, GetTeamStylesResponse, GetFileStylesResponse, GetStyleResponse, PostCommentRequestBody, PostCommentResponse, } from "@figma/rest-api-spec"; const FIGMA_API_BASE_URL = 'https://api.figma.com/v1'; /** * Type definition for CreateFrameOptions */ export interface CreateFrameOptions { name: string; width: number; height: number; x?: number; y?: number; fills?: Array<{ type: string; color: { r: number; g: number; b: number }; opacity: number; }>; pageId?: string; } /** * Type definition for the expected response when creating a frame */ export interface CreateFrameResponse { frame: { id: string; name: string; }; success: boolean; } /** * Service for interacting with the Figma API */ export class FigmaApiService { private readonly headers: Record<string, string>; constructor(accessToken: string = env.FIGMA_PERSONAL_ACCESS_TOKEN) { this.headers = { 'X-Figma-Token': accessToken, }; } /** * Get file by key */ async getFile(fileKey: string, params: { ids?: string; depth?: number; geometry?: string; plugin_data?: string; branch_data?: boolean } = {}): Promise<GetFileResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}`, { headers: this.headers, params, }); return response.data; } /** * Get file nodes by key and node IDs */ async getFileNodes(fileKey: string, nodeIds: string[], params: { depth?: number; geometry?: string; plugin_data?: string } = {}): Promise<GetFileNodesResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/nodes`, { headers: this.headers, params: { ...params, ids: nodeIds.join(','), }, }); return response.data; } /** * Get images for file nodes */ async getImages(fileKey: string, nodeIds: string[], params: { scale?: number; format?: string; svg_include_id?: boolean; svg_include_node_id?: boolean; svg_simplify_stroke?: boolean; use_absolute_bounds?: boolean; version?: string; } = {}): Promise<GetImagesResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/images/${fileKey}`, { headers: this.headers, params: { ...params, ids: nodeIds.join(','), }, }); return response.data; } /** * Get image fills for a file */ async getImageFills(fileKey: string): Promise<GetImageFillsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/images`, { headers: this.headers, }); return response.data; } /** * Get comments for a file */ async getComments(fileKey: string, params: { as_md?: boolean } = {}): Promise<GetCommentsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/comments`, { headers: this.headers, params, }); return response.data; } /** * Post a comment to a file */ async postComment(fileKey: string, data: PostCommentRequestBody): Promise<PostCommentResponse> { const response = await axios.post( `${FIGMA_API_BASE_URL}/files/${fileKey}/comments`, data, { headers: this.headers } ); return response.data; } /** * Create a new frame in a Figma file * Note: This uses the Figma Plugin API which requires appropriate permissions */ async createFrame(fileKey: string, options: CreateFrameOptions): Promise<CreateFrameResponse> { // Build the frame creation request payload const payload = { node: { type: "FRAME", name: options.name, size: { width: options.width, height: options.height, }, position: { x: options.x || 0, y: options.y || 0, }, fills: options.fills || [], }, pageId: options.pageId, }; const response = await axios.post( `${FIGMA_API_BASE_URL}/files/${fileKey}/nodes`, payload, { headers: this.headers } ); return { frame: { id: response.data.node.id, name: response.data.node.name, }, success: true }; } /** * Get file versions */ async getFileVersions(fileKey: string): Promise<GetFileVersionsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/versions`, { headers: this.headers, }); return response.data; } /** * Get team projects */ async getTeamProjects(teamId: string): Promise<GetTeamProjectsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/projects`, { headers: this.headers, }); return response.data; } /** * Get project files */ async getProjectFiles(projectId: string, params: { branch_data?: boolean } = {}): Promise<GetProjectFilesResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/projects/${projectId}/files`, { headers: this.headers, params, }); return response.data; } /** * Get team components */ async getTeamComponents(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamComponentsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/components`, { headers: this.headers, params, }); return response.data; } /** * Get file components */ async getFileComponents(fileKey: string): Promise<GetFileComponentsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/components`, { headers: this.headers, }); return response.data; } /** * Get component by key */ async getComponent(key: string): Promise<GetComponentResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/components/${key}`, { headers: this.headers, }); return response.data; } /** * Get team component sets */ async getTeamComponentSets(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamComponentSetsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/component_sets`, { headers: this.headers, params, }); return response.data; } /** * Get file component sets */ async getFileComponentSets(fileKey: string): Promise<GetFileComponentSetsResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/component_sets`, { headers: this.headers, }); return response.data; } /** * Get component set by key */ async getComponentSet(key: string): Promise<GetComponentSetResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/component_sets/${key}`, { headers: this.headers, }); return response.data; } /** * Get team styles */ async getTeamStyles(teamId: string, params: { page_size?: number; after?: number; before?: number } = {}): Promise<GetTeamStylesResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/teams/${teamId}/styles`, { headers: this.headers, params, }); return response.data; } /** * Get file styles */ async getFileStyles(fileKey: string): Promise<GetFileStylesResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}/styles`, { headers: this.headers, }); return response.data; } /** * Get style by key */ async getStyle(key: string): Promise<GetStyleResponse> { const response = await axios.get(`${FIGMA_API_BASE_URL}/styles/${key}`, { headers: this.headers, }); return response.data; } } export default new FigmaApiService(); ``` -------------------------------------------------------------------------------- /src/plugin/creators/specialCreators.ts: -------------------------------------------------------------------------------- ```typescript /** * Special element creation functions for Figma plugin * Handles more specialized node types like boolean operations, connectors, etc. */ import { createSolidPaint } from '../utils/colorUtils'; import { applyCommonProperties } from '../utils/nodeUtils'; /** * Create a boolean operation from data * @param data Boolean operation configuration data * @returns Created boolean operation node */ export function createBooleanOperationFromData(data: any): BooleanOperationNode | null { // Boolean operations require child nodes if (!data.children || !Array.isArray(data.children) || data.children.length < 2) { console.error('Boolean operation requires at least 2 child nodes'); return null; } // First we need to create the child nodes and ensure they're on the page let childNodes: SceneNode[] = []; try { for (const childData of data.children) { const node = figma.createRectangle(); // Placeholder, would need actual createElement logic here // In actual use, you'll need to create the proper node type and apply properties childNodes.push(node); } // Now create the boolean operation with the child nodes const booleanOperation = figma.createBooleanOperation(); // Set the operation type if (data.booleanOperation) { booleanOperation.booleanOperation = data.booleanOperation; } // Apply common properties applyCommonProperties(booleanOperation, data); return booleanOperation; } catch (error) { console.error('Failed to create boolean operation:', error); // Clean up any created nodes to avoid leaving orphans childNodes.forEach(node => node.remove()); return null; } } /** * Create a connector node from data * @param data Connector configuration data * @returns Created connector node */ export function createConnectorFromData(data: any): ConnectorNode { const connector = figma.createConnector(); // Set connector specific properties if (data.connectorStart) connector.connectorStart = data.connectorStart; if (data.connectorEnd) connector.connectorEnd = data.connectorEnd; if (data.connectorStartStrokeCap) connector.connectorStartStrokeCap = data.connectorStartStrokeCap; if (data.connectorEndStrokeCap) connector.connectorEndStrokeCap = data.connectorEndStrokeCap; if (data.connectorLineType) connector.connectorLineType = data.connectorLineType; // Set stroke properties if (data.strokes) connector.strokes = data.strokes; if (data.strokeWeight) connector.strokeWeight = data.strokeWeight; // Apply common properties applyCommonProperties(connector, data); return connector; } /** * Create a shape with text node (used in FigJam) * This function might not work in all Figma versions * * @param data Shape with text configuration data * @returns Created shape with text node */ export function createShapeWithTextFromData(data: any): ShapeWithTextNode | null { // Check if this node type is supported if (!('createShapeWithText' in figma)) { console.error('ShapeWithText creation is not supported in this Figma version'); return null; } try { const shapeWithText = figma.createShapeWithText(); // Set shape specific properties if (data.shapeType) shapeWithText.shapeType = data.shapeType; // Text content if (data.text || data.characters) { shapeWithText.text.characters = data.text || data.characters; } // Text styling - these properties may not be directly accessible on all versions try { if (data.fontSize) shapeWithText.text.fontSize = data.fontSize; if (data.fontName) shapeWithText.text.fontName = data.fontName; // These properties may not exist directly on TextSublayerNode depending on Figma version if (data.textAlignHorizontal && 'textAlignHorizontal' in shapeWithText.text) { (shapeWithText.text as any).textAlignHorizontal = data.textAlignHorizontal; } if (data.textAlignVertical && 'textAlignVertical' in shapeWithText.text) { (shapeWithText.text as any).textAlignVertical = data.textAlignVertical; } } catch (e) { console.warn('Some text properties could not be set on ShapeWithText:', e); } // Fill and stroke if (data.fills) shapeWithText.fills = data.fills; if (data.strokes) shapeWithText.strokes = data.strokes; // Apply common properties applyCommonProperties(shapeWithText, data); return shapeWithText; } catch (error) { console.error('Failed to create shape with text:', error); return null; } } /** * Create a code block node * @param data Code block configuration data * @returns Created code block node */ export function createCodeBlockFromData(data: any): CodeBlockNode { const codeBlock = figma.createCodeBlock(); // Code content if (data.code) codeBlock.code = data.code; if (data.codeLanguage) codeBlock.codeLanguage = data.codeLanguage; // Apply common properties applyCommonProperties(codeBlock, data); return codeBlock; } /** * Create a table node * @param data Table configuration data * @returns Created table node */ export function createTableFromData(data: any): TableNode { // Create table with specified rows and columns (defaults to 2x2) const table = figma.createTable( data.numRows || 2, data.numColumns || 2 ); // Applying table styling // Note: Some properties may not be directly available depending on Figma version if (data.fills && 'fills' in table) { (table as any).fills = data.fills; } // Process cell data if provided if (data.cells && Array.isArray(data.cells)) { for (const cellData of data.cells) { if (cellData.rowIndex !== undefined && cellData.columnIndex !== undefined) { try { // Different Figma versions may have different API for accessing cells let cell; if ('cellAt' in table) { cell = table.cellAt(cellData.rowIndex, cellData.columnIndex); } else if ('getCellAt' in table) { cell = (table as any).getCellAt(cellData.rowIndex, cellData.columnIndex); } if (cell) { // Apply cell properties if (cellData.text && cell.text) cell.text.characters = cellData.text; if (cellData.fills && 'fills' in cell) cell.fills = cellData.fills; if (cellData.rowSpan && 'rowSpan' in cell) cell.rowSpan = cellData.rowSpan; if (cellData.columnSpan && 'columnSpan' in cell) cell.columnSpan = cellData.columnSpan; } } catch (e) { console.warn(`Could not set properties for cell at ${cellData.rowIndex}, ${cellData.columnIndex}:`, e); } } } } // Apply common properties applyCommonProperties(table, data); return table; } /** * Create a widget node (if supported in current Figma version) * @param data Widget configuration data * @returns Created widget node or null */ export function createWidgetFromData(data: any): WidgetNode | null { // Check if widget creation is supported if (!('createWidget' in figma)) { console.error('Widget creation is not supported in this Figma version'); return null; } // Widgets require a package ID if (!data.widgetId) { console.error('Widget creation requires a widgetId'); return null; } try { // Using type assertion since createWidget may not be recognized by TypeScript const widget = (figma as any).createWidget(data.widgetId); // Set widget properties if (data.widgetData) widget.widgetData = JSON.stringify(data.widgetData); if (data.width && data.height && 'resize' in widget) widget.resize(data.width, data.height); // Apply common properties applyCommonProperties(widget, data); return widget; } catch (error) { console.error('Failed to create widget:', error); return null; } } /** * Create a media node (if supported in current Figma version) * @param data Media configuration data * @returns Created media node or null */ export function createMediaFromData(data: any): MediaNode | null { // Check if media creation is supported if (!('createMedia' in figma)) { console.error('Media creation is not supported in this Figma version'); return null; } // Media requires a hash if (!data.hash) { console.error('Media creation requires a valid media hash'); return null; } try { // Using type assertion since createMedia may not be recognized by TypeScript const media = (figma as any).createMedia(data.hash); // Apply common properties applyCommonProperties(media, data); return media; } catch (error) { console.error('Failed to create media:', error); return null; } } ``` -------------------------------------------------------------------------------- /src/plugin/code.ts: -------------------------------------------------------------------------------- ```typescript /** * Figma MCP Plugin * Allows manipulating elements on canvas through MCP tools */ // Import modules import { createElementFromData, createElementsFromDataArray, } from "./creators/elementCreator"; import { createEllipseFromData, createLineFromData, createPolygonFromData, createRectangleFromData, createStarFromData, createVectorFromData } from "./creators/shapeCreators"; import { createTextFromData } from "./creators/textCreator"; import { hexToRgb } from "./utils/colorUtils"; import { buildResultObject, selectAndFocusNodes } from "./utils/nodeUtils"; // Show plugin UI figma.showUI(__html__, { width: 320, height: 500 }); // Log that the plugin has loaded console.log("Figma MCP Plugin loaded"); // Element creator mapping type ElementCreator = (params: any) => SceneNode | Promise<SceneNode>; const elementCreators: Record<string, ElementCreator> = { "create-rectangle": createRectangleFromData, "create-circle": createEllipseFromData, "create-ellipse": createEllipseFromData, "create-polygon": createPolygonFromData, "create-line": createLineFromData, "create-text": createTextFromData, "create-star": createStarFromData, "create-vector": createVectorFromData, "create-arc": (params: any) => { const ellipse = createEllipseFromData(params); if (params.arcData || (params.startAngle !== undefined && params.endAngle !== undefined)) { ellipse.arcData = { startingAngle: params.startAngle || params.arcData.startingAngle || 0, endingAngle: params.endAngle || params.arcData.endingAngle || 360, innerRadius: params.innerRadius || params.arcData.innerRadius || 0 }; } return ellipse; } }; // Generic element creation function async function createElement(type: string, params: any): Promise<SceneNode | null> { console.log(`Creating ${type} with params:`, params); // Get the creator function const creator = elementCreators[type]; if (!creator) { console.error(`Unknown element type: ${type}`); return null; } try { // Create the element (handle both synchronous and asynchronous creators) const element = await Promise.resolve(creator(params)); // Set position if provided if (element && params) { if (params.x !== undefined) element.x = params.x; if (params.y !== undefined) element.y = params.y; } // Select and focus the element if (element) { selectAndFocusNodes(element); } return element; } catch (error) { console.error(`Error creating ${type}:`, error); return null; } } // Handle messages from UI figma.ui.onmessage = async function (msg) { console.log("Received message from UI:", msg); // Handle different types of messages if (elementCreators[msg.type]) { // Element creation messages await createElement(msg.type, msg); } else if (msg.type === "create-element") { // Unified create element method console.log("Creating element with data:", msg.data); createElementFromData(msg.data); } else if (msg.type === "create-elements") { // Create multiple elements at once console.log("Creating multiple elements with data:", msg.data); createElementsFromDataArray(msg.data); } else if (msg.type === "mcp-command") { // Handle commands from MCP tool via UI console.log( "Received MCP command:", msg.command, "with params:", msg.params ); handleMcpCommand(msg.command, msg.params); } else if (msg.type === "cancel") { console.log("Closing plugin"); figma.closePlugin(); } else { console.log("Unknown message type:", msg.type); } }; // Handle MCP commands async function handleMcpCommand(command: string, params: any) { let result: | SceneNode | PageNode | readonly SceneNode[] | readonly PageNode[] | null = null; try { // Convert command format from mcp (create_rectangle) to plugin (create-rectangle) const pluginCommand = command.replace(/_/g, '-'); switch (pluginCommand) { case "create-rectangle": case "create-circle": case "create-polygon": case "create-line": case "create-arc": case "create-vector": console.log(`MCP command: Creating ${pluginCommand.substring(7)} with params:`, params); result = await createElement(pluginCommand, params); break; case "create-text": console.log("MCP command: Creating text with params:", params); result = await createElement(pluginCommand, params); break; case "create-element": console.log("MCP command: Creating element with params:", params); result = await createElementFromData(params); break; case "create-elements": console.log( "MCP command: Creating multiple elements with params:", params ); result = await createElementsFromDataArray(params); break; case "get-selection": console.log("MCP command: Getting current selection"); result = figma.currentPage.selection; break; case "get-elements": console.log("MCP command: Getting elements with params:", params); const page = params.page_id ? (figma.getNodeById(params.page_id) as PageNode) : figma.currentPage; if (!page || page.type !== "PAGE") { throw new Error("Invalid page ID or node is not a page"); } const nodeType = params.type || "ALL"; const limit = params.limit || 100; const includeHidden = params.include_hidden || false; if (nodeType === "ALL") { // Get all nodes, filtered by visibility if needed result = includeHidden ? page.children.slice(0, limit) : page.children.filter(node => node.visible).slice(0, limit); } else { // Filter by node type and visibility result = page.findAll(node => { const typeMatch = node.type === nodeType; const visibilityMatch = includeHidden || node.visible; return typeMatch && visibilityMatch; }).slice(0, limit); } break; case "get-element": console.log("MCP command: Getting element with ID:", params.node_id); const node = figma.getNodeById(params.node_id); if (!node) { throw new Error("Element not found with ID: " + params.node_id); } // Check if the node is a valid type for our result if (!['DOCUMENT', 'PAGE'].includes(node.type)) { // For scene nodes with children, include children if requested if (params.include_children && 'children' in node) { result = [node as SceneNode, ...((node as any).children || [])]; } else { result = node as SceneNode; } } else if (node.type === 'PAGE') { // Handle page nodes specially result = node as PageNode; } else { // For document or other unsupported node types throw new Error("Unsupported node type: " + node.type); } break; case "get-pages": console.log("MCP command: Getting all pages"); result = figma.root.children; break; case "get-page": console.log("MCP command: Getting page with ID:", params.page_id); if (!params.page_id) { // If no page_id is provided, use the current page console.log("No page_id provided, using current page"); result = figma.currentPage; } else { // If page_id is provided, find the page by ID const pageNode = figma.getNodeById(params.page_id); if (!pageNode || pageNode.type !== "PAGE") throw new Error("Invalid page ID or node is not a page"); result = pageNode; } break; case "create-page": console.log("MCP command: Creating new page with name:", params.name); const newPage = figma.createPage(); newPage.name = params.name || "New Page"; result = newPage; break; case "switch-page": console.log("MCP command: Switching to page with ID:", params.id); if (!params.id) throw new Error("Page ID is required"); const switchPageNode = figma.getNodeById(params.id); if (!switchPageNode || switchPageNode.type !== "PAGE") throw new Error("Invalid page ID"); figma.currentPage = switchPageNode as PageNode; result = switchPageNode; break; case "modify-rectangle": console.log("MCP command: Modifying rectangle with ID:", params.id); if (!params.id) throw new Error("Rectangle ID is required"); const modifyNode = figma.getNodeById(params.id); if (!modifyNode || modifyNode.type !== "RECTANGLE") throw new Error("Invalid rectangle ID"); const rect = modifyNode as RectangleNode; if (params.x !== undefined) rect.x = params.x; if (params.y !== undefined) rect.y = params.y; if (params.width !== undefined && params.height !== undefined) rect.resize(params.width, params.height); if (params.cornerRadius !== undefined) rect.cornerRadius = params.cornerRadius; if (params.color) rect.fills = [{ type: "SOLID", color: hexToRgb(params.color) }]; result = rect; break; default: console.log("Unknown MCP command:", command); throw new Error("Unknown command: " + command); } // Convert PageNode to a compatible format for buildResultObject if needed let resultForBuilder: SceneNode | readonly SceneNode[] | null = null; if (result === null) { resultForBuilder = null; } else if (Array.isArray(result)) { // For arrays, we rely on duck typing - both PageNode[] and SceneNode[] have id, name, type resultForBuilder = result as unknown as readonly SceneNode[]; } else if ("type" in result && result.type === "PAGE") { // For individual PageNode, we rely on duck typing - PageNode has id, name, type like SceneNode resultForBuilder = result as unknown as SceneNode; } else { resultForBuilder = result as SceneNode; } // Build result object, avoiding possible null values const resultObject = buildResultObject(resultForBuilder); console.log("Command result:", resultObject); // Send success response to UI figma.ui.postMessage({ type: "mcp-response", success: true, command: command, result: resultObject, }); console.log("Response sent to UI"); return resultObject; } catch (error) { console.error("Error handling MCP command:", error); // Send error response to UI figma.ui.postMessage({ type: "mcp-response", success: false, command: command, error: error instanceof Error ? error.message : "Unknown error", }); console.log("Error response sent to UI"); throw error; } } ``` -------------------------------------------------------------------------------- /src/tools/widget/widget-tools.ts: -------------------------------------------------------------------------------- ```typescript /** * Widget Tools - MCP server tools for interacting with Figma widgets */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../../services/figma-api.js"; import { FigmaUtils } from "../../utils/figma-utils.js"; /** * Register widget-related tools with the MCP server * @param server The MCP server instance */ export function registerWidgetTools(server: McpServer) { // Get all widget nodes in a file server.tool( "get_widgets", { file_key: z.string().min(1).describe("The Figma file key to retrieve widgets from") }, async ({ file_key }) => { try { const file = await figmaApi.getFile(file_key); // Find all widget nodes in the file const widgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); if (widgetNodes.length === 0) { return { content: [ { type: "text", text: `No widgets found in file ${file_key}` } ] }; } const widgetsList = widgetNodes.map((node, index) => { const widgetSyncData = node.widgetSync ? `\n - Widget Sync Data: Available` : `\n - Widget Sync Data: None`; return `${index + 1}. **${node.name}** (ID: ${node.id}) - Widget ID: ${node.widgetId || 'Unknown'}${widgetSyncData}`; }).join('\n\n'); return { content: [ { type: "text", text: `# Widgets in file ${file_key}` }, { type: "text", text: `Found ${widgetNodes.length} widgets:` }, { type: "text", text: widgetsList } ] }; } catch (error) { console.error('Error fetching widgets:', error); return { content: [ { type: "text", text: `Error getting widgets: ${(error as Error).message}` } ] }; } } ); // Get a specific widget node server.tool( "get_widget", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; // Get the sync data if available let syncDataContent = ''; if (widgetNode.widgetSync) { try { const syncData = JSON.parse(widgetNode.widgetSync); syncDataContent = `\n\n## Widget Sync Data\n\`\`\`json\n${JSON.stringify(syncData, null, 2)}\n\`\`\``; } catch (error) { syncDataContent = '\n\n## Widget Sync Data\nError parsing widget sync data'; } } return { content: [ { type: "text", text: `# Widget: ${widgetNode.name}` }, { type: "text", text: `ID: ${widgetNode.id}` }, { type: "text", text: `Widget ID: ${widgetNode.widgetId || 'Unknown'}` }, { type: "text", text: `Has Sync Data: ${widgetNode.widgetSync ? 'Yes' : 'No'}${syncDataContent}` } ] }; } catch (error) { console.error('Error fetching widget node:', error); return { content: [ { type: "text", text: `Error getting widget: ${(error as Error).message}` } ] }; } } ); // Get widget sync data server.tool( "get_widget_sync_data", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; if (!widgetNode.widgetSync) { return { content: [ { type: "text", text: `Widget ${node_id} does not have any sync data` } ] }; } try { const syncData = JSON.parse(widgetNode.widgetSync); return { content: [ { type: "text", text: `# Widget Sync Data for "${widgetNode.name}"` }, { type: "text", text: `Widget ID: ${widgetNode.id}` }, { type: "text", text: "```json\n" + JSON.stringify(syncData, null, 2) + "\n```" } ] }; } catch (error) { console.error('Error parsing widget sync data:', error); return { content: [ { type: "text", text: `Error parsing widget sync data: ${(error as Error).message}` } ] }; } } catch (error) { console.error('Error fetching widget sync data:', error); return { content: [ { type: "text", text: `Error getting widget sync data: ${(error as Error).message}` } ] }; } } ); // Search widgets by property values server.tool( "search_widgets", { file_key: z.string().min(1).describe("The Figma file key"), property_key: z.string().min(1).describe("The sync data property key to search for"), property_value: z.string().optional().describe("Optional property value to match (if not provided, returns all widgets with the property)") }, async ({ file_key, property_key, property_value }) => { try { const file = await figmaApi.getFile(file_key); // Find all widget nodes const allWidgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); // Filter widgets that have the specified property const matchingWidgets = allWidgetNodes.filter(node => { if (!node.widgetSync) return false; try { const syncData = JSON.parse(node.widgetSync); // If property_value is provided, check for exact match if (property_value !== undefined) { // Handle different types of values (string, number, boolean) const propValue = syncData[property_key]; if (typeof propValue === 'string') { return propValue === property_value; } else if (typeof propValue === 'number') { return propValue.toString() === property_value; } else if (typeof propValue === 'boolean') { return propValue.toString() === property_value; } else if (propValue !== null && typeof propValue === 'object') { return JSON.stringify(propValue) === property_value; } return false; } // If no value provided, just check if the property exists return property_key in syncData; } catch (error) { return false; } }); if (matchingWidgets.length === 0) { return { content: [ { type: "text", text: property_value ? `No widgets found with property "${property_key}" = "${property_value}"` : `No widgets found with property "${property_key}"` } ] }; } const widgetsList = matchingWidgets.map((node, index) => { let syncDataValue = ''; try { const syncData = JSON.parse(node.widgetSync!); const value = syncData[property_key]; syncDataValue = typeof value === 'object' ? JSON.stringify(value) : String(value); } catch (error) { syncDataValue = 'Error parsing sync data'; } return `${index + 1}. **${node.name}** (ID: ${node.id}) - Property "${property_key}": ${syncDataValue}`; }).join('\n\n'); return { content: [ { type: "text", text: property_value ? `# Widgets with property "${property_key}" = "${property_value}"` : `# Widgets with property "${property_key}"` }, { type: "text", text: `Found ${matchingWidgets.length} matching widgets:` }, { type: "text", text: widgetsList } ] }; } catch (error) { console.error('Error searching widgets:', error); return { content: [ { type: "text", text: `Error searching widgets: ${(error as Error).message}` } ] }; } } ); // Get widget properties for modification server.tool( "analyze_widget_structure", { file_key: z.string().min(1).describe("The Figma file key"), node_id: z.string().min(1).describe("The ID of the widget node") }, async ({ file_key, node_id }) => { try { const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); const nodeData = fileNodes.nodes[node_id]; if (!nodeData || nodeData.document.type !== 'WIDGET') { return { content: [ { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } ] }; } const widgetNode = nodeData.document; // Create a full analysis of the widget const widgetAnalysis = { basic: { id: widgetNode.id, name: widgetNode.name, type: widgetNode.type, widgetId: widgetNode.widgetId || 'Unknown' }, placement: { x: widgetNode.x || 0, y: widgetNode.y || 0, width: widgetNode.width || 0, height: widgetNode.height || 0, rotation: widgetNode.rotation || 0 }, syncData: null as any }; // Parse the widget sync data if available if (widgetNode.widgetSync) { try { widgetAnalysis.syncData = JSON.parse(widgetNode.widgetSync); } catch (error) { widgetAnalysis.syncData = { error: 'Invalid sync data format' }; } } return { content: [ { type: "text", text: `# Widget Analysis: ${widgetNode.name}` }, { type: "text", text: `## Basic Information` }, { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.basic, null, 2) + "\n```" }, { type: "text", text: `## Placement` }, { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.placement, null, 2) + "\n```" }, { type: "text", text: `## Sync Data` }, { type: "text", text: widgetAnalysis.syncData ? "```json\n" + JSON.stringify(widgetAnalysis.syncData, null, 2) + "\n```" : "No sync data available" } ] }; } catch (error) { console.error('Error analyzing widget:', error); return { content: [ { type: "text", text: `Error analyzing widget: ${(error as Error).message}` } ] }; } } ); } ``` -------------------------------------------------------------------------------- /src/tools/frame.ts: -------------------------------------------------------------------------------- ```typescript /** * Frame Tools - MCP server tools for working with Figma Frame components */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import figmaApi from "../services/figma-api.js"; import { FigmaUtils } from "../utils/figma-utils.js"; /** * Register frame-related tools with the MCP server * @param server The MCP server instance */ export function registerFrameTools(server: McpServer) { // Get Frame component documentation server.tool("get_frame_documentation", {}, async () => { try { // Frame component documentation based on the provided text return { content: [ { type: "text", text: "# Frame Component Documentation" }, { type: "text", 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.", }, { type: "text", text: "If you want to use autolayout, use AutoLayout instead.", }, { type: "text", text: "## BaseProps" }, { type: "text", text: "- **name**: string - The name of the component", }, { type: "text", text: "- **hidden**: boolean - Toggles whether to show the component", }, { type: "text", text: "- **onClick**: (event: WidgetClickEvent) => Promise<any> | void - Attach a click handler", }, { type: "text", text: "- **key**: string | number - The key of the component", }, { type: "text", text: "- **hoverStyle**: HoverStyle - The style to be applied when hovering", }, { type: "text", text: "- **tooltip**: string - The tooltip shown when hovering", }, { type: "text", text: "- **positioning**: 'auto' | 'absolute' - How to position the node inside an AutoLayout parent", }, { type: "text", text: "## BlendProps" }, { type: "text", text: "- **blendMode**: BlendMode - The blendMode of the component", }, { type: "text", text: "- **opacity**: number - The opacity of the component", }, { type: "text", text: "- **effect**: Effect | Effect[] - The effect of the component", }, { type: "text", text: "## ConstraintProps" }, { type: "text", text: "- **x**: number | HorizontalConstraint - The x position of the node", }, { type: "text", text: "- **y**: number | VerticalConstraint - The y position of the node", }, { type: "text", text: "- **overflow**: 'visible' | 'hidden' | 'scroll' - The overflow behavior", }, { type: "text", text: "## SizeProps (Required)" }, { type: "text", text: "- **width**: Size - The width of the component (required)", }, { type: "text", text: "- **height**: Size - The height of the component (required)", }, { type: "text", text: "- **minWidth**: number - The minimum width" }, { type: "text", text: "- **maxWidth**: number - The maximum width" }, { type: "text", text: "- **minHeight**: number - The minimum height", }, { type: "text", text: "- **maxHeight**: number - The maximum height", }, { type: "text", text: "- **rotation**: number - The rotation in degrees (-180 to 180)", }, { type: "text", text: "## CornerProps" }, { type: "text", text: "- **cornerRadius**: CornerRadius - The corner radius in pixels", }, { type: "text", text: "## GeometryProps" }, { type: "text", text: "- **fill**: HexCode | Color | Paint | (SolidPaint | GradientPaint)[] - The fill paints", }, { type: "text", text: "- **stroke**: HexCode | Color | SolidPaint | GradientPaint | (SolidPaint | GradientPaint)[] - The stroke paints", }, { type: "text", text: "- **strokeWidth**: number - The stroke thickness in pixels", }, { type: "text", text: "- **strokeAlign**: StrokeAlign - The stroke alignment", }, { type: "text", text: "- **strokeDashPattern**: number[] - The stroke dash pattern", }, ], }; } catch (error) { console.error("Error retrieving Frame documentation:", error); return { content: [ { type: "text", text: `Error getting Frame documentation: ${ (error as Error).message }`, }, ], }; } }); // Create a new frame widget server.tool( "create_frame_widget", { name: z .string() .min(1) .describe("The name for the widget containing frames"), width: z.number().min(1).describe("The width of the frame"), height: z.number().min(1).describe("The height of the frame"), fill: z.string().optional().describe("The fill color (hex code)"), }, async ({ name, width, height, fill }) => { try { // Create a sample widget code with Frame component const widgetCode = ` // ${name} - Figma Widget with Frame Component const { widget } = figma; const { Frame, Text } = widget; function ${name.replace(/\\s+/g, "")}Widget() { return ( <Frame name="${name}" width={${width}} height={${height}} fill={${fill ? `"${fill}"` : "[]"}} stroke="#E0E0E0" strokeWidth={1} cornerRadius={8} > <Text x={20} y={20} width={${width - 40}} horizontalAlignText="center" fill="#000000" > Frame Widget Example </Text> </Frame> ); } widget.register(${name.replace(/\\s+/g, "")}Widget); `; return { content: [ { type: "text", text: `# Frame Widget Code` }, { type: "text", text: `The following code creates a widget using the Frame component:`, }, { type: "text", text: "```jsx\n" + widgetCode + "\n```" }, { type: "text", text: `## Instructions` }, { type: "text", text: `1. Create a new widget in Figma` }, { type: "text", text: `2. Copy and paste this code into the widget code editor`, }, { type: "text", text: `3. Customize the content inside the Frame as needed`, }, ], }; } catch (error) { console.error("Error generating frame widget code:", error); return { content: [ { type: "text", text: `Error generating frame widget code: ${ (error as Error).message }`, }, ], }; } } ); // Create a frame directly in Figma file server.tool( "create_frame_in_figma", { file_key: z .string() .min(1) .describe("The Figma file key where the frame will be created"), page_id: z .string() .optional() .describe("The page ID where the frame will be created (optional)"), name: z .string() .min(1) .describe("The name for the new frame"), width: z.number().min(1).describe("The width of the frame in pixels"), height: z.number().min(1).describe("The height of the frame in pixels"), x: z.number().default(0).describe("The X position of the frame (default: 0)"), y: z.number().default(0).describe("The Y position of the frame (default: 0)"), fill_color: z.string().optional().describe("The fill color (hex code)"), }, async ({ file_key, page_id, name, width, height, x, y, fill_color }) => { try { // Create a frame in Figma using the Figma API const createFrameResponse = await figmaApi.createFrame(file_key, { name, width, height, x, y, fills: fill_color ? [{ type: "SOLID", color: hexToRgb(fill_color), opacity: 1 }] : [], pageId: page_id }); return { content: [ { type: "text", text: `# Frame Created Successfully` }, { type: "text", text: `A new frame named "${name}" has been created in your Figma file.` }, { type: "text", text: `- Width: ${width}px\n- Height: ${height}px\n- Position: (${x}, ${y})` }, { type: "text", text: `Frame ID: ${createFrameResponse.frame.id}` }, { type: "text", text: `You can now view and edit this frame in your Figma file.` } ], }; } catch (error) { console.error("Error creating frame in Figma:", error); return { content: [ { type: "text", text: `Error creating frame in Figma: ${ (error as Error).message }`, }, { type: "text", text: "Please make sure you have write access to the file and the file key is correct." } ], }; } } ); // Get all frames in a file server.tool( "get_frames", { file_key: z .string() .min(1) .describe("The Figma file key to retrieve frames from"), }, async ({ file_key }) => { try { const file = await figmaApi.getFile(file_key); // Find all frame nodes in the file const frameNodes = FigmaUtils.getNodesByType(file, "FRAME"); if (frameNodes.length === 0) { return { content: [ { type: "text", text: `No frames found in file ${file_key}` }, ], }; } const framesList = frameNodes .map((node, index) => { // Add type assertion for frame nodes const frameNode = node as { id: string; name: string; width?: number; height?: number; children?: Array<any>; }; return `${index + 1}. **${frameNode.name}** (ID: ${frameNode.id}) - Width: ${frameNode.width || "Unknown"}, Height: ${frameNode.height || "Unknown"} - Children: ${frameNode.children?.length || 0}`; }) .join("\n\n"); return { content: [ { type: "text", text: `# Frames in file ${file_key}` }, { type: "text", text: `Found ${frameNodes.length} frames:` }, { type: "text", text: framesList }, ], }; } catch (error) { console.error("Error fetching frames:", error); return { content: [ { type: "text", text: `Error getting frames: ${(error as Error).message}`, }, ], }; } } ); } /** * Convert hex color code to RGB values * @param hex Hex color code (e.g., #RRGGBB or #RGB) * @returns RGB color object with r, g, b values between 0 and 1 */ function hexToRgb(hex: string) { // Remove # if present hex = hex.replace(/^#/, ''); // Parse hex values let r, g, b; if (hex.length === 3) { // Convert 3-digit hex to 6-digit r = parseInt(hex.charAt(0) + hex.charAt(0), 16) / 255; g = parseInt(hex.charAt(1) + hex.charAt(1), 16) / 255; b = parseInt(hex.charAt(2) + hex.charAt(2), 16) / 255; } else if (hex.length === 6) { r = parseInt(hex.substring(0, 2), 16) / 255; g = parseInt(hex.substring(2, 4), 16) / 255; b = parseInt(hex.substring(4, 6), 16) / 255; } else { throw new Error(`Invalid hex color: ${hex}`); } return { r, g, b }; } ```