@storybook/addon-mcp 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -12
- package/dist/preset.js +147 -119
- package/dist/preview-stories-app-script.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -48,21 +48,19 @@ export default {
|
|
|
48
48
|
options: {
|
|
49
49
|
toolsets: {
|
|
50
50
|
dev: true, // Tools for story URL retrieval and UI building instructions (default: true)
|
|
51
|
-
docs: true, // Tools for component manifest and documentation (default: true
|
|
51
|
+
docs: true, // Tools for component manifest and documentation (default: true)
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
|
-
|
|
57
|
-
experimentalComponentsManifest: true, // Enable manifest generation for the docs toolset, only supported in React-based setups.
|
|
58
|
-
},
|
|
56
|
+
// componentsManifest is enabled by default in recent Storybook versions, no need to set it
|
|
59
57
|
};
|
|
60
58
|
```
|
|
61
59
|
|
|
62
60
|
**Available Toolsets:**
|
|
63
61
|
|
|
64
62
|
- `dev`: Enables [Dev Tools](#dev-tools)
|
|
65
|
-
- `docs`: Enables [Documentation Tools](#docs-tools
|
|
63
|
+
- `docs`: Enables [Documentation Tools](#docs-tools)
|
|
66
64
|
|
|
67
65
|
Disabling the Dev Tools is useful when you want to try out the same experience that your external component consumers will get, because they only get the Component Documentation Tools.
|
|
68
66
|
|
|
@@ -156,9 +154,12 @@ The instructions ensure agents follow your project's conventions when creating o
|
|
|
156
154
|
|
|
157
155
|
Allows agents to retrieve direct URLs to specific stories in your Storybook. The agent can request URLs for multiple stories by providing:
|
|
158
156
|
|
|
159
|
-
-
|
|
160
|
-
- `
|
|
161
|
-
- `
|
|
157
|
+
- **Path-based input** (best when the agent is already editing a `.stories.*` file):
|
|
158
|
+
- `absoluteStoryPath`: Absolute path to the story file
|
|
159
|
+
- `exportName`: The export name of the story
|
|
160
|
+
- `explicitStoryName`: Optional explicit story name
|
|
161
|
+
- **ID-based input** (best when the agent discovered stories via docs tools):
|
|
162
|
+
- `storyId`: Full Storybook story ID (for example `example-button--primary`)
|
|
162
163
|
|
|
163
164
|
Example agent usage:
|
|
164
165
|
|
|
@@ -169,24 +170,31 @@ Agent calls tool, gets response:
|
|
|
169
170
|
http://localhost:6006/?path=/story/example-button--primary
|
|
170
171
|
```
|
|
171
172
|
|
|
172
|
-
### Docs Tools
|
|
173
|
+
### Docs Tools
|
|
173
174
|
|
|
174
|
-
These additional tools are available when the
|
|
175
|
+
These additional tools are available when the component manifest feature is enabled. They provide agents with detailed documentation about your UI components.
|
|
175
176
|
|
|
176
177
|
**Requirements:**
|
|
177
178
|
|
|
178
179
|
- Storybook version 10.1.0 or higher (currently only available as prereleases, `storybook@next`)
|
|
179
180
|
- React-based framework (`react-vite`, `nextjs-vite`, `nextjs`, `react-webpack5`)
|
|
180
|
-
- Feature flag `features.
|
|
181
|
+
- Feature flag `features.componentsManifest` enabled (defaults to `true` in recent Storybook versions)
|
|
181
182
|
|
|
182
183
|
**To enable:**
|
|
183
184
|
|
|
185
|
+
The `componentsManifest` feature is enabled by default in recent Storybook versions — no configuration needed.
|
|
186
|
+
|
|
187
|
+
If you are on an older Storybook version that doesn't default to `true`, you may need to enable it explicitly. Use the flag that matches your Storybook version:
|
|
188
|
+
|
|
184
189
|
```javascript
|
|
185
190
|
// .storybook/main.js
|
|
186
191
|
export default {
|
|
187
192
|
// ... other config
|
|
188
193
|
features: {
|
|
189
|
-
|
|
194
|
+
// For Storybook 10.3.x and later:
|
|
195
|
+
componentsManifest: true,
|
|
196
|
+
// For older Storybook versions (before the flag was renamed):
|
|
197
|
+
// experimentalComponentsManifest: true,
|
|
190
198
|
},
|
|
191
199
|
};
|
|
192
200
|
```
|
|
@@ -195,10 +203,14 @@ export default {
|
|
|
195
203
|
|
|
196
204
|
Returns a list of all available UI components as well as standalone docs in your component library. Useful for the LLM as discovery and understanding what components are available to use.
|
|
197
205
|
|
|
206
|
+
You can pass `withStoryIds: true` to include nested story entries (story name + story ID) under each component, which is useful before calling `preview-stories` or `run-story-tests` with `storyId`.
|
|
207
|
+
|
|
198
208
|
#### 4. Get Documentation (`get-documentation`)
|
|
199
209
|
|
|
200
210
|
Retrieves detailed documentation for a specific component or docs entry.
|
|
201
211
|
|
|
212
|
+
Component documentation includes component ID and story IDs for listed stories, so agents can directly feed those IDs into `preview-stories` and `run-story-tests`.
|
|
213
|
+
|
|
202
214
|
The agent provides a component/docs ID to retrieve its documentation. To get documentation for multiple entries, call this tool multiple times.
|
|
203
215
|
|
|
204
216
|
## Contributing
|
package/dist/preset.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { McpServer } from "tmcp";
|
|
2
2
|
import { ValibotJsonSchemaAdapter } from "@tmcp/adapter-valibot";
|
|
3
3
|
import { HttpTransport } from "@tmcp/transport-http";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import url from "node:url";
|
|
6
|
-
import { normalizeStoryPath } from "storybook/internal/common";
|
|
7
|
-
import { storyNameFromExport } from "storybook/internal/csf";
|
|
8
|
-
import { logger } from "storybook/internal/node-logger";
|
|
9
5
|
import * as v from "valibot";
|
|
6
|
+
import { logger } from "storybook/internal/node-logger";
|
|
10
7
|
import { telemetry } from "storybook/internal/telemetry";
|
|
11
8
|
import { stringify } from "picoquery";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { normalizeStoryPath } from "storybook/internal/common";
|
|
11
|
+
import { storyNameFromExport } from "storybook/internal/csf";
|
|
12
|
+
import { ComponentManifestMap, DocsManifestMap, GET_TOOL_NAME, LIST_TOOL_NAME, addGetDocumentationTool, addGetStoryDocumentationTool, addListAllDocumentationTool } from "@storybook/mcp";
|
|
12
13
|
import fs from "node:fs/promises";
|
|
13
|
-
import { ComponentManifestMap, DocsManifestMap, addGetDocumentationTool, addListAllDocumentationTool } from "@storybook/mcp";
|
|
14
14
|
import { buffer } from "node:stream/consumers";
|
|
15
15
|
|
|
16
16
|
//#region package.json
|
|
17
17
|
var name = "@storybook/addon-mcp";
|
|
18
|
-
var version = "0.3.
|
|
18
|
+
var version = "0.3.4";
|
|
19
19
|
var description = "Help agents automatically write and test stories for your UI components";
|
|
20
20
|
|
|
21
21
|
//#endregion
|
|
@@ -108,6 +108,83 @@ async function fetchStoryIndex(origin) {
|
|
|
108
108
|
return index;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/utils/slash.ts
|
|
113
|
+
/**
|
|
114
|
+
* Normalize paths to forward slashes for cross-platform compatibility
|
|
115
|
+
* Storybook import paths always use forward slashes
|
|
116
|
+
*/
|
|
117
|
+
function slash(path) {
|
|
118
|
+
return path.replace(/\\/g, "/");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/utils/find-story-ids.ts
|
|
123
|
+
function isStoryIdInput(input) {
|
|
124
|
+
return "storyId" in input;
|
|
125
|
+
}
|
|
126
|
+
function normalizeImportPath(importPath) {
|
|
127
|
+
return slash(normalizeStoryPath(path.posix.normalize(slash(importPath))));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Finds story IDs in the story index that match the given story inputs.
|
|
131
|
+
*
|
|
132
|
+
* @param index - The Storybook story index
|
|
133
|
+
* @param stories - Array of story inputs to search for
|
|
134
|
+
* @returns Array of per-input lookup results in the exact same order as the input stories
|
|
135
|
+
*/
|
|
136
|
+
function findStoryIds(index, stories) {
|
|
137
|
+
const entriesList = Object.values(index.entries);
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const storyInput of stories) {
|
|
140
|
+
if (isStoryIdInput(storyInput)) {
|
|
141
|
+
const foundEntry = index.entries[storyInput.storyId];
|
|
142
|
+
if (foundEntry) {
|
|
143
|
+
logger.debug(`Found story ID: ${foundEntry.id}`);
|
|
144
|
+
result.push({
|
|
145
|
+
id: foundEntry.id,
|
|
146
|
+
input: storyInput
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
logger.debug("No story found");
|
|
150
|
+
result.push({
|
|
151
|
+
input: storyInput,
|
|
152
|
+
errorMessage: `No story found for story ID "${storyInput.storyId}"`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const { exportName, explicitStoryName, absoluteStoryPath } = storyInput;
|
|
158
|
+
const normalizedCwd = slash(process.cwd());
|
|
159
|
+
const normalizedAbsolutePath = slash(absoluteStoryPath);
|
|
160
|
+
const relativePath = normalizeImportPath(path.posix.relative(normalizedCwd, normalizedAbsolutePath));
|
|
161
|
+
logger.debug("Searching for:");
|
|
162
|
+
logger.debug({
|
|
163
|
+
exportName,
|
|
164
|
+
explicitStoryName,
|
|
165
|
+
absoluteStoryPath,
|
|
166
|
+
relativePath
|
|
167
|
+
});
|
|
168
|
+
const foundEntry = entriesList.find((entry) => normalizeImportPath(entry.importPath) === relativePath && [explicitStoryName, storyNameFromExport(exportName)].includes(entry.name));
|
|
169
|
+
if (foundEntry) {
|
|
170
|
+
logger.debug(`Found story ID: ${foundEntry.id}`);
|
|
171
|
+
result.push({
|
|
172
|
+
id: foundEntry.id,
|
|
173
|
+
input: storyInput
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
logger.debug("No story found");
|
|
177
|
+
let errorMessage = `No story found for export name "${exportName}" with absolute file path "${absoluteStoryPath}"`;
|
|
178
|
+
if (!explicitStoryName) errorMessage += ` (did you forget to pass the explicit story name?)`;
|
|
179
|
+
result.push({
|
|
180
|
+
input: storyInput,
|
|
181
|
+
errorMessage
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
111
188
|
//#endregion
|
|
112
189
|
//#region src/utils/errors.ts
|
|
113
190
|
/**
|
|
@@ -146,20 +223,30 @@ const AddonOptions = v.object({ toolsets: v.optional(v.object({
|
|
|
146
223
|
docs: true,
|
|
147
224
|
test: true
|
|
148
225
|
}) });
|
|
149
|
-
|
|
150
|
-
* Schema for a single story input when requesting story URLs.
|
|
151
|
-
*/
|
|
152
|
-
const StoryInput = v.object({
|
|
153
|
-
exportName: v.string(),
|
|
154
|
-
explicitStoryName: v.pipe(v.optional(v.string()), v.description(`If the story has an explicit name set via the "name" propoerty, that is different from the export name, provide it here.
|
|
155
|
-
Otherwise don't set this.`)),
|
|
156
|
-
absoluteStoryPath: v.string(),
|
|
226
|
+
const StoryInputProps = {
|
|
157
227
|
props: v.pipe(v.optional(v.record(v.string(), v.any())), v.description(`Optional custom props to pass to the story for rendering. Use this when you don't want to render the default story,
|
|
158
228
|
but you want to customize some args or other props.
|
|
159
229
|
You can look up the component's documentation using the ${GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME} tool to see what props are available.`)),
|
|
160
230
|
globals: v.pipe(v.optional(v.record(v.string(), v.any())), v.description(`Optional Storybook globals to set for the story preview. Globals are used for things like theme, locale, viewport, and other cross-cutting concerns.
|
|
161
231
|
Common globals include 'theme' (e.g., 'dark', 'light'), 'locale' (e.g., 'en', 'fr'), and 'backgrounds' (e.g., { value: '#000' }).`))
|
|
162
|
-
}
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Schema for a single story input when requesting story URLs.
|
|
235
|
+
*/
|
|
236
|
+
const StoryInput = v.union([v.object({
|
|
237
|
+
exportName: v.pipe(v.string(), v.description(`The export name of the story from the story file.
|
|
238
|
+
Use this path-based shape only when you're already editing a .stories.* file and know the export names in that file.
|
|
239
|
+
If you do not already have story file context, prefer the storyId shape instead of searching files.`)),
|
|
240
|
+
explicitStoryName: v.pipe(v.optional(v.string()), v.description(`If the story has an explicit name set via the "name" property, that is different from the export name, provide it here.
|
|
241
|
+
Otherwise don't set this.`)),
|
|
242
|
+
absoluteStoryPath: v.pipe(v.string(), v.description("Absolute path to the story file. Use together with exportName only when story file context is already available.")),
|
|
243
|
+
...StoryInputProps
|
|
244
|
+
}), v.object({
|
|
245
|
+
storyId: v.pipe(v.string(), v.description(`The full Storybook story ID (for example "button--primary").
|
|
246
|
+
Prefer this shape whenever you are not already working in a specific story file.
|
|
247
|
+
Use IDs discovered from ${LIST_TOOL_NAME} (withStoryIds=true) or ${GET_TOOL_NAME}.`)),
|
|
248
|
+
...StoryInputProps
|
|
249
|
+
})]);
|
|
163
250
|
/**
|
|
164
251
|
* Schema for the array of stories to fetch URLs for.
|
|
165
252
|
*/
|
|
@@ -169,24 +256,17 @@ const StoryInputArray = v.array(StoryInput);
|
|
|
169
256
|
//#region src/tools/preview-stories/preview-stories-app-template.html
|
|
170
257
|
var preview_stories_app_template_default = "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n html[data-theme='light'] {\n color-scheme: light;\n }\n html[data-theme='dark'] {\n color-scheme: dark;\n }\n html,\n body {\n width: 100%;\n margin: 0;\n padding: 0;\n background-color: var(--color-background-secondary);\n }\n body {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\n :root {\n /*\n These are fallback values, if the MCP client doesn't set them.\n See https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#theming\n */\n --color-text-primary: light-dark(black, white);\n --color-border-primary: light-dark(#ccc, #444);\n --color-background-secondary: light-dark(#f9f9f9, #1e1e1e);\n --font-sans:\n -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Oxygen-Sans', Ubuntu, Cantarell,\n 'Helvetica Neue', sans-serif;\n --font-heading-xs-size: 1rem;\n --font-heading-xs-line-height: 1.25;\n --border-width-regular: 1px;\n }\n .story-heading {\n font-family: var(--font-sans);\n font-size: var(--font-heading-xs-size);\n line-height: var(--font-heading-xs-line-height);\n color: var(--color-text-primary);\n padding: 0.5rem 0;\n }\n .story-iframe {\n background-color: white;\n border: var(--border-width-regular) solid var(--color-border-primary);\n }\n body:has(> article:only-child) h1 {\n /* if there is only one story rendered, hide its heading */\n display: none;\n }\n </style>\n <template id=\"preview-template\">\n <article>\n <h1 class=\"story-heading\"></h1>\n <iframe class=\"story-iframe\"></iframe>\n </article>\n </template>\n <script type=\"module\">\n // APP_SCRIPT_PLACEHOLDER\n <\/script>\n </head>\n <body></body>\n</html>\n";
|
|
171
258
|
|
|
172
|
-
//#endregion
|
|
173
|
-
//#region src/utils/slash.ts
|
|
174
|
-
/**
|
|
175
|
-
* Normalize paths to forward slashes for cross-platform compatibility
|
|
176
|
-
* Storybook import paths always use forward slashes
|
|
177
|
-
*/
|
|
178
|
-
function slash(path) {
|
|
179
|
-
return path.replace(/\\/g, "/");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
259
|
//#endregion
|
|
183
260
|
//#region src/tools/preview-stories.ts
|
|
184
261
|
const PREVIEW_STORIES_RESOURCE_URI = `ui://${PREVIEW_STORIES_TOOL_NAME}/preview.html`;
|
|
185
|
-
const PreviewStoriesInput = v.object({ stories: StoryInputArray
|
|
262
|
+
const PreviewStoriesInput = v.object({ stories: v.pipe(StoryInputArray, v.description(`Stories to preview.
|
|
263
|
+
Prefer { storyId } when you don't already have story file context, since this avoids filesystem discovery.
|
|
264
|
+
Use { storyId } when IDs were discovered from documentation tools.
|
|
265
|
+
Use { absoluteStoryPath + exportName } only when you're already working in a specific .stories.* file and already have that context.`)) });
|
|
186
266
|
const PreviewStoriesOutput = v.object({ stories: v.array(v.union([v.object({
|
|
187
267
|
title: v.string(),
|
|
188
268
|
name: v.string(),
|
|
189
|
-
previewUrl: v.string()
|
|
269
|
+
previewUrl: v.pipe(v.string(), v.description("Direct URL to open the story preview. Always include this URL in the final user-facing response so users can open it directly."))
|
|
190
270
|
}), v.object({
|
|
191
271
|
input: StoryInput,
|
|
192
272
|
error: v.string()
|
|
@@ -194,9 +274,6 @@ const PreviewStoriesOutput = v.object({ stories: v.array(v.union([v.object({
|
|
|
194
274
|
async function addPreviewStoriesTool(server) {
|
|
195
275
|
const previewStoryAppScript = await fs.readFile(url.fileURLToPath(import.meta.resolve("@storybook/addon-mcp/internal/preview-stories-app-script")), "utf-8");
|
|
196
276
|
const appHtml = preview_stories_app_template_default.replace("// APP_SCRIPT_PLACEHOLDER", previewStoryAppScript);
|
|
197
|
-
const normalizeImportPath = (importPath) => {
|
|
198
|
-
return slash(normalizeStoryPath(path.posix.normalize(slash(importPath))));
|
|
199
|
-
};
|
|
200
277
|
server.resource({
|
|
201
278
|
name: PREVIEW_STORIES_RESOURCE_URI,
|
|
202
279
|
description: "App resource for the Preview Stories tool",
|
|
@@ -222,8 +299,9 @@ async function addPreviewStoriesTool(server) {
|
|
|
222
299
|
});
|
|
223
300
|
server.tool({
|
|
224
301
|
name: PREVIEW_STORIES_TOOL_NAME,
|
|
225
|
-
title: "
|
|
226
|
-
description: `Use this tool to
|
|
302
|
+
title: "Get story preview URLs",
|
|
303
|
+
description: `Use this tool to get one or more Storybook preview URLs.
|
|
304
|
+
Always include each returned preview URL in your final user-facing response so users can open them directly.`,
|
|
227
305
|
schema: PreviewStoriesInput,
|
|
228
306
|
outputSchema: PreviewStoriesOutput,
|
|
229
307
|
enabled: () => server.ctx.custom?.toolsets?.dev ?? true,
|
|
@@ -233,45 +311,38 @@ async function addPreviewStoriesTool(server) {
|
|
|
233
311
|
const { origin, disableTelemetry } = server.ctx.custom ?? {};
|
|
234
312
|
if (!origin) throw new Error("Origin is required in addon context");
|
|
235
313
|
const index = await fetchStoryIndex(origin);
|
|
236
|
-
const
|
|
314
|
+
const resolvedStories = findStoryIds(index, input.stories);
|
|
237
315
|
const structuredResult = [];
|
|
238
316
|
const textResult = [];
|
|
239
|
-
for (const
|
|
240
|
-
|
|
241
|
-
const normalizedCwd = slash(process.cwd());
|
|
242
|
-
const normalizedAbsolutePath = slash(absoluteStoryPath);
|
|
243
|
-
const relativePath = normalizeImportPath(path.posix.relative(normalizedCwd, normalizedAbsolutePath));
|
|
244
|
-
logger.debug("Searching for:");
|
|
245
|
-
logger.debug({
|
|
246
|
-
exportName,
|
|
247
|
-
explicitStoryName,
|
|
248
|
-
absoluteStoryPath,
|
|
249
|
-
relativePath
|
|
250
|
-
});
|
|
251
|
-
const foundStory = entriesList.find((entry) => normalizeImportPath(entry.importPath) === relativePath && [explicitStoryName, storyNameFromExport(exportName)].includes(entry.name));
|
|
252
|
-
if (foundStory) {
|
|
253
|
-
logger.debug(`Found story ID: ${foundStory.id}`);
|
|
254
|
-
let previewUrl = `${origin}/?path=/story/${foundStory.id}`;
|
|
255
|
-
const argsParam = buildArgsParam(inputParams.props ?? {});
|
|
256
|
-
if (argsParam) previewUrl += `&args=${argsParam}`;
|
|
257
|
-
const globalsParam = buildArgsParam(inputParams.globals ?? {});
|
|
258
|
-
if (globalsParam) previewUrl += `&globals=${globalsParam}`;
|
|
317
|
+
for (const story of resolvedStories) {
|
|
318
|
+
if ("errorMessage" in story) {
|
|
259
319
|
structuredResult.push({
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
previewUrl
|
|
320
|
+
input: story.input,
|
|
321
|
+
error: story.errorMessage
|
|
263
322
|
});
|
|
264
|
-
textResult.push(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
323
|
+
textResult.push(story.errorMessage);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const indexEntry = index.entries[story.id];
|
|
327
|
+
if (!indexEntry) {
|
|
269
328
|
structuredResult.push({
|
|
270
|
-
input:
|
|
271
|
-
error:
|
|
329
|
+
input: story.input,
|
|
330
|
+
error: `No story found for story ID "${story.id}"`
|
|
272
331
|
});
|
|
273
|
-
textResult.push(
|
|
332
|
+
textResult.push(`No story found for story ID "${story.id}"`);
|
|
333
|
+
continue;
|
|
274
334
|
}
|
|
335
|
+
let previewUrl = `${origin}/?path=/story/${story.id}`;
|
|
336
|
+
const argsParam = buildArgsParam(story.input.props ?? {});
|
|
337
|
+
if (argsParam) previewUrl += `&args=${argsParam}`;
|
|
338
|
+
const globalsParam = buildArgsParam(story.input.globals ?? {});
|
|
339
|
+
if (globalsParam) previewUrl += `&globals=${globalsParam}`;
|
|
340
|
+
structuredResult.push({
|
|
341
|
+
title: indexEntry.title,
|
|
342
|
+
name: indexEntry.name,
|
|
343
|
+
previewUrl
|
|
344
|
+
});
|
|
345
|
+
textResult.push(previewUrl);
|
|
275
346
|
}
|
|
276
347
|
if (!disableTelemetry) await collectTelemetry({
|
|
277
348
|
event: "tool:previewStories",
|
|
@@ -293,53 +364,6 @@ async function addPreviewStoriesTool(server) {
|
|
|
293
364
|
});
|
|
294
365
|
}
|
|
295
366
|
|
|
296
|
-
//#endregion
|
|
297
|
-
//#region src/utils/find-story-ids.ts
|
|
298
|
-
/**
|
|
299
|
-
* Finds story IDs in the story index that match the given story inputs.
|
|
300
|
-
*
|
|
301
|
-
* @param index - The Storybook story index
|
|
302
|
-
* @param stories - Array of story inputs to search for
|
|
303
|
-
* @returns Object containing found stories with their IDs and not-found stories with error messages
|
|
304
|
-
*/
|
|
305
|
-
function findStoryIds(index, stories) {
|
|
306
|
-
const entriesList = Object.values(index.entries);
|
|
307
|
-
const result = {
|
|
308
|
-
found: [],
|
|
309
|
-
notFound: []
|
|
310
|
-
};
|
|
311
|
-
for (const storyInput of stories) {
|
|
312
|
-
const { exportName, explicitStoryName, absoluteStoryPath } = storyInput;
|
|
313
|
-
const normalizedCwd = slash(process.cwd());
|
|
314
|
-
const normalizedAbsolutePath = slash(absoluteStoryPath);
|
|
315
|
-
const relativePath = `./${path.posix.relative(normalizedCwd, normalizedAbsolutePath)}`;
|
|
316
|
-
logger.debug("Searching for:");
|
|
317
|
-
logger.debug({
|
|
318
|
-
exportName,
|
|
319
|
-
explicitStoryName,
|
|
320
|
-
absoluteStoryPath,
|
|
321
|
-
relativePath
|
|
322
|
-
});
|
|
323
|
-
const foundEntry = entriesList.find((entry) => entry.importPath === relativePath && [explicitStoryName, storyNameFromExport(exportName)].includes(entry.name));
|
|
324
|
-
if (foundEntry) {
|
|
325
|
-
logger.debug(`Found story ID: ${foundEntry.id}`);
|
|
326
|
-
result.found.push({
|
|
327
|
-
id: foundEntry.id,
|
|
328
|
-
input: storyInput
|
|
329
|
-
});
|
|
330
|
-
} else {
|
|
331
|
-
logger.debug("No story found");
|
|
332
|
-
let errorMessage = `No story found for export name "${exportName}" with absolute file path "${absoluteStoryPath}"`;
|
|
333
|
-
if (!explicitStoryName) errorMessage += ` (did you forget to pass the explicit story name?)`;
|
|
334
|
-
result.notFound.push({
|
|
335
|
-
input: storyInput,
|
|
336
|
-
errorMessage
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return result;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
367
|
//#endregion
|
|
344
368
|
//#region src/tools/run-story-tests.ts
|
|
345
369
|
/**
|
|
@@ -360,7 +384,10 @@ async function getAddonVitestConstants() {
|
|
|
360
384
|
const RunStoryTestsInput = v.object({
|
|
361
385
|
stories: v.optional(v.pipe(StoryInputArray, v.description(`Stories to test for focused feedback. Omit this field to run tests for all available stories.
|
|
362
386
|
Prefer running tests for specific stories while developing to get faster feedback,
|
|
363
|
-
and only omit this when you explicitly need to run all tests for comprehensive verification
|
|
387
|
+
and only omit this when you explicitly need to run all tests for comprehensive verification.
|
|
388
|
+
Prefer { storyId } when you don't already have story file context, since this avoids filesystem discovery.
|
|
389
|
+
Use { storyId } when IDs were discovered from documentation tools.
|
|
390
|
+
Use { absoluteStoryPath + exportName } only when you're currently working in a story file and already know those values.`))),
|
|
364
391
|
a11y: v.optional(v.pipe(v.boolean(), v.description("Whether to run accessibility tests. Defaults to true. Disable if you only need component test results.")), true)
|
|
365
392
|
});
|
|
366
393
|
/**
|
|
@@ -417,11 +444,11 @@ For visual/design accessibility violations (for example color contrast), ask the
|
|
|
417
444
|
let storyIds;
|
|
418
445
|
let inputStoryCount = 0;
|
|
419
446
|
if (input.stories) {
|
|
420
|
-
const
|
|
421
|
-
storyIds =
|
|
447
|
+
const resolvedStories = findStoryIds(await fetchStoryIndex(origin), input.stories);
|
|
448
|
+
storyIds = resolvedStories.filter((story) => "id" in story).map((story) => story.id);
|
|
422
449
|
inputStoryCount = input.stories.length;
|
|
423
450
|
if (storyIds.length === 0) {
|
|
424
|
-
const errorMessages =
|
|
451
|
+
const errorMessages = resolvedStories.filter((story) => "errorMessage" in story).map((story) => story.errorMessage).join("\n");
|
|
425
452
|
if (!disableTelemetry) await collectTelemetry({
|
|
426
453
|
event: "tool:runStoryTests",
|
|
427
454
|
server,
|
|
@@ -742,7 +769,7 @@ const getManifestStatus = async (options) => {
|
|
|
742
769
|
options.presets.apply("experimental_componentManifestGenerator")
|
|
743
770
|
]);
|
|
744
771
|
const hasManifests = !!manifests || !!legacyComponentManifestGenerator;
|
|
745
|
-
const hasFeatureFlag = !!features?.experimentalComponentsManifest;
|
|
772
|
+
const hasFeatureFlag = !!(features?.componentsManifest ?? features?.experimentalComponentsManifest);
|
|
746
773
|
return {
|
|
747
774
|
available: hasFeatureFlag && hasManifests,
|
|
748
775
|
hasManifests,
|
|
@@ -839,6 +866,7 @@ const initializeMCPServer = async (options, multiSource) => {
|
|
|
839
866
|
const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true;
|
|
840
867
|
await addListAllDocumentationTool(server, contextAwareEnabled);
|
|
841
868
|
await addGetDocumentationTool(server, contextAwareEnabled, { multiSource });
|
|
869
|
+
await addGetStoryDocumentationTool(server, contextAwareEnabled, { multiSource });
|
|
842
870
|
}
|
|
843
871
|
transport = new HttpTransport(server, { path: null });
|
|
844
872
|
origin = `http://localhost:${options.port}`;
|
|
@@ -951,7 +979,7 @@ function getToolsets(request, addonOptions) {
|
|
|
951
979
|
|
|
952
980
|
//#endregion
|
|
953
981
|
//#region src/template.html
|
|
954
|
-
var template_default = "<!doctype html>\n<html>\n <head>\n
|
|
982
|
+
var template_default = "<!doctype html>\n<html>\n <head>\n <style>\n @font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');\n }\n\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n html,\n body {\n height: 100%;\n font-family:\n 'Nunito Sans',\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n Oxygen,\n Ubuntu,\n Cantarell,\n sans-serif;\n }\n\n body {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n text-align: center;\n padding: 2rem;\n background-color: #ffffff;\n color: rgb(46, 52, 56);\n line-height: 1.6;\n }\n\n p {\n margin-bottom: 1rem;\n }\n\n code {\n font-family: 'Monaco', 'Courier New', monospace;\n background: #f5f5f5;\n padding: 0.2em 0.4em;\n border-radius: 3px;\n }\n\n a {\n color: #1ea7fd;\n }\n\n .container {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n .toolsets {\n margin: 1.5rem 0;\n text-align: left;\n max-width: 500px;\n }\n\n .toolsets h3 {\n font-size: 1rem;\n margin-bottom: 0.75rem;\n text-align: center;\n }\n\n .toolset {\n margin-bottom: 1rem;\n padding: 0.75rem 1rem;\n border-radius: 6px;\n background: #f8f9fa;\n border: 1px solid #e9ecef;\n }\n\n .toolset-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-weight: 600;\n margin-bottom: 0.5rem;\n }\n\n .toolset-status {\n display: inline-block;\n padding: 0.15em 0.5em;\n border-radius: 3px;\n font-size: 0.75rem;\n font-weight: 500;\n text-transform: uppercase;\n }\n\n .toolset-status.enabled {\n background: #d4edda;\n color: #155724;\n }\n\n .toolset-status.disabled {\n background: #f8d7da;\n color: #721c24;\n }\n\n .toolset-tools {\n font-size: 0.875rem;\n color: #6c757d;\n padding-left: 1.5rem;\n margin: 0;\n }\n\n .toolset-tools li {\n margin-bottom: 0.25rem;\n }\n\n .toolset-tools code {\n font-size: 0.8rem;\n }\n\n .toolset-notice {\n font-size: 0.8rem;\n color: #856404;\n background: #fff3cd;\n padding: 0.5rem;\n border-radius: 4px;\n margin-top: 0.5rem;\n }\n\n .toolset-notice a {\n color: #533f03;\n }\n\n @media (prefers-color-scheme: dark) {\n body {\n background-color: rgb(34, 36, 37);\n color: rgb(201, 205, 207);\n }\n\n code {\n background: rgba(255, 255, 255, 0.1);\n }\n\n .toolset {\n background: rgba(255, 255, 255, 0.05);\n border-color: rgba(255, 255, 255, 0.1);\n }\n\n .toolset-tools {\n color: #adb5bd;\n }\n\n .toolset-status.enabled {\n background: rgba(40, 167, 69, 0.2);\n color: #75d67e;\n }\n\n .toolset-status.disabled {\n background: rgba(220, 53, 69, 0.2);\n color: #f5a6ad;\n }\n\n .toolset-notice {\n background: rgba(255, 193, 7, 0.15);\n color: #ffc107;\n }\n\n .toolset-notice a {\n color: #ffe066;\n }\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <p>\n Storybook MCP server successfully running via\n <code>@storybook/addon-mcp</code>.\n </p>\n <p>\n See how to connect to it from your coding agent in\n <a\n target=\"_blank\"\n href=\"https://github.com/storybookjs/mcp/tree/main/packages/addon-mcp#configuring-your-agent\"\n >the addon's README</a\n >.\n </p>\n\n <div class=\"toolsets\">\n <h3>Available Toolsets</h3>\n\n <div class=\"toolset\">\n <div class=\"toolset-header\">\n <span>dev</span>\n <span class=\"toolset-status {{DEV_STATUS}}\">{{DEV_STATUS}}</span>\n </div>\n <ul class=\"toolset-tools\">\n <li><code>preview-stories</code></li>\n <li><code>get-storybook-story-instructions</code></li>\n </ul>\n </div>\n\n <div class=\"toolset\">\n <div class=\"toolset-header\">\n <span>docs</span>\n <span class=\"toolset-status {{DOCS_STATUS}}\">{{DOCS_STATUS}}</span>\n </div>\n <ul class=\"toolset-tools\">\n <li><code>list-all-documentation</code></li>\n <li><code>get-documentation</code></li>\n </ul>\n {{DOCS_NOTICE}}\n </div>\n\n <div class=\"toolset\">\n <div class=\"toolset-header\">\n <span>test</span>\n <span class=\"toolset-status {{TEST_STATUS}}\">{{TEST_STATUS}}</span>\n </div>\n <ul class=\"toolset-tools\">\n <li><code>run-story-tests</code>{{A11Y_BADGE}}</li>\n </ul>\n {{TEST_NOTICE}}\n </div>\n </div>\n\n {{MANIFEST_DEBUGGER_LINK}}\n </div>\n </body>\n</html>\n";
|
|
955
983
|
|
|
956
984
|
//#endregion
|
|
957
985
|
//#region src/auth/composition-auth.ts
|
|
@@ -1273,10 +1301,10 @@ const experimental_devServer = async (app, options) => {
|
|
|
1273
1301
|
This toolset requires enabling the experimental component manifest feature.
|
|
1274
1302
|
<a target="_blank" href="https://github.com/storybookjs/mcp/tree/main/packages/addon-mcp#docs-tools-experimental">Learn how to enable it</a>
|
|
1275
1303
|
</div>`;
|
|
1276
|
-
const testNoticeLines = [!addonVitestConstants && `This toolset requires <code>@storybook/addon-vitest</code>. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/test-addon">Learn how to set it up</a>`, !a11yEnabled && `Add <code>@storybook/addon-a11y</code> for accessibility testing. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/accessibility-testing">Learn more</a>`].filter(Boolean);
|
|
1304
|
+
const testNoticeLines = [!addonVitestConstants && `This toolset requires Storybook 10.3.0+ with <code>@storybook/addon-vitest</code>. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/test-addon">Learn how to set it up</a>`, !a11yEnabled && `Add <code>@storybook/addon-a11y</code> for accessibility testing. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/accessibility-testing">Learn more</a>`].filter(Boolean);
|
|
1277
1305
|
const testNotice = testNoticeLines.length ? `<div class="toolset-notice">${testNoticeLines.join("<br>")}</div>` : "";
|
|
1278
1306
|
const a11yBadge = a11yEnabled ? " <span class=\"toolset-status enabled\">+ accessibility</span>" : "";
|
|
1279
|
-
const html = template_default.
|
|
1307
|
+
const html = template_default.replaceAll("{{DEV_STATUS}}", isDevEnabled ? "enabled" : "disabled").replaceAll("{{DOCS_STATUS}}", isDocsEnabled ? "enabled" : "disabled").replace("{{DOCS_NOTICE}}", docsNotice).replaceAll("{{TEST_STATUS}}", isTestEnabled ? "enabled" : "disabled").replace("{{TEST_NOTICE}}", testNotice).replace("{{MANIFEST_DEBUGGER_LINK}}", manifestStatus.available ? "<p>View the <a href=\"/manifests/components.html\">component manifest debugger</a>.</p>" : "").replace("{{A11Y_BADGE}}", a11yBadge);
|
|
1280
1308
|
res.end(html);
|
|
1281
1309
|
});
|
|
1282
1310
|
return app;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storybook/addon-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Help agents automatically write and test stories for your UI components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"picoquery": "^2.5.0",
|
|
35
35
|
"tmcp": "^1.16.0",
|
|
36
36
|
"valibot": "1.2.0",
|
|
37
|
-
"@storybook/mcp": "0.
|
|
37
|
+
"@storybook/mcp": "0.5.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@storybook/addon-a11y": "10.3.0-alpha.
|
|
41
|
-
"@storybook/addon-vitest": "10.3.0-alpha.
|
|
42
|
-
"storybook": "10.3.0-alpha.
|
|
40
|
+
"@storybook/addon-a11y": "10.3.0-alpha.12",
|
|
41
|
+
"@storybook/addon-vitest": "10.3.0-alpha.12",
|
|
42
|
+
"storybook": "10.3.0-alpha.12"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@storybook/addon-vitest": "^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0",
|