Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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 }
}
}
},
"examples": [
{
"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"
},
"examples": [
{
"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"
},
"examples": [
{
"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" }
}
}
},
"examples": [
{
"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>
<example>
<example_name>Working</example_name>
<example_description>
This example generated successfully.
</example_description>
<example_code>
import { SuccessWithMixedExamples } from '@storybook/design-system';

const Working = () => <SuccessWithMixedExamples text="Hello" />
</example_code>
</example>
<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>
<example>
<example_name>Basic</example_name>
<example_description>
Even though the component parsing failed, this example's code snippet was generated.
</example_description>
<example_code>
const Basic = () => <ErrorWithSuccessExamples>Content</ErrorWithSuccessExamples>
</example_code>
</example>
<example>
<example_name>Advanced</example_name>
<example_description>
Another successfully generated example despite component-level errors.
</example_description>
<example_code>
const Advanced = () => (
<ErrorWithSuccessExamples disabled>
Advanced Content
</ErrorWithSuccessExamples>
)
</example_code>
</example>
</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>
<example>
<example_name>Default</example_name>
<example_description>
Default usage of the component.
</example_description>
<example_code>
import { PartialSuccess } from '@storybook/design-system';

const Default = () => <PartialSuccess title="Hello" />
</example_code>
</example>
<example>
<example_name>With Subtitle</example_name>
<example_description>
Component with both title and subtitle.
</example_description>
<example_code>
import { PartialSuccess } from '@storybook/design-system';

const WithSubtitle = () => <PartialSuccess title="Hello" subtitle="World" />
</example_code>
</example>
<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