Skip to content

Commit 94c01d2

Browse files
authored
Output in markdown instead of XML (#86)
* add e2e tests * improve e2e scripting * add tests for mcp index * add preset tests * add telemetry tests * simplify tool test mocks * simplify mcp-handler tests, improve disableTelemetry handling * add tests for manifest availability * exclude evals from coverage * cleanup * changeset * fix preset registering handlers instead of middlewares * update tests to match changes in base branch * cleanup * await sb process kill * refactor formatter, splitting into markdown and xml, configurable, defaulting to markdown * globally mock storybook deps * clean lock file * fix context arg * fix tests * fix types * "Examples" -> "Stories", simplify tests * simplify tests and types * simplify * use ts-like prop type docs format * add script to clean experiments * add changeset
1 parent 47ab165 commit 94c01d2

36 files changed

+2618
-1343
lines changed

.changeset/config.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": [
11-
"@storybook/mcp-internal-storybook",
12-
"@storybook/mcp-eval",
13-
"@storybook/mcp-eval--*"
14-
]
10+
"ignore": ["@storybook/mcp-internal-storybook", "@storybook/mcp-eval*"]
1511
}

.changeset/four-owls-taste.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@storybook/addon-mcp': patch
3+
'@storybook/mcp': patch
4+
---
5+
6+
Docs toolset: output markdown instead of XML, configurable via experimentalOutput: 'markdown' | 'xml' addon option

