Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions packages/dev/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
```
46 changes: 23 additions & 23 deletions packages/dev/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
};
Expand All @@ -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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that we deployed, do we wanna point to the beta docs link?


function libBaseUrl(library: Library) {
return `${DEFAULT_CDN_BASE}/${library}`;
Expand Down Expand Up @@ -126,12 +126,12 @@ async function buildPageIndex(library: Library): Promise<PageInfo[]> {
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);
}
Expand All @@ -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;
}
Expand All @@ -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<PageInfo> {
Expand All @@ -198,9 +198,9 @@ async function ensureParsedPage(info: PageInfo): Promise<PageInfo> {

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;
}
Expand All @@ -222,7 +222,7 @@ async function resolvePageRef(library: Library, pageName: string): Promise<PageI
const maybe = pageCache.get(normalized);
if (maybe) {return maybe;}
const filePath = `${DEFAULT_CDN_BASE}/${normalized}.md`;
const stub: PageInfo = {key: normalized, title: path.basename(normalized), description: undefined, filePath, sections: []};
const stub: PageInfo = {key: normalized, name: path.basename(normalized), description: undefined, filePath, sections: []};
pageCache.set(stub.key, stub);
return stub;
}
Expand All @@ -231,7 +231,7 @@ async function resolvePageRef(library: Library, pageName: string): Promise<PageI
const maybe = pageCache.get(key);
if (maybe) {return maybe;}
const filePath = `${DEFAULT_CDN_BASE}/${key}.md`;
const stub: PageInfo = {key, title: pageName, description: undefined, filePath, sections: []};
const stub: PageInfo = {key, name: pageName, description: undefined, filePath, sections: []};
pageCache.set(stub.key, stub);
return stub;
}
Expand All @@ -250,8 +250,9 @@ async function startServer(library: Library) {
}

// list_pages tool
const toolPrefix = library === 's2' ? 's2' : 'react_aria';
server.registerTool(
'list_pages',
`list_${toolPrefix}_pages`,
{
title: library === 's2' ? 'List React Spectrum (@react-spectrum/s2) docs pages' : 'List React Aria docs pages',
description: `Returns a list of available pages in the ${library} docs.`,
Expand All @@ -261,7 +262,7 @@ async function startServer(library: Library) {
const pages = await buildPageIndex(library);
const items = pages
.sort((a, b) => 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)}]
};
Expand All @@ -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.',
Expand All @@ -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)
};
Expand All @@ -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.',
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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.',
Expand Down