Skip to content

Commit dce8c8d

Browse files
authored
Improve documentation around toolsets (#93)
* add docs about experimentalComponentsManifest higher up in the readme * add messages to human-readable /mcp about enabled/disabled toolsets * add changeset * remove broken compact from pkg-pr-new * bring back format option doc
1 parent ba30431 commit dce8c8d

File tree

8 files changed

+216
-37
lines changed

8 files changed

+216
-37
lines changed

.changeset/upset-worlds-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@storybook/addon-mcp': patch
3+
---
4+
5+
Improve visibility into which toolsets are available

.github/workflows/publish-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ jobs:
2525
run: pnpm build
2626

2727
- name: Publish preview releases
28-
run: pnpm pkg-pr-new publish --pnpm --compact --no-template './packages/*'
28+
run: pnpm pkg-pr-new publish --pnpm --no-template './packages/*'

packages/addon-mcp/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@ export default {
4848
options: {
4949
toolsets: {
5050
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 👇)
5252
},
5353
experimentalFormat: 'markdown', // Output format: 'markdown' (default) or 'xml'
5454
},
5555
},
5656
],
57+
features: {
58+
experimentalComponentsManifest: true, // Enable manifest generation for the docs toolset, only supported in React-based setups.
59+
},
5760
};
5861
```
5962

packages/addon-mcp/src/mcp-handler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { buffer } from 'node:stream/consumers';
1414
import { collectTelemetry } from './telemetry.ts';
1515
import type { AddonContext, AddonOptionsOutput } from './types.ts';
1616
import { logger } from 'storybook/internal/node-logger';
17-
import { isManifestAvailable } from './tools/is-manifest-available.ts';
17+
import { getManifestStatus } from './tools/is-manifest-available.ts';
1818

1919
let transport: HttpTransport<AddonContext> | undefined;
2020
let origin: string | undefined;
@@ -51,7 +51,8 @@ const initializeMCPServer = async (options: Options) => {
5151
await addGetUIBuildingInstructionsTool(server);
5252

5353
// Only register the additional tools if the component manifest feature is enabled
54-
if (await isManifestAvailable(options)) {
54+
const manifestStatus = await getManifestStatus(options);
55+
if (manifestStatus.available) {
5556
logger.info(
5657
'Experimental components manifest feature detected - registering component tools',
5758
);

packages/addon-mcp/src/preset.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mcpServerHandler } from './mcp-handler.ts';
22
import type { PresetProperty } from 'storybook/internal/types';
33
import { AddonOptions } from './types.ts';
44
import * as v from 'valibot';
5-
import { isManifestAvailable } from './tools/is-manifest-available.ts';
5+
import { getManifestStatus } from './tools/is-manifest-available.ts';
66
import htmlTemplate from './template.html';
77

88
export const experimental_devServer: PresetProperty<
@@ -25,7 +25,11 @@ export const experimental_devServer: PresetProperty<
2525
}),
2626
);
2727

28-
const shouldRedirect = await isManifestAvailable(options);
28+
const manifestStatus = await getManifestStatus(options);
29+
30+
const isDevEnabled = addonOptions.toolsets?.dev ?? true;
31+
const isDocsEnabled =
32+
manifestStatus.available && (addonOptions.toolsets?.docs ?? true);
2933

3034
app!.get('/mcp', (req, res) => {
3135
if (!req.headers['accept']?.includes('text/html')) {
@@ -35,14 +39,30 @@ export const experimental_devServer: PresetProperty<
3539
// Browser request - send HTML with redirect
3640
res.writeHead(200, { 'Content-Type': 'text/html' });
3741

38-
const html = htmlTemplate.replace(
39-
'{{REDIRECT_META}}',
40-
shouldRedirect
41-
? // redirect the user to the component manifest page after 10 seconds
42-
'<meta http-equiv="refresh" content="10;url=/manifests/components.html" />'
43-
: // ... or hide the message about redirection
44-
'<style>#redirect-message { display: none; }</style>',
45-
);
42+
let docsNotice = '';
43+
if (!manifestStatus.hasGenerator) {
44+
docsNotice = `<div class="toolset-notice">
45+
This toolset is only supported in React-based setups.
46+
</div>`;
47+
} else if (!manifestStatus.hasFeatureFlag) {
48+
docsNotice = `<div class="toolset-notice">
49+
This toolset requires enabling the experimental component manifest feature.
50+
<a target="_blank" href="https://github.com/storybookjs/mcp/tree/main/packages/addon-mcp#docs-tools-experimental">Learn how to enable it</a>
51+
</div>`;
52+
}
53+
54+
const html = htmlTemplate
55+
.replace(
56+
'{{REDIRECT_META}}',
57+
manifestStatus.available
58+
? // redirect the user to the component manifest page after 10 seconds
59+
'<meta http-equiv="refresh" content="10;url=/manifests/components.html" />'
60+
: // ... or hide the message about redirection
61+
'<style>#redirect-message { display: none; }</style>',
62+
)
63+
.replaceAll('{{DEV_STATUS}}', isDevEnabled ? 'enabled' : 'disabled')
64+
.replaceAll('{{DOCS_STATUS}}', isDocsEnabled ? 'enabled' : 'disabled')
65+
.replace('{{DOCS_NOTICE}}', docsNotice);
4666
res.end(html);
4767
});
4868
return app;

packages/addon-mcp/src/template.html

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,87 @@
5959
color: #1ea7fd;
6060
}
6161

62+
.container {
63+
display: flex;
64+
flex-direction: column;
65+
align-items: center;
66+
}
67+
68+
.toolsets {
69+
margin: 1.5rem 0;
70+
text-align: left;
71+
max-width: 500px;
72+
}
73+
74+
.toolsets h3 {
75+
font-size: 1rem;
76+
margin-bottom: 0.75rem;
77+
text-align: center;
78+
}
79+
80+
.toolset {
81+
margin-bottom: 1rem;
82+
padding: 0.75rem 1rem;
83+
border-radius: 6px;
84+
background: #f8f9fa;
85+
border: 1px solid #e9ecef;
86+
}
87+
88+
.toolset-header {
89+
display: flex;
90+
align-items: center;
91+
gap: 0.5rem;
92+
font-weight: 600;
93+
margin-bottom: 0.5rem;
94+
}
95+
96+
.toolset-status {
97+
display: inline-block;
98+
padding: 0.15em 0.5em;
99+
border-radius: 3px;
100+
font-size: 0.75rem;
101+
font-weight: 500;
102+
text-transform: uppercase;
103+
}
104+
105+
.toolset-status.enabled {
106+
background: #d4edda;
107+
color: #155724;
108+
}
109+
110+
.toolset-status.disabled {
111+
background: #f8d7da;
112+
color: #721c24;
113+
}
114+
115+
.toolset-tools {
116+
font-size: 0.875rem;
117+
color: #6c757d;
118+
padding-left: 1.5rem;
119+
margin: 0;
120+
}
121+
122+
.toolset-tools li {
123+
margin-bottom: 0.25rem;
124+
}
125+
126+
.toolset-tools code {
127+
font-size: 0.8rem;
128+
}
129+
130+
.toolset-notice {
131+
font-size: 0.8rem;
132+
color: #856404;
133+
background: #fff3cd;
134+
padding: 0.5rem;
135+
border-radius: 4px;
136+
margin-top: 0.5rem;
137+
}
138+
139+
.toolset-notice a {
140+
color: #533f03;
141+
}
142+
62143
@media (prefers-color-scheme: dark) {
63144
body {
64145
background-color: rgb(34, 36, 37);
@@ -68,11 +149,39 @@
68149
code {
69150
background: rgba(255, 255, 255, 0.1);
70151
}
152+
153+
.toolset {
154+
background: rgba(255, 255, 255, 0.05);
155+
border-color: rgba(255, 255, 255, 0.1);
156+
}
157+
158+
.toolset-tools {
159+
color: #adb5bd;
160+
}
161+
162+
.toolset-status.enabled {
163+
background: rgba(40, 167, 69, 0.2);
164+
color: #75d67e;
165+
}
166+
167+
.toolset-status.disabled {
168+
background: rgba(220, 53, 69, 0.2);
169+
color: #f5a6ad;
170+
}
171+
172+
.toolset-notice {
173+
background: rgba(255, 193, 7, 0.15);
174+
color: #ffc107;
175+
}
176+
177+
.toolset-notice a {
178+
color: #ffe066;
179+
}
71180
}
72181
</style>
73182
</head>
74183
<body>
75-
<div>
184+
<div class="container">
76185
<p>
77186
Storybook MCP server successfully running via
78187
<code>@storybook/addon-mcp</code>.
@@ -85,6 +194,34 @@
85194
>the addon's README</a
86195
>.
87196
</p>
197+
198+
<div class="toolsets">
199+
<h3>Available Toolsets</h3>
200+
201+
<div class="toolset">
202+
<div class="toolset-header">
203+
<span>dev</span>
204+
<span class="toolset-status {{DEV_STATUS}}">{{DEV_STATUS}}</span>
205+
</div>
206+
<ul class="toolset-tools">
207+
<li><code>get-story-urls</code></li>
208+
<li><code>get-ui-building-instructions</code></li>
209+
</ul>
210+
</div>
211+
212+
<div class="toolset">
213+
<div class="toolset-header">
214+
<span>docs</span>
215+
<span class="toolset-status {{DOCS_STATUS}}">{{DOCS_STATUS}}</span>
216+
</div>
217+
<ul class="toolset-tools">
218+
<li><code>list-all-components</code></li>
219+
<li><code>get-component-documentation</code></li>
220+
</ul>
221+
{{DOCS_NOTICE}}
222+
</div>
223+
</div>
224+
88225
<p id="redirect-message">
89226
Automatically redirecting to
90227
<a href="/manifests/components.html">component manifest</a>

packages/addon-mcp/src/tools/is-manifest-available.test.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi } from 'vitest';
2-
import { isManifestAvailable } from './is-manifest-available.ts';
2+
import { getManifestStatus } from './is-manifest-available.ts';
33
import type { Options } from 'storybook/internal/types';
44

55
function createMockOptions({
@@ -28,44 +28,43 @@ function createMockOptions({
2828
} as unknown as Options;
2929
}
3030

31-
describe('isManifestAvailable', () => {
31+
describe('getManifestStatus', () => {
3232
it.each([
3333
{
3434
description: 'both feature flag and generator are present',
3535
options: { featureFlag: true, hasGenerator: true },
36-
expected: true,
36+
expected: { available: true, hasGenerator: true, hasFeatureFlag: true },
3737
},
3838
{
39-
description: 'feature flag is disabled',
40-
options: { featureFlag: false, hasGenerator: true },
41-
expected: false,
39+
description: 'missing generator (unsupported framework)',
40+
options: { featureFlag: true, hasGenerator: false },
41+
expected: { available: false, hasGenerator: false, hasFeatureFlag: true },
4242
},
4343
{
44-
description: 'generator is not configured',
45-
options: { featureFlag: true, hasGenerator: false },
46-
expected: false,
44+
description: 'missing feature flag',
45+
options: { featureFlag: false, hasGenerator: true },
46+
expected: { available: false, hasGenerator: true, hasFeatureFlag: false },
4747
},
4848
{
4949
description: 'both are missing',
5050
options: { featureFlag: false, hasGenerator: false },
51-
expected: false,
51+
expected: {
52+
available: false,
53+
hasGenerator: false,
54+
hasFeatureFlag: false,
55+
},
5256
},
5357
{
5458
description: 'features object is missing the flag',
5559
options: { hasGenerator: true, hasFeaturesObject: false },
56-
expected: false,
60+
expected: { available: false, hasGenerator: true, hasFeatureFlag: false },
5761
},
5862
])(
59-
'should return $expected when $description',
63+
'should return correct status when $description',
6064
async ({ options, expected }) => {
6165
const mockOptions = createMockOptions(options);
62-
const result = await isManifestAvailable(mockOptions);
63-
64-
if (expected) {
65-
expect(result).toBeTruthy();
66-
} else {
67-
expect(result).toBeFalsy();
68-
}
66+
const result = await getManifestStatus(mockOptions);
67+
expect(result).toEqual(expected);
6968
},
7069
);
7170
});
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import type { Options } from 'storybook/internal/types';
22

3-
export const isManifestAvailable = async (
3+
export type ManifestStatus = {
4+
available: boolean;
5+
hasGenerator: boolean;
6+
hasFeatureFlag: boolean;
7+
};
8+
9+
export const getManifestStatus = async (
410
options: Options,
5-
): Promise<boolean> => {
11+
): Promise<ManifestStatus> => {
612
const [features, componentManifestGenerator] = await Promise.all([
713
options.presets.apply('features') as any,
814
options.presets.apply('experimental_componentManifestGenerator'),
915
]);
10-
return features.experimentalComponentsManifest && componentManifestGenerator;
16+
17+
const hasGenerator = !!componentManifestGenerator;
18+
const hasFeatureFlag = !!features?.experimentalComponentsManifest;
19+
20+
return {
21+
available: hasFeatureFlag && hasGenerator,
22+
hasGenerator,
23+
hasFeatureFlag,
24+
};
1125
};

0 commit comments

Comments
 (0)