@storybook/addon-mcp 0.1.4-next.2 → 0.1.5

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.
Files changed (3) hide show
  1. package/README.md +8 -4
  2. package/dist/preset.js +71 -43
  3. package/package.json +7 -16
package/README.md CHANGED
@@ -7,7 +7,7 @@ It enables a workflow where for each UI component created, the agent will automa
7
7
  The addon provides tools to improve agents' UI development capabilities, retrieve story URLs, and access component documentation.
8
8
 
9
9
  <div align="center">
10
- <img src="https://storybook.js.org/embed/addon-mcp-claude-code-showcase.gif" alt="Storybook MCP Addon Demo" />
10
+ <img src="./addon-mcp-claude-code-showcase.gif" alt="Storybook MCP Addon Demo" />
11
11
  </div>
12
12
 
13
13
  ## Getting Started
@@ -48,11 +48,15 @@ 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)
51
+ docs: true, // Tools for component manifest and documentation (default: true, requires experimental feature flag below 👇)
52
52
  },
53
+ experimentalFormat: 'markdown', // Output format: 'markdown' (default) or 'xml'
53
54
  },
54
55
  },
55
56
  ],
57
+ features: {
58
+ experimentalComponentsManifest: true, // Enable manifest generation for the docs toolset, only supported in React-based setups.
59
+ },
56
60
  };
