@react-spectrum/mcp 3.0.0-nightly-ecb05e28d-250909
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/package.json +44 -0
- package/src/index.ts +404 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-spectrum/mcp",
|
|
3
|
+
"version": "3.0.0-nightly-ecb05e28d-250909",
|
|
4
|
+
"description": "MCP server for React Spectrum (S2) and React Aria documentation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "node --enable-source-maps dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.17.3",
|
|
14
|
+
"@swc/helpers": "^0.5.0",
|
|
15
|
+
"fast-glob": "^3.3.3",
|
|
16
|
+
"zod": "^3.23.8"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@adobe/spectrum-css-temp": "3.0.0-alpha.1",
|
|
20
|
+
"typescript": "^5.8.2"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/adobe/react-spectrum"
|
|
32
|
+
},
|
|
33
|
+
"main": "dist/main.js",
|
|
34
|
+
"module": "dist/module.js",
|
|
35
|
+
"types": "dist/types.d.ts",
|
|
36
|
+
"source": "src/index.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"src"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": [
|
|
42
|
+
"*.css"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
import {fileURLToPath, pathToFileURL} from 'url';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
|
|
10
|
+
type SectionInfo = {
|
|
11
|
+
name: string,
|
|
12
|
+
startLine: number, // 0-based index where section heading starts
|
|
13
|
+
endLine: number // exclusive end line index for section content
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type PageInfo = {
|
|
17
|
+
key: string, // e.g. "s2/Button"
|
|
18
|
+
title: string, // from top-level heading
|
|
19
|
+
description?: string, // first paragraph after title
|
|
20
|
+
filePath: string, // absolute path to markdown file
|
|
21
|
+
sections: SectionInfo[]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Library = 's2' | 'react-aria';
|
|
25
|
+
|
|
26
|
+
function errorToString(err: unknown): string {
|
|
27
|
+
if (err && typeof err === 'object' && 'stack' in err && typeof (err as any).stack === 'string') {
|
|
28
|
+
return (err as any).stack as string;
|
|
29
|
+
}
|
|
30
|
+
if (err && typeof err === 'object' && 'message' in err && typeof (err as any).message === 'string') {
|
|
31
|
+
return (err as any).message as string;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(err);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve docs dist root based on this file location
|
|
41
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
42
|
+
const __dirname = path.dirname(__filename);
|
|
43
|
+
const DOCS_DIST_ROOT = path.resolve(__dirname, '../../s2-docs/dist');
|
|
44
|
+
|
|
45
|
+
function assertDocsExist() {
|
|
46
|
+
if (!fs.existsSync(DOCS_DIST_ROOT)) {
|
|
47
|
+
const hint = path.resolve(__dirname, '../../s2-docs/scripts/generateMarkdownDocs.mjs');
|
|
48
|
+
throw new Error(`S2 docs dist not found at ${DOCS_DIST_ROOT}. Build them first via: yarn workspace @react-spectrum/s2-docs generate:md (script: ${hint})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Cache of parsed pages
|
|
53
|
+
const pageCache = new Map<string, PageInfo>();
|
|
54
|
+
|
|
55
|
+
const ICONS_DIR = path.resolve(__dirname, '../../../@react-spectrum/s2/s2wf-icons');
|
|
56
|
+
let iconIdCache: string[] | null = null;
|
|
57
|
+
const ILLUSTRATIONS_DIR = path.resolve(__dirname, '../../../@react-spectrum/s2/spectrum-illustrations/linear');
|
|
58
|
+
let illustrationIdCache: string[] | null = null;
|
|
59
|
+
let iconAliasesCache: Record<string, string[]> | null = null;
|
|
60
|
+
let illustrationAliasesCache: Record<string, string[]> | null = null;
|
|
61
|
+
|
|
62
|
+
function ensureIconsExist() {
|
|
63
|
+
if (!fs.existsSync(ICONS_DIR)) {
|
|
64
|
+
throw new Error(`S2 icons directory not found at ${ICONS_DIR}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function listIconNames(): string[] {
|
|
69
|
+
if (iconIdCache) {return iconIdCache;}
|
|
70
|
+
ensureIconsExist();
|
|
71
|
+
const files = fg.sync('*.svg', {cwd: ICONS_DIR, absolute: false, suppressErrors: true});
|
|
72
|
+
const ids = Array.from(new Set(
|
|
73
|
+
files.map(f => f.replace(/\.svg$/i, '')
|
|
74
|
+
// Mirror IconPicker.tsx regex to derive the id from the filename
|
|
75
|
+
.replace(/^S2_Icon_(.*?)(Size\d+)?_2.*/, '$1'))
|
|
76
|
+
)).sort((a, b) => a.localeCompare(b));
|
|
77
|
+
iconIdCache = ids;
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureIllustrationsExist() {
|
|
82
|
+
if (!fs.existsSync(ILLUSTRATIONS_DIR)) {
|
|
83
|
+
throw new Error(`S2 illustrations directory not found at ${ILLUSTRATIONS_DIR}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function listIllustrationNames(): string[] {
|
|
88
|
+
if (illustrationIdCache) {return illustrationIdCache;}
|
|
89
|
+
ensureIllustrationsExist();
|
|
90
|
+
// linear directory may contain multiple sizes per illustration name
|
|
91
|
+
const files = fg.sync('**/*.svg', {cwd: ILLUSTRATIONS_DIR, absolute: false, suppressErrors: true});
|
|
92
|
+
const ids = Array.from(new Set(
|
|
93
|
+
files.map(f => {
|
|
94
|
+
const base = f.replace(/\.svg$/i, '')
|
|
95
|
+
// Pattern: S2_lin_<name>_<size>
|
|
96
|
+
.replace(/^S2_lin_(.*)_\d+$/, '$1');
|
|
97
|
+
return base ? (base.charAt(0).toUpperCase() + base.slice(1)) : base;
|
|
98
|
+
})
|
|
99
|
+
)).sort((a, b) => a.localeCompare(b));
|
|
100
|
+
illustrationIdCache = ids;
|
|
101
|
+
return ids;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadIconAliases(): Promise<Record<string, string[]>> {
|
|
105
|
+
if (iconAliasesCache) {return iconAliasesCache;}
|
|
106
|
+
const aliasesPath = path.resolve(__dirname, '../../s2-docs/src/iconAliases.js');
|
|
107
|
+
if (!fs.existsSync(aliasesPath)) {return iconAliasesCache = {};}
|
|
108
|
+
const mod = await import(pathToFileURL(aliasesPath).href);
|
|
109
|
+
return (iconAliasesCache = (mod.iconAliases ?? {}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadIllustrationAliases(): Promise<Record<string, string[]>> {
|
|
113
|
+
if (illustrationAliasesCache) {return illustrationAliasesCache;}
|
|
114
|
+
const aliasesPath = path.resolve(__dirname, '../../s2-docs/src/illustrationAliases.js');
|
|
115
|
+
if (!fs.existsSync(aliasesPath)) {return illustrationAliasesCache = {};}
|
|
116
|
+
const mod = await import(pathToFileURL(aliasesPath).href);
|
|
117
|
+
return (illustrationAliasesCache = (mod.illustrationAliases ?? {}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readAllPagesFor(library: Library): PageInfo[] {
|
|
121
|
+
assertDocsExist();
|
|
122
|
+
const pattern = `${library}/**/*.md`;
|
|
123
|
+
const absFiles = fg.sync([pattern], {cwd: DOCS_DIST_ROOT, absolute: true, suppressErrors: true});
|
|
124
|
+
const pages: PageInfo[] = [];
|
|
125
|
+
for (const absPath of absFiles) {
|
|
126
|
+
if (path.basename(absPath).toLowerCase() === 'llms.txt') {continue;}
|
|
127
|
+
const rel = path.relative(DOCS_DIST_ROOT, absPath);
|
|
128
|
+
const key = rel.replace(/\\/g, '/').replace(/\.md$/i, '');
|
|
129
|
+
const info = parsePage(absPath, key);
|
|
130
|
+
pages.push(info);
|
|
131
|
+
pageCache.set(info.key, info);
|
|
132
|
+
}
|
|
133
|
+
return pages;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parsePage(absPath: string, keyFromPath?: string): PageInfo {
|
|
137
|
+
const raw = fs.readFileSync(absPath, 'utf8');
|
|
138
|
+
const lines = raw.split(/\r?\n/);
|
|
139
|
+
|
|
140
|
+
let title = '';
|
|
141
|
+
let description: string | undefined = undefined;
|
|
142
|
+
let i = 0;
|
|
143
|
+
// Find first H1 (title)
|
|
144
|
+
for (; i < lines.length; i++) {
|
|
145
|
+
const line = lines[i];
|
|
146
|
+
if (line.startsWith('# ')) {
|
|
147
|
+
title = line.replace(/^#\s+/, '').trim();
|
|
148
|
+
i++;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Collect first paragraph as description (non-empty text until blank line)
|
|
154
|
+
let descLines: string[] = [];
|
|
155
|
+
let inCode = false;
|
|
156
|
+
for (; i < lines.length; i++) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
if (/^```/.test(line.trim())) {inCode = !inCode;}
|
|
159
|
+
if (inCode) {continue;}
|
|
160
|
+
if (line.trim() === '') {
|
|
161
|
+
if (descLines.length > 0) {break;} else {continue;}
|
|
162
|
+
}
|
|
163
|
+
// ignore headings and HTML-like tags if they appear before paragraph
|
|
164
|
+
if (/^#{1,6}\s/.test(line) || /^</.test(line.trim())) {continue;}
|
|
165
|
+
descLines.push(line);
|
|
166
|
+
}
|
|
167
|
+
if (descLines.length > 0) {
|
|
168
|
+
description = descLines.join('\n').trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse sections (## ...)
|
|
172
|
+
const sections: SectionInfo[] = [];
|
|
173
|
+
inCode = false;
|
|
174
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
175
|
+
const line = lines[idx];
|
|
176
|
+
if (/^```/.test(line.trim())) {inCode = !inCode;}
|
|
177
|
+
if (inCode) {continue;}
|
|
178
|
+
if (line.startsWith('## ')) {
|
|
179
|
+
const name = line.replace(/^##\s+/, '').trim();
|
|
180
|
+
sections.push({name, startLine: idx, endLine: lines.length});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Compute endLine for each section as start of next section
|
|
184
|
+
for (let s = 0; s < sections.length - 1; s++) {
|
|
185
|
+
sections[s].endLine = sections[s + 1].startLine;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rel = path.relative(DOCS_DIST_ROOT, absPath).replace(/\\/g, '/');
|
|
189
|
+
const key = keyFromPath ?? rel.replace(/\.md$/i, '');
|
|
190
|
+
return {key, title, description, filePath: absPath, sections};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolvePagePathFor(library: Library, pageName: string): PageInfo {
|
|
194
|
+
// Accept keys like "s2/Button" or plain "Button" but restrict to the selected library
|
|
195
|
+
assertDocsExist();
|
|
196
|
+
|
|
197
|
+
if (pageCache.has(pageName)) {
|
|
198
|
+
return pageCache.get(pageName)!;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (pageName.includes('/')) {
|
|
202
|
+
const normalized = pageName.replace(/\\/g, '/');
|
|
203
|
+
const prefix = normalized.split('/', 1)[0];
|
|
204
|
+
if (prefix !== library) {
|
|
205
|
+
throw new Error(`Page '${pageName}' is not in the '${library}' library.`);
|
|
206
|
+
}
|
|
207
|
+
const abs = path.join(DOCS_DIST_ROOT, `${normalized}.md`);
|
|
208
|
+
if (!fs.existsSync(abs)) {
|
|
209
|
+
throw new Error(`Page not found: ${pageName}`);
|
|
210
|
+
}
|
|
211
|
+
const info = parsePage(abs, normalized);
|
|
212
|
+
pageCache.set(normalized, info);
|
|
213
|
+
return info;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const abs = path.join(DOCS_DIST_ROOT, library, `${pageName}.md`);
|
|
217
|
+
if (!fs.existsSync(abs)) {
|
|
218
|
+
throw new Error(`Page not found in '${library}': ${pageName}`);
|
|
219
|
+
}
|
|
220
|
+
const key = `${library}/${pageName}`;
|
|
221
|
+
const info = parsePage(abs, key);
|
|
222
|
+
pageCache.set(info.key, info);
|
|
223
|
+
return info;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readPageContent(filePath: string): string {
|
|
227
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function startServer(library: Library) {
|
|
231
|
+
const server = new McpServer({
|
|
232
|
+
name: library === 's2' ? 's2-docs-server' : 'react-aria-docs-server',
|
|
233
|
+
version: '0.1.0'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// list_pages tool
|
|
237
|
+
server.registerTool(
|
|
238
|
+
'list_pages',
|
|
239
|
+
{
|
|
240
|
+
title: library === 's2' ? 'List React Spectrum (@react-spectrum/s2) docs pages' : 'List React Aria docs pages',
|
|
241
|
+
description: `Returns a list of available pages in the ${library} docs.`,
|
|
242
|
+
inputSchema: {includeDescription: z.boolean().optional()}
|
|
243
|
+
},
|
|
244
|
+
async ({includeDescription}) => {
|
|
245
|
+
const pages = readAllPagesFor(library);
|
|
246
|
+
const items = pages
|
|
247
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
248
|
+
.map(p => includeDescription ? {key: p.key, title: p.title, description: p.description ?? ''} : {key: p.key, title: p.title});
|
|
249
|
+
return {
|
|
250
|
+
content: [{type: 'text', text: JSON.stringify(items, null, 2)}]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// get_page_info tool
|
|
256
|
+
server.registerTool(
|
|
257
|
+
'get_page_info',
|
|
258
|
+
{
|
|
259
|
+
title: 'Get page info',
|
|
260
|
+
description: 'Returns page description and list of sections for a given page.',
|
|
261
|
+
inputSchema: {page_name: z.string()}
|
|
262
|
+
},
|
|
263
|
+
async ({page_name}) => {
|
|
264
|
+
const info = resolvePagePathFor(library, page_name);
|
|
265
|
+
const out = {
|
|
266
|
+
key: info.key,
|
|
267
|
+
title: info.title,
|
|
268
|
+
description: info.description ?? '',
|
|
269
|
+
sections: info.sections.map(s => s.name)
|
|
270
|
+
};
|
|
271
|
+
return {content: [{type: 'text', text: JSON.stringify(out, null, 2)}]};
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// get_page tool
|
|
276
|
+
server.registerTool(
|
|
277
|
+
'get_page',
|
|
278
|
+
{
|
|
279
|
+
title: 'Get page markdown',
|
|
280
|
+
description: 'Returns the full markdown content for a page, or a specific section if provided.',
|
|
281
|
+
inputSchema: {page_name: z.string(), section_name: z.string().optional()}
|
|
282
|
+
},
|
|
283
|
+
async ({page_name, section_name}) => {
|
|
284
|
+
const info = resolvePagePathFor(library, page_name);
|
|
285
|
+
let text: string;
|
|
286
|
+
if (!section_name) {
|
|
287
|
+
text = readPageContent(info.filePath);
|
|
288
|
+
} else {
|
|
289
|
+
// Find section by exact title match (case-sensitive first, then case-insensitive)
|
|
290
|
+
let section = info.sections.find(s => s.name === section_name);
|
|
291
|
+
if (!section) {
|
|
292
|
+
section = info.sections.find(s => s.name.toLowerCase() === section_name.toLowerCase());
|
|
293
|
+
}
|
|
294
|
+
if (!section) {
|
|
295
|
+
const available = info.sections.map(s => s.name).join(', ');
|
|
296
|
+
throw new Error(`Section '${section_name}' not found in ${info.key}. Available: ${available}`);
|
|
297
|
+
}
|
|
298
|
+
const lines = fs.readFileSync(info.filePath, 'utf8').split(/\r?\n/);
|
|
299
|
+
text = lines.slice(section.startLine, section.endLine).join('\n');
|
|
300
|
+
}
|
|
301
|
+
return {content: [{type: 'text', text}]} as const;
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (library === 's2') {
|
|
306
|
+
// search_icons tool
|
|
307
|
+
server.registerTool(
|
|
308
|
+
'search_icons',
|
|
309
|
+
{
|
|
310
|
+
title: 'Search S2 icons',
|
|
311
|
+
description: 'Searches the S2 workflow icon set by one or more terms; returns matching icon names.',
|
|
312
|
+
inputSchema: {terms: z.union([z.string(), z.array(z.string())])}
|
|
313
|
+
},
|
|
314
|
+
async ({terms}) => {
|
|
315
|
+
const allNames = listIconNames();
|
|
316
|
+
const nameSet = new Set(allNames);
|
|
317
|
+
const aliases = await loadIconAliases();
|
|
318
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
319
|
+
const normalized = Array.from(new Set(rawTerms.map(t => String(t ?? '').trim().toLowerCase()).filter(Boolean)));
|
|
320
|
+
if (normalized.length === 0) {
|
|
321
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
322
|
+
}
|
|
323
|
+
// direct name matches
|
|
324
|
+
const results = new Set(allNames.filter(name => {
|
|
325
|
+
const nameLower = name.toLowerCase();
|
|
326
|
+
return normalized.some(term => nameLower.includes(term));
|
|
327
|
+
}));
|
|
328
|
+
// alias matches
|
|
329
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
330
|
+
if (!targets || targets.length === 0) {continue;}
|
|
331
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
332
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
333
|
+
for (const t of targets) {
|
|
334
|
+
const n = String(t);
|
|
335
|
+
if (nameSet.has(n)) {results.add(n);}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {content: [{type: 'text', text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)}]};
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// search_illustrations tool
|
|
344
|
+
server.registerTool(
|
|
345
|
+
'search_illustrations',
|
|
346
|
+
{
|
|
347
|
+
title: 'Search S2 illustrations',
|
|
348
|
+
description: 'Searches the S2 illustrations set by one or more terms; returns matching illustration names.',
|
|
349
|
+
inputSchema: {terms: z.union([z.string(), z.array(z.string())])}
|
|
350
|
+
},
|
|
351
|
+
async ({terms}) => {
|
|
352
|
+
const allNames = listIllustrationNames();
|
|
353
|
+
const nameSet = new Set(allNames);
|
|
354
|
+
const aliases = await loadIllustrationAliases();
|
|
355
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
356
|
+
const normalized = Array.from(new Set(rawTerms.map(t => String(t ?? '').trim().toLowerCase()).filter(Boolean)));
|
|
357
|
+
if (normalized.length === 0) {
|
|
358
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
359
|
+
}
|
|
360
|
+
// direct name matches
|
|
361
|
+
const results = new Set(allNames.filter(name => {
|
|
362
|
+
const nameLower = name.toLowerCase();
|
|
363
|
+
return normalized.some(term => nameLower.includes(term));
|
|
364
|
+
}));
|
|
365
|
+
// alias matches
|
|
366
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
367
|
+
if (!targets || targets.length === 0) {continue;}
|
|
368
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
369
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
370
|
+
for (const t of targets) {
|
|
371
|
+
const n = String(t);
|
|
372
|
+
if (nameSet.has(n)) {results.add(n);}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {content: [{type: 'text', text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)}]};
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const transport = new StdioServerTransport();
|
|
382
|
+
await server.connect(transport);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function printUsage() {
|
|
386
|
+
const usage = 'Usage: mcp <subcommand>\n\nSubcommands:\n s2 Start MCP server for React Spectrum S2 docs\n react-aria Start MCP server for React Aria docs\n\nExamples:\n npx @react-spectrum/mcp s2\n npx @react-spectrum/mcp react-aria';
|
|
387
|
+
console.log(usage);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// CLI entry
|
|
391
|
+
(async () => {
|
|
392
|
+
try {
|
|
393
|
+
const arg = (process.argv[2] || '').trim();
|
|
394
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
395
|
+
printUsage();
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
const library: Library = arg === 'react-aria' ? 'react-aria' : 's2';
|
|
399
|
+
await startServer(library);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error(errorToString(err));
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
})();
|