@react-aria/mcp 3.0.0-nightly-46eb0efb5-251114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @react-aria/mcp
2
+
3
+ The `@react-aria/mcp` package provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) server for React Aria documentation. It exposes a set of tools that MCP clients can discover and call to browse the docs.
4
+
5
+ ## Installation
6
+
7
+ ### Quick Start
8
+
9
+ Simply run the server using npx:
10
+
11
+ ```bash
12
+ npx @react-aria/mcp@latest
13
+ ```
14
+
15
+ ### Using with an MCP client
16
+
17
+ Add the server to your MCP client configuration (the exact file and schema may depend on your client).
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "React Aria": {
23
+ "command": "npx",
24
+ "args": ["@react-aria/mcp@latest"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ <details>
31
+ <summary>Cursor</summary>
32
+
33
+ #### Click the button to install:
34
+
35
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=React%20Aria&config=eyJjb21tYW5kIjoibnB4IEByZWFjdC1hcmlhL21jcEBsYXRlc3QifQ%3D%3D)
36
+
37
+ Or follow the MCP install [guide](https://docs.cursor.com/en/context/mcp#installing-mcp-servers) and use the standard config above.
38
+
39
+ </details>
40
+
41
+ <details>
42
+ <summary>VS Code</summary>
43
+
44
+ #### Click the button to install:
45
+
46
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](vscode:mcp/install?%7B%22name%22%3A%22React%20Aria%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40react-aria%2Fmcp%40latest%22%5D%7D)
47
+
48
+ #### Or install manually:
49
+
50
+ Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) and use the standard config above. You can also add the server using the VS Code CLI:
51
+
52
+ ```bash
53
+ code --add-mcp '{"name":"React Aria","command":"npx","args":["@react-aria/mcp@latest"]}'
54
+ ```
55
+
56
+ </details>
57
+
58
+ <details>
59
+ <summary>Claude Code</summary>
60
+
61
+ Use the Claude Code CLI to add the server:
62
+
63
+ ```bash
64
+ claude mcp add react-aria npx @react-aria/mcp@latest
65
+ ```
66
+ For more information, see the [Claude Code MCP documentation](https://docs.claude.com/en/docs/claude-code/mcp).
67
+ </details>
68
+
69
+ <details>
70
+ <summary>Codex</summary>
71
+
72
+ Create or edit the configuration file `~/.codex/config.toml` and add:
73
+
74
+ ```toml
75
+ [mcp_servers.react-aria]
76
+ command = "npx"
77
+ args = ["@react-aria/mcp@latest"]
78
+ ```
79
+
80
+ For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/docs/config.md#mcp_servers).
81
+
82
+ </details>
83
+
84
+ <details>
85
+ <summary>Gemini CLI</summary>
86
+
87
+ Use the Gemini CLI to add the server:
88
+
89
+ ```bash
90
+ gemini mcp add react-aria npx @react-aria/mcp@latest
91
+ ```
92
+
93
+ For more information, see the [Gemini CLI MCP documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server).
94
+
95
+ </details>
96
+
97
+ <details>
98
+ <summary>Windsurf</summary>
99
+
100
+ Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp) and use the standard config above.
101
+
102
+ </details>
103
+
104
+ ## Tools
105
+
106
+ | Tool | Input | Description |
107
+ | --- | --- | --- |
108
+ | `list_react_aria_pages` | `{ includeDescription?: boolean }` | List available pages in the React Aria docs. |
109
+ | `get_react_aria_page_info` | `{ page_name: string }` | Return page description and list of section titles. |
110
+ | `get_react_aria_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. |
111
+
112
+ ## Development
113
+
114
+ ### Testing locally
115
+
116
+ Build the docs and MCP server locally, then start the docs server.
117
+
118
+ ```bash
119
+ yarn workspace @react-spectrum/s2-docs generate:md
120
+ yarn workspace @react-aria/mcp build
121
+ yarn start:s2-docs
122
+ ```
123
+
124
+ Update your MCP client configuration to use the local MCP server:
125
+
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "React Aria": {
130
+ "command": "node",
131
+ "args": ["{your path here}/react-spectrum/packages/dev/mcp/react-aria/dist/index.js"],
132
+ "env": {
133
+ "DOCS_CDN_BASE": "http://localhost:1234"
134
+ }
135
+ }
136
+ }
137
+ }
138
+ ```
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ import { errorToString } from '../../shared/src/utils.js';
4
+ import { startServer } from '../../shared/src/server.js';
5
+ // CLI entry for React Aria
6
+ (async () => {
7
+ try {
8
+ const arg = (process.argv[2] || '').trim();
9
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
10
+ console.log('Usage: npx @react-aria/mcp@latest\n\nStarts the MCP server for React Aria documentation.');
11
+ process.exit(0);
12
+ }
13
+ await startServer('react-aria', '0.1.0');
14
+ }
15
+ catch (err) {
16
+ console.error(errorToString(err));
17
+ process.exit(1);
18
+ }
19
+ })();
@@ -0,0 +1,83 @@
1
+ import { DEFAULT_CDN_BASE, fetchText } from './utils.js';
2
+ import { extractNameAndDescription, parseSectionsFromMarkdown } from './parser.js';
3
+ import path from 'path';
4
+ // Cache of parsed pages
5
+ const pageCache = new Map();
6
+ // Whether we've loaded the page index for a library yet.
7
+ const pageIndexLoaded = new Set();
8
+ function libBaseUrl(library) {
9
+ return `${DEFAULT_CDN_BASE}/${library}`;
10
+ }
11
+ // Build an index of pages for the given library from the CDN's llms.txt.
12
+ export async function buildPageIndex(library) {
13
+ if (pageIndexLoaded.has(library)) {
14
+ return Array.from(pageCache.values()).filter(p => p.key.startsWith(`${library}/`));
15
+ }
16
+ const pages = [];
17
+ // Read llms.txt to enumerate available pages without downloading them all.
18
+ const llmsUrl = `${libBaseUrl(library)}/llms.txt`;
19
+ const txt = await fetchText(llmsUrl);
20
+ const re = /^\s*-\s*\[([^\]]+)\]\(([^)]+)\)(?:\s*:\s*(.*))?\s*$/;
21
+ for (const line of txt.split(/\r?\n/)) {
22
+ const m = line.match(re);
23
+ if (!m) {
24
+ continue;
25
+ }
26
+ const display = (m[1] || '').trim();
27
+ const href = (m[2] || '').trim();
28
+ const description = (m[3] || '').trim() || undefined;
29
+ if (!href || !/\.md$/i.test(href)) {
30
+ continue;
31
+ }
32
+ const key = href.replace(/\.md$/i, '').replace(/\\/g, '/');
33
+ const name = display || path.basename(key);
34
+ const filePath = `${DEFAULT_CDN_BASE}/${key}.md`;
35
+ const info = { key, name, description, filePath, sections: [] };
36
+ pages.push(info);
37
+ pageCache.set(info.key, info);
38
+ }
39
+ pageIndexLoaded.add(library);
40
+ return pages.sort((a, b) => a.key.localeCompare(b.key));
41
+ }
42
+ export async function ensureParsedPage(info) {
43
+ if (info.sections && info.sections.length > 0 && info.description !== undefined) {
44
+ return info;
45
+ }
46
+ const text = await fetchText(info.filePath);
47
+ const lines = text.split(/\r?\n/);
48
+ const { name, description } = extractNameAndDescription(lines);
49
+ const sections = parseSectionsFromMarkdown(lines);
50
+ const updated = { ...info, name: name || info.name, description, sections };
51
+ pageCache.set(updated.key, updated);
52
+ return updated;
53
+ }
54
+ export async function resolvePageRef(library, pageName) {
55
+ await buildPageIndex(library);
56
+ if (pageCache.has(pageName)) {
57
+ return pageCache.get(pageName);
58
+ }
59
+ if (pageName.includes('/')) {
60
+ const normalized = pageName.replace(/\\/g, '/');
61
+ const prefix = normalized.split('/', 1)[0];
62
+ if (prefix !== library) {
63
+ throw new Error(`Page '${pageName}' is not in the '${library}' library.`);
64
+ }
65
+ const maybe = pageCache.get(normalized);
66
+ if (maybe) {
67
+ return maybe;
68
+ }
69
+ const filePath = `${DEFAULT_CDN_BASE}/${normalized}.md`;
70
+ const stub = { key: normalized, name: path.basename(normalized), description: undefined, filePath, sections: [] };
71
+ pageCache.set(stub.key, stub);
72
+ return stub;
73
+ }
74
+ const key = `${library}/${pageName}`;
75
+ const maybe = pageCache.get(key);
76
+ if (maybe) {
77
+ return maybe;
78
+ }
79
+ const filePath = `${DEFAULT_CDN_BASE}/${key}.md`;
80
+ const stub = { key, name: pageName, description: undefined, filePath, sections: [] };
81
+ pageCache.set(stub.key, stub);
82
+ return stub;
83
+ }
@@ -0,0 +1,61 @@
1
+ export function parseSectionsFromMarkdown(lines) {
2
+ const sections = [];
3
+ let inCode = false;
4
+ for (let idx = 0; idx < lines.length; idx++) {
5
+ const line = lines[idx];
6
+ if (/^```/.test(line.trim())) {
7
+ inCode = !inCode;
8
+ }
9
+ if (inCode) {
10
+ continue;
11
+ }
12
+ if (line.startsWith('## ')) {
13
+ const name = line.replace(/^##\s+/, '').trim();
14
+ sections.push({ name, startLine: idx, endLine: lines.length });
15
+ }
16
+ }
17
+ for (let s = 0; s < sections.length - 1; s++) {
18
+ sections[s].endLine = sections[s + 1].startLine;
19
+ }
20
+ return sections;
21
+ }
22
+ export function extractNameAndDescription(lines) {
23
+ let name = '';
24
+ let description = undefined;
25
+ let i = 0;
26
+ for (; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ if (line.startsWith('# ')) {
29
+ name = line.replace(/^#\s+/, '').trim();
30
+ i++;
31
+ break;
32
+ }
33
+ }
34
+ let descLines = [];
35
+ let inCode = false;
36
+ for (; i < lines.length; i++) {
37
+ const line = lines[i];
38
+ if (/^```/.test(line.trim())) {
39
+ inCode = !inCode;
40
+ }
41
+ if (inCode) {
42
+ continue;
43
+ }
44
+ if (line.trim() === '') {
45
+ if (descLines.length > 0) {
46
+ break;
47
+ }
48
+ else {
49
+ continue;
50
+ }
51
+ }
52
+ if (/^#{1,6}\s/.test(line) || /^</.test(line.trim())) {
53
+ continue;
54
+ }
55
+ descLines.push(line);
56
+ }
57
+ if (descLines.length > 0) {
58
+ description = descLines.join('\n').trim();
59
+ }
60
+ return { name, description };
61
+ }
@@ -0,0 +1,76 @@
1
+ import { buildPageIndex, ensureParsedPage, resolvePageRef } from './page-manager.js';
2
+ import { errorToString, fetchText } from './utils.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { parseSectionsFromMarkdown } from './parser.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { z } from 'zod';
7
+ export async function startServer(library, version, registerAdditionalTools) {
8
+ const server = new McpServer({
9
+ name: library === 's2' ? 's2-docs-server' : 'react-aria-docs-server',
10
+ version
11
+ });
12
+ // Build page index at startup.
13
+ try {
14
+ await buildPageIndex(library);
15
+ }
16
+ catch (e) {
17
+ console.warn(`Warning: failed to load ${library} docs index (${errorToString(e)}).`);
18
+ }
19
+ const toolPrefix = library === 's2' ? 's2' : 'react_aria';
20
+ server.registerTool(`list_${toolPrefix}_pages`, {
21
+ title: library === 's2' ? 'List React Spectrum (@react-spectrum/s2) docs pages' : 'List React Aria docs pages',
22
+ description: `Returns a list of available pages in the ${library} docs.`,
23
+ inputSchema: { includeDescription: z.boolean().optional() }
24
+ }, async ({ includeDescription }) => {
25
+ const pages = await buildPageIndex(library);
26
+ const items = pages
27
+ .sort((a, b) => a.key.localeCompare(b.key))
28
+ .map(p => includeDescription ? { name: p.name, description: p.description ?? '' } : { name: p.name });
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify(items, null, 2) }]
31
+ };
32
+ });
33
+ server.registerTool(`get_${toolPrefix}_page_info`, {
34
+ title: 'Get page info',
35
+ description: 'Returns page description and list of sections for a given page.',
36
+ inputSchema: { page_name: z.string() }
37
+ }, async ({ page_name }) => {
38
+ const ref = await resolvePageRef(library, page_name);
39
+ const info = await ensureParsedPage(ref);
40
+ const out = {
41
+ name: info.name,
42
+ description: info.description ?? '',
43
+ sections: info.sections.map(s => s.name)
44
+ };
45
+ return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
46
+ });
47
+ server.registerTool(`get_${toolPrefix}_page`, {
48
+ title: 'Get page markdown',
49
+ description: 'Returns the full markdown content for a page, or a specific section if provided.',
50
+ inputSchema: { page_name: z.string(), section_name: z.string().optional() }
51
+ }, async ({ page_name, section_name }) => {
52
+ const ref = await resolvePageRef(library, page_name);
53
+ let text;
54
+ text = await fetchText(ref.filePath);
55
+ if (!section_name) {
56
+ return { content: [{ type: 'text', text }] };
57
+ }
58
+ const lines = text.split(/\r?\n/);
59
+ const sections = parseSectionsFromMarkdown(lines);
60
+ let section = sections.find(s => s.name === section_name);
61
+ if (!section) {
62
+ section = sections.find(s => s.name.toLowerCase() === section_name.toLowerCase());
63
+ }
64
+ if (!section) {
65
+ const available = sections.map(s => s.name).join(', ');
66
+ throw new Error(`Section '${section_name}' not found in ${ref.key}. Available: ${available}`);
67
+ }
68
+ const snippet = lines.slice(section.startLine, section.endLine).join('\n');
69
+ return { content: [{ type: 'text', text: snippet }] };
70
+ });
71
+ if (registerAdditionalTools) {
72
+ await registerAdditionalTools(server);
73
+ }
74
+ const transport = new StdioServerTransport();
75
+ await server.connect(transport);
76
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ export function errorToString(err) {
2
+ if (err && typeof err === 'object' && 'stack' in err && typeof err.stack === 'string') {
3
+ return err.stack;
4
+ }
5
+ if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
6
+ return err.message;
7
+ }
8
+ try {
9
+ return JSON.stringify(err);
10
+ }
11
+ catch {
12
+ return String(err);
13
+ }
14
+ }
15
+ // CDN base for docs. Can be overridden via env variable.
16
+ export const DEFAULT_CDN_BASE = process.env.DOCS_CDN_BASE ?? 'https://react-spectrum.adobe.com/beta';
17
+ export async function fetchText(url, timeoutMs = 15000) {
18
+ const ctrl = new AbortController();
19
+ const id = setTimeout(() => ctrl.abort(), timeoutMs).unref?.();
20
+ try {
21
+ const res = await fetch(url, { signal: ctrl.signal, cache: 'no-store' });
22
+ if (!res.ok) {
23
+ throw new Error(`HTTP ${res.status} for ${url}`);
24
+ }
25
+ return await res.text();
26
+ }
27
+ finally {
28
+ clearTimeout(id);
29
+ }
30
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@react-aria/mcp",
3
+ "version": "3.0.0-nightly-46eb0efb5-251114",
4
+ "description": "MCP server for React Aria documentation",
5
+ "type": "module",
6
+ "bin": "dist/react-aria/src/index.js",
7
+ "scripts": {
8
+ "prepublishOnly": "yarn build",
9
+ "build": "tsc -p tsconfig.json",
10
+ "start": "node dist/react-aria/src/index.js",
11
+ "dev": "node --enable-source-maps dist/react-aria/src/index.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.17.3",
15
+ "@swc/helpers": "^0.5.0",
16
+ "zod": "^3.23.8"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.8.2"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "license": "Apache-2.0",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/adobe/react-spectrum"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src"
35
+ ],
36
+ "sideEffects": false,
37
+ "main": "dist/main.js",
38
+ "module": "dist/module.js",
39
+ "types": "dist/types.d.ts",
40
+ "source": "src/index.ts"
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ import {errorToString} from '../../shared/src/utils.js';
4
+ import {startServer} from '../../shared/src/server.js';
5
+
6
+ // CLI entry for React Aria
7
+ (async () => {
8
+ try {
9
+ const arg = (process.argv[2] || '').trim();
10
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
11
+ console.log('Usage: npx @react-aria/mcp@latest\n\nStarts the MCP server for React Aria documentation.');
12
+ process.exit(0);
13
+ }
14
+ await startServer('react-aria', '0.1.0');
15
+ } catch (err) {
16
+ console.error(errorToString(err));
17
+ process.exit(1);
18
+ }
19
+ })();