57
61
  ```
58
62
 
@@ -193,12 +197,12 @@ Returns a list of all available UI components in your component library. Useful
193
197
 
194
198
  #### 4. Get Component Documentation (`get-component-documentation`)
195
199
 
196
- Retrieves detailed documentation for specific components, including:
200
+ Retrieves detailed documentation for a specific component, including:
197
201
 
198
202
  - Component documentation
199
203
  - Usage examples
200
204
 
201
- The agent provides component IDs to retrieve their documentation.
205
+ The agent provides a component ID to retrieve its documentation. To get documentation for multiple components, call this tool multiple times.
202
206
 
203
207
  ## Contributing
204
208
 
package/dist/preset.js CHANGED
@@ -11,12 +11,12 @@ import { buffer } from "node:stream/consumers";
11
11
 
12
12
  //#region package.json
13
13
  var name = "@storybook/addon-mcp";
14
- var version = "0.1.4-next.2";
14
+ var version = "0.1.5";
15
15
  var description = "Help agents automatically write and test stories for your UI components";
16
16
 
17
17
  //#endregion
18
18
  //#region src/telemetry.ts
19
- async function collectTelemetry({ event, server,...payload }) {
19
+ async function collectTelemetry({ event, server, ...payload }) {
20
20
  try {
21
21
  return await telemetry("addon-mcp", {
22
22
  event,
@@ -26,7 +26,7 @@ async function collectTelemetry({ event, server,...payload }) {
26
26
  ...payload
27
27
  });
28
28
  } catch (error) {
29
- logger.debug("Error collecting telemetry:", error);
29
+ logger.debug(`Error collecting telemetry: ${error}`);
30
30
  }
31
31
  }
32
32
 
@@ -41,11 +41,11 @@ async function collectTelemetry({ event, server,...payload }) {
41
41
  */
42
42
  async function fetchStoryIndex(origin$1) {
43
43
  const indexUrl = `${origin$1}/index.json`;
44
- logger.debug("Fetching story index from:", indexUrl);
44
+ logger.debug(`Fetching story index from: ${indexUrl}`);
45
45
  const response = await fetch(indexUrl);
46
46
  if (!response.ok) throw new Error(`Failed to fetch story index: ${response.status} ${response.statusText}`);
47
47
  const index = await response.json();
48
- logger.debug("Story index entries found:", Object.keys(index.entries).length);
48
+ logger.debug(`Story index entries found: ${Object.keys(index.entries).length}`);
49
49
  return index;
50
50
  }
51
51
 
@@ -69,13 +69,16 @@ const errorToMCPContent = (error) => {
69
69
 
70
70
  //#endregion
71
71
  //#region src/types.ts
72
- const AddonOptions = v.object({ toolsets: v.optional(v.object({
73
- dev: v.exactOptional(v.boolean(), true),
74
- docs: v.exactOptional(v.boolean(), true)
75
- }), {
76
- dev: true,
77
- docs: true
78
- }) });
72
+ const AddonOptions = v.object({
73
+ toolsets: v.optional(v.object({
74
+ dev: v.exactOptional(v.boolean(), true),
75
+ docs: v.exactOptional(v.boolean(), true)
76
+ }), {
77
+ dev: true,
78
+ docs: true
79
+ }),
80
+ experimentalFormat: v.optional(v.picklist(["xml", "markdown"]), "markdown")
81
+ });
79
82
  /**
80
83
  * Schema for a single story input when requesting story URLs.
81
84
  */
@@ -106,7 +109,7 @@ async function addGetStoryUrlsTool(server) {
106
109
  enabled: () => server.ctx.custom?.toolsets?.dev ?? true
107
110
  }, async (input) => {
108
111
  try {
109
- const { origin: origin$1, disableTelemetry } = server.ctx.custom ?? {};
112
+ const { origin: origin$1, disableTelemetry: disableTelemetry$1 } = server.ctx.custom ?? {};
110
113
  if (!origin$1) throw new Error("Origin is required in addon context");
111
114
  const index = await fetchStoryIndex(origin$1);
112
115
  const entriesList = Object.values(index.entries);
@@ -123,7 +126,7 @@ async function addGetStoryUrlsTool(server) {
123
126
  });
124
127
  const foundStoryId = entriesList.find((entry) => entry.importPath === relativePath && [explicitStoryName, storyNameFromExport(exportName)].includes(entry.name))?.id;
125
128
  if (foundStoryId) {
126
- logger.debug("Found story ID:", foundStoryId);
129
+ logger.debug(`Found story ID: ${foundStoryId}`);
127
130
  result.push(`${origin$1}/?path=/story/${foundStoryId}`);
128
131
  foundStoryCount++;
129
132
  } else {
@@ -133,9 +136,10 @@ async function addGetStoryUrlsTool(server) {
133
136
  result.push(errorMessage);
134
137
  }
135
138
  }
136
- if (!disableTelemetry) await collectTelemetry({
139
+ if (!disableTelemetry$1) await collectTelemetry({
137
140
  event: "tool:getStoryUrls",
138
141
  server,
142
+ toolset: "dev",
139
143
  inputStoryCount: input.stories.length,
140
144
  outputStoryCount: foundStoryCount
141
145
  });
@@ -151,7 +155,7 @@ async function addGetStoryUrlsTool(server) {
151
155
 
152
156
  //#endregion
153
157
  //#region src/ui-building-instructions.md
154
- var ui_building_instructions_default = "# Writing User Interfaces\n\nWhen writing UI, prefer breaking larger components up into smaller parts.\n\nALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.\n\n## Storybook 9 Essential Changes for Story Writing\n\n### Package Consolidation\n\n#### `Meta` and `StoryObj` imports\n\nUpdate story imports to use the framework package:\n\n```diff\n- import { Meta, StoryObj } from '{{RENDERER}}';\n+ import { Meta, StoryObj } from '{{FRAMEWORK}}';\n```\n\n#### Test utility imports\n\nUpdate test imports to use `storybook/test` instead of `@storybook/test`\n\n```diff\n- import { fn } from '@storybook/test';\n+ import { fn } from 'storybook/test';\n```\n\n### Global State Changes\n\nThe `globals` annotation has be renamed to `initialGlobals`:\n\n```diff\n// .storybook/preview.js\nexport default {\n- globals: { theme: 'light' }\n+ initialGlobals: { theme: 'light' }\n};\n```\n\n### Autodocs Configuration\n\nInstead of `parameters.docs.autodocs` in main.js, use tags:\n\n```js\n// .storybook/preview.js or in individual stories\nexport default {\n tags: ['autodocs'], // generates autodocs for all stories\n};\n```\n\n### Mocking imports in Storybook\n\nTo mock imports in Storybook, use Storybook's mocking features. ALWAYS mock external dependencies to ensure stories render consistently.\n\n1. **Register in the mock in Storybook's preview file**:\n To mock dependendencies, you MUST register a module mock in `.storybook/preview.ts` (or equivalent):\n\n```js\nimport { sb } from 'storybook/test';\n\n// Full mock (replaces with empty fn functions)\nsb.mock(import('some-library'));\n```\n\n2. **Specify mock values in stories**:\n You can override the behaviour of the mocks per-story using `beforeEach` and the `mocked()` type function:\n\n```js\nimport { expect, mocked, fn } from 'storybook/test';\nimport { library } from 'some-library';\n\nconst meta = {\n component: AuthButton,\n beforeEach: async () => {\n mocked(library).mockResolvedValue({ user: 'data' });\n },\n};\n\nexport const LoggedIn: Story = {\n play: async ({ canvas }) => {\n await expect(library).toHaveBeenCalled();\n },\n};\n```\n\nBefore doing this ensure you have mocked the import in the preview file.\n\n### Key Requirements\n\n- **Node.js 20+**, **TypeScript 4.9+**, **Vite 5+**\n- React Native uses `.rnstorybook` directory\n- `afterEach` hook is now stable (was `experimental_afterEach`)\n\n## Story Linking Agent Behavior\n\n- ALWAYS provide story links after any changes to stories files, including changes to existing stories.\n- After changing any UI components, ALWAYS search for related stories that might cover the changes you've made. If you find any, provide the story links to the user. THIS IS VERY IMPORTANT, as it allows the user to visually inspect the changes you've made. Even later in a session when changing UI components or stories that have already been linked to previously, YOU MUST PROVIDE THE LINKS AGAIN.\n- Use the {{GET_STORY_URLS_TOOL_NAME}} tool to get the correct URLs for links to stories.\n";
158
+ var ui_building_instructions_default = "# Writing User Interfaces\n\nWhen writing UI, prefer breaking larger components up into smaller parts.\n\nALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.\n\n## Storybook 9 Essential Changes for Story Writing\n\n### Package Consolidation\n\n#### `Meta` and `StoryObj` imports\n\nUpdate story imports to use the framework package:\n\n```diff\n- import { Meta, StoryObj } from '{{RENDERER}}';\n+ import { Meta, StoryObj } from '{{FRAMEWORK}}';\n```\n\n#### Test utility imports\n\nUpdate test imports to use `storybook/test` instead of `@storybook/test`\n\n```diff\n- import { fn } from '@storybook/test';\n+ import { fn } from 'storybook/test';\n```\n\n### Global State Changes\n\nThe `globals` annotation has be renamed to `initialGlobals`:\n\n```diff\n// .storybook/preview.js\nexport default {\n- globals: { theme: 'light' }\n+ initialGlobals: { theme: 'light' }\n};\n```\n\n### Autodocs Configuration\n\nInstead of `parameters.docs.autodocs` in main.js, use tags:\n\n```js\n// .storybook/preview.js or in individual stories\nexport default {\n tags: ['autodocs'], // generates autodocs for all stories\n};\n```\n\n### Mocking imports in Storybook\n\nTo mock imports in Storybook, use Storybook's mocking features. ALWAYS mock external dependencies to ensure stories render consistently.\n\n1. **Register in the mock in Storybook's preview file**:\n To mock dependendencies, you MUST register a module mock in `.storybook/preview.ts` (or equivalent):\n\n```js\nimport { sb } from 'storybook/test';\n\n// Prefer spy mocks (keeps functions, but allows to override them and spy on them)\nsb.mock(import('some-library'), { spy: true });\n```\n\n2. **Specify mock values in stories**:\n You can override the behaviour of the mocks per-story using `beforeEach` and the `mocked()` type function:\n\n```js\nimport { expect, mocked, fn } from 'storybook/test';\nimport { library } from 'some-library';\n\nconst meta = {\n component: AuthButton,\n beforeEach: async () => {\n mocked(library).mockResolvedValue({ user: 'data' });\n },\n};\n\nexport const LoggedIn: Story = {\n play: async ({ canvas }) => {\n await expect(library).toHaveBeenCalled();\n },\n};\n```\n\nBefore doing this ensure you have mocked the import in the preview file.\n\n### Key Requirements\n\n- **Node.js 20+**, **TypeScript 4.9+**, **Vite 5+**\n- React Native uses `.rnstorybook` directory\n\n## Story Linking Agent Behavior\n\n- ALWAYS provide story links after any changes to stories files, including changes to existing stories.\n- After changing any UI components, ALWAYS search for related stories that might cover the changes you've made. If you find any, provide the story links to the user. THIS IS VERY IMPORTANT, as it allows the user to visually inspect the changes you've made. Even later in a session when changing UI components or stories that have already been linked to previously, YOU MUST PROVIDE THE LINKS AGAIN.\n- Use the {{GET_STORY_URLS_TOOL_NAME}} tool to get the correct URLs for links to stories.\n";
155
159
 
