这类工具最值得先看的不是功能列表,而是能不能在普通开发环境里稳定跑起来,以及它到底解决了设计稿到代码转换中的哪个具体痛点。Figma MCP 配合 Codex 这个组合,核心价值在于它能让你用代码的方式,直接、批量地读取 Figma 文件里的设计节点,并输出结构化的 JSON 数据。这比手动在 Figma 界面上一个个复制属性,或者依赖一些不稳定的截图识别工具要可靠得多。
它适合两类人:一是前端开发,尤其是需要频繁从设计稿中提取颜色、尺寸、间距、文本样式等 Token 的开发;二是希望将设计系统资产(如组件库)自动化同步到代码库的团队。最关键的能力是“一次拿全”,这意味着你可以通过一个脚本,把整个画板、页面甚至文件的设计数据,按你需要的层级和格式,完整地抓取下来,为后续的代码生成、样式同步或设计走查提供数据基础。
下面我会按实际落地顺序拆一遍,从环境准备、权限获取、脚本编写到数据解析和常见坑点。
1. 先搞清楚 MCP 和 Codex 在这里分别扮演什么角色
很多人看到“Figma MCP”和“Codex”这两个词容易混淆,以为是一个东西。其实它们是协作关系,分工明确。
1.1 MCP:负责与 Figma 官方 API “握手”
MCP 在这里通常指的是一个基于 Figma 官方 REST API 封装的客户端或 SDK。它的核心工作是:
- 认证:帮你处理 Personal Access Token 或 OAuth 流程,获得访问 Figma 文件的权限。
- 请求构造:将你想要获取节点、文件信息等操作,转换成 Figma API 能理解的 HTTP 请求。
- 响应处理:接收 Figma API 返回的原始 JSON 数据,可能做一些初步的解析或错误处理。
简单说,MCP 是你的脚本和 Figma 服务器之间的“翻译官”和“信使”。没有它,你就得自己从头写 HTTP 客户端、处理认证、管理请求频率限制,非常繁琐。
1.2 Codex:负责执行你的“读取”逻辑并输出 JSON
这里的 Codex 不是指 OpenAI 的 Codex,而是在这个上下文中,指代你编写的、用于控制整个读取流程的脚本或程序(可能是 Node.js、Python 等)。它负责:
- 流程控制:告诉 MCP 要去读取哪个 Figma 文件的哪个节点(通过文件 Key 和节点 ID)。
- 数据遍历与提取:Figma 的节点树可能很复杂,Codex 脚本需要决定是读取整个画板,还是只读取特定类型的图层(如矩形、文本)。
- 结构重塑:将 MCP 返回的原始 API 数据,过滤、转换、组装成你最终想要的 JSON 结构。比如,你可能只关心矩形的
width,height,fills(填充色),而忽略其他属性。 - 输出:将处理好的数据写入到一个
.json文件中,或者直接输出到控制台供其他程序使用。
所以,整个流程是:你的 Codex 脚本 -> 调用 MCP 客户端 -> MCP 向 Figma API 发起请求 -> 获取原始数据 -> MCP 返回给 Codex -> Codex 处理并输出最终 JSON。
2. 动手前的环境与权限准备
在写任何代码之前,先把这两件事搞定,能避免 80% 的“为什么跑不起来”的问题。
2.1 获取 Figma Personal Access Token
这是访问 Figma API 的钥匙。
- 登录你的 Figma 账号。
- 点击右上角头像,进入 “Settings”。
- 在左侧找到 “Account”, 向下滚动找到 “Personal access tokens” 部分。
- 点击 “Create new token”, 给它起个名字,比如 “Local Dev MCP”。
- 创建后,立即复制生成的 Token 字符串。这个页面关闭后就再也看不到了,只能重新生成。
注意:这个 Token 拥有你账户的权限,务必像保管密码一样保管它,不要提交到公开的代码仓库。通常我们会把它放在环境变量里。
2.2 获取目标 Figma 文件的 Key 和节点 ID
你需要知道要读取哪个文件,以及文件里的哪个部分。
- 文件 Key:打开你的 Figma 文件,浏览器地址栏的 URL 看起来像
https://www.figma.com/file/<FILE_KEY>/...。其中<FILE_KEY>就是你要的。 - 节点 ID:如果你想读取特定画板或组件,需要它的 ID。在 Figma 界面中,选中一个画板或图层,在右侧 “Properties” 面板最下方,可以看到 “ID” 字段。如果你想读取整个文件,可以使用根节点的 ID(通常是
0:0或类似格式,但更常见的做法是在 API 请求中不指定节点 ID,以获取整个文件树)。
2.3 本地开发环境准备
以最常用的 Node.js 环境为例:
- 确保安装了 Node.js(建议 LTS 版本)和 npm/yarn/pnpm。
- 创建一个新的项目目录。
- 初始化项目并安装必要的依赖。这里的关键是选择一个靠谱的 Figma API 客户端库,它本质上就是你的“MCP”。社区流行的有
figma-api和figma-js。这里以figma-js为例:
mkdir figma-json-extractor && cd figma-json-extractor npm init -y npm install figma-js dotenvdotenv用来管理环境变量,安全地加载你的 Figma Token。
3. 从单文件读取到结构化 JSON 输出的完整流程
我们从一个最简单的脚本开始,目标是读取一个 Figma 文件,并将所有画板(Frame)的基本信息和其中的矩形元素提取出来。
3.1 基础脚本:连接并获取原始文件数据
首先,在项目根目录创建.env文件,放入你的 Token:
FIGMA_PERSONAL_ACCESS_TOKEN=你的Token然后,创建一个index.js文件:
require('dotenv').config(); const { Figma } = require('figma-js'); // 1. 初始化客户端 (MCP的核心作用) const client = Figma.Client({ personalAccessToken: process.env.FIGMA_PERSONAL_ACCESS_TOKEN, }); // 2. 你的 Figma 文件 Key const FILE_KEY = '你的文件Key'; async function getFigmaFile() { try { // 3. 调用 API 获取文件数据 (MCP发起请求) const { data } = await client.file(FILE_KEY); console.log('文件名称:', data.name); console.log('文档节点类型:', data.document.type); // 此时 data.document 包含了整个文件的节点树 return data.document; } catch (error) { console.error('获取 Figma 文件失败:', error.message); if (error.response) { console.error('API 响应状态:', error.response.status); console.error('API 响应数据:', error.response.data); } } } getFigmaFile();运行node index.js。如果一切正常,你会看到文件名称和文档类型。这说明你的 MCP(figma-js库)已经成功连接并拿到了数据。如果报错(如 403、404),请回头检查 Token 是否正确、是否有文件访问权限、文件 Key 是否正确。
3.2 编写 Codex 逻辑:遍历节点并提取所需属性
现在,我们给脚本加上数据处理逻辑(这就是 Codex 的工作)。我们修改getFigmaFile函数,增加一个递归遍历节点的函数:
async function getFigmaFile() { try { const { data } = await client.file(FILE_KEY); const documentNode = data.document; // 最终要输出的结构化数据 const extractedData = { file_name: data.name, last_modified: data.lastModified, canvases: [], // 存放所有画板 }; // 递归遍历函数 function traverseNode(node, parentCanvas = null) { // 定义我们关心的节点类型和要提取的属性 switch (node.type) { case 'CANVAS': // Figma API 中,画板是 CANVAS 类型 const canvas = { id: node.id, name: node.name, children: [], }; extractedData.canvases.push(canvas); // 继续遍历画板下的子节点,并传入当前画板作为父级 if (node.children) { node.children.forEach(child => traverseNode(child, canvas)); } break; case 'FRAME': // 帧/画板(有时也在 CANVAS 下) case 'GROUP': // 如果是组或帧,继续向下遍历 if (node.children) { const container = { id: node.id, name: node.name, type: node.type, children: [], }; if (parentCanvas) { parentCanvas.children.push(container); } node.children.forEach(child => traverseNode(child, container)); } break; case 'RECTANGLE': case 'ELLIPSE': case 'VECTOR': // 提取形状元素 const shape = { id: node.id, name: node.name, type: node.type, bounding_box: { x: node.absoluteBoundingBox?.x, y: node.absoluteBoundingBox?.y, width: node.absoluteBoundingBox?.width, height: node.absoluteBoundingBox?.height, }, // 提取填充色(取第一个填充,可能是纯色) fills: node.fills?.[0]?.color ? { r: node.fills[0].color.r, g: node.fills[0].color.g, b: node.fills[0].color.b, a: node.fills[0].color.a, } : null, // 提取描边 strokes: node.strokes, cornerRadius: node.cornerRadius, }; if (parentCanvas) { // 找到最直接的父容器(可能是FRAME, GROUP, 或CANVAS) let targetParent = parentCanvas; while (targetParent && !targetParent.children) { // 向上查找,直到找到有children属性的容器 // 这里简化处理,实际可能需要更精确的层级追踪 } if (targetParent && targetParent.children) { targetParent.children.push(shape); } } break; case 'TEXT': // 提取文本元素 const text = { id: node.id, name: node.name, type: 'TEXT', characters: node.characters, style: { font_family: node.style?.fontFamily, font_weight: node.style?.fontWeight, font_size: node.style?.fontSize, line_height: node.style?.lineHeightPx, text_align: node.style?.textAlignHorizontal, }, bounding_box: { x: node.absoluteBoundingBox?.x, y: node.absoluteBoundingBox?.y, width: node.absoluteBoundingBox?.width, height: node.absoluteBoundingBox?.height, }, color: node.fills?.[0]?.color, }; if (parentCanvas && parentCanvas.children) { parentCanvas.children.push(text); } break; default: // 对于其他类型节点,可以选择忽略或继续遍历其子节点 if (node.children) { node.children.forEach(child => traverseNode(child, parentCanvas)); } break; } } // 开始遍历文档根节点的子节点(通常是 CANVAS) if (documentNode.children) { documentNode.children.forEach(child => traverseNode(child)); } // 4. 输出结构化 JSON const fs = require('fs'); fs.writeFileSync( `output_${FILE_KEY}.json`, JSON.stringify(extractedData, null, 2) // 美化输出,缩进2空格 ); console.log(`✅ 数据已成功提取并保存到 output_${FILE_KEY}.json`); console.log(` 共提取 ${extractedData.canvases.length} 个画板。`); // 也可以简单打印到控制台看看 // console.log(JSON.stringify(extractedData, null, 2)); } catch (error) { console.error('处理失败:', error); } }这个脚本做了几件事:
- 初始化与请求:通过 MCP (
figma-js) 获取数据。 - 遍历与筛选:递归遍历整个节点树,只挑选我们关心的节点类型(
CANVAS,RECTANGLE,TEXT等)。 - 属性映射:从 Figma 复杂的节点对象中,提取出前端开发更关心的属性(如尺寸、颜色、字体)。
- 结构重组:将数据重新组织成
{ file_name, canvases: [ { id, name, children: [ ... ] } ] }这样的层级结构。 - 输出 JSON:将最终结构写入文件。
运行后,你会得到一个output_<FILE_KEY>.json文件,里面就是“一次拿全”的结构化设计数据。
4. 处理复杂场景与提升可靠性
单文件读取只是开始。实际项目中,你会遇到更复杂的需求。
4.1 处理大型文件与分页
Figma API 对单个请求返回的数据量有限制。如果文件非常大,返回的节点树可能会被截断。此时,你需要使用nodes参数进行分页查询。
- 思路:不要一次性请求整个文件。先通过
client.fileNodes(FILE_KEY, [nodeId1, nodeId2, ...])获取你关心的特定节点(比如首页画板的 ID)。 - 做法:可以先获取文件的“版本”或“组件列表”,拿到关键节点的 ID,再分批请求其详情。
figma-js库的client.file方法其实已经处理了部分分页,但对于巨型文件,主动管理节点 ID 列表更可靠。
4.2 提取设计 Token(颜色、字体、间距)
这是前端提效的核心。上面的示例只提取了元素的直接属性。要提取 Token,需要更智能的遍历:
- 颜色 Token:遍历所有节点的
fills,strokes,effects属性,收集所有唯一的颜色值(RGBA),并尝试根据图层命名(如Primary/500,Text/Secondary)进行归类。 - 字体样式 Token:遍历所有
TEXT节点,提取fontFamily,fontWeight,fontSize,lineHeightPx等,组合成唯一的字体样式对象,并关联到文本图层的样式名(如果设计师使用了样式)。 - 间距 Token:这更复杂,通常需要通过计算兄弟节点或父子节点的相对位置(
x,y)差值来推断常用的间距值(如 4, 8, 16, 24, 32...)。可以结合图层命名(如Spacing-16)来辅助识别。
一个提取颜色 Token 的简化示例:
const colorTokens = new Map(); // 用 Map 去重 function extractColors(node) { // 处理填充色 if (node.fills && Array.isArray(node.fills)) { node.fills.forEach(fill => { if (fill.color) { const key = `${fill.color.r},${fill.color.g},${fill.color.b},${fill.color.a}`; if (!colorTokens.has(key)) { colorTokens.set(key, { value: fill.color, // 尝试从节点名或父节点名推断 Token 名 suggestedName: node.name.includes('/') ? node.name.split('/').pop() : null, sourceNodeId: node.id, }); } } }); } // 递归处理子节点 if (node.children) { node.children.forEach(child => extractColors(child)); } } // 在获取文件数据后调用 extractColors(documentNode); console.log('提取到的唯一颜色数量:', colorTokens.size); // 可以将 colorTokens 输出为 JSON4.3 错误处理与重试机制
网络请求可能失败,API 也有速率限制。生产级脚本必须考虑这些。
- 速率限制:Figma API 有请求频率限制。在循环或批量请求时,需要在请求间加入延迟(例如使用
setTimeout或async/await配合sleep函数)。 - 错误重试:对于网络超时或 5xx 服务器错误,可以实现简单的重试逻辑。
- 增量更新:如果你的目标是同步设计系统,可以记录上次同步的文件版本号,只获取版本变化的节点,而不是每次都全量拉取。
一个简单的带延迟和重试的请求示例:
async function fetchWithRetry(fileKey, nodeIds, retries = 3, delay = 1000) { for (let i = 0; i < retries; i++) { try { const { data } = await client.fileNodes(fileKey, nodeIds); return data; } catch (error) { if (error.response && error.response.status >= 500 && i < retries - 1) { console.warn(`请求失败,${delay}ms后重试 (${i + 1}/${retries})...`); await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; // 指数退避 } else { throw error; // 重试次数用完或非5xx错误,直接抛出 } } } }5. 将提取的 JSON 集成到前端工作流
拿到 JSON 不是终点,让它产生价值才是。
5.1 生成 CSS/SCSS 变量或 JS 常量
你可以写一个后处理脚本,读取上一步输出的colorTokens.json,然后生成一个design-tokens.scss文件:
const colorTokens = require('./output_color_tokens.json'); const fs = require('fs'); let scssContent = '// Auto-generated design tokens from Figma\n\n'; colorTokens.forEach((token, key) => { const { r, g, b, a } = token.value; const name = token.suggestedName || `color-${key.replace(/,/g, '-')}`; scssContent += `$${name}: rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a});\n`; }); fs.writeFileSync('src/styles/_design-tokens.scss', scssContent); console.log('✅ SCSS 变量文件已生成。');5.2 与样式检查或代码生成工具结合
- 样式检查:在 CI/CD 流程中,运行你的提取脚本,将得到的 JSON 与代码库中定义的样式常量进行对比,如果发现不一致(比如设计稿颜色更新了但代码没更新),则发出警告。
- 代码生成:对于简单的 UI 组件,可以根据矩形、文本的位置和样式信息,尝试生成基础的 HTML 和 CSS 骨架代码。虽然无法 100% 准确,但对于标准化高的组件库,可以大幅减少重复劳动。
5.3 注意事项与排查清单
当你发现脚本不工作或数据不对时,按这个顺序排查:
- Token 与权限:
- Figma Personal Access Token 是否有效且未过期?
- 该 Token 是否有权限访问目标文件?(文件是否在团队项目中,Token 所属账号是否是该团队成员?)
- 文件与节点 ID:
- 文件 Key 是否正确?复制时是否带了多余字符?
- 如果要获取特定节点,节点 ID 是否正确?是否已经通过
client.file确认了节点 ID 的层次结构?
- 网络与 API 限制:
- 是否触发了 API 速率限制?检查错误响应中的
x-ratelimit-*头信息。 - 本地网络是否能正常访问
api.figma.com?
- 是否触发了 API 速率限制?检查错误响应中的
- 数据解析逻辑:
- Figma API 的响应结构是否和你代码中访问的属性路径一致?API 版本更新可能导致字段变化。最可靠的方法是,在脚本中先
console.log(JSON.stringify(data.document, null, 2))打印出完整的原始响应,对照着写解析逻辑。 - 你的遍历逻辑是否覆盖了所有需要的节点类型?
FRAME、GROUP、INSTANCE(组件实例)等都需要考虑。 - 颜色值
fills可能是一个数组,且可能是图片填充 (type: 'IMAGE') 或渐变填充 (type: 'GRADIENT_...'),你的代码是否处理了这些情况?
- Figma API 的响应结构是否和你代码中访问的属性路径一致?API 版本更新可能导致字段变化。最可靠的方法是,在脚本中先
- 输出与集成:
- 输出的 JSON 文件路径是否正确?是否有写入权限?
- 生成的 CSS/JS 文件格式是否符合你项目的编码规范?
我个人的经验是,第一次跑通后,把核心的“连接-获取-遍历-输出”流程封装成一个可靠的函数或模块。后续不同的提取需求(如只提颜色、只提文本样式、提取组件结构),就只是编写不同的“遍历器”(Traversal) 和“转换器”(Transformer) 的问题。这样,你的 Figma MCP + Codex 方案就从一个一次性脚本,变成了一个可维护、可扩展的前端设计资产同步管道。