Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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-examples": {
"id": "success-component-with-mixed-examples",
"path": "src/components/SuccessWithMixedExamples.tsx",
"name": "SuccessWithMixedExamples",
"description": "A component that loaded successfully but has some examples that failed to generate.",
"summary": "Success component with both working and failing examples",
"import": "import { SuccessWithMixedExamples } 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": [
{
"id": "success-component-with-mixed-examples--working",
"name": "Working",
"description": "This example generated successfully.",
"summary": "A working example",
"import": "import { SuccessWithMixedExamples } from '@storybook/design-system';",
"snippet": "const Working = () => <SuccessWithMixedExamples text=\"Hello\" />"
},
{
"id": "success-component-with-mixed-examples--failed",
"name": "Failed",
"error": {
"name": "SyntaxError",
"message": "Unexpected token in story code. Unable to generate code snippet."
}
}
]
},
"error-component-with-success-examples": {
"id": "error-component-with-success-examples",
"path": "src/components/ErrorWithSuccessExamples.tsx",
"name": "ErrorWithSuccessExamples",
"error": {
"name": "TypeError",
"message": "Failed to parse component: Cannot read property 'name' of undefined in react-docgen parser"
},
"stories": [
{
"id": "error-component-with-success-examples--basic",
"name": "Basic",
"description": "Even though the component parsing failed, this example's code snippet was generated.",
"summary": "Basic usage example",
"snippet": "const Basic = () => <ErrorWithSuccessExamples>Content</ErrorWithSuccessExamples>"
},
{
"id": "error-component-with-success-examples--advanced",
"name": "Advanced",
"description": "Another successfully generated example despite component-level errors.",
"summary": "Advanced usage example",
"snippet": "const Advanced = () => (\n <ErrorWithSuccessExamples disabled>\n Advanced Content\n </ErrorWithSuccessExamples>\n)"
}
]
},
"error-component-with-error-examples": {
"id": "error-component-with-error-examples",
"path": "src/components/ErrorWithErrorExamples.tsx",
"name": "ErrorWithErrorExamples",
"error": {
"name": "Error",
"message": "Failed to extract component metadata: File not found or contains invalid TypeScript"
},
"stories": [
{
"id": "error-component-with-error-examples--broken-example-1",
"name": "BrokenExample1",
"description": "This example failed to generate.",
"error": {
"name": "Error",
"message": "Story render function is too complex to analyze"
}
},
{
"id": "error-component-with-error-examples--broken-example-2",
"name": "BrokenExample2",
"description": "This example 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 example.",
"summary": "Mostly working component with one failing example",
"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 example 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 examples (only successful ones)', () => {
const component =
withErrorsFixture.components['success-component-with-mixed-examples'];
const result = formatComponentManifest(component);
expect(result).toMatchInlineSnapshot(`
"<component>
<id>success-component-with-mixed-examples</id>
<name>SuccessWithMixedExamples</name>
<description>
A component that loaded successfully but has some examples that failed to generate.
</description>
<story>
<story_name>Working</story_name>
<story_description>
This example generated successfully.
</story_description>
<story_code>
import { SuccessWithMixedExamples } from '@storybook/design-system';

const Working = () => <SuccessWithMixedExamples text="Hello" />
</story_code>
</story>
<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 examples', () => {
const component =
withErrorsFixture.components['error-component-with-success-examples'];
const result = formatComponentManifest(component);
expect(result).toMatchInlineSnapshot(`
"<component>
<id>error-component-with-success-examples</id>
<name>ErrorWithSuccessExamples</name>
<story>
<story_name>Basic</story_name>
<story_description>
Even though the component parsing failed, this example's code snippet was generated.
</story_description>
<story_code>
const Basic = () => <ErrorWithSuccessExamples>Content</ErrorWithSuccessExamples>
</story_code>
</story>
<story>
<story_name>Advanced</story_name>
<story_description>
Another successfully generated example despite component-level errors.
</story_description>
<story_code>
const Advanced = () => (
<ErrorWithSuccessExamples disabled>
Advanced Content
</ErrorWithSuccessExamples>
)
</story_code>
</story>
</component>"
`);
});

it('should format partial success component (skips failed example)', () => {
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 example.
</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>
<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-examples</id>
<name>SuccessWithMixedExamples</name>
<summary>
Success component with both working and failing examples
</summary>
</component>
<component>
<id>error-component-with-success-examples</id>
<name>ErrorWithSuccessExamples</name>
</component>
<component>
<id>error-component-with-error-examples</id>
<name>ErrorWithErrorExamples</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 example
</summary>
</component>
</components>"
`);
});
});
});
Loading