This is page 1 of 3. Use http://codebase.md/sichang824/mcp-figma?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .envrc ├── .gitignore ├── .mcp.pid ├── bun.lockb ├── docs │ ├── 01-overview.md │ ├── 02-implementation-steps.md │ ├── 03-components-and-features.md │ ├── 04-usage-guide.md │ ├── 05-project-status.md │ ├── image.png │ ├── README.md │ └── widget-tools-guide.md ├── Makefile ├── manifest.json ├── package.json ├── prompt.md ├── README.md ├── README.zh.md ├── src │ ├── config │ │ └── env.ts │ ├── index.ts │ ├── plugin │ │ ├── code.js │ │ ├── code.ts │ │ ├── creators │ │ │ ├── componentCreators.ts │ │ │ ├── containerCreators.ts │ │ │ ├── elementCreator.ts │ │ │ ├── imageCreators.ts │ │ │ ├── shapeCreators.ts │ │ │ ├── sliceCreators.ts │ │ │ ├── specialCreators.ts │ │ │ └── textCreator.ts │ │ ├── manifest.json │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── ui.html │ │ └── utils │ │ ├── colorUtils.ts │ │ └── nodeUtils.ts │ ├── resources.ts │ ├── services │ │ ├── figma-api.ts │ │ ├── websocket.ts │ │ └── widget-api.ts │ ├── tools │ │ ├── canvas.ts │ │ ├── comment.ts │ │ ├── component.ts │ │ ├── file.ts │ │ ├── frame.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── node.ts │ │ ├── page.ts │ │ ├── search.ts │ │ ├── utils │ │ │ └── widget-utils.ts │ │ ├── version.ts │ │ ├── widget │ │ │ ├── analyze-widget-structure.ts │ │ │ ├── get-widget-sync-data.ts │ │ │ ├── get-widget.ts │ │ │ ├── get-widgets.ts │ │ │ ├── index.ts │ │ │ ├── README.md │ │ │ ├── search-widgets.ts │ │ │ └── widget-tools.ts │ │ └── zod-schemas.ts │ ├── utils │ │ ├── figma-utils.ts │ │ └── widget-utils.ts │ ├── utils.ts │ ├── widget │ │ └── utils │ │ └── widget-tools.ts │ └── widget-tools.ts ├── tsconfig.json └── tsconfig.widget.json ``` # Files -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- ``` 1 | dotenv ``` -------------------------------------------------------------------------------- /.mcp.pid: -------------------------------------------------------------------------------- ``` 1 | 11949 2 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Figma API credentials 2 | FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token_here 3 | 4 | # Server configuration 5 | PORT=3001 6 | NODE_ENV=development 7 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | .env.test 177 | 178 | # Environment files 179 | .env.* 180 | !.env.example 181 | 182 | # Dependencies 183 | node_modules/ 184 | .npm 185 | npm-debug.log* 186 | yarn-debug.log* 187 | yarn-error.log* 188 | 189 | # Build output 190 | dist/ 191 | build/ 192 | 193 | # OS specific files 194 | .DS_Store 195 | Thumbs.db 196 | 197 | # IDE specific files 198 | .idea/ 199 | .vscode/ 200 | *.sublime-* 201 | .history/ 202 | ``` -------------------------------------------------------------------------------- /src/tools/widget/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Widget Tools for Figma MCP 2 | 3 | This directory contains tools for interacting with Figma widgets through the MCP server. 4 | 5 | ## Directory Structure 6 | 7 | Each tool is organized in its own directory: 8 | 9 | - `get-widgets`: Lists all widgets in a file 10 | - `get-widget`: Gets detailed information about a specific widget 11 | - `get-widget-sync-data`: Retrieves a widget's synchronized state data 12 | - `search-widgets`: Searches for widgets with specific properties 13 | - `analyze-widget-structure`: Provides detailed analysis of a widget's structure 14 | 15 | ## Adding New Widget Tools 16 | 17 | To add a new widget tool: 18 | 19 | 1. Create a new directory for your tool under `src/tools/widget/` 20 | 2. Create an `index.ts` file with your tool implementation 21 | 3. Update the main `index.ts` to import and register your tool 22 | 23 | ## Using Shared Utilities 24 | 25 | Common widget utilities can be found in `src/tools/utils/widget-utils.ts`. 26 | 27 | ## Widget Tool Pattern 28 | 29 | Each widget tool follows this pattern: 30 | 31 | ```typescript 32 | export const yourToolName = (server: McpServer) => { 33 | server.tool( 34 | "tool_name", 35 | { 36 | // Parameter schema using zod 37 | param1: z.string().describe("Description"), 38 | param2: z.number().describe("Description") 39 | }, 40 | async ({ param1, param2 }) => { 41 | try { 42 | // Tool implementation 43 | return { 44 | content: [ 45 | { type: "text", text: "Response content" } 46 | ] 47 | }; 48 | } catch (error) { 49 | console.error('Error message:', error); 50 | return { 51 | content: [ 52 | { type: "text", text: `Error: ${(error as Error).message}` } 53 | ] 54 | }; 55 | } 56 | } 57 | ); 58 | }; 59 | ``` 60 | 61 | For more information, see the [Widget Tools Guide](/docs/widget-tools-guide.md). 62 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Figma MCP Server Documentation 2 | 3 | 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. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Project Overview](./01-overview.md) 8 | - Introduction and purpose 9 | - Core features 10 | - Technology stack 11 | - Project structure 12 | - Integration with AI systems 13 | 14 | 2. [Implementation Steps](./02-implementation-steps.md) 15 | - Project setup 16 | - Configuration 17 | - Figma API integration 18 | - MCP server implementation 19 | - Build system 20 | - Documentation 21 | - Testing and verification 22 | 23 | 3. [Components and Features](./03-components-and-features.md) 24 | - Core components 25 | - MCP tools 26 | - Resource templates 27 | - Error handling 28 | - Response formatting 29 | 30 | 4. [Usage Guide](./04-usage-guide.md) 31 | - Setup instructions 32 | - Running the server 33 | - Using MCP tools (with examples) 34 | - Using resource templates 35 | - Error handling examples 36 | - Tips and best practices 37 | 38 | 5. [Project Status and Roadmap](./05-project-status.md) 39 | - Current status 40 | - Next steps 41 | - Version history 42 | - Known issues 43 | - Contribution guidelines 44 | - Support and feedback 45 | 46 | ## Quick Start 47 | 48 | To get started with the Figma MCP server: 49 | 50 | 1. Install dependencies: 51 | ```bash 52 | make install 53 | ``` 54 | 55 | 2. Configure your Figma API token in `.env` 56 | 57 | 3. Start the server: 58 | ```bash 59 | make mcp 60 | ``` 61 | 62 | For more detailed instructions, see the [Usage Guide](./04-usage-guide.md). 63 | 64 | ## Project Status 65 | 66 | 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. 67 | 68 | ## Contributing 69 | 70 | Contributions are welcome! See the [Contribution Guidelines](./05-project-status.md#5-contribution-guidelines) for more information on how to contribute to the project. 71 | 72 | --- 73 | 74 | Last updated: April 13, 2025 75 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/sichang824-mcp-figma) 2 | 3 | # Figma MCP Server 4 | 5 | A Figma API server implementation based on Model Context Protocol (MCP), supporting Figma plugin and widget integration. 6 | 7 | ## Features 8 | 9 | - Interact with Figma API through MCP 10 | - WebSocket server for Figma plugin communication 11 | - Support for Figma widget development 12 | - Environment variable configuration via command line arguments 13 | - Rich set of Figma operation tools 14 | 15 | ## Installation 16 | 17 | 1. Clone the repository: 18 | 19 | ```bash 20 | git clone <repository-url> 21 | cd figma-mcp 22 | ``` 23 | 24 | 2. Install dependencies: 25 | 26 | ```bash 27 | bun install 28 | ``` 29 | 30 | ## Configuration 31 | 32 | ### Environment Variables 33 | 34 | Create a `.env` file and set the following environment variables: 35 | 36 | ``` 37 | FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token 38 | PORT=3001 39 | NODE_ENV=development 40 | ``` 41 | 42 | ### Getting a Figma Access Token 43 | 44 | 1. Log in to [Figma](https://www.figma.com/) 45 | 2. Go to Account Settings > Personal Access Tokens 46 | 3. Create a new access token 47 | 4. Copy the token to your `.env` file or pass it via command line arguments 48 | 49 | ## Usage 50 | 51 | ### Build the Project 52 | 53 | ```bash 54 | bun run build 55 | ``` 56 | 57 | ### Run the Development Server 58 | 59 | ```bash 60 | bun run dev 61 | ``` 62 | 63 | ### Using Command Line Arguments 64 | 65 | Support for setting environment variables via the `-e` parameter: 66 | 67 | ```bash 68 | bun --watch src/index.ts -e FIGMA_PERSONAL_ACCESS_TOKEN=your_token -e PORT=6000 69 | ``` 70 | 71 | You can also use a dedicated token parameter: 72 | 73 | ```bash 74 | bun --watch src/index.ts --token your_token 75 | ``` 76 | 77 | Or its shorthand: 78 | 79 | ```bash 80 | bun --watch src/index.ts -t your_token 81 | ``` 82 | 83 | ## Configuring MCP in Cursor 84 | 85 | Add to the `.cursor/mcp.json` file: 86 | 87 | ```json 88 | { 89 | "Figma MCP": { 90 | "command": "bun", 91 | "args": [ 92 | "--watch", 93 | "/path/to/figma-mcp/src/index.ts", 94 | "-e", 95 | "FIGMA_PERSONAL_ACCESS_TOKEN=your_token_here", 96 | "-e", 97 | "PORT=6000" 98 | ] 99 | } 100 | } 101 | ``` 102 | 103 | ## Available Tools 104 | 105 | The server provides the following Figma operation tools: 106 | 107 | - File operations: Get files, versions, etc. 108 | - Node operations: Get and manipulate Figma nodes 109 | - Comment operations: Manage comments in Figma files 110 | - Image operations: Export Figma elements as images 111 | - Search functionality: Search content in Figma files 112 | - Component operations: Manage Figma components 113 | - Canvas operations: Create rectangles, circles, text, etc. 114 | - Widget operations: Manage Figma widgets 115 | 116 | ## Figma Plugin Development 117 | 118 | ### Plugin Introduction 119 | 120 | 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. 121 | 122 | ### Building and Testing 123 | 124 | Build the plugin: 125 | 126 | ```bash 127 | bun run build:plugin 128 | ``` 129 | 130 | Run in development mode: 131 | 132 | ```bash 133 | bun run dev:plugin 134 | ``` 135 | 136 | ### Loading the Plugin in Figma 137 | 138 |  139 | 140 | 1. Right-click in Figma to open the menu -> Plugins -> Development -> Import plugin from manifest... 141 | 2. Select the plugin's `manifest.json` file 142 | 3. Your plugin will now appear in Figma's plugin menu 143 | 144 | ### Plugin Interaction with MCP Server 145 | 146 | Plugins can communicate with the MCP server via WebSocket to achieve: 147 | 148 | - Complex data processing 149 | - External API integration 150 | - Cross-session data persistence 151 | - AI functionality integration 152 | 153 | ## Development 154 | 155 | ### Build Widget 156 | 157 | ```bash 158 | bun run build:widget 159 | ``` 160 | 161 | ### Build Plugin 162 | 163 | ```bash 164 | bun run build:plugin 165 | ``` 166 | 167 | ### Development Mode 168 | 169 | ```bash 170 | bun run dev:widget # Widget development mode 171 | bun run dev:plugin # Plugin development mode 172 | ``` 173 | 174 | ## License 175 | 176 | MIT 177 | ``` -------------------------------------------------------------------------------- /src/widget-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | ``` -------------------------------------------------------------------------------- /src/plugin/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "Figma MCP Canvas Operation Tool", 3 | "id": "figma-mcp-canvas-tools", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma"], 8 | "permissions": [] 9 | } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "MCP Figma Widget", 3 | "id": "mcp-figma-widget", 4 | "api": "1.0.0", 5 | "main": "dist/widget-code.js", 6 | "capabilities": ["network-access"], 7 | "editorType": ["figma"], 8 | "containsWidget": true, 9 | "widgetApi": "1.0.0" 10 | } 11 | ``` -------------------------------------------------------------------------------- /src/plugin/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6", "dom"], 5 | "typeRoots": ["../../node_modules/@types", "../../node_modules/@figma"], 6 | "moduleResolution": "node", 7 | "strict": true 8 | }, 9 | "include": ["*.ts", "utils/**/*.ts"], 10 | "exclude": ["node_modules"] 11 | } ``` -------------------------------------------------------------------------------- /tsconfig.widget.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "jsxFactory": "figma.widget.h", 5 | "jsxFragmentFactory": "figma.widget.Fragment", 6 | "target": "es6", 7 | "strict": true, 8 | "typeRoots": [ 9 | "./node_modules/@types", 10 | "./node_modules/@figma" 11 | ] 12 | }, 13 | "include": ["src/widget/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "outDir": "dist", 12 | "declaration": true, 13 | "declarationDir": "dist/types", 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["src/*"] 17 | } 18 | }, 19 | "include": ["src/**/*", "types/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | ``` -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility functions for Figma MCP Server 3 | */ 4 | 5 | /** 6 | * Log function that writes to stderr instead of stdout 7 | * to avoid interfering with MCP stdio communication 8 | */ 9 | export function log(message: string): void { 10 | process.stderr.write(`${message}\n`); 11 | } 12 | 13 | /** 14 | * Error log function that writes to stderr 15 | */ 16 | export function logError(message: string, error?: unknown): void { 17 | const errorMessage = error instanceof Error 18 | ? error.message 19 | : error ? String(error) : 'Unknown error'; 20 | 21 | process.stderr.write(`ERROR: ${message}: ${errorMessage}\n`); 22 | } ``` -------------------------------------------------------------------------------- /src/tools/widget/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Widget Tools - Index file to export all widget-related tools 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { getWidgetsTool } from "./get-widgets.js"; 6 | import { getWidgetTool } from "./get-widget.js"; 7 | import { getWidgetSyncDataTool } from "./get-widget-sync-data.js"; 8 | import { searchWidgetsTool } from "./search-widgets.js"; 9 | import { analyzeWidgetStructureTool } from "./analyze-widget-structure.js"; 10 | 11 | /** 12 | * Registers all widget-related tools with the MCP server 13 | * @param server The MCP server instance 14 | */ 15 | export function registerWidgetTools(server: McpServer): void { 16 | // Register all widget tools 17 | getWidgetsTool(server); 18 | getWidgetTool(server); 19 | getWidgetSyncDataTool(server); 20 | searchWidgetsTool(server); 21 | analyzeWidgetStructureTool(server); 22 | } 23 | ``` -------------------------------------------------------------------------------- /src/plugin/utils/colorUtils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Color utility functions for Figma plugin 3 | */ 4 | 5 | /** 6 | * Convert hex color to RGB object 7 | * @param hex Hexadecimal color string (with or without #) 8 | * @returns RGB object with values between 0 and 1 9 | */ 10 | export function hexToRgb(hex: string): RGB { 11 | hex = hex.replace('#', ''); 12 | const r = parseInt(hex.substring(0, 2), 16) / 255; 13 | const g = parseInt(hex.substring(2, 4), 16) / 255; 14 | const b = parseInt(hex.substring(4, 6), 16) / 255; 15 | return { r, g, b }; 16 | } 17 | 18 | /** 19 | * Convert RGB object to hex color string 20 | * @param rgb RGB object with values between 0 and 1 21 | * @returns Hexadecimal color string with # 22 | */ 23 | export function rgbToHex(rgb: RGB): string { 24 | const r = Math.round(rgb.r * 255).toString(16).padStart(2, '0'); 25 | const g = Math.round(rgb.g * 255).toString(16).padStart(2, '0'); 26 | const b = Math.round(rgb.b * 255).toString(16).padStart(2, '0'); 27 | return `#${r}${g}${b}`; 28 | } 29 | 30 | /** 31 | * Create a solid color paint 32 | * @param color RGB color object or hex string 33 | * @returns Solid paint object 34 | */ 35 | export function createSolidPaint(color: RGB | string): SolidPaint { 36 | if (typeof color === 'string') { 37 | return { type: 'SOLID', color: hexToRgb(color) }; 38 | } 39 | return { type: 'SOLID', color }; 40 | } ``` -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tools - Main index file for all MCP tools 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { registerWidgetTools } from "./widget/index.js"; 6 | import { registerFileTools } from "./file.js"; 7 | import { registerNodeTools } from "./node.js"; 8 | import { registerCommentTools } from "./comment.js"; 9 | import { registerImageTools } from "./image.js"; 10 | import { registerVersionTools } from "./version.js"; 11 | import { registerSearchTools } from "./search.js"; 12 | import { registerComponentTools } from "./component.js"; 13 | import { registerFrameTools } from "./frame.js"; 14 | import { registerCanvasTools } from "./canvas.js"; 15 | import { registerPageTools } from "./page.js"; 16 | 17 | /** 18 | * Registers all tools with the MCP server 19 | * @param server The MCP server instance 20 | */ 21 | export function registerAllTools(server: McpServer): void { 22 | // Register all tool categories 23 | registerFileTools(server); 24 | registerNodeTools(server); 25 | registerCommentTools(server); 26 | registerImageTools(server); 27 | registerVersionTools(server); 28 | registerSearchTools(server); 29 | registerComponentTools(server); 30 | registerWidgetTools(server); 31 | registerFrameTools(server); 32 | registerCanvasTools(server); 33 | registerPageTools(server); 34 | } 35 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "figma-mcp-server", 3 | "version": "1.0.0", 4 | "description": "MCP server for accessing Figma API with widget support", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "start": "bun run dist/index.js", 9 | "dev": "bun --watch src/index.ts", 10 | "mcp": "bun --watch src/index.ts", 11 | "build": "bun build src/index.ts --outdir dist --target node", 12 | "build:mcp": "bun build src/index.ts --outdir dist --target node", 13 | "build:widget": "bun build src/widget/widget.tsx --outfile dist/widget-code.js --target browser", 14 | "dev:widget": "bun build src/widget/widget.tsx --outfile dist/widget-code.js --target browser --watch", 15 | "build:plugin": "bun build src/plugin/code.ts --outfile src/plugin/code.js --target browser", 16 | "dev:plugin": "bun build src/plugin/code.ts --outfile src/plugin/code.js --target browser --watch", 17 | "test": "bun test" 18 | }, 19 | "keywords": [ 20 | "figma", 21 | "api", 22 | "mcp", 23 | "server", 24 | "widget" 25 | ], 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@create-figma-plugin/ui": "^4.0.0", 30 | "@create-figma-plugin/utilities": "^4.0.0", 31 | "@figma/rest-api-spec": "^0.27.0", 32 | "@figma/widget-typings": "^1.11.0", 33 | "@modelcontextprotocol/sdk": "^1.9.0", 34 | "@types/ws": "^8.18.1", 35 | "axios": "^1.6.2", 36 | "cors": "^2.8.5", 37 | "dotenv": "^16.3.1", 38 | "express": "^5.1.0", 39 | "js-yaml": "^4.1.0", 40 | "ws": "^8.18.1", 41 | "zod": "^3.24.2" 42 | }, 43 | "devDependencies": { 44 | "@figma/plugin-typings": "^1.109.0", 45 | "@types/cors": "^2.8.17", 46 | "@types/express": "^4.17.21", 47 | "@types/js-yaml": "^4.0.9", 48 | "@types/node": "^20.10.0", 49 | "typescript": "^5.3.2" 50 | } 51 | } ``` -------------------------------------------------------------------------------- /src/tools/node.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Node tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getNodeTool = (server: McpServer) => { 9 | server.tool( 10 | "get_node", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key to retrieve from"), 13 | node_id: z.string().min(1).describe("The ID of the node to retrieve") 14 | }, 15 | async ({ file_key, node_id }) => { 16 | try { 17 | const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); 18 | const nodeData = fileNodes.nodes[node_id]; 19 | 20 | if (!nodeData) { 21 | return { 22 | content: [ 23 | { type: "text", text: `Node ${node_id} not found in file ${file_key}` } 24 | ] 25 | }; 26 | } 27 | 28 | return { 29 | content: [ 30 | { type: "text", text: `# Node: ${nodeData.document.name}` }, 31 | { type: "text", text: `Type: ${nodeData.document.type}` }, 32 | { type: "text", text: `ID: ${nodeData.document.id}` }, 33 | { type: "text", text: `Children: ${nodeData.document.children?.length || 0}` }, 34 | { type: "text", text: "```json\n" + JSON.stringify(nodeData.document, null, 2) + "\n```" } 35 | ] 36 | }; 37 | } catch (error) { 38 | console.error('Error fetching node:', error); 39 | return { 40 | content: [ 41 | { type: "text", text: `Error getting node: ${(error as Error).message}` } 42 | ] 43 | }; 44 | } 45 | } 46 | ); 47 | }; 48 | 49 | /** 50 | * Registers all node-related tools with the MCP server 51 | */ 52 | export const registerNodeTools = (server: McpServer): void => { 53 | getNodeTool(server); 54 | }; 55 | ``` -------------------------------------------------------------------------------- /prompt.md: -------------------------------------------------------------------------------- ```markdown 1 | # Basic Command Line Coding Assistant 2 | 3 | You are a command line coding assistant. Help me write and manage code using these essential terminal commands: 4 | 5 | ## Basic File Operations 6 | 7 | - View files recursively: `tree -fiI ".venv|node_modules|.git|dist|<MORE_IGNORE_PATTERNS>"` 8 | - View file contents: `cat file.py` 9 | - Search in files: `grep "function" file.py` 10 | - Search recursively: `grep -r "pattern" directory/` 11 | - Find files by name: `find . -name "*.py"` 12 | - Write to file using cat: 13 | 14 | ``` 15 | cat > file.py << EOF 16 | # Add your code here 17 | EOF 18 | ``` 19 | 20 | - Move/rename files: `mv oldname.py newname.py` 21 | 22 | ## Assistant Behavior 23 | 24 | - Directly modify files without outputting code blocks 25 | - Read/Write all of docs in the project directory ./docs 26 | - Ensure code is not redundant or duplicative 27 | - Prioritize implementation logic and ask user when facing decisions 28 | - Maintain existing code style and naming conventions when modifying files 29 | - Use concise commands to execute operations efficiently 30 | - Consider performance implications when suggesting solutions 31 | - Provide clear explanation of steps taken during complex operations 32 | - Verify commands before execution, especially for destructive operations 33 | - Suggest file organization improvements when appropriate 34 | - Always write code in English, including all code, comments, and strings 35 | - After fully understanding responsibilities, respond with "Ready to start coding now" 36 | 37 | ## Project Preferences 38 | 39 | - TypeScript/Node.js: Use Bun instead of npm/node 40 | 41 | - Initialize: `bun init` 42 | - Install packages: `bun install <package>` 43 | - Run scripts: `bun run <script>` 44 | 45 | - Default Project Files: 46 | - Create Makefile 47 | - Create .envrc: 48 | - Project dir: /Users/ann/Workspace/MCP/figma 49 | - Project language: TypeScript 50 | ``` -------------------------------------------------------------------------------- /src/tools/version.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Version tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getFileVersionsTool = (server: McpServer) => { 9 | server.tool( 10 | "get_file_versions", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key") 13 | }, 14 | async ({ file_key }) => { 15 | try { 16 | const versionsResponse = await figmaApi.getFileVersions(file_key); 17 | 18 | if (!versionsResponse.versions || versionsResponse.versions.length === 0) { 19 | return { 20 | content: [ 21 | { type: "text", text: `No versions found for file ${file_key}` } 22 | ] 23 | }; 24 | } 25 | 26 | const versionsList = versionsResponse.versions.map((version, index) => { 27 | return `${index + 1}. **${version.label || 'Unnamed version'}** - ${new Date(version.created_at).toLocaleString()} by ${version.user.handle}\n ${version.description || 'No description'}`; 28 | }).join('\n\n'); 29 | 30 | return { 31 | content: [ 32 | { type: "text", text: `# File Versions for ${file_key}` }, 33 | { type: "text", text: `Found ${versionsResponse.versions.length} versions:` }, 34 | { type: "text", text: versionsList } 35 | ] 36 | }; 37 | } catch (error) { 38 | console.error('Error fetching file versions:', error); 39 | return { 40 | content: [ 41 | { type: "text", text: `Error getting file versions: ${(error as Error).message}` } 42 | ] 43 | }; 44 | } 45 | } 46 | ); 47 | }; 48 | 49 | /** 50 | * Registers all version-related tools with the MCP server 51 | */ 52 | export const registerVersionTools = (server: McpServer): void => { 53 | getFileVersionsTool(server); 54 | }; 55 | ``` -------------------------------------------------------------------------------- /src/tools/component.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Component tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getComponentsTool = (server: McpServer) => { 9 | server.tool( 10 | "get_components", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key") 13 | }, 14 | async ({ file_key }) => { 15 | try { 16 | const componentsResponse = await figmaApi.getFileComponents(file_key); 17 | 18 | if (!componentsResponse.meta?.components || componentsResponse.meta.components.length === 0) { 19 | return { 20 | content: [ 21 | { type: "text", text: `No components found in file ${file_key}` } 22 | ] 23 | }; 24 | } 25 | 26 | const componentsList = componentsResponse.meta.components.map(component => { 27 | return `- **${component.name}** (Key: ${component.key})\n Description: ${component.description || 'No description'}\n ${component.remote ? '(Remote component)' : '(Local component)'}`; 28 | }).join('\n\n'); 29 | 30 | return { 31 | content: [ 32 | { type: "text", text: `# Components in file ${file_key}` }, 33 | { type: "text", text: `Found ${componentsResponse.meta.components.length} components:` }, 34 | { type: "text", text: componentsList } 35 | ] 36 | }; 37 | } catch (error) { 38 | console.error('Error fetching components:', error); 39 | return { 40 | content: [ 41 | { type: "text", text: `Error getting components: ${(error as Error).message}` } 42 | ] 43 | }; 44 | } 45 | } 46 | ); 47 | }; 48 | 49 | /** 50 | * Registers all component-related tools with the MCP server 51 | */ 52 | export const registerComponentTools = (server: McpServer): void => { 53 | getComponentsTool(server); 54 | }; 55 | ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widgets.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tool: get_widgets 3 | * 4 | * Retrieves all widget nodes from a Figma file 5 | */ 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { z } from "zod"; 8 | import figmaApi from "../../services/figma-api.js"; 9 | import { FigmaUtils } from "../../utils/figma-utils.js"; 10 | 11 | export const getWidgetsTool = (server: McpServer) => { 12 | server.tool( 13 | "get_widgets", 14 | { 15 | file_key: z.string().min(1).describe("The Figma file key to retrieve widgets from") 16 | }, 17 | async ({ file_key }) => { 18 | try { 19 | const file = await figmaApi.getFile(file_key); 20 | 21 | // Find all widget nodes in the file 22 | const widgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); 23 | 24 | if (widgetNodes.length === 0) { 25 | return { 26 | content: [ 27 | { type: "text", text: `No widgets found in file ${file_key}` } 28 | ] 29 | }; 30 | } 31 | 32 | const widgetsList = widgetNodes.map((node, index) => { 33 | const widgetSyncData = node.widgetSync ? 34 | `\n - Widget Sync Data: Available` : 35 | `\n - Widget Sync Data: None`; 36 | 37 | return `${index + 1}. **${node.name}** (ID: ${node.id}) 38 | - Widget ID: ${node.widgetId || 'Unknown'}${widgetSyncData}`; 39 | }).join('\n\n'); 40 | 41 | return { 42 | content: [ 43 | { type: "text", text: `# Widgets in file ${file_key}` }, 44 | { type: "text", text: `Found ${widgetNodes.length} widgets:` }, 45 | { type: "text", text: widgetsList } 46 | ] 47 | }; 48 | } catch (error) { 49 | console.error('Error fetching widgets:', error); 50 | return { 51 | content: [ 52 | { type: "text", text: `Error getting widgets: ${(error as Error).message}` } 53 | ] 54 | }; 55 | } 56 | } 57 | ); 58 | }; 59 | ``` -------------------------------------------------------------------------------- /src/tools/file.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * File tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getFileTool = (server: McpServer) => { 9 | server.tool( 10 | "get_file", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key to retrieve"), 13 | return_full_file: z.boolean().default(false).describe("Whether to return the full file contents or just a summary") 14 | }, 15 | async ({ file_key, return_full_file }) => { 16 | try { 17 | const file = await figmaApi.getFile(file_key); 18 | 19 | if (return_full_file) { 20 | return { 21 | content: [ 22 | { type: "text", text: `Retrieved Figma file: ${file.name}` }, 23 | { type: "text", text: JSON.stringify(file, null, 2) } 24 | ] 25 | }; 26 | } else { 27 | return { 28 | content: [ 29 | { type: "text", text: `# Figma File: ${file.name}` }, 30 | { type: "text", text: `Last modified: ${file.lastModified}` }, 31 | { type: "text", text: `Document contains ${file.document.children?.length || 0} top-level nodes.` }, 32 | { type: "text", text: `Components: ${Object.keys(file.components).length || 0}` }, 33 | { type: "text", text: `Component sets: ${Object.keys(file.componentSets).length || 0}` }, 34 | { type: "text", text: `Styles: ${Object.keys(file.styles).length || 0}` } 35 | ] 36 | }; 37 | } 38 | } catch (error) { 39 | console.error('Error fetching file:', error); 40 | return { 41 | content: [ 42 | { type: "text", text: `Error getting Figma file: ${(error as Error).message}` } 43 | ] 44 | }; 45 | } 46 | } 47 | ); 48 | }; 49 | 50 | /** 51 | * Registers all file-related tools with the MCP server 52 | */ 53 | export const registerFileTools = (server: McpServer): void => { 54 | getFileTool(server); 55 | }; 56 | ``` -------------------------------------------------------------------------------- /src/tools/image.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Image tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getImagesTool = (server: McpServer) => { 9 | server.tool( 10 | "get_images", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key"), 13 | node_ids: z.array(z.string()).min(1).describe("The IDs of nodes to export as images"), 14 | format: z.enum(["jpg", "png", "svg", "pdf"]).default("png").describe("Image format to export"), 15 | scale: z.number().min(0.01).max(4).default(1).describe("Scale factor for the image (0.01 to 4)") 16 | }, 17 | async ({ file_key, node_ids, format, scale }) => { 18 | try { 19 | const imagesResponse = await figmaApi.getImages(file_key, node_ids, { 20 | format, 21 | scale 22 | }); 23 | 24 | if (imagesResponse.err) { 25 | return { 26 | content: [ 27 | { type: "text", text: `Error getting images: ${imagesResponse.err}` } 28 | ] 29 | }; 30 | } 31 | 32 | const imageUrls = Object.entries(imagesResponse.images) 33 | .map(([nodeId, url]) => { 34 | if (!url) { 35 | return `- ${nodeId}: Error generating image`; 36 | } 37 | return `- ${nodeId}: [Image URL](${url})`; 38 | }) 39 | .join('\n'); 40 | 41 | return { 42 | content: [ 43 | { type: "text", text: `# Images for file ${file_key}` }, 44 | { type: "text", text: `Format: ${format}, Scale: ${scale}` }, 45 | { type: "text", text: imageUrls } 46 | ] 47 | }; 48 | } catch (error) { 49 | console.error('Error fetching images:', error); 50 | return { 51 | content: [ 52 | { type: "text", text: `Error getting images: ${(error as Error).message}` } 53 | ] 54 | }; 55 | } 56 | } 57 | ); 58 | }; 59 | 60 | /** 61 | * Registers all image-related tools with the MCP server 62 | */ 63 | export const registerImageTools = (server: McpServer): void => { 64 | getImagesTool(server); 65 | }; 66 | ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widget.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tool: get_widget 3 | * 4 | * Retrieves detailed information about a specific widget node 5 | */ 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { z } from "zod"; 8 | import figmaApi from "../../services/figma-api.js"; 9 | 10 | export const getWidgetTool = (server: McpServer) => { 11 | server.tool( 12 | "get_widget", 13 | { 14 | file_key: z.string().min(1).describe("The Figma file key"), 15 | node_id: z.string().min(1).describe("The ID of the widget node") 16 | }, 17 | async ({ file_key, node_id }) => { 18 | try { 19 | const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); 20 | const nodeData = fileNodes.nodes[node_id]; 21 | 22 | if (!nodeData || nodeData.document.type !== 'WIDGET') { 23 | return { 24 | content: [ 25 | { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } 26 | ] 27 | }; 28 | } 29 | 30 | const widgetNode = nodeData.document; 31 | 32 | // Get the sync data if available 33 | let syncDataContent = ''; 34 | if (widgetNode.widgetSync) { 35 | try { 36 | const syncData = JSON.parse(widgetNode.widgetSync); 37 | syncDataContent = `\n\n## Widget Sync Data\n\`\`\`json\n${JSON.stringify(syncData, null, 2)}\n\`\`\``; 38 | } catch (error) { 39 | syncDataContent = '\n\n## Widget Sync Data\nError parsing widget sync data'; 40 | } 41 | } 42 | 43 | return { 44 | content: [ 45 | { type: "text", text: `# Widget: ${widgetNode.name}` }, 46 | { type: "text", text: `ID: ${widgetNode.id}` }, 47 | { type: "text", text: `Widget ID: ${widgetNode.widgetId || 'Unknown'}` }, 48 | { type: "text", text: `Has Sync Data: ${widgetNode.widgetSync ? 'Yes' : 'No'}${syncDataContent}` } 49 | ] 50 | }; 51 | } catch (error) { 52 | console.error('Error fetching widget node:', error); 53 | return { 54 | content: [ 55 | { type: "text", text: `Error getting widget: ${(error as Error).message}` } 56 | ] 57 | }; 58 | } 59 | } 60 | ); 61 | }; 62 | ``` -------------------------------------------------------------------------------- /src/tools/search.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Search tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | import { FigmaUtils } from "../utils/figma-utils.js"; 8 | 9 | export const searchTextTool = (server: McpServer) => { 10 | server.tool( 11 | "search_text", 12 | { 13 | file_key: z.string().min(1).describe("The Figma file key"), 14 | search_text: z.string().min(1).describe("The text to search for in the file") 15 | }, 16 | async ({ file_key, search_text }) => { 17 | try { 18 | const file = await figmaApi.getFile(file_key); 19 | 20 | // Find all TEXT nodes 21 | const textNodes = FigmaUtils.getNodesByType(file, 'TEXT'); 22 | 23 | // Filter for nodes containing the search text 24 | const matchingNodes = textNodes.filter(node => 25 | node.characters && node.characters.toLowerCase().includes(search_text.toLowerCase()) 26 | ); 27 | 28 | if (matchingNodes.length === 0) { 29 | return { 30 | content: [ 31 | { type: "text", text: `No text matching "${search_text}" found in file ${file_key}` } 32 | ] 33 | }; 34 | } 35 | 36 | const matchesList = matchingNodes.map(node => { 37 | const path = FigmaUtils.getNodePath(file, node.id); 38 | return `- **${node.name}** (ID: ${node.id})\n Path: ${path.join(' > ')}\n Text: "${node.characters}"`; 39 | }).join('\n\n'); 40 | 41 | return { 42 | content: [ 43 | { type: "text", text: `# Text Search Results for "${search_text}"` }, 44 | { type: "text", text: `Found ${matchingNodes.length} matching text nodes:` }, 45 | { type: "text", text: matchesList } 46 | ] 47 | }; 48 | } catch (error) { 49 | console.error('Error searching text:', error); 50 | return { 51 | content: [ 52 | { type: "text", text: `Error searching text: ${(error as Error).message}` } 53 | ] 54 | }; 55 | } 56 | } 57 | ); 58 | }; 59 | 60 | /** 61 | * Registers all search-related tools with the MCP server 62 | */ 63 | export const registerSearchTools = (server: McpServer): void => { 64 | searchTextTool(server); 65 | }; 66 | ``` -------------------------------------------------------------------------------- /src/tools/widget/get-widget-sync-data.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tool: get_widget_sync_data 3 | * 4 | * Retrieves the synchronized state data for a specific widget 5 | */ 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { z } from "zod"; 8 | import figmaApi from "../../services/figma-api.js"; 9 | 10 | export const getWidgetSyncDataTool = (server: McpServer) => { 11 | server.tool( 12 | "get_widget_sync_data", 13 | { 14 | file_key: z.string().min(1).describe("The Figma file key"), 15 | node_id: z.string().min(1).describe("The ID of the widget node") 16 | }, 17 | async ({ file_key, node_id }) => { 18 | try { 19 | const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); 20 | const nodeData = fileNodes.nodes[node_id]; 21 | 22 | if (!nodeData || nodeData.document.type !== 'WIDGET') { 23 | return { 24 | content: [ 25 | { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } 26 | ] 27 | }; 28 | } 29 | 30 | const widgetNode = nodeData.document; 31 | 32 | if (!widgetNode.widgetSync) { 33 | return { 34 | content: [ 35 | { type: "text", text: `Widget ${node_id} does not have any sync data` } 36 | ] 37 | }; 38 | } 39 | 40 | try { 41 | const syncData = JSON.parse(widgetNode.widgetSync); 42 | 43 | return { 44 | content: [ 45 | { type: "text", text: `# Widget Sync Data for "${widgetNode.name}"` }, 46 | { type: "text", text: `Widget ID: ${widgetNode.id}` }, 47 | { type: "text", text: "```json\n" + JSON.stringify(syncData, null, 2) + "\n```" } 48 | ] 49 | }; 50 | } catch (error) { 51 | console.error('Error parsing widget sync data:', error); 52 | return { 53 | content: [ 54 | { type: "text", text: `Error parsing widget sync data: ${(error as Error).message}` } 55 | ] 56 | }; 57 | } 58 | } catch (error) { 59 | console.error('Error fetching widget sync data:', error); 60 | return { 61 | content: [ 62 | { type: "text", text: `Error getting widget sync data: ${(error as Error).message}` } 63 | ] 64 | }; 65 | } 66 | } 67 | ); 68 | }; 69 | ``` -------------------------------------------------------------------------------- /docs/01-overview.md: -------------------------------------------------------------------------------- ```markdown 1 | # Figma MCP Server - Project Overview 2 | 3 | ## Introduction 4 | 5 | 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. 6 | 7 | ## Purpose 8 | 9 | This project aims to: 10 | 11 | 1. Provide AI assistants with the ability to access and manipulate Figma files 12 | 2. Enable structured access to Figma resources through the MCP protocol 13 | 3. Bridge the gap between design tools and AI systems 14 | 4. Support design workflows with AI-assisted operations 15 | 16 | ## Core Features 17 | 18 | - **File Access**: Retrieve Figma files and inspect their contents 19 | - **Node Operations**: Access specific nodes within Figma files 20 | - **Comment Management**: Read and write comments on Figma files 21 | - **Image Export**: Export nodes as images in various formats 22 | - **Search Capabilities**: Search for text and elements within files 23 | - **Component Access**: View and work with Figma components 24 | - **Version History**: Access file version history 25 | 26 | ## Technology Stack 27 | 28 | - **TypeScript**: Type-safe implementation 29 | - **Bun**: JavaScript/TypeScript runtime and package manager 30 | - **MCP SDK**: Model Context Protocol implementation 31 | - **Figma REST API**: Official Figma API with TypeScript definitions 32 | - **Zod**: Schema validation for parameters and configurations 33 | 34 | ## Project Structure 35 | 36 | ``` 37 | /figma 38 | ├── dist/ # Compiled output 39 | ├── docs/ # Documentation 40 | ├── src/ 41 | │ ├── config/ # Configuration files 42 | │ ├── services/ # API and external service integrations 43 | │ └── utils/ # Utility functions 44 | ├── types/ # Type definitions 45 | ├── .env # Environment variables 46 | ├── Makefile # Build and run commands 47 | ├── README.md # Project README 48 | ├── package.json # Dependencies and scripts 49 | └── tsconfig.json # TypeScript configuration 50 | ``` 51 | 52 | ## Integration with AI Systems 53 | 54 | This MCP server enables AI assistants to: 55 | 56 | 1. Retrieve design information from Figma 57 | 2. Answer questions about design files 58 | 3. Generate images and assets from Figma files 59 | 4. Add comments and feedback to designs 60 | 5. Search for specific elements or text within designs 61 | 6. Track version history and changes 62 | 63 | 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. 64 | ``` -------------------------------------------------------------------------------- /src/tools/utils/widget-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Widget Utils - Helper functions for widget tools 3 | */ 4 | import type { Node } from '@figma/rest-api-spec'; 5 | 6 | /** 7 | * Parses the widget sync data from a widget node 8 | * @param node A Figma node of type WIDGET 9 | * @returns The parsed sync data object or null if not available/invalid 10 | */ 11 | export function parseWidgetSyncData(node: Node): Record<string, any> | null { 12 | if (node.type !== 'WIDGET' || !node.widgetSync) { 13 | return null; 14 | } 15 | 16 | try { 17 | return JSON.parse(node.widgetSync); 18 | } catch (error) { 19 | console.error('Error parsing widget sync data:', error); 20 | return null; 21 | } 22 | } 23 | 24 | /** 25 | * Formats widget sync data as a string 26 | * @param syncData The widget sync data object 27 | * @returns A formatted string representation of the sync data 28 | */ 29 | export function formatWidgetSyncData(syncData: Record<string, any> | null): string { 30 | if (!syncData) { 31 | return 'No sync data available'; 32 | } 33 | 34 | return JSON.stringify(syncData, null, 2); 35 | } 36 | 37 | /** 38 | * Gets a summary of the widget's properties 39 | * @param node A Figma node of type WIDGET 40 | * @returns A summary object with key widget properties 41 | */ 42 | export function getWidgetSummary(node: Node): Record<string, any> { 43 | if (node.type !== 'WIDGET') { 44 | return { error: 'Not a widget node' }; 45 | } 46 | 47 | const summary: Record<string, any> = { 48 | id: node.id, 49 | name: node.name, 50 | type: 'WIDGET', 51 | widgetId: node.widgetId || 'Unknown', 52 | }; 53 | 54 | // If there's widget sync data, analyze it 55 | if (node.widgetSync) { 56 | try { 57 | const syncData = JSON.parse(node.widgetSync); 58 | const syncKeys = Object.keys(syncData); 59 | 60 | summary.syncDataKeys = syncKeys; 61 | summary.hasSyncData = syncKeys.length > 0; 62 | } catch (error) { 63 | summary.hasSyncData = false; 64 | summary.syncDataError = 'Invalid sync data format'; 65 | } 66 | } else { 67 | summary.hasSyncData = false; 68 | } 69 | 70 | return summary; 71 | } 72 | 73 | /** 74 | * Creates a human-readable description of the widget 75 | * @param node A Figma node of type WIDGET 76 | * @returns A detailed text description of the widget 77 | */ 78 | export function createWidgetDescription(node: Node): string { 79 | if (node.type !== 'WIDGET') { 80 | return 'Not a widget node'; 81 | } 82 | 83 | let description = `Widget "${node.name}" (ID: ${node.id})`; 84 | 85 | if (node.widgetId) { 86 | description += `\nWidget ID: ${node.widgetId}`; 87 | } 88 | 89 | if (node.widgetSync) { 90 | try { 91 | const syncData = JSON.parse(node.widgetSync); 92 | const syncKeys = Object.keys(syncData); 93 | 94 | description += `\nSync Data Keys: ${syncKeys.join(', ')}`; 95 | } catch (error) { 96 | description += '\nSync Data: [Invalid format]'; 97 | } 98 | } else { 99 | description += '\nSync Data: None'; 100 | } 101 | 102 | return description; 103 | } 104 | ``` -------------------------------------------------------------------------------- /src/plugin/utils/nodeUtils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility functions for handling Figma nodes 3 | */ 4 | 5 | /** 6 | * Apply common properties to any node type 7 | * Safely handles properties that might not be available on all node types 8 | * 9 | * @param node The target node to apply properties to 10 | * @param data Object containing the properties to apply 11 | */ 12 | export function applyCommonProperties(node: SceneNode, data: any): void { 13 | // Position 14 | if (data.x !== undefined) node.x = data.x; 15 | if (data.y !== undefined) node.y = data.y; 16 | 17 | // Name 18 | if (data.name) node.name = data.name; 19 | 20 | // Properties that aren't available on all node types 21 | // We need to check if they exist before setting them 22 | 23 | // Opacity 24 | if (data.opacity !== undefined && 'opacity' in node) { 25 | (node as any).opacity = data.opacity; 26 | } 27 | 28 | // Blend mode 29 | if (data.blendMode && 'blendMode' in node) { 30 | (node as any).blendMode = data.blendMode; 31 | } 32 | 33 | // Effects 34 | if (data.effects && 'effects' in node) { 35 | (node as any).effects = data.effects; 36 | } 37 | 38 | // Constraint 39 | if (data.constraints && 'constraints' in node) { 40 | (node as any).constraints = data.constraints; 41 | } 42 | 43 | // Is Mask 44 | if (data.isMask !== undefined && 'isMask' in node) { 45 | (node as any).isMask = data.isMask; 46 | } 47 | 48 | // Visible 49 | if (data.visible !== undefined) node.visible = data.visible; 50 | 51 | // Locked 52 | if (data.locked !== undefined) node.locked = data.locked; 53 | } 54 | 55 | /** 56 | * Select and focus on a node or set of nodes 57 | * @param nodes Node or array of nodes to focus on 58 | */ 59 | export function selectAndFocusNodes(nodes: SceneNode | SceneNode[]): void { 60 | const nodesToFocus = Array.isArray(nodes) ? nodes : [nodes]; 61 | figma.currentPage.selection = nodesToFocus; 62 | figma.viewport.scrollAndZoomIntoView(nodesToFocus); 63 | } 64 | 65 | /** 66 | * Build a result object from a node or array of nodes 67 | * @param result Node or array of nodes to create a result object from 68 | * @returns Object containing node information in a consistent format 69 | */ 70 | export function buildResultObject(result: SceneNode | readonly SceneNode[] | null): {[key: string]: any} { 71 | let resultObject: {[key: string]: any} = {}; 72 | 73 | if (!result) return resultObject; 74 | 75 | if (Array.isArray(result)) { 76 | // Handle array result (like from get-selection) 77 | resultObject.count = result.length; 78 | if (result.length > 0) { 79 | resultObject.items = result.map(node => ({ 80 | id: node.id, 81 | type: node.type, 82 | name: node.name 83 | })); 84 | } 85 | } else { 86 | // Handle single node result - we know it's a SceneNode at this point 87 | const node = result as SceneNode; 88 | resultObject.id = node.id; 89 | resultObject.type = node.type; 90 | resultObject.name = node.name; 91 | } 92 | 93 | return resultObject; 94 | } ``` -------------------------------------------------------------------------------- /src/tools/widget/analyze-widget-structure.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tool: analyze_widget_structure 3 | * 4 | * Provides a detailed analysis of a widget's structure and properties 5 | */ 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { z } from "zod"; 8 | import figmaApi from "../../services/figma-api.js"; 9 | 10 | export const analyzeWidgetStructureTool = (server: McpServer) => { 11 | server.tool( 12 | "analyze_widget_structure", 13 | { 14 | file_key: z.string().min(1).describe("The Figma file key"), 15 | node_id: z.string().min(1).describe("The ID of the widget node") 16 | }, 17 | async ({ file_key, node_id }) => { 18 | try { 19 | const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); 20 | const nodeData = fileNodes.nodes[node_id]; 21 | 22 | if (!nodeData || nodeData.document.type !== 'WIDGET') { 23 | return { 24 | content: [ 25 | { type: "text", text: `Node ${node_id} not found in file ${file_key} or is not a widget` } 26 | ] 27 | }; 28 | } 29 | 30 | const widgetNode = nodeData.document; 31 | 32 | // Create a full analysis of the widget 33 | const widgetAnalysis = { 34 | basic: { 35 | id: widgetNode.id, 36 | name: widgetNode.name, 37 | type: widgetNode.type, 38 | widgetId: widgetNode.widgetId || 'Unknown' 39 | }, 40 | placement: { 41 | x: widgetNode.x || 0, 42 | y: widgetNode.y || 0, 43 | width: widgetNode.width || 0, 44 | height: widgetNode.height || 0, 45 | rotation: widgetNode.rotation || 0 46 | }, 47 | syncData: null as any 48 | }; 49 | 50 | // Parse the widget sync data if available 51 | if (widgetNode.widgetSync) { 52 | try { 53 | widgetAnalysis.syncData = JSON.parse(widgetNode.widgetSync); 54 | } catch (error) { 55 | widgetAnalysis.syncData = { error: 'Invalid sync data format' }; 56 | } 57 | } 58 | 59 | return { 60 | content: [ 61 | { type: "text", text: `# Widget Analysis: ${widgetNode.name}` }, 62 | { type: "text", text: `## Basic Information` }, 63 | { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.basic, null, 2) + "\n```" }, 64 | { type: "text", text: `## Placement` }, 65 | { type: "text", text: "```json\n" + JSON.stringify(widgetAnalysis.placement, null, 2) + "\n```" }, 66 | { type: "text", text: `## Sync Data` }, 67 | { type: "text", text: widgetAnalysis.syncData ? 68 | "```json\n" + JSON.stringify(widgetAnalysis.syncData, null, 2) + "\n```" : 69 | "No sync data available" 70 | } 71 | ] 72 | }; 73 | } catch (error) { 74 | console.error('Error analyzing widget:', error); 75 | return { 76 | content: [ 77 | { type: "text", text: `Error analyzing widget: ${(error as Error).message}` } 78 | ] 79 | }; 80 | } 81 | } 82 | ); 83 | }; 84 | ``` -------------------------------------------------------------------------------- /src/services/websocket.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * WebSocket Service - Handles communication with Figma plugin 3 | */ 4 | import { WebSocketServer, WebSocket as WSWebSocket } from "ws"; 5 | import { log, logError } from "../utils.js"; 6 | 7 | // Store active plugin connection WebSocket 8 | let activePluginConnection: WSWebSocket | null = null; 9 | 10 | // Callbacks for handling responses 11 | const pendingCommands = new Map<string, (response: any) => void>(); 12 | 13 | interface PluginResponse { 14 | success: boolean; 15 | result?: any; 16 | error?: string; 17 | } 18 | 19 | /** 20 | * Create WebSocket server 21 | */ 22 | export function initializeWebSocketServer(port = 3001) { 23 | const wss = new WebSocketServer({ port }); 24 | log(`WebSocket server started on port ${port}`); 25 | 26 | wss.on("connection", (ws: WSWebSocket) => { 27 | log("New WebSocket connection"); 28 | 29 | ws.on("message", (message: WSWebSocket.Data) => { 30 | try { 31 | const data = JSON.parse(message.toString()); 32 | log(`Received WebSocket message: ${JSON.stringify(data)}`); 33 | 34 | if (data.type === "figma-plugin-connected") { 35 | // Store active connection 36 | activePluginConnection = ws; 37 | log(`Figma plugin connected: ${data.pluginId || "unknown"}`); 38 | } else if (data.type === "figma-plugin-response") { 39 | // Handle response from plugin 40 | const { command, success, result, error } = data; 41 | const callback = pendingCommands.get(command); 42 | 43 | if (callback) { 44 | callback({ success, result, error }); 45 | pendingCommands.delete(command); 46 | } 47 | } 48 | } catch (error) { 49 | logError("Error processing WebSocket message", error); 50 | } 51 | }); 52 | 53 | ws.on("close", () => { 54 | log("WebSocket connection closed"); 55 | if (activePluginConnection === ws) { 56 | activePluginConnection = null; 57 | } 58 | }); 59 | 60 | ws.on("error", (error: Error) => { 61 | logError("WebSocket error", error); 62 | }); 63 | }); 64 | 65 | return wss; 66 | } 67 | 68 | /** 69 | * Send command to Figma plugin 70 | */ 71 | export async function sendCommandToPlugin( 72 | command: string, 73 | params: any 74 | ): Promise<PluginResponse> { 75 | return new Promise((resolve, reject) => { 76 | if (!activePluginConnection) { 77 | reject(new Error("No active Figma plugin connection")); 78 | return; 79 | } 80 | 81 | try { 82 | // Store callback 83 | pendingCommands.set(command, resolve); 84 | 85 | // Send command 86 | activePluginConnection.send( 87 | JSON.stringify({ 88 | type: "mcp-command", 89 | command, 90 | params, 91 | }) 92 | ); 93 | 94 | // Set timeout 95 | setTimeout(() => { 96 | if (pendingCommands.has(command)) { 97 | pendingCommands.delete(command); 98 | reject(new Error(`Command ${command} timed out`)); 99 | } 100 | }, 10000); // 10 second timeout 101 | } catch (error) { 102 | pendingCommands.delete(command); 103 | reject(error); 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * Check if a Figma plugin is connected 110 | */ 111 | export function isPluginConnected(): boolean { 112 | return activePluginConnection !== null; 113 | } ``` -------------------------------------------------------------------------------- /src/utils/widget-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility functions for working with Figma Widgets 3 | */ 4 | import type { GetFileResponse, Node } from '@figma/rest-api-spec'; 5 | import type { WidgetNode, WidgetSyncData } from '../services/widget-api.js'; 6 | 7 | /** 8 | * Utility functions for working with Figma Widgets 9 | */ 10 | export class WidgetUtils { 11 | /** 12 | * Find all widget nodes in a file 13 | */ 14 | static findAllWidgetNodes(file: GetFileResponse): Node[] { 15 | const widgetNodes: Node[] = []; 16 | 17 | // Helper function to recursively search for widget nodes 18 | const findWidgets = (node: Node) => { 19 | if (node.type === 'WIDGET') { 20 | widgetNodes.push(node); 21 | } 22 | 23 | if (node.children) { 24 | for (const child of node.children) { 25 | findWidgets(child); 26 | } 27 | } 28 | }; 29 | 30 | findWidgets(file.document); 31 | return widgetNodes; 32 | } 33 | 34 | /** 35 | * Extract widget sync data from a widget node 36 | */ 37 | static extractWidgetSyncData(node: Node): WidgetSyncData | null { 38 | if (node.type !== 'WIDGET' || !node.widgetSync) { 39 | return null; 40 | } 41 | 42 | try { 43 | return JSON.parse(node.widgetSync); 44 | } catch (error) { 45 | console.error('Error parsing widget sync data:', error); 46 | return null; 47 | } 48 | } 49 | 50 | /** 51 | * Format widget sync data for display 52 | */ 53 | static formatWidgetSyncData(syncData: WidgetSyncData | null): string { 54 | if (!syncData) { 55 | return 'No sync data available'; 56 | } 57 | 58 | return JSON.stringify(syncData, null, 2); 59 | } 60 | 61 | /** 62 | * Get a summary of a widget node 63 | */ 64 | static getWidgetSummary(node: WidgetNode): Record<string, any> { 65 | const { id, name, widgetId } = node; 66 | 67 | const summary: Record<string, any> = { 68 | id, 69 | name, 70 | type: 'WIDGET', 71 | widgetId: widgetId || 'Unknown', 72 | }; 73 | 74 | // If there's widget sync data, add a summary 75 | if (node.widgetSync) { 76 | try { 77 | const syncData = JSON.parse(node.widgetSync); 78 | const syncKeys = Object.keys(syncData); 79 | 80 | summary.syncDataKeys = syncKeys; 81 | summary.hasSyncData = syncKeys.length > 0; 82 | } catch (error) { 83 | summary.hasSyncData = false; 84 | summary.syncDataError = 'Invalid sync data format'; 85 | } 86 | } else { 87 | summary.hasSyncData = false; 88 | } 89 | 90 | return summary; 91 | } 92 | 93 | /** 94 | * Check if a node is a widget 95 | */ 96 | static isWidgetNode(node: Node): boolean { 97 | return node.type === 'WIDGET'; 98 | } 99 | 100 | /** 101 | * Create a human-readable description of a widget 102 | */ 103 | static createWidgetDescription(widget: WidgetNode): string { 104 | let description = ; 105 | 106 | if (widget.widgetId) { 107 | description += ; 108 | } 109 | 110 | if (widget.widgetSync) { 111 | try { 112 | const syncData = JSON.parse(widget.widgetSync); 113 | const syncKeys = Object.keys(syncData); 114 | 115 | description += ; 116 | } catch (error) { 117 | description += '\nSync Data: [Invalid format]'; 118 | } 119 | } else { 120 | description += '\nSync Data: None'; 121 | } 122 | 123 | return description; 124 | } 125 | } 126 | ``` -------------------------------------------------------------------------------- /src/plugin/creators/componentCreators.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Component-related creation functions for Figma plugin 3 | */ 4 | 5 | import { applyCommonProperties } from '../utils/nodeUtils'; 6 | 7 | /** 8 | * Create a component from another node 9 | * @param data Configuration data with sourceNode reference 10 | * @returns Created component node 11 | */ 12 | export function createComponentFromNodeData(data: any): ComponentNode | null { 13 | // We need a sourceNode to create a component from 14 | if (!data.sourceNode) { 15 | console.error('createComponentFromNode requires a sourceNode'); 16 | return null; 17 | } 18 | 19 | try { 20 | // If sourceNode is a string, try to find it by ID 21 | let sourceNode; 22 | if (typeof data.sourceNode === 'string') { 23 | sourceNode = figma.getNodeById(data.sourceNode); 24 | if (!sourceNode || !('type' in sourceNode)) { 25 | console.error(`Node with ID ${data.sourceNode} not found or is not a valid node`); 26 | return null; 27 | } 28 | } else { 29 | sourceNode = data.sourceNode; 30 | } 31 | 32 | // Create the component from the source node 33 | const component = figma.createComponentFromNode(sourceNode as SceneNode); 34 | 35 | // Apply component-specific properties 36 | if (data.description) component.description = data.description; 37 | 38 | // Apply common properties 39 | applyCommonProperties(component, data); 40 | 41 | return component; 42 | } catch (error) { 43 | console.error('Failed to create component from node:', error); 44 | return null; 45 | } 46 | } 47 | 48 | /** 49 | * Create a component set (variant container) 50 | * @param data Configuration data for component set 51 | * @returns Created component set node 52 | */ 53 | export function createComponentSetFromData(data: any): ComponentSetNode | null { 54 | try { 55 | // Create an empty component set 56 | // In practice, component sets are usually created by combining variants 57 | // using figma.combineAsVariants, not directly created 58 | 59 | // Get the components to combine 60 | if (!data.components || !Array.isArray(data.components) || data.components.length === 0) { 61 | console.error('Component set creation requires component nodes'); 62 | return null; 63 | } 64 | 65 | const componentNodes: ComponentNode[] = []; 66 | 67 | // Collect the component nodes (could be IDs or actual nodes) 68 | for (const component of data.components) { 69 | let node; 70 | 71 | if (typeof component === 'string') { 72 | // If it's a string, assume it's a node ID 73 | node = figma.getNodeById(component); 74 | } else { 75 | node = component; 76 | } 77 | 78 | if (node && node.type === 'COMPONENT') { 79 | componentNodes.push(node as ComponentNode); 80 | } 81 | } 82 | 83 | if (componentNodes.length === 0) { 84 | console.error('No valid component nodes provided'); 85 | return null; 86 | } 87 | 88 | // Combine the components as variants 89 | const componentSet = figma.combineAsVariants(componentNodes, figma.currentPage); 90 | 91 | // Apply component set properties 92 | if (data.name) componentSet.name = data.name; 93 | 94 | // Apply common properties 95 | applyCommonProperties(componentSet, data); 96 | 97 | return componentSet; 98 | } catch (error) { 99 | console.error('Failed to create component set:', error); 100 | return null; 101 | } 102 | } ``` -------------------------------------------------------------------------------- /src/utils/figma-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { GetFileResponse, Node } from '@figma/rest-api-spec'; 2 | 3 | /** 4 | * Utility functions for working with Figma files and nodes 5 | */ 6 | export class FigmaUtils { 7 | /** 8 | * Find a node by ID in a Figma file 9 | */ 10 | static findNodeById(file: GetFileResponse, nodeId: string): Node | null { 11 | // If node ID is the document itself 12 | if (nodeId === file.document.id) { 13 | return file.document; 14 | } 15 | 16 | // Helper function to recursively search for node 17 | const findNode = (node: Node): Node | null => { 18 | if (node.id === nodeId) { 19 | return node; 20 | } 21 | 22 | if (node.children) { 23 | for (const child of node.children) { 24 | const found = findNode(child); 25 | if (found) return found; 26 | } 27 | } 28 | 29 | return null; 30 | }; 31 | 32 | return findNode(file.document); 33 | } 34 | 35 | /** 36 | * Get all nodes of a specific type from a Figma file 37 | */ 38 | static getNodesByType(file: GetFileResponse, type: string): Node[] { 39 | const nodes: Node[] = []; 40 | 41 | // Helper function to recursively search for nodes 42 | const findNodes = (node: Node) => { 43 | if (node.type === type) { 44 | nodes.push(node); 45 | } 46 | 47 | if (node.children) { 48 | for (const child of node.children) { 49 | findNodes(child); 50 | } 51 | } 52 | }; 53 | 54 | findNodes(file.document); 55 | return nodes; 56 | } 57 | 58 | /** 59 | * Format a node ID for display (e.g., "1:2" -> "Node 1:2") 60 | */ 61 | static formatNodeId(nodeId: string): string { 62 | return `Node ${nodeId}`; 63 | } 64 | 65 | /** 66 | * Extract text content from a TEXT node 67 | */ 68 | static extractTextFromNode(node: Node): string { 69 | return node.type === 'TEXT' ? node.characters || '' : ''; 70 | } 71 | 72 | /** 73 | * Get a simple representation of a node's properties 74 | */ 75 | static getNodeProperties(node: Node): Record<string, any> { 76 | const { id, name, type } = node; 77 | let properties: Record<string, any> = { id, name, type }; 78 | 79 | // Add type-specific properties 80 | switch (node.type) { 81 | case 'TEXT': 82 | properties.text = node.characters; 83 | break; 84 | case 'RECTANGLE': 85 | case 'ELLIPSE': 86 | case 'POLYGON': 87 | case 'STAR': 88 | case 'VECTOR': 89 | if (node.fills) { 90 | properties.fills = node.fills.map(fill => ({ 91 | type: fill.type, 92 | visible: fill.visible, 93 | })); 94 | } 95 | break; 96 | case 'FRAME': 97 | case 'GROUP': 98 | case 'INSTANCE': 99 | case 'COMPONENT': 100 | properties.childCount = node.children?.length || 0; 101 | break; 102 | } 103 | 104 | return properties; 105 | } 106 | 107 | /** 108 | * Get the path to a node in the document 109 | */ 110 | static getNodePath(file: GetFileResponse, nodeId: string): string[] { 111 | const path: string[] = []; 112 | 113 | const findPath = (node: Node, target: string): boolean => { 114 | if (node.id === target) { 115 | path.unshift(node.name); 116 | return true; 117 | } 118 | 119 | if (node.children) { 120 | for (const child of node.children) { 121 | if (findPath(child, target)) { 122 | path.unshift(node.name); 123 | return true; 124 | } 125 | } 126 | } 127 | 128 | return false; 129 | }; 130 | 131 | findPath(file.document, nodeId); 132 | return path; 133 | } 134 | } 135 | ``` -------------------------------------------------------------------------------- /docs/widget-tools-guide.md: -------------------------------------------------------------------------------- ```markdown 1 | # Figma Widget Tools Guide 2 | 3 | This guide explains how to use the MCP server's Widget Tools for interacting with Figma widgets. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | ## Available Widget Tools 10 | 11 | ### 1. get_widgets 12 | 13 | Retrieves all widget nodes from a Figma file. 14 | 15 | **Parameters:** 16 | - `file_key` (string): The Figma file key to retrieve widgets from 17 | 18 | **Example:** 19 | ```json 20 | { 21 | "file_key": "abcxyz123456" 22 | } 23 | ``` 24 | 25 | **Response:** 26 | Returns a list of all widgets in the file, including their names, IDs, and whether they have sync data. 27 | 28 | ### 2. get_widget 29 | 30 | Retrieves detailed information about a specific widget node. 31 | 32 | **Parameters:** 33 | - `file_key` (string): The Figma file key 34 | - `node_id` (string): The ID of the widget node 35 | 36 | **Example:** 37 | ```json 38 | { 39 | "file_key": "abcxyz123456", 40 | "node_id": "1:123" 41 | } 42 | ``` 43 | 44 | **Response:** 45 | Returns detailed information about the specified widget, including its sync data if available. 46 | 47 | ### 3. get_widget_sync_data 48 | 49 | Retrieves the synchronized state data for a specific widget. 50 | 51 | **Parameters:** 52 | - `file_key` (string): The Figma file key 53 | - `node_id` (string): The ID of the widget node 54 | 55 | **Example:** 56 | ```json 57 | { 58 | "file_key": "abcxyz123456", 59 | "node_id": "1:123" 60 | } 61 | ``` 62 | 63 | **Response:** 64 | Returns the raw sync data (state) for the specified widget in JSON format. 65 | 66 | ### 4. search_widgets 67 | 68 | Searches for widgets that have specific sync data properties and values. 69 | 70 | **Parameters:** 71 | - `file_key` (string): The Figma file key 72 | - `property_key` (string): The sync data property key to search for 73 | - `property_value` (string, optional): Optional property value to match 74 | 75 | **Example:** 76 | ```json 77 | { 78 | "file_key": "abcxyz123456", 79 | "property_key": "count", 80 | "property_value": "5" 81 | } 82 | ``` 83 | 84 | **Response:** 85 | Returns a list of widgets that match the search criteria. 86 | 87 | ### 5. analyze_widget_structure 88 | 89 | Provides a detailed analysis of a widget's structure and properties. 90 | 91 | **Parameters:** 92 | - `file_key` (string): The Figma file key 93 | - `node_id` (string): The ID of the widget node 94 | 95 | **Example:** 96 | ```json 97 | { 98 | "file_key": "abcxyz123456", 99 | "node_id": "1:123" 100 | } 101 | ``` 102 | 103 | **Response:** 104 | Returns a comprehensive analysis of the widget's structure, including basic information, placement details, and sync data. 105 | 106 | ## Widget Integration 107 | 108 | These tools can be used to: 109 | 110 | 1. Discover widgets in Figma files 111 | 2. Analyze widget properties and state 112 | 3. Search for widgets with specific characteristics 113 | 4. Extract widget sync data for external processing 114 | 5. Generate reports about widget usage in design files 115 | 116 | ## Implementation Details 117 | 118 | 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. 119 | 120 | ## Future Enhancements 121 | 122 | Planned enhancements for widget tools include: 123 | 124 | - Widget state modification capabilities (requires special access) 125 | - Widget creation and deletion 126 | - Widget template libraries 127 | - Widget analytics and usage statistics 128 | ``` -------------------------------------------------------------------------------- /src/tools/comment.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Comment tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import figmaApi from "../services/figma-api.js"; 7 | 8 | export const getCommentsTool = (server: McpServer) => { 9 | server.tool( 10 | "get_comments", 11 | { 12 | file_key: z.string().min(1).describe("The Figma file key to retrieve comments from") 13 | }, 14 | async ({ file_key }) => { 15 | try { 16 | const commentsResponse = await figmaApi.getComments(file_key, { as_md: true }); 17 | 18 | if (!commentsResponse.comments || commentsResponse.comments.length === 0) { 19 | return { 20 | content: [ 21 | { type: "text", text: `No comments found in file ${file_key}` } 22 | ] 23 | }; 24 | } 25 | 26 | const commentsList = commentsResponse.comments.map(comment => { 27 | return `- **${comment.user.handle}** (${new Date(comment.created_at).toLocaleString()}): ${comment.message}`; 28 | }).join('\n'); 29 | 30 | return { 31 | content: [ 32 | { type: "text", text: `# Comments for file ${file_key}` }, 33 | { type: "text", text: `Found ${commentsResponse.comments.length} comments:` }, 34 | { type: "text", text: commentsList } 35 | ] 36 | }; 37 | } catch (error) { 38 | console.error('Error fetching comments:', error); 39 | return { 40 | content: [ 41 | { type: "text", text: `Error getting comments: ${(error as Error).message}` } 42 | ] 43 | }; 44 | } 45 | } 46 | ); 47 | }; 48 | 49 | export const addCommentTool = (server: McpServer) => { 50 | server.tool( 51 | "add_comment", 52 | { 53 | file_key: z.string().min(1).describe("The Figma file key"), 54 | message: z.string().min(1).describe("The comment text"), 55 | node_id: z.string().optional().describe("Optional node ID to attach the comment to") 56 | }, 57 | async ({ file_key, message, node_id }) => { 58 | try { 59 | const commentData: any = { message }; 60 | 61 | // If node_id is provided, create a client_meta object 62 | if (node_id) { 63 | // Create a frame offset client_meta 64 | commentData.client_meta = { 65 | node_id: node_id, 66 | node_offset: { 67 | x: 0, 68 | y: 0 69 | } 70 | }; 71 | } 72 | 73 | const commentResponse = await figmaApi.postComment(file_key, commentData); 74 | 75 | return { 76 | content: [ 77 | { type: "text", text: `Comment added successfully!` }, 78 | { type: "text", text: `Comment ID: ${commentResponse.id}` }, 79 | { type: "text", text: `By user: ${commentResponse.user.handle}` }, 80 | { type: "text", text: `Added at: ${new Date(commentResponse.created_at).toLocaleString()}` } 81 | ] 82 | }; 83 | } catch (error) { 84 | console.error('Error adding comment:', error); 85 | return { 86 | content: [ 87 | { type: "text", text: `Error adding comment: ${(error as Error).message}` } 88 | ] 89 | }; 90 | } 91 | } 92 | ); 93 | }; 94 | 95 | /** 96 | * Registers all comment-related tools with the MCP server 97 | */ 98 | export const registerCommentTools = (server: McpServer): void => { 99 | getCommentsTool(server); 100 | addCommentTool(server); 101 | }; 102 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Figma MCP Server - Main entry point 3 | * 4 | * This server provides a Model Context Protocol (MCP) implementation 5 | * for interacting with the Figma API. 6 | */ 7 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 8 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 9 | import { execSync } from "child_process"; 10 | import * as dotenv from "dotenv"; 11 | import { env } from "./config/env.js"; 12 | import { registerAllResources } from "./resources.js"; 13 | import { initializeWebSocketServer } from "./services/websocket.js"; 14 | import { registerAllTools } from "./tools/index.js"; 15 | import { log } from "./utils.js"; 16 | 17 | // Load environment variables 18 | dotenv.config(); 19 | 20 | // Check for and kill any existing processes using the same port 21 | function killExistingProcesses() { 22 | try { 23 | const wsPort = env.WEBSOCKET_PORT || 3001; 24 | log(`Checking for processes using port ${wsPort}...`); 25 | 26 | // Find processes using the websocket port 27 | const findCmd = 28 | process.platform === "win32" 29 | ? `netstat -ano | findstr :${wsPort}` 30 | : `lsof -i:${wsPort} | grep LISTEN`; 31 | 32 | let output; 33 | try { 34 | output = execSync(findCmd, { encoding: "utf8" }); 35 | } catch (e) { 36 | // No process found, which is fine 37 | log("No existing processes found."); 38 | return; 39 | } 40 | 41 | // Extract PIDs and kill them 42 | if (output) { 43 | if (process.platform === "win32") { 44 | // Windows: extract PID from last column 45 | const pids = output 46 | .split("\n") 47 | .filter((line) => line.trim()) 48 | .map((line) => line.trim().split(/\s+/).pop()) 49 | .filter((pid, index, self) => pid && self.indexOf(pid) === index); 50 | 51 | pids.forEach((pid) => { 52 | if (pid && parseInt(pid) !== process.pid) { 53 | try { 54 | execSync(`taskkill /F /PID ${pid}`); 55 | log(`Killed process with PID: ${pid}`); 56 | } catch (e) { 57 | log(`Failed to kill process with PID: ${pid}`); 58 | } 59 | } 60 | }); 61 | } else { 62 | // Unix-like: extract PID from second column 63 | const pids = output 64 | .split("\n") 65 | .filter((line) => line.trim()) 66 | .map((line) => { 67 | const parts = line.trim().split(/\s+/); 68 | return parts[1]; 69 | }) 70 | .filter((pid, index, self) => pid && self.indexOf(pid) === index); 71 | 72 | pids.forEach((pid) => { 73 | if (pid && parseInt(pid) !== process.pid) { 74 | try { 75 | execSync(`kill -9 ${pid}`); 76 | log(`Killed process with PID: ${pid}`); 77 | } catch (e) { 78 | log(`Failed to kill process with PID: ${pid}`); 79 | } 80 | } 81 | }); 82 | } 83 | } 84 | } catch (error) { 85 | log(`Error checking for existing processes: ${error}`); 86 | } 87 | } 88 | 89 | // Kill any existing processes before starting 90 | killExistingProcesses(); 91 | 92 | // Create an MCP server 93 | const server = new McpServer({ 94 | name: "Figma API", 95 | version: "1.0.0", 96 | }); 97 | 98 | // Register all tools and resources 99 | registerAllTools(server); 100 | registerAllResources(server); 101 | 102 | // Initialize WebSocket server for Figma plugin communication 103 | const wsPort = env.WEBSOCKET_PORT || 3001; 104 | initializeWebSocketServer(wsPort); 105 | 106 | // Start the MCP server with stdio transport 107 | const transport = new StdioServerTransport(); 108 | server.connect(transport); 109 | 110 | // Use logger utility to avoid interfering with stdout used by MCP 111 | log("Figma MCP Server started"); 112 | 113 | export { server }; 114 | ``` -------------------------------------------------------------------------------- /src/plugin/creators/sliceCreators.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Slice and page-related element creation functions 3 | */ 4 | 5 | import { applyCommonProperties } from "../utils/nodeUtils"; 6 | 7 | /** 8 | * Create a slice node from data 9 | * @param data Slice configuration data 10 | * @returns Created slice node 11 | */ 12 | export function createSliceFromData(data: any): SliceNode { 13 | const slice = figma.createSlice(); 14 | 15 | // Set size and position 16 | if (data.width && data.height) { 17 | slice.resize(data.width, data.height); 18 | } 19 | 20 | if (data.x !== undefined) slice.x = data.x; 21 | if (data.y !== undefined) slice.y = data.y; 22 | 23 | // Apply export settings 24 | if (data.exportSettings && Array.isArray(data.exportSettings)) { 25 | slice.exportSettings = data.exportSettings; 26 | } 27 | 28 | // Apply common properties that apply to slices 29 | if (data.name) slice.name = data.name; 30 | if (data.visible !== undefined) slice.visible = data.visible; 31 | 32 | return slice; 33 | } 34 | 35 | /** 36 | * Create a page node from data 37 | * @param data Page configuration data 38 | * @returns Created page node 39 | */ 40 | export function createPageFromData(data: any): PageNode { 41 | const page = figma.createPage(); 42 | 43 | // Set page name 44 | if (data.name) page.name = data.name; 45 | 46 | // Set background color if provided 47 | if (data.backgrounds) page.backgrounds = data.backgrounds; 48 | 49 | return page; 50 | } 51 | 52 | /** 53 | * Create a page divider (used for sections) 54 | * @param data Page divider configuration data 55 | * @returns Created page divider node 56 | */ 57 | export function createPageDividerFromData(data: any) { 58 | // Check if this method is available in the current Figma version 59 | if (!("createPageDivider" in figma)) { 60 | console.error("createPageDivider is not supported in this Figma version"); 61 | return null; 62 | } 63 | 64 | try { 65 | // Using type assertion since API might not be recognized in all Figma versions 66 | const pageDivider = (figma as any).createPageDivider(); 67 | 68 | // Set properties 69 | if (data.name) pageDivider.name = data.name; 70 | 71 | return pageDivider; 72 | } catch (error) { 73 | console.error("Failed to create page divider:", error); 74 | return null; 75 | } 76 | } 77 | 78 | /** 79 | * Create a slide node (for Figma Slides) 80 | * @param data Slide configuration data 81 | * @returns Created slide node 82 | */ 83 | export function createSlideFromData(data: any): SlideNode | null { 84 | // Check if this method is available in the current Figma version 85 | if (!("createSlide" in figma)) { 86 | console.error("createSlide is not supported in this Figma version"); 87 | return null; 88 | } 89 | 90 | try { 91 | // Using type assertion since API might not be recognized 92 | const slide = (figma as any).createSlide(); 93 | 94 | // Set slide properties 95 | if (data.name) slide.name = data.name; 96 | 97 | // Apply common properties 98 | applyCommonProperties(slide, data); 99 | 100 | return slide; 101 | } catch (error) { 102 | console.error("Failed to create slide:", error); 103 | return null; 104 | } 105 | } 106 | 107 | /** 108 | * Create a slide row node (for Figma Slides) 109 | * @param data Slide row configuration data 110 | * @returns Created slide row node 111 | */ 112 | export function createSlideRowFromData(data: any): SlideRowNode | null { 113 | // Check if this method is available in the current Figma version 114 | if (!("createSlideRow" in figma)) { 115 | console.error("createSlideRow is not supported in this Figma version"); 116 | return null; 117 | } 118 | 119 | try { 120 | // Using type assertion since API might not be recognized 121 | const slideRow = (figma as any).createSlideRow(); 122 | 123 | // Set slide row properties 124 | if (data.name) slideRow.name = data.name; 125 | 126 | // Apply common properties 127 | applyCommonProperties(slideRow, data); 128 | 129 | return slideRow; 130 | } catch (error) { 131 | console.error("Failed to create slide row:", error); 132 | return null; 133 | } 134 | } 135 | ``` -------------------------------------------------------------------------------- /src/widget/utils/widget-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Widget Tools - Utility functions for Figma widget development 3 | */ 4 | 5 | // Theme constants 6 | export const COLORS = { 7 | primary: "#0D99FF", 8 | primaryHover: "#0870B8", 9 | secondary: "#F0F0F0", 10 | secondaryHover: "#E0E0E0", 11 | text: "#333333", 12 | lightText: "#666666", 13 | background: "#FFFFFF", 14 | border: "#E6E6E6", 15 | success: "#36B37E", 16 | error: "#FF5630", 17 | warning: "#FFAB00", 18 | }; 19 | 20 | // Widget sizing helpers 21 | export const SPACING = { 22 | xs: 4, 23 | sm: 8, 24 | md: 16, 25 | lg: 24, 26 | xl: 32, 27 | }; 28 | 29 | // Common shadow effects 30 | export const EFFECTS = { 31 | dropShadow: { 32 | type: "drop-shadow" as const, 33 | color: { r: 0, g: 0, b: 0, a: 0.1 }, 34 | offset: { x: 0, y: 2 }, 35 | blur: 4, 36 | }, 37 | strongShadow: { 38 | type: "drop-shadow" as const, 39 | color: { r: 0, g: 0, b: 0, a: 0.2 }, 40 | offset: { x: 0, y: 4 }, 41 | blur: 8, 42 | } 43 | }; 44 | 45 | // Formatting helpers 46 | export const formatDate = (date: Date): string => { 47 | return date.toLocaleDateString('en-US', { 48 | year: 'numeric', 49 | month: 'short', 50 | day: 'numeric' 51 | }); 52 | }; 53 | 54 | export const truncateText = (text: string, maxLength: number = 100): string => { 55 | if (text.length <= maxLength) return text; 56 | return text.substring(0, maxLength) + '...'; 57 | }; 58 | 59 | // Figma widget helper functions 60 | export const createNodeId = (): string => { 61 | return 'id_' + Math.random().toString(36).substring(2, 11); 62 | }; 63 | 64 | // UI Component generators 65 | export type ButtonVariant = 'primary' | 'secondary' | 'danger'; 66 | 67 | export const buttonStyles = (variant: ButtonVariant = 'primary') => { 68 | switch (variant) { 69 | case 'primary': 70 | return { 71 | fill: COLORS.primary, 72 | hoverFill: COLORS.primaryHover, 73 | textColor: '#FFFFFF', 74 | }; 75 | case 'secondary': 76 | return { 77 | fill: COLORS.secondary, 78 | hoverFill: COLORS.secondaryHover, 79 | textColor: COLORS.text, 80 | }; 81 | case 'danger': 82 | return { 83 | fill: COLORS.error, 84 | hoverFill: '#E64C3D', 85 | textColor: '#FFFFFF', 86 | }; 87 | default: 88 | return { 89 | fill: COLORS.primary, 90 | hoverFill: COLORS.primaryHover, 91 | textColor: '#FFFFFF', 92 | }; 93 | } 94 | }; 95 | 96 | // Network request utilities for widgets 97 | export const fetchWithTimeout = async ( 98 | url: string, 99 | options: RequestInit = {}, 100 | timeout: number = 10000 101 | ): Promise<Response> => { 102 | const controller = new AbortController(); 103 | const id = setTimeout(() => controller.abort(), timeout); 104 | 105 | try { 106 | const response = await fetch(url, { 107 | ...options, 108 | signal: controller.signal 109 | }); 110 | clearTimeout(id); 111 | return response; 112 | } catch (error) { 113 | clearTimeout(id); 114 | throw error; 115 | } 116 | }; 117 | 118 | // Storage helpers 119 | export const saveToLocalStorage = (key: string, data: any): void => { 120 | try { 121 | localStorage.setItem(key, JSON.stringify(data)); 122 | } catch (error) { 123 | console.error('Error saving to localStorage:', error); 124 | } 125 | }; 126 | 127 | export const getFromLocalStorage = <T>(key: string, defaultValue: T): T => { 128 | try { 129 | const item = localStorage.getItem(key); 130 | return item ? JSON.parse(item) : defaultValue; 131 | } catch (error) { 132 | console.error('Error reading from localStorage:', error); 133 | return defaultValue; 134 | } 135 | }; 136 | 137 | // Widget data utilities 138 | export interface WidgetData { 139 | id: string; 140 | name: string; 141 | createdAt: string; 142 | updatedAt: string; 143 | data: Record<string, any>; 144 | } 145 | 146 | export const createWidgetData = (name: string, data: Record<string, any> = {}): WidgetData => { 147 | const now = new Date().toISOString(); 148 | return { 149 | id: createNodeId(), 150 | name, 151 | createdAt: now, 152 | updatedAt: now, 153 | data 154 | }; 155 | }; 156 | 157 | export const updateWidgetData = (widgetData: WidgetData, newData: Partial<Record<string, any>>): WidgetData => { 158 | return { 159 | ...widgetData, 160 | updatedAt: new Date().toISOString(), 161 | data: { ...widgetData.data, ...newData } 162 | }; 163 | }; 164 | ``` -------------------------------------------------------------------------------- /src/tools/widget/search-widgets.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tool: search_widgets 3 | * 4 | * Searches for widgets that have specific sync data properties and values 5 | */ 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { z } from "zod"; 8 | import figmaApi from "../../services/figma-api.js"; 9 | import { FigmaUtils } from "../../utils/figma-utils.js"; 10 | 11 | export const searchWidgetsTool = (server: McpServer) => { 12 | server.tool( 13 | "search_widgets", 14 | { 15 | file_key: z.string().min(1).describe("The Figma file key"), 16 | property_key: z.string().min(1).describe("The sync data property key to search for"), 17 | property_value: z.string().optional().describe("Optional property value to match (if not provided, returns all widgets with the property)") 18 | }, 19 | async ({ file_key, property_key, property_value }) => { 20 | try { 21 | const file = await figmaApi.getFile(file_key); 22 | 23 | // Find all widget nodes 24 | const allWidgetNodes = FigmaUtils.getNodesByType(file, 'WIDGET'); 25 | 26 | // Filter widgets that have the specified property 27 | const matchingWidgets = allWidgetNodes.filter(node => { 28 | if (!node.widgetSync) return false; 29 | 30 | try { 31 | const syncData = JSON.parse(node.widgetSync); 32 | 33 | // If property_value is provided, check for exact match 34 | if (property_value !== undefined) { 35 | // Handle different types of values (string, number, boolean) 36 | const propValue = syncData[property_key]; 37 | 38 | if (typeof propValue === 'string') { 39 | return propValue === property_value; 40 | } else if (typeof propValue === 'number') { 41 | return propValue.toString() === property_value; 42 | } else if (typeof propValue === 'boolean') { 43 | return propValue.toString() === property_value; 44 | } else if (propValue !== null && typeof propValue === 'object') { 45 | return JSON.stringify(propValue) === property_value; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | // If no value provided, just check if the property exists 52 | return property_key in syncData; 53 | } catch (error) { 54 | return false; 55 | } 56 | }); 57 | 58 | if (matchingWidgets.length === 0) { 59 | return { 60 | content: [ 61 | { type: "text", text: property_value ? 62 | `No widgets found with property "${property_key}" = "${property_value}"` : 63 | `No widgets found with property "${property_key}"` 64 | } 65 | ] 66 | }; 67 | } 68 | 69 | const widgetsList = matchingWidgets.map((node, index) => { 70 | let syncDataValue = ''; 71 | try { 72 | const syncData = JSON.parse(node.widgetSync!); 73 | const value = syncData[property_key]; 74 | syncDataValue = typeof value === 'object' ? 75 | JSON.stringify(value) : 76 | String(value); 77 | } catch (error) { 78 | syncDataValue = 'Error parsing sync data'; 79 | } 80 | 81 | return `${index + 1}. **${node.name}** (ID: ${node.id}) 82 | - Property "${property_key}": ${syncDataValue}`; 83 | }).join('\n\n'); 84 | 85 | return { 86 | content: [ 87 | { type: "text", text: property_value ? 88 | `# Widgets with property "${property_key}" = "${property_value}"` : 89 | `# Widgets with property "${property_key}"` 90 | }, 91 | { type: "text", text: `Found ${matchingWidgets.length} matching widgets:` }, 92 | { type: "text", text: widgetsList } 93 | ] 94 | }; 95 | } catch (error) { 96 | console.error('Error searching widgets:', error); 97 | return { 98 | content: [ 99 | { type: "text", text: `Error searching widgets: ${(error as Error).message}` } 100 | ] 101 | }; 102 | } 103 | } 104 | ); 105 | }; 106 | ``` -------------------------------------------------------------------------------- /docs/02-implementation-steps.md: -------------------------------------------------------------------------------- ```markdown 1 | # Implementation Steps 2 | 3 | This document outlines the process followed to implement the Figma MCP server, from project setup to final testing. 4 | 5 | ## 1. Project Setup 6 | 7 | ### Initial Directory Structure 8 | 9 | The project was organized with the following structure: 10 | - `/src` for TypeScript source files 11 | - `/config` for configuration files 12 | - `/services` for API integrations 13 | - `/utils` for utility functions 14 | - `/types` for type definitions 15 | 16 | ### Dependencies Installation 17 | 18 | The project uses Bun as its package manager and runtime. Key dependencies include: 19 | - `@modelcontextprotocol/sdk` for MCP implementation 20 | - `@figma/rest-api-spec` for Figma API type definitions 21 | - `axios` for HTTP requests 22 | - `zod` for schema validation 23 | - `dotenv` for environment variable management 24 | 25 | ## 2. Configuration Setup 26 | 27 | ### Environment Variables 28 | 29 | Created a configuration system using Zod to validate environment variables: 30 | - `FIGMA_PERSONAL_ACCESS_TOKEN`: For Figma API authentication 31 | - `PORT`: Server port (default: 3001) 32 | - `NODE_ENV`: Environment (development/production) 33 | 34 | ## 3. Figma API Integration 35 | 36 | ### API Service Implementation 37 | 38 | Created a comprehensive service for interacting with the Figma API: 39 | - Used official Figma REST API specification types 40 | - Implemented methods for all required API endpoints 41 | - Added request/response handling with proper error management 42 | - Organized methods by resource type (files, nodes, comments, etc.) 43 | 44 | ### Utility Functions 45 | 46 | Implemented utility functions for common operations: 47 | - Finding nodes by ID 48 | - Getting nodes by type 49 | - Extracting text from nodes 50 | - Formatting node information 51 | - Calculating node paths in document hierarchy 52 | 53 | ## 4. MCP Server Implementation 54 | 55 | ### Server Setup 56 | 57 | Set up the MCP server using the MCP SDK: 58 | - Configured server metadata (name, version) 59 | - Connected to standard I/O for communication 60 | - Set up error handling and logging 61 | 62 | ### Tools Implementation 63 | 64 | Created tools for various Figma operations: 65 | - `get_file`: Retrieve file information 66 | - `get_node`: Access specific nodes 67 | - `get_comments`: Read file comments 68 | - `get_images`: Export node images 69 | - `get_file_versions`: Access version history 70 | - `search_text`: Search for text in files 71 | - `get_components`: Get file components 72 | - `add_comment`: Add comments to files 73 | 74 | Each tool includes: 75 | - Parameter validation using Zod 76 | - Error handling and response formatting 77 | - Proper response formatting for AI consumption 78 | 79 | ### Resource Templates 80 | 81 | Implemented resource templates for consistent access patterns: 82 | - `figma-file://{file_key}`: Access to Figma files 83 | - `figma-node://{file_key}/{node_id}`: Access to specific nodes 84 | 85 | ## 5. Build System 86 | 87 | ### Build Configuration 88 | 89 | Set up a build system with Bun: 90 | - Configured TypeScript compilation 91 | - Set up build scripts for development and production 92 | - Created a Makefile for common operations 93 | 94 | ### Scripts 95 | 96 | Implemented various scripts: 97 | - `start`: Run the server 98 | - `dev`: Development mode with auto-reload 99 | - `mcp`: MCP server with auto-reload 100 | - `build`: Build the project 101 | - `build:mcp`: Build the MCP server 102 | - `test`: Run tests 103 | - `clean`: Clean build artifacts 104 | 105 | ## 6. Documentation 106 | 107 | ### README 108 | 109 | Created a comprehensive README with: 110 | - Project description 111 | - Installation instructions 112 | - Usage examples 113 | - Available tools and resources 114 | - Development guidelines 115 | 116 | ### Code Documentation 117 | 118 | Added documentation throughout the codebase: 119 | - Function and method descriptions 120 | - Parameter documentation 121 | - Type definitions 122 | - Usage examples 123 | 124 | ## 7. Testing and Verification 125 | 126 | ### Build Verification 127 | 128 | Verified the build process: 129 | - Confirmed successful compilation 130 | - Checked for TypeScript errors 131 | - Ensured all dependencies were properly resolved 132 | 133 | ### File Structure Verification 134 | 135 | Confirmed the final directory structure: 136 | - All required files in place 137 | - Proper organization of code 138 | - Correct file permissions 139 | 140 | ## Next Steps 141 | 142 | - **Integration Testing**: Test with real AI assistants 143 | - **Performance Optimization**: Optimize for response time 144 | - **Caching**: Add caching for frequent requests 145 | - **Extended Capabilities**: Add more tools and resources 146 | - **User Documentation**: Create end-user documentation 147 | ``` -------------------------------------------------------------------------------- /docs/05-project-status.md: -------------------------------------------------------------------------------- ```markdown 1 | # Project Status and Roadmap 2 | 3 | This document tracks the current status of the Figma MCP server project and outlines future development plans. 4 | 5 | ## 1. Current Status 6 | 7 | ### Completed Tasks 8 | 9 | ✅ **Project Setup** 10 | - Created project structure 11 | - Installed dependencies 12 | - Configured TypeScript environment 13 | - Set up build system 14 | 15 | ✅ **Core Components** 16 | - Environment configuration 17 | - Figma API service 18 | - Utility functions 19 | - MCP server implementation 20 | 21 | ✅ **MCP Tools** 22 | - get_file: Retrieve Figma files 23 | - get_node: Access specific nodes 24 | - get_comments: Read file comments 25 | - get_images: Export node images 26 | - get_file_versions: Access version history 27 | - search_text: Search for text in files 28 | - get_components: Get file components 29 | - add_comment: Add comments to files 30 | 31 | ✅ **Resource Templates** 32 | - figma-file: Access to Figma files 33 | - figma-node: Access to specific nodes 34 | 35 | ✅ **Documentation** 36 | - Project overview 37 | - Implementation steps 38 | - Components and features 39 | - Usage guide 40 | - Project status and roadmap 41 | 42 | ### Current Limitations 43 | 44 | - No authentication refresh mechanism 45 | - Limited error reporting detail 46 | - No caching mechanism for frequent requests 47 | - Limited support for advanced Figma features 48 | - No pagination support for large result sets 49 | - Limited testing 50 | 51 | ## 2. Next Steps 52 | 53 | ### Short-Term Goals (Next 2-4 Weeks) 54 | 55 | - [ ] **Comprehensive Testing** 56 | - Unit tests for all components 57 | - Integration tests with Figma API 58 | - Performance testing 59 | 60 | - [ ] **Error Handling Improvements** 61 | - More detailed error messages 62 | - Better error categorization 63 | - Recovery mechanisms 64 | 65 | - [ ] **Caching System** 66 | - Implement response caching 67 | - Configure TTL for different resource types 68 | - Cache invalidation mechanisms 69 | 70 | - [ ] **Authentication Enhancements** 71 | - Token refresh mechanism 72 | - Better error handling for authentication issues 73 | - Support for OAuth authentication 74 | 75 | ### Medium-Term Goals (Next 2-3 Months) 76 | 77 | - [ ] **Additional Tools** 78 | - Team and project management 79 | - Style operations 80 | - Branch management 81 | - Widget interactions 82 | - Variable access and manipulation 83 | 84 | - [ ] **Enhanced Resource Templates** 85 | - More granular resource access 86 | - Improved filtering and searching 87 | - Resource relationships 88 | 89 | - [ ] **Performance Optimizations** 90 | - Parallel request processing 91 | - Response size optimization 92 | - Processing time improvements 93 | 94 | - [ ] **Security Enhancements** 95 | - Request validation 96 | - Rate limiting 97 | - Access control for sensitive operations 98 | 99 | ### Long-Term Goals (3+ Months) 100 | 101 | - [ ] **Advanced Feature Support** 102 | - FigJam-specific features 103 | - Prototyping capabilities 104 | - Dev mode integration 105 | - Widget creation and management 106 | 107 | - [ ] **Real-Time Updates** 108 | - Webhook integration for file changes 109 | - Live updates for collaborative editing 110 | 111 | - [ ] **Extended Integration** 112 | - Integration with other design tools 113 | - Version control system integration 114 | - CI/CD pipeline integration 115 | 116 | - [ ] **Advanced AI Features** 117 | - Design analysis capabilities 118 | - Automated design suggestions 119 | - Design consistency checking 120 | 121 | ## 3. Version History 122 | 123 | ### v1.0.0 (April 13, 2025) 124 | - Initial release 125 | - Core tools and resources 126 | - Basic documentation 127 | 128 | ## 4. Known Issues 129 | 130 | - Large files may cause performance issues 131 | - Certain complex node types may not be fully supported 132 | - Error handling in nested operations needs improvement 133 | - Some API rate limits may be encountered with frequent use 134 | 135 | ## 5. Contribution Guidelines 136 | 137 | ### Priority Areas for Contribution 138 | 139 | 1. **Testing**: Unit and integration tests 140 | 2. **Documentation**: Usage examples and API docs 141 | 3. **Feature Expansion**: Additional tools and resources 142 | 4. **Performance**: Optimizations for large files and complex operations 143 | 5. **Error Handling**: Improved error reporting and recovery 144 | 145 | ### Contribution Process 146 | 147 | 1. Select an issue or feature from the project board 148 | 2. Create a branch with a descriptive name 149 | 3. Implement the change with appropriate tests 150 | 4. Submit a pull request with a clear description 151 | 5. Address review feedback 152 | 6. Merge upon approval 153 | 154 | ## 6. Support and Feedback 155 | 156 | For support or to provide feedback, please: 157 | - Open an issue in the GitHub repository 158 | - Contact the project maintainers 159 | - Join the project discussion forum 160 | 161 | --- 162 | 163 | Last updated: April 13, 2025 164 | ``` -------------------------------------------------------------------------------- /src/services/widget-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Service for interacting with Figma Widget API 3 | */ 4 | import axios from 'axios'; 5 | import { env } from '../config/env.js'; 6 | import { z } from 'zod'; 7 | 8 | const FIGMA_API_BASE_URL = 'https://api.figma.com/v1'; 9 | 10 | // Widget data schemas 11 | export const WidgetNodeSchema = z.object({ 12 | id: z.string(), 13 | name: z.string(), 14 | type: z.literal('WIDGET'), 15 | widgetId: z.string().optional(), 16 | widgetSync: z.string().optional(), 17 | pluginData: z.record(z.unknown()).optional(), 18 | sharedPluginData: z.record(z.record(z.unknown())).optional(), 19 | }); 20 | 21 | export type WidgetNode = z.infer<typeof WidgetNodeSchema>; 22 | 23 | export const WidgetSyncDataSchema = z.record(z.unknown()); 24 | export type WidgetSyncData = z.infer<typeof WidgetSyncDataSchema>; 25 | 26 | /** 27 | * Service for interacting with Figma Widget API 28 | */ 29 | export class WidgetApiService { 30 | private readonly headers: Record<string, string>; 31 | 32 | constructor(accessToken: string = env.FIGMA_PERSONAL_ACCESS_TOKEN) { 33 | this.headers = { 34 | 'X-Figma-Token': accessToken, 35 | }; 36 | } 37 | 38 | /** 39 | * Get all widget nodes in a file 40 | */ 41 | async getWidgetNodes(fileKey: string): Promise<WidgetNode[]> { 42 | try { 43 | const response = await axios.get(, { 44 | headers: this.headers, 45 | }); 46 | 47 | const file = response.data; 48 | return this.findAllWidgetNodes(file.document); 49 | } catch (error) { 50 | console.error('Error fetching widget nodes:', error); 51 | throw error; 52 | } 53 | } 54 | 55 | /** 56 | * Get a specific widget node by ID 57 | */ 58 | async getWidgetNode(fileKey: string, nodeId: string): Promise<WidgetNode | null> { 59 | try { 60 | const response = await axios.get(, { 61 | headers: this.headers, 62 | }); 63 | 64 | const node = response.data.nodes[nodeId]?.document; 65 | if (!node || node.type !== 'WIDGET') { 66 | return null; 67 | } 68 | 69 | return WidgetNodeSchema.parse(node); 70 | } catch (error) { 71 | console.error('Error fetching widget node:', error); 72 | throw error; 73 | } 74 | } 75 | 76 | /** 77 | * Get the widget sync data (state) for a specific widget 78 | */ 79 | async getWidgetSyncData(fileKey: string, nodeId: string): Promise<WidgetSyncData | null> { 80 | try { 81 | const widgetNode = await this.getWidgetNode(fileKey, nodeId); 82 | 83 | if (!widgetNode || !widgetNode.widgetSync) { 84 | return null; 85 | } 86 | 87 | // Parse the widgetSync data string (it's stored as a JSON string) 88 | try { 89 | return JSON.parse(widgetNode.widgetSync); 90 | } catch (parseError) { 91 | console.error('Error parsing widget sync data:', parseError); 92 | return null; 93 | } 94 | } catch (error) { 95 | console.error('Error fetching widget sync data:', error); 96 | throw error; 97 | } 98 | } 99 | 100 | /** 101 | * Create a widget instance in a file (requires special access) 102 | * Note: This is only available to Figma widget developers or partners. 103 | */ 104 | async createWidget(fileKey: string, options: { 105 | name: string, 106 | widgetId: string, 107 | x: number, 108 | y: number, 109 | initialSyncData?: Record<string, any>, 110 | parentNodeId?: string, 111 | }): Promise<{ widgetNodeId: string } | null> { 112 | try { 113 | // This endpoint might not be publicly available 114 | const response = await axios.post( 115 | , 116 | options, 117 | { headers: this.headers } 118 | ); 119 | 120 | return response.data; 121 | } catch (error) { 122 | console.error('Error creating widget:', error); 123 | throw error; 124 | } 125 | } 126 | 127 | /** 128 | * Update a widget's properties (requires widget developer access) 129 | * Note: This functionality is limited to Figma widget developers. 130 | */ 131 | async updateWidgetProperties(fileKey: string, nodeId: string, properties: Record<string, any>): Promise<boolean> { 132 | try { 133 | // This endpoint might not be publicly available 134 | await axios.patch( 135 | , 136 | { properties }, 137 | { headers: this.headers } 138 | ); 139 | 140 | return true; 141 | } catch (error) { 142 | console.error('Error updating widget properties:', error); 143 | throw error; 144 | } 145 | } 146 | 147 | /** 148 | * Helper method to recursively find all widget nodes in a file 149 | */ 150 | private findAllWidgetNodes(node: any): WidgetNode[] { 151 | let widgets: WidgetNode[] = []; 152 | 153 | if (node.type === 'WIDGET') { 154 | try { 155 | widgets.push(WidgetNodeSchema.parse(node)); 156 | } catch (error) { 157 | console.error('Error parsing widget node:', error); 158 | } 159 | } 160 | 161 | if (node.children) { 162 | for (const child of node.children) { 163 | widgets = widgets.concat(this.findAllWidgetNodes(child)); 164 | } 165 | } 166 | 167 | return widgets; 168 | } 169 | } 170 | 171 | export default new WidgetApiService(); 172 | ``` -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Resources for the Figma MCP server 3 | */ 4 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import figmaApi from "./services/figma-api.js"; 6 | 7 | /** 8 | * Resource template for Figma files 9 | */ 10 | export const figmaFileResource = (server: McpServer): void => { 11 | server.resource( 12 | "figma-file", 13 | new ResourceTemplate("figma-file://{file_key}", { 14 | // Define listCallback instead of just providing a string for 'list' 15 | listCallback: async () => { 16 | try { 17 | // Here we would typically get a list of files 18 | // For now, return an empty list since we don't have access to "all files" 19 | return { 20 | contents: [{ 21 | uri: "figma-file://", 22 | title: "Figma Files", 23 | description: "List of Figma files you have access to", 24 | text: "# Figma Files\n\nTo access a specific file, you need to provide its file key." 25 | }] 26 | }; 27 | } catch (error) { 28 | console.error('Error listing files:', error); 29 | return { 30 | contents: [{ 31 | uri: "figma-file://", 32 | title: "Error listing files", 33 | text: `Error: ${(error as Error).message}` 34 | }] 35 | }; 36 | } 37 | } 38 | }), 39 | async (uri, { file_key }) => { 40 | try { 41 | const file = await figmaApi.getFile(file_key); 42 | 43 | return { 44 | contents: [{ 45 | uri: uri.href, 46 | title: file.name, 47 | description: `Last modified: ${file.lastModified}`, 48 | 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}` 49 | }] 50 | }; 51 | } catch (error) { 52 | console.error('Error fetching file for resource:', error); 53 | return { 54 | contents: [{ 55 | uri: uri.href, 56 | title: `File not found: ${file_key}`, 57 | text: `Error: ${(error as Error).message}` 58 | }] 59 | }; 60 | } 61 | } 62 | ); 63 | }; 64 | 65 | /** 66 | * Resource template for Figma nodes 67 | */ 68 | export const figmaNodeResource = (server: McpServer): void => { 69 | server.resource( 70 | "figma-node", 71 | new ResourceTemplate("figma-node://{file_key}/{node_id}", { 72 | // Define listCallback instead of just providing a string for 'list' 73 | listCallback: async (uri, { file_key }) => { 74 | try { 75 | // If only file_key is provided, list all top-level nodes 76 | const file = await figmaApi.getFile(file_key); 77 | 78 | return { 79 | contents: file.document.children?.map(node => ({ 80 | uri: `figma-node://${file_key}/${node.id}`, 81 | title: node.name, 82 | description: `Type: ${node.type}`, 83 | text: `# ${node.name}\n\nType: ${node.type}\nID: ${node.id}` 84 | })) || [] 85 | }; 86 | } catch (error) { 87 | console.error('Error listing nodes:', error); 88 | return { 89 | contents: [{ 90 | uri: `figma-node://${file_key}`, 91 | title: "Error listing nodes", 92 | text: `Error: ${(error as Error).message}` 93 | }] 94 | }; 95 | } 96 | } 97 | }), 98 | async (uri, { file_key, node_id }) => { 99 | try { 100 | // Get specific node 101 | const fileNodes = await figmaApi.getFileNodes(file_key, [node_id]); 102 | const nodeData = fileNodes.nodes[node_id]; 103 | 104 | if (!nodeData) { 105 | return { 106 | contents: [{ 107 | uri: uri.href, 108 | title: `Node not found: ${node_id}`, 109 | text: `Node ${node_id} not found in file ${file_key}` 110 | }] 111 | }; 112 | } 113 | 114 | return { 115 | contents: [{ 116 | uri: uri.href, 117 | title: nodeData.document.name, 118 | description: `Type: ${nodeData.document.type}`, 119 | text: `# ${nodeData.document.name}\n\nType: ${nodeData.document.type}\nID: ${nodeData.document.id}\nChildren: ${nodeData.document.children?.length || 0}` 120 | }] 121 | }; 122 | } catch (error) { 123 | console.error('Error fetching node for resource:', error); 124 | return { 125 | contents: [{ 126 | uri: uri.href, 127 | title: `Error`, 128 | text: `Error: ${(error as Error).message}` 129 | }] 130 | }; 131 | } 132 | } 133 | ); 134 | }; 135 | 136 | /** 137 | * Registers all resources with the MCP server 138 | */ 139 | export function registerAllResources(server: McpServer): void { 140 | figmaFileResource(server); 141 | figmaNodeResource(server); 142 | } 143 | ``` -------------------------------------------------------------------------------- /src/plugin/creators/imageCreators.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Image and media element creation functions for Figma plugin 3 | */ 4 | 5 | import { applyCommonProperties } from '../utils/nodeUtils'; 6 | 7 | /** 8 | * Create an image node from data 9 | * @param data Image configuration data 10 | * @returns Created image node 11 | */ 12 | export function createImageFromData(data: any): SceneNode | null { 13 | try { 14 | // Image creation requires a hash 15 | if (!data.hash) { 16 | console.error('Image creation requires an image hash'); 17 | return null; 18 | } 19 | 20 | const image = figma.createImage(data.hash); 21 | 22 | // Create a rectangle to display the image 23 | const rect = figma.createRectangle(); 24 | 25 | // Set size 26 | if (data.width && data.height) { 27 | rect.resize(data.width, data.height); 28 | } 29 | 30 | // Apply image as fill 31 | rect.fills = [{ 32 | type: 'IMAGE', 33 | scaleMode: data.scaleMode || 'FILL', 34 | imageHash: image.hash 35 | }]; 36 | 37 | // Apply common properties 38 | applyCommonProperties(rect, data); 39 | 40 | return rect; 41 | } catch (error) { 42 | console.error('Failed to create image:', error); 43 | return null; 44 | } 45 | } 46 | 47 | /** 48 | * Create an image node asynchronously from data 49 | * @param data Image configuration data with bytes or file 50 | * @returns Promise resolving to created image node 51 | */ 52 | export async function createImageFromBytesAsync(data: any): Promise<SceneNode | null> { 53 | try { 54 | // Image creation requires bytes or a file 55 | if (!data.bytes && !data.file) { 56 | console.error('Image creation requires image bytes or file'); 57 | return null; 58 | } 59 | 60 | let image; 61 | if (data.bytes) { 62 | image = await figma.createImageAsync(data.bytes); 63 | } else if (data.file) { 64 | // Note: file would need to be provided through some UI interaction 65 | // as plugins cannot directly access the file system 66 | image = await figma.createImageAsync(data.file); 67 | } else { 68 | return null; 69 | } 70 | 71 | // Create a rectangle to display the image 72 | const rect = figma.createRectangle(); 73 | 74 | // Set size 75 | if (data.width && data.height) { 76 | rect.resize(data.width, data.height); 77 | } 78 | 79 | // Apply image as fill 80 | rect.fills = [{ 81 | type: 'IMAGE', 82 | scaleMode: data.scaleMode || 'FILL', 83 | imageHash: image.hash 84 | }]; 85 | 86 | // Apply common properties 87 | applyCommonProperties(rect, data); 88 | 89 | return rect; 90 | } catch (error) { 91 | console.error('Failed to create image asynchronously:', error); 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * Create a GIF node from data 98 | * @param data GIF configuration data 99 | * @returns Created gif node 100 | */ 101 | export function createGifFromData(data: any): SceneNode | null { 102 | // As of my knowledge, there isn't a direct createGif API 103 | // Even though it's in the list of methods 104 | // For now, return null and log an error 105 | console.error('createGif API is not directly available or implemented'); 106 | return null; 107 | } 108 | 109 | /** 110 | * Create a video node asynchronously from data 111 | * This depends on figma.createVideoAsync which may not be available in all versions 112 | * 113 | * @param data Video configuration data 114 | * @returns Promise resolving to created video node 115 | */ 116 | export async function createVideoFromDataAsync(data: any): Promise<SceneNode | null> { 117 | // Check if video creation is supported 118 | if (!('createVideoAsync' in figma)) { 119 | console.error('Video creation is not supported in this Figma version'); 120 | return null; 121 | } 122 | 123 | try { 124 | // Video creation requires bytes 125 | if (!data.bytes) { 126 | console.error('Video creation requires video bytes'); 127 | return null; 128 | } 129 | 130 | // Using type assertion since createVideoAsync may not be recognized by TypeScript 131 | const video = await (figma as any).createVideoAsync(data.bytes); 132 | 133 | // Apply common properties 134 | applyCommonProperties(video, data); 135 | 136 | return video; 137 | } catch (error) { 138 | console.error('Failed to create video:', error); 139 | return null; 140 | } 141 | } 142 | 143 | /** 144 | * Create a link preview node asynchronously from data 145 | * This depends on figma.createLinkPreviewAsync which may not be available in all versions 146 | * 147 | * @param data Link preview configuration data 148 | * @returns Promise resolving to created link preview node 149 | */ 150 | export async function createLinkPreviewFromDataAsync(data: any): Promise<SceneNode | null> { 151 | // Check if link preview creation is supported 152 | if (!('createLinkPreviewAsync' in figma)) { 153 | console.error('Link preview creation is not supported in this Figma version'); 154 | return null; 155 | } 156 | 157 | try { 158 | // Link preview creation requires a URL 159 | if (!data.url) { 160 | console.error('Link preview creation requires a URL'); 161 | return null; 162 | } 163 | 164 | // Using type assertion since createLinkPreviewAsync may not be recognized by TypeScript 165 | const linkPreview = await (figma as any).createLinkPreviewAsync(data.url); 166 | 167 | // Apply common properties 168 | applyCommonProperties(linkPreview, data); 169 | 170 | return linkPreview; 171 | } catch (error) { 172 | console.error('Failed to create link preview:', error); 173 | return null; 174 | } 175 | } ``` -------------------------------------------------------------------------------- /docs/03-components-and-features.md: -------------------------------------------------------------------------------- ```markdown 1 | # Components and Features 2 | 3 | This document provides detailed information about the key components and features of the Figma MCP server. 4 | 5 | ## 1. Core Components 6 | 7 | ### Environment Configuration (`src/config/env.ts`) 8 | 9 | The environment configuration component: 10 | 11 | - Loads variables from `.env` file using dotenv 12 | - Validates environment variables using Zod schema 13 | - Provides type-safe access to configuration values 14 | - Ensures required variables are present 15 | - Sets sensible defaults for optional variables 16 | 17 | ```typescript 18 | // Example of environment validation 19 | const envSchema = z.object({ 20 | FIGMA_PERSONAL_ACCESS_TOKEN: z.string().min(1), 21 | PORT: z.string().default("3001").transform(Number), 22 | NODE_ENV: z 23 | .enum(["development", "production", "test"]) 24 | .default("development"), 25 | }); 26 | ``` 27 | 28 | ### Figma API Service (`src/services/figma-api.ts`) 29 | 30 | A comprehensive service for interacting with the Figma API: 31 | 32 | - Uses official Figma API TypeScript definitions 33 | - Provides methods for all relevant API endpoints 34 | - Handles authentication and request formatting 35 | - Processes responses and errors consistently 36 | - Supports all Figma resource types: 37 | - Files and nodes 38 | - Comments 39 | - Images 40 | - Components and styles 41 | - Versions 42 | - Teams and projects 43 | 44 | ```typescript 45 | // Example method for retrieving a Figma file 46 | async getFile(fileKey: string, params: { 47 | ids?: string; 48 | depth?: number; 49 | geometry?: string 50 | } = {}): Promise<GetFileResponse> { 51 | const response = await axios.get(`${FIGMA_API_BASE_URL}/files/${fileKey}`, { 52 | headers: this.headers, 53 | params, 54 | }); 55 | return response.data; 56 | } 57 | ``` 58 | 59 | ### Figma Utilities (`src/utils/figma-utils.ts`) 60 | 61 | Utility functions for working with Figma data: 62 | 63 | - Node search and traversal 64 | - Text extraction 65 | - Property formatting 66 | - Path calculation 67 | - Type-specific operations 68 | 69 | ```typescript 70 | // Example utility for finding a node by ID 71 | static findNodeById(file: GetFileResponse, nodeId: string): Node | null { 72 | // Implementation details... 73 | } 74 | ``` 75 | 76 | ### MCP Server Implementation (`src/index.ts`) 77 | 78 | The main MCP server implementation: 79 | 80 | - Configures the MCP server 81 | - Defines tools and resources 82 | - Handles communication via standard I/O 83 | - Manages error handling and response formatting 84 | 85 | ```typescript 86 | // Example of MCP server configuration 87 | const server = new McpServer({ 88 | name: "Figma API", 89 | version: "1.0.0", 90 | }); 91 | ``` 92 | 93 | ## 2. MCP Tools 94 | 95 | The server provides the following tools: 96 | 97 | ### `get_file` 98 | 99 | Retrieves a Figma file by key: 100 | 101 | - Parameters: 102 | - `file_key`: The Figma file key 103 | - `return_full_file`: Whether to return the full file structure 104 | - Returns: 105 | - File name, modification date 106 | - Document structure summary 107 | - Component and style counts 108 | - Full file contents (if requested) 109 | 110 | ### `get_node` 111 | 112 | Retrieves a specific node from a Figma file: 113 | 114 | - Parameters: 115 | - `file_key`: The Figma file key 116 | - `node_id`: The ID of the node to retrieve 117 | - Returns: 118 | - Node name, type, and ID 119 | - Node properties and attributes 120 | - Child node count 121 | 122 | ### `get_comments` 123 | 124 | Retrieves comments from a Figma file: 125 | 126 | - Parameters: 127 | - `file_key`: The Figma file key 128 | - Returns: 129 | - Comment count 130 | - Comment text 131 | - Author information 132 | - Timestamps 133 | 134 | ### `get_images` 135 | 136 | Exports nodes as images: 137 | 138 | - Parameters: 139 | - `file_key`: The Figma file key 140 | - `node_ids`: Array of node IDs to export 141 | - `format`: Image format (jpg, png, svg, pdf) 142 | - `scale`: Scale factor for the image 143 | - Returns: 144 | - Image URLs for each node 145 | - Error information for failed exports 146 | 147 | ### `get_file_versions` 148 | 149 | Retrieves version history for a file: 150 | 151 | - Parameters: 152 | - `file_key`: The Figma file key 153 | - Returns: 154 | - Version list 155 | - Version labels and descriptions 156 | - Author information 157 | - Timestamps 158 | 159 | ### `search_text` 160 | 161 | Searches for text within a Figma file: 162 | 163 | - Parameters: 164 | - `file_key`: The Figma file key 165 | - `search_text`: The text to search for 166 | - Returns: 167 | - Matching text nodes 168 | - Node paths in document hierarchy 169 | - Matching text content 170 | 171 | ### `get_components` 172 | 173 | Retrieves components from a Figma file: 174 | 175 | - Parameters: 176 | - `file_key`: The Figma file key 177 | - Returns: 178 | - Component list 179 | - Component names and keys 180 | - Component descriptions 181 | - Remote status 182 | 183 | ### `add_comment` 184 | 185 | Adds a comment to a Figma file: 186 | 187 | - Parameters: 188 | - `file_key`: The Figma file key 189 | - `message`: The comment text 190 | - `node_id`: Optional node ID to attach the comment to 191 | - Returns: 192 | - Comment ID 193 | - Author information 194 | - Timestamp 195 | 196 | ## 3. Resource Templates 197 | 198 | The server provides the following resource templates: 199 | 200 | ### `figma-file://{file_key}` 201 | 202 | Provides access to Figma files: 203 | 204 | - URI format: `figma-file://{file_key}` 205 | - List URI: `figma-file://` 206 | - Returns: 207 | - File name 208 | - Last modified date 209 | - Document structure summary 210 | 211 | ### `figma-node://{file_key}/{node_id}` 212 | 213 | Provides access to nodes within Figma files: 214 | 215 | - URI format: `figma-node://{file_key}/{node_id}` 216 | - List URI: `figma-node://{file_key}` 217 | - Returns: 218 | - Node name and type 219 | - Node properties 220 | - Child node count 221 | 222 | ## 4. Error Handling 223 | 224 | The server implements comprehensive error handling: 225 | 226 | - API request errors 227 | - Authentication failures 228 | - Invalid parameters 229 | - Resource not found errors 230 | - Server errors 231 | 232 | Each error is properly formatted and returned to the client with: 233 | 234 | - Error message 235 | - Error type 236 | - Context information (when available) 237 | 238 | ## 5. Response Formatting 239 | 240 | Responses are formatted for optimal consumption by AI assistants: 241 | 242 | - Clear headings 243 | - Structured information 244 | - Formatted lists 245 | - Contextual descriptions 246 | - Links and references where appropriate 247 | ``` -------------------------------------------------------------------------------- /docs/04-usage-guide.md: -------------------------------------------------------------------------------- ```markdown 1 | # Usage Guide 2 | 3 | This document provides detailed instructions for setting up, running, and using the Figma MCP server. 4 | 5 | ## 1. Setup Instructions 6 | 7 | ### Prerequisites 8 | 9 | Before you begin, ensure you have the following: 10 | - [Bun](https://bun.sh/) v1.0.0 or higher installed 11 | - A Figma account with API access 12 | - A personal access token from Figma 13 | 14 | ### Installation 15 | 16 | 1. Clone the repository: 17 | ```bash 18 | git clone <repository-url> 19 | cd figma-mcp-server 20 | ``` 21 | 22 | 2. Install dependencies: 23 | ```bash 24 | make install 25 | ``` 26 | or 27 | ```bash 28 | bun install 29 | ``` 30 | 31 | 3. Configure environment variables: 32 | - Copy the example environment file: 33 | ```bash 34 | cp .env.example .env 35 | ``` 36 | - Edit `.env` and add your Figma personal access token: 37 | ``` 38 | FIGMA_PERSONAL_ACCESS_TOKEN=your_figma_token_here 39 | PORT=3001 40 | NODE_ENV=development 41 | ``` 42 | 43 | ## 2. Running the Server 44 | 45 | ### Development Mode 46 | 47 | Run the server in development mode with auto-reload: 48 | ```bash 49 | make mcp 50 | ``` 51 | or 52 | ```bash 53 | bun run mcp 54 | ``` 55 | 56 | ### Production Mode 57 | 58 | 1. Build the server: 59 | ```bash 60 | make build-mcp 61 | ``` 62 | or 63 | ```bash 64 | bun run build:mcp 65 | ``` 66 | 67 | 2. Run the built server: 68 | ```bash 69 | make start 70 | ``` 71 | or 72 | ```bash 73 | bun run start 74 | ``` 75 | 76 | ## 3. Using the MCP Tools 77 | 78 | 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. 79 | 80 | ### Example: Retrieving a Figma File 81 | 82 | Using the `get_file` tool: 83 | 84 | ```json 85 | { 86 | "tool": "get_file", 87 | "parameters": { 88 | "file_key": "abc123xyz789", 89 | "return_full_file": false 90 | } 91 | } 92 | ``` 93 | 94 | Expected response: 95 | ```json 96 | { 97 | "content": [ 98 | { "type": "text", "text": "# Figma File: My Design" }, 99 | { "type": "text", "text": "Last modified: 2025-04-10T15:30:45Z" }, 100 | { "type": "text", "text": "Document contains 5 top-level nodes." }, 101 | { "type": "text", "text": "Components: 12" }, 102 | { "type": "text", "text": "Component sets: 3" }, 103 | { "type": "text", "text": "Styles: 8" } 104 | ] 105 | } 106 | ``` 107 | 108 | ### Example: Searching for Text 109 | 110 | Using the `search_text` tool: 111 | 112 | ```json 113 | { 114 | "tool": "search_text", 115 | "parameters": { 116 | "file_key": "abc123xyz789", 117 | "search_text": "Welcome" 118 | } 119 | } 120 | ``` 121 | 122 | Expected response: 123 | ```json 124 | { 125 | "content": [ 126 | { "type": "text", "text": "# Text Search Results for \"Welcome\"" }, 127 | { "type": "text", "text": "Found 2 matching text nodes:" }, 128 | { "type": "text", "text": "- **Header Text** (ID: 123:456)\n Path: Page 1 > Header > Text\n Text: \"Welcome to our application\"" } 129 | ] 130 | } 131 | ``` 132 | 133 | ### Example: Adding a Comment 134 | 135 | Using the `add_comment` tool: 136 | 137 | ```json 138 | { 139 | "tool": "add_comment", 140 | "parameters": { 141 | "file_key": "abc123xyz789", 142 | "message": "This design looks great! Consider adjusting the contrast on the buttons.", 143 | "node_id": "123:456" 144 | } 145 | } 146 | ``` 147 | 148 | Expected response: 149 | ```json 150 | { 151 | "content": [ 152 | { "type": "text", "text": "Comment added successfully!" }, 153 | { "type": "text", "text": "Comment ID: 987654" }, 154 | { "type": "text", "text": "By user: John Doe" }, 155 | { "type": "text", "text": "Added at: 4/13/2025, 12:34:56 PM" } 156 | ] 157 | } 158 | ``` 159 | 160 | ## 4. Using Resource Templates 161 | 162 | Resource templates provide a consistent way to access Figma resources. 163 | 164 | ### Example: Accessing a File 165 | 166 | Resource URI: `figma-file://abc123xyz789` 167 | 168 | Expected response: 169 | ```json 170 | { 171 | "contents": [{ 172 | "uri": "figma-file://abc123xyz789", 173 | "title": "My Design", 174 | "description": "Last modified: 2025-04-10T15:30:45Z", 175 | "text": "# My Design\n\nLast modified: 2025-04-10T15:30:45Z\n\nDocument contains 5 top-level nodes.\nComponents: 12\nStyles: 8" 176 | }] 177 | } 178 | ``` 179 | 180 | ### Example: Listing Nodes in a File 181 | 182 | Resource URI: `figma-node://abc123xyz789` 183 | 184 | Expected response: 185 | ```json 186 | { 187 | "contents": [ 188 | { 189 | "uri": "figma-node://abc123xyz789/1:1", 190 | "title": "Page 1", 191 | "description": "Type: CANVAS", 192 | "text": "# Page 1\n\nType: CANVAS\nID: 1:1" 193 | }, 194 | { 195 | "uri": "figma-node://abc123xyz789/1:2", 196 | "title": "Page 2", 197 | "description": "Type: CANVAS", 198 | "text": "# Page 2\n\nType: CANVAS\nID: 1:2" 199 | } 200 | ] 201 | } 202 | ``` 203 | 204 | ### Example: Accessing a Specific Node 205 | 206 | Resource URI: `figma-node://abc123xyz789/123:456` 207 | 208 | Expected response: 209 | ```json 210 | { 211 | "contents": [{ 212 | "uri": "figma-node://abc123xyz789/123:456", 213 | "title": "Header Text", 214 | "description": "Type: TEXT", 215 | "text": "# Header Text\n\nType: TEXT\nID: 123:456\nChildren: 0" 216 | }] 217 | } 218 | ``` 219 | 220 | ## 5. Error Handling Examples 221 | 222 | ### Example: File Not Found 223 | 224 | ```json 225 | { 226 | "content": [ 227 | { "type": "text", "text": "Error getting Figma file: File not found" } 228 | ] 229 | } 230 | ``` 231 | 232 | ### Example: Node Not Found 233 | 234 | ```json 235 | { 236 | "content": [ 237 | { "type": "text", "text": "Node 123:456 not found in file abc123xyz789" } 238 | ] 239 | } 240 | ``` 241 | 242 | ### Example: Authentication Error 243 | 244 | ```json 245 | { 246 | "content": [ 247 | { "type": "text", "text": "Error getting Figma file: Authentication failed. Please check your personal access token." } 248 | ] 249 | } 250 | ``` 251 | 252 | ## 6. Tips and Best Practices 253 | 254 | 1. **File Keys**: Obtain file keys from Figma file URLs. The format is typically `https://www.figma.com/file/FILE_KEY/FILE_NAME`. 255 | 256 | 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. 257 | 258 | 3. **Performance**: For large files, use targeted queries with specific node IDs rather than retrieving the entire file. 259 | 260 | 4. **Image Export**: When exporting images, use appropriate scale factors: 1 for normal resolution, 2 for @2x, etc. 261 | 262 | 5. **Comments**: When adding comments, provide node IDs to attach comments to specific elements. 263 | 264 | 6. **Error Handling**: Always handle potential errors in your client application. 265 | 266 | 7. **Resource Caching**: Consider caching resource responses for improved performance in your client application. 267 | ``` -------------------------------------------------------------------------------- /src/plugin/creators/containerCreators.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Container element creation functions for Figma plugin 3 | * Including Frame, Component, and other container-like nodes 4 | */ 5 | 6 | import { createSolidPaint } from '../utils/colorUtils'; 7 | import { applyCommonProperties } from '../utils/nodeUtils'; 8 | 9 | /** 10 | * Create a frame from data 11 | * @param data Frame configuration data 12 | * @returns Created frame node 13 | */ 14 | export function createFrameFromData(data: any): FrameNode { 15 | const frame = figma.createFrame(); 16 | 17 | // Size 18 | frame.resize(data.width || 100, data.height || 100); 19 | 20 | // Background 21 | if (data.fills) { 22 | frame.fills = data.fills; 23 | } else if (data.fill) { 24 | if (typeof data.fill === 'string') { 25 | frame.fills = [createSolidPaint(data.fill)]; 26 | } else { 27 | frame.fills = [data.fill]; 28 | } 29 | } 30 | 31 | // Auto layout properties 32 | if (data.layoutMode) frame.layoutMode = data.layoutMode; 33 | if (data.primaryAxisSizingMode) frame.primaryAxisSizingMode = data.primaryAxisSizingMode; 34 | if (data.counterAxisSizingMode) frame.counterAxisSizingMode = data.counterAxisSizingMode; 35 | if (data.primaryAxisAlignItems) frame.primaryAxisAlignItems = data.primaryAxisAlignItems; 36 | if (data.counterAxisAlignItems) frame.counterAxisAlignItems = data.counterAxisAlignItems; 37 | if (data.paddingLeft !== undefined) frame.paddingLeft = data.paddingLeft; 38 | if (data.paddingRight !== undefined) frame.paddingRight = data.paddingRight; 39 | if (data.paddingTop !== undefined) frame.paddingTop = data.paddingTop; 40 | if (data.paddingBottom !== undefined) frame.paddingBottom = data.paddingBottom; 41 | if (data.itemSpacing !== undefined) frame.itemSpacing = data.itemSpacing; 42 | 43 | // Corner radius 44 | if (data.cornerRadius !== undefined) frame.cornerRadius = data.cornerRadius; 45 | if (data.topLeftRadius !== undefined) frame.topLeftRadius = data.topLeftRadius; 46 | if (data.topRightRadius !== undefined) frame.topRightRadius = data.topRightRadius; 47 | if (data.bottomLeftRadius !== undefined) frame.bottomLeftRadius = data.bottomLeftRadius; 48 | if (data.bottomRightRadius !== undefined) frame.bottomRightRadius = data.bottomRightRadius; 49 | 50 | return frame; 51 | } 52 | 53 | /** 54 | * Create a component from data 55 | * @param data Component configuration data 56 | * @returns Created component node 57 | */ 58 | export function createComponentFromData(data: any): ComponentNode { 59 | const component = figma.createComponent(); 60 | 61 | // Size 62 | component.resize(data.width || 100, data.height || 100); 63 | 64 | // Background 65 | if (data.fills) { 66 | component.fills = data.fills; 67 | } else if (data.fill) { 68 | if (typeof data.fill === 'string') { 69 | component.fills = [createSolidPaint(data.fill)]; 70 | } else { 71 | component.fills = [data.fill]; 72 | } 73 | } 74 | 75 | // Auto layout properties (components support same auto layout as frames) 76 | if (data.layoutMode) component.layoutMode = data.layoutMode; 77 | if (data.primaryAxisSizingMode) component.primaryAxisSizingMode = data.primaryAxisSizingMode; 78 | if (data.counterAxisSizingMode) component.counterAxisSizingMode = data.counterAxisSizingMode; 79 | if (data.primaryAxisAlignItems) component.primaryAxisAlignItems = data.primaryAxisAlignItems; 80 | if (data.counterAxisAlignItems) component.counterAxisAlignItems = data.counterAxisAlignItems; 81 | if (data.paddingLeft !== undefined) component.paddingLeft = data.paddingLeft; 82 | if (data.paddingRight !== undefined) component.paddingRight = data.paddingRight; 83 | if (data.paddingTop !== undefined) component.paddingTop = data.paddingTop; 84 | if (data.paddingBottom !== undefined) component.paddingBottom = data.paddingBottom; 85 | if (data.itemSpacing !== undefined) component.itemSpacing = data.itemSpacing; 86 | 87 | // Component properties 88 | if (data.description) component.description = data.description; 89 | 90 | return component; 91 | } 92 | 93 | /** 94 | * Create a group from data 95 | * Note: Groups require children, so this typically needs to be used after creating child nodes 96 | * 97 | * @param data Group configuration data 98 | * @param children Child nodes to include in the group 99 | * @returns Created group node 100 | */ 101 | export function createGroupFromData(data: any, children: SceneNode[]): GroupNode { 102 | // Create group with the provided children 103 | const group = figma.group(children, figma.currentPage); 104 | 105 | // Apply common properties 106 | applyCommonProperties(group, data); 107 | 108 | return group; 109 | } 110 | 111 | /** 112 | * Create an instance from data 113 | * @param data Instance configuration data (must include componentId) 114 | * @returns Created instance node 115 | */ 116 | export function createInstanceFromData(data: any): InstanceNode | null { 117 | if (!data.componentId) { 118 | console.error('Cannot create instance: componentId is required'); 119 | return null; 120 | } 121 | 122 | // Try to find the component 123 | const component = figma.getNodeById(data.componentId) as ComponentNode; 124 | if (!component || component.type !== 'COMPONENT') { 125 | console.error(`Cannot create instance: component with id ${data.componentId} not found`); 126 | return null; 127 | } 128 | 129 | // Create instance 130 | const instance = component.createInstance(); 131 | 132 | // Apply common properties 133 | applyCommonProperties(instance, data); 134 | 135 | // Handle instance-specific properties 136 | if (data.componentProperties) { 137 | for (const [key, value] of Object.entries(data.componentProperties)) { 138 | if (key in instance.componentProperties) { 139 | // Handle different types of component properties 140 | const prop = instance.componentProperties[key]; 141 | if (prop.type === 'BOOLEAN') { 142 | instance.setProperties({ [key]: !!value }); 143 | } else if (prop.type === 'TEXT') { 144 | instance.setProperties({ [key]: String(value) }); 145 | } else if (prop.type === 'INSTANCE_SWAP') { 146 | instance.setProperties({ [key]: String(value) }); 147 | } else if (prop.type === 'VARIANT') { 148 | instance.setProperties({ [key]: String(value) }); 149 | } 150 | } 151 | } 152 | } 153 | 154 | return instance; 155 | } 156 | 157 | /** 158 | * Create a section from data 159 | * Sections are a special type of node used to organize frames in Figma 160 | * 161 | * @param data Section configuration data 162 | * @returns Created section node 163 | */ 164 | export function createSectionFromData(data: any): SectionNode { 165 | const section = figma.createSection(); 166 | 167 | // Section-specific properties 168 | if (data.name) section.name = data.name; 169 | if (data.sectionContentsHidden !== undefined) section.sectionContentsHidden = data.sectionContentsHidden; 170 | 171 | // Apply common properties that apply to sections 172 | if (data.x !== undefined) section.x = data.x; 173 | if (data.y !== undefined) section.y = data.y; 174 | 175 | return section; 176 | } ``` -------------------------------------------------------------------------------- /src/plugin/creators/elementCreator.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Universal element creator for Figma plugin 3 | * Acts as a central entry point for creating any type of Figma element 4 | */ 5 | 6 | import { 7 | createRectangleFromData, 8 | createEllipseFromData, 9 | createPolygonFromData, 10 | createStarFromData, 11 | createLineFromData, 12 | createVectorFromData 13 | } from './shapeCreators'; 14 | 15 | import { 16 | createFrameFromData, 17 | createComponentFromData, 18 | createInstanceFromData, 19 | createGroupFromData, 20 | createSectionFromData 21 | } from './containerCreators'; 22 | 23 | import { createTextFromData } from './textCreator'; 24 | 25 | import { 26 | createBooleanOperationFromData, 27 | createConnectorFromData, 28 | createShapeWithTextFromData, 29 | createCodeBlockFromData, 30 | createTableFromData, 31 | createWidgetFromData, 32 | createMediaFromData 33 | } from './specialCreators'; 34 | 35 | import { 36 | createImageFromData, 37 | createImageFromBytesAsync, 38 | createGifFromData, 39 | createVideoFromDataAsync, 40 | createLinkPreviewFromDataAsync 41 | } from './imageCreators'; 42 | 43 | import { 44 | createSliceFromData, 45 | createPageFromData, 46 | createPageDividerFromData, 47 | createSlideFromData, 48 | createSlideRowFromData 49 | } from './sliceCreators'; 50 | 51 | import { 52 | createComponentFromNodeData, 53 | createComponentSetFromData 54 | } from './componentCreators'; 55 | 56 | import { applyCommonProperties, selectAndFocusNodes } from '../utils/nodeUtils'; 57 | 58 | /** 59 | * Unified create element function that works with structured data 60 | * Detects the type of element to create based on the data.type property 61 | * 62 | * @param data Configuration data with type and other properties 63 | * @returns Created Figma node or null if creation failed 64 | */ 65 | export async function createElementFromData(data: any): Promise<SceneNode | null> { 66 | if (!data || !data.type) { 67 | console.error('Invalid element data: missing type'); 68 | return null; 69 | } 70 | 71 | let element: SceneNode | null = null; 72 | 73 | try { 74 | // Create the element based on its type 75 | switch (data.type.toLowerCase()) { 76 | // Basic shapes 77 | case 'rectangle': 78 | element = createRectangleFromData(data); 79 | break; 80 | 81 | case 'ellipse': 82 | case 'circle': 83 | element = createEllipseFromData(data); 84 | break; 85 | 86 | case 'polygon': 87 | element = createPolygonFromData(data); 88 | break; 89 | 90 | case 'star': 91 | element = createStarFromData(data); 92 | break; 93 | 94 | case 'line': 95 | element = createLineFromData(data); 96 | break; 97 | 98 | case 'vector': 99 | element = createVectorFromData(data); 100 | break; 101 | 102 | // Container elements 103 | case 'frame': 104 | element = createFrameFromData(data); 105 | break; 106 | 107 | case 'component': 108 | element = createComponentFromData(data); 109 | break; 110 | 111 | case 'componentfromnode': 112 | element = createComponentFromNodeData(data); 113 | break; 114 | 115 | case 'componentset': 116 | element = createComponentSetFromData(data); 117 | break; 118 | 119 | case 'instance': 120 | element = createInstanceFromData(data); 121 | break; 122 | 123 | case 'section': 124 | element = createSectionFromData(data); 125 | break; 126 | 127 | // Text 128 | case 'text': 129 | element = await createTextFromData(data); 130 | break; 131 | 132 | // Special types 133 | case 'boolean': 134 | case 'booleanoperation': 135 | element = createBooleanOperationFromData(data); 136 | break; 137 | 138 | case 'connector': 139 | element = createConnectorFromData(data); 140 | break; 141 | 142 | case 'shapewithtext': 143 | element = createShapeWithTextFromData(data); 144 | break; 145 | 146 | case 'codeblock': 147 | element = createCodeBlockFromData(data); 148 | break; 149 | 150 | case 'table': 151 | element = createTableFromData(data); 152 | break; 153 | 154 | case 'widget': 155 | element = createWidgetFromData(data); 156 | break; 157 | 158 | case 'media': 159 | element = createMediaFromData(data); 160 | break; 161 | 162 | // Image and media types 163 | case 'image': 164 | if (data.bytes || data.file) { 165 | element = await createImageFromBytesAsync(data); 166 | } else { 167 | element = createImageFromData(data); 168 | } 169 | break; 170 | 171 | case 'gif': 172 | element = createGifFromData(data); 173 | break; 174 | 175 | case 'video': 176 | element = await createVideoFromDataAsync(data); 177 | break; 178 | 179 | case 'linkpreview': 180 | element = await createLinkPreviewFromDataAsync(data); 181 | break; 182 | 183 | // Page and slice types 184 | case 'slice': 185 | element = createSliceFromData(data); 186 | break; 187 | 188 | case 'page': 189 | // PageNode is not a SceneNode in Figma's type system 190 | // So we create it but don't return it through the same path 191 | const page = createPageFromData(data); 192 | console.log(`Created page: ${page.name}`); 193 | // We return null as we can't return a PageNode as SceneNode 194 | return null; 195 | 196 | case 'pagedivider': 197 | element = createPageDividerFromData(data); 198 | break; 199 | 200 | case 'slide': 201 | element = createSlideFromData(data); 202 | break; 203 | 204 | case 'sliderow': 205 | element = createSlideRowFromData(data); 206 | break; 207 | 208 | // Special cases 209 | case 'group': 210 | if (!data.children || !Array.isArray(data.children) || data.children.length < 1) { 211 | console.error('Cannot create group: children array is required'); 212 | return null; 213 | } 214 | 215 | // Create all child elements first 216 | const childNodes: SceneNode[] = []; 217 | for (const childData of data.children) { 218 | const child = await createElementFromData(childData); 219 | if (child) childNodes.push(child); 220 | } 221 | 222 | if (childNodes.length > 0) { 223 | element = createGroupFromData(data, childNodes); 224 | } else { 225 | console.error('Cannot create group: no valid children were created'); 226 | return null; 227 | } 228 | break; 229 | 230 | default: 231 | console.error(`Unsupported element type: ${data.type}`); 232 | return null; 233 | } 234 | 235 | // Apply common properties if element was created 236 | if (element) { 237 | applyCommonProperties(element, data); 238 | 239 | // Select and focus on the element if requested 240 | if (data.select !== false) { 241 | selectAndFocusNodes(element); 242 | } 243 | } 244 | 245 | return element; 246 | } catch (error) { 247 | console.error(`Error creating element: ${error instanceof Error ? error.message : 'Unknown error'}`); 248 | return null; 249 | } 250 | } 251 | 252 | /** 253 | * Create multiple elements from an array of data objects 254 | * @param dataArray Array of element configuration data 255 | * @returns Array of created nodes 256 | */ 257 | export async function createElementsFromDataArray(dataArray: any[]): Promise<SceneNode[]> { 258 | const createdNodes: SceneNode[] = []; 259 | 260 | for (const data of dataArray) { 261 | const node = await createElementFromData(data); 262 | if (node) createdNodes.push(node); 263 | } 264 | 265 | // If there are created nodes, select them all at the end 266 | if (createdNodes.length > 0) { 267 | selectAndFocusNodes(createdNodes); 268 | } 269 | 270 | return createdNodes; 271 | } ``` -------------------------------------------------------------------------------- /src/tools/page.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Page tools for the Figma MCP server 3 | */ 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | import { z } from "zod"; 6 | import { 7 | isPluginConnected, 8 | sendCommandToPlugin, 9 | } from "../services/websocket.js"; 10 | 11 | // Define interfaces for page data 12 | interface PageData { 13 | id: string; 14 | name: string; 15 | children?: any[]; 16 | [key: string]: any; 17 | } 18 | 19 | export const getPagesTool = (server: McpServer) => { 20 | server.tool("get_pages", {}, async () => { 21 | try { 22 | // Get pages using WebSocket only if plugin is connected 23 | if (!isPluginConnected()) { 24 | return { 25 | content: [ 26 | { 27 | type: "text", 28 | text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", 29 | }, 30 | ], 31 | }; 32 | } 33 | 34 | const response = await sendCommandToPlugin("get-pages", {}); 35 | 36 | if (!response.success) { 37 | throw new Error(response.error || "Failed to get pages"); 38 | } 39 | 40 | // Process the response to handle different result formats 41 | const result = response.result || {}; 42 | const pages = result.items || []; 43 | const pagesCount = result.count || 0; 44 | 45 | // Check if we have pages to display 46 | if (pagesCount === 0 && !pages.length) { 47 | return { 48 | content: [ 49 | { type: "text", text: `# Pages in Figma File` }, 50 | { type: "text", text: `No pages found in the current Figma file.` }, 51 | ], 52 | }; 53 | } 54 | 55 | return { 56 | content: [ 57 | { type: "text", text: `# Pages in Figma File` }, 58 | { type: "text", text: `Found ${pagesCount || pages.length} pages:` }, 59 | { 60 | type: "text", 61 | text: pages 62 | .map((page: PageData) => `- ${page.name} (ID: ${page.id})`) 63 | .join("\n"), 64 | }, 65 | ], 66 | }; 67 | } catch (error) { 68 | console.error("Error fetching pages:", error); 69 | return { 70 | content: [ 71 | { 72 | type: "text", 73 | text: `Error getting pages: ${(error as Error).message}`, 74 | }, 75 | ], 76 | }; 77 | } 78 | }); 79 | }; 80 | 81 | export const getPageTool = (server: McpServer) => { 82 | server.tool( 83 | "get_page", 84 | { 85 | page_id: z.string().min(1).describe("The ID of the page to retrieve").optional(), 86 | }, 87 | async ({ page_id }) => { 88 | try { 89 | // Get page using WebSocket only if plugin is connected 90 | if (!isPluginConnected()) { 91 | return { 92 | content: [ 93 | { 94 | type: "text", 95 | text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", 96 | }, 97 | ], 98 | }; 99 | } 100 | 101 | // If page_id is not provided, get the current page 102 | const response = await sendCommandToPlugin("get-page", { 103 | page_id, 104 | }); 105 | 106 | if (!response.success) { 107 | throw new Error(response.error || "Failed to get page"); 108 | } 109 | 110 | const pageNode = response.result; 111 | 112 | return { 113 | content: [ 114 | { type: "text", text: `# Page: ${pageNode.name}` }, 115 | { type: "text", text: `ID: ${pageNode.id}` }, 116 | { type: "text", text: `Type: ${pageNode.type}` }, 117 | { 118 | type: "text", 119 | text: `Elements: ${pageNode.children?.length || 0}`, 120 | }, 121 | { 122 | type: "text", 123 | text: "```json\n" + JSON.stringify(pageNode, null, 2) + "\n```", 124 | }, 125 | ], 126 | }; 127 | } catch (error) { 128 | console.error("Error fetching page:", error); 129 | return { 130 | content: [ 131 | { 132 | type: "text", 133 | text: `Error getting page: ${(error as Error).message}`, 134 | }, 135 | ], 136 | }; 137 | } 138 | } 139 | ); 140 | }; 141 | 142 | export const createPageTool = (server: McpServer) => { 143 | server.tool( 144 | "create_page", 145 | { 146 | page_name: z.string().min(1).describe("Name for the new page"), 147 | }, 148 | async ({ page_name }) => { 149 | try { 150 | if (!isPluginConnected()) { 151 | return { 152 | content: [ 153 | { 154 | type: "text", 155 | text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", 156 | }, 157 | ], 158 | }; 159 | } 160 | 161 | // Use WebSocket to send command to plugin 162 | const response = await sendCommandToPlugin("create-page", { 163 | name: page_name, 164 | }); 165 | 166 | if (!response.success) { 167 | throw new Error(response.error || "Failed to create page"); 168 | } 169 | 170 | return { 171 | content: [ 172 | { 173 | type: "text", 174 | text: `# Page Created Successfully`, 175 | }, 176 | { 177 | type: "text", 178 | text: `A new page named "${page_name}" has been created.`, 179 | }, 180 | { 181 | type: "text", 182 | text: 183 | response.result && response.result.id 184 | ? `Page ID: ${response.result.id}` 185 | : `Creation successful`, 186 | }, 187 | ], 188 | }; 189 | } catch (error) { 190 | console.error("Error creating page:", error); 191 | return { 192 | content: [ 193 | { 194 | type: "text", 195 | text: `Error creating page: ${(error as Error).message}`, 196 | }, 197 | ], 198 | }; 199 | } 200 | } 201 | ); 202 | }; 203 | 204 | export const switchPageTool = (server: McpServer) => { 205 | server.tool( 206 | "switch_page", 207 | { 208 | page_id: z.string().min(1).describe("The ID of the page to switch to"), 209 | }, 210 | async ({ page_id }) => { 211 | try { 212 | if (!isPluginConnected()) { 213 | return { 214 | content: [ 215 | { 216 | type: "text", 217 | text: "No Figma plugin is connected. Please make sure the Figma plugin is running and connected to the MCP server.", 218 | }, 219 | ], 220 | }; 221 | } 222 | 223 | // Use WebSocket to send command to plugin 224 | const response = await sendCommandToPlugin("switch-page", { 225 | id: page_id, // Note: plugin expects 'id', not 'page_id' 226 | }); 227 | 228 | if (!response.success) { 229 | throw new Error(response.error || "Failed to switch page"); 230 | } 231 | 232 | return { 233 | content: [ 234 | { 235 | type: "text", 236 | text: `# Page Switched Successfully`, 237 | }, 238 | { 239 | type: "text", 240 | text: `Successfully switched to page with ID: ${page_id}`, 241 | }, 242 | { 243 | type: "text", 244 | text: 245 | response.result && response.result.name 246 | ? `Current page: ${response.result.name}` 247 | : `Switch successful`, 248 | }, 249 | ], 250 | }; 251 | } catch (error) { 252 | console.error("Error switching page:", error); 253 | return { 254 | content: [ 255 | { 256 | type: "text", 257 | text: `Error switching page: ${(error as Error).message}`, 258 | }, 259 | ], 260 | }; 261 | } 262 | } 263 | ); 264 | }; 265 | 266 | /** 267 | * Registers all page-related tools with the MCP server 268 | */ 269 | export const registerPageTools = (server: McpServer): void => { 270 | getPagesTool(server); 271 | getPageTool(server); 272 | createPageTool(server); 273 | switchPageTool(server); 274 | }; 275 | ``` -------------------------------------------------------------------------------- /src/plugin/creators/textCreator.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Text element creation functions for Figma plugin 3 | */ 4 | 5 | import { createSolidPaint } from '../utils/colorUtils'; 6 | import { selectAndFocusNodes } from '../utils/nodeUtils'; 7 | 8 | /** 9 | * Create a text node from data 10 | * @param data Text configuration data 11 | * @returns Created text node 12 | */ 13 | export async function createTextFromData(data: any): Promise<TextNode> { 14 | const text = figma.createText(); 15 | 16 | // Load font - default to Inter if not specified 17 | const fontFamily = data.fontFamily || (data.fontName.family) || "Inter"; 18 | const fontStyle = data.fontStyle || (data.fontName.style) || "Regular"; 19 | 20 | // Load the font before setting text 21 | try { 22 | await figma.loadFontAsync({ family: fontFamily, style: fontStyle }); 23 | } catch (error) { 24 | console.warn(`Failed to load font ${fontFamily} ${fontStyle}. Falling back to Inter Regular.`); 25 | await figma.loadFontAsync({ family: "Inter", style: "Regular" }); 26 | } 27 | 28 | // Set basic text content 29 | text.characters = data.text || data.characters || "Text"; 30 | 31 | // Position and size 32 | if (data.x !== undefined) text.x = data.x; 33 | if (data.y !== undefined) text.y = data.y; 34 | 35 | // Text size and dimensions 36 | if (data.fontSize) text.fontSize = data.fontSize; 37 | if (data.width) text.resize(data.width, text.height); 38 | 39 | // Text style and alignment 40 | if (data.fontName) text.fontName = data.fontName; 41 | if (data.textAlignHorizontal) text.textAlignHorizontal = data.textAlignHorizontal; 42 | if (data.textAlignVertical) text.textAlignVertical = data.textAlignVertical; 43 | if (data.textAutoResize) text.textAutoResize = data.textAutoResize; 44 | if (data.textTruncation) text.textTruncation = data.textTruncation; 45 | if (data.maxLines !== undefined) text.maxLines = data.maxLines; 46 | 47 | // Paragraph styling 48 | if (data.paragraphIndent) text.paragraphIndent = data.paragraphIndent; 49 | if (data.paragraphSpacing) text.paragraphSpacing = data.paragraphSpacing; 50 | if (data.listSpacing) text.listSpacing = data.listSpacing; 51 | if (data.hangingPunctuation !== undefined) text.hangingPunctuation = data.hangingPunctuation; 52 | if (data.hangingList !== undefined) text.hangingList = data.hangingList; 53 | if (data.autoRename !== undefined) text.autoRename = data.autoRename; 54 | 55 | // Text styling 56 | if (data.letterSpacing) text.letterSpacing = data.letterSpacing; 57 | if (data.lineHeight) text.lineHeight = data.lineHeight; 58 | if (data.leadingTrim) text.leadingTrim = data.leadingTrim; 59 | if (data.textCase) text.textCase = data.textCase; 60 | if (data.textDecoration) text.textDecoration = data.textDecoration; 61 | if (data.textStyleId) text.textStyleId = data.textStyleId; 62 | 63 | // Text decoration details 64 | if (data.textDecorationStyle) text.textDecorationStyle = data.textDecorationStyle; 65 | if (data.textDecorationOffset) text.textDecorationOffset = data.textDecorationOffset; 66 | if (data.textDecorationThickness) text.textDecorationThickness = data.textDecorationThickness; 67 | if (data.textDecorationColor) text.textDecorationColor = data.textDecorationColor; 68 | if (data.textDecorationSkipInk !== undefined) text.textDecorationSkipInk = data.textDecorationSkipInk; 69 | 70 | // Text fill 71 | if (data.fills) { 72 | text.fills = data.fills; 73 | } else if (data.fill) { 74 | if (typeof data.fill === 'string') { 75 | text.fills = [createSolidPaint(data.fill)]; 76 | } else { 77 | text.fills = [data.fill]; 78 | } 79 | } 80 | 81 | // Text hyperlink 82 | if (data.hyperlink) { 83 | text.hyperlink = data.hyperlink; 84 | } 85 | 86 | // Layout properties 87 | if (data.layoutAlign) text.layoutAlign = data.layoutAlign; 88 | if (data.layoutGrow !== undefined) text.layoutGrow = data.layoutGrow; 89 | if (data.layoutSizingHorizontal) text.layoutSizingHorizontal = data.layoutSizingHorizontal; 90 | if (data.layoutSizingVertical) text.layoutSizingVertical = data.layoutSizingVertical; 91 | 92 | // Apply text range styles if provided 93 | if (data.rangeStyles && Array.isArray(data.rangeStyles)) { 94 | applyTextRangeStyles(text, data.rangeStyles); 95 | } 96 | 97 | // Apply common base properties 98 | if (data.name) text.name = data.name; 99 | if (data.visible !== undefined) text.visible = data.visible; 100 | if (data.locked !== undefined) text.locked = data.locked; 101 | if (data.opacity !== undefined) text.opacity = data.opacity; 102 | if (data.blendMode) text.blendMode = data.blendMode; 103 | if (data.effects) text.effects = data.effects; 104 | if (data.effectStyleId) text.effectStyleId = data.effectStyleId; 105 | if (data.exportSettings) text.exportSettings = data.exportSettings; 106 | if (data.constraints) text.constraints = data.constraints; 107 | 108 | return text; 109 | } 110 | 111 | /** 112 | * Create a simple text node with basic properties 113 | * @param x X coordinate 114 | * @param y Y coordinate 115 | * @param content Text content 116 | * @param fontSize Font size 117 | * @returns Created text node 118 | */ 119 | export async function createText(x: number, y: number, content: string, fontSize: number): Promise<TextNode> { 120 | // Use the data-driven function 121 | const text = await createTextFromData({ 122 | text: content, 123 | fontSize, 124 | x, 125 | y 126 | }); 127 | 128 | // Select and focus 129 | selectAndFocusNodes(text); 130 | 131 | return text; 132 | } 133 | 134 | /** 135 | * Apply character-level styling to text ranges in a text node 136 | * @param textNode Text node to style 137 | * @param ranges Array of range objects with start, end, and style properties 138 | */ 139 | export function applyTextRangeStyles(textNode: TextNode, ranges: Array<{start: number, end: number, style: any}>): void { 140 | for (const range of ranges) { 141 | // Apply individual style properties to the range 142 | for (const [property, value] of Object.entries(range.style)) { 143 | if (property === 'fills') { 144 | textNode.setRangeFills(range.start, range.end, value as Paint[]); 145 | } else if (property === 'fillStyleId') { 146 | textNode.setRangeFillStyleId(range.start, range.end, value as string); 147 | } else if (property === 'fontName') { 148 | textNode.setRangeFontName(range.start, range.end, value as FontName); 149 | } else if (property === 'fontSize') { 150 | textNode.setRangeFontSize(range.start, range.end, value as number); 151 | } else if (property === 'textCase') { 152 | textNode.setRangeTextCase(range.start, range.end, value as TextCase); 153 | } else if (property === 'textDecoration') { 154 | textNode.setRangeTextDecoration(range.start, range.end, value as TextDecoration); 155 | } else if (property === 'textDecorationStyle') { 156 | textNode.setRangeTextDecorationStyle(range.start, range.end, value as TextDecorationStyle); 157 | } else if (property === 'textDecorationOffset') { 158 | textNode.setRangeTextDecorationOffset(range.start, range.end, value as TextDecorationOffset); 159 | } else if (property === 'textDecorationThickness') { 160 | textNode.setRangeTextDecorationThickness(range.start, range.end, value as TextDecorationThickness); 161 | } else if (property === 'textDecorationColor') { 162 | textNode.setRangeTextDecorationColor(range.start, range.end, value as TextDecorationColor); 163 | } else if (property === 'textDecorationSkipInk') { 164 | textNode.setRangeTextDecorationSkipInk(range.start, range.end, value as boolean); 165 | } else if (property === 'letterSpacing') { 166 | textNode.setRangeLetterSpacing(range.start, range.end, value as LetterSpacing); 167 | } else if (property === 'lineHeight') { 168 | textNode.setRangeLineHeight(range.start, range.end, value as LineHeight); 169 | } else if (property === 'hyperlink') { 170 | textNode.setRangeHyperlink(range.start, range.end, value as HyperlinkTarget); 171 | } else if (property === 'textStyleId') { 172 | textNode.setRangeTextStyleId(range.start, range.end, value as string); 173 | } else if (property === 'indentation') { 174 | textNode.setRangeIndentation(range.start, range.end, value as number); 175 | } else if (property === 'paragraphIndent') { 176 | textNode.setRangeParagraphIndent(range.start, range.end, value as number); 177 | } else if (property === 'paragraphSpacing') { 178 | textNode.setRangeParagraphSpacing(range.start, range.end, value as number); 179 | } else if (property === 'listOptions') { 180 | textNode.setRangeListOptions(range.start, range.end, value as TextListOptions); 181 | } else if (property === 'listSpacing') { 182 | textNode.setRangeListSpacing(range.start, range.end, value as number); 183 | } 184 | } 185 | } 186 | } ```