From 2aba4ab79ec45f6372e21412744604082930f9e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:40:49 +0000 Subject: [PATCH 1/8] Initial plan From 9c5701310050858995ab3e4e2b743e0e11c3ffd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:52:02 +0000 Subject: [PATCH 2/8] Wire up reactDocgen parsing to format-manifest - Add reactDocgen field to ComponentManifest type - Import and use parseReactDocgen in format-manifest - Format props section with XML structure including all optional fields - Add comprehensive tests for props formatting - All tests passing with 100% coverage on format-manifest.ts Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- packages/mcp/src/types.ts | 1 + .../mcp/src/utils/format-manifest.test.ts | 205 ++++++++++++++++++ packages/mcp/src/utils/format-manifest.ts | 40 +++- 3 files changed, 245 insertions(+), 1 deletion(-) diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 3b81ed4d..d3f397f0 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -37,6 +37,7 @@ export const ComponentManifest = v.object({ summary: v.exactOptional(v.string()), examples: v.exactOptional(v.array(Example)), props: v.exactOptional(v.any()), + reactDocgen: v.exactOptional(v.any()), }); 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 3ebd9050..11288671 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -408,6 +408,211 @@ describe('formatComponentManifest', () => { `); }); }); + + describe('props section', () => { + it('should format props from reactDocgen', () => { + const manifest: ComponentManifest = { + id: 'button', + name: 'Button', + reactDocgen: { + props: { + variant: { + description: 'The visual style variant', + required: false, + defaultValue: { value: '"primary"', computed: false }, + tsType: { + name: 'union', + raw: '"primary" | "secondary"', + elements: [ + { name: 'literal', value: '"primary"' }, + { name: 'literal', value: '"secondary"' }, + ], + }, + }, + disabled: { + description: 'Whether the button is disabled', + required: false, + defaultValue: { value: 'false', computed: false }, + tsType: { + name: 'boolean', + }, + }, + onClick: { + description: 'Click handler', + required: true, + tsType: { + name: 'signature', + type: 'function', + signature: { + arguments: [{ name: 'event', type: { name: 'MouseEvent' } }], + return: { name: 'void' }, + }, + }, + }, + }, + }, + }; + + const result = formatComponentManifest(manifest); + + expect(result).toMatchInlineSnapshot(` + " + button + Button + + + variant + "primary" | "secondary" + false + "primary" + + The visual style variant + + + + disabled + boolean + false + false + + Whether the button is disabled + + + + onClick + (event: MouseEvent) => void + true + + Click handler + + + + " + `); + }); + + it('should handle props with minimal information', () => { + const manifest: ComponentManifest = { + id: 'button', + name: 'Button', + reactDocgen: { + props: { + children: { + tsType: { + name: 'string', + }, + }, + }, + }, + }; + + const result = formatComponentManifest(manifest); + + expect(result).toMatchInlineSnapshot(` + " + button + Button + + + children + string + + + " + `); + }); + + it('should omit props section when reactDocgen is not present', () => { + const manifest: ComponentManifest = { + id: 'button', + name: 'Button', + description: 'A button component', + }; + + const result = formatComponentManifest(manifest); + + expect(result).toMatchInlineSnapshot(` + " + button + Button + + A button component + + " + `); + }); + + it('should omit props section when reactDocgen has no props', () => { + const manifest: ComponentManifest = { + id: 'button', + name: 'Button', + reactDocgen: { + props: {}, + }, + }; + + const result = formatComponentManifest(manifest); + + expect(result).toMatchInlineSnapshot(` + " + button + Button + " + `); + }); + + it('should include all optional fields when present', () => { + const manifest: ComponentManifest = { + id: 'input', + name: 'Input', + reactDocgen: { + props: { + placeholder: { + description: 'Placeholder text', + required: false, + defaultValue: { value: '""', computed: false }, + tsType: { + name: 'string', + }, + }, + maxLength: { + description: 'Maximum input length', + tsType: { + name: 'number', + }, + }, + }, + }, + }; + + const result = formatComponentManifest(manifest); + + expect(result).toMatchInlineSnapshot(` + " + input + Input + + + placeholder + string + false + "" + + Placeholder text + + + + maxLength + number + + Maximum input length + + + + " + `); + }); + }); }); describe('formatComponentManifestMapToList', () => { diff --git a/packages/mcp/src/utils/format-manifest.ts b/packages/mcp/src/utils/format-manifest.ts index 16008ed1..ae963c4e 100644 --- a/packages/mcp/src/utils/format-manifest.ts +++ b/packages/mcp/src/utils/format-manifest.ts @@ -1,5 +1,6 @@ import type { ComponentManifest, ComponentManifestMap } from '../types.ts'; import { dedent } from './dedent.ts'; +import { parseReactDocgen } from './parse-react-docgen.ts'; export function formatComponentManifest( componentManifest: ComponentManifest, @@ -46,7 +47,44 @@ export function formatComponentManifest( } } - // TODO: props section + // Props section - only if reactDocgen is present + if (componentManifest.reactDocgen) { + const parsedDocgen = parseReactDocgen(componentManifest.reactDocgen); + const propEntries = Object.entries(parsedDocgen.props); + + if (propEntries.length > 0) { + parts.push(''); + for (const [propName, propInfo] of propEntries) { + parts.push(dedent` + ${propName}`); + + if (propInfo.type !== undefined) { + parts.push(dedent`${propInfo.type}`); + } + + if (propInfo.required !== undefined) { + parts.push( + dedent`${propInfo.required}`, + ); + } + + if (propInfo.defaultValue !== undefined) { + parts.push( + dedent`${propInfo.defaultValue}`, + ); + } + + if (propInfo.description !== undefined) { + parts.push(dedent` + ${propInfo.description} + `); + } + + parts.push(''); + } + parts.push(''); + } + } parts.push(''); From e5f436465986d00da2d58e14cb92163aa25e9db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:55:14 +0000 Subject: [PATCH 3/8] Update Copilot instructions to document reactDocgen support - Document Component Manifest and ReactDocgen support section - Add details about parsing and formatting flow - Include XML output format examples - Update file structure to reflect current state - Update key components list Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- .github/instructions/mcp.instructions.md | 47 +++++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/.github/instructions/mcp.instructions.md b/.github/instructions/mcp.instructions.md index b9340260..3e22b083 100644 --- a/.github/instructions/mcp.instructions.md +++ b/.github/instructions/mcp.instructions.md @@ -13,7 +13,8 @@ This is a Model Context Protocol (MCP) server for Storybook that serves knowledg ### Key Components - **MCP Server**: Built using the `tmcp` library with HTTP transport -- **Tools System**: Extensible tool registration system (currently includes `list_all_components`) +- **Tools System**: Extensible tool registration system (includes `list_all_components` and `get_component_documentation`) +- **Component Manifest**: Parses and formats component documentation including React prop information from react-docgen - **Schema Validation**: Uses Valibot for JSON schema validation via `@tmcp/adapter-valibot` - **HTTP Transport**: Provides HTTP-based MCP communication via `@tmcp/transport-http` @@ -24,7 +25,15 @@ src/ index.ts # Main entry point - exports createStorybookMcpHandler serve.ts # Development server setup tools/ - list.ts # Tool definitions (e.g., list_all_components) + list-all-components.ts # List all components tool + get-component-documentation.ts # Get component documentation tool + utils/ + format-manifest.ts # Format component manifest to XML + parse-react-docgen.ts # Parse react-docgen output + get-manifest.ts # Fetch and validate manifest + dedent.ts # Template string dedentation + error-to-mcp-content.test.ts # Error formatting utilities + types.ts # TypeScript types and Valibot schemas ``` ### Key Design Patterns @@ -33,6 +42,40 @@ src/ 2. **Tool Registration**: Tools are added to the server using `server.tool()` method 3. **Async Handler**: Returns a Promise-based request handler compatible with standard HTTP servers +### Component Manifest and ReactDocgen Support + +Component manifests can include a `reactDocgen` property containing prop information parsed by [react-docgen](https://github.com/reactjs/react-docgen). This library analyzes React components to extract prop types, descriptions, default values, and other metadata. + +**How it works:** + +1. **Input**: A component manifest may include a `reactDocgen` field containing the raw output from react-docgen's `Documentation` type +2. **Parsing**: The `parseReactDocgen()` utility in `src/utils/parse-react-docgen.ts` converts the react-docgen output into a simplified structure: + - Extracts prop names + - Serializes TypeScript types into readable strings (handles unions, intersections, functions, objects, etc.) + - Includes optional fields: `description`, `type`, `defaultValue`, `required` +3. **Formatting**: The `formatComponentManifest()` function in `src/utils/format-manifest.ts` generates an XML representation of the component including a `` section when `reactDocgen` is present +4. **Output**: Each prop is formatted as: + ```xml + + propName + string | number + false + "default" + + Prop description text + + + ``` + +**Type serialization examples:** +- Unions: `"primary" | "secondary"` +- Functions: `(event: MouseEvent) => void` +- Objects: `{ name: string; age?: number }` +- Arrays: `string[]` +- Generics: `Promise` + +All optional fields (`description`, `type`, `defaultValue`, `required`) are only included in the output when they have defined values. + ## Development Workflow ### Prerequisites From 1c0771e3f51405fc42fde5546f526cd29f2866eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:57:00 +0000 Subject: [PATCH 4/8] Add integration test for reactDocgen in get-component-documentation - Test that props section is included when reactDocgen is present - Verify end-to-end flow from manifest to XML output - All 67 tests passing with >91% coverage Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- .../tools/get-component-documentation.test.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/packages/mcp/src/tools/get-component-documentation.test.ts b/packages/mcp/src/tools/get-component-documentation.test.ts index 804513cb..a354893c 100644 --- a/packages/mcp/src/tools/get-component-documentation.test.ts +++ b/packages/mcp/src/tools/get-component-documentation.test.ts @@ -299,4 +299,93 @@ describe('getComponentDocumentationTool', () => { } `); }); + + it('should include props section when reactDocgen is present', async () => { + const manifestWithReactDocgen = { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + description: 'A button component', + reactDocgen: { + props: { + variant: { + description: 'Button style variant', + required: false, + defaultValue: { value: '"primary"', computed: false }, + tsType: { + name: 'union', + raw: '"primary" | "secondary"', + elements: [ + { name: 'literal', value: '"primary"' }, + { name: 'literal', value: '"secondary"' }, + ], + }, + }, + disabled: { + description: 'Disable the button', + required: false, + tsType: { + name: 'boolean', + }, + }, + }, + }, + }, + }, + }; + + getManifestSpy.mockResolvedValue(manifestWithReactDocgen); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + componentIds: ['button'], + }, + }, + }; + + const response = await server.receive(request); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": " + button + Button + + A button component + + + + variant + "primary" | "secondary" + false + "primary" + + Button style variant + + + + disabled + boolean + false + + Disable the button + + + + ", + "type": "text", + }, + ], + } + `); + }); }); From cb0d158b7abfc8d6f6a88a7e5ae33b3bcb7c4731 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 24 Oct 2025 23:10:04 +0200 Subject: [PATCH 5/8] cleanup --- .../tools/get-component-documentation.test.ts | 10 +-- packages/mcp/src/types.ts | 5 +- .../mcp/src/utils/format-manifest.test.ts | 68 +++---------------- packages/mcp/src/utils/format-manifest.ts | 13 ++-- packages/mcp/src/utils/parse-react-docgen.ts | 3 +- 5 files changed, 23 insertions(+), 76 deletions(-) diff --git a/packages/mcp/src/tools/get-component-documentation.test.ts b/packages/mcp/src/tools/get-component-documentation.test.ts index a354893c..c1c0d6a8 100644 --- a/packages/mcp/src/tools/get-component-documentation.test.ts +++ b/packages/mcp/src/tools/get-component-documentation.test.ts @@ -365,20 +365,20 @@ describe('getComponentDocumentationTool', () => { variant - "primary" | "secondary" - false - "primary" Button style variant + "primary" | "secondary" + false + "primary" disabled - boolean - false Disable the button + boolean + false ", diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index d3f397f0..113f77c3 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -1,3 +1,4 @@ +import type { Documentation } from 'react-docgen/dist/Documentation'; import * as v from 'valibot'; /** @@ -36,8 +37,8 @@ export const ComponentManifest = v.object({ id: v.string(), summary: v.exactOptional(v.string()), examples: v.exactOptional(v.array(Example)), - props: v.exactOptional(v.any()), - reactDocgen: v.exactOptional(v.any()), + // loose schema for react-docgen types, as they are pretty complex + reactDocgen: v.exactOptional(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 11288671..ec224520 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -462,29 +462,29 @@ describe('formatComponentManifest', () => { variant - "primary" | "secondary" - false - "primary" The visual style variant + "primary" | "secondary" + false + "primary" disabled - boolean - false - false Whether the button is disabled + boolean + false + false onClick - (event: MouseEvent) => void - true Click handler + (event: MouseEvent) => void + true " @@ -560,58 +560,6 @@ describe('formatComponentManifest', () => { " `); }); - - it('should include all optional fields when present', () => { - const manifest: ComponentManifest = { - id: 'input', - name: 'Input', - reactDocgen: { - props: { - placeholder: { - description: 'Placeholder text', - required: false, - defaultValue: { value: '""', computed: false }, - tsType: { - name: 'string', - }, - }, - maxLength: { - description: 'Maximum input length', - tsType: { - name: 'number', - }, - }, - }, - }, - }; - - const result = formatComponentManifest(manifest); - - expect(result).toMatchInlineSnapshot(` - " - input - Input - - - placeholder - string - false - "" - - Placeholder text - - - - maxLength - number - - Maximum input length - - - - " - `); - }); }); }); diff --git a/packages/mcp/src/utils/format-manifest.ts b/packages/mcp/src/utils/format-manifest.ts index ae963c4e..3689a786 100644 --- a/packages/mcp/src/utils/format-manifest.ts +++ b/packages/mcp/src/utils/format-manifest.ts @@ -47,7 +47,6 @@ export function formatComponentManifest( } } - // Props section - only if reactDocgen is present if (componentManifest.reactDocgen) { const parsedDocgen = parseReactDocgen(componentManifest.reactDocgen); const propEntries = Object.entries(parsedDocgen.props); @@ -58,6 +57,12 @@ export function formatComponentManifest( parts.push(dedent` ${propName}`); + if (propInfo.description !== undefined) { + parts.push(dedent` + ${propInfo.description} + `); + } + if (propInfo.type !== undefined) { parts.push(dedent`${propInfo.type}`); } @@ -74,12 +79,6 @@ export function formatComponentManifest( ); } - if (propInfo.description !== undefined) { - parts.push(dedent` - ${propInfo.description} - `); - } - parts.push(''); } parts.push(''); diff --git a/packages/mcp/src/utils/parse-react-docgen.ts b/packages/mcp/src/utils/parse-react-docgen.ts index adb8ab6d..93997005 100644 --- a/packages/mcp/src/utils/parse-react-docgen.ts +++ b/packages/mcp/src/utils/parse-react-docgen.ts @@ -1,5 +1,4 @@ -import { type Documentation } from 'react-docgen'; -import { type PropDescriptor } from 'react-docgen/dist/Documentation'; +import type { PropDescriptor, Documentation } from 'react-docgen'; export type ParsedDocgen = { props: Record< From 482df82caa772569c7ca48a3021c88a8bdf1de71 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 24 Oct 2025 23:18:32 +0200 Subject: [PATCH 6/8] update fixtures with react docgen format --- packages/mcp/fixtures/button.fixture.json | 81 ++++-- packages/mcp/fixtures/card.fixture.json | 77 ++++-- .../mcp/fixtures/full-manifest.fixture.json | 246 ++++++++++++------ packages/mcp/fixtures/input.fixture.json | 88 +++++-- .../format-manifest.test.ts.snap | 209 +++++++++++++++ 5 files changed, 547 insertions(+), 154 deletions(-) diff --git a/packages/mcp/fixtures/button.fixture.json b/packages/mcp/fixtures/button.fixture.json index 42cb0ed6..9616bcb6 100644 --- a/packages/mcp/fixtures/button.fixture.json +++ b/packages/mcp/fixtures/button.fixture.json @@ -5,46 +5,75 @@ "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", "import": "import { Button } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "variant": { - "type": "string", - "enum": ["primary", "secondary", "tertiary", "danger"], - "default": "primary", - "description": "The visual style variant of the button" + "description": "The visual style variant of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"primary\" | \"secondary\" | \"tertiary\" | \"danger\"", + "elements": [ + { "name": "literal", "value": "\"primary\"" }, + { "name": "literal", "value": "\"secondary\"" }, + { "name": "literal", "value": "\"tertiary\"" }, + { "name": "literal", "value": "\"danger\"" } + ] + }, + "defaultValue": { "value": "\"primary\"", "computed": false } }, "size": { - "type": "string", - "enum": ["small", "medium", "large"], - "default": "medium", - "description": "The size of the button" + "description": "The size of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } }, "disabled": { - "type": "boolean", - "default": false, - "description": "Whether the button is disabled" + "description": "Whether the button is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "loading": { - "type": "boolean", - "default": false, - "description": "Whether the button is in a loading state" + "description": "Whether the button is in a loading state", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "fullWidth": { - "type": "boolean", - "default": false, - "description": "Whether the button should take up the full width of its container" + "description": "Whether the button should take up the full width of its container", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "onClick": { - "type": "function", - "description": "Callback function when the button is clicked" + "description": "Callback function when the button is clicked", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } }, "children": { - "type": "string", - "description": "The content of the button" + "description": "The content of the button", + "required": true, + "tsType": { "name": "ReactNode" } } - }, - "required": ["children"] + } }, "examples": [ { diff --git a/packages/mcp/fixtures/card.fixture.json b/packages/mcp/fixtures/card.fixture.json index adbafd47..037a186d 100644 --- a/packages/mcp/fixtures/card.fixture.json +++ b/packages/mcp/fixtures/card.fixture.json @@ -5,44 +5,73 @@ "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", "import": "import { Card } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "variant": { - "type": "string", - "enum": ["elevated", "outlined", "flat"], - "default": "elevated", - "description": "The visual style variant of the card" + "description": "The visual style variant of the card", + "required": false, + "tsType": { + "name": "union", + "raw": "\"elevated\" | \"outlined\" | \"flat\"", + "elements": [ + { "name": "literal", "value": "\"elevated\"" }, + { "name": "literal", "value": "\"outlined\"" }, + { "name": "literal", "value": "\"flat\"" } + ] + }, + "defaultValue": { "value": "\"elevated\"", "computed": false } }, "padding": { - "type": "string", - "enum": ["none", "small", "medium", "large"], - "default": "medium", - "description": "The amount of internal padding" + "description": "The amount of internal padding", + "required": false, + "tsType": { + "name": "union", + "raw": "\"none\" | \"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"none\"" }, + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } }, "clickable": { - "type": "boolean", - "default": false, - "description": "Whether the entire card is clickable/interactive" + "description": "Whether the entire card is clickable/interactive", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "header": { - "type": "node", - "description": "Content to display in the card header" + "description": "Content to display in the card header", + "required": false, + "tsType": { "name": "ReactNode" } }, "footer": { - "type": "node", - "description": "Content to display in the card footer" + "description": "Content to display in the card footer", + "required": false, + "tsType": { "name": "ReactNode" } }, "children": { - "type": "node", - "description": "The main content of the card" + "description": "The main content of the card", + "required": true, + "tsType": { "name": "ReactNode" } }, "onClick": { - "type": "function", - "description": "Callback function when the card is clicked (requires clickable=true)" + "description": "Callback function when the card is clicked (requires clickable=true)", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } } - }, - "required": ["children"] + } }, "examples": [ { diff --git a/packages/mcp/fixtures/full-manifest.fixture.json b/packages/mcp/fixtures/full-manifest.fixture.json index 6a9d4095..a536769b 100644 --- a/packages/mcp/fixtures/full-manifest.fixture.json +++ b/packages/mcp/fixtures/full-manifest.fixture.json @@ -7,46 +7,75 @@ "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", "import": "import { Button } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "variant": { - "type": "string", - "enum": ["primary", "secondary", "tertiary", "danger"], - "default": "primary", - "description": "The visual style variant of the button" + "description": "The visual style variant of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"primary\" | \"secondary\" | \"tertiary\" | \"danger\"", + "elements": [ + { "name": "literal", "value": "\"primary\"" }, + { "name": "literal", "value": "\"secondary\"" }, + { "name": "literal", "value": "\"tertiary\"" }, + { "name": "literal", "value": "\"danger\"" } + ] + }, + "defaultValue": { "value": "\"primary\"", "computed": false } }, "size": { - "type": "string", - "enum": ["small", "medium", "large"], - "default": "medium", - "description": "The size of the button" + "description": "The size of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } }, "disabled": { - "type": "boolean", - "default": false, - "description": "Whether the button is disabled" + "description": "Whether the button is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "loading": { - "type": "boolean", - "default": false, - "description": "Whether the button is in a loading state" + "description": "Whether the button is in a loading state", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "fullWidth": { - "type": "boolean", - "default": false, - "description": "Whether the button should take up the full width of its container" + "description": "Whether the button should take up the full width of its container", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "onClick": { - "type": "function", - "description": "Callback function when the button is clicked" + "description": "Callback function when the button is clicked", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } }, "children": { - "type": "string", - "description": "The content of the button" + "description": "The content of the button", + "required": true, + "tsType": { "name": "ReactNode" } } - }, - "required": ["children"] + } }, "examples": [ { @@ -145,44 +174,73 @@ "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", "import": "import { Card } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "variant": { - "type": "string", - "enum": ["elevated", "outlined", "flat"], - "default": "elevated", - "description": "The visual style variant of the card" + "description": "The visual style variant of the card", + "required": false, + "tsType": { + "name": "union", + "raw": "\"elevated\" | \"outlined\" | \"flat\"", + "elements": [ + { "name": "literal", "value": "\"elevated\"" }, + { "name": "literal", "value": "\"outlined\"" }, + { "name": "literal", "value": "\"flat\"" } + ] + }, + "defaultValue": { "value": "\"elevated\"", "computed": false } }, "padding": { - "type": "string", - "enum": ["none", "small", "medium", "large"], - "default": "medium", - "description": "The amount of internal padding" + "description": "The amount of internal padding", + "required": false, + "tsType": { + "name": "union", + "raw": "\"none\" | \"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"none\"" }, + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } }, "clickable": { - "type": "boolean", - "default": false, - "description": "Whether the entire card is clickable/interactive" + "description": "Whether the entire card is clickable/interactive", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "header": { - "type": "node", - "description": "Content to display in the card header" + "description": "Content to display in the card header", + "required": false, + "tsType": { "name": "ReactNode" } }, "footer": { - "type": "node", - "description": "Content to display in the card footer" + "description": "Content to display in the card footer", + "required": false, + "tsType": { "name": "ReactNode" } }, "children": { - "type": "node", - "description": "The main content of the card" + "description": "The main content of the card", + "required": true, + "tsType": { "name": "ReactNode" } }, "onClick": { - "type": "function", - "description": "Callback function when the card is clicked (requires clickable=true)" + "description": "Callback function when the card is clicked (requires clickable=true)", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } } - }, - "required": ["children"] + } }, "examples": [ { @@ -285,52 +343,86 @@ "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", "import": "import { Input } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "type": { - "type": "string", - "enum": ["text", "email", "password", "number", "tel", "url"], - "default": "text", - "description": "The type of input field" + "description": "The type of input field", + "required": false, + "tsType": { + "name": "union", + "raw": "\"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\"", + "elements": [ + { "name": "literal", "value": "\"text\"" }, + { "name": "literal", "value": "\"email\"" }, + { "name": "literal", "value": "\"password\"" }, + { "name": "literal", "value": "\"number\"" }, + { "name": "literal", "value": "\"tel\"" }, + { "name": "literal", "value": "\"url\"" } + ] + }, + "defaultValue": { "value": "\"text\"", "computed": false } }, "label": { - "type": "string", - "description": "The label text for the input" + "description": "The label text for the input", + "required": false, + "tsType": { "name": "string" } }, "placeholder": { - "type": "string", - "description": "Placeholder text shown when the input is empty" + "description": "Placeholder text shown when the input is empty", + "required": false, + "tsType": { "name": "string" } }, "value": { - "type": "string", - "description": "The controlled value of the input" + "description": "The controlled value of the input", + "required": false, + "tsType": { "name": "string" } }, "defaultValue": { - "type": "string", - "description": "The initial value for an uncontrolled input" + "description": "The initial value for an uncontrolled input", + "required": false, + "tsType": { "name": "string" } }, "disabled": { - "type": "boolean", - "default": false, - "description": "Whether the input is disabled" + "description": "Whether the input is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "required": { - "type": "boolean", - "default": false, - "description": "Whether the input is required" + "description": "Whether the input is required", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "error": { - "type": "string", - "description": "Error message to display below the input" + "description": "Error message to display below the input", + "required": false, + "tsType": { "name": "string" } }, "helperText": { - "type": "string", - "description": "Helper text to display below the input" + "description": "Helper text to display below the input", + "required": false, + "tsType": { "name": "string" } }, "onChange": { - "type": "function", - "description": "Callback function when the input value changes" + "description": "Callback function when the input value changes", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { + "name": "event", + "type": { + "name": "ChangeEvent", + "elements": [{ "name": "HTMLInputElement" }] + } + } + ], + "return": { "name": "void" } + } + } } } }, diff --git a/packages/mcp/fixtures/input.fixture.json b/packages/mcp/fixtures/input.fixture.json index 42b92c20..52df3dcc 100644 --- a/packages/mcp/fixtures/input.fixture.json +++ b/packages/mcp/fixtures/input.fixture.json @@ -5,52 +5,86 @@ "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", "import": "import { Input } from '@storybook/design-system';", - "props": { - "type": "object", - "properties": { + "reactDocgen": { + "props": { "type": { - "type": "string", - "enum": ["text", "email", "password", "number", "tel", "url"], - "default": "text", - "description": "The type of input field" + "description": "The type of input field", + "required": false, + "tsType": { + "name": "union", + "raw": "\"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\"", + "elements": [ + { "name": "literal", "value": "\"text\"" }, + { "name": "literal", "value": "\"email\"" }, + { "name": "literal", "value": "\"password\"" }, + { "name": "literal", "value": "\"number\"" }, + { "name": "literal", "value": "\"tel\"" }, + { "name": "literal", "value": "\"url\"" } + ] + }, + "defaultValue": { "value": "\"text\"", "computed": false } }, "label": { - "type": "string", - "description": "The label text for the input" + "description": "The label text for the input", + "required": false, + "tsType": { "name": "string" } }, "placeholder": { - "type": "string", - "description": "Placeholder text shown when the input is empty" + "description": "Placeholder text shown when the input is empty", + "required": false, + "tsType": { "name": "string" } }, "value": { - "type": "string", - "description": "The controlled value of the input" + "description": "The controlled value of the input", + "required": false, + "tsType": { "name": "string" } }, "defaultValue": { - "type": "string", - "description": "The initial value for an uncontrolled input" + "description": "The initial value for an uncontrolled input", + "required": false, + "tsType": { "name": "string" } }, "disabled": { - "type": "boolean", - "default": false, - "description": "Whether the input is disabled" + "description": "Whether the input is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "required": { - "type": "boolean", - "default": false, - "description": "Whether the input is required" + "description": "Whether the input is required", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } }, "error": { - "type": "string", - "description": "Error message to display below the input" + "description": "Error message to display below the input", + "required": false, + "tsType": { "name": "string" } }, "helperText": { - "type": "string", - "description": "Helper text to display below the input" + "description": "Helper text to display below the input", + "required": false, + "tsType": { "name": "string" } }, "onChange": { - "type": "function", - "description": "Callback function when the input value changes" + "description": "Callback function when the input value changes", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { + "name": "event", + "type": { + "name": "ChangeEvent", + "elements": [{ "name": "HTMLInputElement" }] + } + } + ], + "return": { "name": "void" } + } + } } } }, diff --git a/packages/mcp/src/utils/__snapshots__/format-manifest.test.ts.snap b/packages/mcp/src/utils/__snapshots__/format-manifest.test.ts.snap index a9d8a9a7..d546dff7 100644 --- a/packages/mcp/src/utils/__snapshots__/format-manifest.test.ts.snap +++ b/packages/mcp/src/utils/__snapshots__/format-manifest.test.ts.snap @@ -88,6 +88,69 @@ import { Button } from '@storybook/design-system'; const Danger = () => + + +variant + +The visual style variant of the button + +"primary" | "secondary" | "tertiary" | "danger" +false +"primary" + + +size + +The size of the button + +"small" | "medium" | "large" +false +"medium" + + +disabled + +Whether the button is disabled + +boolean +false +false + + +loading + +Whether the button is in a loading state + +boolean +false +false + + +fullWidth + +Whether the button should take up the full width of its container + +boolean +false +false + + +onClick + +Callback function when the button is clicked + +(event: MouseEvent) => void +false + + +children + +The content of the button + +ReactNode +true + + " `; @@ -233,6 +296,67 @@ const UserProfile = () => ( ) + + +variant + +The visual style variant of the card + +"elevated" | "outlined" | "flat" +false +"elevated" + + +padding + +The amount of internal padding + +"none" | "small" | "medium" | "large" +false +"medium" + + +clickable + +Whether the entire card is clickable/interactive + +boolean +false +false + + +header + +Content to display in the card header + +ReactNode +false + + +footer + +Content to display in the card footer + +ReactNode +false + + +children + +The main content of the card + +ReactNode +true + + +onClick + +Callback function when the card is clicked (requires clickable=true) + +(event: MouseEvent) => void +false + + " `; @@ -321,6 +445,91 @@ import { Input } from '@storybook/design-system'; const Disabled = () => + + +type + +The type of input field + +"text" | "email" | "password" | "number" | "tel" | "url" +false +"text" + + +label + +The label text for the input + +string +false + + +placeholder + +Placeholder text shown when the input is empty + +string +false + + +value + +The controlled value of the input + +string +false + + +defaultValue + +The initial value for an uncontrolled input + +string +false + + +disabled + +Whether the input is disabled + +boolean +false +false + + +required + +Whether the input is required + +boolean +false +false + + +error + +Error message to display below the input + +string +false + + +helperText + +Helper text to display below the input + +string +false + + +onChange + +Callback function when the input value changes + +(event: ChangeEvent) => void +false + + " `; From ce7de90b27844ff4174e670c5768f46f2c6d8c7c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 24 Oct 2025 23:23:00 +0200 Subject: [PATCH 7/8] fix type import --- packages/mcp/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 113f77c3..318f8588 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -1,4 +1,4 @@ -import type { Documentation } from 'react-docgen/dist/Documentation'; +import type { Documentation } from 'react-docgen'; import * as v from 'valibot'; /** From fc83cd1c7f50cc0d12bc24ed427c5b38fa52acee Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 24 Oct 2025 23:24:27 +0200 Subject: [PATCH 8/8] add changeset --- .changeset/eleven-buttons-wink.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eleven-buttons-wink.md diff --git a/.changeset/eleven-buttons-wink.md b/.changeset/eleven-buttons-wink.md new file mode 100644 index 00000000..37874001 --- /dev/null +++ b/.changeset/eleven-buttons-wink.md @@ -0,0 +1,6 @@ +--- +'@storybook/addon-mcp': patch +'@storybook/mcp': patch +--- + +include prop types in component documentation tool