Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
36a9e1d
add e2e tests
JReinhold Nov 18, 2025
310f3a1
improve e2e scripting
JReinhold Nov 19, 2025
c153b93
add tests for mcp index
JReinhold Nov 19, 2025
f3a467f
add preset tests
JReinhold Nov 19, 2025
d67a038
add telemetry tests
JReinhold Nov 19, 2025
c807fe7
simplify tool test mocks
JReinhold Nov 19, 2025
61baa0f
simplify mcp-handler tests, improve disableTelemetry handling
JReinhold Nov 19, 2025
e01da53
add tests for manifest availability
JReinhold Nov 19, 2025
1d4fbfd
exclude evals from coverage
JReinhold Nov 19, 2025
1cc6bbd
cleanup
JReinhold Nov 19, 2025
23daa64
changeset
JReinhold Nov 19, 2025
54d5157
Merge branch 'next' of https://github.com/storybookjs/mcp into improv…
JReinhold Nov 19, 2025
ba47326
fix preset registering handlers instead of middlewares
JReinhold Nov 19, 2025
35859a4
update tests to match changes in base branch
JReinhold Nov 19, 2025
f38a3bf
cleanup
JReinhold Nov 19, 2025
85c6076
await sb process kill
JReinhold Nov 19, 2025
d615a8d
refactor formatter, splitting into markdown and xml, configurable, de…
JReinhold Nov 19, 2025
02e2914
globally mock storybook deps
JReinhold Nov 19, 2025
6f01bc6
clean lock file
JReinhold Nov 19, 2025
4d1c0c2
Merge branch 'next' of https://github.com/storybookjs/mcp into improv…
JReinhold Nov 19, 2025
716e1f3
Merge branch 'improve-test-coverage' of https://github.com/storybookj…
JReinhold Nov 19, 2025
58e0a75
fix context arg
JReinhold Nov 19, 2025
920dea2
fix tests
JReinhold Nov 19, 2025
26a9874
fix types
JReinhold Nov 19, 2025
ca77d82
"Examples" -> "Stories", simplify tests
JReinhold Nov 19, 2025
d6065b5
simplify tests and types
JReinhold Nov 19, 2025
18a51dd
simplify
JReinhold Nov 20, 2025
c7150af
use ts-like prop type docs format
JReinhold Nov 20, 2025
80a488e
add script to clean experiments
JReinhold Nov 20, 2025
7ed1dfd
add changeset
JReinhold Nov 20, 2025
8cf05b5
Merge branch 'next' of https://github.com/storybookjs/mcp into markdo…
JReinhold Nov 20, 2025
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: 1 addition & 5 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@storybook/mcp-internal-storybook",
"@storybook/mcp-eval",
"@storybook/mcp-eval--*"
]
"ignore": ["@storybook/mcp-internal-storybook", "@storybook/mcp-eval*"]
}
6 changes: 6 additions & 0 deletions .changeset/four-owls-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@storybook/addon-mcp': patch
'@storybook/mcp': patch
---

Docs toolset: output markdown instead of XML, configurable via experimentalOutput: 'markdown' | 'xml' addon option
8 changes: 7 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ The addon supports configuring which toolsets are enabled:
toolsets: {
dev: true, // get-story-urls, get-ui-building-instructions
docs: true, // list-all-components, get-component-documentation
}
},
experimentalFormat: 'markdown' // Output format: 'markdown' (default) or 'xml'
}
}
```
Expand Down Expand Up @@ -73,6 +74,11 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic:
- `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
- **Output Format**: The `format` property in context controls output format:
- `'markdown'` (default): Token-efficient markdown with adaptive formatting
- `'xml'`: Legacy XML format
- Format is configurable via addon options or directly in `StorybookContext`
- Formatters are implemented in `packages/mcp/src/utils/manifest-formatter/` with separate files for XML and markdown

## Development Environment

Expand Down
1 change: 0 additions & 1 deletion .github/instructions/addon-mcp.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,6 @@ This addon implements MCP using `tmcp`:

