本文记录了在使用 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
问题原因可能是:
- Figma API 速率限制
- 批量请求超时
- MCP 工具的实现问题
官方建议的 Workaround 是逐张下载,但实测单张下载也会卡死。
四、解决方案设计
既然官方工具有 Bug 且短期内难以修复,决定自研替代工具:
设计目标
- 绕过 MCP 工具的 Bug - 直接调用 Figma REST API
- 支持 CLI 使用 - 方便命令行调用
- 支持 MCP 集成 - 可以在 Windsurf/Cursor 中作为 MCP 工具使用
- 处理速率限制 - 实现指数退避重试机制
- 跨平台 - 发布到 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 工具层的问题:
- ✅ 支持 CLI 和 MCP 两种使用方式
- ✅ 支持 PNG/SVG 格式导出
- ✅ 支持 1-4 倍缩放
- ✅ 实现指数退避重试处理速率限制
- ✅ 发布到 npm,开箱即用
资源链接
- npm 包 : https://www.npmjs.com/package/figma-dl
- GitHub 仓库 : https://github.com/Larryzhang-S/figma-dl
使用建议
- 禁止使用
figma-developer-mcp的download_figma_images功能 - 推荐使用
figma-dlCLI 或 MCP 服务器 - 获取设计数据仍可使用
get_figma_data(正常工作)
遇到技术问题时,不要只是等待官方修复,有时候自己动手实现一个替代方案反而更高效。希望这个工具能帮助到有同样需求的开发者!