From d332453cf60faccec37e935c5e4ad400b050bafb Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 29 Oct 2025 22:13:25 +0100 Subject: [PATCH 1/4] add GET handler to /mcp path --- packages/addon-mcp/src/mcp-handler.ts | 8 +- packages/addon-mcp/src/preset.ts | 123 +++++++++++++++++- .../src/tools/is-manifest-available.ts | 11 ++ 3 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 packages/addon-mcp/src/tools/is-manifest-available.ts diff --git a/packages/addon-mcp/src/mcp-handler.ts b/packages/addon-mcp/src/mcp-handler.ts index 625dca5..350d0a1 100644 --- a/packages/addon-mcp/src/mcp-handler.ts +++ b/packages/addon-mcp/src/mcp-handler.ts @@ -15,6 +15,7 @@ import { buffer } from 'node:stream/consumers'; import { collectTelemetry } from './telemetry.ts'; import type { AddonContext, AddonOptionsOutput } from './types.ts'; import { logger } from 'storybook/internal/node-logger'; +import { isManifestAvailable } from './tools/is-manifest-available.ts'; let transport: HttpTransport | undefined; let origin: string | undefined; @@ -50,12 +51,7 @@ const initializeMCPServer = async (options: Options) => { await addGetUIBuildingInstructionsTool(server); // Only register the additional tools if the component manifest feature is enabled - const [features, componentManifestGenerator] = await Promise.all([ - options.presets.apply('features') as any, - options.presets.apply('experimental_componentManifestGenerator'), - ]); - - if (features.experimentalComponentsManifest && componentManifestGenerator) { + if (await isManifestAvailable(options)) { logger.info( 'Experimental components manifest feature detected - registering component tools', ); diff --git a/packages/addon-mcp/src/preset.ts b/packages/addon-mcp/src/preset.ts index 449689f..33de920 100644 --- a/packages/addon-mcp/src/preset.ts +++ b/packages/addon-mcp/src/preset.ts @@ -2,15 +2,16 @@ import { mcpServerHandler } from './mcp-handler.ts'; import type { PresetProperty } from 'storybook/internal/types'; import { AddonOptions, type AddonOptionsInput } from './types.ts'; import * as v from 'valibot'; +import { isManifestAvailable } from './tools/is-manifest-available.ts'; export const experimental_devServer: PresetProperty< 'experimental_devServer' -> = (app, options) => { +> = async (app, options) => { const addonOptions = v.parse(AddonOptions, { toolsets: (options as AddonOptionsInput).toolsets ?? {}, }); - app!.use('/mcp', (req, res, next) => + app!.post('/mcp', (req, res, next) => mcpServerHandler({ req, res, @@ -19,5 +20,123 @@ export const experimental_devServer: PresetProperty< addonOptions, }), ); + + const shouldRedirect = await isManifestAvailable(options); + + app!.get('/mcp', async (req, res) => { + const acceptHeader = req.headers['accept'] || ''; + + if (acceptHeader.includes('text/html')) { + // Browser request - send HTML with redirect + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + + ${shouldRedirect ? '' : ''} + + + +
+

+ Storybook MCP server successfully running via + @storybook/addon-mcp. +

+

+ See how to connect to it from your coding agent in the addon's README. +

+ ${ + shouldRedirect + ? ` +

+ Automatically redirecting to + component manifest + in 10 seconds... +

` + : '' + } +
+ ${ + shouldRedirect + ? ` + + ` + : '' + } + + + `); + } else { + // Non-browser request (API, curl, etc.) - send plain text + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end( + 'Storybook MCP server successfully running via @storybook/addon-mcp', + ); + } + }); return app; }; diff --git a/packages/addon-mcp/src/tools/is-manifest-available.ts b/packages/addon-mcp/src/tools/is-manifest-available.ts new file mode 100644 index 0000000..6adbc9e --- /dev/null +++ b/packages/addon-mcp/src/tools/is-manifest-available.ts @@ -0,0 +1,11 @@ +import type { Options } from 'storybook/internal/types'; + +export const isManifestAvailable = async ( + options: Options, +): Promise => { + const [features, componentManifestGenerator] = await Promise.all([ + options.presets.apply('features') as any, + options.presets.apply('experimental_componentManifestGenerator'), + ]); + return features.experimentalComponentsManifest && componentManifestGenerator; +}; From 7cb64b608503ec058240dbba516f1401c02d8c76 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 29 Oct 2025 22:14:22 +0100 Subject: [PATCH 2/4] merge changelogs --- CHANGELOG.md | 36 --------------------------------- packages/addon-mcp/CHANGELOG.md | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 36 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 185fc64..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# @storybook/addon-mcp - -## 0.0.5 - -### Patch Changes - -- [#21](https://github.com/storybookjs/addon-mcp/pull/21) [`b91acac`](https://github.com/storybookjs/addon-mcp/commit/b91acac6fcb7d8e3556e07a499432c1779d59680) Thanks [@shilman](https://github.com/shilman)! - Embed demo image from storybook.js.org - -- [#16](https://github.com/storybookjs/addon-mcp/pull/16) [`bf41737`](https://github.com/storybookjs/addon-mcp/commit/bf41737f3409ff25a023993bf1475bf9620c085d) Thanks [@shilman](https://github.com/shilman)! - Improve README - -## 0.0.4 - -### Patch Changes - -- [#12](https://github.com/storybookjs/addon-mcp/pull/12) [`b448cd4`](https://github.com/storybookjs/addon-mcp/commit/b448cd45093866556cfb1b3edba8e98c0db23a9a) Thanks [@JReinhold](https://github.com/JReinhold)! - Add instructions on when to write stories - -## 0.0.3 - -### Patch Changes - -- [#11](https://github.com/storybookjs/addon-mcp/pull/11) [`bba9b8c`](https://github.com/storybookjs/addon-mcp/commit/bba9b8c683acdd5dfa835d4dea848dce7355ee82) Thanks [@JReinhold](https://github.com/JReinhold)! - - Improved UI Building Instructions - - Improved output format of Get Story URLs tool - -- [#9](https://github.com/storybookjs/addon-mcp/pull/9) [`e5e2adf`](https://github.com/storybookjs/addon-mcp/commit/e5e2adf7192d5e12f21229056b644e7aa32287ed) Thanks [@JReinhold](https://github.com/JReinhold)! - Add basic telemetry for sessions and tool calls - -## 0.0.2 - -### Patch Changes - -- [#8](https://github.com/storybookjs/addon-mcp/pull/8) [`77d0779`](https://github.com/storybookjs/addon-mcp/commit/77d0779f471537bd72eca42543a559e97d329f6f) Thanks [@JReinhold](https://github.com/JReinhold)! - Add initial readme content - -## 0.0.1 - -### Patch Changes - -- [#5](https://github.com/storybookjs/addon-mcp/pull/5) [`e4978f3`](https://github.com/storybookjs/addon-mcp/commit/e4978f3cc0f587f3fc51aa26f49b8183bfbbc966) Thanks [@JReinhold](https://github.com/JReinhold)! - Initial release with UI instruction and story link tools diff --git a/packages/addon-mcp/CHANGELOG.md b/packages/addon-mcp/CHANGELOG.md index a336cff..d087bc3 100644 --- a/packages/addon-mcp/CHANGELOG.md +++ b/packages/addon-mcp/CHANGELOG.md @@ -67,3 +67,38 @@ EDIT: The above is not true anymore, see version [0.1.1](#011) of this package. ### Patch Changes - [#29](https://github.com/storybookjs/mcp/pull/29) [`4086e0d`](https://github.com/storybookjs/mcp/commit/4086e0d41d29a2e5c412a5cfd6bc65d97bf9ee76) Thanks [@JReinhold](https://github.com/JReinhold)! - Update documentation and repository links + +## 0.0.5 + +### Patch Changes + +- [#21](https://github.com/storybookjs/addon-mcp/pull/21) [`b91acac`](https://github.com/storybookjs/addon-mcp/commit/b91acac6fcb7d8e3556e07a499432c1779d59680) Thanks [@shilman](https://github.com/shilman)! - Embed demo image from storybook.js.org + +- [#16](https://github.com/storybookjs/addon-mcp/pull/16) [`bf41737`](https://github.com/storybookjs/addon-mcp/commit/bf41737f3409ff25a023993bf1475bf9620c085d) Thanks [@shilman](https://github.com/shilman)! - Improve README + +## 0.0.4 + +### Patch Changes + +- [#12](https://github.com/storybookjs/addon-mcp/pull/12) [`b448cd4`](https://github.com/storybookjs/addon-mcp/commit/b448cd45093866556cfb1b3edba8e98c0db23a9a) Thanks [@JReinhold](https://github.com/JReinhold)! - Add instructions on when to write stories + +## 0.0.3 + +### Patch Changes + +- [#11](https://github.com/storybookjs/addon-mcp/pull/11) [`bba9b8c`](https://github.com/storybookjs/addon-mcp/commit/bba9b8c683acdd5dfa835d4dea848dce7355ee82) Thanks [@JReinhold](https://github.com/JReinhold)! - - Improved UI Building Instructions + - Improved output format of Get Story URLs tool + +- [#9](https://github.com/storybookjs/addon-mcp/pull/9) [`e5e2adf`](https://github.com/storybookjs/addon-mcp/commit/e5e2adf7192d5e12f21229056b644e7aa32287ed) Thanks [@JReinhold](https://github.com/JReinhold)! - Add basic telemetry for sessions and tool calls + +## 0.0.2 + +### Patch Changes + +- [#8](https://github.com/storybookjs/addon-mcp/pull/8) [`77d0779`](https://github.com/storybookjs/addon-mcp/commit/77d0779f471537bd72eca42543a559e97d329f6f) Thanks [@JReinhold](https://github.com/JReinhold)! - Add initial readme content + +## 0.0.1 + +### Patch Changes + +- [#5](https://github.com/storybookjs/addon-mcp/pull/5) [`e4978f3`](https://github.com/storybookjs/addon-mcp/commit/e4978f3cc0f587f3fc51aa26f49b8183bfbbc966) Thanks [@JReinhold](https://github.com/JReinhold)! - Initial release with UI instruction and story link tools From 7f94bd92bfcd5227caffcf025c195295cb190903 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 29 Oct 2025 22:51:24 +0100 Subject: [PATCH 3/4] update manifest type to match upcoming manifest changes in https://github.com/storybookjs/storybook/pull/32855 --- packages/mcp/fixtures/button.fixture.json | 1 + packages/mcp/fixtures/card.fixture.json | 1 + .../mcp/fixtures/full-manifest.fixture.json | 3 ++ packages/mcp/fixtures/input.fixture.json | 1 + .../mcp/fixtures/small-manifest.fixture.json | 3 ++ packages/mcp/src/types.ts | 20 +++++++----- .../mcp/src/utils/format-manifest.test.ts | 31 +++++++++++++++++++ packages/mcp/src/utils/format-manifest.ts | 3 ++ packages/mcp/src/utils/get-manifest.test.ts | 23 +++++++++----- 9 files changed, 71 insertions(+), 15 deletions(-) diff --git a/packages/mcp/fixtures/button.fixture.json b/packages/mcp/fixtures/button.fixture.json index 9616bcb..16e9103 100644 --- a/packages/mcp/fixtures/button.fixture.json +++ b/packages/mcp/fixtures/button.fixture.json @@ -1,6 +1,7 @@ { "button": { "id": "button", + "path": "src/components/Button.tsx", "name": "Button", "description": "A versatile button component that supports multiple variants, sizes, and states.\n\nThe Button component is a fundamental building block for user interactions. It can be styled as primary, secondary, or tertiary actions, and supports disabled and loading states.\n\n## Usage\n\nButtons should be used for actions that affect the current page or trigger operations. For navigation, consider using a Link component instead.", "summary": "A versatile button component for user interactions", diff --git a/packages/mcp/fixtures/card.fixture.json b/packages/mcp/fixtures/card.fixture.json index 037a186..8fc300f 100644 --- a/packages/mcp/fixtures/card.fixture.json +++ b/packages/mcp/fixtures/card.fixture.json @@ -1,6 +1,7 @@ { "card": { "id": "card", + "path": "src/components/Card.tsx", "name": "Card", "description": "A flexible container component for grouping related content with optional header, footer, and action areas.\n\nThe Card component provides a consistent way to present information in a contained, elevated surface. It's commonly used for displaying articles, products, user profiles, or any grouped content that benefits from visual separation.\n\n## Design Principles\n\n- Cards should contain a single subject or action\n- Maintain consistent padding and spacing\n- Use elevation to indicate interactive vs static cards\n- Keep content hierarchy clear with proper use of typography", "summary": "A flexible container component for grouping related content", diff --git a/packages/mcp/fixtures/full-manifest.fixture.json b/packages/mcp/fixtures/full-manifest.fixture.json index a536769..65da764 100644 --- a/packages/mcp/fixtures/full-manifest.fixture.json +++ b/packages/mcp/fixtures/full-manifest.fixture.json @@ -3,6 +3,7 @@ "components": { "button": { "id": "button", + "path": "src/components/Button.tsx", "name": "Button", "description": "A versatile button component that supports multiple variants, sizes, and states.\n\nThe Button component is a fundamental building block for user interactions. It can be styled as primary, secondary, or tertiary actions, and supports disabled and loading states.\n\n## Usage\n\nButtons should be used for actions that affect the current page or trigger operations. For navigation, consider using a Link component instead.", "summary": "A versatile button component for user interactions", @@ -170,6 +171,7 @@ }, "card": { "id": "card", + "path": "src/components/Card.tsx", "name": "Card", "description": "A flexible container component for grouping related content with optional header, footer, and action areas.\n\nThe Card component provides a consistent way to present information in a contained, elevated surface. It's commonly used for displaying articles, products, user profiles, or any grouped content that benefits from visual separation.\n\n## Design Principles\n\n- Cards should contain a single subject or action\n- Maintain consistent padding and spacing\n- Use elevation to indicate interactive vs static cards\n- Keep content hierarchy clear with proper use of typography", "summary": "A flexible container component for grouping related content", @@ -339,6 +341,7 @@ }, "input": { "id": "input", + "path": "src/components/Input.tsx", "name": "Input", "description": "A flexible text input component that supports various input types, validation states, and accessibility features.\n\nThe Input component is a foundational form element that wraps the native HTML input with consistent styling and behavior. It includes support for labels, error messages, helper text, and different visual states.\n\n## Accessibility\n\nThe Input component automatically manages ARIA attributes for labels, descriptions, and error messages to ensure screen reader compatibility.", "summary": "A flexible text input component with validation support", diff --git a/packages/mcp/fixtures/input.fixture.json b/packages/mcp/fixtures/input.fixture.json index 52df3dc..e0d192e 100644 --- a/packages/mcp/fixtures/input.fixture.json +++ b/packages/mcp/fixtures/input.fixture.json @@ -1,6 +1,7 @@ { "input": { "id": "input", + "path": "src/components/Input.tsx", "name": "Input", "description": "A flexible text input component that supports various input types, validation states, and accessibility features.\n\nThe Input component is a foundational form element that wraps the native HTML input with consistent styling and behavior. It includes support for labels, error messages, helper text, and different visual states.\n\n## Accessibility\n\nThe Input component automatically manages ARIA attributes for labels, descriptions, and error messages to ensure screen reader compatibility.", "summary": "A flexible text input component with validation support", diff --git a/packages/mcp/fixtures/small-manifest.fixture.json b/packages/mcp/fixtures/small-manifest.fixture.json index 29c9392..fc62ba5 100644 --- a/packages/mcp/fixtures/small-manifest.fixture.json +++ b/packages/mcp/fixtures/small-manifest.fixture.json @@ -3,6 +3,7 @@ "components": { "button": { "id": "button", + "path": "src/components/Button.tsx", "name": "Button", "summary": "A simple button component", "examples": [ @@ -15,6 +16,7 @@ }, "card": { "id": "card", + "path": "src/components/Card.tsx", "name": "Card", "description": "A container component for grouping related content.", "examples": [ @@ -27,6 +29,7 @@ }, "input": { "id": "input", + "path": "src/components/Input.tsx", "name": "Input", "description": "A text input component with validation support.", "examples": [ diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 9d13afc..1e2305c 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -40,23 +40,29 @@ const JSDocTag = v.record(v.string(), v.array(v.string())); const BaseManifest = v.object({ name: v.string(), - description: v.exactOptional(v.string()), - import: v.exactOptional(v.string()), - jsDocTags: v.exactOptional(JSDocTag), + description: v.optional(v.string()), + import: v.optional(v.string()), + jsDocTags: v.optional(JSDocTag), + error: v.optional( + v.object({ + message: v.string(), + }), + ), }); const Example = v.object({ ...BaseManifest.entries, - snippet: v.string(), + snippet: v.optional(v.string()), }); export const ComponentManifest = v.object({ ...BaseManifest.entries, id: v.string(), - summary: v.exactOptional(v.string()), - examples: v.exactOptional(v.array(Example)), + path: v.string(), + summary: v.optional(v.string()), + examples: v.optional(v.array(Example)), // loose schema for react-docgen types, as they are pretty complex - reactDocgen: v.exactOptional(v.custom(() => true)), + reactDocgen: v.optional(v.custom(() => true)), }); export type ComponentManifest = v.InferOutput; diff --git a/packages/mcp/src/utils/format-manifest.test.ts b/packages/mcp/src/utils/format-manifest.test.ts index ec22452..7d06578 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -23,6 +23,7 @@ describe('formatComponentManifest', () => { it('should include component name in component_name tag', () => { const manifest: ComponentManifest = { id: 'test-component', + path: 'src/components/TestComponent.tsx', name: 'TestComponent', }; @@ -41,6 +42,7 @@ describe('formatComponentManifest', () => { it('should include description when provided', () => { const manifest: ComponentManifest = { id: 'button', + path: 'src/components/Button.tsx', name: 'Button', description: 'A simple button component', }; @@ -61,6 +63,7 @@ describe('formatComponentManifest', () => { it('should handle multi-line descriptions', () => { const manifest: ComponentManifest = { id: 'button', + path: 'src/components/Button.tsx', name: 'Button', description: 'A versatile button component.\n\nSupports multiple variants and sizes.', @@ -85,6 +88,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', }; const result = formatComponentManifest(manifest); @@ -103,6 +107,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', import: 'import { Button } from "@/components";', examples: [ { @@ -138,6 +143,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', import: 'import { Button } from "@/components";', examples: [ { @@ -181,6 +187,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', examples: [ { name: 'WithIcon', @@ -219,6 +226,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', import: 'import { Button } from "@/components";', examples: [ { @@ -251,6 +259,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', examples: [ { name: 'Simple', @@ -279,6 +288,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', examples: [ { name: 'NoImport', @@ -307,6 +317,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'A button component', }; @@ -327,6 +338,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', examples: [], }; @@ -346,6 +358,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'A versatile button component.\n\nSupports multiple variants, sizes, and states.', summary: 'A button for user interactions', @@ -414,6 +427,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', reactDocgen: { props: { variant: { @@ -495,6 +509,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', reactDocgen: { props: { children: { @@ -526,6 +541,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'A button component', }; @@ -546,6 +562,7 @@ describe('formatComponentManifest', () => { const manifest: ComponentManifest = { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', reactDocgen: { props: {}, }, @@ -577,6 +594,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', }, }, }; @@ -600,14 +618,17 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', }, card: { id: 'card', name: 'Card', + path: 'src/components/Card.tsx', }, input: { id: 'input', name: 'Input', + path: 'src/components/Input.tsx', }, }, }; @@ -641,6 +662,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', summary: 'A versatile button component', }, }, @@ -668,6 +690,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', summary: 'Short summary', description: 'This is a longer description that should be ignored', }, @@ -687,6 +710,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'A simple button component', }, }, @@ -714,6 +738,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'This is a very long description that exceeds ninety characters and should be truncated with ellipsis', }, @@ -742,6 +767,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', description: 'A description with exactly eighty characters is fine and should not be truncated', }, @@ -763,6 +789,7 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', }, }, }; @@ -789,16 +816,19 @@ describe('formatComponentManifestMapToList', () => { button: { id: 'button', name: 'Button', + path: 'src/components/Button.tsx', summary: 'A versatile button component', }, card: { id: 'card', name: 'Card', + path: 'src/components/Card.tsx', description: 'A flexible container for grouping content', }, input: { id: 'input', name: 'Input', + path: 'src/components/Input.tsx', summary: 'Text input with validation', description: 'A comprehensive input component with validation, error states, and accessibility features', @@ -806,6 +836,7 @@ describe('formatComponentManifestMapToList', () => { modal: { id: 'modal', name: 'Modal', + path: 'src/components/Modal.tsx', }, }, }; diff --git a/packages/mcp/src/utils/format-manifest.ts b/packages/mcp/src/utils/format-manifest.ts index 3689a78..effd7cd 100644 --- a/packages/mcp/src/utils/format-manifest.ts +++ b/packages/mcp/src/utils/format-manifest.ts @@ -22,6 +22,9 @@ export function formatComponentManifest( // Examples section - only if there are examples if (componentManifest.examples && componentManifest.examples.length > 0) { for (const example of componentManifest.examples) { + if (!example.snippet) { + continue; + } const exampleParts: string[] = []; // Convert PascalCase to Human Readable Case // "WithSizes" -> "With Sizes" diff --git a/packages/mcp/src/utils/get-manifest.test.ts b/packages/mcp/src/utils/get-manifest.test.ts index 8932ac9..348367b 100644 --- a/packages/mcp/src/utils/get-manifest.test.ts +++ b/packages/mcp/src/utils/get-manifest.test.ts @@ -90,10 +90,12 @@ describe('getManifest', () => { headers: { get: vi.fn().mockReturnValue('application/json'), }, - text: vi.fn().mockResolvedValue(JSON.stringify({ - // Missing required 'v' field - components: {}, - })), + text: vi.fn().mockResolvedValue( + JSON.stringify({ + // Missing required 'v' field + components: {}, + }), + ), }); await expect( @@ -109,10 +111,12 @@ describe('getManifest', () => { headers: { get: vi.fn().mockReturnValue('application/json'), }, - text: vi.fn().mockResolvedValue(JSON.stringify({ - v: 1, - components: {}, - })), + text: vi.fn().mockResolvedValue( + JSON.stringify({ + v: 1, + components: {}, + }), + ), }); await expect( @@ -161,6 +165,7 @@ describe('getManifest', () => { components: { button: { id: 'button', + path: 'src/components/Button.tsx', name: 'Button', description: 'A button component', }, @@ -191,6 +196,7 @@ describe('getManifest', () => { components: { button: { id: 'button', + path: 'src/components/Button.tsx', name: 'Button', description: 'A button component', }, @@ -220,6 +226,7 @@ describe('getManifest', () => { components: { button: { id: 'button', + path: 'src/components/Button.tsx', name: 'Button', description: 'A button component', }, From cdaca09ebafa90fbb24d644c3d5a547b618a4c14 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 29 Oct 2025 22:55:40 +0100 Subject: [PATCH 4/4] add changesets --- .changeset/blue-eels-take.md | 5 +++++ .changeset/tall-boxes-build.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/blue-eels-take.md create mode 100644 .changeset/tall-boxes-build.md diff --git a/.changeset/blue-eels-take.md b/.changeset/blue-eels-take.md new file mode 100644 index 0000000..5a7cdba --- /dev/null +++ b/.changeset/blue-eels-take.md @@ -0,0 +1,5 @@ +--- +'@storybook/addon-mcp': patch +--- + +Add GET handler that serves HTML when visiting `/mcp`, and redirects to human-readable component manifest when applicable diff --git a/.changeset/tall-boxes-build.md b/.changeset/tall-boxes-build.md new file mode 100644 index 0000000..5f6fd3e --- /dev/null +++ b/.changeset/tall-boxes-build.md @@ -0,0 +1,5 @@ +--- +'@storybook/addon-mcp': patch +--- + +Update manifest format