Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ s2-docs-production:
build-s2-docs:
yarn workspace @react-spectrum/s2-docs generate:md
yarn workspace @react-spectrum/s2-docs generate:og
yarn workspace @react-spectrum/mcp build
yarn workspace @react-aria/mcp build
yarn workspace @react-spectrum/s2-docs generate:mcpb
LIBRARY=react-aria node scripts/buildRegistry.mjs
yarn build:s2-docs
LIBRARY=react-aria node scripts/createFeedS2.mjs
Expand Down
4 changes: 3 additions & 1 deletion packages/dev/s2-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"build:s2": "LIBRARY=s2 parcel build 'pages/s2/**/*.mdx' --config .parcelrc-s2-docs --dist-dir dist/s2 --cache-dir ../../../.parcel-cache/s2",
"build:react-aria": "LIBRARY=react-aria parcel build 'pages/react-aria/**/*.mdx' --config .parcelrc-s2-docs --dist-dir dist/react-aria --cache-dir ../../../.parcel-cache/react-aria",
"generate:og": "node scripts/generateOGImages.mjs",
"generate:md": "node scripts/generateMarkdownDocs.mjs"
"generate:md": "node scripts/generateMarkdownDocs.mjs",
"generate:mcpb": "node scripts/generateMcpb.mjs"
},
"targets": {
"react-static": {
Expand Down Expand Up @@ -69,6 +70,7 @@
"vanilla-starter": "../../../starters/docs/src"
},
"devDependencies": {
"@anthropic-ai/mcpb": "^2.1.2",
"axe-playwright": "^2.2.2",
"playwright": "^1.57.0"
},
Expand Down
13 changes: 11 additions & 2 deletions packages/dev/s2-docs/pages/react-aria/ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const tags = ['ai', 'mcp', 'agent', 'skills', 'llms.txt', 'markdown'];
[Node.js](https://nodejs.org/) must be installed on your system to run the MCP server.

<Tabs aria-label="MCP Clients" density="compact">
<TabList><Tab id="cursor">Cursor</Tab><Tab id="vscode">VS Code</Tab><Tab id="claude-code">Claude Code</Tab><Tab id="codex">Codex</Tab><Tab id="gemini-cli">Gemini CLI</Tab><Tab id="other">Other</Tab></TabList>
<TabList><Tab id="cursor">Cursor</Tab><Tab id="vscode">VS Code</Tab><Tab id="claude-desktop">Claude Desktop</Tab><Tab id="claude-code">Claude Code</Tab><Tab id="codex">Codex</Tab><Tab id="gemini-cli">Gemini CLI</Tab><Tab id="other">Other</Tab></TabList>
<TabPanel id="cursor">
Click the button to install:

Expand Down Expand Up @@ -64,6 +64,15 @@ export const tags = ['ai', 'mcp', 'agent', 'skills', 'llms.txt', 'markdown'];
}
```
</TabPanel>
<TabPanel id="claude-desktop">
Download the extension, then open it in Claude Desktop and click Install:

<Link href="react-aria.mcpb" download aria-label="Download for Claude Desktop">
<img src="https://img.shields.io/badge/Claude_Desktop-Claude_Desktop?style=flat-square&label=Download%20Extension&color=D97757" alt="Download for Claude Desktop" />
</Link>

For more information, see Anthropic's [Desktop Extensions announcement](https://www.anthropic.com/engineering/desktop-extensions).
</TabPanel>
<TabPanel id="claude-code">
Use the Claude Code CLI to add the server:

Expand Down Expand Up @@ -121,4 +130,4 @@ Add the `.md` extension to the URL to get the markdown version of a page. Additi

## llms.txt

The <Link href="llms.txt" target="_blank">llms.txt</Link> file contains a list of all the markdown pages available in the React Aria documentation.
The <Link href="llms.txt" target="_blank">llms.txt</Link> file contains a list of all the markdown pages available in the React Aria documentation.
11 changes: 10 additions & 1 deletion packages/dev/s2-docs/pages/s2/ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const tags = ['ai', 'mcp', 'agent', 'skills', 'llms.txt', 'markdown'];
[Node.js](https://nodejs.org/) must be installed on your system to run the MCP server.

<Tabs aria-label="MCP Clients" density="compact">
<TabList><Tab id="cursor">Cursor</Tab><Tab id="vscode">VS Code</Tab><Tab id="claude-code">Claude Code</Tab><Tab id="codex">Codex</Tab><Tab id="gemini-cli">Gemini CLI</Tab><Tab id="other">Other</Tab></TabList>
<TabList><Tab id="cursor">Cursor</Tab><Tab id="vscode">VS Code</Tab><Tab id="claude-desktop">Claude Desktop</Tab><Tab id="claude-code">Claude Code</Tab><Tab id="codex">Codex</Tab><Tab id="gemini-cli">Gemini CLI</Tab><Tab id="other">Other</Tab></TabList>
<TabPanel id="cursor">
Click the button to install:

Expand Down Expand Up @@ -64,6 +64,15 @@ export const tags = ['ai', 'mcp', 'agent', 'skills', 'llms.txt', 'markdown'];
}
```
</TabPanel>
<TabPanel id="claude-desktop">
Download the extension, then open it in Claude Desktop and click Install:

<Link href="react-spectrum-s2.mcpb" download aria-label="Download for Claude Desktop">
<img src="https://img.shields.io/badge/Claude_Desktop-Claude_Desktop?style=flat-square&label=Download%20Extension&color=D97757" alt="Download for Claude Desktop" />
</Link>

For more information, see Anthropic's [Desktop Extensions announcement](https://www.anthropic.com/engineering/desktop-extensions).
</TabPanel>
<TabPanel id="claude-code">
Use the Claude Code CLI to add the server:

Expand Down
199 changes: 199 additions & 0 deletions packages/dev/s2-docs/scripts/generateMcpb.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {execFileSync} from 'child_process';
import {fileURLToPath} from 'url';
import fs from 'fs';
import os from 'os';
import path from 'path';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '../../../../');

