解决 Figma MCP 下载图片卡死问题:从踩坑到自研 npm 工具全记录

本文记录了在使用 Figma MCP 工具时遇到下载图片卡死问题,从问题排查、原因分析到自研替代工具的完整过程。最终开发了 figma-dl 这个开源工具,支持 CLI 和 MCP 两种使用方式。

一、问题背景

在日常开发中,我们经常需要从 Figma 设计稿中导出切图。为了提高效率,我们使用了 Windsurf(Cursor 类似产品)配合 MCP(Model Context Protocol)工具来自动化这个流程。

MCP 是一种让 AI 助手调用外部工具的协议,Figma 官方提供了 figma-developer-mcp 工具,包含两个核心功能:

  • get_figma_data - 获取设计稿数据
  • download_figma_images - 下载图片资源

二、遇到问题

在实际使用中,我们发现:

功能 状态
get_figma_data ✅ 正常工作
download_figma_images ❌ 频繁卡死

具体表现:

  • 下载单张图片时偶尔成功
  • 下载多张图片时几乎必然卡死
  • 工具显示 "This is taking a long time..." 后无限等待
  • PNG 和 SVG 格式都会出现问题

三、问题排查

3.1 检查 API Key 权限

首先怀疑是 API Key 权限问题,使用 PowerShell 直接测试:

powershell 复制代码
# 测试 API Key 是否有效
$headers = @{ "X-Figma-Token" = "your-api-key" }
Invoke-RestMethod -Uri "https://api.figma.com/v1/me" -Headers $headers
# 结果:正常返回用户信息 ✅

# 测试图片导出 API
Invoke-RestMethod -Uri "https://api.figma.com/v1/images/fileKey?ids=nodeId&format=svg" -Headers $headers
# 结果:正常返回图片 URL ✅

# 直接下载图片
Invoke-WebRequest -Uri $imageUrl -OutFile "test.svg"
# 结果:正常下载 ✅

结论:API Key 权限正常,Figma API 本身没有问题

3.2 查找官方 Issue

在 GitHub 上搜索后发现这是一个 已知问题

GitHub Issue #143: Bulk Image Download Fails

问题原因可能是:

  1. Figma API 速率限制
  2. 批量请求超时
  3. MCP 工具的实现问题

官方建议的 Workaround 是逐张下载,但实测单张下载也会卡死。

四、解决方案设计

既然官方工具有 Bug 且短期内难以修复,决定自研替代工具:

设计目标

  1. 绕过 MCP 工具的 Bug - 直接调用 Figma REST API
  2. 支持 CLI 使用 - 方便命令行调用
  3. 支持 MCP 集成 - 可以在 Windsurf/Cursor 中作为 MCP 工具使用
  4. 处理速率限制 - 实现指数退避重试机制
  5. 跨平台 - 发布到 npm,任何有 Node.js 的环境都能使用

技术栈

  • 语言: Node.js (ES Module)
  • CLI 框架: Commander.js
  • MCP SDK: @modelcontextprotocol/sdk
  • 发布平台: npm

五、工具开发

5.1 项目结构

复制代码
figma-dl/
├── package.json
├── README.md
└── src/
    ├── index.js        # 主入口
    ├── cli.js          # CLI 命令
    ├── figma-api.js    # Figma API 封装
    └── mcp-server.js   # MCP 服务器

5.2 核心代码:Figma API 封装

javascript 复制代码
// src/figma-api.js
const FIGMA_API_BASE = 'https://api.figma.com/v1';
const MAX_RETRIES = 5;
const INITIAL_DELAY = 2000;

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

export class FigmaDownloader {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  /**
   * 带指数退避重试的请求方法
   */
  async fetchWithRetry(url, options, retries = MAX_RETRIES) {
    for (let attempt = 0; attempt <= retries; attempt++) {
      const response = await fetch(url, options);
      
      if (response.status === 429) {
        if (attempt === retries) {
          throw new Error(`Rate limit exceeded after ${retries} retries`);
        }
        
        const retryAfter = response.headers.get('retry-after');
        const delay = retryAfter 
          ? parseInt(retryAfter) * 1000 
          : INITIAL_DELAY * Math.pow(2, attempt);
        
        console.log(`[WAIT] Rate limited. Waiting ${delay/1000}s before retry...`);
        await sleep(delay);
        continue;
      }
      
      return response;
    }
  }

