Skip to main content
Glama
figma-client.ts7.37 kB
import axios from "axios"; import type { AxiosInstance } from "axios"; import type { GetFileNodesResponse, GetImagesResponse, } from "@figma/rest-api-spec"; import { handleAxiosError, withTimeout, withErrorHandling, logger, ErrorType, FigmaServerError, } from "../utils/error-handling.js"; import { isValidFileKey, isValidNodeId } from "../utils/validation.js"; export interface FigmaClientConfig { apiToken: string; baseURL?: string; timeout?: number; maxRetries?: number; } export class FigmaApiClient { private instance: AxiosInstance; private config: Required<FigmaClientConfig>; private requestTimings: Map<string, number> = new Map(); constructor(config: FigmaClientConfig) { this.config = { baseURL: "https://api.figma.com/v1", timeout: 30000, maxRetries: 3, ...config, }; this.instance = axios.create({ baseURL: this.config.baseURL, headers: { "X-Figma-Token": this.config.apiToken, "User-Agent": "Figma-MCP-Server/1.0.0", }, timeout: this.config.timeout, }); this.setupInterceptors(); } private setupInterceptors(): void { this.instance.interceptors.request.use( (config) => { const requestKey = `${config.method}_${config.url}_${Date.now()}`; this.requestTimings.set(requestKey, Date.now()); (config as any).__requestKey = requestKey; logger.apiRequest( config.method?.toUpperCase() || "UNKNOWN", config.url || "unknown", { params: config.params, timeout: config.timeout, } ); return config; }, (error) => { logger.error("Request interceptor error:", { message: error.message, stack: error.stack, }); return Promise.reject(error); } ); this.instance.interceptors.response.use( (response) => { const requestKey = (response.config as any).__requestKey; const startTime = this.requestTimings.get(requestKey); const duration = startTime ? Date.now() - startTime : 0; if (requestKey) { this.requestTimings.delete(requestKey); } logger.apiResponse( response.config.method?.toUpperCase() || "UNKNOWN", response.config.url || "unknown", response.status, duration, { statusText: response.statusText, dataSize: JSON.stringify(response.data).length, } ); return response; }, (error) => { const requestKey = (error.config as any)?.__requestKey; const startTime = requestKey ? this.requestTimings.get(requestKey) : null; const duration = startTime ? Date.now() - startTime : 0; if (requestKey) { this.requestTimings.delete(requestKey); } if (error.response) { logger.apiResponse( error.config?.method?.toUpperCase() || "UNKNOWN", error.config?.url || "unknown", error.response.status, duration, { statusText: error.response.statusText, errorData: error.response.data, } ); } handleAxiosError(error, "API Request"); } ); } /** * Get file nodes with comprehensive error handling and validation. */ public getFileNodes = withErrorHandling( "getFileNodes", async ( fileKey: string, nodeIds: string | string[] ): Promise<GetFileNodesResponse> => { if (!isValidFileKey(fileKey)) { throw new FigmaServerError( "Invalid file key format", ErrorType.VALIDATION_ERROR, { operation: "getFileNodes", input: { fileKey }, timestamp: new Date().toISOString(), }, 400 ); } const nodeIdArray = Array.isArray(nodeIds) ? nodeIds : [nodeIds]; for (const nodeId of nodeIdArray) { if (!isValidNodeId(nodeId)) { throw new FigmaServerError( `Invalid node ID format: ${nodeId}`, ErrorType.VALIDATION_ERROR, { operation: "getFileNodes", input: { fileKey, nodeIds }, timestamp: new Date().toISOString(), }, 400 ); } } const nodeIdsString = nodeIdArray.join(","); const response = await withTimeout( this.instance.get<GetFileNodesResponse>(`/files/${fileKey}/nodes`, { params: { ids: nodeIdsString }, }), this.config.timeout, "getFileNodes" ); // Validate response structure if (!response.data || !response.data.nodes) { throw new FigmaServerError( "Invalid response format from Figma API", ErrorType.API_ERROR, { operation: "getFileNodes", timestamp: new Date().toISOString(), } ); } return response.data; } ); /** * Get images with proper error handling. */ public getImages = withErrorHandling( "getImages", async ( fileKey: string, nodeIds: string | string[], format: "png" | "jpg" | "svg" | "pdf" = "png", scale: number = 1 ): Promise<GetImagesResponse> => { if (!isValidFileKey(fileKey)) { throw new FigmaServerError( "Invalid file key format", ErrorType.VALIDATION_ERROR, { operation: "getImages", input: { fileKey }, timestamp: new Date().toISOString(), }, 400 ); } const nodeIdArray = Array.isArray(nodeIds) ? nodeIds : [nodeIds]; for (const nodeId of nodeIdArray) { if (!isValidNodeId(nodeId)) { throw new FigmaServerError( `Invalid node ID format: ${nodeId}`, ErrorType.VALIDATION_ERROR, { operation: "getImages", input: { fileKey, nodeIds }, timestamp: new Date().toISOString(), }, 400 ); } } if (scale < 0.01 || scale > 4) { throw new FigmaServerError( "Scale must be between 0.01 and 4", ErrorType.VALIDATION_ERROR, { operation: "getImages", input: { scale }, timestamp: new Date().toISOString(), }, 400 ); } const nodeIdsString = nodeIdArray.join(","); const response = await withTimeout( this.instance.get<GetImagesResponse>(`/images/${fileKey}`, { params: { ids: nodeIdsString, format, scale: scale.toString(), }, }), this.config.timeout, "getImages" ); if (response.data.err) { throw new FigmaServerError( `Figma API error: ${response.data.err}`, ErrorType.API_ERROR, { operation: "getImages", timestamp: new Date().toISOString(), } ); } return response.data; } ); /** * Transform node ID format (hyphens to colons). */ public transformNodeId(nodeId: string): string { return nodeId.replace(/-/g, ":"); } }

Latest Blog Posts

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/toddle-edu/figma-mcp-server'

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