Storybook MCP:按 Story ID 预览既有 Story 且不改动源码的完整工作流解析
本文以 Storybook 仓库中 agent-eval/evals/915-preview-story-by-id 评测用例(其任务提示见 PROMPT.md)为核心骨架展开。该用例要求 AI Agent 对既有的 Button 组件的 Primary 与 Secondary 两个 Story 生成预览链接,且不得修改任何组件或 Story 文件。读完后你能掌握:如何用 Storybook 的 MCP / stories-preview 工作流按 Story ID 精确预览既有 Story、Story ID 的命名规则如何从 title 与导出名推导、评测断言如何校验「按 ID 预览」这一行为,以及预览工具在仓库中的底层实现。
一、任务背景:一个「只读预览」评测场景
915-preview-story-by-id 是仓库 agent-eval 目录下的一个评测用例,其 PROMPT.md 原文只有两句话,却完整定义了一个典型任务边界:
Show a preview of two existing Button stories: Primary and Secondary. Do not modify any component or story files.
这两句浓缩了三个关键约束:
- 对象是「既有的 Story」——不是新建组件、不是改 Story,目标 Story 已经存在于仓库中。
- 目标是「生成预览」——把 Story 渲染出来并以可访问的链接形式交付给调用方。
- 行为红线是「只读」——不得改动任何组件或 Story 文件,任何写盘操作都算失败。
这类任务在 Storybook 面向 AI Agent 的场景中很有代表性:Agent 常常需要「看一眼某个组件现在长什么样」,而不是「改它」。因此正确做法是调用预览类工作流,而非触发组件创建/编辑类工作流。
该用例在 package.json 中声明了所使用的沙箱模板:
{
"name": "915-preview-story-by-id",
"type": "module",
"evals": {
"template": "reshaped-storybook"
}
}
template: "reshaped-storybook" 指向 agent-eval/templates/reshaped-storybook 模板,评测会在该模板生成的沙箱项目中运行 Agent,再由断言脚本检验其调用轨迹。
二、评测夹具中的组件与 Story
理解 Story ID 从何而来,需要先看夹具本身。
组件 src/components/Button.tsx 是一个极简的受控按钮:
type ButtonProps = {
label: string;
disabled?: boolean;
};
export default function Button({ label, disabled = false }: ButtonProps) {
return (
<button type="button" disabled={disabled} data-testid="button-component">
{label}
</button>
);
}
Story 文件 stories/Button.stories.tsx 采用 CSF3 写法,是推导 Story ID 的关键:
import type { Meta, StoryObj } from '@storybook/react';
import Button from '../src/components/Button';
const meta = {
title: 'Example/Button',
component: Button,
tags: ['test'],
args: {
label: 'Click me',
disabled: false,
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
label: 'Primary',
},
};
export const Secondary: Story = {
args: {
label: 'Secondary',
},
};
从这份 Story 文件可以直接读出 Story ID 的构成规则:
title: 'Example/Button'会被 kebab-case 化,前缀为example-button;- 导出名
Primary转为--primary,Secondary转为--secondary; - 最终得到
example-button--primary与example-button--secondary两个 Story ID。
这套「title kebab-case + -- + 导出名 kebab-case」的拼接逻辑,与评测工具 agent-eval/lib/test-utils.ts 中 kebabCase 辅助函数的实现保持一致(把驼峰拆成连字符、转小写),可据此交叉验证 ID 的推导是否正确。
三、断言逻辑:如何判定 Agent「按 ID 预览」成功
EVAL.ts 是该用例的验收脚本,用 vitest 编写,全部断言围绕 stories-preview 这一个工作流:
import { describe, expect, test } from 'vitest';
import {
expectWorkflowCalls,
getWorkflowCalls,
workflowCallIncludesStory,
workflowCallUsesStoryId,
} from '#test-utils';
describe('previewing Button stories by story ID', () => {
test('previews stories using story IDs', () => {
const previewCalls = getWorkflowCalls('stories-preview');
expectWorkflowCalls(['stories-preview']);
expect(previewCalls.some(workflowCallUsesStoryId)).toBe(true);
expect(
previewCalls.some((call) =>
workflowCallIncludesStory(call, { storyId: 'example-button--primary' })
)
).toBe(true);
expect(
previewCalls.some((call) =>
workflowCallIncludesStory(call, { storyId: 'example-button--secondary' })
)
).toBe(true);
});
});
逐条解读这组断言,可以清楚知道「及格线」在哪里:
expectWorkflowCalls(['stories-preview']):要求 Agent 至少调用过一次stories-preview工作流。这确保了 Agent 走的是「预览」路径,而不是去创建/编辑组件。previewCalls.some(workflowCallUsesStoryId):要求至少有一次预览调用是通过storyId字段发起的。这正是用例标题「by id」的含义——不能只靠文件路径或导出名,必须使用规范化的 Story ID。- 两个
workflowCallIncludesStory(...):分别要求预览调用覆盖了example-button--primary和example-button--secondary,即 Primary 与 Secondary 两个 Story 都必须被预览到,缺一不可。
这三个断言共同刻画了「按 Story ID 预览既有 Story」这一行为的完整判定:正确的预览工作流 + 使用 storyId 入参 + 覆盖到任务指定的两个具体 Story。
其中几个校验函数定义在共享工具 agent-eval/lib/test-utils.ts:
getWorkflowCalls(name)(第 122-124 行):从 Agent 的调用轨迹中过滤出指定名称的工作流调用。workflowCallUsesStoryId(第 801-803 行):判断某次调用的 story 入参里是否存在字符串类型的storyId。workflowCallIncludesStory(第 794-799 行):判断某次调用是否覆盖了期望的 story 入参(可含storyId/exportName/absoluteStoryPath)。
这些函数会解析 Agent 的 transcript(__agent_eval__/transcript.txt)与 shell 命令记录(__agent_eval__/results.json),兼容 MCP 工具调用与 storybook ai CLI 两条路径(见 test-utils.ts 中 getStorybookWorkflowCalls 对 integration === 'plugin' 与 MCP 的分支处理)。
四、底层实现:preview-stories 工具如何产出预览
评测断言校验的是 Agent 的调用轨迹,而 stories-preview 工作流背后的真实执行逻辑位于 MCP 插件包中。code/addons/mcp/src/tools/preview-stories.ts 注册了一个供 MCP 客户端内联渲染 story 预览的应用资源:
export const PREVIEW_STORIES_RESOURCE_URI = `ui://${PREVIEW_STORIES_TOOL_NAME}/preview.html`;
/**
* Serves the MCP app that renders story previews inline in the client.
*
* The app reads the tool result's `structuredContent`, so it is bound to the preview tool's output
* contract rather than to its implementation.
*/
export async function addPreviewStoriesResource(server: McpServer<any, AddonContext>) {
const previewStoryAppScript = await fs.readFile(
url.fileURLToPath(
import.meta.resolve('@storybook/addon-mcp/internal/preview-stories-app-script')
),
'utf-8'
);
const appHtml = appTemplate.replace('// APP_SCRIPT_PLACEHOLDER', previewStoryAppScript);
server.resource(
{
name: PREVIEW_STORIES_RESOURCE_URI,
description: 'App resource for the Preview Stories tool',
uri: PREVIEW_STORIES_RESOURCE_URI,
mimeType: 'text/html;profile=mcp-app',
},
() => {
const origin = server.ctx.custom!.origin;
return {
contents: [
{
uri: PREVIEW_STORIES_RESOURCE_URI,
mimeType: 'text/html;profile=mcp-app',
text: appHtml,
_meta: {
ui: {
prefersBorder: false,
domain: origin,
csp: {
connectDomains: [origin],
resourceDomains: [origin],
frameDomains: [origin],
baseUriDomains: [origin],
},
},
},
},
],
};
}
);
}
从源码结构看,该资源把预览页 HTML 模板 preview-stories-app-template.html 与应用脚本(来自 @storybook/addon-mcp/internal/preview-stories-app-script)拼接后,作为 text/html;profile=mcp-app 的 MCP 资源暴露出去,并附带面向预览源站的 CSP 域名配置(connectDomains / resourceDomains / frameDomains / baseUriDomains)。注释也明确说明:该应用读取工具结果的 structuredContent,即它绑定的是预览工具的输出契约而非其具体实现。这与评测断言「绑定输出而非实现」的校验思路一脉相承——断言只关心 Agent 是否以正确的 story 入参触发了预览、并交付了预览链接,而不关心预览页内部如何渲染。
配合 code/addons/mcp/README.md 与 code/addons/mcp/src/tools/tool-names.ts 中的工具命名常量,可以确认 stories-preview 正是该插件对外暴露的「预览 Story」工作流入口。
五、可操作要点与适用边界
把上述证据串起来,针对「按 Story ID 预览既有 Story」这一任务,可提炼出以下可操作要点:
- 优先使用
storyId入参:当已知目标 Story 的规范 ID(如example-button--primary)时,应以storyId作为stories-preview的 story 入参,而不是仅传文件路径或导出名。评测中workflowCallUsesStoryId断言专门校验了这一点。 - ID 推导要可复现:Story ID 由
title的 kebab-case 形式拼接--与导出名的 kebab-case 形式得到。修改title或导出名会直接改变 ID,因此预览前应核对 Story 文件的title与export const名称。 - 严守「只读」边界:任务明确禁止改动组件或 Story 文件时,正确行为是只调用预览工作流。任何写盘/编辑类工作流都不应出现。
- 预览结果以链接交付:从 test-utils.ts 的
STORYBOOK_PREVIEW_URL_PATTERN(第 517 行,/[?&]path=\/|\/iframe\.html\?/)可推断,预览链接的形态是 manager 页的?path=/story/…或 iframe 的/iframe.html?id=…,两者都以 Story ID 为核心定位参数。
适用前提:以上行为基于当前仓库中 915-preview-story-by-id 评测夹具与 @storybook/addon-mcp 的实现,storyId 的 kebab-case 拼接规则与 stories-preview 工作流入口以本仓库对应文件为准。若目标项目使用了不同的 title 约定或自定义 Story ID 生成逻辑,应以项目实际的 Story 文件与 Storybook 版本为准。
六、参考文件
- 任务提示:agent-eval/evals/915-preview-story-by-id/PROMPT.md
- 评测断言:agent-eval/evals/915-preview-story-by-id/EVAL.ts
- 用例配置:agent-eval/evals/915-preview-story-by-id/package.json
- 夹具组件:agent-eval/evals/915-preview-story-by-id/src/components/Button.tsx
- 夹具 Story:agent-eval/evals/915-preview-story-by-id/stories/Button.stories.tsx
- 评测共享工具:agent-eval/lib/test-utils.ts
- 预览工具实现:code/addons/mcp/src/tools/preview-stories.ts
- 预览页模板:code/addons/mcp/src/tools/preview-stories/preview-stories-app-template.html
- MCP 插件说明:code/addons/mcp/README.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00