  async getImageUrls(fileKey, nodeIds, format = 'png', scale = 2) {
    const formattedIds = nodeIds.map(id => id.replace(/-/g, ':'));
    const params = new URLSearchParams({
      ids: formattedIds.join(','),
      format: format,
    });
    
    if (format === 'png') {
      params.append('scale', scale.toString());
    }

    const url = `${FIGMA_API_BASE}/images/${fileKey}?${params}`;
    
    const response = await this.fetchWithRetry(url, {
      headers: { 'X-Figma-Token': this.apiKey },
    });

    const data = await response.json();
    return data.images;
  }

  async downloadImages(fileKey, nodeIds, outputDir, options = {}) {
    const { format = 'png', scale = 2 } = options;
    
    // 确保输出目录存在
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    const imageUrls = await this.getImageUrls(fileKey, nodeIds, format, scale);
    const results = [];
    
    for (const [nodeId, imageUrl] of Object.entries(imageUrls)) {
      if (!imageUrl) {
        results.push({ nodeId, success: false, error: 'Cannot export' });
        continue;
      }

      const safeNodeId = nodeId.replace(/:/g, '_');
      const fileName = `${safeNodeId}.${format}`;
      const filePath = path.join(outputDir, fileName);

      const response = await fetch(imageUrl);
      const fileStream = fs.createWriteStream(filePath);
      await pipeline(Readable.fromWeb(response.body), fileStream);
      
      results.push({ nodeId, success: true, fileName });
    }

    return results;
  }
}

5.3 CLI 实现

javascript 复制代码
// src/cli.js
#!/usr/bin/env node

import { Command } from 'commander';
import { FigmaDownloader } from './figma-api.js';

const program = new Command();

program
  .name('figma-dl')
  .description('Figma image downloader - reliable alternative to buggy MCP tools')
  .version('1.0.4')
  .requiredOption('-f, --file-key <key>', 'Figma file key (from URL)')
  .requiredOption('-n, --node-ids <ids>', 'Node IDs, comma separated')
  .requiredOption('-o, --output <dir>', 'Output directory')
  .option('--format <format>', 'Image format: png or svg', 'png')
  .option('--scale <scale>', 'PNG scale: 1-4', '2')
  .option('--api-key <key>', 'Figma API key (or set FIGMA_API_KEY env var)')
  .action(async (options) => {
    const apiKey = options.apiKey || process.env.FIGMA_API_KEY;
    const downloader = new FigmaDownloader(apiKey);
    const nodeIds = options.nodeIds.split(',').map(id => id.trim());
    
    await downloader.downloadImages(
      options.fileKey,
      nodeIds,
      options.output,
      { format: options.format, scale: parseInt(options.scale, 10) }
    );
  });

program.parse();

5.4 MCP 服务器实现

javascript 复制代码
// src/mcp-server.js
#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { FigmaDownloader } from './figma-api.js';

