Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-buses-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/mcp': patch
---

Support error.name in manifests
6 changes: 4 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ jobs:
uses: ./.github/actions/setup-node-and-install

- name: Run tests with coverage
run: pnpm --filter @storybook/mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml
run: pnpm --filter @storybook/mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml

- name: Upload test and coverage artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: test-mcp
path: |
Expand All @@ -90,9 +91,10 @@ jobs:
run: pnpm build --filter @storybook/mcp

- name: Run tests with coverage
run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml
run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml

- name: Upload test and coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: test-addon-mcp
Expand Down
163 changes: 163 additions & 0 deletions packages/mcp/fixtures/with-errors.fixture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{
"v": 1,
"components": {
"success-component-with-mixed-stories": {
"id": "success-component-with-mixed-stories",
"path": "src/components/SuccessWithMixedStories.tsx",
"name": "SuccessWithMixedStories",
"description": "A component that loaded successfully but has some stories that failed to generate.",
"summary": "Success component with both working and failing stories",
"import": "import { SuccessWithMixedStories } from '@storybook/design-system';",
"reactDocgen": {
"props": {
"text": {
"description": "The text to display",
"required": true,
"tsType": { "name": "string" }
},
"variant": {
"description": "The visual variant",
"required": false,
"tsType": {
"name": "union",
"raw": "\"primary\" | \"secondary\"",
"elements": [
{ "name": "literal", "value": "\"primary\"" },
{ "name": "literal", "value": "\"secondary\"" }
]
},
"defaultValue": { "value": "\"primary\"", "computed": false }
}
}
},
"stories": [
Comment thread
JReinhold marked this conversation as resolved.
{
"id": "success-component-with-mixed-stories--working",
"name": "Working",
"description": "This story generated successfully.",
"summary": "A working story",
"import": "import { SuccessWithMixedStories } from '@storybook/design-system';",
"snippet": "const Working = () => <SuccessWithMixedStories text=\"Hello\" />"
},
{
"id": "success-component-with-mixed-stories--failed",
"name": "Failed",
"error": {
"name": "SyntaxError",
"message": "Unexpected token in story code. Unable to generate code snippet."
}
}
]
},
"error-component-with-success-stories": {
"id": "error-component-with-success-stories",
"path": "src/components/ErrorWithSuccessStories.tsx",
"name": "ErrorWithSuccessStories",
"error": {
"name": "TypeError",
"message": "Failed to parse component: Cannot read property 'name' of undefined in react-docgen parser"
},
"stories": [
{
"id": "error-component-with-success-stories--basic",
"name": "Basic",
"description": "Even though the component parsing failed, this story's code snippet was generated.",
"summary": "Basic usage story",
"snippet": "const Basic = () => <ErrorWithSuccessStories>Content</ErrorWithSuccessStories>"
},
{
"id": "error-component-with-success-stories--advanced",
"name": "Advanced",
"description": "Another successfully generated story despite component-level errors.",
"summary": "Advanced usage story",
"snippet": "const Advanced = () => (\n <ErrorWithSuccessStories disabled>\n Advanced Content\n </ErrorWithSuccessStories>\n)"
}
]
},
"error-component-with-error-stories": {
"id": "error-component-with-error-stories",
"path": "src/components/ErrorWithErrorStories.tsx",
"name": "ErrorWithErrorStories",
"error": {
"name": "Error",
"message": "Failed to extract component metadata: File not found or contains invalid TypeScript"
},
"stories": [
{
"id": "error-component-with-error-stories--broken-story-1",
"name": "BrokenStory1",
"description": "This story failed to generate.",
"error": {
"name": "Error",
"message": "Story render function is too complex to analyze"
}
},
{
"id": "error-component-with-error-stories--broken-story-2",
"name": "BrokenStory2",
"description": "This story also failed to generate.",
"error": {
"name": "ReferenceError",
"message": "Undefined variable referenced in story: missingImport"
}
}
]
},
"complete-error-component": {
"id": "complete-error-component",
"path": "src/components/CompleteError.tsx",
"name": "CompleteError",
"error": {
"name": "ModuleNotFoundError",
"message": "Cannot find module './CompleteError' or its corresponding type declarations"
}
},
"partial-success": {
"id": "partial-success",
"path": "src/components/PartialSuccess.tsx",
"name": "PartialSuccess",
"description": "A component where everything worked except one story.",
"summary": "Mostly working component with one failing story",
"import": "import { PartialSuccess } from '@storybook/design-system';",
"reactDocgen": {
"props": {
"title": {
"description": "The title text",
"required": true,
"tsType": { "name": "string" }
},
"subtitle": {
"description": "Optional subtitle",
"required": false,
"tsType": { "name": "string" }
}
}
},
"stories": [
{
"id": "partial-success--default",
"name": "Default",
"description": "Default usage of the component.",
"import": "import { PartialSuccess } from '@storybook/design-system';",
"snippet": "const Default = () => <PartialSuccess title=\"Hello\" />"
},
{
"id": "partial-success--with-subtitle",
"name": "WithSubtitle",
"description": "Component with both title and subtitle.",
"import": "import { PartialSuccess } from '@storybook/design-system';",
"snippet": "const WithSubtitle = () => <PartialSuccess title=\"Hello\" subtitle=\"World\" />"
},
{
"id": "partial-success--complex-case",
"name": "ComplexCase",
"description": "A complex story that failed to generate.",
"error": {
"name": "Error",
"message": "Story uses hooks that cannot be statically analyzed"
}
}
]
}
}
}
1 change: 1 addition & 0 deletions packages/mcp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const BaseManifest = v.object({
jsDocTags: v.optional(JSDocTag),
error: v.optional(
v.object({
name: v.string(),
message: v.string(),
}),
),
Expand Down
5 changes: 1 addition & 4 deletions packages/mcp/src/utils/error-to-mcp-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { errorToMCPContent, ManifestGetError } from './get-manifest.ts';

describe('errorToMCPContent', () => {
it('should convert ManifestGetError to MCP error content', () => {
const error = new ManifestGetError(
'Failed to get',
'https://example.com',
);
const error = new ManifestGetError('Failed to get', 'https://example.com');

const result = errorToMCPContent(error);

Expand Down
172 changes: 172 additions & 0 deletions packages/mcp/src/utils/format-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from './format-manifest';
import type { ComponentManifest, ComponentManifestMap } from '../types';
import fullManifestFixture from '../../fixtures/full-manifest.fixture.json' with { type: 'json' };
import withErrorsFixture from '../../fixtures/with-errors.fixture.json' with { type: 'json' };

describe('formatComponentManifest', () => {
it('formats all full fixtures', () => {
Expand Down Expand Up @@ -874,4 +875,175 @@ describe('formatComponentManifestMapToList', () => {
`);
});
});

describe('with-errors fixture', () => {
it('should format success component with mixed stories (only successful ones)', () => {
const component =
withErrorsFixture.components['success-component-with-mixed-stories'];
const result = formatComponentManifest(component);
expect(result).toMatchInlineSnapshot(`
"<component>
<id>success-component-with-mixed-stories</id>
<name>SuccessWithMixedStories</name>
<description>
A component that loaded successfully but has some stories that failed to generate.
</description>
<story>
<story_name>Working</story_name>
<story_description>
This story generated successfully.
</story_description>
<story_code>
import { SuccessWithMixedStories } from '@storybook/design-system';

const Working = () => <SuccessWithMixedStories text="Hello" />
</story_code>
</story>
Comment thread
JReinhold marked this conversation as resolved.
<props>
<prop>
<prop_name>text</prop_name>
<prop_description>
The text to display
</prop_description>
<prop_type>string</prop_type>
<prop_required>true</prop_required>
</prop>
<prop>
<prop_name>variant</prop_name>
<prop_description>
The visual variant
</prop_description>
<prop_type>"primary" | "secondary"</prop_type>
<prop_required>false</prop_required>
<prop_default>"primary"</prop_default>
</prop>
</props>
</component>"
`);
});

it('should format error component with success stories', () => {
const component =
withErrorsFixture.components['error-component-with-success-stories'];
const result = formatComponentManifest(component);
expect(result).toMatchInlineSnapshot(`
"<component>
<id>error-component-with-success-stories</id>
<name>ErrorWithSuccessStories</name>
<story>
<story_name>Basic</story_name>
<story_description>
Even though the component parsing failed, this story's code snippet was generated.
</story_description>
<story_code>
const Basic = () => <ErrorWithSuccessStories>Content</ErrorWithSuccessStories>
</story_code>
</story>
<story>
<story_name>Advanced</story_name>
<story_description>
Another successfully generated story despite component-level errors.
</story_description>
<story_code>
const Advanced = () => (
<ErrorWithSuccessStories disabled>
Advanced Content
</ErrorWithSuccessStories>
)
</story_code>
</story>
Comment thread
JReinhold marked this conversation as resolved.
</component>"
`);
});

it('should format partial success component (skips failed story)', () => {
const component = withErrorsFixture.components['partial-success'];
const result = formatComponentManifest(component);
expect(result).toMatchInlineSnapshot(`
"<component>
<id>partial-success</id>
<name>PartialSuccess</name>
<description>
A component where everything worked except one story.
</description>
<story>
<story_name>Default</story_name>
<story_description>
Default usage of the component.
</story_description>
<story_code>
import { PartialSuccess } from '@storybook/design-system';

const Default = () => <PartialSuccess title="Hello" />
</story_code>
</story>
<story>
<story_name>With Subtitle</story_name>
<story_description>
Component with both title and subtitle.
</story_description>
<story_code>
import { PartialSuccess } from '@storybook/design-system';

const WithSubtitle = () => <PartialSuccess title="Hello" subtitle="World" />
</story_code>
</story>
Comment thread
JReinhold marked this conversation as resolved.
<props>
<prop>
<prop_name>title</prop_name>
<prop_description>
The title text
</prop_description>
<prop_type>string</prop_type>
<prop_required>true</prop_required>
</prop>
<prop>
<prop_name>subtitle</prop_name>
<prop_description>
Optional subtitle
</prop_description>
<prop_type>string</prop_type>
<prop_required>false</prop_required>
</prop>
</props>
</component>"
`);
});

it('should format list of components with errors', () => {
const result = formatComponentManifestMapToList(
withErrorsFixture as ComponentManifestMap,
);
expect(result).toMatchInlineSnapshot(`
"<components>
<component>
<id>success-component-with-mixed-stories</id>
<name>SuccessWithMixedStories</name>
<summary>
Success component with both working and failing stories
</summary>
</component>
<component>
<id>error-component-with-success-stories</id>
<name>ErrorWithSuccessStories</name>
</component>
<component>
<id>error-component-with-error-stories</id>
<name>ErrorWithErrorStories</name>
</component>
<component>
<id>complete-error-component</id>
<name>CompleteError</name>
</component>
<component>
<id>partial-success</id>
<name>PartialSuccess</name>
<summary>
Mostly working component with one failing story
</summary>
</component>
</components>"
`);
});
});
});
Loading