mcp-figma

  • build
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { AuthManager } from './utils/auth.js'; import { FigmaApi } from './utils/api.js'; import { FilesHandler } from './handlers/files.js'; import { ProjectsHandler } from './handlers/projects.js'; class FigmaServer { validateArgs(args, requiredFields) { if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Arguments are required'); } for (const field of requiredFields) { if (!(field in args)) { throw new McpError(ErrorCode.InvalidParams, `Missing required field: ${field}`); } } return args; } constructor() { this.server = new Server({ name: 'mcp-figma', version: '0.1.0', capabilities: { tools: {} } }); this.authManager = new AuthManager(); this.api = new FigmaApi(this.authManager); this.filesHandler = new FilesHandler(this.api); this.projectsHandler = new ProjectsHandler(this.api); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'set_api_key', description: 'Set your Figma API personal access token (will be saved to ~/.mcp-figma/config.json)', inputSchema: { type: 'object', properties: { api_key: { type: 'string', description: 'Your Figma API personal access token' } }, required: ['api_key'] }, }, { name: 'check_api_key', description: 'Check if a Figma API key is already configured', inputSchema: { type: 'object', properties: {}, required: [] }, }, { name: 'get_file', description: 'Get a Figma file by key', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get' }, version: { type: 'string', description: 'Optional. A specific version ID to get' }, depth: { type: 'number', description: 'Optional. Depth of nodes to return (1-4)' }, branch_data: { type: 'boolean', description: 'Optional. Include branch data if true' } }, required: ['fileKey'] }, }, { name: 'get_file_nodes', description: 'Get specific nodes from a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get nodes from' }, node_ids: { type: 'array', items: { type: 'string' }, description: 'Array of node IDs to get' }, depth: { type: 'number', description: 'Optional. Depth of nodes to return (1-4)' }, version: { type: 'string', description: 'Optional. A specific version ID to get' } }, required: ['fileKey', 'node_ids'] }, }, { name: 'get_image', description: 'Get images for nodes in a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get images from' }, ids: { type: 'array', items: { type: 'string' }, description: 'Array of node IDs to render' }, scale: { type: 'number', description: 'Optional. Scale factor to render at (0.01-4)' }, format: { type: 'string', enum: ['jpg', 'png', 'svg', 'pdf'], description: 'Optional. Image format' }, svg_include_id: { type: 'boolean', description: 'Optional. Include IDs in SVG output' }, svg_simplify_stroke: { type: 'boolean', description: 'Optional. Simplify strokes in SVG output' }, use_absolute_bounds: { type: 'boolean', description: 'Optional. Use absolute bounds' } }, required: ['fileKey', 'ids'] }, }, { name: 'get_image_fills', description: 'Get URLs for images used in a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get image fills from' } }, required: ['fileKey'] }, }, { name: 'get_comments', description: 'Get comments on a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get comments from' } }, required: ['fileKey'] }, }, { name: 'post_comment', description: 'Post a comment on a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to comment on' }, message: { type: 'string', description: 'Comment message text' }, client_meta: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' }, node_id: { type: 'string' }, node_offset: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } } }, description: 'Optional. Position of the comment' }, comment_id: { type: 'string', description: 'Optional. ID of comment to reply to' } }, required: ['fileKey', 'message'] }, }, { name: 'delete_comment', description: 'Delete a comment from a Figma file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to delete a comment from' }, comment_id: { type: 'string', description: 'ID of the comment to delete' } }, required: ['fileKey', 'comment_id'] }, }, { name: 'get_team_projects', description: 'Get projects for a team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'The team ID' }, page_size: { type: 'number', description: 'Optional. Number of items per page' }, cursor: { type: 'string', description: 'Optional. Cursor for pagination' } }, required: ['team_id'] }, }, { name: 'get_project_files', description: 'Get files for a project', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'The project ID' }, page_size: { type: 'number', description: 'Optional. Number of items per page' }, cursor: { type: 'string', description: 'Optional. Cursor for pagination' }, branch_data: { type: 'boolean', description: 'Optional. Include branch data if true' } }, required: ['project_id'] }, }, { name: 'get_team_components', description: 'Get components for a team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'The team ID' }, page_size: { type: 'number', description: 'Optional. Number of items per page' }, cursor: { type: 'string', description: 'Optional. Cursor for pagination' } }, required: ['team_id'] }, }, { name: 'get_file_components', description: 'Get components from a file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get components from' } }, required: ['fileKey'] }, }, { name: 'get_component', description: 'Get a component by key', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'The component key' } }, required: ['key'] }, }, { name: 'get_team_component_sets', description: 'Get component sets for a team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'The team ID' }, page_size: { type: 'number', description: 'Optional. Number of items per page' }, cursor: { type: 'string', description: 'Optional. Cursor for pagination' } }, required: ['team_id'] }, }, { name: 'get_team_styles', description: 'Get styles for a team', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'The team ID' }, page_size: { type: 'number', description: 'Optional. Number of items per page' }, cursor: { type: 'string', description: 'Optional. Cursor for pagination' } }, required: ['team_id'] }, }, { name: 'get_file_styles', description: 'Get styles from a file', inputSchema: { type: 'object', properties: { fileKey: { type: 'string', description: 'The key of the file to get styles from' } }, required: ['fileKey'] }, }, { name: 'get_style', description: 'Get a style by key', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'The style key' } }, required: ['key'] }, } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case 'set_api_key': { const args = this.validateArgs(request.params.arguments, ['api_key']); this.authManager.setApiKey(args.api_key); return { content: [{ type: 'text', text: 'API key set successfully and saved to config file' }], }; } case 'check_api_key': { try { const token = await this.authManager.getAccessToken(); // 只显示部分API key作为安全措施 const maskedToken = token.substring(0, 5) + '...' + token.substring(token.length - 5); return { content: [{ type: 'text', text: `API key is configured (${maskedToken})` }], }; } catch (error) { return { content: [{ type: 'text', text: 'No API key is configured. Please use set_api_key to configure one.' }], }; } } case 'get_file': { const args = this.validateArgs(request.params.arguments, ['fileKey']); const result = await this.filesHandler.getFile(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_file_nodes': { const args = this.validateArgs(request.params.arguments, ['fileKey', 'node_ids']); const result = await this.filesHandler.getFileNodes(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_image': { const args = this.validateArgs(request.params.arguments, ['fileKey', 'ids']); const result = await this.filesHandler.getImage(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_image_fills': { const args = this.validateArgs(request.params.arguments, ['fileKey']); const result = await this.filesHandler.getImageFills(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_comments': { const args = this.validateArgs(request.params.arguments, ['fileKey']); const result = await this.filesHandler.getComments(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'post_comment': { const args = this.validateArgs(request.params.arguments, ['fileKey', 'message']); const result = await this.filesHandler.postComment(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'delete_comment': { const args = this.validateArgs(request.params.arguments, ['fileKey', 'comment_id']); const result = await this.filesHandler.deleteComment(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_team_projects': { const args = this.validateArgs(request.params.arguments, ['team_id']); const result = await this.projectsHandler.getTeamProjects(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_project_files': { const args = this.validateArgs(request.params.arguments, ['project_id']); const result = await this.projectsHandler.getProjectFiles(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_team_components': { const args = this.validateArgs(request.params.arguments, ['team_id']); const result = await this.projectsHandler.getTeamComponents(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_file_components': { const args = this.validateArgs(request.params.arguments, ['fileKey']); const result = await this.projectsHandler.getFileComponents(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_component': { const args = this.validateArgs(request.params.arguments, ['key']); const result = await this.projectsHandler.getComponent(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_team_component_sets': { const args = this.validateArgs(request.params.arguments, ['team_id']); const result = await this.projectsHandler.getTeamComponentSets(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_team_styles': { const args = this.validateArgs(request.params.arguments, ['team_id']); const result = await this.projectsHandler.getTeamStyles(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_file_styles': { const args = this.validateArgs(request.params.arguments, ['fileKey']); const result = await this.projectsHandler.getFileStyles(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_style': { const args = this.validateArgs(request.params.arguments, ['key']); const result = await this.projectsHandler.getStyle(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Unexpected error: ${error}`); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Figma MCP server running on stdio'); } } const server = new FigmaServer(); server.run().catch(console.error);