.github/copilot-instructions.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ The addon supports configuring which toolsets are enabled:
3535
toolsets: {
3636
dev: true, // get-story-urls, get-ui-building-instructions
3737
docs: true, // list-all-components, get-component-documentation
38-
}
38+
},
39+
experimentalFormat: 'markdown' // Output format: 'markdown' (default) or 'xml'
3940
}
4041
}
4142
```
@@ -73,6 +74,11 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic:
7374
- `onSessionInitialize`: Called when an MCP session is initialized
7475
- `onListAllComponents`: Called when the list-all-components tool is invoked
7576
- `onGetComponentDocumentation`: Called when the get-component-documentation tool is invoked
77+
- **Output Format**: The `format` property in context controls output format:
78+
- `'markdown'` (default): Token-efficient markdown with adaptive formatting
79+
- `'xml'`: Legacy XML format
80+
- Format is configurable via addon options or directly in `StorybookContext`
81+
- Formatters are implemented in `packages/mcp/src/utils/manifest-formatter/` with separate files for XML and markdown
7682

7783
## Development Environment
7884

.github/instructions/addon-mcp.instructions.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,6 @@ This addon implements MCP using `tmcp`:
417417

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

apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts

Lines changed: 61 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -321,30 +321,12 @@ describe('MCP Endpoint E2E Tests', () => {
321321
{
322322
"content": [
323323
{
324-
"text": "<components>
325-
<component>
326-
<id>example-button</id>
327-
<name>Button</name>
328-
<summary>
329-
A customizable button component for user interactions.
330-
</summary>
331-
</component>
332-
<component>
333-
<id>header</id>
334-
<name>Header</name>
335-
</component>
336-
<component>
337-
<id>page</id>
338-
<name>Page</name>
339-
</component>
340-
<component>
341-
<id>other-ui-card</id>
342-
<name>Card</name>
343-
<summary>
344-
Card component with title, image, content, and action button
345-
</summary>
346-
</component>
347-
</components>",
324+
"text": "# Components
325+
326+
- Button (example-button): A customizable button component for user interactions.
327+
- Header (header)
328+
- Page (page)
329+
- Card (other-ui-card): Card component with title, image, content, and action button",
348330
"type": "text",
349331
},
350332
],
@@ -362,9 +344,9 @@ describe('MCP Endpoint E2E Tests', () => {
362344
});
363345

364346
const listText = listResponse.result.content[0].text;
365-
const idMatch = listText.match(/<id>([^<]+)<\/id>/);
347+
// Match markdown format: - ComponentName (component-id)
348+
const idMatch = listText.match(/- \w+ \(([^)]+)\)/);
366349
expect(idMatch).toBeTruthy();
367-
368350
const componentId = idMatch![1];
369351

370352
// Now get documentation for that component
@@ -379,89 +361,72 @@ describe('MCP Endpoint E2E Tests', () => {
379361
{
380362
"content": [
381363
{
382-
"text": "<component>
383-
<id>example-button</id>
384-
<name>Button</name>
385-
<description>
364+
"text": "# Button
365+
366+
ID: example-button
367+
386368
Primary UI component for user interaction
387-
</description>
388-
<story>
389-
<story_name>Primary</story_name>
390-
<story_code>
369+
370+
## Stories
371+
372+
### Primary
373+
374+
\`\`\`
391375
import { Button } from "@my-org/my-component-library";
392376
393377
const Primary = () => <Button onClick={fn()} primary label="Button"></Button>;
394-
</story_code>
395-
</story>
396-
<story>
397-
<story_name>Secondary</story_name>
398-
<story_code>
378+
\`\`\`
379+
380+
### Secondary
381+
382+
\`\`\`
399383
import { Button } from "@my-org/my-component-library";
400384
401385
const Secondary = () => <Button onClick={fn()} label="Button"></Button>;
402-
</story_code>
403-
</story>
404-
<story>
405-
<story_name>Large</story_name>
406-
<story_code>
386+
\`\`\`
387+
388+
### Large
389+
390+
\`\`\`
407391
import { Button } from "@my-org/my-component-library";
408392
409393
const Large = () => <Button onClick={fn()} size="large" label="Button"></Button>;
410-
</story_code>
411-
</story>
412-
<story>
413-
<story_name>Small</story_name>
414-
<story_code>
394+
\`\`\`
395+
396+
### Small
397+
398+
\`\`\`
415399
import { Button } from "@my-org/my-component-library";
416400
417401
const Small = () => <Button onClick={fn()} size="small" label="Button"></Button>;
418-
</story_code>
419-
</story>
420-
<props>
421-
<prop>
422-
<prop_name>primary</prop_name>
423-
<prop_description>
424-
Is this the principal call to action on the page?
425-
</prop_description>
426-
<prop_type>boolean</prop_type>
427-
<prop_required>false</prop_required>
428-
<prop_default>false</prop_default>
429-
</prop>
430-
<prop>
431-
<prop_name>backgroundColor</prop_name>
432-
<prop_description>
433-
What background color to use
434-
</prop_description>
435-
<prop_type>string</prop_type>
436-
<prop_required>false</prop_required>
437-
</prop>
438-
<prop>
439-
<prop_name>size</prop_name>
440-
<prop_description>
441-
How large should the button be?
442-
</prop_description>
443-
<prop_type>'small' | 'medium' | 'large'</prop_type>
444-
<prop_required>false</prop_required>
445-
<prop_default>'medium'</prop_default>
446-
</prop>
447-
<prop>
448-
<prop_name>label</prop_name>
449-
<prop_description>
450-
Button contents
451-
</prop_description>
452-
<prop_type>string</prop_type>
453-
<prop_required>true</prop_required>
454-
</prop>
455-
<prop>
456-
<prop_name>onClick</prop_name>
457-
<prop_description>
458-
Optional click handler
459-
</prop_description>
460-
<prop_type>() => void</prop_type>
461-
<prop_required>false</prop_required>
462-
</prop>
463-
</props>
464-
</component>",
402+
\`\`\`
403+
404+
## Props
405+
406+
\`\`\`
407+
export type Props = {
408+
/**
409+
Is this the principal call to action on the page?
410+
*/
411+
primary?: boolean = false;
412+
/**
413+
What background color to use
414+
*/
415+
backgroundColor?: string;
416+
/**
417+
How large should the button be?
418+
*/
419+
size?: 'small' | 'medium' | 'large' = 'medium';
420+
/**
421+
Button contents
422+
*/
423+
label: string;
424+
/**
425+
Optional click handler
426+
*/
427+
onClick?: () => void;
428+
}
429+
\`\`\`",
465430
"type": "text",
466431
},
467432
],

eval/clean-experiments.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { glob, rm } from 'node:fs/promises';
2+
import * as path from 'node:path';
3+
import { installDependencies } from 'nypm';
4+
5+
const experimentsPaths = await glob('evals/*/experiments');
6+
7+
for await (const experimentsPath of experimentsPaths) {
8+
const relativePath = path.relative(process.cwd(), experimentsPath);
9+
try {
10+
await rm(relativePath, { recursive: true, force: true });
11+
console.log(`Removed: ${relativePath}`);
12+
} catch (error) {
13+
console.error(`Failed to remove ${relativePath}:`, error);
14+
}
15+
}
16+
17+
console.log('Updating lock file...');
18+
await installDependencies();
19+
20+
console.log('Done!');

eval/lib/collect-args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export async function collectArgs() {
182182
case 'components-manifest': {
183183
rerunCommandParts.push(
184184
'--context',
185-
`'${JSON.stringify(parsedArgValues.context.manifestPath)}'`,
185+
JSON.stringify(parsedArgValues.context.manifestPath),
186186
);
187187
return {
188188
type: 'components-manifest',

eval/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"description": "The project for evaluating UI component development with and without Storybook MCP",
66
"type": "module",
77
"scripts": {
8+
"clean-experiments": "node clean-experiments.ts",
89
"eval": "node eval.ts",
910
"storybook": "storybook dev --port 6007 --no-open",
1011
"typecheck": "tsc"

eval/tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@
1313
"DOM.Iterable"
1414
]
1515
},
16-
"include": ["eval", "lib", "templates/result-docs", "google-apps-script.js"]
16+
"include": [
17+
"eval.ts",
18+
"clean-experiments.ts",
19+
"lib",
20+
"templates/result-docs",
21+
"google-apps-script.js"
22+
]
1723
}

packages/addon-mcp/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default {
5050
dev: true, // Tools for story URL retrieval and UI building instructions (default: true)
5151
docs: true, // Tools for component manifest and documentation (default: true, requires experimental feature)
5252
},
53+
experimentalFormat: 'markdown', // Output format: 'markdown' (default) or 'xml'
5354
},
5455
},
5556
],

0 commit comments

Comments
 (0)