Paper 应用
Paper 是一个设计工具,其画布由真实的 HTML 和 CSS 构成。Paper 桌面应用运行一个本地 MCP 服务器,让代理能够读取画布并将 HTML 作为设计节点写入其中。
服务器自带指南。本技能涵盖了该指南未提及的内容:在实际会话中通过实践积累的经验。
开始之前
- 检查工具是否存在:在代码模式下运行
await describeNamespace("mcp__paper")。如果命名空间为空或ALL_TOOLS中没有mcp__paper__*条目,则说明会话未连接,通常是因为导致会话开始时 Paper 未打开。服务器位于http://127.0.0.1:29979/mcp,并且仅在 Paper 打开时响应。pi mcp list(从 bash 运行)显示服务器本身是否健康。如果健康,会话无法自行重新连接:请要求用户运行/mcp reconnect paper(在 pi 中),如果无效则运行/reload。 - 每个会话加载一次官方指南:
get_guide({ topic: "paper-mcp-instructions" })。遵循指南:在新设计前发布设计简报,每次write_html调用编写一个视觉组,通过截图审查,完成后调用finish_working_on_nodes。其他主题:mobile-status-bar、figma-import、image-generation。 - 查找文件。
list_resources({ limit: 200 })会列出文件,无需打开特定文件。将fileId传递给每个文件范围调用。get_basic_info({ fileId })可以在非前台标签的文件上工作,因此很少需要open_file(它会在用户应用中打开一个标签)。 - 如果项目有
AGENTS.md,请在探索之前在其中查找 Paper 文件 ID 和画布约定。
从代码模式调用 Paper
- 在一个脚本中使用
await或按顺序轮次运行依赖的 Paper 调用。切勿在同一轮次中将其作为并行工具调用发出。并行调用导致了以往会话中的大多数失败,并且每个失败都被误认为是 Paper 的错误:“Node not found”(兄弟调用刚刚创建的节点)、子节点顺序混乱、导出缺少最近的写入、以及read对尚未写入的导出返回 ENOENT。 - 每个文件范围工具都需要在其代码模式声明中包含
fileId,即使服务器的说明说可以省略。省略会导致验证失败。只有get_basic_info和get_selection可以接受无fileId。 - 在首次使用某个工具之前运行
describeTool("mcp__paper__<name>")。参数形状不可猜测:get_font_family_info接受familyNames: string[],set_text_content和update_styles接受updates数组,而export接受nodes映射。 - 文件范围工具返回一个纯字符串:先是
{ file, contentHash }的头部 JSON 块,然后是正文。没有文件范围的工具(list_resources、create_file、open_file、get_guide)返回{ content: [{ type: "text", text }] }。不要从结果中通过正则匹配第一个"id":它是文件 ID,将其作为节点 ID 传递会导致“Node not found”。 - 文件范围的失败和验证错误会抛出异常。一些非文件范围的失败(例如“打开一个 Paper 文件以使用此工具。”)会通过
isError: true解决。不要用.catch(() => {})包装 Paper 调用:错误的参数形状会静默失败,编辑永远不会出现。 - “Your Paper file is currently disconnected from the server” 表示写入失败,但读取仍然有效。在以往会话中,休眠和重试未能解决此问题。要求用户将 Paper 文件带回前台,然后重试。
- 在代码模式下
get_screenshot不返回图像:其结果被扁平化为头部字符串。请使用下面的shot()辅助函数,该函数在同一脚本中导出 PNG 并读取它。当测试工具直接通过 MCP 调用(而非通过代码模式)时,get_screenshot图像确实会返回。
解析正文后的有用结果形状:
| 工具 | 正文 |
|---|---|
create_artboard | { id, name, worldX, worldY, width, height } |
write_html | { createdNodes: [{ id, name, component }] },仅顶层节点 |
get_tree_summary | { summary },一个带缩进的文本树,格式为 Type "name" (id) W×H |
get_children | { children: [{ id, name, component, childCount, x, y }], count } |
find_nodes | { nodes: [{ id, name, component, matched: [{ styleName, styleValue }] }] } |
duplicate_nodes | { duplicatedNodes: [{ sourceId, newId, descendantIdMap }] } |
export | { exports: [{ nodeId, filePath }] } |
update_styles | 当样式在相应上下文中无效时,列出 ignoredStyles。请检查它。 |
辅助函数
将这些粘贴到代码模式脚本的顶部,或存储一次并在后续调用中 eval:
var F = "<fileId>";
var txt = r => typeof r === "string" ? r : (r?.content ?? []).map(c => c.text ?? "").join("\n");
// 去除文件范围工具预先添加的 { file, contentHash } 头部。
var body = r => { const t = txt(r); const i = t.indexOf("\n}\n"); return t.startsWith('{\n "file"') && i > -1 ? t.slice(i + 3) : t; };
var json = r => JSON.parse(body(r));
var w = async (target, html, mode = "insert-children") =>
json(await tools.mcp__paper__write_html({ fileId: F, targetNodeId: target, mode, html })).createdNodes;
// 将节点导出为 PNG,显示它,然后从 ~/Downloads 删除该文件。
var shot = async (id, scale = "1x") => {
const { exports } = json(await tools.mcp__paper__export({ fileId: F, nodes: { [id]: [{ format: "png", scale }] } }));
const p = exports[0].filePath;
image(await tools.read({ path: p, offset: null, limit: null }));
await tools.bash({ command: `rm -f "${p}"`, timeout: 5 });
};
要在调用间重用辅助函数:store("paper", helperSource),然后在每个后续脚本顶部使用 eval(load("paper"))。使用 var 声明辅助函数,因为 eval 内的 const 和 let 不会泄露出去。store() 仅在脚本成功时保留值,因此请在一个不执行其他操作的脚本中存储辅助函数。
截图和导出
- 在单独的代码模式调用中进行截图,与编辑分开。如果导出与写入在同一脚本中,则可能在 Paper 渲染它们之前执行,导致 PNG 显示缺失或过时的内容。
export写入到~/Downloads/<artboard name>.png,其他比例带@2x或@0.5x后缀,当存在同名文件时添加(1)、(2)后缀。像?和:这样的字符会变成_。始终使用返回的filePath。不要使用ls -t ~/Downloads或名称通配符选择文件:那可能从早期运行中获取过时的导出。- 读取后移动或删除导出,以免它们堆积在
~/Downloads中。不要动未创建的文件。 - 对于审查截图,使用
0.5x以节省令牌;对于交付物,使用2x(1920×1080 的画板变成 3840×2160)。 export_combined_pdf按画布位置顺序将画板合并为一个 PDF。export也支持mp4/webm视频,以及用于 SVG 节点的svg、jpg、webp、avif和pdf。- 如需将画板转换为 Google Slides,请参见 references/slides-export.md。
编写 HTML
write_html 工具描述列出了规则(内联样式、flex、无 margin、无 display: grid、无表格、使用 layer-name 作为图层名称)。额外发现:
- 空的
<div>会变成矩形节点,即使有display: flex,并且矩形不能有子节点。向其中写入会失败并提示“cannot have children”。请将容器及其第一个子节点一起写入,或者在replace模式下使用write_html替换矩形。 - 不支持富文本。对于语法高亮的代码或包含一个彩色单词的句子,构建一行 flex 行,每个令牌一个文本节点,每个节点设置
white-space: pre。代码变成每行一个 flex 行。使用辅助函数生成 HTML,而不是手动编写。 - 带有渐变图层的
background会忽略backgroundColor。将基础颜色添加为最后一层:linear-gradient(#F6F7F9, #F6F7F9)。 - 内联
<svg>适用于图标、标志和连接线,并接受fill/stroke颜色和令牌变量。内联品牌标志的 SVG 而不是链接到它。 - SVG 路径填充对
find_nodes和get_computed_styles(对它们返回{})不可见。在get_tree_summary中找到SVGVisualElement "Path"节点,并使用update_styles({ styles: { fill } })重新着色,这确实有效。 - 本地图像需要使用绝对路径和
paper-asset://方案:<img src="paper-asset:///private/tmp/spec/foo.png">。 - 在 flex 行中等宽列需要
flex: 1; flex-basis: 0。否则,某一列上的内边距会使其比兄弟列更宽。 - 如果内容被裁剪,使用
update_styles将画板设置为height: "fit-content",而不是猜测新的高度。
字体
get_font_family_info({ fileId, familyNames: [...] })检查机器上安装的字体和 Google Fonts。在会话中首次排版样式设置前调用它。- 会话期间安装的字体(例如,在 macOS 上安装到
~/Library/Fonts)直到 Paper 重启才会显示。请用户退出并重新打开 Paper。不要自己终止 Paper,因为这可能导致状态丢失。 - 在将字体用于公共网站之前检查其许可证。许多 dafont.com 的字体仅限个人使用免费。
- 要比较多种字体而无需重启 Paper,可以将标本本地渲染为透明 PNG(例如,使用 Pillow),将它们作为
paper-asset://图像放置在标本画板上,然后只安装最终候选字体。
组织画布
- 在对设计进行迭代时,复制画板并修改副本。不要原地编辑较旧的画板:用户希望看到进展,并且经常要求回到早期版本。一位用户不得不在一个会话中要求两次才得到满足。
create_artboard和duplicate_nodes会将新画板放在最佳空白位置,这会导致它们分散。要排列它们,请使用update_styles设置画板的top和left。画板之间保持至少 80px 的间距。- 将设计探索与选定方向分开:放在画布自己的区域中,或放在自己的页面中(
create_page,然后传递pageId)。无法切换用户正在查看的页面。 - 对于幻灯片和变体集,使用固定网格(例如,每行四个,
x = x0 + col × (width + gap))。当添加或移除画板时,重新排列其余画板使其无缝对齐,并重新命名。 - 对于浅色和深色变体,将浅色行放在顶部,并将匹配的深色版本直接放在每个下方。要制作深色版本,请参见 references/dark-mode-variants.md。
- 为画板指定描述性名称,如
Homepage · A (Editorial)或04 · What are Workers?。导出文件名来源于它们。 - 节点 ID 不会在会话结束后保留。将文件 ID、URL、命名方案、网格公式和样式规则写入项目的
AGENTS.md,以便下一个会话可以继续。
设计转代码
- 使用
get_jsx(format: "inline-styles"或"tailwind")和get_computed_styles读取精确值。切勿从截图中测量大小或颜色。 - 计算后的颜色可能以
oklab(...)形式返回。直接复制它们而不是转换。 - 构建页面后,在真实浏览器中与 Paper 导出进行比较,并检查浅色和深色模式。
MCP 无法执行的操作
- 共享文件或管理权限。用户从 Paper 应用共享:邀请团队成员编辑,或发送文件 URL 用于只读访问。
- 将文件移动到文件夹中。从
create_file新建的文件位于用户文件夹之外,用户必须手动拖入。 - 在文件之间移动节点。
move_nodes仅在同一文件内跨页面移动。要移动整个文件的内容,请使用create_file({ cloneFileId, name }),检查节点和画板数量是否匹配,然后delete_nodes删除原始文件。删除前请与用户确认。 - 切换用户正在查看的页面。
