Skip to main content
Glama

Figma MCP Server

by xxflux
server.js112 kB
// server.js // Run with: node server.js // // Desired MCP flow: // 1) GET /sse-cursor => SSE => event:endpoint => /message?sessionId=XYZ // 2) POST /message?sessionId=XYZ => {method:"initialize"} => minimal HTTP ack => SSE => big "capabilities" // 3) {method:"tools/list"} => SSE => Tools array (including addNumbersTool) // 4) {method:"tools/call"} => SSE => result of the call (like summing two numbers) // 5) notifications/initialized => ack // // To avoid "unknown ID" errors, we always use rpc.id in the SSE response. import express from 'express'; import cors from 'cors'; import { v4 as uuidv4 } from 'uuid'; import WebSocket from 'ws'; import fetch from 'node-fetch'; import dotenv from 'dotenv'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // Get current directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load environment variables dotenv.config(); // Lucide icons support const LUCIDE_ICONS = { // Common icons 'activity': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>', 'alert-circle': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-alert-circle"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>', 'archive': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-archive"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>', 'arrow-right': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>', 'bell': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>', 'calendar': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>', 'check': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>', 'chevron-down': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>', 'chevron-left': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>', 'chevron-right': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>', 'chevron-up': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>', 'clipboard': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>', 'clock': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>', 'copy': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>', 'download': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>', 'edit': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>', 'external-link': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" x2="21" y1="14" y2="3"/></svg>', 'file-text': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/></svg>', 'filter': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>', 'heart': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>', 'home': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-home"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>', 'image': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>', 'link': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>', 'mail': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>', 'menu': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>', 'minus': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus"><line x1="5" x2="19" y1="12" y2="12"/></svg>', 'more-horizontal': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-more-horizontal"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>', 'more-vertical': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-more-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>', 'plus': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus"><line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/></svg>', 'search': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/></svg>', 'settings': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>', 'square-x': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-x"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>', 'trash': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>', 'upload': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-upload"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>', 'user': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', 'x': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><line x1="18" x2="6" y1="6" y2="18"/><line x1="6" x2="18" y1="6" y2="18"/></svg>' }; // You can add more icons here or load them dynamically from the Lucide library const app = express(); const port = 3000; // Constants const FIGMA_ACCESS_TOKEN = process.env.FIGMA_ACCESS_TOKEN; const FIGMA_API_BASE_URL = 'https://api.figma.com/v1'; // Enable CORS app.use(cors()); // Parse JSON bodies app.use(express.json()); // We store sessions by sessionId => { sseRes, initialized: boolean } const sessions = new Map(); // Store connected Figma plugin clients const figmaClients = new Map(); /* |-------------------------------------------------------------------------- | WebSocket server for plugin communication |-------------------------------------------------------------------------- */ const wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket server running on port 8080'); // Handle WebSocket connections from Figma plugins wss.on('connection', (ws) => { const clientId = Date.now().toString(); figmaClients.set(clientId, ws); console.log(`Figma plugin connected: ${clientId}`); // Handle incoming messages from Figma plugin ws.on('message', (message) => { try { const data = JSON.parse(message.toString()); console.log('Received message from plugin:', JSON.stringify(data, null, 2)); // Handle different message types from plugin if (data.type === 'plugin-ready') { console.log('Plugin is ready and connected'); } else if (data.type === 'operation-completed') { console.log('Operation completed:', data.originalOperation, data.status); // Check if this is part of a delete operation if (data.originalOperation === 'delete-node') { console.log('Delete operation completed successfully:', data.data); } else if (data.originalOperation === 'move-node') { console.log('Move operation completed successfully:', data.data); } } else if (data.type === 'nodes-deleted') { console.log('Nodes deleted:', data.count, data.nodeIds); } else if (data.type === 'nodes-moved') { console.log('Nodes moved:', data.count, 'nodes to new positions'); console.log('Move details:', data.nodes); } else if (data.type === 'operation-error') { console.error('Operation error:', data.originalOperation, data.error); } else if (data.type === 'fonts-list') { console.log('Fonts list received:', data.fonts?.length); } else if (data.type === 'nodes-list') { console.log('Nodes list received:', data.count); } else { console.log('Unhandled message type:', data.type); } } catch (error) { console.error('Error parsing WebSocket message:', error); } }); // Handle disconnection ws.on('close', () => { console.log(`Figma plugin disconnected: ${clientId}`); figmaClients.delete(clientId); }); // Send initial message to plugin ws.send(JSON.stringify({ type: 'server-ready', message: 'MCP server is ready' })); }); // Function to send operations to Figma plugin via WebSocket const sendOperationToPlugin = (operation) => { if (figmaClients.size === 0) { console.warn('No Figma plugins connected to send operation to'); return; } // Send to all connected Figma plugins for (const [clientId, client] of figmaClients.entries()) { try { client.send(JSON.stringify(operation)); console.log(`Operation sent to plugin ${clientId}`); } catch (error) { console.error(`Error sending to plugin ${clientId}:`, error); } } }; // Health check endpoint app.get('/', (req, res) => { return res.json({ status: 'Figma MCP Server is running' }); }); /* |-------------------------------------------------------------------------- | 1) SSE => GET /sse-cursor |-------------------------------------------------------------------------- | => Sends event:endpoint => /message?sessionId=XYZ | => Does NOT send big JSON at this point | => Also sends a heartbeat every 10 seconds */ app.get("/sse-cursor", (req, res) => { console.log("[MCP] SSE => /sse-cursor connected"); // SSE headers res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); // Generate a sessionId const sessionId = uuidv4(); sessions.set(sessionId, { sseRes: res, initialized: false }); console.log("[MCP] Created sessionId:", sessionId); // event: endpoint => /message?sessionId=... res.write(`event: endpoint\n`); res.write(`data: /message?sessionId=${sessionId}\n\n`); // Heartbeat every 10 seconds const hb = setInterval(() => { res.write(`event: heartbeat\ndata: ${Date.now()}\n\n`); }, 10000); // Cleanup on disconnect req.on("close", () => { clearInterval(hb); sessions.delete(sessionId); console.log("[MCP] SSE closed => sessionId=", sessionId); }); }); /* |-------------------------------------------------------------------------- | 2) JSON-RPC => POST /message?sessionId=... |-------------------------------------------------------------------------- | => "initialize" => minimal ack => SSE => big "capabilities" | => "tools/list" => minimal ack => SSE => array of tools | => "tools/call" => minimal ack => SSE => result of the call, e.g. sum | => "notifications/initialized" => ack |-------------------------------------------------------------------------- */ app.post("/message", async (req, res) => { console.log("[MCP] POST /message => body:", req.body, " query:", req.query); const sessionId = req.query.sessionId || req.header('X-Figma-MCP-SessionId'); if (!sessionId) { return res.status(400).json({ error: "Missing sessionId in ?sessionId=... or X-Figma-MCP-SessionId header" }); } const sessionData = sessions.get(sessionId); if (!sessionData) { return res.status(404).json({ error: "No SSE session with that sessionId" }); } const rpc = req.body; // Check JSON-RPC formatting if (!rpc || rpc.jsonrpc !== "2.0" || !rpc.method) { return res.json({ jsonrpc: "2.0", id: rpc?.id ?? null, error: { code: -32600, message: "Invalid JSON-RPC request" } }); } // Minimal HTTP ack res.json({ jsonrpc: "2.0", id: rpc.id, result: { ack: `Received ${rpc.method}` } }); // The actual response => SSE const sseRes = sessionData.sseRes; if (!sseRes) { console.log("[MCP] No SSE response found => sessionId=", sessionId); return; } // Process the JSON-RPC method let result; let error; switch (rpc.method) { // -- initialize case "initialize": { sessionData.initialized = true; // SSE => event: message => big "capabilities" const initCaps = { jsonrpc: "2.0", id: rpc.id, // Use the same ID => no "unknown ID" error result: { protocolVersion: "2024-11-05", capabilities: { tools: { listChanged: true }, resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, logging: {} }, serverInfo: { name: "final-capabilities-server", version: "1.0.0" } } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(initCaps)}\n\n`); console.log("[MCP] SSE => event: message => init caps => sessionId=", sessionId); return; } // -- tools/list case "tools/list": { const toolsMsg = { jsonrpc: "2.0", id: rpc.id, // same ID => no "unknown ID" result: { tools: [ { name: "addNumbersTool", description: "Adds two numbers 'a' and 'b' and returns their sum.", inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] } }, { name: "figma.getFile", description: "Get a Figma file by ID", inputSchema: { type: "object", properties: { fileId: { type: "string", description: "The ID of the Figma file" } }, required: ["fileId"] } }, { name: "figma.createRectangle", description: "Create a rectangle shape in Figma", inputSchema: { type: "object", properties: { position: { type: "object", description: "Position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, size: { type: "object", description: "Size dimensions", properties: { width: { type: "number" }, height: { type: "number" } }, required: ["width", "height"] }, color: { type: "object", description: "Fill color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } } }, required: ["position", "size"] } }, { name: "figma.createText", description: "Create a text element in Figma", inputSchema: { type: "object", properties: { text: { type: "string", description: "The text content" }, position: { type: "object", description: "Position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, fontSize: { type: "number", description: "Font size in pixels" }, color: { type: "object", description: "Text color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, fontFamily: { type: "string", description: "Font family to use (e.g., 'Inter', 'Roboto')" }, resizeMode: { type: "string", description: "Text resize behavior: 'AUTO_WIDTH', 'AUTO_HEIGHT', or 'FIXED_SIZE'", enum: ["AUTO_WIDTH", "AUTO_HEIGHT", "FIXED_SIZE"] } }, required: ["text", "position"] } }, { name: "figma.createPage", description: "Create a complete page based on description", inputSchema: { type: "object", properties: { pageName: { type: "string", description: "Name of the page to create" }, description: { type: "string", description: "Detailed description of the page layout and contents" }, styleGuide: { type: "object", description: "Style guide parameters", properties: { colors: { type: "object", description: "Color palette to use" }, spacing: { type: "object", description: "Spacing guidelines" }, typography: { type: "object", description: "Typography settings" } } } }, required: ["pageName", "description"] } }, // New tools { name: "figma.selectNode", description: "Select a node by its ID in the Figma canvas", inputSchema: { type: "object", properties: { nodeId: { type: "string", description: "The ID of the node to select" } }, required: ["nodeId"] } }, { name: "figma.changeColor", description: "Change the color of the selected node(s)", inputSchema: { type: "object", properties: { color: { type: "object", description: "Fill color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } }, required: ["r", "g", "b"] }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } }, required: ["color"] } }, { name: "figma.changeRadius", description: "Change the corner radius of the selected node(s)", inputSchema: { type: "object", properties: { radius: { type: "number", description: "Corner radius value in pixels" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } }, required: ["radius"] } }, { name: "figma.changeTypeface", description: "Change the typeface of the selected text node(s)", inputSchema: { type: "object", properties: { fontFamily: { type: "string", description: "Font family name (e.g., 'Inter', 'Roboto')" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } }, required: ["fontFamily"] } }, { name: "figma.changeFontStyle", description: "Change the font style of the selected text node(s)", inputSchema: { type: "object", properties: { fontSize: { type: "number", description: "Font size in pixels" }, fontWeight: { type: "string", description: "Font weight (e.g., 'Regular', 'Bold', 'SemiBold')" }, italic: { type: "boolean", description: "Whether the text should be italic" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } } } }, { name: "figma.changeAlignment", description: "Change the text alignment of the selected text node(s)", inputSchema: { type: "object", properties: { horizontal: { type: "string", description: "Horizontal alignment ('LEFT', 'CENTER', 'RIGHT', 'JUSTIFIED')" }, vertical: { type: "string", description: "Vertical alignment ('TOP', 'CENTER', 'BOTTOM')" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } } } }, { name: "figma.changeSpacing", description: "Change margin or padding of the selected auto layout node(s)", inputSchema: { type: "object", properties: { padding: { type: "object", description: "Padding values", properties: { top: { type: "number" }, right: { type: "number" }, bottom: { type: "number" }, left: { type: "number" } } }, itemSpacing: { type: "number", description: "Spacing between items in auto layout" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } } } }, { name: "figma.listFonts", description: "Get a list of available font families in Figma", inputSchema: { type: "object", properties: {} } }, { name: "figma.changeTextResize", description: "Change the resize behavior of text elements", inputSchema: { type: "object", properties: { resizeMode: { type: "string", description: "Text resize behavior: 'AUTO_WIDTH', 'AUTO_HEIGHT', or 'FIXED_SIZE'", enum: ["AUTO_WIDTH", "AUTO_HEIGHT", "FIXED_SIZE"] }, width: { type: "number", description: "Width in pixels (used for AUTO_HEIGHT and FIXED_SIZE modes)" }, height: { type: "number", description: "Height in pixels (used for FIXED_SIZE mode only)" }, nodeId: { type: "string", description: "Optional node ID to target. If not provided, currently selected nodes will be used" } }, required: ["resizeMode"] } }, { name: "figma.listNodes", description: "List all node IDs and types in the current page", inputSchema: { type: "object", properties: { includeDetails: { type: "boolean", description: "Whether to include details about each node (name, type, etc.)" } } } }, { name: "figma.deleteNode", description: "Delete a node or nodes from the Figma canvas", inputSchema: { type: "object", properties: { nodeId: { type: "string", description: "Optional node ID to delete. If not provided, currently selected nodes will be deleted" } } } }, { name: "figma.moveNode", description: "Move a node or nodes to a specific position on the canvas", inputSchema: { type: "object", properties: { position: { type: "object", description: "The position to move the node(s) to", properties: { x: { type: "number", description: "X coordinate in Figma's coordinate system" }, y: { type: "number", description: "Y coordinate in Figma's coordinate system" } }, required: ["x", "y"] }, nodeId: { type: "string", description: "Optional node ID to move. If not provided, currently selected nodes will be moved" } }, required: ["position"] } }, { name: "figma.createIcon", description: "Create a Lucide icon in Figma", inputSchema: { type: "object", properties: { iconName: { type: "string", description: "Name of the Lucide icon to create (e.g., 'heart', 'home', 'settings')" }, position: { type: "object", description: "Position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, size: { type: "number", description: "Icon size in pixels (default: 24)" }, color: { type: "object", description: "Icon color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, strokeWidth: { type: "number", description: "Icon stroke width (default: 2)" } }, required: ["iconName", "position"] } }, { name: "figma.createBorderBox", description: "Create a border line box or button in Figma", inputSchema: { type: "object", properties: { position: { type: "object", description: "Position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, size: { type: "object", description: "Size dimensions", properties: { width: { type: "number" }, height: { type: "number" } }, required: ["width", "height"] }, options: { type: "object", description: "Box/button styling options", properties: { type: { type: "string", description: "Type of element to create ('box' or 'button')", enum: ["box", "button"] }, borderColor: { type: "object", description: "Border color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, borderWidth: { type: "number", description: "Border width in pixels (default: 1)" }, fillColor: { type: "object", description: "Fill color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, cornerRadius: { type: "number", description: "Corner radius in pixels" }, text: { type: "string", description: "Text to display inside the box/button" }, textColor: { type: "object", description: "Text color in RGB (values from 0-1)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, fontSize: { type: "number", description: "Text font size in pixels (default: 16)" }, fontFamily: { type: "string", description: "Text font family (default: Inter)" } }, required: ["type"] } }, required: ["position", "size", "options"] } }, { name: "figma.drawLine", description: "Draw a line in Figma", inputSchema: { type: "object", properties: { start: { type: "object", description: "Starting position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, end: { type: "object", description: "Ending position coordinates", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, color: { type: "object", description: "Line color in RGB (values from 0-1, default: black)", properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" } } }, thickness: { type: "number", description: "Line thickness in pixels (default: 1)" } }, required: ["start", "end"] } } ], count: 19 } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(toolsMsg)}\n\n`); console.log("[MCP] SSE => event: message => tools/list => sessionId=", sessionId); return; } // -- tools/call: e.g. addNumbersTool or Figma tools case "tools/call": { // e.g. { name: "addNumbersTool", arguments: { a:..., b:... } } const toolName = rpc.params?.name; const args = rpc.params?.arguments || {}; console.log("[MCP] tools/call => name=", toolName, "args=", args); if (toolName === "addNumbersTool") { const sum = (args.a || 0) + (args.b || 0); // SSE => event: message => the result const callMsg = { jsonrpc: "2.0", id: rpc.id, // use the same ID => no unknown ID result: { content: [ { type: "text", text: `Sum of ${args.a} + ${args.b} = ${sum}` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => tools/call => sum", sum); } else if (toolName === "figma.getFile") { // Use Figma API to get file data if (!args.fileId) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing fileId parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } try { const response = await fetch(`${FIGMA_API_BASE_URL}/files/${args.fileId}`, { headers: { 'X-Figma-Token': FIGMA_ACCESS_TOKEN || '' } }); if (!response.ok) { throw new Error(`Figma API error: ${response.statusText}`); } const data = await response.json(); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: JSON.stringify(data, null, 2) } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.getFile success"); } catch (err) { console.error('Error fetching Figma file:', err); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Internal error", data: "Failed to fetch Figma file" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); } } else if (toolName === "figma.createRectangle") { // Use WebSocket to send operation to plugin if (!args.position || !args.size) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing position or size parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'create-rectangle', position: args.position, size: args.size, color: args.color || { r: 0.8, g: 0.8, b: 0.8 } }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: "Rectangle creation operation sent to plugin" } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.createRectangle success"); } else if (toolName === "figma.createText") { // Use WebSocket to send operation to plugin if (!args.text || !args.position) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing text or position parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'create-text', text: args.text, position: args.position, fontSize: args.fontSize || 24, color: args.color || { r: 0, g: 0, b: 0 }, fontFamily: args.fontFamily || "Inter", resizeMode: args.resizeMode || "AUTO_WIDTH" }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: "Text creation operation sent to plugin" } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.createText success"); } else if (toolName === "figma.createPage") { // Use WebSocket to send operation to plugin if (!args.pageName || !args.description) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing pageName or description parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'create-page', pageName: args.pageName, description: args.description, styleGuide: args.styleGuide || {} }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: "Page creation operation sent to plugin" } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.createPage success"); } // New tool handlers else if (toolName === "figma.selectNode") { if (!args.nodeId) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing nodeId parameter. Please first use figma.listNodes to get available node IDs." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } // Send operation to plugin sendOperationToPlugin({ type: 'select-node', nodeId: args.nodeId }); // Set up a timeout to handle the response const timeout = setTimeout(() => { const timeoutMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "Node selection operation timed out. Try using figma.listNodes first to get valid node IDs." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(timeoutMsg)}\n\n`); }, 5000); // 5 second timeout // Listen for operation completion or error const handlePluginResponse = (message) => { try { const data = JSON.parse(message); if (data.type === 'operation-completed' && data.originalOperation === 'select-node') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully selected node: ${args.nodeId}` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.selectNode success"); } else if (data.type === 'operation-error' && data.originalOperation === 'select-node') { clearTimeout(timeout); // Format a more helpful error message with guidance let helpfulMessage = data.error; if (data.error && data.error.includes("not found")) { helpfulMessage = `${data.error}\n\nPlease try these steps:\n1. Use figma.listNodes to get valid node IDs\n2. Make sure you're on the correct page in Figma\n3. Try using one of the available node IDs from the list`; } const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation failed", data: helpfulMessage } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.selectNode error:", data.error); } } catch (error) { console.error('Error handling plugin response:', error); } }; // Add one-time listener for the next plugin response const ws = Array.from(figmaClients.values())[0]; if (ws) { ws.once('message', handlePluginResponse); } else { clearTimeout(timeout); const noConnectionMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "No Figma connection", data: "No Figma plugin connection available. Make sure the Figma plugin is running and connected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(noConnectionMsg)}\n\n`); } } else if (toolName === "figma.changeColor") { if (!args.color) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing color parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-color', color: args.color, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Color change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeColor success"); } else if (toolName === "figma.changeRadius") { if (args.radius === undefined) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing radius parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-radius', radius: args.radius, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Corner radius change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeRadius success"); } else if (toolName === "figma.changeTypeface") { if (!args.fontFamily) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing fontFamily parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-typeface', fontFamily: args.fontFamily, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Typeface change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeTypeface success"); } else if (toolName === "figma.changeFontStyle") { if (!args.fontSize && !args.fontWeight && args.italic === undefined) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "At least one of fontSize, fontWeight, or italic must be provided" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-font-style', fontSize: args.fontSize, fontWeight: args.fontWeight, italic: args.italic, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Font style change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeFontStyle success"); } else if (toolName === "figma.changeAlignment") { if (!args.horizontal && !args.vertical) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "At least one of horizontal or vertical alignment must be provided" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-alignment', horizontal: args.horizontal, vertical: args.vertical, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Alignment change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeAlignment success"); } else if (toolName === "figma.changeSpacing") { if (!args.padding && args.itemSpacing === undefined) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "At least one of padding or itemSpacing must be provided" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-spacing', padding: args.padding, itemSpacing: args.itemSpacing, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Spacing change operation sent to plugin` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeSpacing success"); } else if (toolName === "figma.listFonts") { // Send operation to plugin to get available fonts sendOperationToPlugin({ type: 'list-fonts' }); // Define a set of common fonts available in Figma as a fallback const commonFonts = [ "Inter", "Roboto", "SF Pro", "Helvetica Neue", "Arial", "Georgia", "Times New Roman", "Courier New", "Comic Sans MS", "Open Sans", "Montserrat", "Lato", "Poppins", "Playfair Display", "Nunito", "Work Sans", "Source Sans Pro", "IBM Plex Sans", "Roboto Mono" ]; const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Font families available in Figma: ${JSON.stringify(commonFonts)}` } ], fonts: commonFonts } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.listFonts success"); } else if (toolName === "figma.listNodes") { // Send operation to plugin to get available nodes sendOperationToPlugin({ type: 'list-nodes', includeDetails: args.includeDetails || false }); // Set up a timeout to handle the response const timeout = setTimeout(() => { const timeoutMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "Node listing operation timed out" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(timeoutMsg)}\n\n`); }, 5000); // 5 second timeout // Listen for operation completion or error const handlePluginResponse = (message) => { try { const data = JSON.parse(message); if (data.type === 'nodes-list') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Available nodes in the current page: ${JSON.stringify(data.nodes, null, 2)}` } ], nodes: data.nodes } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.listNodes success"); } else if (data.type === 'operation-error' && data.originalOperation === 'list-nodes') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation failed", data: data.error } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.listNodes error:", data.error); } } catch (error) { console.error('Error handling plugin response:', error); } }; // Add one-time listener for the next plugin response const ws = Array.from(figmaClients.values())[0]; if (ws) { ws.once('message', handlePluginResponse); } else { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "No Figma plugin connected", data: "Cannot list nodes because no Figma plugin is connected" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.listNodes error: No plugin connected"); clearTimeout(timeout); } } else if (toolName === "figma.changeTextResize") { if (!args.resizeMode) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing resizeMode parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } sendOperationToPlugin({ type: 'change-text-resize', resizeMode: args.resizeMode, width: args.width, height: args.height, nodeId: args.nodeId || null }); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Text resize mode changed to ${args.resizeMode}` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.changeTextResize success"); } else if (toolName === "figma.deleteNode") { // Send operation to plugin sendOperationToPlugin({ type: 'delete-node', nodeId: args.nodeId || null }); // Set up a timeout to handle the response const timeout = setTimeout(() => { const timeoutMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "Delete node operation timed out. The plugin might be disconnected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(timeoutMsg)}\n\n`); }, 5000); // 5 second timeout // Listen for operation completion or error const handlePluginResponse = (message) => { try { const data = JSON.parse(message); if (data.type === 'operation-completed' && data.originalOperation === 'delete-node') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully deleted ${args.nodeId ? `node: ${args.nodeId}` : 'selected nodes'}` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.deleteNode success"); } else if (data.type === 'nodes-deleted') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully deleted ${data.count} node(s): ${data.nodeIds.join(', ')}` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.deleteNode success"); } else if (data.type === 'operation-error' && data.originalOperation === 'delete-node') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation failed", data: data.error || "Failed to delete node(s)" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); console.log("[MCP] SSE => event: message => figma.deleteNode error:", data.error); } } catch (error) { console.error('Error handling plugin response:', error); } }; // Add one-time listener for the next plugin response const ws = Array.from(figmaClients.values())[0]; if (ws) { ws.once('message', handlePluginResponse); } else { clearTimeout(timeout); const noConnectionMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "No Figma connection", data: "No Figma plugin connection available. Make sure the Figma plugin is running and connected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(noConnectionMsg)}\n\n`); } } else if (toolName === "figma.moveNode") { // Use WebSocket to send operation to plugin if (!args.position) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing position parameter" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } // Create a timeout to handle possible failures const timeout = setTimeout(() => { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "The operation timed out. The plugin might be disconnected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); }, 5000); // Set up message listener for operation completion or error const handleMovedNodes = (clientId, ws, message) => { try { const data = JSON.parse(message.toString()); if (data.type === 'nodes-moved') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Moved ${data.count} node(s) to position (${args.position.x}, ${args.position.y})` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); ws.removeListener('message', handleMovedNodes.bind(null, clientId, ws)); } else if (data.type === 'operation-error' && data.originalOperation === 'move-node') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation error", data: data.error } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); ws.removeListener('message', handleMovedNodes.bind(null, clientId, ws)); } else if (data.type === 'operation-completed' && data.originalOperation === 'move-node') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully moved node(s) to position (${args.position.x}, ${args.position.y})` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); ws.removeListener('message', handleMovedNodes.bind(null, clientId, ws)); } } catch (error) { console.error(`Error parsing WebSocket message:`, error); } }; // Add temporary message handler for each client for (const [clientId, ws] of figmaClients.entries()) { ws.on('message', handleMovedNodes.bind(null, clientId, ws)); } sendOperationToPlugin({ type: 'move-node', position: args.position, nodeId: args.nodeId }); // We don't send a response here as it will be sent by the message handler } else if (toolName === "figma.createIcon") { // Use WebSocket to send operation to plugin if (!args.iconName || !args.position) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing iconName or position parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } let iconNameToUse = args.iconName; let svgDataToSend = LUCIDE_ICONS[iconNameToUse]; let fallbackUsed = false; // Check if the icon exists, use fallback if not if (!svgDataToSend) { console.warn(`Icon '${args.iconName}' not found in Lucide icon set. Falling back to 'square-x'.`); iconNameToUse = 'square-x'; svgDataToSend = LUCIDE_ICONS[iconNameToUse]; // Get fallback SVG fallbackUsed = true; // Ensure fallback icon exists if (!svgDataToSend) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Internal Server Error", data: "Fallback icon 'square-x' is missing from the server configuration." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } } // Create a timeout to handle possible failures const timeout = setTimeout(() => { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "The operation timed out. The plugin might be disconnected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); // Ensure the listener is removed on timeout for (const [clientId, ws] of figmaClients.entries()) { ws.removeListener('message', handleIconCreation.bind(null, clientId, ws)); } }, 5000); // Set up message listener for operation completion or error const handleIconCreation = (clientId, ws, message) => { try { const data = JSON.parse(message.toString()); if (data.type === 'operation-completed' && data.originalOperation === 'create-icon') { clearTimeout(timeout); let successText = `Successfully created '${iconNameToUse}' icon (ID: ${data.data?.nodeId || 'unknown'})`; if (fallbackUsed) { successText = `Icon '${args.iconName}' not found. Created fallback '${iconNameToUse}' icon instead (ID: ${data.data?.nodeId || 'unknown'})`; } const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: successText } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); ws.removeListener('message', handleIconCreation.bind(null, clientId, ws)); } else if (data.type === 'operation-error' && data.originalOperation === 'create-icon') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation error creating icon", data: data.error } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); ws.removeListener('message', handleIconCreation.bind(null, clientId, ws)); } } catch (error) { console.error(`Error parsing WebSocket message during icon creation:`, error); // Clear timeout and remove listener in case of parsing error clearTimeout(timeout); ws.removeListener('message', handleIconCreation.bind(null, clientId, ws)); } }; // Add temporary message handler for each client for (const [clientId, ws] of figmaClients.entries()) { ws.on('message', handleIconCreation.bind(null, clientId, ws)); } // Send the operation to the plugin with the potentially modified icon name and SVG data sendOperationToPlugin({ type: 'create-icon', iconName: iconNameToUse, // Use potentially fallback name svgData: svgDataToSend, // Use potentially fallback SVG position: args.position, size: args.size, // Pass through optional args color: args.color, strokeWidth: args.strokeWidth }); // We don't send a response here as it will be sent by the message handler } else if (toolName === "figma.createBorderBox") { // Use WebSocket to send operation to plugin if (!args.position || !args.size || !args.options) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing position, size, or options parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } // Create a timeout to handle possible failures const timeout = setTimeout(() => { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "The operation timed out. The plugin might be disconnected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); }, 5000); // Set up message listener for operation completion or error const handleBorderBoxCreation = (clientId, ws, message) => { try { const data = JSON.parse(message.toString()); if (data.type === 'operation-completed' && data.originalOperation === 'create-border-box') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully created border box (ID: ${data.data?.nodeId || 'unknown'})` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); ws.removeListener('message', handleBorderBoxCreation.bind(null, clientId, ws)); } else if (data.type === 'operation-error' && data.originalOperation === 'create-border-box') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation error", data: data.error } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); ws.removeListener('message', handleBorderBoxCreation.bind(null, clientId, ws)); } } catch (error) { console.error(`Error parsing WebSocket message:`, error); } }; // Add temporary message handler for each client for (const [clientId, ws] of figmaClients.entries()) { ws.on('message', handleBorderBoxCreation.bind(null, clientId, ws)); } // Send the operation to the plugin with the styling options sendOperationToPlugin({ type: 'create-border-box', position: args.position, size: args.size, options: args.options }); // We don't send a response here as it will be sent by the message handler } else if (toolName === "figma.drawLine") { // Use WebSocket to send operation to plugin if (!args.start || !args.end) { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32602, message: "Invalid params", data: "Missing start or end parameters" } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); return; } // Create a timeout to handle possible failures const timeout = setTimeout(() => { const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation timeout", data: "The operation timed out. The plugin might be disconnected." } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); // Ensure the listener is removed on timeout for (const [clientId, ws] of figmaClients.entries()) { ws.removeListener('message', handleLineDrawing.bind(null, clientId, ws)); } }, 5000); // 5 second timeout // Set up message listener for operation completion or error const handleLineDrawing = (clientId, ws, message) => { try { const data = JSON.parse(message.toString()); if (data.type === 'operation-completed' && data.originalOperation === 'draw-line') { clearTimeout(timeout); const callMsg = { jsonrpc: "2.0", id: rpc.id, result: { content: [ { type: "text", text: `Successfully drew line (ID: ${data.data?.nodeId || 'unknown'}) from (${args.start.x}, ${args.start.y}) to (${args.end.x}, ${args.end.y})` } ] } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(callMsg)}\n\n`); ws.removeListener('message', handleLineDrawing.bind(null, clientId, ws)); } else if (data.type === 'operation-error' && data.originalOperation === 'draw-line') { clearTimeout(timeout); const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32603, message: "Operation error drawing line", data: data.error } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); ws.removeListener('message', handleLineDrawing.bind(null, clientId, ws)); } } catch (error) { console.error(`Error parsing WebSocket message during line drawing:`, error); // Clear timeout and remove listener in case of parsing error clearTimeout(timeout); ws.removeListener('message', handleLineDrawing.bind(null, clientId, ws)); } }; // Add temporary message handler for each client for (const [clientId, ws] of figmaClients.entries()) { ws.on('message', handleLineDrawing.bind(null, clientId, ws)); } // Send the operation to the plugin sendOperationToPlugin({ type: 'draw-line', start: args.start, end: args.end, color: args.color, // Pass color directly, plugin handles default thickness: args.thickness // Pass thickness directly, plugin handles default }); // We don't send a response here as it will be sent by the message handler } else { // Unknown tool const errorMsg = { jsonrpc: "2.0", id: rpc.id, error: { code: -32601, message: "Method not found", data: `Tool '${toolName}' not implemented` } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errorMsg)}\n\n`); } return; } // -- notifications/initialized case "notifications/initialized": { console.log("[MCP] notifications/initialized => sessionId=", sessionId); // done, no SSE needed return; } default: { console.log("[MCP] unknown method =>", rpc.method); const errObj = { jsonrpc: "2.0", id: rpc.id, error: { code: -32601, message: `Method '${rpc.method}' not recognized` } }; sseRes.write(`event: message\n`); sseRes.write(`data: ${JSON.stringify(errObj)}\n\n`); return; } } }); // Start the server app.listen(port, () => { console.log(`[MCP] final server with tools/call at http://localhost:${port}`); console.log("GET /sse-cursor => SSE => endpoint => /message?sessionId=..."); console.log("POST /message?sessionId=... => initialize => SSE => capabilities, tools/list => SSE => Tools, tools/call => SSE => sum, etc."); console.log("Starting Figma MCP Server..."); }); // Handle server shutdown process.on('SIGINT', () => { console.log('Shutting down server...'); // Close all SSE connections for (const [sessionId, session] of sessions.entries()) { sessions.delete(sessionId); console.log(`Closed SSE connection: ${sessionId}`); } // Close the WebSocket server wss.close(); process.exit(0); });

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/xxflux/figma_MCP'

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