156
160
  //#endregion
157
161
  //#region src/tools/get-ui-building-instructions.ts
@@ -167,11 +171,12 @@ async function addGetUIBuildingInstructionsTool(server) {
167
171
  enabled: () => server.ctx.custom?.toolsets?.dev ?? true
168
172
  }, async () => {
169
173
  try {
170
- const { options, disableTelemetry } = server.ctx.custom ?? {};
174
+ const { options, disableTelemetry: disableTelemetry$1 } = server.ctx.custom ?? {};
171
175
  if (!options) throw new Error("Options are required in addon context");
172
- if (!disableTelemetry) await collectTelemetry({
176
+ if (!disableTelemetry$1) await collectTelemetry({
173
177
  event: "tool:getUIBuildingInstructions",
174
- server
178
+ server,
179
+ toolset: "dev"
175
180
  });
176
181
  const frameworkPreset = await options.presets.apply("framework");
177
182
  const framework = typeof frameworkPreset === "string" ? frameworkPreset : frameworkPreset?.name;
@@ -203,9 +208,15 @@ const frameworkToRendererMap = {
203
208
 
204
209
  //#endregion
205
210
  //#region src/tools/is-manifest-available.ts
206
- const isManifestAvailable = async (options) => {
211
+ const getManifestStatus = async (options) => {
207
212
  const [features, componentManifestGenerator] = await Promise.all([options.presets.apply("features"), options.presets.apply("experimental_componentManifestGenerator")]);
208
- return features.experimentalComponentsManifest && componentManifestGenerator;
213
+ const hasGenerator = !!componentManifestGenerator;
214
+ const hasFeatureFlag = !!features?.experimentalComponentsManifest;
215
+ return {
216
+ available: hasFeatureFlag && hasGenerator,
217
+ hasGenerator,
218
+ hasFeatureFlag
219
+ };
209
220
  };
210
221
 
211
222
  //#endregion
@@ -213,7 +224,9 @@ const isManifestAvailable = async (options) => {
213
224
  let transport;
214
225
  let origin;
215
226
  let initialize;
227
+ let disableTelemetry;
216
228
  const initializeMCPServer = async (options) => {
229
+ disableTelemetry = (await options.presets.apply("core", {}))?.disableTelemetry ?? false;
217
230
  const server = new McpServer({
218
231
  name,
219
232
  version,
@@ -222,15 +235,15 @@ const initializeMCPServer = async (options) => {
222
235
  adapter: new ValibotJsonSchemaAdapter(),
223
236
  capabilities: { tools: { listChanged: true } }
224
237
  }).withContext();
225
- server.on("initialize", async () => {
226
- if (!options.disableTelemetry) await collectTelemetry({
238
+ if (!disableTelemetry) server.on("initialize", async () => {
239
+ await collectTelemetry({
227
240
  event: "session:initialized",
228
241
  server
229
242
  });
230
243
  });
231
244
  await addGetStoryUrlsTool(server);
232
245
  await addGetUIBuildingInstructionsTool(server);
233
- if (await isManifestAvailable(options)) {
246
+ if ((await getManifestStatus(options)).available) {
234
247
  logger.info("Experimental components manifest feature detected - registering component tools");
235
248
  const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true;
236
249
  await addListAllComponentsTool(server, contextAwareEnabled);
@@ -238,35 +251,36 @@ const initializeMCPServer = async (options) => {
238
251
  }
239
252
  transport = new HttpTransport(server, { path: null });
240
253
  origin = `http://localhost:${options.port}`;
241
- logger.debug("MCP server origin:", origin);
254
+ logger.debug(`MCP server origin: ${origin}`);
242
255
  return server;
243
256
  };
244
257
  const mcpServerHandler = async ({ req, res, options, addonOptions }) => {
245
- const disableTelemetry = options.disableTelemetry ?? false;
246
258
  if (!initialize) initialize = initializeMCPServer(options);
247
259
  const server = await initialize;
248
260
  const webRequest = await incomingMessageToWebRequest(req);
249
261
  const addonContext = {
250
262
  options,
251
263
  toolsets: getToolsets(webRequest, addonOptions),
264
+ format: addonOptions.experimentalFormat,
252
265
  origin,
253
266
  disableTelemetry,
254
- source: `${origin}/manifests/components.json`,
267
+ request: webRequest,
255
268
  ...!disableTelemetry && {
256
269
  onListAllComponents: async ({ manifest }) => {
257
270
  await collectTelemetry({
258
271
  event: "tool:listAllComponents",
259
272
  server,
273
+ toolset: "docs",
260
274
  componentCount: Object.keys(manifest.components).length
261
275
  });
262
276
  },
263
- onGetComponentDocumentation: async ({ input, foundComponents, notFoundIds }) => {
277
+ onGetComponentDocumentation: async ({ input, foundComponent }) => {
264
278
  await collectTelemetry({
265
279
  event: "tool:getComponentDocumentation",
266
280
  server,
267
- inputComponentCount: input.componentIds.length,
268
- foundCount: foundComponents.length,
269
- notFoundCount: notFoundIds.length
281
+ toolset: "docs",
282
+ componentId: input.componentId,
283
+ found: !!foundComponent
270
284
  });
271
285
  }
272
286
  }
@@ -327,28 +341,42 @@ function getToolsets(request, addonOptions) {
327
341
 
328
342
  //#endregion
329
343
  //#region src/template.html
330
- 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 @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 </style>\n </head>\n <body>\n <div>\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 <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";
344
+ 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>get-story-urls</code></li>\n <li><code>get-ui-building-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-components</code></li>\n <li><code>get-component-documentation</code></li>\n </ul>\n {{DOCS_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";
331
345
 
332
346
  //#endregion
333
347
  //#region src/preset.ts
334
348
  const experimental_devServer = async (app, options) => {
335
- const addonOptions = v.parse(AddonOptions, { toolsets: options.toolsets ?? {} });
349
+ const addonOptions = v.parse(AddonOptions, {
350
+ toolsets: "toolsets" in options ? options.toolsets : {},
351
+ experimentalFormat: "experimentalFormat" in options ? options.experimentalFormat : "markdown"
352
+ });
336
353
  app.post("/mcp", (req, res) => mcpServerHandler({
337
354
  req,
338
355
  res,
339
356
  options,
340
357
  addonOptions
341
358
  }));
342
- const shouldRedirect = await isManifestAvailable(options);
343
- app.get("/mcp", async (req, res) => {
344
- if ((req.headers["accept"] || "").includes("text/html")) {
345
- res.writeHead(200, { "Content-Type": "text/html" });
346
- const html = template_default.replace("{{REDIRECT_META}}", shouldRedirect ? "<meta http-equiv=\"refresh\" content=\"10;url=/manifests/components.html\" />" : "<style>#redirect-message { display: none; }</style>");
347
- res.end(html);
348
- } else {
349
- res.writeHead(200, { "Content-Type": "text/plain" });
350
- res.end("Storybook MCP server successfully running via @storybook/addon-mcp");
351
- }
359
+ const manifestStatus = await getManifestStatus(options);
360
+ const isDevEnabled = addonOptions.toolsets?.dev ?? true;
361
+ const isDocsEnabled = manifestStatus.available && (addonOptions.toolsets?.docs ?? true);
362
+ app.get("/mcp", (req, res) => {
363
+ if (!req.headers["accept"]?.includes("text/html")) return mcpServerHandler({
364
+ req,
365
+ res,
366
+ options,
367
+ addonOptions
368
+ });
369
+ res.writeHead(200, { "Content-Type": "text/html" });
370
+ let docsNotice = "";
371
+ if (!manifestStatus.hasGenerator) docsNotice = `<div class="toolset-notice">
372
+ This toolset is only supported in React-based setups.
373
+ </div>`;
374
+ else if (!manifestStatus.hasFeatureFlag) docsNotice = `<div class="toolset-notice">
375
+ This toolset requires enabling the experimental component manifest feature.
376
+ <a target="_blank" href="https://github.com/storybookjs/mcp/tree/main/packages/addon-mcp#docs-tools-experimental">Learn how to enable it</a>
377
+ </div>`;
378
+ 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);
379
+ res.end(html);
352
380
  });
353
381
  return app;
354
382
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/addon-mcp",
3
- "version": "0.1.4-next.2",
3
+ "version": "0.1.5",
4
4
  "description": "Help agents automatically write and test stories for your UI components",
5
5
  "keywords": [
6
6
  "storybook-addon",
@@ -28,19 +28,11 @@
28
28
  "@tmcp/adapter-valibot": "^0.1.4",
29
29
  "@tmcp/transport-http": "^0.8.0",
30
30
  "tmcp": "^1.16.0",
31
- "valibot": "^1.1.0",
32
- "@storybook/mcp": "0.0.7-next.0"
31
+ "valibot": "1.1.0",
32
+ "@storybook/mcp": "0.1.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "20.19.0",
36
- "@vitest/coverage-v8": "3.2.4",
37
- "publint": "0.3.15",
38
- "storybook": "^10.1.0-alpha.2",
39
- "ts-dedent": "^2.2.0",
40
- "tsdown": "^0.15.12",
41
- "typescript": "^5.9.3",
42
- "vite": "^7.0.5",
43
- "vitest": "3.2.4"
35
+ "storybook": "10.1.2"
44
36
  },
45
37
  "peerDependencies": {
46
38
  "storybook": "^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0"
@@ -69,11 +61,10 @@
69
61
  },
70
62
  "scripts": {
71
63
  "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
72
- "build": "tsdown --config ../../tsdown.config.ts",
73
- "dev": "pnpm run build -- --watch",
64
+ "build": "tsdown",
74
65
  "inspect": "mcp-inspector --config ../../.mcp.inspect.json --server storybook-addon-mcp",
75
66
  "publint": "publint",
76
- "test": "vitest",
77
- "typecheck": "tsc --noEmit"
67
+ "test": "cd ../.. && pnpm vitest --project=@storybook/addon-mcp",
68
+ "typecheck": "tsc"
78
69
  }
79
70
  }