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
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 @@ -112,15 +112,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