Skip to content

Commit 7fc2ad9

Browse files
committed
Render subcomponents in get-documentation
1 parent 70b5b23 commit 7fc2ad9

File tree

4 files changed

+301
-49
lines changed

4 files changed

+301
-49
lines changed

packages/mcp/src/tools/get-documentation.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,100 @@ describe('getDocumentationTool', () => {
381381
`);
382382
});
383383

384+
it('should include subcomponents in get-documentation output', async () => {
385+
getManifestsSpy.mockResolvedValue({
386+
componentManifest: {
387+
v: 1,
388+
components: {
389+
'combo-box': {
390+
id: 'combo-box',
391+
name: 'ComboBox',
392+
path: 'src/components/ComboBox.tsx',
393+
description: 'A combo box component',
394+
subcomponents: {
395+
Item: {
396+
name: 'ComboBoxItem',
397+
path: 'src/components/ComboBoxItem.tsx',
398+
description: 'Use for individual options.',
399+
import: 'import { ComboBoxItem } from "@/components";',
400+
reactComponentMeta: {
401+
displayName: 'ComboBoxItem',
402+
filePath: 'src/components/ComboBoxItem.tsx',
403+
description: '',
404+
exportName: 'ComboBoxItem',
405+
props: {
406+
textValue: {
407+
name: 'textValue',
408+
description: 'Required when children are not plain text.',
409+
required: false,
410+
defaultValue: null,
411+
type: {
412+
name: 'string',
413+
},
414+
},
415+
},
416+
},
417+
},
418+
},
419+
},
420+
},
421+
},
422+
});
423+
424+
const request = {
425+
jsonrpc: '2.0' as const,
426+
id: 1,
427+
method: 'tools/call',
428+
params: {
429+
name: GET_TOOL_NAME,
430+
arguments: {
431+
id: 'combo-box',
432+
},
433+
},
434+
};
435+
436+
const mockHttpRequest = new Request('https://example.com/mcp');
437+
const response = await server.receive(request, {
438+
custom: { request: mockHttpRequest },
439+
});
440+
441+
expect(response.result).toMatchInlineSnapshot(`
442+
{
443+
"content": [
444+
{
445+
"text": "# ComboBox
446+
447+
ID: combo-box
448+
449+
A combo box component
450+
451+
## Subcomponents
452+
453+
### ComboBoxItem
454+
455+
Use for individual options.
456+
457+
\`\`\`
458+
import { ComboBoxItem } from "@/components";
459+
\`\`\`
460+
461+
#### Props
462+
463+
\`\`\`
464+
export type ComboBoxItemProps = {
465+
/**
466+
Required when children are not plain text.
467+
*/
468+
textValue?: string;
469+
}
470+
\`\`\`",
471+
"type": "text",
472+
},
473+
],
474+
}
475+
`);
476+
});
477+
384478
describe('multi-source mode', () => {
385479
const sources = [
386480
{ id: 'local', title: 'Local' },

packages/mcp/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,28 @@ export type Doc = v.InferOutput<typeof Doc>;
122122
*/
123123
export type ComponentDocWithExportName = ComponentDoc & { exportName: string };
124124

125+
export const ComponentSubcomponentManifest = v.object({
126+
...BaseManifest.entries,
127+
path: v.string(),
128+
summary: v.optional(v.string()),
129+
import: v.optional(v.string()),
130+
// loose schema for react-docgen types
131+
reactDocgen: v.optional(v.any()),
132+
// loose schema for react-docgen-typescript types
133+
reactDocgenTypescript: v.optional(v.any()),
134+
// loose schema for react-component-meta types
135+
reactComponentMeta: v.optional(v.any()),
136+
});
137+
export type ComponentSubcomponentManifest = v.InferOutput<typeof ComponentSubcomponentManifest>;
138+
125139
export const ComponentManifest = v.object({
126140
...BaseManifest.entries,
127141
id: v.string(),
128142
path: v.string(),
129143
summary: v.optional(v.string()),
130144
import: v.optional(v.string()),
131145
stories: v.optional(v.array(Story)),
146+
subcomponents: v.optional(v.record(v.string(), ComponentSubcomponentManifest)),
132147
// loose schema for react-docgen types, as they are pretty complex
133148
reactDocgen: v.optional(v.any()),
134149
// loose schema for react-docgen-typescript types

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,79 @@ describe('MarkdownFormatter - formatComponentManifest', () => {
6666
});
6767
});
6868

69+
describe('subcomponents section', () => {
70+
it('should include subcomponent docs and props before stories', () => {
71+
const manifest: ComponentManifest = {
72+
id: 'combo-box',
73+
name: 'ComboBox',
74+
path: 'src/components/ComboBox.tsx',
75+
description: 'A combo box component',
76+
subcomponents: {
77+
Item: {
78+
name: 'ComboBoxItem',
79+
path: 'src/components/ComboBoxItem.tsx',
80+
description: 'Use for individual list items.',
81+
import: 'import { ComboBoxItem } from "@/components";',
82+
reactComponentMeta: {
83+
displayName: 'ComboBoxItem',
84+
filePath: 'src/components/ComboBoxItem.tsx',
85+
description: '',
86+
exportName: 'ComboBoxItem',
87+
props: {
88+
textValue: {
89+
name: 'textValue',
90+
description: 'Required when the children are not plain text.',
91+
required: false,
92+
defaultValue: null,
93+
type: {
94+
name: 'string',
95+
},
96+
},
97+
},
98+
},
99+
},
100+
},
101+
stories: [
102+
{
103+
name: 'Default',
104+
snippet: '<ComboBox />',
105+
},
106+
],
107+
};
108+
109+
const result = formatComponentManifest(manifest);
110+
111+
expect(result).toContain('## Subcomponents');
112+
expect(result).toContain('### ComboBoxItem');
113+
expect(result).toContain('#### Props');
114+
expect(result).toContain('export type ComboBoxItemProps = {');
115+
expect(result.indexOf('## Subcomponents')).toBeLessThan(result.indexOf('## Stories'));
116+
});
117+
118+
it('should include subcomponent errors when docgen is unavailable', () => {
119+
const manifest: ComponentManifest = {
120+
id: 'combo-box',
121+
name: 'ComboBox',
122+
path: 'src/components/ComboBox.tsx',
123+
subcomponents: {
124+
Item: {
125+
name: 'ComboBoxItem',
126+
path: 'src/components/ComboBoxItem.tsx',
127+
error: {
128+
name: 'No component import found',
129+
message: 'No component file found for ComboBoxItem.',
130+
},
131+
},
132+
},
133+
};
134+
135+
const result = formatComponentManifest(manifest);
136+
137+
expect(result).toContain('Error: No component import found');
138+
expect(result).toContain('No component file found for ComboBoxItem.');
139+
});
140+
});
141+
69142
describe('stories section', () => {
70143
it('should include story ID in detailed story output', () => {
71144
const manifest: ComponentManifest = {

0 commit comments

Comments
 (0)