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
5 changes: 5 additions & 0 deletions .changeset/large-carrots-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/addon-mcp': patch
---

Add support for docs entries in manifests, sourced by MDX files.
Comment thread
JReinhold marked this conversation as resolved.
17 changes: 17 additions & 0 deletions .changeset/stale-ducks-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
Comment thread
JReinhold marked this conversation as resolved.
'@storybook/mcp': minor
---

Add support for docs entries in manifests, sourced by MDX files.

# Breaking Changes

This change introduces a number of minor breaking changes to `@storybook/mcp`:

1. The lower level tool adder functions have been renamed:
2. `addGetComponentDocumentationTool` -> `addGetDocumentationTool`
3. `addListAllComponentsTool` -> `addListAllDocumentationTool`
4. The optional tool hooks have been renamed:
5. `onListAllComponents` -> `onListAllDocumentation`
6. `onGetComponentDocumentation` -> `onGetDocumentation`
7. The exported `MANIFEST_PATH` constant have been removed in favor of two new constants, `COMPONENT_MANIFEST_PATH` and `DOCS_MANIFEST_PATH`
16 changes: 8 additions & 8 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The addon supports configuring which toolsets are enabled:
options: {
toolsets: {
dev: true, // get-story-urls, get-ui-building-instructions
docs: true, // list-all-components, get-component-documentation
docs: true, // list-all-documentation, get-component-documentation
},
experimentalFormat: 'markdown' // Output format: 'markdown' (default) or 'xml'
}
Expand Down Expand Up @@ -72,8 +72,8 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic:
- Returns the manifest JSON as a string
- **Optional handlers**: `StorybookContext` supports optional handlers that are called at various points, allowing consumers to track usage or collect telemetry:
- `onSessionInitialize`: Called when an MCP session is initialized
- `onListAllComponents`: Called when the list-all-components tool is invoked
- `onGetComponentDocumentation`: Called when the get-component-documentation tool is invoked
- `onListAllDocumentation`: Called when the list-all-documentation tool is invoked
- `onGetDocumentation`: Called when the get-component-documentation tool is invoked
- **Output Format**: The `format` property in context controls output format:
- `'markdown'` (default): Token-efficient markdown with adaptive formatting
- `'xml'`: Legacy XML format
Expand Down Expand Up @@ -261,14 +261,14 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts';
- `AddonContext` extends `StorybookContext` to ensure type compatibility
- Component manifest tools are conditionally registered based on feature flags:
- Checks `features.experimentalComponentsManifest` flag
- Checks for `experimental_componentManifestGenerator` preset
- Only registers `addListAllComponentsTool` and `addGetComponentDocumentationTool` when enabled
- Checks for `experimental_manifests` preset
- Only registers `addListAllDocumentationTool` and `addGetDocumentationTool` when enabled
- Context includes `request` (HTTP Request object) which tools use to determine manifest location
- Default manifest URL is constructed from request origin, replacing `/mcp` with `/manifests/components.json`
- **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 with componentId, and optional foundComponent
- `onListAllDocumentation`: Called when list tool is invoked, receives context and manifest
- `onGetDocumentation`: Called when get tool is invoked, receives context, input with id, and optional foundDocumentation
- Addon-mcp uses these handlers to collect telemetry on tool usage

**Storybook internals used:**
Expand All @@ -278,7 +278,7 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts';
- `storybook/internal/node-logger` - Logging utilities
- Framework detection via `options.presets.apply('framework')`
- Feature flags via `options.presets.apply('features')`
- Component manifest generator via `options.presets.apply('experimental_componentManifestGenerator')`
- Component manifest generator via `options.presets.apply('experimental_manifests')`

**Story URL generation:**

Expand Down
25 changes: 11 additions & 14 deletions .github/instructions/addon-mcp.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The addon supports two toolsets that can be enabled/disabled:
- `get-ui-building-instructions`: Provide UI development guidelines

2. **`docs`** (default: true)
- `list-all-components`: List all available components from manifest
- `list-all-documentation`: List all available components from manifest
- `get-component-documentation`: Get detailed component documentation
- Requires experimental feature flag `features.experimentalComponentsManifest`

Expand Down Expand Up @@ -430,32 +430,29 @@ The addon collects anonymous usage data:

**For addon-specific tools**: Telemetry is collected directly in the tool implementation using `collectTelemetry()`.

**For reused tools from `@storybook/mcp`**: The addon uses optional handlers (`onListAllComponents`, `onGetComponentDocumentation`) provided by the `StorybookContext` to track usage. These handlers are passed in the context when calling `transport.respond()` in `mcp-handler.ts`:
**For reused tools from `@storybook/mcp`**: The addon uses optional handlers (`onListAllDocumentation`, `onGetDocumentation`) provided by the `StorybookContext` to track usage. These handlers are passed in the context when calling `transport.respond()` in `mcp-handler.ts`:

