diff --git a/packages/dev/mcp/README.md b/packages/dev/mcp/README.md index 1293d204c6a..258a785a298 100644 --- a/packages/dev/mcp/README.md +++ b/packages/dev/mcp/README.md @@ -118,10 +118,55 @@ Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/m ## Tools +### React Spectrum (S2) + +| Tool | Input | Description | +| --- | --- | --- | +| `list_s2_pages` | `{ includeDescription?: boolean }` | List available pages in the S2 docs. | +| `get_s2_page_info` | `{ page_name: string }` | Return page description and list of section titles. | +| `get_s2_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. | +| `search_s2_icons` | `{ terms: string or string[] }` | Search S2 workflow icon names. | +| `search_s2_illustrations` | `{ terms: string or string[] }` | Search S2 illustration names. | + +### React Aria + | Tool | Input | Description | | --- | --- | --- | -| `list_pages` | `{ includeDescription?: boolean }` | List available pages in the selected docs library. | -| `get_page_info` | `{ page_name: string }` | Return page description and list of section titles. | -| `get_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. | -| `search_icons` (S2 only) | `{ terms: string or string[] }` | Search S2 workflow icon names. | -| `search_illustrations` (S2 only) | `{ terms: string or string[] }` | Search S2 illustration names. | +| `list_react_aria_pages` | `{ includeDescription?: boolean }` | List available pages in the React Aria docs. | +| `get_react_aria_page_info` | `{ page_name: string }` | Return page description and list of section titles. | +| `get_react_aria_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. | + +## Development + +### Testing locally + +Build the docs and MCP server locally, then start the docs server. + +```bash +yarn workspace @react-spectrum/s2-docs generate:md +yarn workspace @react-spectrum/mcp build +yarn start:s2-docs +``` + +Update your MCP client configuration to use the local MCP server: + +```json +{ + "mcpServers": { + "React Spectrum (S2)": { + "command": "node", + "args": ["{your path here}/react-spectrum/packages/dev/mcp/dist/index.js", "s2"], + "env": { + "DOCS_CDN_BASE": "http://localhost:1234" + } + }, + "React Aria": { + "command": "node", + "args": ["{your path here}/react-spectrum/packages/dev/mcp/dist/index.js", "react-aria"], + "env": { + "DOCS_CDN_BASE": "http://localhost:1234" + } + } + } +} +``` diff --git a/packages/dev/mcp/src/index.ts b/packages/dev/mcp/src/index.ts index 89567c65ada..5ad5221fa01 100644 --- a/packages/dev/mcp/src/index.ts +++ b/packages/dev/mcp/src/index.ts @@ -15,8 +15,8 @@ type SectionInfo = { type PageInfo = { key: string, // e.g. "s2/Button" - title: string, // from top-level heading - description?: string, // first paragraph after title + name: string, // from top-level heading + description?: string, // first paragraph after name filePath: string, // absolute path to markdown file sections: SectionInfo[] }; @@ -42,7 +42,7 @@ const __dirname = path.dirname(__filename); // CDN base for docs. Can be overridden via env variable. const DEFAULT_CDN_BASE = process.env.DOCS_CDN_BASE - ?? 'https://reactspectrum.blob.core.windows.net/reactspectrum/7d2883a56fb1a0554864b21324d405f758deb3ce/s2-docs'; + ?? 'https://reactspectrum.blob.core.windows.net/reactspectrum/a22a0aed3e97d0a23b9883679798b85eed68413d/s2-docs'; function libBaseUrl(library: Library) { return `${DEFAULT_CDN_BASE}/${library}`; @@ -126,12 +126,12 @@ async function buildPageIndex(library: Library): Promise { if (!m) {continue;} const display = (m[1] || '').trim(); const href = (m[2] || '').trim(); - const desc = (m[3] || '').trim() || undefined; + const description = (m[3] || '').trim() || undefined; if (!href || !/\.md$/i.test(href)) {continue;} const key = href.replace(/\.md$/i, '').replace(/\\/g, '/'); - const title = display || path.basename(key); - const url = `${DEFAULT_CDN_BASE}/${key}.md`; - const info: PageInfo = {key, title, description: desc, filePath: url, sections: []}; + const name = display || path.basename(key); + const filePath = `${DEFAULT_CDN_BASE}/${key}.md`; + const info: PageInfo = {key, name, description, filePath, sections: []}; pages.push(info); pageCache.set(info.key, info); } @@ -158,15 +158,15 @@ function parseSectionsFromMarkdown(lines: string[]): SectionInfo[] { return sections; } -function extractTitleAndDescription(lines: string[]): {title: string, description?: string} { - let title = ''; +function extractNameAndDescription(lines: string[]): {name: string, description?: string} { + let name = ''; let description: string | undefined = undefined; let i = 0; for (; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('# ')) { - title = line.replace(/^#\s+/, '').trim(); + name = line.replace(/^#\s+/, '').trim(); i++; break; } @@ -188,7 +188,7 @@ function extractTitleAndDescription(lines: string[]): {title: string, descriptio description = descLines.join('\n').trim(); } - return {title, description}; + return {name, description}; } async function ensureParsedPage(info: PageInfo): Promise { @@ -198,9 +198,9 @@ async function ensureParsedPage(info: PageInfo): Promise { const text = await fetchText(info.filePath); const lines = text.split(/\r?\n/); - const {title, description} = extractTitleAndDescription(lines); + const {name, description} = extractNameAndDescription(lines); const sections = parseSectionsFromMarkdown(lines); - const updated = {...info, title: title || info.title, description, sections}; + const updated = {...info, name: name || info.name, description, sections}; pageCache.set(updated.key, updated); return updated; } @@ -222,7 +222,7 @@ async function resolvePageRef(library: Library, pageName: string): Promise a.key.localeCompare(b.key)) - .map(p => includeDescription ? {key: p.key, title: p.title, description: p.description ?? ''} : {key: p.key, title: p.title}); + .map(p => includeDescription ? {name: p.name, description: p.description ?? ''} : {name: p.name}); return { content: [{type: 'text', text: JSON.stringify(items, null, 2)}] }; @@ -270,7 +271,7 @@ async function startServer(library: Library) { // get_page_info tool server.registerTool( - 'get_page_info', + `get_${toolPrefix}_page_info`, { title: 'Get page info', description: 'Returns page description and list of sections for a given page.', @@ -280,8 +281,7 @@ async function startServer(library: Library) { const ref = await resolvePageRef(library, page_name); const info = await ensureParsedPage(ref); const out = { - key: info.key, - title: info.title, + name: info.name, description: info.description ?? '', sections: info.sections.map(s => s.name) }; @@ -291,7 +291,7 @@ async function startServer(library: Library) { // get_page tool server.registerTool( - 'get_page', + `get_${toolPrefix}_page`, { title: 'Get page markdown', description: 'Returns the full markdown content for a page, or a specific section if provided.', @@ -324,7 +324,7 @@ async function startServer(library: Library) { if (library === 's2') { // search_icons tool server.registerTool( - 'search_icons', + 'search_s2_icons', { title: 'Search S2 icons', description: 'Searches the S2 workflow icon set by one or more terms; returns matching icon names.', @@ -361,7 +361,7 @@ async function startServer(library: Library) { // search_illustrations tool server.registerTool( - 'search_illustrations', + 'search_s2_illustrations', { title: 'Search S2 illustrations', description: 'Searches the S2 illustrations set by one or more terms; returns matching illustration names.',