const libraries = {
s2: {
packageDir: path.join(repoRoot, 'packages/dev/mcp/s2'),
packageName: '@react-spectrum/mcp',
outputDir: path.join(repoRoot, 'packages/dev/s2-docs/dist/s2'),
outputFile: 'react-spectrum-s2.mcpb',
serverEntryPoint: 'server/s2/src/index.js',
displayName: 'React Spectrum (S2)',
extensionName: 'react-spectrum-s2',
description: 'Browse the React Spectrum docs, icons, illustrations, and style macro values.',
homepage: 'https://react-spectrum.adobe.com/ai.html',
documentation: 'https://react-spectrum.adobe.com/ai.html',
srcDirs: [
{
from: path.join(repoRoot, 'packages/dev/mcp/s2/dist/s2/src'),
to: 'server/s2/src'
},
{
from: path.join(repoRoot, 'packages/dev/mcp/s2/dist/shared/src'),
to: 'server/shared/src'
},
{
from: path.join(repoRoot, 'packages/dev/mcp/s2/dist/data'),
to: 'server/data'
}
]
},
'react-aria': {
packageDir: path.join(repoRoot, 'packages/dev/mcp/react-aria'),
packageName: '@react-aria/mcp',
outputDir: path.join(repoRoot, 'packages/dev/s2-docs/dist/react-aria'),
outputFile: 'react-aria.mcpb',
serverEntryPoint: 'server/react-aria/src/index.js',
displayName: 'React Aria',
extensionName: 'react-aria',
description: 'Browse the React Aria docs.',
homepage: 'https://react-aria.adobe.com/ai.html',
documentation: 'https://react-aria.adobe.com/ai.html',
srcDirs: [
{
from: path.join(repoRoot, 'packages/dev/mcp/react-aria/dist/react-aria/src'),
to: 'server/react-aria/src'
},
{
from: path.join(repoRoot, 'packages/dev/mcp/react-aria/dist/shared/src'),
to: 'server/shared/src'
}
]
}
};

const requestedLibraries = process.argv.slice(2);