```typescript
const addonContext: AddonContext = {
// ... other context properties
onListAllComponents: async ({ manifest }) => {
onListAllDocumentation: async ({ manifests }) => {
if (!disableTelemetry && server) {
await collectTelemetry({
event: 'tool:listAllComponents',
event: 'tool:listAllDocumentation',
server,
componentCount: Object.keys(manifest.components).length,
componentCount: Object.keys(manifests.componentManifest.components)
.length,
docsCount: Object.keys(manifests.docsManifest.docs).length,
});
}
},
onGetComponentDocumentation: async ({
input,
foundComponents,
notFoundIds,
}) => {
onGetDocumentation: async ({ input, foundDocumentation }) => {
if (!disableTelemetry && server) {
await collectTelemetry({
event: 'tool:getComponentDocumentation',
event: 'tool:getDocumentation',
server,
inputComponentCount: input.componentIds.length,
foundCount: foundComponents.length,
notFoundCount: notFoundIds.length,
id: input.id,
found: !!foundDocumentation,
});
}
},
Expand Down
6 changes: 3 additions & 3 deletions .github/instructions/eval.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,17 +681,17 @@ When using `--context storybook-dev`, the framework:

### Storybook MCP - Docs Pattern (Component Manifest)

When using `--context components.json`, the framework:
When using `--context storybook-docs`, the framework:

1. Reads the manifest file from the eval directory
1. Reads the manifest files from the eval directory, components.json and optionally docs.json
2. Creates `.mcp.json` in project with stdio server config:
```json
{
"mcpServers": {
"storybook-mcp": {
"type": "stdio",
"command": "node",
"args": ["../../packages/mcp/bin.ts", "--manifestPath", "/path/to/components.json"]
"args": ["../../packages/mcp/bin.ts", "--manifestsDir", "/path/to/manifests/dir/"]
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions .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. The `onGetComponentDocumentation` handler receives a single `componentId` input and an optional `foundComponent` result.
- **Context System**: `StorybookContext` allows passing optional handlers (`onSessionInitialize`, `onListAllDocumentation`, `onGetDocumentation`) that are called at various points when provided. The `onGetDocumentation` handler receives a single `id` input and an optional `foundDocumentation` result.

### File Structure

Expand All @@ -26,7 +26,7 @@ src/
index.ts # Main entry point - exports createStorybookMcpHandler
serve.ts # Development server setup
tools/
list-all-components.ts # List all components tool
list-all-documentation.ts # List all documentation tool
get-component-documentation.ts # Get component documentation tool
utils/
format-manifest.ts # Format component manifest to XML
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ curl -X POST http://localhost:13316/mcp \
"id": 2,
"method": "tools/call",
"params": {
"name": "list-all-components",
"name": "list-all-documentation",
"arguments": {}
}
}'
Expand Down
4 changes: 2 additions & 2 deletions apps/internal-storybook/.claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"allow": [
"mcp__storybook-addon-mcp__get-ui-building-instructions",
"mcp__storybook-addon-mcp__get-story-urls",
"mcp__storybook-addon-mcp__list-all-components",
"mcp__storybook-addon-mcp__get-component-documentation"
"mcp__storybook-addon-mcp__list-all-documentation",
"mcp__storybook-addon-mcp__get-documentation"
]
},
"enabledMcpjsonServers": ["storybook-addon-mcp", "storybook-mcp"]
Expand Down
10 changes: 10 additions & 0 deletions apps/internal-storybook/stories/components/Button.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as ButtonStories from './Button.stories';

<Meta of={ButtonStories} name="Additional Information" />

It is critical when using the Button component, that the string passed to the `label` prop uses the 🍌-emoji instead of spaces.

Here is the button:

<Canvas of={ButtonStories.Primary} />
7 changes: 7 additions & 0 deletions apps/internal-storybook/stories/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Getting Started

This is the getting started documentation of this design system.

## Usage

The most critical information to know, is that when using the Button component, you MUST put the 🍇-emoji before and after the component, otherwise it will break.
8 changes: 3 additions & 5 deletions apps/internal-storybook/tests/check-deps.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it } from 'vitest';
import { x } from 'tinyexec';
import path from 'node:path';

