import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { FigmaService, type FigmaError } from "./services/figma.js";
import express, { type Request, type Response } from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { type IncomingMessage, type ServerResponse } from "http";
import { type Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { SimplifiedDesign } from "./types/index.js";
import {
DESIGN_TO_CODE_PROMPT,
COMPONENT_ANALYSIS_PROMPT,
STYLE_EXTRACTION_PROMPT,
} from "./prompts/index.js";
import {
getFileMetadata,
getStyleTokens,
getComponentList,
getAssetList,
createFileMetadataTemplate,
createStylesTemplate,
createComponentsTemplate,
createAssetsTemplate,
FIGMA_MCP_HELP,
} from "./resources/index.js";
// ==================== Logging Utilities ====================
export const Logger = {
log: (..._args: unknown[]) => {},
error: (..._args: unknown[]) => {},
};
// ==================== Error Formatting ====================
/**
* Check if error is a Figma API error
*/
function isFigmaError(error: unknown): error is FigmaError {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
typeof (error as FigmaError).status === "number"
);
}
/**
* Format error information for AI understanding
*/
function formatErrorForAI(error: unknown, context: string): string {
if (isFigmaError(error)) {
const parts: string[] = [`[Figma API Error] ${context}`];
parts.push(`Status: ${error.status}`);
parts.push(`Message: ${error.err}`);
if (error.rateLimitInfo) {
const { remaining, resetAfter, retryAfter } = error.rateLimitInfo;
if (remaining !== null) parts.push(`Rate Limit Remaining: ${remaining}`);
if (retryAfter !== null) parts.push(`Retry After: ${retryAfter} seconds`);
if (resetAfter !== null) parts.push(`Reset After: ${resetAfter} seconds`);
}
return parts.join("\n");
}
if (error instanceof Error) {
return `[Error] ${context}: ${error.message}`;
}
return `[Error] ${context}: ${String(error)}`;
}
// ==================== MCP Server ====================
export class FigmaMcpServer {
private readonly server: McpServer;
private readonly figmaService: FigmaService;
private sseTransport: SSEServerTransport | null = null;
constructor(figmaApiKey: string) {
this.figmaService = new FigmaService(figmaApiKey);
this.server = new McpServer(
{
name: "Figma MCP Server",
version: "1.0.2",
},
{
capabilities: {
logging: {},
tools: {},
prompts: {},
resources: {},
},
},
);
this.registerTools();
this.registerPrompts();
this.registerResources();
}
private registerTools(): void {
// Tool: Get Figma data
this.server.tool(
"get_figma_data",
"Get layout and style information from a Figma file or specific node. " +
"Returns simplified design data including CSS styles, text content, and export info. " +
"Results are cached for 24 hours to reduce API calls.",
{
fileKey: z
.string()
.describe(
"The key of the Figma file to fetch, found in URL like figma.com/(file|design)/<fileKey>/...",
),
nodeId: z
.string()
.optional()
.describe(
"The ID of a specific node to fetch (e.g., '1234:5678'), found as URL parameter node-id=<nodeId>. Use this for better performance with large files.",
),
depth: z
.number()
.optional()
.describe(
"How many levels deep to traverse the node tree (1-100). Only use if explicitly needed.",
),
},
async ({ fileKey, nodeId, depth }) => {
try {
Logger.log(
`Fetching ${depth ? `${depth} layers deep` : "all layers"} of ${
nodeId ? `node ${nodeId} from file` : `full file`
} ${fileKey}`,
);
let file: SimplifiedDesign;
if (nodeId) {
file = await this.figmaService.getNode(fileKey, nodeId, depth);
} else {
file = await this.figmaService.getFile(fileKey, depth);
}
Logger.log(`Successfully fetched file: ${file.name}`);
const { nodes, ...metadata } = file;
// Serialize in segments to handle large files
const nodesJson = `[${nodes.map((node) => JSON.stringify(node, null, 2)).join(",")}]`;
const metadataJson = JSON.stringify(metadata, null, 2);
const resultJson = `{ "metadata": ${metadataJson}, "nodes": ${nodesJson} }`;
// Add cache status information
const rateLimitInfo = this.figmaService.getRateLimitInfo();
let statusNote = "";
if (rateLimitInfo && rateLimitInfo.remaining !== null) {
statusNote = `\n\n[API Status] Rate limit remaining: ${rateLimitInfo.remaining}`;
}
return {
content: [{ type: "text", text: resultJson + statusNote }],
};
} catch (error) {
Logger.error(`Error fetching file ${fileKey}:`, error);
const errorMessage = formatErrorForAI(
error,
`Failed to fetch Figma data for file ${fileKey}`,
);
return {
isError: true,
content: [{ type: "text", text: errorMessage }],
};
}
},
);
// Tool: Download images
this.server.tool(
"download_figma_images",
"Download SVG and PNG images from a Figma file. " +
"Supports both rendered node images and image fills. " +
"Images are cached locally to avoid repeated downloads.",
{
fileKey: z.string().describe("The key of the Figma file containing the images"),
nodes: z
.object({
nodeId: z
.string()
.describe("The ID of the Figma image node to fetch (e.g., '1234:5678')"),
imageRef: z
.string()
.optional()
.describe(
"Required for image fills (background images). Leave blank for vector/icon SVGs.",
),
fileName: z
.string()
.describe("The local filename to save as (e.g., 'icon.svg', 'photo.png')"),
})
.array()
.describe("Array of image nodes to download"),
localPath: z
.string()
.describe(
"Absolute path to the directory where images should be saved. Directories will be created if needed.",
),
},
async ({ fileKey, nodes, localPath }) => {
try {
// Classify processing: image fills vs rendered nodes
const imageFills = nodes.filter(({ imageRef }) => !!imageRef) as {
nodeId: string;
imageRef: string;
fileName: string;
}[];
const renderRequests = nodes
.filter(({ imageRef }) => !imageRef)
.map(({ nodeId, fileName }) => ({
nodeId,
fileName,
fileType: fileName.toLowerCase().endsWith(".svg")
? ("svg" as const)
: ("png" as const),
}));
// Execute sequentially to reduce rate limit risk
const fillResults = await this.figmaService.getImageFills(fileKey, imageFills, localPath);
const renderResults = await this.figmaService.getImages(
fileKey,
renderRequests,
localPath,
);
const allDownloads = [...fillResults, ...renderResults];
const successfulDownloads = allDownloads.filter((path) => path && path.length > 0);
const failedCount = allDownloads.length - successfulDownloads.length;
let resultMessage: string;
if (successfulDownloads.length === allDownloads.length) {
resultMessage = `Successfully downloaded ${successfulDownloads.length} images:\n${successfulDownloads.join("\n")}`;
} else if (successfulDownloads.length > 0) {
resultMessage = `Downloaded ${successfulDownloads.length}/${allDownloads.length} images (${failedCount} failed):\n${successfulDownloads.join("\n")}`;
} else {
resultMessage = `Failed to download any images. Please check the node IDs and try again.`;
}
return {
content: [{ type: "text", text: resultMessage }],
};
} catch (error) {
Logger.error(`Error downloading images from file ${fileKey}:`, error);
const errorMessage = formatErrorForAI(
error,
`Failed to download images from file ${fileKey}`,
);
return {
isError: true,
content: [{ type: "text", text: errorMessage }],
};
}
},
);
}
private registerPrompts(): void {
// Prompt: Design to Code - Full workflow
this.server.prompt(
"design_to_code",
"Complete workflow for converting Figma designs to production-ready code with project analysis",
{
framework: z
.enum(["react", "vue", "html", "auto"])
.optional()
.describe("Target framework for code generation (default: auto-detect from project)"),
includeResponsive: z
.boolean()
.optional()
.describe("Include responsive/mobile adaptation guidelines (default: true)"),
},
async ({ framework, includeResponsive }) => {
let prompt = DESIGN_TO_CODE_PROMPT;
// Add framework-specific context
if (framework && framework !== "auto") {
prompt += `\n\n## Framework Context\nTarget framework: **${framework.toUpperCase()}**\n`;
if (framework === "vue") {
prompt += `- USE Vue 3 Composition API with <script setup>
- USE defineProps/defineEmits for component interface
- PREFER template syntax over JSX`;
} else if (framework === "react") {
prompt += `- USE functional components with hooks
- USE TypeScript for props interface
- PREFER named exports for components`;
}
}
// Add responsive guidelines toggle
if (includeResponsive === false) {
prompt += `\n\n## Note\nSkip Phase 6 (Responsive Adaptation) - desktop only implementation required.`;
}
return {
messages: [
{
role: "user",
content: {
type: "text",
text: prompt,
},
},
],
};
},
);
// Prompt: Component Analysis
this.server.prompt(
"analyze_components",
"Analyze Figma design to identify optimal component structure and reusability",
{},
async () => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: COMPONENT_ANALYSIS_PROMPT,
},
},
],
};
},
);
// Prompt: Style Extraction
this.server.prompt(
"extract_styles",
"Extract design tokens (colors, typography, spacing) from Figma design data",
{},
async () => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: STYLE_EXTRACTION_PROMPT,
},
},
],
};
},
);
}
private registerResources(): void {
// Static Resource: Help guide
this.server.resource(
"figma_help",
"figma://help",
{
description: "Figma MCP Server usage guide and resource documentation",
mimeType: "text/markdown",
},
async () => {
return {
contents: [
{
uri: "figma://help",
mimeType: "text/markdown",
text: FIGMA_MCP_HELP,
},
],
};
},
);
// Template Resource: File metadata
this.server.resource(
"figma_file",
createFileMetadataTemplate(),
{
description: "Get Figma file metadata (name, pages, last modified). Low token cost (~200).",
mimeType: "application/json",
},
async (uri, variables) => {
const fileKey = variables.fileKey as string;
if (!fileKey) {
throw new Error("fileKey is required");
}
try {
const metadata = await getFileMetadata(this.figmaService, fileKey);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(metadata, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch file metadata: ${message}`);
}
},
);
// Template Resource: Style tokens
this.server.resource(
"figma_styles",
createStylesTemplate(),
{
description:
"Extract design tokens (colors, typography, effects) from Figma file. Token cost ~500.",
mimeType: "application/json",
},
async (uri, variables) => {
const fileKey = variables.fileKey as string;
if (!fileKey) {
throw new Error("fileKey is required");
}
try {
const styles = await getStyleTokens(this.figmaService, fileKey);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(styles, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch styles: ${message}`);
}
},
);
// Template Resource: Component list
this.server.resource(
"figma_components",
createComponentsTemplate(),
{
description: "List all components and component sets in Figma file. Token cost ~300.",
mimeType: "application/json",
},
async (uri, variables) => {
const fileKey = variables.fileKey as string;
if (!fileKey) {
throw new Error("fileKey is required");
}
try {
const components = await getComponentList(this.figmaService, fileKey);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(components, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch components: ${message}`);
}
},
);
// Template Resource: Asset list
this.server.resource(
"figma_assets",
createAssetsTemplate(),
{
description:
"List exportable assets (icons, images, vectors) with node IDs for download. Token cost ~400.",
mimeType: "application/json",
},
async (uri, variables) => {
const fileKey = variables.fileKey as string;
if (!fileKey) {
throw new Error("fileKey is required");
}
try {
const assets = await getAssetList(this.figmaService, fileKey);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(assets, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to fetch assets: ${message}`);
}
},
);
}
async connect(transport: Transport): Promise<void> {
await this.server.connect(transport);
Logger.log = (...args: unknown[]) => {
this.server.server.sendLoggingMessage({
level: "info",
data: args,
});
};
Logger.error = (...args: unknown[]) => {
this.server.server.sendLoggingMessage({
level: "error",
data: args,
});
};
Logger.log("Server connected and ready to process requests");
}
async startHttpServer(port: number): Promise<void> {
const app = express();
app.get("/sse", async (req: Request, res: Response) => {
console.log("New SSE connection established");
this.sseTransport = new SSEServerTransport(
"/messages",
res as unknown as ServerResponse<IncomingMessage>,
);
await this.server.connect(this.sseTransport);
});
app.post("/messages", async (req: Request, res: Response) => {
if (!this.sseTransport) {
res.sendStatus(400);
return;
}
await this.sseTransport.handlePostMessage(
req as unknown as IncomingMessage,
res as unknown as ServerResponse<IncomingMessage>,
);
});
Logger.log = console.log;
Logger.error = console.error;
app.listen(port, () => {
Logger.log(`HTTP server listening on port ${port}`);
Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
});
}
}