const server = new Server(
  { name: 'figma-dl', version: '1.0.4' },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'download_images',
    description: 'Download images from Figma by node IDs',
    inputSchema: {
      type: 'object',
      properties: {
        fileKey: { type: 'string' },
        nodeIds: { type: 'array', items: { type: 'string' } },
        outputDir: { type: 'string' },
        format: { type: 'string', enum: ['png', 'svg'], default: 'png' },
        scale: { type: 'number', minimum: 1, maximum: 4, default: 2 },
      },
      required: ['fileKey', 'nodeIds', 'outputDir'],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { fileKey, nodeIds, outputDir, format, scale } = request.params.arguments;
  const downloader = new FigmaDownloader(process.env.FIGMA_API_KEY);
  const results = await downloader.downloadImages(fileKey, nodeIds, outputDir, { format, scale });
  return { content: [{ type: 'text', text: JSON.stringify(results) }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);

5.5 package.json 配置

json 复制代码
{
  "name": "figma-dl",
  "version": "1.0.4",
  "description": "Figma image downloader - CLI & MCP server",
  "type": "module",
  "bin": {
    "figma-dl": "./src/cli.js",
    "figma-dl-mcp": "./src/mcp-server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "commander": "^12.0.0"
  }
}

六、使用方法

6.1 CLI 方式

bash 复制代码
# 设置环境变量
export FIGMA_API_KEY=your-api-key

# 下载 PNG(默认 2x 缩放)
npx figma-dl -f "fileKey" -n "1:2,3:4" -o ./images

# 下载 SVG
npx figma-dl -f "fileKey" -n "1:2" -o ./images --format svg

# 指定缩放比例(1-4)
npx figma-dl -f "fileKey" -n "1:2" -o ./images --scale 3

6.2 MCP 方式

mcp_config.json 中配置:

json 复制代码
{
  "mcpServers": {
    "figma-dl": {
      "command": "npx",
      "args": ["-y", "-p", "figma-dl", "figma-dl-mcp"],
      "env": {
        "FIGMA_API_KEY": "your-api-key"
      }
    }
  }
}

配置后,AI 助手就可以调用 download_images 工具下载 Figma 图片。

6.3 获取 File Key 和 Node ID

从 Figma URL 中提取:

复制代码
https://www.figma.com/design/abc123def/MyDesign?node-id=1:2
                            ↑                         ↑
                        File Key                   Node ID

七、关键优化:指数退避重试

Figma API 有速率限制,频繁请求会返回 429 错误。我们实现了指数退避重试机制:

javascript 复制代码
async fetchWithRetry(url, options, retries = 5) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      // 使用 retry-after 头或指数退避计算等待时间
      const delay = INITIAL_DELAY * Math.pow(2, attempt);
      console.log(`[WAIT] Rate limited. Waiting ${delay/1000}s...`);
      await sleep(delay);
      continue;
    }
    
    return response;
  }
  throw new Error('Rate limit exceeded after max retries');
}

重试策略:

  • 初始等待:2 秒
  • 指数增长:2s → 4s → 8s → 16s → 32s
  • 最大重试:5 次

八、总结

问题根因

figma-developer-mcp 官方工具的 download_figma_images 功能存在 Bug,在处理图片下载时会卡死。这是一个已知问题,但官方尚未修复。

解决方案

自研 figma-dl 工具,直接调用 Figma REST API,绕过 MCP 工具层的问题:

  1. ✅ 支持 CLI 和 MCP 两种使用方式
  2. ✅ 支持 PNG/SVG 格式导出
  3. ✅ 支持 1-4 倍缩放
  4. ✅ 实现指数退避重试处理速率限制
  5. ✅ 发布到 npm,开箱即用

资源链接

使用建议

  1. 禁止使用 figma-developer-mcpdownload_figma_images 功能
  2. 推荐使用 figma-dl CLI 或 MCP 服务器
  3. 获取设计数据仍可使用 get_figma_data(正常工作)

遇到技术问题时,不要只是等待官方修复,有时候自己动手实现一个替代方案反而更高效。希望这个工具能帮助到有同样需求的开发者!

相关推荐
前端缘梦41 分钟前
JavaScript核心机制:执行栈、作用域与this指向完全解析
前端·javascript·面试
JarvanMo43 分钟前
Flutter 3.38 动画新特性:动画引擎有什么新变化?
前端
ByteCraze1 小时前
CDN 引入 与 npm 引入的区别
前端·npm·node.js
Youyzq1 小时前
react 元素触底hooks封装
前端·javascript·react.js
crary,记忆1 小时前
PNPM 和 NPM
前端·学习·npm·node.js
爱吃无爪鱼1 小时前
04-npm 与 Bun 快速入门实战指南
前端·vue.js·react.js·npm·sass
灵犀坠1 小时前
前端面试&项目实战核心知识点总结(Vue3+Pinia+UniApp+Axios)
前端·javascript·css·面试·职场和发展·uni-app·html
GISer_Jing1 小时前
SSE Conf大会分享——大模型驱动的智能 可视分析与故事叙述
前端·人工智能·信息可视化
Lovely Ruby1 小时前
前端er Go-Frame 的学习笔记:实现 to-do 功能(一)
前端·学习·golang