const PACKAGES_TO_CHECK = [
'@storybook/addon-docs',
Expand All @@ -15,17 +16,14 @@ describe('Storybook Dependencies', () => {
for (const pkg of PACKAGES_TO_CHECK) {
// Get local version
const listResult = await x('pnpm', ['list', pkg, '--json'], {
nodeOptions: { cwd: process.cwd() },
nodeOptions: { cwd: path.join(import.meta.dirname, '..') },
});

const listData = JSON.parse(listResult.stdout);
const currentVersion =
listData[0]?.devDependencies?.[pkg]?.version ||
listData[0]?.dependencies?.[pkg]?.version;

if (!currentVersion) {
continue;
}

// Get registry version for @next tag
const viewResult = await x('pnpm', ['view', `${pkg}@next`, 'version'], {
nodeOptions: { cwd: process.cwd() },
Expand Down
54 changes: 29 additions & 25 deletions apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
resolve();
return;
}
} catch (error) {

Check warning on line 72 in apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts

View workflow job for this annotation

GitHub Actions / Lint

eslint(no-unused-vars)

Catch parameter 'error' is caught but never used.
// Server not ready yet
}

Expand Down Expand Up @@ -213,30 +213,30 @@
"title": "UI Component Building Instructions",
},
{
"description": "List all available UI components from the component library",
"description": "List all available UI components and documentation entries from the Storybook",
"inputSchema": {
"properties": {},
"type": "object",
},
"name": "list-all-components",
"title": "List All Components",
"name": "list-all-documentation",
"title": "List All Documentation",
},
{
"description": "Get detailed documentation for a specific UI component",
"description": "Get detailed documentation for a specific UI component or docs entry",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"componentId": {
"id": {
"type": "string",
},
},
"required": [
"componentId",
"id",
],
"type": "object",
},
"name": "get-component-documentation",
"title": "Get Documentation for Component",
"name": "get-documentation",
"title": "Get Documentation",
},
]
`);
Expand Down Expand Up @@ -310,10 +310,10 @@
});
});

describe('Tool: list-all-components', () => {
it('should list all components from manifest', async () => {
describe('Tool: list-all-documentation', () => {
it('should list all documentation from manifest', async () => {
const response = await mcpRequest('tools/call', {
name: 'list-all-components',
name: 'list-all-documentation',
arguments: {},
});

Expand All @@ -326,7 +326,11 @@
- Button (example-button): A customizable button component for user interactions.
- Header (header)
- Page (page)
- Card (other-ui-card): Card component with title, image, content, and action button",
- Card (other-ui-card): Card component with title, image, content, and action button

# Docs

- getting-started (getting-started--docs): # Getting Started This is the getting started documentation of this design system. ## Usag...",
"type": "text",
},
],
Expand All @@ -335,11 +339,11 @@
});
});

describe('Tool: get-component-documentation', () => {
describe('Tool: get-documentation', () => {
it('should return documentation for a specific component', async () => {
// First, get the list to find a valid component ID
const listResponse = await mcpRequest('tools/call', {
name: 'list-all-components',
name: 'list-all-documentation',
arguments: {},
});

Expand All @@ -351,9 +355,9 @@

// Now get documentation for that component
const response = await mcpRequest('tools/call', {
name: 'get-component-documentation',
name: 'get-documentation',
arguments: {
componentId,
id: componentId,
},
});

Expand All @@ -374,31 +378,31 @@
\`\`\`
import { Button } from "@my-org/my-component-library";

const Primary = () => <Button onClick={fn()} primary label="Button"></Button>;
const Primary = () => <Button onClick={fn()} primary label="Button" />;
\`\`\`

### Secondary

\`\`\`
import { Button } from "@my-org/my-component-library";

const Secondary = () => <Button onClick={fn()} label="Button"></Button>;
const Secondary = () => <Button onClick={fn()} label="Button" />;
\`\`\`

### Large

\`\`\`
import { Button } from "@my-org/my-component-library";

const Large = () => <Button onClick={fn()} size="large" label="Button"></Button>;
const Large = () => <Button onClick={fn()} size="large" label="Button" />;
\`\`\`

### Small

\`\`\`
import { Button } from "@my-org/my-component-library";

const Small = () => <Button onClick={fn()} size="small" label="Button"></Button>;
const Small = () => <Button onClick={fn()} size="small" label="Button" />;
\`\`\`

## Props
Expand Down Expand Up @@ -436,17 +440,17 @@

it('should return error for non-existent component', async () => {
const response = await mcpRequest('tools/call', {
name: 'get-component-documentation',
name: 'get-documentation',
arguments: {
componentId: 'non-existent-component-id',
id: 'non-existent-component-id',
},
});

expect(response.result).toMatchInlineSnapshot(`
{
"content": [
{
"text": "Component not found: "non-existent-component-id". Use the list-all-components tool to see available components.",
"text": "Component or Docs Entry not found: "non-existent-component-id". Use the list-all-documentation tool to see available components and documentation entries.",
"type": "text",
},
],
Expand Down Expand Up @@ -499,8 +503,8 @@

expect(toolNames).toMatchInlineSnapshot(`
[
"list-all-components",
"get-component-documentation",
"list-all-documentation",
"get-documentation",
]
`);
});
Expand Down
2 changes: 1 addition & 1 deletion eval/clean-experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { glob, rm } from 'node:fs/promises';
import * as path from 'node:path';
import { installDependencies } from 'nypm';

const experimentsPaths = await glob('evals/*/experiments');
const experimentsPaths = glob('evals/*/experiments');

for await (const experimentsPath of experimentsPaths) {
const relativePath = path.relative(process.cwd(), experimentsPath);
Expand Down
Loading
Loading