Skip to content
Open
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
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
216 changes: 216 additions & 0 deletions packages/dev/s2-docs/scripts/generateMcpb.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import {execFileSync} from 'child_process';
import {fileURLToPath} from 'url';
import fs from 'fs';
import os from 'os';
import path from 'path';
import sharp from 'sharp';

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

const assetsDir = path.join(scriptDir, '../assets');

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',
iconSvg: path.join(assetsDir, 'rsp-favicon.svg'),
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',
iconSvg: path.join(assetsDir, 'react-aria-favicon.svg'),
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
*/
async 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);
}

// Convert SVG icon to 128x128 PNG for the bundle.
const iconFile = 'icon.png';
let svg = fs.readFileSync(config.iconSvg, 'utf8');
// The React Aria favicon uses light-dark() CSS which sharp doesn't support.
// Replace it with the dark-mode color so the icon works on any background.
svg = svg.replace(/light-dark\([^,]+,\s*([^)]+)\)/, '$1');
await sharp(Buffer.from(svg))
.resize(112, 112, {fit: 'inside'})
.extend({top: 8, bottom: 8, left: 8, right: 8, background: {r: 0, g: 0, b: 0, alpha: 0}})
.png().toFile(path.join(tempDir, iconFile));

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',
icon: iconFile,
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) {
await generateBundle(name, libraries[name]);
}
Loading
Loading