# Directory Structure
```
├── .gitignore
├── LICENSE
├── mcp-settings-example.json
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── accessibility.ts
│ ├── component-generator.ts
│ ├── figma-client.ts
│ ├── figma-react-tools.ts
│ ├── figma-tailwind-converter.ts
│ ├── figma-tools.ts
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependency directories
node_modules/
# Build output
dist/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
pnpm-lock.yaml
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Figma to React Converter
This is a Model Context Protocol (MCP) server that converts Figma designs to React components. It provides tools for fetching Figma designs and generating React components with TypeScript and Tailwind CSS.
## Features
- Fetch Figma designs using the Figma API
- Extract components from Figma designs
- Generate React components with TypeScript
- Apply Tailwind CSS classes based on Figma styles
- Enhance components with accessibility features
- Support for both stdio and SSE transports
## Prerequisites
- Node.js 18 or higher
- A Figma API token
## Installation
1. Clone the repository
2. Install dependencies:
```bash
npm install
```
3. Build the project:
```bash
npm run build
```
## Configuration
You need to set the `FIGMA_API_TOKEN` environment variable to your Figma API token. You can get a personal access token from the Figma account settings page.
## Usage
### Running as a local MCP server
```bash
FIGMA_API_TOKEN=your_token_here npm start
```
Or with explicit transport:
```bash
FIGMA_API_TOKEN=your_token_here node dist/index.js --transport=stdio
```
### Running as an HTTP server
```bash
FIGMA_API_TOKEN=your_token_here node dist/index.js --transport=sse
```
## Available Tools
### Figma Tools
- `getFigmaProject`: Get a Figma project structure
- `getFigmaComponentNodes`: Get component nodes from a Figma file
- `extractFigmaComponents`: Extract components from a Figma file
- `getFigmaComponentSets`: Get component sets from a Figma file
### React Tools
- `generateReactComponent`: Generate a React component from a Figma node
- `generateComponentLibrary`: Generate multiple React components from Figma components
- `writeComponentsToFiles`: Write generated components to files
- `figmaToReactWorkflow`: Complete workflow to convert Figma designs to React components
## Example Workflow
1. Get a Figma file key (the string after `figma.com/file/` in the URL)
2. Use the `figmaToReactWorkflow` tool with the file key and output directory
3. The tool will extract components, generate React code, and save the files
## Development
For development, you can use the watch mode:
```bash
npm run dev
```
## License
ISC
```
--------------------------------------------------------------------------------
/mcp-settings-example.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"figma-to-react": {
"command": "node",
"args": ["path/to/mcp-figma-to-react/dist/index.js", "--transport=stdio"],
"env": {
"FIGMA_API_TOKEN": "your_figma_api_token_here"
},
"disabled": false,
"alwaysAllow": []
}
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-figma-to-react",
"version": "1.0.0",
"description": "MCP server for converting Figma designs to React components",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc -w & node --watch dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"figma",
"react",
"mcp",
"component-generator"
],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "1.10.2",
"axios": "^1.8.4",
"express": "5.1.0",
"prettier": "^3.5.3",
"typescript": "5.8.3",
"ws": "^8.18.1",
"zod": "3.24.3"
},
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "22.14.1",
"@types/ws": "8.18.1"
}
}
```
--------------------------------------------------------------------------------
/src/figma-client.ts:
--------------------------------------------------------------------------------
```typescript
import axios from 'axios';
// Define types for Figma API responses
export interface FigmaNode {
id: string;
name: string;
type: string;
[key: string]: any;
}
export interface GetFileResponse {
document: {
children: FigmaNode[];
[key: string]: any;
};
components: Record<string, any>;
styles: Record<string, any>;
[key: string]: any;
}
export interface GetFileNodesResponse {
nodes: Record<string, {
document: FigmaNode;
components?: Record<string, any>;
styles?: Record<string, any>;
[key: string]: any;
}>;
}
export class FigmaClient {
private token: string;
private baseUrl = 'https://api.figma.com/v1';
constructor(token: string) {
this.token = token;
}
async getFile(fileKey: string): Promise<GetFileResponse> {
try {
const response = await axios.get<GetFileResponse>(
`${this.baseUrl}/files/${fileKey}`,
{
headers: {
'X-Figma-Token': this.token
}
}
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
async getFileNodes(fileKey: string, nodeIds: string[]): Promise<GetFileNodesResponse> {
try {
const response = await axios.get<GetFileNodesResponse>(
`${this.baseUrl}/files/${fileKey}/nodes`,
{
params: { ids: nodeIds.join(',') },
headers: {
'X-Figma-Token': this.token
}
}
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
async getImageFills(fileKey: string, nodeIds: string[]): Promise<Record<string, string>> {
try {
const response = await axios.get(
`${this.baseUrl}/images/${fileKey}`,
{
params: { ids: nodeIds.join(','), format: 'png' },
headers: {
'X-Figma-Token': this.token
}
}
);
return response.data.images || {};
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
async getComponentSets(fileKey: string): Promise<any> {
try {
const response = await axios.get(
`${this.baseUrl}/files/${fileKey}/component_sets`,
{
headers: {
'X-Figma-Token': this.token
}
}
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
}
throw error;
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import { FigmaClient } from './figma-client.js';
import { registerFigmaTools } from './figma-tools.js';
import { ComponentGenerator } from './component-generator.js';
import { registerReactTools } from './figma-react-tools.js';
// Get Figma API token from environment variable
const FIGMA_API_TOKEN = process.env.FIGMA_API_TOKEN;
if (!FIGMA_API_TOKEN) {
console.error('FIGMA_API_TOKEN environment variable is required');
process.exit(1);
}
// Create the MCP server
const server = new McpServer({
name: 'Figma to React Converter',
version: '1.0.0',
description: 'MCP server for converting Figma designs to React components'
});
// Initialize Figma client and component generator
const figmaClient = new FigmaClient(FIGMA_API_TOKEN);
const componentGenerator = new ComponentGenerator();
// Register tools with the server
registerFigmaTools(server, figmaClient);
registerReactTools(server, componentGenerator, figmaClient);
// Determine the transport to use based on command-line arguments
const transportArg = process.argv.find(arg => arg.startsWith('--transport='));
const transportType = transportArg ? transportArg.split('=')[1] : 'stdio';
async function main() {
try {
if (transportType === 'stdio') {
// Use stdio transport for local MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Figma to React MCP server running on stdio');
} else if (transportType === 'sse') {
// Set up Express server
const app = express();
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
app.use(express.json());
// Health check endpoint
app.get('/health', (_req, res) => {
res.status(200).send('OK');
});
// SSE endpoint
app.get('/sse', async (req: express.Request, res: express.Response) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const transport = new SSEServerTransport('/messages', res);
await server.connect(transport);
req.on('close', async () => {
await server.close();
});
});
// Message endpoint
app.post('/messages', express.json(), async (req: express.Request, res: express.Response) => {
// This endpoint would be used by the client to send messages to the server
res.status(200).json({ status: 'ok' });
});
// Start the Express server
const httpServer = app.listen(port, () => {
console.error(`Figma to React MCP server running on port ${port}`);
});
// Handle server shutdown
process.on('SIGINT', async () => {
console.error('Shutting down server...');
await server.close();
httpServer.close();
process.exit(0);
});
} else {
console.error(`Unsupported transport type: ${transportType}`);
process.exit(1);
}
} catch (error) {
console.error('Error starting server:', error);
process.exit(1);
}
}
// Start the server
main().catch(console.error);
```
--------------------------------------------------------------------------------
/src/figma-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { FigmaClient } from './figma-client.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
export function registerFigmaTools(server: McpServer, figmaClient: FigmaClient): void {
// Register getFigmaProject tool
server.tool(
'getFigmaProject',
{
fileKey: z.string().describe('Figma file key')
},
async ({ fileKey }) => {
try {
const fileData = await figmaClient.getFile(fileKey);
return {
content: [
{
type: 'text',
text: JSON.stringify({
name: fileData.name,
documentId: fileData.document.id,
lastModified: fileData.lastModified,
version: fileData.version,
componentCount: Object.keys(fileData.components || {}).length,
styleCount: Object.keys(fileData.styles || {}).length
}, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to get Figma project: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register getFigmaComponentNodes tool
server.tool(
'getFigmaComponentNodes',
{
fileKey: z.string().describe('Figma file key'),
nodeIds: z.array(z.string()).describe('Node IDs to fetch')
},
async ({ fileKey, nodeIds }) => {
try {
const nodesData = await figmaClient.getFileNodes(fileKey, nodeIds);
return {
content: [
{
type: 'text',
text: JSON.stringify(nodesData.nodes, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to get Figma component nodes: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register extractFigmaComponents tool
server.tool(
'extractFigmaComponents',
{
fileKey: z.string().describe('Figma file key')
},
async ({ fileKey }) => {
try {
const fileData = await figmaClient.getFile(fileKey);
// Find all components in the file
const components: Array<{ id: string, name: string, type: string }> = [];
// Helper function to recursively traverse the document
function findComponents(node: any) {
if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
components.push({
id: node.id,
name: node.name,
type: node.type
});
}
if (node.children) {
for (const child of node.children) {
findComponents(child);
}
}
}
// Start traversal from the document root
findComponents(fileData.document);
return {
content: [
{
type: 'text',
text: JSON.stringify({ components }, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to extract Figma components: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register getFigmaComponentSets tool
server.tool(
'getFigmaComponentSets',
{
fileKey: z.string().describe('Figma file key')
},
async ({ fileKey }) => {
try {
const componentSets = await figmaClient.getComponentSets(fileKey);
return {
content: [
{
type: 'text',
text: JSON.stringify(componentSets, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to get Figma component sets: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
}
```
--------------------------------------------------------------------------------
/src/accessibility.ts:
--------------------------------------------------------------------------------
```typescript
import { FigmaNode } from './figma-client.js';
// Determine the likely heading level based on font size and other properties
function determineLikelyHeadingLevel(figmaNode: any): number {
if (!figmaNode.style) return 2; // Default to h2 if no style information
const { fontSize, fontWeight } = figmaNode.style;
// Use font size as the primary indicator
if (fontSize >= 32) return 1;
if (fontSize >= 24) return 2;
if (fontSize >= 20) return 3;
if (fontSize >= 18) return 4;
if (fontSize >= 16) return 5;
return 6;
}
// Check if a node is likely a button based on its properties
function isLikelyButton(figmaNode: FigmaNode): boolean {
// Check name for button-related keywords
const nameLower = figmaNode.name.toLowerCase();
if (nameLower.includes('button') || nameLower.includes('btn')) return true;
// Check for common button styles
if (figmaNode.cornerRadius && figmaNode.cornerRadius > 0) {
// Buttons often have rounded corners
if (figmaNode.fills && figmaNode.fills.length > 0) {
// Buttons typically have a background fill
return true;
}
}
return false;
}
// Check if a node is likely an image
function isLikelyImage(figmaNode: FigmaNode): boolean {
if (figmaNode.type === 'IMAGE') return true;
const nameLower = figmaNode.name.toLowerCase();
if (nameLower.includes('image') || nameLower.includes('img') || nameLower.includes('icon')) return true;
// Check for image fills
if (figmaNode.fills) {
for (const fill of figmaNode.fills) {
if (fill.type === 'IMAGE') return true;
}
}
return false;
}
// Check if a node is likely an input field
function isLikelyInputField(figmaNode: FigmaNode): boolean {
const nameLower = figmaNode.name.toLowerCase();
return nameLower.includes('input') ||
nameLower.includes('field') ||
nameLower.includes('text field') ||
nameLower.includes('form field');
}
// Generate appropriate alt text for an image
function generateAltText(figmaNode: FigmaNode): string {
// Start with the node name
let altText = figmaNode.name;
// Remove common prefixes/suffixes that aren't useful in alt text
altText = altText.replace(/^(img|image|icon|pic|picture)[-_\s]*/i, '');
altText = altText.replace(/[-_\s]*(img|image|icon|pic|picture)$/i, '');
// If the alt text is empty or just contains "image" or similar, use a more generic description
if (!altText || /^(img|image|icon|pic|picture)$/i.test(altText)) {
altText = 'Image';
}
return altText;
}
export function enhanceWithAccessibility(jsx: string, figmaNode: FigmaNode): string {
let enhancedJsx = jsx;
// Add appropriate ARIA attributes based on component type
if (figmaNode.type === 'TEXT' || (figmaNode.characters && figmaNode.characters.length > 0)) {
// For text elements, check if they might be headings
if (figmaNode.style && figmaNode.style.fontSize >= 16) {
const headingLevel = determineLikelyHeadingLevel(figmaNode);
enhancedJsx = enhancedJsx.replace(/<div([^>]*)>(.*?)<\/div>/g, `<h${headingLevel}$1>$2</h${headingLevel}>`);
}
}
// Add alt text to images
if (isLikelyImage(figmaNode)) {
const altText = generateAltText(figmaNode);
if (enhancedJsx.includes('<img')) {
enhancedJsx = enhancedJsx.replace(/<img([^>]*)>/g, `<img$1 alt="${altText}">`);
} else if (enhancedJsx.includes('<Image')) {
enhancedJsx = enhancedJsx.replace(/<Image([^>]*)>/g, `<Image$1 alt="${altText}">`);
} else {
// If it's a div with a background image, add role="img" and aria-label
enhancedJsx = enhancedJsx.replace(
/<div([^>]*)>/g,
`<div$1 role="img" aria-label="${altText}">`
);
}
}
// Add appropriate role attributes for interactive elements
if (isLikelyButton(figmaNode)) {
if (!enhancedJsx.includes('<button')) {
enhancedJsx = enhancedJsx.replace(
/<div([^>]*)>/g,
'<div$1 role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && onClick && onClick(e)}>'
);
// If there's an onClick handler, make sure it's keyboard accessible
enhancedJsx = enhancedJsx.replace(
/onClick={([^}]+)}/g,
'onClick={$1}'
);
}
}
// Add label and appropriate attributes for input fields
if (isLikelyInputField(figmaNode)) {
const inputId = `input-${figmaNode.id.replace(/[^a-zA-Z0-9]/g, '-')}`;
const labelText = figmaNode.name.replace(/input|field|text field|form field/gi, '').trim() || 'Input';
if (enhancedJsx.includes('<input')) {
enhancedJsx = enhancedJsx.replace(
/<input([^>]*)>/g,
`<label htmlFor="${inputId}">${labelText}<input$1 id="${inputId}" aria-label="${labelText}"></label>`
);
} else {
// If it's a div that should be an input, transform it
enhancedJsx = enhancedJsx.replace(
/<div([^>]*)>(.*?)<\/div>/g,
`<label htmlFor="${inputId}">${labelText}<input$1 id="${inputId}" aria-label="${labelText}" /></label>`
);
}
}
return enhancedJsx;
}
```
--------------------------------------------------------------------------------
/src/figma-tailwind-converter.ts:
--------------------------------------------------------------------------------
```typescript
// Helper functions for color conversion
function rgbToHex(r: number, g: number, b: number): string {
return '#' + [r, g, b]
.map(x => Math.round(x).toString(16).padStart(2, '0'))
.join('');
}
// Map RGB color to closest Tailwind color
function mapToTailwindColor(hexColor: string): string {
// This is a simplified implementation
// In a real-world scenario, you would have a more comprehensive mapping
const tailwindColors: Record<string, string> = {
'#000000': 'text-black',
'#ffffff': 'text-white',
'#ef4444': 'text-red-500',
'#3b82f6': 'text-blue-500',
'#10b981': 'text-green-500',
'#f59e0b': 'text-yellow-500',
'#6366f1': 'text-indigo-500',
'#8b5cf6': 'text-purple-500',
'#ec4899': 'text-pink-500',
'#6b7280': 'text-gray-500',
// Add more color mappings as needed
};
// Find the closest color by calculating the distance in RGB space
let minDistance = Number.MAX_VALUE;
let closestColor = 'text-black';
const r1 = parseInt(hexColor.slice(1, 3), 16);
const g1 = parseInt(hexColor.slice(3, 5), 16);
const b1 = parseInt(hexColor.slice(5, 7), 16);
for (const [color, className] of Object.entries(tailwindColors)) {
const r2 = parseInt(color.slice(1, 3), 16);
const g2 = parseInt(color.slice(3, 5), 16);
const b2 = parseInt(color.slice(5, 7), 16);
const distance = Math.sqrt(
Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2)
);
if (distance < minDistance) {
minDistance = distance;
closestColor = className;
}
}
return closestColor;
}
// Map font size to Tailwind class
function mapFontSizeToTailwind(fontSize: number): string {
if (fontSize <= 12) return 'text-xs';
if (fontSize <= 14) return 'text-sm';
if (fontSize <= 16) return 'text-base';
if (fontSize <= 18) return 'text-lg';
if (fontSize <= 20) return 'text-xl';
if (fontSize <= 24) return 'text-2xl';
if (fontSize <= 30) return 'text-3xl';
if (fontSize <= 36) return 'text-4xl';
if (fontSize <= 48) return 'text-5xl';
return 'text-6xl';
}
// Map font weight to Tailwind class
function mapFontWeightToTailwind(fontWeight: number): string {
if (fontWeight < 400) return 'font-light';
if (fontWeight < 500) return 'font-normal';
if (fontWeight < 600) return 'font-medium';
if (fontWeight < 700) return 'font-semibold';
return 'font-bold';
}
// Map size values to Tailwind size classes
function mapToTailwindSize(size: number): string {
// This is a simplified implementation
if (size <= 4) return '1';
if (size <= 8) return '2';
if (size <= 12) return '3';
if (size <= 16) return '4';
if (size <= 20) return '5';
if (size <= 24) return '6';
if (size <= 32) return '8';
if (size <= 40) return '10';
if (size <= 48) return '12';
if (size <= 64) return '16';
if (size <= 80) return '20';
if (size <= 96) return '24';
if (size <= 128) return '32';
if (size <= 160) return '40';
if (size <= 192) return '48';
if (size <= 256) return '64';
if (size <= 320) return '80';
if (size <= 384) return '96';
return 'full';
}
export function convertFigmaStylesToTailwind(figmaStyles: any): string[] {
const tailwindClasses: string[] = [];
// Convert colors
if (figmaStyles.fills && figmaStyles.fills.length > 0) {
const fill = figmaStyles.fills[0];
if (fill && fill.type === 'SOLID') {
const { r, g, b } = fill.color;
// Convert RGB to hex and find closest Tailwind color
const hexColor = rgbToHex(r * 255, g * 255, b * 255);
const tailwindColor = mapToTailwindColor(hexColor);
tailwindClasses.push(tailwindColor);
// Add opacity if needed
if (fill.opacity && fill.opacity < 1) {
const opacityValue = Math.round(fill.opacity * 100);
tailwindClasses.push(`opacity-${opacityValue}`);
}
}
}
// Convert typography
if (figmaStyles.style) {
const { fontSize, fontWeight, lineHeight, letterSpacing } = figmaStyles.style;
// Map font size to Tailwind classes
if (fontSize) {
tailwindClasses.push(mapFontSizeToTailwind(fontSize));
}
// Map font weight
if (fontWeight) {
tailwindClasses.push(mapFontWeightToTailwind(fontWeight));
}
// Map line height
if (lineHeight) {
// Simplified mapping
if (lineHeight <= 1) tailwindClasses.push('leading-none');
else if (lineHeight <= 1.25) tailwindClasses.push('leading-tight');
else if (lineHeight <= 1.5) tailwindClasses.push('leading-normal');
else if (lineHeight <= 1.75) tailwindClasses.push('leading-relaxed');
else tailwindClasses.push('leading-loose');
}
// Map letter spacing
if (letterSpacing) {
// Simplified mapping
if (letterSpacing <= -0.05) tailwindClasses.push('tracking-tighter');
else if (letterSpacing <= 0) tailwindClasses.push('tracking-tight');
else if (letterSpacing <= 0.05) tailwindClasses.push('tracking-normal');
else if (letterSpacing <= 0.1) tailwindClasses.push('tracking-wide');
else tailwindClasses.push('tracking-wider');
}
}
// Convert layout properties
if (figmaStyles.absoluteBoundingBox) {
const { width, height } = figmaStyles.absoluteBoundingBox;
tailwindClasses.push(`w-${mapToTailwindSize(width)}`);
tailwindClasses.push(`h-${mapToTailwindSize(height)}`);
}
// Convert border radius
if (figmaStyles.cornerRadius) {
if (figmaStyles.cornerRadius <= 2) tailwindClasses.push('rounded-sm');
else if (figmaStyles.cornerRadius <= 4) tailwindClasses.push('rounded');
else if (figmaStyles.cornerRadius <= 6) tailwindClasses.push('rounded-md');
else if (figmaStyles.cornerRadius <= 8) tailwindClasses.push('rounded-lg');
else if (figmaStyles.cornerRadius <= 12) tailwindClasses.push('rounded-xl');
else if (figmaStyles.cornerRadius <= 16) tailwindClasses.push('rounded-2xl');
else if (figmaStyles.cornerRadius <= 24) tailwindClasses.push('rounded-3xl');
else tailwindClasses.push('rounded-full');
}
// Convert borders
if (figmaStyles.strokes && figmaStyles.strokes.length > 0) {
const stroke = figmaStyles.strokes[0];
if (stroke && stroke.type === 'SOLID') {
const { r, g, b } = stroke.color;
const hexColor = rgbToHex(r * 255, g * 255, b * 255);
const tailwindColor = mapToTailwindColor(hexColor).replace('text-', 'border-');
tailwindClasses.push(tailwindColor);
// Border width
if (figmaStyles.strokeWeight) {
if (figmaStyles.strokeWeight <= 1) tailwindClasses.push('border');
else if (figmaStyles.strokeWeight <= 2) tailwindClasses.push('border-2');
else if (figmaStyles.strokeWeight <= 4) tailwindClasses.push('border-4');
else tailwindClasses.push('border-8');
}
}
}
// Convert shadows
if (figmaStyles.effects) {
const shadowEffect = figmaStyles.effects.find((effect: any) => effect.type === 'DROP_SHADOW');
if (shadowEffect) {
if (shadowEffect.offset.x === 0 && shadowEffect.offset.y === 1 && shadowEffect.radius <= 2) {
tailwindClasses.push('shadow-sm');
} else if (shadowEffect.offset.y <= 3 && shadowEffect.radius <= 4) {
tailwindClasses.push('shadow');
} else if (shadowEffect.offset.y <= 8 && shadowEffect.radius <= 10) {
tailwindClasses.push('shadow-md');
} else if (shadowEffect.offset.y <= 15 && shadowEffect.radius <= 15) {
tailwindClasses.push('shadow-lg');
} else {
tailwindClasses.push('shadow-xl');
}
}
}
return tailwindClasses;
}
```
--------------------------------------------------------------------------------
/src/figma-react-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { ComponentGenerator } from './component-generator.js';
import { FigmaClient, FigmaNode } from './figma-client.js';
import * as fs from 'fs/promises';
import * as path from 'path';
export function registerReactTools(server: McpServer, componentGenerator: ComponentGenerator, figmaClient: FigmaClient): void {
// Register generateReactComponent tool
server.tool(
'generateReactComponent',
{
componentName: z.string().describe('Name for the React component'),
figmaNodeId: z.string().describe('Figma node ID'),
fileKey: z.string().describe('Figma file key')
},
async ({ componentName, figmaNodeId, fileKey }) => {
try {
// Get the Figma node data
const nodeData = await figmaClient.getFileNodes(fileKey, [figmaNodeId]);
const figmaNode = nodeData.nodes[figmaNodeId]?.document;
if (!figmaNode) {
return {
isError: true,
content: [
{
type: 'text',
text: `Figma node with ID ${figmaNodeId} not found`
}
]
};
}
// Generate the React component
const componentCode = await componentGenerator.generateReactComponent(componentName, figmaNode);
return {
content: [
{
type: 'text',
text: componentCode
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to generate React component: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register generateComponentLibrary tool
server.tool(
'generateComponentLibrary',
{
components: z.array(
z.object({
name: z.string(),
nodeId: z.string()
})
).describe('Components to generate'),
fileKey: z.string().describe('Figma file key')
},
async ({ components, fileKey }) => {
try {
// Get all node IDs
const nodeIds = components.map(comp => comp.nodeId);
// Get the Figma node data for all components
const nodesData = await figmaClient.getFileNodes(fileKey, nodeIds);
// Prepare the components for generation
const componentsForGeneration = components
.filter(comp => nodesData.nodes[comp.nodeId]?.document)
.map(comp => ({
name: comp.name,
node: nodesData.nodes[comp.nodeId].document
}));
// Generate the component library
const generatedComponents = await componentGenerator.generateComponentLibrary(componentsForGeneration);
return {
content: [
{
type: 'text',
text: JSON.stringify(
Object.entries(generatedComponents).map(([name, code]) => ({
name,
code
})),
null,
2
)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to generate component library: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register writeComponentsToFiles tool
server.tool(
'writeComponentsToFiles',
{
components: z.array(
z.object({
name: z.string(),
code: z.string()
})
).describe('Components to write to files'),
outputDir: z.string().describe('Output directory')
},
async ({ components, outputDir }) => {
try {
// Create the output directory if it doesn't exist
await fs.mkdir(outputDir, { recursive: true });
// Write each component to a file
const results = await Promise.all(
components.map(async (component) => {
const fileName = `${component.name}.tsx`;
const filePath = path.join(outputDir, fileName);
await fs.writeFile(filePath, component.code, 'utf-8');
return {
name: component.name,
path: filePath
};
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify({ results }, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to write components to files: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register figmaToReactWorkflow tool
server.tool(
'figmaToReactWorkflow',
{
fileKey: z.string().describe('Figma file key'),
outputDir: z.string().describe('Output directory for components')
},
async ({ fileKey, outputDir }) => {
try {
// Step 1: Extract components from Figma file
const fileData = await figmaClient.getFile(fileKey);
// Find all components in the file
const components: Array<{ id: string, name: string, type: string }> = [];
// Helper function to recursively traverse the document
function findComponents(node: any) {
if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
components.push({
id: node.id,
name: node.name,
type: node.type
});
}
if (node.children) {
for (const child of node.children) {
findComponents(child);
}
}
}
// Start traversal from the document root
findComponents(fileData.document);
// Step 2: Get the Figma node data for all components
const nodeIds = components.map(comp => comp.id);
const nodesData = await figmaClient.getFileNodes(fileKey, nodeIds);
// Step 3: Prepare the components for generation
const componentsForGeneration = components
.filter(comp => nodesData.nodes[comp.id]?.document)
.map(comp => ({
name: comp.name,
node: nodesData.nodes[comp.id].document
}));
// Step 4: Generate the component library
const generatedComponents = await componentGenerator.generateComponentLibrary(componentsForGeneration);
// Step 5: Create the output directory if it doesn't exist
await fs.mkdir(outputDir, { recursive: true });
// Step 6: Write each component to a file
const results = await Promise.all(
Object.entries(generatedComponents).map(async ([name, code]) => {
const fileName = `${name}.tsx`;
const filePath = path.join(outputDir, fileName);
await fs.writeFile(filePath, code, 'utf-8');
return {
name,
path: filePath
};
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
componentsFound: components.length,
componentsGenerated: results.length,
results
}, null, 2)
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Failed to execute Figma to React workflow: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
}
```
--------------------------------------------------------------------------------
/src/component-generator.ts:
--------------------------------------------------------------------------------
```typescript
import * as prettier from 'prettier';
import { FigmaNode } from './figma-client.js';
import { convertFigmaStylesToTailwind } from './figma-tailwind-converter.js';
import { enhanceWithAccessibility } from './accessibility.js';
interface PropDefinition {
name: string;
type: string;
defaultValue?: string;
description?: string;
}
interface ReactComponentParts {
jsx: string;
imports: string[];
props: PropDefinition[];
styles?: Record<string, any>;
}
// Helper function to convert Figma node name to a valid React component name
function toComponentName(name: string): string {
// Remove invalid characters and convert to PascalCase
return name
.replace(/[^\w\s-]/g, '')
.split(/[-_\s]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
// Helper function to convert Figma node name to a valid prop name
function toPropName(name: string): string {
// Convert to camelCase
const parts = name
.replace(/[^\w\s-]/g, '')
.split(/[-_\s]+/);
return parts[0].toLowerCase() +
parts.slice(1)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
// Extract potential props from Figma node
function extractProps(node: FigmaNode): PropDefinition[] {
const props: PropDefinition[] = [];
// Text content could be a prop
if (node.type === 'TEXT' && node.characters) {
const propName = toPropName(node.name) || 'text';
props.push({
name: propName,
type: 'string',
defaultValue: JSON.stringify(node.characters),
description: `Text content for ${node.name}`
});
}
// If node has a "variant" property, it could be a prop
if (node.name.toLowerCase().includes('variant')) {
props.push({
name: 'variant',
type: "'primary' | 'secondary' | 'outline' | 'text'",
defaultValue: "'primary'",
description: 'Visual variant of the component'
});
}
// If node looks like a button, add onClick prop
if (node.name.toLowerCase().includes('button') || node.name.toLowerCase().includes('btn')) {
props.push({
name: 'onClick',
type: '() => void',
description: 'Function called when button is clicked'
});
}
// If node has children that could be dynamic, add children prop
if (node.children && node.children.length > 0) {
// Check if it has a container-like name
if (
node.name.toLowerCase().includes('container') ||
node.name.toLowerCase().includes('wrapper') ||
node.name.toLowerCase().includes('layout') ||
node.name.toLowerCase().includes('section')
) {
props.push({
name: 'children',
type: 'React.ReactNode',
description: 'Child elements to render inside the component'
});
}
}
// Add className prop for styling customization
props.push({
name: 'className',
type: 'string',
description: 'Additional CSS classes to apply'
});
return props;
}
// Convert a Figma node to JSX
function figmaNodeToJSX(node: FigmaNode, level = 0): ReactComponentParts {
const imports: string[] = [];
const allProps: PropDefinition[] = [];
// Default result
let result: ReactComponentParts = {
jsx: '',
imports: [],
props: []
};
// Handle different node types
switch (node.type) {
case 'TEXT':
// Extract text content and convert to JSX
const textContent = node.characters || '';
const tailwindClasses = convertFigmaStylesToTailwind(node);
const textProps = extractProps(node);
result = {
jsx: `<p className="${tailwindClasses.join(' ')}">{${textProps[0]?.name || 'text'}}</p>`,
imports: [],
props: textProps
};
break;
case 'RECTANGLE':
case 'ELLIPSE':
case 'POLYGON':
case 'STAR':
case 'VECTOR':
case 'LINE':
// Convert to a div with appropriate styling
const shapeClasses = convertFigmaStylesToTailwind(node);
result = {
jsx: `<div className="${shapeClasses.join(' ')} ${node.type.toLowerCase()} ${node.name.toLowerCase().replace(/\s+/g, '-')}"></div>`,
imports: [],
props: []
};
break;
case 'COMPONENT':
case 'INSTANCE':
case 'FRAME':
case 'GROUP':
// These are container elements that might have children
const containerClasses = convertFigmaStylesToTailwind(node);
const containerProps = extractProps(node);
// Process children if they exist
let childrenJSX = '';
if (node.children && node.children.length > 0) {
for (const child of node.children) {
const childResult = figmaNodeToJSX(child, level + 1);
childrenJSX += `\n${' '.repeat(level + 1)}${childResult.jsx}`;
// Collect imports and props from children
imports.push(...childResult.imports);
allProps.push(...childResult.props);
}
childrenJSX += `\n${' '.repeat(level)}`;
}
// If this is a component that looks like a button
if (node.name.toLowerCase().includes('button') || node.name.toLowerCase().includes('btn')) {
result = {
jsx: `<button
className="${containerClasses.join(' ')} ${node.name.toLowerCase().replace(/\s+/g, '-')}"
onClick={onClick}
>${childrenJSX}</button>`,
imports: imports,
props: [...containerProps, ...allProps]
};
} else {
// Check if we should use children prop
const hasChildrenProp = containerProps.some(p => p.name === 'children');
result = {
jsx: `<div
className="${containerClasses.join(' ')} ${node.name.toLowerCase().replace(/\s+/g, '-')}"
>${hasChildrenProp ? '{children}' : childrenJSX}</div>`,
imports: imports,
props: [...containerProps, ...(hasChildrenProp ? [] : allProps)]
};
}
break;
case 'IMAGE':
// Convert to an img tag
const imageClasses = convertFigmaStylesToTailwind(node);
result = {
jsx: `<img
src="${node.name.toLowerCase().replace(/\s+/g, '-')}.png"
className="${imageClasses.join(' ')}"
alt="${node.name}"
/>`,
imports: [],
props: [{
name: 'src',
type: 'string',
description: 'Image source URL'
}]
};
break;
default:
// Default to a simple div
result = {
jsx: `<div className="${node.name.toLowerCase().replace(/\s+/g, '-')}"></div>`,
imports: [],
props: []
};
}
return result;
}
export class ComponentGenerator {
async generateReactComponent(componentName: string, figmaNode: FigmaNode): Promise<string> {
// Extract styles, structure, and props from Figma node
const componentParts = figmaNodeToJSX(figmaNode);
// Enhance with accessibility features
const enhancedJSX = enhanceWithAccessibility(componentParts.jsx, figmaNode);
// Deduplicate props
const uniqueProps = componentParts.props.filter((prop, index, self) =>
index === self.findIndex(p => p.name === prop.name)
);
// Create component template
const componentCode = `
import React from 'react';
${componentParts.imports.join('\n')}
interface ${componentName}Props {
${uniqueProps.map(prop =>
`/** ${prop.description || ''} */
${prop.name}${prop.type.includes('?') || prop.defaultValue ? '?' : ''}: ${prop.type};`
).join('\n ')}
}
export const ${componentName} = ({
${uniqueProps.map(p =>
p.defaultValue
? `${p.name} = ${p.defaultValue}`
: p.name
).join(', ')}
}: ${componentName}Props) => {
return (
${enhancedJSX}
);
};
`;
// Format the code
try {
return await prettier.format(componentCode, {
parser: 'typescript',
singleQuote: true,
trailingComma: 'es5',
tabWidth: 2
});
} catch (error) {
console.error('Error formatting component code:', error);
return componentCode;
}
}
async generateComponentLibrary(components: Array<{ name: string, node: FigmaNode }>): Promise<Record<string, string>> {
const generatedComponents: Record<string, string> = {};
for (const { name, node } of components) {
const componentName = toComponentName(name);
generatedComponents[componentName] = await this.generateReactComponent(componentName, node);
}
return generatedComponents;
}
}
```