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 | }
```