Skip to content

Commit bab8ec9

Browse files
committed
add attached docs to component documentation output
1 parent baa0f02 commit bab8ec9

4 files changed

Lines changed: 128 additions & 70 deletions

File tree

.changeset/silent-lamps-argue.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@storybook/mcp': patch
3+
'@storybook/addon-mcp': patch
4+
---
5+
6+
Render component-attached MDX docs entries in markdown output for `get-documentation`.
7+
8+
This fixes a regression where docs attached to components via `component.docs` in `components.json` were not included in markdown responses. The markdown formatter now emits a `## Docs` section below stories (and before props).

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

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -493,77 +493,19 @@ describe('MCP Endpoint E2E Tests', () => {
493493
},
494494
});
495495

496-
expect(response.result).toMatchInlineSnapshot(`
497-
{
498-
"content": [
499-
{
500-
"text": "# Button
501-
502-
ID: example-button
503-
504-
Primary UI component for user interaction
505-
506-
## Stories
507-
508-
### Primary
509-
510-
\`\`\`
511-
import { Button } from "@my-org/my-component-library";
512-
513-
const Primary = () => <Button onClick={fn()} primary label="Button" />;
514-
\`\`\`
515-
516-
### Secondary
517-
518-
\`\`\`
519-
import { Button } from "@my-org/my-component-library";
520-
521-
const Secondary = () => <Button onClick={fn()} label="Button" />;
522-
\`\`\`
523-
524-
### Large
525-
526-
\`\`\`
527-
import { Button } from "@my-org/my-component-library";
528-
529-
const Large = () => <Button onClick={fn()} size="large" label="Button" />;
530-
\`\`\`
531-
532-
### Other Stories
533-
534-
- Small
535-
536-
## Props
496+
expect(response.result).toHaveProperty('content');
497+
expect(response.result.content[0]).toHaveProperty('type', 'text');
537498

538-
\`\`\`
539-
export type Props = {
540-
/**
541-
Is this the principal call to action on the page?
542-
*/
543-
primary?: boolean = false;
544-
/**
545-
What background color to use
546-
*/
547-
backgroundColor?: string;
548-
/**
549-
How large should the button be?
550-
*/
551-
size?: 'small' | 'medium' | 'large' = 'medium';
552-
/**
553-
Button contents
554-
*/
555-
label: string;
556-
/**
557-
Optional click handler
558-
*/
559-
onClick?: () => void;
560-
}
561-
\`\`\`",
562-
"type": "text",
563-
},
564-
],
565-
}
566-
`);
499+
const text = response.result.content[0].text as string;
500+
expect(text).toContain('# Button');
501+
expect(text).toContain('## Stories');
502+
expect(text).toContain('## Docs');
503+
expect(text).toContain('### Additional Information');
504+
expect(text).toContain(
505+
'that the string passed to the `label` prop uses the 🍌-emoji instead of spaces.',
506+
);
507+
expect(text).toContain('<Canvas of={ButtonStories.Primary} />');
508+
expect(text).toContain('## Props');
567509
});
568510

569511
it('should return error for non-existent component', async () => {

packages/mcp/src/utils/manifest-formatter/markdown.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,94 @@ describe('MarkdownFormatter - formatComponentManifest', () => {
385385
});
386386
});
387387

388+
describe('attached docs section', () => {
389+
it('should include attached docs', () => {
390+
const manifest: ComponentManifest = {
391+
id: 'button',
392+
name: 'Button',
393+
path: 'src/components/Button.tsx',
394+
stories: [
395+
{
396+
name: 'Primary',
397+
snippet: '<Button>Primary</Button>',
398+
},
399+
],
400+
docs: {
401+
'button--additional-information': {
402+
id: 'button--additional-information',
403+
name: 'Additional Information',
404+
title: 'Button',
405+
path: 'src/components/Button.mdx',
406+
content: 'Detailed docs content',
407+
},
408+
},
409+
reactDocgen: {
410+
props: {
411+
size: {
412+
type: { name: 'string' },
413+
},
414+
},
415+
},
416+
};
417+
418+
const result = markdownFormatter.formatComponentManifest(manifest);
419+
420+
expect(result).toMatchInlineSnapshot(`
421+
"# Button
422+
423+
ID: button
424+
425+
## Stories
426+
427+
### Primary
428+
429+
\`\`\`
430+
<Button>Primary</Button>
431+
\`\`\`
432+
433+
## Props
434+
435+
\`\`\`
436+
export type Props = {
437+
size: string;
438+
}
439+
\`\`\`
440+
441+
## Docs
442+
443+
### Additional Information
444+
445+
Detailed docs content"
446+
`);
447+
448+
expect(result.indexOf('## Docs')).toBeGreaterThan(result.indexOf('## Stories'));
449+
expect(result.indexOf('## Docs')).toBeGreaterThan(result.indexOf('## Props'));
450+
expect(result).not.toContain('ID: button--additional-information');
451+
});
452+
453+
it('should not include docs section when all attached docs have empty trimmed content', () => {
454+
const manifest: ComponentManifest = {
455+
id: 'button',
456+
name: 'Button',
457+
path: 'src/components/Button.tsx',
458+
docs: {
459+
'button--blank': {
460+
id: 'button--blank',
461+
name: 'Blank Doc',
462+
title: 'Button',
463+
path: 'src/components/ButtonBlank.mdx',
464+
content: ' \n\t ',
465+
},
466+
},
467+
};
468+
469+
const result = markdownFormatter.formatComponentManifest(manifest);
470+
471+
expect(result).not.toContain('## Docs');
472+
expect(result).not.toContain('### Blank Doc');
473+
});
474+
});
475+
388476
describe('props section - table format', () => {
389477
it('should format props with rich metadata as table', () => {
390478
const manifest: ComponentManifest = {

packages/mcp/src/utils/manifest-formatter/markdown.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,26 @@ export const markdownFormatter: ManifestFormatter = {
156156
}
157157
}
158158

159+
// Attached docs section
160+
if (componentManifest.docs && Object.keys(componentManifest.docs).length > 0) {
161+
const docsWithContent = Object.values(componentManifest.docs).filter(
162+
(doc) => doc.content.trim().length > 0,
163+
);
164+
165+
if (docsWithContent.length > 0) {
166+
parts.push('## Docs');
167+
parts.push('');
168+
169+
for (const doc of docsWithContent) {
170+
parts.push(`### ${doc.name}`);
171+
parts.push('');
172+
173+
parts.push(doc.content);
174+
parts.push('');
175+
}
176+
}
177+
}
178+
159179
return parts.join('\n').trim();
160180
},
161181

0 commit comments

Comments
 (0)