Skip to main content
Glama

Figma MCP Server

by TimHolden
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { LRUCache } from 'lru-cache'; import { z } from 'zod'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; interface FigmaApiCallStats { lastApiCallTime: number; totalApiCalls: number; failedApiCalls: number; rateLimitRemaining: number | null; rateLimitReset: number | null; apiResponseTimes: number[]; lastError?: { time: number; message: string; endpoint: string; }; } interface FigmaError { message: string; status?: number; endpoint?: string; } interface VariableReference { sourceId: string; targetId: string; expression: string; } interface ReferenceValidation { isValid: boolean; errors: Array<{ variableId: string; error: string; }>; dependencies: Map<string, string[]>; } interface ThemeMode { name: string; variables: Array<{ variableId: string; value: string; }>; } interface ThemeConfig { name: string; modes: ThemeMode[]; } interface FigmaVariable { name: string; type: 'COLOR' | 'FLOAT' | 'STRING'; value: string; scope: 'LOCAL' | 'ALL_FRAMES'; description?: string; } interface VariableUpdate { variableId: string; value: string; description?: string; } interface CreateVariablesResponse { id: string; name: string; variables: Array<{ id: string; name: string; resolvedType: string; }>; } type StatsCallback = (stats: Partial<FigmaApiCallStats>) => void; export class FigmaHandler { protected cache: LRUCache<string, any>; protected figmaToken: string; protected statsCallback?: StatsCallback; protected rateLimitRemaining: number | null = null; protected rateLimitReset: number | null = null; constructor(figmaToken: string, statsCallback?: StatsCallback) { this.figmaToken = figmaToken; this.statsCallback = statsCallback; this.cache = new LRUCache({ max: 500, ttl: 1000 * 60 * 5 // 5 minutes }); } private updateStats(stats: Partial<FigmaApiCallStats>) { if (this.statsCallback) { this.statsCallback(stats); } } private async makeFigmaRequest(endpoint: string, options: RequestInit = {}): Promise<any> { const startTime = Date.now(); let responseTime: number; try { // Validate request parameters if (!endpoint) { throw new Error('Empty endpoint provided to Figma API request'); } if (!this.figmaToken) { throw new Error('No Figma access token provided. Please set FIGMA_ACCESS_TOKEN environment variable.'); } // Make request with enhanced error handling const response = await fetch(`https://api.figma.com/v1${endpoint}`, { headers: { 'X-Figma-Token': this.figmaToken, 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'figma-mcp-server/1.0.0' }, ...options }); responseTime = Date.now() - startTime; // Update rate limit info this.rateLimitRemaining = Number(response.headers.get('x-rate-limit-remaining')) || null; const resetTime = response.headers.get('x-rate-limit-reset'); this.rateLimitReset = resetTime ? Number(new Date(resetTime)) : null; // Handle API errors with specific codes if (!response.ok) { const errorBody = await response.text(); let parsedError = null; try { parsedError = JSON.parse(errorBody); } catch (e) { // If not parsable JSON, use text as error message } const errorMessage = parsedError?.message || errorBody || response.statusText; throw new Error(`Figma API error (${response.status}): ${errorMessage}`); } // Safely parse JSON response with error handling let data; try { data = await response.json(); } catch (jsonError) { throw new Error(`Failed to parse Figma API response as JSON: ${jsonError instanceof Error ? jsonError.message : 'Unknown JSON parsing error'}`); } // Update stats for successful request this.updateStats({ lastApiCallTime: Date.now(), totalApiCalls: 1, apiResponseTimes: [responseTime], rateLimitRemaining: this.rateLimitRemaining, rateLimitReset: this.rateLimitReset }); return data; } catch (error) { // Update stats for failed request responseTime = Date.now() - startTime; const errorDetails = { time: Date.now(), message: error instanceof Error ? error.message : 'Unknown error', endpoint, stack: error instanceof Error ? error.stack : undefined }; this.updateStats({ lastApiCallTime: Date.now(), totalApiCalls: 1, failedApiCalls: 1, apiResponseTimes: [responseTime], rateLimitRemaining: this.rateLimitRemaining, rateLimitReset: this.rateLimitReset, lastError: { time: errorDetails.time, message: errorDetails.message, endpoint } }); // Ensure clean error objects for JSON serialization if (error instanceof Error) { const cleanError = new Error(error.message); cleanError.name = error.name; throw cleanError; } throw error; } } async listTools() { return [ { name: "get-file", description: "Get details of a Figma file", inputSchema: { type: "object", properties: { fileKey: { type: "string", description: "The Figma file key" } }, required: ["fileKey"] } }, { name: "list-files", description: "List files in a Figma project", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "The Figma project ID" } }, required: ["projectId"] } } ]; } async callTool(name: string, args: unknown) { try { switch (name) { case "create_reference": return await this.createReference(args); case "validate_references": return await this.validateReferences(args); case "create_theme": return await this.createTheme(args); case "delete_variables": return await this.deleteVariables(args); case "update_variables": return await this.updateVariables(args); case "create_variables": return await this.createVariables(args); case "get-file": return await this.getFigmaFile(args); case "list-files": return await this.listFigmaFiles(args); default: return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] }; } } catch (error) { if (error instanceof z.ZodError) { return { isError: true, content: [{ type: "text", text: `Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}` }] }; } const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { isError: true, content: [{ type: "text", text: `Tool execution failed: ${errMsg}` }] }; } } async getFigmaFile(args: unknown) { const schema = z.object({ fileKey: z.string() }); const { fileKey } = schema.parse(args); const cacheKey = `file:${fileKey}`; try { let fileData = this.cache.get(cacheKey); if (!fileData) { fileData = await this.makeFigmaRequest(`/files/${fileKey}`); this.cache.set(cacheKey, fileData); } // Format the file data in a more readable way const formattedData = { name: fileData.name, lastModified: fileData.lastModified, version: fileData.version, editorType: fileData.editorType, documentKey: fileData.documentKey, nodes: Object.keys(fileData.document?.children || {}).length + ' nodes', components: Object.keys(fileData.components || {}).length + ' components', styles: Object.keys(fileData.styles || {}).length + ' styles' }; return { content: [{ type: "text", text: `File details for: ${fileData.name}\n\n${Object.entries(formattedData) .map(([key, value]) => `${key}: ${value}`) .join('\n')}` }] }; } catch (error) { let errorMessage = 'Failed to retrieve Figma file'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}. Please verify the file key is correct.`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has the correct permissions.'; } else { errorMessage = `Error accessing file: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } async listFigmaFiles(args: unknown) { const schema = z.object({ projectId: z.string() }); const { projectId } = schema.parse(args); const cacheKey = `project:${projectId}`; let filesData = this.cache.get(cacheKey); if (!filesData) { filesData = await this.makeFigmaRequest(`/projects/${projectId}/files`); this.cache.set(cacheKey, filesData); } return { content: [{ type: "text", text: JSON.stringify(filesData, null, 2) }] }; } async createVariables(args: unknown) { const { CreateVariablesSchema } = require('./figma-tools'); const { fileKey, variables } = CreateVariablesSchema.parse(args); try { // Create a collection if it doesn't exist const collection = await this.makeFigmaRequest(`/files/${fileKey}/variables/create-collection`, { method: 'POST', body: JSON.stringify({ name: "MCP Generated Variables", variableTypes: [...new Set(variables.map((v: FigmaVariable) => v.type))] }) }); // Create variables in the collection const createdVariables = await this.makeFigmaRequest(`/files/${fileKey}/variables`, { method: 'POST', body: JSON.stringify({ variableCollectionId: collection.id, variables: variables.map((v: FigmaVariable) => ({ name: v.name, resolvedType: v.type, description: v.description, value: v.value, scope: v.scope })) }) }); return { content: [{ type: "text", text: `Successfully created ${variables.length} variables:\n${ variables.map((v: FigmaVariable) => `- ${v.name} (${v.type})`).join('\n') }` }] }; } catch (error) { let errorMessage = 'Failed to create variables'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has write permissions.'; } else { errorMessage = `Error creating variables: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } async updateVariables(args: unknown) { const { UpdateVariablesSchema } = require('./figma-tools'); const { fileKey, updates } = UpdateVariablesSchema.parse(args); try { // Get existing variables to validate updates const variables = await this.makeFigmaRequest(`/files/${fileKey}/variables`); // Process updates const results = await Promise.all( updates.map(async (update: VariableUpdate) => { const variable = variables.find((v: any) => v.id === update.variableId); if (!variable) { return `Variable ${update.variableId} not found`; } try { await this.makeFigmaRequest(`/files/${fileKey}/variables/${update.variableId}`, { method: 'PATCH', body: JSON.stringify({ value: update.value, ...(update.description && { description: update.description }) }) }); return `Updated ${variable.name}`; } catch (error) { return `Failed to update ${variable.name}: ${error instanceof Error ? error.message : 'Unknown error'}`; } }) ); return { content: [{ type: "text", text: `Variable update results:\n${results.map(r => `- ${r}`).join('\n')}` }] }; } catch (error) { let errorMessage = 'Failed to update variables'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has write permissions.'; } else { errorMessage = `Error updating variables: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } async deleteVariables(args: unknown) { const { DeleteVariablesSchema } = require('./figma-tools'); const { fileKey, variableIds, softDelete } = DeleteVariablesSchema.parse(args); try { // Get existing variables to validate deletions const variables = await this.makeFigmaRequest(`/files/${fileKey}/variables`); // Process deletions const results = await Promise.all( variableIds.map(async (id: string) => { const variable = variables.find((v: any) => v.id === id); if (!variable) { return `Variable ${id} not found`; } try { await this.makeFigmaRequest(`/files/${fileKey}/variables/${id}`, { method: 'DELETE', body: JSON.stringify({ softDelete }) }); return `Deleted ${variable.name}${softDelete ? ' (soft delete)' : ''}`; } catch (error) { return `Failed to delete ${variable.name}: ${error instanceof Error ? error.message : 'Unknown error'}`; } }) ); return { content: [{ type: "text", text: `Variable deletion results:\n${results.map(r => `- ${r}`).join('\n')}` }] }; } catch (error) { let errorMessage = 'Failed to delete variables'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has write permissions.'; } else { errorMessage = `Error deleting variables: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } private buildDependencyGraph(variables: any[]): Map<string, string[]> { const graph = new Map<string, string[]>(); variables.forEach(variable => { if (variable.resolvedType === 'VARIABLE_REFERENCE') { const sourceId = variable.id; const targetId = variable.valuesByMode?.default?.referencedVariableId; if (!graph.has(sourceId)) { graph.set(sourceId, []); } if (targetId) { graph.get(sourceId)!.push(targetId); } } }); return graph; } private detectCycles(graph: Map<string, string[]>, start: string, visited = new Set<string>(), path = new Set<string>()): boolean { if (path.has(start)) { return true; // Cycle detected } if (visited.has(start)) { return false; // Already checked this path } visited.add(start); path.add(start); const dependencies = graph.get(start) || []; for (const dep of dependencies) { if (this.detectCycles(graph, dep, visited, path)) { return true; } } path.delete(start); return false; } async createReference(args: unknown) { const { CreateReferenceSchema } = require('./figma-tools'); const { fileKey, sourceId, targetId, expression } = CreateReferenceSchema.parse(args); try { // Validate that both variables exist const variables = await this.makeFigmaRequest(`/files/${fileKey}/variables`); const source = variables.find((v: any) => v.id === sourceId); const target = variables.find((v: any) => v.id === targetId); if (!source) { return { isError: true, content: [{ type: "text", text: `Source variable ${sourceId} not found` }] }; } if (!target) { return { isError: true, content: [{ type: "text", text: `Target variable ${targetId} not found` }] }; } // Create the reference await this.makeFigmaRequest(`/files/${fileKey}/variables/${sourceId}/reference`, { method: 'PUT', body: JSON.stringify({ referencedVariableId: targetId, expression: expression || undefined }) }); return { content: [{ type: "text", text: `Created reference from ${source.name} to ${target.name}${expression ? ` with expression: ${expression}` : ''}` }] }; } catch (error) { let errorMessage = 'Failed to create reference'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has write permissions.'; } else { errorMessage = `Error creating reference: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } async validateReferences(args: unknown) { const { ValidateReferencesSchema } = require('./figma-tools'); const { fileKey, variableIds } = ValidateReferencesSchema.parse(args); try { // Get all variables const variables = await this.makeFigmaRequest(`/files/${fileKey}/variables`); // Build dependency graph const graph = this.buildDependencyGraph(variables); // Check for cycles and validate references const results: Array<{ variableId: string; error: string }> = []; const varsToCheck = variableIds || [...graph.keys()]; for (const varId of varsToCheck) { // Check if variable exists const variable = variables.find((v: any) => v.id === varId); if (!variable) { results.push({ variableId: varId, error: 'Variable not found' }); continue; } // Check for circular references if (this.detectCycles(graph, varId)) { results.push({ variableId: varId, error: 'Circular reference detected' }); } // Validate referenced variables exist const dependencies = graph.get(varId) || []; for (const depId of dependencies) { const dep = variables.find((v: any) => v.id === depId); if (!dep) { results.push({ variableId: varId, error: `Referenced variable ${depId} not found` }); } } } return { content: [{ type: "text", text: results.length > 0 ? `Validation issues found:\n${results.map(r => `- ${r.variableId}: ${r.error}`).join('\n')}` : 'All references are valid' }] }; } catch (error) { let errorMessage = 'Failed to validate references'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has read permissions.'; } else { errorMessage = `Error validating references: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } async createTheme(args: unknown) { const { CreateThemeSchema } = require('./figma-tools'); const { fileKey, name, modes } = CreateThemeSchema.parse(args); try { // Create a collection for the theme const collection = await this.makeFigmaRequest(`/files/${fileKey}/variables/create-collection`, { method: 'POST', body: JSON.stringify({ name: name, variableTypes: ["COLOR", "FLOAT", "STRING"] // Support all variable types }) }); // Process each mode const results = await Promise.all( modes.map(async (mode: ThemeMode) => { try { // Create the mode in the collection await this.makeFigmaRequest(`/files/${fileKey}/variables/modes`, { method: 'POST', body: JSON.stringify({ collectionId: collection.id, name: mode.name }) }); // Apply variable values for this mode const modeUpdates = await Promise.all( mode.variables.map(async (varConfig) => { try { await this.makeFigmaRequest(`/files/${fileKey}/variables/${varConfig.variableId}/modes`, { method: 'PATCH', body: JSON.stringify({ mode: mode.name, value: varConfig.value }) }); return `Set ${varConfig.variableId} for mode ${mode.name}`; } catch (error) { return `Failed to set ${varConfig.variableId} for mode ${mode.name}: ${error instanceof Error ? error.message : 'Unknown error'}`; } }) ); return `Created mode ${mode.name}:\n${modeUpdates.map(msg => ` - ${msg}`).join('\n')}`; } catch (error) { return `Failed to create mode ${mode.name}: ${error instanceof Error ? error.message : 'Unknown error'}`; } }) ); return { content: [{ type: "text", text: `Created theme "${name}" with modes:\n${results.map(r => r).join('\n')}` }] }; } catch (error) { let errorMessage = 'Failed to create theme'; if (error instanceof Error) { if (error.message.includes('404')) { errorMessage = `File not found: ${fileKey}`; } else if (error.message.includes('403')) { errorMessage = 'Access denied. Please verify your Figma access token has write permissions.'; } else { errorMessage = `Error creating theme: ${error.message}`; } } return { isError: true, content: [{ type: "text", text: errorMessage }] }; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TimHolden/figma-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server