import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
const FIGMA_TOKEN = process.env.FIGMA_TOKEN;
if (!FIGMA_TOKEN) {
throw new Error("FIGMA_TOKEN is not set in .env");
}
const figmaApi = axios.create({
baseURL: "https://api.figma.com/v1/",
headers: { "X-Figma-Token": FIGMA_TOKEN },
});
const server = new McpServer({
name: "figma-mcp-server",
version: "1.0.0",
});
// Helper function to traverse Figma nodes and extract design info
function traverseNode(node: any, depth = 0): string {
let result = `${" ".repeat(depth)}${node.type}: ${node.name}\n`;
// Extract styling information
if (node.type === "TEXT") {
result += `${" ".repeat(depth + 1)}Text: "${node.characters}"\n`;
if (node.style) {
result += `${" ".repeat(depth + 1)}Font: ${node.style.fontFamily} ${node.style.fontWeight}\n`;
result += `${" ".repeat(depth + 1)}Size: ${node.style.fontSize}px\n`;
}
}
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color) {
const { r, g, b } = fill.color;
const hex = `#${Math.round(r * 255).toString(16).padStart(2, '0')}${Math.round(g * 255).toString(16).padStart(2, '0')}${Math.round(b * 255).toString(16).padStart(2, '0')}`;
result += `${" ".repeat(depth + 1)}Fill: ${hex}\n`;
}
}
if (node.strokes && node.strokes.length > 0) {
const stroke = node.strokes[0];
if (stroke.type === "SOLID" && stroke.color) {
const { r, g, b } = stroke.color;
const hex = `#${Math.round(r * 255).toString(16).padStart(2, '0')}${Math.round(g * 255).toString(16).padStart(2, '0')}${Math.round(b * 255).toString(16).padStart(2, '0')}`;
result += `${" ".repeat(depth + 1)}Stroke: ${hex}\n`;
}
}
if (node.absoluteBoundingBox) {
result += `${" ".repeat(depth + 1)}Position: (${node.absoluteBoundingBox.x}, ${node.absoluteBoundingBox.y})\n`;
result += `${" ".repeat(depth + 1)}Size: ${node.absoluteBoundingBox.width}x${node.absoluteBoundingBox.height}\n`;
}
if (node.children) {
for (const child of node.children) {
result += traverseNode(child, depth + 1);
}
}
return result;
}
// Helper function to normalize class names (replace colons and special chars)
function normalizeClassName(id: string): string {
return id.replace(/:/g, '-').replace(/;/g, '-').replace(/\./g, '-');
}
// Helper function to generate CSS from Figma node
function generateCSS(node: any, selector = ".figma-component", isChild = false): string {
let css = `${selector} {\n`;
// Add positioning for layout
if (node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT") {
css += ` position: relative;\n`;
} else if (isChild && node.absoluteBoundingBox) {
// For child elements, we could add absolute positioning if needed
// css += ` position: absolute;\n`;
}
if (node.absoluteBoundingBox) {
css += ` width: ${Math.round(node.absoluteBoundingBox.width)}px;\n`;
css += ` height: ${Math.round(node.absoluteBoundingBox.height)}px;\n`;
}
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
const { r, g, b, a = 1 } = fill.color;
if (a > 0) {
css += ` background-color: rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a});\n`;
}
}
}
if (node.strokes && node.strokes.length > 0 && node.strokeWeight) {
const stroke = node.strokes[0];
if (stroke.type === "SOLID" && stroke.color) {
const { r, g, b, a = 1 } = stroke.color;
css += ` border: ${node.strokeWeight}px solid rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a});\n`;
}
}
if (node.cornerRadius && node.cornerRadius > 0) {
css += ` border-radius: ${node.cornerRadius}px;\n`;
}
if (node.type === "TEXT" && node.style) {
css += ` font-family: "${node.style.fontFamily}";\n`;
css += ` font-size: ${node.style.fontSize}px;\n`;
css += ` font-weight: ${node.style.fontWeight};\n`;
css += ` line-height: ${node.style.lineHeightPx || node.style.fontSize * 1.2}px;\n`;
if (node.style.textAlignHorizontal) {
css += ` text-align: ${node.style.textAlignHorizontal.toLowerCase()};\n`;
}
css += ` white-space: pre-wrap;\n`;
css += ` word-wrap: break-word;\n`;
}
if (node.effects && node.effects.length > 0) {
const shadows = node.effects
.filter((e: any) => e.type === "DROP_SHADOW" && e.visible !== false)
.map((e: any) => {
const { r, g, b, a = 1 } = e.color;
return `${e.offset.x}px ${e.offset.y}px ${e.radius}px rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
});
if (shadows.length > 0) {
css += ` box-shadow: ${shadows.join(", ")};\n`;
}
}
// Add some padding for frames to improve readability
if (node.type === "FRAME" && node.children && node.children.length > 0) {
css += ` padding: 10px;\n`;
css += ` margin-bottom: 20px;\n`;
}
css += "}\n";
return css;
}
// Helper function to generate HTML from Figma node
function generateHTML(node: any, depth = 0): string {
const indent = " ".repeat(depth);
let html = "";
const normalizedId = normalizeClassName(node.id);
if (node.type === "TEXT") {
html += `${indent}<div class="text-${normalizedId}">${node.characters || ""}</div>\n`;
} else if (node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT") {
const className = `${node.type.toLowerCase()}-${normalizedId}`;
html += `${indent}<div class="${className}">\n`;
if (node.children) {
for (const child of node.children) {
html += generateHTML(child, depth + 1);
}
}
html += `${indent}</div>\n`;
} else if (node.type === "RECTANGLE" || node.type === "ELLIPSE" || node.type === "VECTOR") {
html += `${indent}<div class="${node.type.toLowerCase()}-${normalizedId}"></div>\n`;
} else if (node.type === "INSTANCE") {
const className = `instance-${normalizedId}`;
html += `${indent}<div class="${className}">\n`;
if (node.children) {
for (const child of node.children) {
html += generateHTML(child, depth + 1);
}
}
html += `${indent}</div>\n`;
}
return html;
}
server.registerTool(
"getFile",
{
title: "Get Figma File",
description: "Fetch a Figma file by its file key.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
}),
},
async ({ fileKey }) => {
const response = await figmaApi.get(`files/${fileKey}`);
return {
content: [{
type: "text",
text: JSON.stringify(response.data, null, 2)
}]
};
}
);
server.registerTool(
"getNode",
{
title: "Get Figma Node",
description: "Fetch a specific node from a Figma file by file key and node ID.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to fetch (e.g., '8384-4')."),
}),
},
async ({ fileKey, nodeId }) => {
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
return {
content: [{
type: "text",
text: JSON.stringify(nodeData, null, 2)
}]
};
}
);
server.registerTool(
"analyzeDesign",
{
title: "Analyze Figma Design",
description: "Analyze a Figma node and extract its design properties in a readable format.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to analyze (e.g., '8384-4')."),
}),
},
async ({ fileKey, nodeId }) => {
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
const analysis = traverseNode(nodeData.document);
return {
content: [{
type: "text",
text: `Design Analysis for Node ${nodeId}:\n\n${analysis}`
}]
};
}
);
server.registerTool(
"generateCSS",
{
title: "Generate CSS from Figma Node",
description: "Generate CSS styles from a Figma node.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to convert to CSS (e.g., '8384-4')."),
className: z.string().optional().describe("Custom CSS class name (optional, defaults to '.figma-component')."),
}),
},
async ({ fileKey, nodeId, className }) => {
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
let allCSS = "";
const node = nodeData.document;
// Generate CSS for the main node
const normalizedId = normalizeClassName(node.id);
const selector = className || `.${node.type.toLowerCase()}-${normalizedId}`;
allCSS += generateCSS(node, selector) + "\n";
// Generate CSS for children
if (node.children) {
for (const child of node.children) {
const childNormalizedId = normalizeClassName(child.id);
const childSelector = `.${child.type.toLowerCase()}-${childNormalizedId}`;
allCSS += generateCSS(child, childSelector) + "\n";
}
}
return {
content: [{
type: "text",
text: allCSS
}]
};
}
);
server.registerTool(
"generateHTML",
{
title: "Generate HTML from Figma Node",
description: "Generate HTML structure from a Figma node.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to convert to HTML (e.g., '8384-4')."),
}),
},
async ({ fileKey, nodeId }) => {
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
const html = generateHTML(nodeData.document);
return {
content: [{
type: "text",
text: `<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n <title>Figma Design</title>\n</head>\n<body>\n${html}</body>\n</html>`
}]
};
}
);
server.registerTool(
"generateFullDesign",
{
title: "Generate Complete HTML/CSS from Figma",
description: "Generate a complete HTML file with inline CSS from a Figma node.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to convert (e.g., '8384-4')."),
}),
},
async ({ fileKey, nodeId }) => {
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
const node = nodeData.document;
let allCSS = "";
// Generate CSS for main node and all children
function collectCSS(n: any): void {
const normalizedId = normalizeClassName(n.id);
const selector = `.${n.type.toLowerCase()}-${normalizedId}`;
allCSS += generateCSS(n, selector) + "\n";
if (n.children) {
for (const child of n.children) {
collectCSS(child);
}
}
}
collectCSS(node);
const html = generateHTML(node);
const fullHTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${node.name || 'Figma Design'}</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
}
${allCSS} </style>
</head>
<body>
${html}</body>
</html>`;
return {
content: [{
type: "text",
text: fullHTML
}]
};
}
);
server.registerTool(
"getImage",
{
title: "Get Image/Screenshot from Figma",
description: "Get a rendered image/screenshot of a Figma node. Returns image URLs in various formats.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to capture (e.g., '8384-4')."),
scale: z.number().optional().describe("Scale factor for the image (0.01 to 4). Default is 1."),
format: z.enum(["jpg", "png", "svg", "pdf"]).optional().describe("Image format. Default is 'png'."),
}),
},
async ({ fileKey, nodeId, scale, format }) => {
const params: any = {
ids: nodeId,
format: format || "png",
};
if (scale) {
params.scale = scale;
}
try {
const response = await figmaApi.get(`images/${fileKey}`, { params });
if (response.data.err) {
return {
content: [{
type: "text",
text: `Error: ${response.data.err}`
}]
};
}
const imageUrl = response.data.images[nodeId];
if (!imageUrl) {
return {
content: [{
type: "text",
text: `No image URL returned for node ${nodeId}. The node may not exist or may not be renderable.`
}]
};
}
return {
content: [{
type: "text",
text: `Image URL for node ${nodeId}:\n\n${imageUrl}\n\nFormat: ${format || "png"}\nScale: ${scale || 1}\n\nNote: This URL is temporary and will expire. Download the image if you need to keep it.`
}]
};
} catch (error: any) {
return {
content: [{
type: "text",
text: `Error fetching image: ${error.message}`
}]
};
}
}
);
server.registerTool(
"generateDesignWithScreenshot",
{
title: "Generate HTML/CSS with Screenshot Reference",
description: "Generate HTML/CSS using both Figma API data and a screenshot for more accurate visual representation. The screenshot URL is included in the output for reference.",
inputSchema: z.object({
fileKey: z.string().describe("The Figma file key (from the file URL)."),
nodeId: z.string().describe("The node ID to convert (e.g., '8384-4')."),
includeScreenshot: z.boolean().optional().describe("Include screenshot URL in comments (default: true)."),
}),
},
async ({ fileKey, nodeId, includeScreenshot = true }) => {
try {
// Step 1: Get the design data
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
return {
content: [{
type: "text",
text: `Node ${nodeId} not found in file ${fileKey}`
}]
};
}
const node = nodeData.document;
// Step 2: Get the screenshot
let screenshotUrl = "";
let screenshotInfo = "";
if (includeScreenshot) {
try {
const imageResponse = await figmaApi.get(`images/${fileKey}`, {
params: { ids: nodeId, format: "png", scale: 2 }
});
if (imageResponse.data.images && imageResponse.data.images[nodeId]) {
screenshotUrl = imageResponse.data.images[nodeId];
screenshotInfo = `
/*
DESIGN REFERENCE SCREENSHOT:
${screenshotUrl}
This screenshot shows the actual Figma design for visual reference.
Note: URL expires in ~30 minutes. Download if needed.
Tips for matching the design:
- Compare element positioning and spacing
- Check font sizes and weights against screenshot
- Verify colors match the visual appearance
- Adjust padding/margins for pixel-perfect match
*/
`;
}
} catch (imgError) {
screenshotInfo = "/* Could not fetch screenshot, using Figma API data only */\n";
}
}
// Step 3: Generate enhanced CSS with better positioning
let allCSS = "";
function collectCSS(n: any): void {
const normalizedId = normalizeClassName(n.id);
const selector = `.${n.type.toLowerCase()}-${normalizedId}`;
allCSS += generateCSS(n, selector) + "\n";
if (n.children) {
for (const child of n.children) {
collectCSS(child);
}
}
}
collectCSS(node);
const html = generateHTML(node);
// Step 4: Create enhanced HTML with screenshot reference
const fullHTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${node.name || 'Figma Design'}</title>
<style>
${screenshotInfo}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
padding: 20px;
}
/* GENERATED STYLES FROM FIGMA API */
${allCSS}
/*
DESIGN NOTES:
- Component: ${node.name}
- Original size: ${node.absoluteBoundingBox?.width}x${node.absoluteBoundingBox?.height}px
- Generated from Figma API data
${screenshotUrl ? `- Reference screenshot included above` : ''}
RECOMMENDATIONS:
1. Compare this output with the screenshot
2. Adjust spacing/positioning as needed
3. Fine-tune colors if they don't match exactly
4. Check text rendering and font weights
*/
</style>
</head>
<body>
${screenshotUrl ? `<!--
SCREENSHOT REFERENCE: ${screenshotUrl}
Download and compare with this HTML output for accuracy.
-->
` : ''}${html}</body>
</html>`;
return {
content: [{
type: "text",
text: fullHTML
}]
};
} catch (error: any) {
return {
content: [{
type: "text",
text: `Error generating design: ${error.message}`
}]
};
}
}
);
const transport = new StdioServerTransport();
server.connect(transport);