/**
* Generate an MCPB bundle for a given library. This makes the MCP servers easier to install in certain MCP clients like Claude Desktop.
* Reference: https://github.com/modelcontextprotocol/mcpb
*/
function generateBundle(libraryName, config) {
const packageJsonPath = path.join(config.packageDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `rsp-${libraryName}-mcpb-`));

try {
for (const dir of config.srcDirs) {
if (!fs.existsSync(dir.from)) {
throw new Error(`Missing built MCP output at ${dir.from}. Build ${config.packageName} first.`);
}
copyDirectory(dir.from, path.join(tempDir, dir.to));
}

const bundledPackages = new Set();
for (const dependency of Object.keys(packageJson.dependencies || {})) {
copyDependencyTree(dependency, path.join(tempDir, 'node_modules'), bundledPackages);
}

fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({
name: config.extensionName,
private: true,
type: 'module'
}, null, 2) + '\n');

fs.writeFileSync(path.join(tempDir, 'manifest.json'), JSON.stringify({
manifest_version: '0.3',
name: config.extensionName,
display_name: config.displayName,
version: packageJson.version,
description: config.description,
author: {
name: 'Adobe'
},
homepage: config.homepage,
documentation: config.documentation,
support: 'https://github.com/adobe/react-spectrum/issues',
server: {
type: 'node',
entry_point: config.serverEntryPoint,
mcp_config: {
command: 'node',
args: [`\${__dirname}/${config.serverEntryPoint}`]
}
}
}, null, 2) + '\n');

fs.mkdirSync(config.outputDir, {recursive: true});
const outputPath = path.join(config.outputDir, config.outputFile);
runMcpbCli(['validate', tempDir]);
runMcpbCli(['pack', tempDir, outputPath]);

const sizeKb = (fs.statSync(outputPath).size / 1024).toFixed(1);
console.log(`Generated ${config.outputFile} (${sizeKb} kB)`);
} finally {
fs.rmSync(tempDir, {recursive: true, force: true});
}
}

function copyDependencyTree(packageName, outputNodeModulesDir, bundledPackages, fromDir = repoRoot) {
if (bundledPackages.has(packageName)) {
return;
}

const packageDir = resolvePackageDir(packageName, fromDir);
const packageJsonPath = path.join(packageDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
bundledPackages.add(packageName);

copyDirectory(packageDir, path.join(outputNodeModulesDir, packageName));

for (const dependency of Object.keys(packageJson.dependencies || {})) {
copyDependencyTree(dependency, outputNodeModulesDir, bundledPackages, packageDir);
}

for (const dependency of Object.keys(packageJson.optionalDependencies || {})) {
copyDependencyTree(dependency, outputNodeModulesDir, bundledPackages, packageDir);
}
}

function resolvePackageDir(packageName, fromDir) {
let currentDir = fromDir;
const root = path.parse(currentDir).root;

while (true) {
const packageJsonPath = path.join(currentDir, 'node_modules', packageName, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.name === packageName) {
return path.dirname(packageJsonPath);
}
}

if (currentDir === root) {
break;
}
currentDir = path.dirname(currentDir);
}

throw new Error(`Could not resolve the package directory for ${packageName}`);
}

function copyDirectory(from, to) {
fs.mkdirSync(path.dirname(to), {recursive: true});
fs.cpSync(from, to, {
recursive: true,
dereference: true
});
}

function runMcpbCli(args) {
const mcpbPackageDir = resolvePackageDir('@anthropic-ai/mcpb', repoRoot);
const cliPath = path.join(mcpbPackageDir, 'dist/cli/cli.js');
if (!fs.existsSync(cliPath)) {
throw new Error(`Could not find MCPB CLI at ${cliPath}`);
}

execFileSync(process.execPath, [cliPath, ...args], {
cwd: repoRoot,
stdio: 'inherit'
});
}

const targets = requestedLibraries.length > 0 ? requestedLibraries : Object.keys(libraries);

for (const name of targets) {
if (!(name in libraries)) {
throw new Error(`Unknown MCP bundle target '${name}'. Expected one of: ${Object.keys(libraries).join(', ')}`);
}
}

for (const name of targets) {
generateBundle(name, libraries[name]);
}
Loading
Loading