@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 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, requires experimental feature flag below 👇)
51
+ docs: true, // Tools for component manifest and documentation (default: true)
52
52
  },
53
53
  },
54
54
  },
55
55
  ],
56
- features: {
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-experimental)
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
- - `absoluteStoryPath`: Absolute path to the story file
160
- - `exportName`: The export name of the story
161
- - `explicitStoryName`: Optional explicit story name
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 (Experimental)
173
+ ### Docs Tools
173
174
 
174
- These additional tools are available when the **experimental** component manifest feature is enabled. They provide agents with detailed documentation about your UI components.
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.experimentalComponentsManifest` set to `true` in `.storybook/main.js`
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
- experimentalComponentsManifest: true,
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.2";
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: "Preview stories",
226
- description: `Use this tool to preview one or more stories, rendering them as an MCP App using the UI Resource or returning the raw URL for users to visit.`,
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 entriesList = Object.values(index.entries);
314
+ const resolvedStories = findStoryIds(index, input.stories);
237
315
  const structuredResult = [];
238
316
  const textResult = [];
239
- for (const inputParams of input.stories) {
240
- const { exportName, explicitStoryName, absoluteStoryPath } = inputParams;
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
- title: foundStory.title,
261
- name: foundStory.name,
262
- previewUrl
320
+ input: story.input,
321
+ error: story.errorMessage
263
322
  });
264
- textResult.push(previewUrl);
265
- } else {
266
- logger.debug("No story found");
267
- let errorMessage = `No story found for export name "${exportName}" with absolute file path "${absoluteStoryPath}"`;
268
- if (!explicitStoryName) errorMessage += ` (did you forget to pass the explicit story name?)`;
323
+ textResult.push(story.errorMessage);
324
+ continue;
325
+ }
326
+ const indexEntry = index.entries[story.id];
327
+ if (!indexEntry) {
269
328
  structuredResult.push({
270
- input: inputParams,
271
- error: errorMessage
329
+ input: story.input,
330
+ error: `No story found for story ID "${story.id}"`
272
331
  });
273
- textResult.push(errorMessage);
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 { found, notFound } = findStoryIds(await fetchStoryIndex(origin), input.stories);
421
- storyIds = found.map((story) => story.id);
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 = notFound.map((story) => story.errorMessage).join("\n");
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 {{REDIRECT_META}}\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 <p id=\"redirect-message\">\n Automatically redirecting to\n <a href=\"/manifests/components.html\">component manifest</a>\n in <span id=\"countdown\">10</span> seconds...\n </p>\n </div>\n <script>\n let countdown = 10;\n const countdownElement = document.getElementById('countdown');\n if (countdownElement) {\n setInterval(() => {\n countdown -= 1;\n countdownElement.textContent = countdown.toString();\n }, 1000);\n }\n <\/script>\n </body>\n</html>\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.replace("{{REDIRECT_META}}", manifestStatus.available ? "<meta http-equiv=\"refresh\" content=\"10;url=/manifests/components.html\" />" : "<style>#redirect-message { display: none; }</style>").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("{{A11Y_BADGE}}", a11yBadge);
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;
@@ -4,7 +4,7 @@ const MCP_APP_SIZE_CHANGED_EVENT = "storybook-mcp:size-changed";
4
4
 
5
5
  //#endregion
6
6
  //#region package.json
7
- var version = "0.3.2";
7
+ var version = "0.3.4";
8
8
 
9
9
  //#endregion
10
10
  //#region src/tools/preview-stories/preview-stories-app-script.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/addon-mcp",
3
- "version": "0.3.2",
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.4.1"
37
+ "@storybook/mcp": "0.5.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@storybook/addon-a11y": "10.3.0-alpha.8",
41
- "@storybook/addon-vitest": "10.3.0-alpha.8",
42
- "storybook": "10.3.0-alpha.8"
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",