Skip to content

Commit 5af44e5

Browse files
Merge pull request #221 from jpuzz0/PF-3167-pf-api-mcp-integration
feat(API): [PF-3167] Add component-index.json API endpoint for MCP
2 parents 309bd28 + abdafc5 commit 5af44e5

File tree

1 file changed

+119
-0
lines changed

1 file changed

+119
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { APIRoute } from 'astro'
2+
import { getApiIndex } from '../../utils/apiIndex/get'
3+
import { removeSubsection } from '../../utils/case'
4+
import { getOutputDir } from '../../utils/getOutputDir'
5+
import { join } from 'path'
6+
import { readFile } from 'fs/promises'
7+
8+
export const prerender = true
9+
10+
interface ComponentEntry {
11+
section: string
12+
page: string
13+
tabs: string[]
14+
hasProps: boolean
15+
hasCss: boolean
16+
exampleCount: number
17+
}
18+
19+
export interface ComponentIndex {
20+
version: string
21+
components: Record<string, ComponentEntry>
22+
}
23+
24+
// "about-modal" → "AboutModal", "forms_checkbox" → "Checkbox"
25+
function pageToPascalCase(page: string): string {
26+
return removeSubsection(page)
27+
.split('-')
28+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
29+
.join('')
30+
}
31+
32+
export const GET: APIRoute = async () => {
33+
try {
34+
const index = await getApiIndex()
35+
36+
// props.json keys include both component names ("Alert") and their prop
37+
// interfaces ("AlertProps"). Filter out the interface entries to get the
38+
// set of component names that have prop documentation available.
39+
let componentNamesWithProps = new Set<string>()
40+
try {
41+
const outputDir = await getOutputDir()
42+
const propsFile = await readFile(join(outputDir, 'props.json'), 'utf-8')
43+
const propsData = JSON.parse(propsFile)
44+
const propsSuffixPattern = /Props/i
45+
componentNamesWithProps = new Set(
46+
Object.keys(propsData).filter((name) => !propsSuffixPattern.test(name)),
47+
)
48+
} catch (error) {
49+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
50+
throw error
51+
}
52+
}
53+
54+
if (index.versions.length === 0) {
55+
throw new Error('API index contains no versions')
56+
}
57+
58+
// Latest version is last in sorted array (e.g., ["v5", "v6"] → "v6")
59+
const version = index.versions[index.versions.length - 1]
60+
const sections = index.sections[version] || []
61+
62+
const components: Record<string, ComponentEntry> = {}
63+
64+
for (const section of sections) {
65+
const pagesKey = `${version}::${section}`
66+
const pages = index.pages[pagesKey] || []
67+
68+
for (const page of pages) {
69+
const tabsKey = `${version}::${section}::${page}`
70+
const tabs = index.tabs[tabsKey] || []
71+
72+
let exampleCount = 0
73+
for (const tab of tabs) {
74+
const examplesKey = `${tabsKey}::${tab}`
75+
const examples = index.examples[examplesKey] || []
76+
exampleCount += examples.length
77+
}
78+
79+
const hasCss = tabsKey in index.css && index.css[tabsKey].length > 0
80+
81+
const pascalName = pageToPascalCase(page)
82+
const hasProps = componentNamesWithProps.has(pascalName)
83+
84+
// Prefer the first occurrence when multiple sections produce the same
85+
// PascalCase key (e.g., components/table vs extensions/data-view_table)
86+
if (!components[pascalName]) {
87+
components[pascalName] = {
88+
section,
89+
page,
90+
tabs,
91+
hasProps,
92+
hasCss,
93+
exampleCount,
94+
}
95+
}
96+
}
97+
}
98+
99+
const componentIndex: ComponentIndex = { version, components }
100+
101+
return new Response(JSON.stringify(componentIndex), {
102+
status: 200,
103+
headers: {
104+
'Content-Type': 'application/json',
105+
},
106+
})
107+
} catch (error) {
108+
return new Response(
109+
JSON.stringify({
110+
error: 'Failed to generate component index',
111+
details: error instanceof Error ? error.message : String(error),
112+
}),
113+
{
114+
status: 500,
115+
headers: { 'Content-Type': 'application/json' },
116+
},
117+
)
118+
}
119+
}

0 commit comments

Comments
 (0)