#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const FIGMA_API_BASE = 'https://api.figma.com/v1'; const FIGMA_TOKEN = process.env.FIGMA_ACCESS_TOKEN; if (!FIGMA_TOKEN) { console.error('Error: FIGMA_ACCESS_TOKEN environment variable is required'); process.exit(1); } /** * Fetch comments from a Figma file * @param {string} fileId - The Figma file ID * @returns {Promise} Array of comments with full details */ async function getFigmaComments(fileId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); // Transform comments to include all requested information const comments = data.comments.map(comment => ({ id: comment.id, message: comment.message, author: { id: comment.user.id, handle: comment.user.handle, img_url: comment.user.img_url, }, timestamp: comment.created_at, resolvedAt: comment.resolved_at || null, location: comment.client_meta ? { node_id: comment.client_meta.node_id, node_offset: comment.client_meta.node_offset, x: comment.client_meta.x, y: comment.client_meta.y, } : null, parentId: comment.parent_id || null, // Collect replies replies: data.comments .filter(reply => reply.parent_id === comment.id) .map(reply => ({ id: reply.id, message: reply.message, author: { id: reply.user.id, handle: reply.user.handle, img_url: reply.user.img_url, }, timestamp: reply.created_at, })), })); // Return only top-level comments (not replies) return comments.filter(c => !c.parentId); } catch (error) { throw new Error(`Failed to fetch Figma comments: ${error.message}`); } } /** * Post a new comment to a Figma file * @param {string} fileId - The Figma file ID * @param {string} message - The comment message text (can include @mentions) * @param {Object} location - Optional location object with node_id or coordinates * @returns {Promise} The created comment */ async function postFigmaComment(fileId, message, location = null) { try { const body = {}; // Check if message contains mentions const hasMentions = /@\w+/.test(message); if (hasMentions) { // Get users to resolve mentions const { usersMap } = await getCommentUsers(fileId); const messageMeta = parseMessageWithMentions(message, usersMap); body.message_meta = messageMeta; } else { // Simple message without mentions body.message = message; } // Add location if provided if (location) { body.client_meta = {}; if (location.node_id) { body.client_meta.node_id = location.node_id; // Add node offset if provided if (location.node_offset) { body.client_meta.node_offset = location.node_offset; } } // Add coordinates if provided if (location.x !== undefined && location.y !== undefined) { body.client_meta.x = location.x; body.client_meta.y = location.y; } } const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to post Figma comment: ${error.message}`); } } /** * Parse message with @mentions and convert to Figma's message_meta format * @param {string} message - The message text with @mentions * @param {Map} usersMap - Map of user handles to user IDs * @returns {Array} Array of message_meta objects for Figma API */ function parseMessageWithMentions(message, usersMap) { const messageMeta = []; const mentionRegex = /@(\w+)/g; let lastIndex = 0; let match; while ((match = mentionRegex.exec(message)) !== null) { // Add text before mention if (match.index > lastIndex) { const textBefore = message.substring(lastIndex, match.index); if (textBefore) { messageMeta.push({ t: textBefore }); } } // Add mention const handle = match[1]; const userId = usersMap.get(handle); if (userId) { // Format as Figma mention (undocumented format based on web client) messageMeta.push({ t: `@${handle}`, mention: userId }); } else { // If user not found, keep as plain text messageMeta.push({ t: `@${handle}` }); } lastIndex = match.index + match[0].length; } // Add remaining text after last mention if (lastIndex < message.length) { const textAfter = message.substring(lastIndex); if (textAfter) { messageMeta.push({ t: textAfter }); } } // If no mentions found, return simple format if (messageMeta.length === 0) { return [{ t: message }]; } return messageMeta; } /** * Get unique users who have commented on a Figma file * Returns both as array and as Map for mention processing * @param {string} fileId - The Figma file ID * @returns {Promise} Object with users array and usersMap */ async function getCommentUsers(fileId) { try { const comments = await getFigmaComments(fileId); const usersById = new Map(); const usersByHandle = new Map(); // Collect all unique users from comments and replies comments.forEach(comment => { if (!usersById.has(comment.author.id)) { const userData = { id: comment.author.id, handle: comment.author.handle, img_url: comment.author.img_url, }; usersById.set(comment.author.id, userData); usersByHandle.set(comment.author.handle, comment.author.id); } // Also collect users from replies comment.replies.forEach(reply => { if (!usersById.has(reply.author.id)) { const userData = { id: reply.author.id, handle: reply.author.handle, img_url: reply.author.img_url, }; usersById.set(reply.author.id, userData); usersByHandle.set(reply.author.handle, reply.author.id); } }); }); return { users: Array.from(usersById.values()), usersMap: usersByHandle }; } catch (error) { throw new Error(`Failed to get comment users: ${error.message}`); } } /** * Reply to an existing Figma comment * @param {string} fileId - The Figma file ID * @param {string} commentId - The ID of the comment to reply to * @param {string} message - The reply message text (can include @mentions) * @returns {Promise} The created reply */ async function replyToFigmaComment(fileId, commentId, message) { try { const body = { comment_id: commentId, }; // Check if message contains mentions const hasMentions = /@\w+/.test(message); if (hasMentions) { // Get users to resolve mentions const { usersMap } = await getCommentUsers(fileId); const messageMeta = parseMessageWithMentions(message, usersMap); body.message_meta = messageMeta; } else { // Simple message without mentions body.message = message; } const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to reply to Figma comment: ${error.message}`); } } /** * Delete a comment from a Figma file * @param {string} fileId - The Figma file ID * @param {string} commentId - The ID of the comment to delete * @returns {Promise} The deletion response */ async function deleteFigmaComment(fileId, commentId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments/${commentId}`, { method: 'DELETE', headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } return { success: true, message: 'Comment deleted successfully' }; } catch (error) { throw new Error(`Failed to delete Figma comment: ${error.message}`); } } /** * Get reactions for a comment * @param {string} fileId - The Figma file ID * @param {string} commentId - The ID of the comment * @returns {Promise} The reactions data */ async function getCommentReactions(fileId, commentId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments/${commentId}/reactions`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get comment reactions: ${error.message}`); } } /** * Post a reaction to a comment * @param {string} fileId - The Figma file ID * @param {string} commentId - The ID of the comment * @param {string} emoji - The emoji reaction * @returns {Promise} The created reaction */ async function postCommentReaction(fileId, commentId, emoji) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments/${commentId}/reactions`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ emoji }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to post comment reaction: ${error.message}`); } } /** * Delete a reaction from a comment * @param {string} fileId - The Figma file ID * @param {string} commentId - The ID of the comment * @param {string} emoji - The emoji reaction to delete * @returns {Promise} The deletion response */ async function deleteCommentReaction(fileId, commentId, emoji) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/comments/${commentId}/reactions?emoji=${encodeURIComponent(emoji)}`, { method: 'DELETE', headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } return { success: true, message: 'Reaction deleted successfully' }; } catch (error) { throw new Error(`Failed to delete comment reaction: ${error.message}`); } } /** * Get a Figma file as JSON * @param {string} fileId - The Figma file ID * @param {Object} options - Optional parameters (version, ids, depth, geometry) * @returns {Promise} The file data */ async function getFile(fileId, options = {}) { try { const params = new URLSearchParams(); if (options.version) params.append('version', options.version); if (options.ids) params.append('ids', options.ids); if (options.depth) params.append('depth', options.depth); if (options.geometry) params.append('geometry', options.geometry); const queryString = params.toString(); const url = `${FIGMA_API_BASE}/files/${fileId}${queryString ? '?' + queryString : ''}`; const response = await fetch(url, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get Figma file: ${error.message}`); } } /** * Get specific nodes from a Figma file * @param {string} fileId - The Figma file ID * @param {string} ids - Comma-separated list of node IDs * @returns {Promise} The nodes data */ async function getFileNodes(fileId, ids) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/nodes?ids=${encodeURIComponent(ids)}`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get file nodes: ${error.message}`); } } /** * Render images from file nodes * @param {string} fileId - The Figma file ID * @param {Object} options - Options (ids, scale, format, svg_include_id, svg_simplify_stroke, use_absolute_bounds) * @returns {Promise} The rendered images URLs */ async function renderImages(fileId, options = {}) { try { const params = new URLSearchParams(); if (options.ids) params.append('ids', options.ids); if (options.scale) params.append('scale', options.scale); if (options.format) params.append('format', options.format); if (options.svg_include_id !== undefined) params.append('svg_include_id', options.svg_include_id); if (options.svg_simplify_stroke !== undefined) params.append('svg_simplify_stroke', options.svg_simplify_stroke); if (options.use_absolute_bounds !== undefined) params.append('use_absolute_bounds', options.use_absolute_bounds); const queryString = params.toString(); const url = `${FIGMA_API_BASE}/images/${fileId}${queryString ? '?' + queryString : ''}`; const response = await fetch(url, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to render images: ${error.message}`); } } /** * Get image fills from a Figma file * @param {string} fileId - The Figma file ID * @returns {Promise} The image fills data */ async function getImageFills(fileId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/images`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get image fills: ${error.message}`); } } /** * Get current user information * @returns {Promise} The user data */ async function getCurrentUser() { try { const response = await fetch(`${FIGMA_API_BASE}/me`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get current user: ${error.message}`); } } /** * Get version history of a Figma file * @param {string} fileId - The Figma file ID * @returns {Promise} The version history data */ async function getVersionHistory(fileId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/versions`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get version history: ${error.message}`); } } /** * Get team projects * @param {string} teamId - The team ID * @returns {Promise} The team projects data */ async function getTeamProjects(teamId) { try { const response = await fetch(`${FIGMA_API_BASE}/teams/${teamId}/projects`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get team projects: ${error.message}`); } } /** * Get project files * @param {string} projectId - The project ID * @returns {Promise} The project files data */ async function getProjectFiles(projectId) { try { const response = await fetch(`${FIGMA_API_BASE}/projects/${projectId}/files`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get project files: ${error.message}`); } } /** * Get local variables from a Figma file * @param {string} fileId - The Figma file ID * @returns {Promise} The local variables data */ async function getLocalVariables(fileId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/variables/local`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get local variables: ${error.message}`); } } /** * Get published variables from a Figma file * @param {string} fileId - The Figma file ID * @returns {Promise} The published variables data */ async function getPublishedVariables(fileId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/variables/published`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get published variables: ${error.message}`); } } /** * Create or update variables in a Figma file * @param {string} fileId - The Figma file ID * @param {Object} variablesData - The variables data to create/update * @returns {Promise} The created/updated variables */ async function createVariables(fileId, variablesData) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/variables`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(variablesData), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to create variables: ${error.message}`); } } /** * Get dev resources from a Figma file * @param {string} fileId - The Figma file ID * @param {string} nodeId - Optional node ID to filter resources * @returns {Promise} The dev resources data */ async function getDevResources(fileId, nodeId = null) { try { const url = nodeId ? `${FIGMA_API_BASE}/files/${fileId}/dev_resources?node_id=${encodeURIComponent(nodeId)}` : `${FIGMA_API_BASE}/files/${fileId}/dev_resources`; const response = await fetch(url, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get dev resources: ${error.message}`); } } /** * Create dev resources in a Figma file * @param {string} fileId - The Figma file ID * @param {Object} devResourceData - The dev resource data (node_id, url, name) * @returns {Promise} The created dev resource */ async function createDevResource(fileId, devResourceData) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/dev_resources`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(devResourceData), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to create dev resource: ${error.message}`); } } /** * Delete a dev resource from a Figma file * @param {string} fileId - The Figma file ID * @param {string} devResourceId - The dev resource ID to delete * @returns {Promise} The deletion response */ async function deleteDevResource(fileId, devResourceId) { try { const response = await fetch(`${FIGMA_API_BASE}/files/${fileId}/dev_resources/${devResourceId}`, { method: 'DELETE', headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } return { success: true, message: 'Dev resource deleted successfully' }; } catch (error) { throw new Error(`Failed to delete dev resource: ${error.message}`); } } /** * Create a webhook * @param {Object} webhookData - The webhook data (event_type, team_id, passcode, endpoint, description) * @returns {Promise} The created webhook */ async function createWebhook(webhookData) { try { const response = await fetch(`${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks`, { method: 'POST', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(webhookData), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to create webhook: ${error.message}`); } } /** * Get a webhook by ID * @param {string} webhookId - The webhook ID * @returns {Promise} The webhook data */ async function getWebhook(webhookId) { try { const response = await fetch(`${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks/${webhookId}`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get webhook: ${error.message}`); } } /** * Get all webhooks * @param {string} teamId - Optional team ID to filter webhooks * @returns {Promise} The webhooks data */ async function getWebhooks(teamId = null) { try { const url = teamId ? `${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks?team_id=${teamId}` : `${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks`; const response = await fetch(url, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get webhooks: ${error.message}`); } } /** * Update a webhook * @param {string} webhookId - The webhook ID * @param {Object} updateData - The data to update (status, description, endpoint, passcode) * @returns {Promise} The updated webhook */ async function updateWebhook(webhookId, updateData) { try { const response = await fetch(`${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks/${webhookId}`, { method: 'PUT', headers: { 'X-Figma-Token': FIGMA_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify(updateData), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Figma API error: ${response.status} ${response.statusText} - ${errorData.message || ''}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to update webhook: ${error.message}`); } } /** * Delete a webhook * @param {string} webhookId - The webhook ID * @returns {Promise} The deletion response */ async function deleteWebhook(webhookId) { try { const response = await fetch(`${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks/${webhookId}`, { method: 'DELETE', headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } return { success: true, message: 'Webhook deleted successfully' }; } catch (error) { throw new Error(`Failed to delete webhook: ${error.message}`); } } /** * Get webhook requests (for debugging) * @param {string} webhookId - The webhook ID * @returns {Promise} The webhook requests data */ async function getWebhookRequests(webhookId) { try { const response = await fetch(`${FIGMA_API_BASE.replace('/v1', '/v2')}/webhooks/${webhookId}/requests`, { headers: { 'X-Figma-Token': FIGMA_TOKEN, }, }); if (!response.ok) { throw new Error(`Figma API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get webhook requests: ${error.message}`); } } /** * Generate a summary of comments * @param {Array} comments - Array of comment objects * @returns {Object} Summary statistics and structured data */ function summarizeComments(comments) { const totalComments = comments.length; const totalReplies = comments.reduce((sum, c) => sum + c.replies.length, 0); const resolvedComments = comments.filter(c => c.resolvedAt).length; const unresolvedComments = totalComments - resolvedComments; // Group by author const authorStats = {}; comments.forEach(comment => { const handle = comment.author.handle; if (!authorStats[handle]) { authorStats[handle] = { comments: 0, replies: 0 }; } authorStats[handle].comments++; comment.replies.forEach(reply => { const replyHandle = reply.author.handle; if (!authorStats[replyHandle]) { authorStats[replyHandle] = { comments: 0, replies: 0 }; } authorStats[replyHandle].replies++; }); }); // Group by location (node) const locationStats = {}; comments.forEach(comment => { if (comment.location && comment.location.node_id) { const nodeId = comment.location.node_id; locationStats[nodeId] = (locationStats[nodeId] || 0) + 1; } }); return { summary: { total_comments: totalComments, total_replies: totalReplies, resolved: resolvedComments, unresolved: unresolvedComments, }, by_author: authorStats, by_location: locationStats, comments: comments.map(c => ({ id: c.id, preview: c.message.substring(0, 100) + (c.message.length > 100 ? '...' : ''), author: c.author.handle, timestamp: c.timestamp, replies_count: c.replies.length, resolved: !!c.resolvedAt, location: c.location, })), }; } // Create MCP server const server = new Server( { name: 'mcp-figma-comment-summary', version: '2.0.0', }, { capabilities: { tools: {}, }, } ); // Define available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Comments endpoints { name: 'get_figma_comments', description: 'Retrieve all comments from a Figma file, including comment text, author info, timestamps, location, and reply threads', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, { name: 'summarize_figma_comments', description: 'Get a summary and analysis of all comments in a Figma file, including statistics by author, location, and resolution status', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, { name: 'post_figma_comment', description: 'Create a new comment on a Figma file. Optionally specify a location (node_id and/or coordinates) where the comment should be placed', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, message: { type: 'string', description: 'The text content of the comment. You can mention users using @username format (e.g., "Hey @john, please review this")', }, node_id: { type: 'string', description: 'Optional: The ID of the node/element to attach the comment to', }, x: { type: 'number', description: 'Optional: X coordinate for the comment position', }, y: { type: 'number', description: 'Optional: Y coordinate for the comment position', }, }, required: ['file_id', 'message'], }, }, { name: 'reply_to_figma_comment', description: 'Reply to an existing comment in a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, comment_id: { type: 'string', description: 'The ID of the comment to reply to', }, message: { type: 'string', description: 'The text content of the reply. You can mention users using @username format (e.g., "@maria I agree with your point")', }, }, required: ['file_id', 'comment_id', 'message'], }, }, { name: 'delete_figma_comment', description: 'Delete a comment from a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, comment_id: { type: 'string', description: 'The ID of the comment to delete', }, }, required: ['file_id', 'comment_id'], }, }, { name: 'get_comment_reactions', description: 'Get all reactions for a specific comment', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, comment_id: { type: 'string', description: 'The ID of the comment', }, }, required: ['file_id', 'comment_id'], }, }, { name: 'post_comment_reaction', description: 'Add a reaction (emoji) to a comment', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, comment_id: { type: 'string', description: 'The ID of the comment', }, emoji: { type: 'string', description: 'The emoji reaction (e.g., "👍", "❤️", "😊")', }, }, required: ['file_id', 'comment_id', 'emoji'], }, }, { name: 'delete_comment_reaction', description: 'Remove a reaction (emoji) from a comment', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, comment_id: { type: 'string', description: 'The ID of the comment', }, emoji: { type: 'string', description: 'The emoji reaction to remove (e.g., "👍", "❤️", "😊")', }, }, required: ['file_id', 'comment_id', 'emoji'], }, }, { name: 'get_comment_users', description: 'Get a list of all users who have commented on a Figma file. Useful to see who can be mentioned in comments using @username', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, // Files endpoints { name: 'get_file', description: 'Get a Figma file as JSON with complete document structure, components, and metadata. Supports filtering by version, specific nodes, depth, and geometry', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, version: { type: 'string', description: 'Optional: A specific version ID to retrieve', }, ids: { type: 'string', description: 'Optional: Comma-separated list of node IDs to retrieve (e.g., "1:5,1:6")', }, depth: { type: 'number', description: 'Optional: Depth to traverse (default: traverse all)', }, geometry: { type: 'string', description: 'Optional: Set to "paths" to export vector data', }, }, required: ['file_id'], }, }, { name: 'get_file_nodes', description: 'Get specific nodes from a Figma file by their IDs', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, ids: { type: 'string', description: 'Comma-separated list of node IDs to retrieve (e.g., "1:5,1:6,1:7")', }, }, required: ['file_id', 'ids'], }, }, { name: 'render_images', description: 'Render images from file nodes in various formats (JPG, PNG, SVG, PDF) with customizable scale and options', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, ids: { type: 'string', description: 'Comma-separated list of node IDs to render as images (e.g., "1:5,1:6")', }, scale: { type: 'number', description: 'Optional: Image scale (between 0.01 and 4)', }, format: { type: 'string', description: 'Optional: Image format (jpg, png, svg, pdf). Default: png', }, svg_include_id: { type: 'boolean', description: 'Optional: Whether to include id attributes in SVG', }, svg_simplify_stroke: { type: 'boolean', description: 'Optional: Whether to simplify strokes in SVG', }, use_absolute_bounds: { type: 'boolean', description: 'Optional: Use absolute bounding box', }, }, required: ['file_id', 'ids'], }, }, { name: 'get_image_fills', description: 'Get download links for all images used in image fills throughout the document', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, // Users endpoint { name: 'get_current_user', description: 'Get information about the current user (the owner of the access token)', inputSchema: { type: 'object', properties: {}, }, }, // Version History endpoint { name: 'get_version_history', description: 'Get the version history of a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, // Projects & Teams endpoints { name: 'get_team_projects', description: 'Get all projects for a specific team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'The team ID', }, }, required: ['team_id'], }, }, { name: 'get_project_files', description: 'Get all files within a specific project', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'The project ID', }, }, required: ['project_id'], }, }, // Variables endpoints { name: 'get_local_variables', description: 'Get local variables defined in a Figma file (requires Enterprise plan)', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, { name: 'get_published_variables', description: 'Get published variables from a Figma file (requires Enterprise plan)', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, }, required: ['file_id'], }, }, { name: 'create_variables', description: 'Create or update variables in a Figma file (requires Enterprise plan with file_variables:write scope)', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, variables_data: { type: 'object', description: 'The variables data to create/update (refer to Figma API docs for structure)', }, }, required: ['file_id', 'variables_data'], }, }, // Dev Resources endpoints { name: 'get_dev_resources', description: 'Get dev resources (developer-contributed URLs) attached to nodes in a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, node_id: { type: 'string', description: 'Optional: Filter resources by specific node ID', }, }, required: ['file_id'], }, }, { name: 'create_dev_resource', description: 'Create a dev resource (developer-contributed URL) attached to a node in a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, node_id: { type: 'string', description: 'The node ID to attach the resource to', }, url: { type: 'string', description: 'The URL of the dev resource', }, name: { type: 'string', description: 'The name/title of the dev resource', }, }, required: ['file_id', 'node_id', 'url', 'name'], }, }, { name: 'delete_dev_resource', description: 'Delete a dev resource from a Figma file', inputSchema: { type: 'object', properties: { file_id: { type: 'string', description: 'The Figma file ID (from the URL: figma.com/file/FILE_ID/...)', }, dev_resource_id: { type: 'string', description: 'The ID of the dev resource to delete', }, }, required: ['file_id', 'dev_resource_id'], }, }, // Webhooks V2 endpoints { name: 'create_webhook', description: 'Create a new webhook for Figma events (requires webhooks:write scope)', inputSchema: { type: 'object', properties: { event_type: { type: 'string', description: 'The event type (e.g., "FILE_UPDATE", "FILE_VERSION_UPDATE", "LIBRARY_PUBLISH")', }, team_id: { type: 'string', description: 'The team ID', }, endpoint: { type: 'string', description: 'The URL endpoint to receive webhook events', }, passcode: { type: 'string', description: 'A passcode for webhook verification', }, description: { type: 'string', description: 'Optional: Description of the webhook', }, }, required: ['event_type', 'team_id', 'endpoint', 'passcode'], }, }, { name: 'get_webhook', description: 'Get details of a specific webhook by ID', inputSchema: { type: 'object', properties: { webhook_id: { type: 'string', description: 'The webhook ID', }, }, required: ['webhook_id'], }, }, { name: 'get_webhooks', description: 'Get all webhooks, optionally filtered by team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'Optional: Filter webhooks by team ID', }, }, }, }, { name: 'update_webhook', description: 'Update an existing webhook', inputSchema: { type: 'object', properties: { webhook_id: { type: 'string', description: 'The webhook ID to update', }, status: { type: 'string', description: 'Optional: Webhook status (ACTIVE or PAUSED)', }, description: { type: 'string', description: 'Optional: New description', }, endpoint: { type: 'string', description: 'Optional: New endpoint URL', }, passcode: { type: 'string', description: 'Optional: New passcode', }, }, required: ['webhook_id'], }, }, { name: 'delete_webhook', description: 'Delete a webhook', inputSchema: { type: 'object', properties: { webhook_id: { type: 'string', description: 'The webhook ID to delete', }, }, required: ['webhook_id'], }, }, { name: 'get_webhook_requests', description: 'Get webhook requests for debugging (shows requests from the past 7 days)', inputSchema: { type: 'object', properties: { webhook_id: { type: 'string', description: 'The webhook ID', }, }, required: ['webhook_id'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === 'get_figma_comments') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const comments = await getFigmaComments(file_id); return { content: [ { type: 'text', text: JSON.stringify(comments, null, 2), }, ], }; } if (name === 'summarize_figma_comments') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const comments = await getFigmaComments(file_id); const summary = summarizeComments(comments); return { content: [ { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } if (name === 'post_figma_comment') { const { file_id, message, node_id, x, y } = args; if (!file_id) { throw new Error('file_id is required'); } if (!message) { throw new Error('message is required'); } // Build location object if any location parameters provided const location = {}; if (node_id) location.node_id = node_id; if (x !== undefined) location.x = x; if (y !== undefined) location.y = y; const result = await postFigmaComment( file_id, message, Object.keys(location).length > 0 ? location : null ); return { content: [ { type: 'text', text: `Comment posted successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'reply_to_figma_comment') { const { file_id, comment_id, message } = args; if (!file_id) { throw new Error('file_id is required'); } if (!comment_id) { throw new Error('comment_id is required'); } if (!message) { throw new Error('message is required'); } const result = await replyToFigmaComment(file_id, comment_id, message); return { content: [ { type: 'text', text: `Reply posted successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'get_comment_users') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const { users } = await getCommentUsers(file_id); return { content: [ { type: 'text', text: `Found ${users.length} user(s) who have commented:\n\n${JSON.stringify(users, null, 2)}\n\nYou can mention these users in comments using @${users.map(u => u.handle).join(', @')}`, }, ], }; } // Comment deletion and reactions if (name === 'delete_figma_comment') { const { file_id, comment_id } = args; if (!file_id) { throw new Error('file_id is required'); } if (!comment_id) { throw new Error('comment_id is required'); } const result = await deleteFigmaComment(file_id, comment_id); return { content: [ { type: 'text', text: `${result.message}\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'get_comment_reactions') { const { file_id, comment_id } = args; if (!file_id) { throw new Error('file_id is required'); } if (!comment_id) { throw new Error('comment_id is required'); } const result = await getCommentReactions(file_id, comment_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'post_comment_reaction') { const { file_id, comment_id, emoji } = args; if (!file_id) { throw new Error('file_id is required'); } if (!comment_id) { throw new Error('comment_id is required'); } if (!emoji) { throw new Error('emoji is required'); } const result = await postCommentReaction(file_id, comment_id, emoji); return { content: [ { type: 'text', text: `Reaction posted successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'delete_comment_reaction') { const { file_id, comment_id, emoji } = args; if (!file_id) { throw new Error('file_id is required'); } if (!comment_id) { throw new Error('comment_id is required'); } if (!emoji) { throw new Error('emoji is required'); } const result = await deleteCommentReaction(file_id, comment_id, emoji); return { content: [ { type: 'text', text: `${result.message}\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } // Files endpoints if (name === 'get_file') { const { file_id, version, ids, depth, geometry } = args; if (!file_id) { throw new Error('file_id is required'); } const options = {}; if (version) options.version = version; if (ids) options.ids = ids; if (depth) options.depth = depth; if (geometry) options.geometry = geometry; const result = await getFile(file_id, options); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_file_nodes') { const { file_id, ids } = args; if (!file_id) { throw new Error('file_id is required'); } if (!ids) { throw new Error('ids is required'); } const result = await getFileNodes(file_id, ids); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'render_images') { const { file_id, ids, scale, format, svg_include_id, svg_simplify_stroke, use_absolute_bounds } = args; if (!file_id) { throw new Error('file_id is required'); } if (!ids) { throw new Error('ids is required'); } const options = { ids }; if (scale) options.scale = scale; if (format) options.format = format; if (svg_include_id !== undefined) options.svg_include_id = svg_include_id; if (svg_simplify_stroke !== undefined) options.svg_simplify_stroke = svg_simplify_stroke; if (use_absolute_bounds !== undefined) options.use_absolute_bounds = use_absolute_bounds; const result = await renderImages(file_id, options); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_image_fills') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const result = await getImageFills(file_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } // Users endpoint if (name === 'get_current_user') { const result = await getCurrentUser(); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } // Version History endpoint if (name === 'get_version_history') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const result = await getVersionHistory(file_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } // Projects & Teams endpoints if (name === 'get_team_projects') { const { team_id } = args; if (!team_id) { throw new Error('team_id is required'); } const result = await getTeamProjects(team_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_project_files') { const { project_id } = args; if (!project_id) { throw new Error('project_id is required'); } const result = await getProjectFiles(project_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } // Variables endpoints if (name === 'get_local_variables') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const result = await getLocalVariables(file_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_published_variables') { const { file_id } = args; if (!file_id) { throw new Error('file_id is required'); } const result = await getPublishedVariables(file_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'create_variables') { const { file_id, variables_data } = args; if (!file_id) { throw new Error('file_id is required'); } if (!variables_data) { throw new Error('variables_data is required'); } const result = await createVariables(file_id, variables_data); return { content: [ { type: 'text', text: `Variables created/updated successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } // Dev Resources endpoints if (name === 'get_dev_resources') { const { file_id, node_id } = args; if (!file_id) { throw new Error('file_id is required'); } const result = await getDevResources(file_id, node_id || null); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'create_dev_resource') { const { file_id, node_id, url, name: resourceName } = args; if (!file_id) { throw new Error('file_id is required'); } if (!node_id) { throw new Error('node_id is required'); } if (!url) { throw new Error('url is required'); } if (!resourceName) { throw new Error('name is required'); } const devResourceData = { node_id, url, name: resourceName, }; const result = await createDevResource(file_id, devResourceData); return { content: [ { type: 'text', text: `Dev resource created successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'delete_dev_resource') { const { file_id, dev_resource_id } = args; if (!file_id) { throw new Error('file_id is required'); } if (!dev_resource_id) { throw new Error('dev_resource_id is required'); } const result = await deleteDevResource(file_id, dev_resource_id); return { content: [ { type: 'text', text: `${result.message}\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } // Webhooks V2 endpoints if (name === 'create_webhook') { const { event_type, team_id, endpoint, passcode, description } = args; if (!event_type) { throw new Error('event_type is required'); } if (!team_id) { throw new Error('team_id is required'); } if (!endpoint) { throw new Error('endpoint is required'); } if (!passcode) { throw new Error('passcode is required'); } const webhookData = { event_type, team_id, endpoint, passcode, }; if (description) { webhookData.description = description; } const result = await createWebhook(webhookData); return { content: [ { type: 'text', text: `Webhook created successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'get_webhook') { const { webhook_id } = args; if (!webhook_id) { throw new Error('webhook_id is required'); } const result = await getWebhook(webhook_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_webhooks') { const { team_id } = args; const result = await getWebhooks(team_id || null); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'update_webhook') { const { webhook_id, status, description, endpoint, passcode } = args; if (!webhook_id) { throw new Error('webhook_id is required'); } const updateData = {}; if (status) updateData.status = status; if (description) updateData.description = description; if (endpoint) updateData.endpoint = endpoint; if (passcode) updateData.passcode = passcode; const result = await updateWebhook(webhook_id, updateData); return { content: [ { type: 'text', text: `Webhook updated successfully!\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'delete_webhook') { const { webhook_id } = args; if (!webhook_id) { throw new Error('webhook_id is required'); } const result = await deleteWebhook(webhook_id); return { content: [ { type: 'text', text: `${result.message}\n\n${JSON.stringify(result, null, 2)}`, }, ], }; } if (name === 'get_webhook_requests') { const { webhook_id } = args; if (!webhook_id) { throw new Error('webhook_id is required'); } const result = await getWebhookRequests(webhook_id); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Figma Comment Summary MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });