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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@storybook/mcp-internal-storybook"]
"ignore": [
"@storybook/mcp-internal-storybook",
"@storybook/mcp-eval",
"@storybook/mcp-eval--*"
]
}
5 changes: 5 additions & 0 deletions .changeset/petite-toes-dig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/mcp': patch
---

get-component-documentation now only handles one component at a time
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts';
- **Optional handlers for tracking**:
- `onSessionInitialize`: Called when an MCP session is initialized, receives context
- `onListAllComponents`: Called when list tool is invoked, receives context and manifest
- `onGetComponentDocumentation`: Called when get tool is invoked, receives context, input, found components, and not found IDs
- `onGetComponentDocumentation`: Called when get tool is invoked, receives context, input with componentId, and optional foundComponent
- Addon-mcp uses these handlers to collect telemetry on tool usage

**Storybook internals used:**
Expand Down
8 changes: 6 additions & 2 deletions .github/instructions/eval.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,16 @@ The framework supports four distinct context types:
- Tests baseline agent capabilities

2. **Component Manifest** (`--context components.json`):
- Provides component documentation via `@storybook/mcp`
- Provides component documentation via `@storybook/mcp` package
- Uses stdio transport with `packages/mcp/bin.ts`
- Best for testing agents with library/component documentation
- This uses the Storybook MCP server, not a custom MCP server

3. **MCP Server** (`--context mcp.config.json` or inline JSON):
- Custom MCP server configuration (HTTP or stdio)
- Supports multiple named servers
- Flexible for testing different MCP tool combinations
- Use this for fully custom MCP servers; use components.json for Storybook MCP

4. **Extra Prompts** (`--context extra-prompt-01.md,extra-prompt-02.md`):
- Appends additional markdown files to main prompt
Expand All @@ -106,9 +108,11 @@ node eval.ts

**Non-interactive mode:**
```bash
node eval.ts --agent claude-code --context components.json --upload 100-flight-booking-plain
node eval.ts --agent claude-code --context components.json --upload --no-storybook 100-flight-booking-plain
```

**IMPORTANT**: Always use the `--no-storybook` flag when running evals to prevent the process from hanging at the end waiting for user input about starting Storybook.

**Get help:**
```bash
node eval.ts --help
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/mcp.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This is a Model Context Protocol (MCP) server for Storybook that serves knowledg
- **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`
- **Context System**: `StorybookContext` allows passing optional handlers (`onSessionInitialize`, `onListAllComponents`, `onGetComponentDocumentation`) that are called at various points when provided
- **Context System**: `StorybookContext` allows passing optional handlers (`onSessionInitialize`, `onListAllComponents`, `onGetComponentDocumentation`) that are called at various points when provided. The `onGetComponentDocumentation` handler receives a single `componentId` input and an optional `foundComponent` result.

### File Structure

Expand Down
4 changes: 2 additions & 2 deletions eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ node eval.ts --agent claude-code --context components.json --upload 100-flight-b
The framework supports four context modes:

1. **No context** (`--no-context`): Agent uses only default tools
2. **Component manifest** (`--context components.json`): Provides component documentation via `@storybook/mcp`
3. **MCP server config** (`--context mcp.config.json` or inline JSON): Custom MCP server setup
2. **Component manifest** (`--context components.json`): Provides component documentation via the `@storybook/mcp` package
3. **MCP server config** (`--context mcp.config.json` or inline JSON): Custom MCP server setup (use this for fully custom MCP servers, not for Storybook MCP)
4. **Extra prompts** (`--context extra-prompt-01.md,extra-prompt-02.md`): Additional markdown files appended to main prompt

## Project Structure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export const Submitted: Story = {
await userEvent.click(returnToggle);
});

await step('Select fromt flight', async () => {
await step('Select from flight', async () => {
const fromFlightTrigger = (
await looseGetInteractiveElements('flight-trigger-from', 'From', step)
)[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export const Submitted: Story = {
await userEvent.click(returnToggle);
});

await step('Select fromt flight', async () => {
await step('Select from flight', async () => {
const fromFlightTrigger = (
await looseGetInteractiveElements('flight-trigger-from', 'From', step)
)[0];
Expand Down
2 changes: 0 additions & 2 deletions eval/evals/130-flight-booking-rsuite/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import type { Hooks } from '../../types.ts';
import { addDependency } from 'nypm';

Expand Down
4 changes: 2 additions & 2 deletions packages/addon-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ Returns a list of all available UI components in your component library. Useful

#### 4. Get Component Documentation (`get-component-documentation`)

Retrieves detailed documentation for specific components, including:
Retrieves detailed documentation for a specific component, including:

- Component documentation
- Usage examples

The agent provides component IDs to retrieve their documentation.
The agent provides a component ID to retrieve its documentation. To get documentation for multiple components, call this tool multiple times.

## Contributing

Expand Down
8 changes: 3 additions & 5 deletions packages/addon-mcp/src/mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,13 @@ export const mcpServerHandler = async ({
},
onGetComponentDocumentation: async ({
input,
foundComponents,
notFoundIds,
foundComponent,
}) => {
await collectTelemetry({
event: 'tool:getComponentDocumentation',
server,
inputComponentCount: input.componentIds.length,
foundCount: foundComponents.length,
notFoundCount: notFoundIds.length,
componentId: input.componentId,
found: !!foundComponent,
});
},
}),
Expand Down
168 changes: 8 additions & 160 deletions packages/mcp/src/tools/get-component-documentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('getComponentDocumentationTool', () => {
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button'],
componentId: 'button',
},
},
};
Expand Down Expand Up @@ -89,88 +89,6 @@ describe('getComponentDocumentationTool', () => {
`);
});

it('should return formatted documentation for multiple components', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'tools/call',
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button', 'card', 'input'],
},
},
};

const response = await server.receive(request);

expect(response.result).toMatchInlineSnapshot(`
{
"content": [
{
"text": "<component>
<id>button</id>
<name>Button</name>
<story>
<story_name>Primary</story_name>
<story_description>
The primary button variant.
</story_description>
<story_code>
const Primary = () => <Button variant="primary">Click Me</Button>
</story_code>
</story>
</component>",
"type": "text",
},
{
"text": "<component>
<id>card</id>
<name>Card</name>
<description>
A container component for grouping related content.
</description>
<story>
<story_name>Basic</story_name>
<story_description>
A basic card with content.
</story_description>
<story_code>
const Basic = () => (
<Card>
<h3>Title</h3>
<p>Content</p>
</Card>
)
</story_code>
</story>
</component>",
"type": "text",
},
{
"text": "<component>
<id>input</id>
<name>Input</name>
<description>
A text input component with validation support.
</description>
<story>
<story_name>Basic</story_name>
<story_description>
A basic text input.
</story_description>
<story_code>
const Basic = () => <Input label="Name" placeholder="Enter name" />
</story_code>
</story>
</component>",
"type": "text",
},
],
}
`);
});

it('should return an error when a component is not found', async () => {
const request = {
jsonrpc: '2.0' as const,
Expand All @@ -179,7 +97,7 @@ describe('getComponentDocumentationTool', () => {
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['nonexistent'],
componentId: 'nonexistent',
},
},
};
Expand All @@ -190,7 +108,7 @@ describe('getComponentDocumentationTool', () => {
{
"content": [
{
"text": "Error: Component not found: nonexistent",
"text": "Component not found: "nonexistent". Use the list-all-components tool to see available components.",
"type": "text",
},
],
Expand All @@ -199,72 +117,6 @@ describe('getComponentDocumentationTool', () => {
`);
});

it('should return partial results and a warning when some components are not found', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'tools/call',
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button', 'nonexistent', 'card'],
},
},
};

const response = await server.receive(request);
expect(response.result).toMatchInlineSnapshot(`
{
"content": [
{
"text": "<component>
<id>button</id>
<name>Button</name>
<story>
<story_name>Primary</story_name>
<story_description>
The primary button variant.
</story_description>
<story_code>
const Primary = () => <Button variant="primary">Click Me</Button>
</story_code>
</story>
</component>",
"type": "text",
},
{
"text": "<component>
<id>card</id>
<name>Card</name>
<description>
A container component for grouping related content.
</description>
<story>
<story_name>Basic</story_name>
<story_description>
A basic card with content.
</story_description>
<story_code>
const Basic = () => (
<Card>
<h3>Title</h3>
<p>Content</p>
</Card>
)
</story_code>
</story>
</component>",
"type": "text",
},
{
"text": "Warning: Component not found: nonexistent",
"type": "text",
},
],
}
`);
});

it('should handle fetch errors gracefully', async () => {
getManifestSpy.mockRejectedValue(
new getManifest.ManifestGetError(
Expand All @@ -280,7 +132,7 @@ describe('getComponentDocumentationTool', () => {
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button'],
componentId: 'button',
},
},
};
Expand Down Expand Up @@ -310,7 +162,7 @@ describe('getComponentDocumentationTool', () => {
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button', 'card', 'non-existent'],
componentId: 'button',
},
},
};
Expand All @@ -325,12 +177,8 @@ describe('getComponentDocumentationTool', () => {
context: expect.objectContaining({
onGetComponentDocumentation: handler,
}),
input: { componentIds: ['button', 'card', 'non-existent'] },
foundComponents: [
expect.objectContaining({ id: 'button', name: 'Button' }),
expect.objectContaining({ id: 'card', name: 'Card' }),
],
notFoundIds: ['non-existent'],
input: { componentId: 'button' },
foundComponent: expect.objectContaining({ id: 'button', name: 'Button' }),
});
});

Expand Down Expand Up @@ -379,7 +227,7 @@ describe('getComponentDocumentationTool', () => {
params: {
name: GET_TOOL_NAME,
arguments: {
componentIds: ['button'],
componentId: 'button',
},
},
};
Expand Down
Loading
Loading