Skip to main content
Glama
index.ts17.2 kB
#!/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 { FigmaService } from './figma-service.js'; import { FigmaImageExtractor, ImageExportOptions } from './image-extractor.js'; import { FigmaStyleExtractor } from './style-extractor.js'; import { FigmaElementExtractor } from './element-extractor.js'; class FigmaMCPServer { private server: Server; private figmaService: FigmaService; private imageExtractor: FigmaImageExtractor; private styleExtractor: FigmaStyleExtractor; private elementExtractor: FigmaElementExtractor; constructor(accessToken: string) { this.server = new Server( { name: 'figma-mcp-full-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.figmaService = new FigmaService(accessToken); this.imageExtractor = new FigmaImageExtractor(this.figmaService); this.styleExtractor = new FigmaStyleExtractor(this.figmaService); this.elementExtractor = new FigmaElementExtractor(this.figmaService); this.setupHandlers(); } private setupHandlers() { // 注册工具列表处理器 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_figma_image', description: '根据Figma URL获取节点的图片', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL,必须包含node-id参数', }, format: { type: 'string', enum: ['png', 'jpg', 'svg', 'pdf'], description: '图片格式,默认为png', default: 'png', }, scale: { type: 'number', description: '图片缩放比例,默认为1', default: 1, minimum: 0.01, maximum: 4, }, }, required: ['url'], }, }, { name: 'get_figma_styles', description: '根据Figma URL获取节点的样式数据', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL,可以包含node-id参数指定特定节点', }, generateCSS: { type: 'boolean', description: '是否生成CSS代码', default: false, }, }, required: ['url'], }, }, { name: 'export_multiple_images', description: '批量导出多个节点的图片', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'Figma文件ID', }, nodeIds: { type: 'array', items: { type: 'string' }, description: '节点ID列表', }, format: { type: 'string', enum: ['png', 'jpg', 'svg', 'pdf'], description: '图片格式,默认为png', default: 'png', }, scale: { type: 'number', description: '图片缩放比例,默认为1', default: 1, minimum: 0.01, maximum: 4, }, }, required: ['fileId', 'nodeIds'], }, }, { name: 'get_file_info', description: '获取Figma文件的基本信息', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL', }, }, required: ['url'], }, }, { name: 'get_node_images', description: '获取节点中的所有图片资源', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL,必须包含node-id参数', }, }, required: ['url'], }, }, { name: 'get_node_svg', description: '获取节点的SVG数据', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL,必须包含node-id参数', }, }, required: ['url'], }, }, { name: 'extract_node_elements', description: '提取节点中的所有设计元素(图片、矢量、组件)', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Figma文件URL,必须包含node-id参数', }, includeDetails: { type: 'boolean', description: '是否包含详细信息,默认为false', default: false, }, }, required: ['url'], }, }, ], }; }); // 注册工具调用处理器 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_figma_image': return await this.handleGetImage(args); case 'get_figma_styles': return await this.handleGetStyles(args); case 'export_multiple_images': return await this.handleExportMultipleImages(args); case 'get_file_info': return await this.handleGetFileInfo(args); case 'get_node_images': return await this.handleGetNodeImages(args); case 'get_node_svg': return await this.handleGetNodeSVG(args); case 'extract_node_elements': return await this.handleExtractNodeElements(args); default: throw new Error(`未知的工具: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `错误: ${error instanceof Error ? error.message : '未知错误'}`, }, ], }; } }); } private async handleGetImage(args: any) { const { url, format = 'png', scale = 1 } = args; try { console.error(`开始处理图片请求: ${url}`); const options: ImageExportOptions = { format, scale }; const results = await this.imageExtractor.getImageFromUrl(url, options); if (results.length === 0) { throw new Error('未找到可导出的图片'); } const result = results[0]; console.error(`图片导出成功: ${result.nodeName} (${result.nodeId})`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { imageUrl: result.url, nodeId: result.nodeId, nodeName: result.nodeName, format: result.format, scale: result.scale, }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`图片获取失败: ${errorMessage}`); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: errorMessage, troubleshooting: { commonIssues: [ '检查Figma URL是否包含node-id参数', '确认Figma token是否有效', '验证对该文件是否有访问权限', '检查节点是否存在且可见' ], urlFormat: 'https://www.figma.com/design/{fileId}/{name}?node-id={nodeId}' } }, null, 2), }, ], }; } } private async handleGetStyles(args: any) { const { url, generateCSS = false } = args; const styleData = await this.styleExtractor.getStylesFromUrl(url); let cssCode = ''; if (generateCSS && styleData.styles.length > 0) { const cssRules = styleData.styles.map(style => { const selector = `.${style.nodeName.toLowerCase().replace(/\\s+/g, '-')}`; const css = this.styleExtractor.generateCSS(style); return css ? `${selector} {\\n ${css}\\n}` : ''; }).filter(rule => rule); cssCode = cssRules.join('\\n\\n'); } return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { fileInfo: styleData.fileInfo, styles: styleData.styles, globalStyles: styleData.globalStyles, ...(cssCode && { generatedCSS: cssCode }), }, }, null, 2), }, ], }; } private async handleExportMultipleImages(args: any) { const { fileId, nodeIds, format = 'png', scale = 1 } = args; const options: ImageExportOptions = { format, scale }; const results = await this.imageExtractor.getMultipleImages(fileId, nodeIds, options); return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { images: results, totalCount: results.length, }, }, null, 2), }, ], }; } private async handleGetFileInfo(args: any) { const { url } = args; const urlInfo = this.figmaService.parseUrl(url); const file = await this.figmaService.getFile(urlInfo.fileId); return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { fileId: urlInfo.fileId, fileName: file.name, lastModified: file.lastModified, version: file.version, componentsCount: Object.keys(file.components || {}).length, stylesCount: Object.keys(file.styles || {}).length, pagesCount: file.document.children?.length || 0, }, }, null, 2), }, ], }; } private async handleGetNodeImages(args: any) { const { url } = args; try { console.error(`开始获取节点图片资源: ${url}`); const images = await this.elementExtractor.getNodeImages( this.figmaService.parseUrl(url).fileId, this.figmaService.parseUrl(url).nodeId! ); console.error(`成功获取 ${images.length} 个图片资源`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { images, totalCount: images.length, }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`获取节点图片失败: ${errorMessage}`); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: errorMessage, troubleshooting: { commonIssues: [ '检查Figma URL是否包含node-id参数', '确认节点中是否包含图片资源', '验证对该文件是否有访问权限', ], urlFormat: 'https://www.figma.com/design/{fileId}/{name}?node-id={nodeId}' } }, null, 2), }, ], }; } } private async handleGetNodeSVG(args: any) { const { url } = args; try { console.error(`开始获取节点SVG数据: ${url}`); const urlInfo = this.figmaService.parseUrl(url); if (!urlInfo.nodeId) { throw new Error('URL中缺少node-id参数'); } const svgData = await this.elementExtractor.getNodeAsSVG(urlInfo.fileId, urlInfo.nodeId); console.error(`成功获取SVG数据,长度: ${svgData.length} 字符`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: { svg: svgData, fileId: urlInfo.fileId, nodeId: urlInfo.nodeId, dataLength: svgData.length, }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`获取SVG数据失败: ${errorMessage}`); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: errorMessage, troubleshooting: { commonIssues: [ '检查节点是否为矢量图形或可导出为SVG', '确认Figma token权限是否充足', '验证节点ID格式是否正确', ], } }, null, 2), }, ], }; } } private async handleExtractNodeElements(args: any) { const { url, includeDetails = false } = args; try { console.error(`开始提取节点设计元素: ${url}`); const elements = await this.elementExtractor.getElementsFromUrl(url); console.error(`成功提取设计元素: ${elements.totalElements} 个`); const responseData: any = { success: true, data: { nodeId: elements.nodeId, nodeName: elements.nodeName, summary: { totalElements: elements.totalElements, images: elements.images.length, vectors: elements.vectors.length, components: elements.components.length, }, elements: includeDetails ? { images: elements.images, vectors: elements.vectors, components: elements.components, } : undefined, }, }; // 如果不包含详细信息,提供可读的摘要 if (!includeDetails) { responseData.data.textSummary = this.elementExtractor.generateElementsSummary(elements); } return { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`提取设计元素失败: ${errorMessage}`); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: errorMessage, troubleshooting: { commonIssues: [ '检查Figma URL是否包含node-id参数', '确认对该文件和节点有访问权限', '验证节点是否存在且包含设计元素', ], tip: '使用includeDetails=true获取详细的元素信息' } }, null, 2), }, ], }; } } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Figma MCP服务器已启动'); } } // 启动服务器 async function main() { const accessToken = process.argv[2] || process.env.FIGMA_TOKEN; if (!accessToken) { console.error('错误: 缺少Figma访问令牌'); console.error('使用方法: node build/index.js <FIGMA_ACCESS_TOKEN>'); console.error('或设置环境变量: FIGMA_TOKEN'); process.exit(1); } try { const server = new FigmaMCPServer(accessToken); await server.start(); } catch (error) { console.error('启动服务器失败:', error); process.exit(1); } } main().catch(console.error);

Implementation Reference

Latest Blog Posts

MCP directory API

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

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

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