@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.
- package/README.md +48 -2
- package/dist/preset.js +150 -14
- 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
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
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
|
-
|
|
218
|
-
if (features.experimentalComponentsManifest && componentManifestGenerator) {
|
|
233
|
+
if (await isManifestAvailable(options)) {
|
|
219
234
|
logger.info("Experimental components manifest feature detected - registering component tools");
|
|
220
|
-
|
|
221
|
-
await
|
|
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
|
-
|
|
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.
|
|
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.
|
|
31
|
-
"tmcp": "^1.
|
|
30
|
+
"@tmcp/transport-http": "^0.8.0",
|
|
31
|
+
"tmcp": "^1.16.0",
|
|
32
32
|
"valibot": "^1.1.0",
|
|
33
|
-
"@storybook/mcp": "0.0.
|
|
33
|
+
"@storybook/mcp": "0.0.6"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "20.19.0",
|