@storybook/addon-mcp 0.1.1 → 0.1.3

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 +48 -2
  2. package/dist/preset.js +150 -14
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -33,6 +33,52 @@ npm run storybook
33
33
 
34
34
  The MCP server will be available at `<your_storybook_dev_server_origin>/mcp` when Storybook is running.
35
35
 
36
+ ### Configuration
37
+
38
+ #### Addon Options
39
+
40
+ You can configure which toolsets are enabled by default in your `.storybook/main.js`:
41
+
42
+ ```javascript
43
+ // .storybook/main.js
44
+ export default {
45
+ addons: [
46
+ {
47
+ name: '@storybook/addon-mcp',
48
+ options: {
49
+ toolsets: {
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)
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ };
57
+ ```
58
+
59
+ **Available Toolsets:**
60
+
61
+ - `dev`: Enables [Dev Tools](#dev-tools)
62
+ - `docs`: Enables [Documentation Tools](#docs-tools-experimental)
63
+
64
+ 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.
65
+
66
+ #### Configuring toolsets with headers
67
+
68
+ You can also configure the available toolsets when setting up the MCP Server in your MCP Client by setting the `X-MCP-Toolsets` header. The header is a comma-separated list of toolset names, `X-MCP-Toolsets: dev,docs`. Eg. to configure your client to only have the Component Documentation Tools, the `.mcp.json`-file could look like this (format depends on the exact client you're using):
69
+
70
+ ```json
71
+ {
72
+ "storybook-mcp": {
73
+ "url": "http://localhost:6006/mcp",
74
+ "type": "http",
75
+ "headers": {
76
+ "X-MCP-Toolsets": "docs"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
36
82
  ### Configuring Your Agent
37
83
 
38
84
  > [!NOTE]
@@ -88,7 +134,7 @@ This addon provides MCP tools that your agent can use. The goal is that the agen
88
134
 
89
135
  **If you are prompting from an IDE like VSCode or Cursor, be sure to use `Agent` mode and `sonnet-4.5` or better.**
90
136
 
91
- ### Core Tools
137
+ ### Dev Tools
92
138
 
93
139
  These tools are always available when the addon is installed:
94
140
 
@@ -119,7 +165,7 @@ Agent calls tool, gets response:
119
165
  http://localhost:6006/?path=/story/example-button--primary
120
166
  ```
121
167
 
122
- ### Component Documentation Tools (Experimental)
168
+ ### Docs Tools (Experimental)
123
169
 
124
170
  These additional tools are available when the **experimental** component manifest feature is enabled. They provide agents with detailed documentation about your UI components.
125
171
 
package/dist/preset.js CHANGED
@@ -11,7 +11,7 @@ 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.1";
14
+ var version = "0.1.3";
15
15
  var description = "Help agents automatically write and test stories for your UI components";
16
16
 
17
17
  //#endregion
@@ -69,6 +69,13 @@ 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
79
  /**
73
80
  * Schema for a single story input when requesting story URLs.
74
81
  */
@@ -95,7 +102,8 @@ async function addGetStoryUrlsTool(server) {
95
102
  name: GET_STORY_URLS_TOOL_NAME,
96
103
  title: "Get stories' URLs",
97
104
  description: `Get the URL for one or more stories.`,
98
- schema: GetStoryUrlsInput
105
+ schema: GetStoryUrlsInput,
106
+ enabled: () => server.ctx.custom?.toolsets?.dev ?? true
99
107
  }, async (input) => {
100
108
  try {
101
109
  const { origin: origin$1, disableTelemetry } = server.ctx.custom ?? {};
@@ -155,7 +163,8 @@ async function addGetUIBuildingInstructionsTool(server) {
155
163
  description: `Instructions on how to do UI component development.
156
164
 
157
165
  ALWAYS call this tool before doing any UI/frontend/React/component development, including but not
158
- limited to adding or updating new components, pages, screens or layouts.`
166
+ limited to adding or updating new components, pages, screens or layouts.`,
167
+ enabled: () => server.ctx.custom?.toolsets?.dev ?? true
159
168
  }, async () => {
160
169
  try {
161
170
  const { options, disableTelemetry } = server.ctx.custom ?? {};
@@ -192,6 +201,13 @@ const frameworkToRendererMap = {
192
201
  "@storybook/html-vite": "@storybook/html"
193
202
  };
194
203
 
204
+ //#endregion
205
+ //#region src/tools/is-manifest-available.ts
206
+ const isManifestAvailable = async (options) => {
207
+ const [features, componentManifestGenerator] = await Promise.all([options.presets.apply("features"), options.presets.apply("experimental_componentManifestGenerator")]);
208
+ return features.experimentalComponentsManifest && componentManifestGenerator;
209
+ };
210
+
195
211
  //#endregion
196
212
  //#region src/mcp-handler.ts
197
213
  let transport;
@@ -214,28 +230,25 @@ const initializeMCPServer = async (options) => {
214
230
  });
215
231
  await addGetStoryUrlsTool(server);
216
232
  await addGetUIBuildingInstructionsTool(server);
217
- const [features, componentManifestGenerator] = await Promise.all([options.presets.apply("features"), options.presets.apply("experimental_componentManifestGenerator")]);
218
- if (features.experimentalComponentsManifest && componentManifestGenerator) {
233
+ if (await isManifestAvailable(options)) {
219
234
  logger.info("Experimental components manifest feature detected - registering component tools");
220
- await addListAllComponentsTool(server);
221
- await addGetComponentDocumentationTool(server);
235
+ const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true;
236
+ await addListAllComponentsTool(server, contextAwareEnabled);
237
+ await addGetComponentDocumentationTool(server, contextAwareEnabled);
222
238
  }
223
239
  transport = new HttpTransport(server, { path: null });
224
240
  origin = `http://localhost:${options.port}`;
225
241
  logger.debug("MCP server origin:", origin);
226
242
  return server;
227
243
  };
228
- /**
229
- * Vite middleware handler that wraps the MCP handler.
230
- * This converts Node.js IncomingMessage/ServerResponse to Web API Request/Response.
231
- */
232
- const mcpServerHandler = async (req, res, next, options) => {
244
+ const mcpServerHandler = async ({ req, res, next, options, addonOptions }) => {
233
245
  const disableTelemetry = options.disableTelemetry ?? false;
234
246
  if (!initialize) initialize = initializeMCPServer(options);
235
247
  const server = await initialize;
236
248
  const webRequest = await incomingMessageToWebRequest(req);
237
249
  const addonContext = {
238
250
  options,
251
+ toolsets: getToolsets(webRequest, addonOptions),
239
252
  origin,
240
253
  disableTelemetry,
241
254
  source: `${origin}/manifests/components.json`,
@@ -297,11 +310,134 @@ async function webResponseToServerResponse(webResponse, nodeResponse) {
297
310
  }
298
311
  nodeResponse.end();
299
312
  }
313
+ function getToolsets(request, addonOptions) {
314
+ const toolsetHeader = request.headers.get("X-MCP-Toolsets");
315
+ if (!toolsetHeader || toolsetHeader.trim() === "") return addonOptions.toolsets;
316
+ const toolsets = {
317
+ dev: false,
318
+ docs: false
319
+ };
320
+ const enabledToolsets = toolsetHeader.split(",");
321
+ for (const enabledToolset of enabledToolsets) {
322
+ const trimmedToolset = enabledToolset.trim();
323
+ if (trimmedToolset in toolsets) toolsets[trimmedToolset] = true;
324
+ }
325
+ return toolsets;
326
+ }
300
327
 
301
328
  //#endregion
302
329
  //#region src/preset.ts
303
- const experimental_devServer = (app, options) => {
304
- app.use("/mcp", (req, res, next) => mcpServerHandler(req, res, next, options));
330
+ const experimental_devServer = async (app, options) => {
331
+ const addonOptions = v.parse(AddonOptions, { toolsets: options.toolsets ?? {} });
332
+ app.post("/mcp", (req, res, next) => mcpServerHandler({
333
+ req,
334
+ res,
335
+ next,
336
+ options,
337
+ addonOptions
338
+ }));
339
+ const shouldRedirect = await isManifestAvailable(options);
340
+ app.get("/mcp", async (req, res) => {
341
+ if ((req.headers["accept"] || "").includes("text/html")) {
342
+ res.writeHead(200, { "Content-Type": "text/html" });
343
+ res.end(`
344
+ <!DOCTYPE html>
345
+ <html>
346
+ <head>
347
+ ${shouldRedirect ? "<meta http-equiv=\"refresh\" content=\"10;url=/manifests/components.html\" />" : ""}
348
+ <style>
349
+ @font-face {
350
+ font-family: 'Nunito Sans';
351
+ font-style: normal;
352
+ font-weight: 400;
353
+ font-display: swap;
354
+ src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');
355
+ }
356
+
357
+ * {
358
+ margin: 0;
359
+ padding: 0;
360
+ box-sizing: border-box;
361
+ }
362
+
363
+ html, body {
364
+ height: 100%;
365
+ font-family: 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
366
+ }
367
+
368
+ body {
369
+ display: flex;
370
+ flex-direction: column;
371
+ justify-content: center;
372
+ align-items: center;
373
+ text-align: center;
374
+ padding: 2rem;
375
+ background-color: #ffffff;
376
+ color: rgb(46, 52, 56);
377
+ line-height: 1.6;
378
+ }
379
+
380
+ p {
381
+ margin-bottom: 1rem;
382
+ }
383
+
384
+ code {
385
+ font-family: 'Monaco', 'Courier New', monospace;
386
+ background: #f5f5f5;
387
+ padding: 0.2em 0.4em;
388
+ border-radius: 3px;
389
+ }
390
+
391
+ a {
392
+ color: #1ea7fd;
393
+ }
394
+
395
+ @media (prefers-color-scheme: dark) {
396
+ body {
397
+ background-color: rgb(34, 36, 37);
398
+ color: rgb(201, 205, 207);
399
+ }
400
+
401
+ code {
402
+ background: rgba(255, 255, 255, 0.1);
403
+ }
404
+ }
405
+ </style>
406
+ </head>
407
+ <body>
408
+ <div>
409
+ <p>
410
+ Storybook MCP server successfully running via
411
+ <code>@storybook/addon-mcp</code>.
412
+ </p>
413
+ <p>
414
+ See how to connect to it from your coding agent in <a target="_blank" href="https://github.com/storybookjs/mcp/tree/main/packages/addon-mcp#configuring-your-agent">the addon's README</a>.
415
+ </p>
416
+ ${shouldRedirect ? `
417
+ <p>
418
+ Automatically redirecting to
419
+ <a href="/manifests/components.html">component manifest</a>
420
+ in <span id="countdown">10</span> seconds...
421
+ </p>` : ""}
422
+ </div>
423
+ ${shouldRedirect ? `
424
+ <script>
425
+ let countdown = 10;
426
+ const countdownElement = document.getElementById('countdown');
427
+ setInterval(() => {
428
+ countdown -= 1;
429
+ countdownElement.textContent = countdown.toString();
430
+ }, 1000);
431
+ <\/script>
432
+ ` : ""}
433
+ </body>
434
+ </html>
435
+ `);
436
+ } else {
437
+ res.writeHead(200, { "Content-Type": "text/plain" });
438
+ res.end("Storybook MCP server successfully running via @storybook/addon-mcp");
439
+ }
440
+ });
305
441
  return app;
306
442
  };
307
443
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/addon-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Help agents automatically write and test stories for your UI components",
5
5
  "keywords": [
6
6
  "storybook-addon",
@@ -27,10 +27,10 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "@tmcp/adapter-valibot": "^0.1.4",
30
- "@tmcp/transport-http": "^0.7.0",
31
- "tmcp": "^1.15.2",
30
+ "@tmcp/transport-http": "^0.8.0",
31
+ "tmcp": "^1.16.0",
32
32
  "valibot": "^1.1.0",
33
- "@storybook/mcp": "0.0.5"
33
+ "@storybook/mcp": "0.0.6"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "20.19.0",