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 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 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/tools/get-component-documentation.test.ts b/packages/mcp/src/tools/get-component-documentation.test.ts index 804513cb..c1c0d6a8 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 + + Button style variant + + "primary" | "secondary" + false + "primary" + + + disabled + + Disable the button + + boolean + false + + + ", + "type": "text", + }, + ], + } + `); + }); }); diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 3b81ed4d..318f8588 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -1,3 +1,4 @@ +import type { Documentation } from 'react-docgen'; import * as v from 'valibot'; /** @@ -36,7 +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()), + // 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/__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 + + " `; diff --git a/packages/mcp/src/utils/format-manifest.test.ts b/packages/mcp/src/utils/format-manifest.test.ts index 3ebd9050..ec224520 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -408,6 +408,159 @@ 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 + + The visual style variant + + "primary" | "secondary" + false + "primary" + + + disabled + + Whether the button is disabled + + boolean + false + false + + + onClick + + Click handler + + (event: MouseEvent) => void + true + + + " + `); + }); + + 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 + " + `); + }); + }); }); describe('formatComponentManifestMapToList', () => { diff --git a/packages/mcp/src/utils/format-manifest.ts b/packages/mcp/src/utils/format-manifest.ts index 16008ed1..3689a786 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,43 @@ export function formatComponentManifest( } } - // TODO: props section + 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.description !== undefined) { + parts.push(dedent` + ${propInfo.description} + `); + } + + 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}`, + ); + } + + parts.push(''); + } + 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<