- `storybook` - Peer dependency (Storybook framework)
- `valibot` - Schema validation for tool inputs/outputs
- `ts-dedent` - Template string formatting
- `tsdown` - Build tool (rolldown-based)
- `vite` - Peer dependency for middleware injection

Expand Down
157 changes: 61 additions & 96 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 @@ -321,30 +321,12 @@
{
"content": [
{
"text": "<components>
<component>
<id>example-button</id>
<name>Button</name>
<summary>
A customizable button component for user interactions.
</summary>
</component>
<component>
<id>header</id>
<name>Header</name>
</component>
<component>
<id>page</id>
<name>Page</name>
</component>
<component>
<id>other-ui-card</id>
<name>Card</name>
<summary>
Card component with title, image, content, and action button
</summary>
</component>
</components>",
"text": "# Components

- 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",
"type": "text",
},
],
Expand All @@ -362,9 +344,9 @@
});

const listText = listResponse.result.content[0].text;
const idMatch = listText.match(/<id>([^<]+)<\/id>/);
// Match markdown format: - ComponentName (component-id)
const idMatch = listText.match(/- \w+ \(([^)]+)\)/);
expect(idMatch).toBeTruthy();

const componentId = idMatch![1];

// Now get documentation for that component
Expand All @@ -379,89 +361,72 @@
{
"content": [
{
"text": "<component>
<id>example-button</id>
<name>Button</name>
<description>
"text": "# Button

ID: example-button

Primary UI component for user interaction
</description>
<story>
<story_name>Primary</story_name>
<story_code>

## Stories

### Primary

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

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

### Secondary

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

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

### Large

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

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

### Small

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

const Small = () => <Button onClick={fn()} size="small" label="Button"></Button>;
</story_code>
</story>
<props>
<prop>
<prop_name>primary</prop_name>
<prop_description>
Is this the principal call to action on the page?
</prop_description>
<prop_type>boolean</prop_type>
<prop_required>false</prop_required>
<prop_default>false</prop_default>
</prop>
<prop>
<prop_name>backgroundColor</prop_name>
<prop_description>
What background color to use
</prop_description>
<prop_type>string</prop_type>
<prop_required>false</prop_required>
</prop>
<prop>
<prop_name>size</prop_name>
<prop_description>
How large should the button be?
</prop_description>
<prop_type>'small' | 'medium' | 'large'</prop_type>
<prop_required>false</prop_required>
<prop_default>'medium'</prop_default>
</prop>
<prop>
<prop_name>label</prop_name>
<prop_description>
Button contents
</prop_description>
<prop_type>string</prop_type>
<prop_required>true</prop_required>
</prop>
<prop>
<prop_name>onClick</prop_name>
<prop_description>
Optional click handler
</prop_description>
<prop_type>() => void</prop_type>
<prop_required>false</prop_required>
</prop>
</props>
</component>",
\`\`\`

## Props

\`\`\`
export type Props = {
/**
Is this the principal call to action on the page?
*/
primary?: boolean = false;
/**
What background color to use
*/
backgroundColor?: string;
/**
How large should the button be?
*/
size?: 'small' | 'medium' | 'large' = 'medium';
/**
Button contents
*/
label: string;
/**
Optional click handler
*/
onClick?: () => void;
}
\`\`\`",
"type": "text",
},
],
Expand Down
20 changes: 20 additions & 0 deletions eval/clean-experiments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { glob, rm } from 'node:fs/promises';
import * as path from 'node:path';
import { installDependencies } from 'nypm';

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

Check warning on line 5 in eval/clean-experiments.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(await-thenable)

Unexpected `await` of a non-Promise (non-"Thenable") value.

for await (const experimentsPath of experimentsPaths) {
const relativePath = path.relative(process.cwd(), experimentsPath);
try {
await rm(relativePath, { recursive: true, force: true });
console.log(`Removed: ${relativePath}`);
} catch (error) {
console.error(`Failed to remove ${relativePath}:`, error);
}
}

console.log('Updating lock file...');
await installDependencies();

console.log('Done!');
2 changes: 1 addition & 1 deletion eval/lib/collect-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export async function collectArgs() {
case 'components-manifest': {
rerunCommandParts.push(
'--context',
`'${JSON.stringify(parsedArgValues.context.manifestPath)}'`,
JSON.stringify(parsedArgValues.context.manifestPath),
Comment thread
JReinhold marked this conversation as resolved.
);
return {
type: 'components-manifest',
Expand Down
1 change: 1 addition & 0 deletions eval/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "The project for evaluating UI component development with and without Storybook MCP",
"type": "module",
"scripts": {
"clean-experiments": "node clean-experiments.ts",
"eval": "node eval.ts",
"storybook": "storybook dev --port 6007 --no-open",
"typecheck": "tsc"
Expand Down
8 changes: 7 additions & 1 deletion eval/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
"DOM.Iterable"
]
},
"include": ["eval", "lib", "templates/result-docs", "google-apps-script.js"]
"include": [
"eval.ts",
"clean-experiments.ts",
"lib",
"templates/result-docs",
"google-apps-script.js"
]
}
1 change: 1 addition & 0 deletions packages/addon-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default {
dev: true, // Tools for story URL retrieval and UI building instructions (default: true)
docs: true, // Tools for component manifest and documentation (default: true, requires experimental feature)
},
experimentalFormat: 'markdown', // Output format: 'markdown' (default) or 'xml'
},
},
],
Expand Down
3 changes: 1 addition & 2 deletions packages/addon-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@
"valibot": "catalog:"
},
"devDependencies": {
"storybook": "catalog:",
"ts-dedent": "^2.2.0"
"storybook": "catalog:"
},
"peerDependencies": {
"storybook": "^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0"
Expand Down
6 changes: 6 additions & 0 deletions packages/addon-mcp/src/mcp-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ describe('getToolsets', () => {
dev: true,
docs: false,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand All @@ -418,6 +419,7 @@ describe('getToolsets', () => {
dev: true,
docs: true,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand All @@ -439,6 +441,7 @@ describe('getToolsets', () => {
dev: false,
docs: false,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand All @@ -460,6 +463,7 @@ describe('getToolsets', () => {
dev: false,
docs: false,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand All @@ -481,6 +485,7 @@ describe('getToolsets', () => {
dev: false,
docs: false,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand All @@ -500,6 +505,7 @@ describe('getToolsets', () => {
dev: true,
docs: true,
},
experimentalFormat: 'markdown' as const,
};

const result = getToolsets(request, addonOptions);
Expand Down
1 change: 1 addition & 0 deletions packages/addon-mcp/src/mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const mcpServerHandler = async ({
const addonContext: AddonContext = {
options,
toolsets: getToolsets(webRequest, addonOptions),
format: addonOptions.experimentalFormat,
origin: origin!,
disableTelemetry: disableTelemetry!,
request: webRequest,
Expand Down
2 changes: 2 additions & 0 deletions packages/addon-mcp/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const experimental_devServer: PresetProperty<
// ValiError: Invalid type: Expected boolean but received "false"
const addonOptions = v.parse(AddonOptions, {
toolsets: 'toolsets' in options ? options.toolsets : {},
experimentalFormat:
'experimentalFormat' in options ? options.experimentalFormat : 'markdown',
});

app!.post('/mcp', (req, res) =>
Expand Down
1 change: 1 addition & 0 deletions packages/addon-mcp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const AddonOptions = v.object({
docs: true,
},
),
experimentalFormat: v.optional(v.picklist(['xml', 'markdown']), 'markdown'),
});

export type AddonOptionsInput = v.InferInput<typeof AddonOptions>;
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('bin.ts stdio MCP server', () => {
content: [
{
type: 'text',
text: expect.stringContaining('<components>'),
text: expect.stringContaining('# Components'),
},
],
},
Expand Down
Loading
Loading