@react-spectrum/mcp 1.0.0 → 1.2.0
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 +24 -1
- package/dist/data/iconAliases.json +16 -1
- package/dist/data/icons.json +8 -0
- package/dist/data/styleMacroPropertyValues.json +1 -1
- package/dist/data/styleProperties.json +1 -1
- package/dist/s2/src/index.js +116 -88
- package/dist/s2/src/s2-data.js +8 -4
- package/dist/shared/src/page-manager.js +17 -11
- package/dist/shared/src/server.js +21 -10
- package/dist/shared/src/utils.js +33 -3
- package/package.json +22 -22
- package/src/index.ts +169 -96
- package/src/s2-data.ts +34 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @react-spectrum/mcp
|
|
2
2
|
|
|
3
|
-
The `@react-spectrum/mcp` package provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) server for React Spectrum (S2) documentation. It exposes a set of tools that MCP clients can discover and call to browse the docs, search for icons and illustrations, and more.
|
|
3
|
+
The `@react-spectrum/mcp` package provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) server for React Spectrum (S2) documentation. It exposes a set of tools that MCP clients can discover and call to browse the docs, search for icons and illustrations, and more. It also bundles the React Aria docs tools, so you can browse React Aria documentation from the same server.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -110,6 +110,14 @@ Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/m
|
|
|
110
110
|
| `get_s2_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. |
|
|
111
111
|
| `search_s2_icons` | `{ terms: string \| string[] }` | Search S2 workflow icon names. |
|
|
112
112
|
| `search_s2_illustrations` | `{ terms: string \| string[] }` | Search S2 illustration names. |
|
|
113
|
+
| `get_style_macro_property_values` | `{ propertyName: string }` | Return allowed values for an S2 style macro property. |
|
|
114
|
+
| `list_react_aria_pages` | `{ includeDescription?: boolean }` | List available pages in the React Aria docs. |
|
|
115
|
+
| `get_react_aria_page_info` | `{ page_name: string }` | Return page description and list of section titles. |
|
|
116
|
+
| `get_react_aria_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. |
|
|
117
|
+
|
|
118
|
+
## Privacy Policy
|
|
119
|
+
|
|
120
|
+
See [Adobe's privacy policy](https://www.adobe.com/privacy/policy.html).
|
|
113
121
|
|
|
114
122
|
## Development
|
|
115
123
|
|
|
@@ -137,4 +145,19 @@ Update your MCP client configuration to use the local MCP server:
|
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Bundling
|
|
151
|
+
|
|
152
|
+
To build an [MCP Bundle (MCPB)](https://github.com/modelcontextprotocol/mcpb), first prepare the staging directory:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
yarn workspace @react-spectrum/s2-docs generate:mcpb
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Then validate and pack using the [`@anthropic-ai/mcpb`](https://www.npmjs.com/package/@anthropic-ai/mcpb) CLI (paths are printed by the script above):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx @anthropic-ai/mcpb validate <staging-dir>
|
|
162
|
+
npx @anthropic-ai/mcpb pack <staging-dir> <output-path>
|
|
140
163
|
```
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"MusicNote",
|
|
11
11
|
"StickyNote"
|
|
12
12
|
],
|
|
13
|
+
"approve": [
|
|
14
|
+
"BadgeVerified",
|
|
15
|
+
"CheckmarkCircle"
|
|
16
|
+
],
|
|
13
17
|
"arrow": [
|
|
14
18
|
"ChevronDoubleLeft",
|
|
15
19
|
"ChevronDoubleRight",
|
|
@@ -107,6 +111,9 @@
|
|
|
107
111
|
"TextAdd",
|
|
108
112
|
"UserAdd"
|
|
109
113
|
],
|
|
114
|
+
"crown": [
|
|
115
|
+
"PremiumIcon"
|
|
116
|
+
],
|
|
110
117
|
"date": [
|
|
111
118
|
"Calendar",
|
|
112
119
|
"CalendarAdd",
|
|
@@ -468,6 +475,9 @@
|
|
|
468
475
|
"program": [
|
|
469
476
|
"Code"
|
|
470
477
|
],
|
|
478
|
+
"pro": [
|
|
479
|
+
"PremiumIcon"
|
|
480
|
+
],
|
|
471
481
|
"raise": [
|
|
472
482
|
"ChevronUp",
|
|
473
483
|
"OrderOneUp",
|
|
@@ -489,7 +499,9 @@
|
|
|
489
499
|
"Undo"
|
|
490
500
|
],
|
|
491
501
|
"save": [
|
|
492
|
-
"Download"
|
|
502
|
+
"Download",
|
|
503
|
+
"Bookmark",
|
|
504
|
+
"BookmarkSingleFilled"
|
|
493
505
|
],
|
|
494
506
|
"schedule": [
|
|
495
507
|
"Calendar",
|
|
@@ -573,6 +585,9 @@
|
|
|
573
585
|
"up": [
|
|
574
586
|
"TextIncrease"
|
|
575
587
|
],
|
|
588
|
+
"verified": [
|
|
589
|
+
"CheckmarkCircle"
|
|
590
|
+
],
|
|
576
591
|
"versus": [
|
|
577
592
|
"Compare"
|
|
578
593
|
],
|
package/dist/data/icons.json
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"Apps",
|
|
22
22
|
"AppsAll",
|
|
23
23
|
"Archive",
|
|
24
|
+
"ArrowCurved",
|
|
24
25
|
"ArrowHeadTool",
|
|
26
|
+
"ArrowUpSend",
|
|
25
27
|
"Artboard",
|
|
26
28
|
"AspectRatio",
|
|
27
29
|
"Asset",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"Binoculars",
|
|
37
39
|
"Blur",
|
|
38
40
|
"Bookmark",
|
|
41
|
+
"BookmarkSingleFilled",
|
|
39
42
|
"Brand",
|
|
40
43
|
"Briefcase",
|
|
41
44
|
"BrightnessContrast",
|
|
@@ -259,6 +262,7 @@
|
|
|
259
262
|
"Polygon4",
|
|
260
263
|
"Polygon5",
|
|
261
264
|
"Polygon6",
|
|
265
|
+
"PremiumIcon",
|
|
262
266
|
"Preview",
|
|
263
267
|
"Print",
|
|
264
268
|
"Project",
|
|
@@ -318,6 +322,7 @@
|
|
|
318
322
|
"StepBackward",
|
|
319
323
|
"StepForward",
|
|
320
324
|
"StickyNote",
|
|
325
|
+
"StopProcessing",
|
|
321
326
|
"StrokeDotted",
|
|
322
327
|
"StrokeSolid",
|
|
323
328
|
"StrokeWidth",
|
|
@@ -399,6 +404,9 @@
|
|
|
399
404
|
"Wallet",
|
|
400
405
|
"WebNavBar",
|
|
401
406
|
"WebPage",
|
|
407
|
+
"ZoomFitToHeight",
|
|
408
|
+
"ZoomFitToScreen",
|
|
409
|
+
"ZoomFitToWidth",
|
|
402
410
|
"ZoomIn",
|
|
403
411
|
"ZoomOut"
|
|
404
412
|
]
|
package/dist/s2/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { errorToString } from '../../shared/src/utils.js';
|
|
3
|
+
import { errorToString, readPackageVersion } from '../../shared/src/utils.js';
|
|
4
4
|
import { listIconNames, listIllustrationNames, loadIconAliases, loadIllustrationAliases, loadStyleMacroPropertyValues } from './s2-data.js';
|
|
5
5
|
import { startServer } from '../../shared/src/server.js';
|
|
6
6
|
import { z } from 'zod';
|
|
@@ -12,102 +12,130 @@ import { z } from 'zod';
|
|
|
12
12
|
console.log('Usage: npx @react-spectrum/mcp@latest\n\nStarts the MCP server for React Spectrum (S2) documentation.');
|
|
13
13
|
process.exit(0);
|
|
14
14
|
}
|
|
15
|
-
await startServer('s2',
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
36
|
-
if (!targets || targets.length === 0) {
|
|
37
|
-
continue;
|
|
15
|
+
await startServer('s2', readPackageVersion(import.meta.url), {
|
|
16
|
+
additionalLibraries: ['react-aria'],
|
|
17
|
+
registerAdditionalTools: (server) => {
|
|
18
|
+
server.registerTool('search_s2_icons', {
|
|
19
|
+
title: 'Search S2 icons',
|
|
20
|
+
description: 'Searches the S2 workflow icon set by one or more terms; returns matching icon names.',
|
|
21
|
+
inputSchema: { terms: z.union([z.string(), z.array(z.string())]) },
|
|
22
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
23
|
+
}, async ({ terms }) => {
|
|
24
|
+
const allNames = listIconNames();
|
|
25
|
+
const nameSet = new Set(allNames);
|
|
26
|
+
const aliases = await loadIconAliases();
|
|
27
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
28
|
+
const normalized = Array.from(new Set(rawTerms
|
|
29
|
+
.map(t => String(t ?? '')
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase())
|
|
32
|
+
.filter(Boolean)));
|
|
33
|
+
if (normalized.length === 0) {
|
|
34
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
// direct name matches
|
|
37
|
+
const results = new Set(allNames.filter(name => {
|
|
38
|
+
const nameLower = name.toLowerCase();
|
|
39
|
+
return normalized.some(term => nameLower.includes(term));
|
|
40
|
+
}));
|
|
41
|
+
// alias matches
|
|
42
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
43
|
+
if (!targets || targets.length === 0) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
47
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
48
|
+
for (const t of targets) {
|
|
49
|
+
const n = String(t);
|
|
50
|
+
if (nameSet.has(n)) {
|
|
51
|
+
results.add(n);
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
server.registerTool('search_s2_illustrations', {
|
|
66
|
+
title: 'Search S2 illustrations',
|
|
67
|
+
description: 'Searches the S2 illustrations set by one or more terms; returns matching illustration names.',
|
|
68
|
+
inputSchema: { terms: z.union([z.string(), z.array(z.string())]) },
|
|
69
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
70
|
+
}, async ({ terms }) => {
|
|
71
|
+
const allNames = listIllustrationNames();
|
|
72
|
+
const nameSet = new Set(allNames);
|
|
73
|
+
const aliases = await loadIllustrationAliases();
|
|
74
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
75
|
+
const normalized = Array.from(new Set(rawTerms
|
|
76
|
+
.map(t => String(t ?? '')
|
|
77
|
+
.trim()
|
|
78
|
+
.toLowerCase())
|
|
79
|
+
.filter(Boolean)));
|
|
80
|
+
if (normalized.length === 0) {
|
|
81
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
73
82
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
// direct name matches
|
|
84
|
+
const results = new Set(allNames.filter(name => {
|
|
85
|
+
const nameLower = name.toLowerCase();
|
|
86
|
+
return normalized.some(term => nameLower.includes(term));
|
|
87
|
+
}));
|
|
88
|
+
// alias matches
|
|
89
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
90
|
+
if (!targets || targets.length === 0) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
94
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
95
|
+
for (const t of targets) {
|
|
96
|
+
const n = String(t);
|
|
97
|
+
if (nameSet.has(n)) {
|
|
98
|
+
results.add(n);
|
|
99
|
+
}
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
server.registerTool('get_style_macro_property_values', {
|
|
113
|
+
title: 'Get style macro property values',
|
|
114
|
+
description: 'Returns the allowed values for a given S2 style macro property (including expanded color/spacing value lists where applicable).',
|
|
115
|
+
inputSchema: { propertyName: z.string() },
|
|
116
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
117
|
+
}, async ({ propertyName }) => {
|
|
118
|
+
const name = String(propertyName ?? '').trim();
|
|
119
|
+
if (!name) {
|
|
120
|
+
throw new Error('Provide a non-empty propertyName.');
|
|
121
|
+
}
|
|
122
|
+
const all = loadStyleMacroPropertyValues();
|
|
123
|
+
let def = all[name];
|
|
124
|
+
if (!def) {
|
|
125
|
+
// fallback to case-insensitive lookup
|
|
126
|
+
const lower = name.toLowerCase();
|
|
127
|
+
const matchKey = Object.keys(all).find(k => k.toLowerCase() === lower);
|
|
128
|
+
if (matchKey) {
|
|
129
|
+
def = all[matchKey];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!def) {
|
|
133
|
+
const available = Object.keys(all).sort((a, b) => a.localeCompare(b));
|
|
134
|
+
throw new Error(`Unknown style macro property '${name}'. Available properties: ${available.join(', ')}`);
|
|
103
135
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
throw new Error(`Unknown style macro property '${name}'. Available properties: ${available.join(', ')}`);
|
|
108
|
-
}
|
|
109
|
-
return { content: [{ type: 'text', text: JSON.stringify(def, null, 2) }] };
|
|
110
|
-
});
|
|
136
|
+
return { content: [{ type: 'text', text: JSON.stringify(def, null, 2) }] };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
111
139
|
});
|
|
112
140
|
}
|
|
113
141
|
catch (err) {
|
package/dist/s2/src/s2-data.js
CHANGED
|
@@ -27,28 +27,32 @@ export function listIconNames() {
|
|
|
27
27
|
return iconIdCache;
|
|
28
28
|
}
|
|
29
29
|
const bundled = readBundledJson('icons.json');
|
|
30
|
-
return (iconIdCache = Array.isArray(bundled)
|
|
30
|
+
return (iconIdCache = Array.isArray(bundled)
|
|
31
|
+
? bundled.slice().sort((a, b) => a.localeCompare(b))
|
|
32
|
+
: []);
|
|
31
33
|
}
|
|
32
34
|
export function listIllustrationNames() {
|
|
33
35
|
if (illustrationIdCache) {
|
|
34
36
|
return illustrationIdCache;
|
|
35
37
|
}
|
|
36
38
|
const bundled = readBundledJson('illustrations.json');
|
|
37
|
-
return (illustrationIdCache = Array.isArray(bundled)
|
|
39
|
+
return (illustrationIdCache = Array.isArray(bundled)
|
|
40
|
+
? bundled.slice().sort((a, b) => a.localeCompare(b))
|
|
41
|
+
: []);
|
|
38
42
|
}
|
|
39
43
|
export async function loadIconAliases() {
|
|
40
44
|
if (iconAliasesCache) {
|
|
41
45
|
return iconAliasesCache;
|
|
42
46
|
}
|
|
43
47
|
const bundled = readBundledJson('iconAliases.json');
|
|
44
|
-
return (iconAliasesCache =
|
|
48
|
+
return (iconAliasesCache = bundled && typeof bundled === 'object' ? bundled : {});
|
|
45
49
|
}
|
|
46
50
|
export async function loadIllustrationAliases() {
|
|
47
51
|
if (illustrationAliasesCache) {
|
|
48
52
|
return illustrationAliasesCache;
|
|
49
53
|
}
|
|
50
54
|
const bundled = readBundledJson('illustrationAliases.json');
|
|
51
|
-
return (illustrationAliasesCache =
|
|
55
|
+
return (illustrationAliasesCache = bundled && typeof bundled === 'object' ? bundled : {});
|
|
52
56
|
}
|
|
53
57
|
export function loadStyleMacroPropertyValues() {
|
|
54
58
|
if (styleMacroPropertyValuesCache) {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_CDN_BASE, fetchText } from './utils.js';
|
|
2
1
|
import { extractNameAndDescription, parseSectionsFromMarkdown } from './parser.js';
|
|
2
|
+
import { fetchText, getLibraryBaseUrl } from './utils.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
// Cache of parsed pages
|
|
5
5
|
const pageCache = new Map();
|
|
6
6
|
// Whether we've loaded the page index for a library yet.
|
|
7
7
|
const pageIndexLoaded = new Set();
|
|
8
|
-
function libBaseUrl(library) {
|
|
9
|
-
return `${DEFAULT_CDN_BASE}/${library}`;
|
|
10
|
-
}
|
|
11
8
|
// Build an index of pages for the given library from the CDN's llms.txt.
|
|
12
9
|
export async function buildPageIndex(library) {
|
|
13
10
|
if (pageIndexLoaded.has(library)) {
|
|
@@ -15,7 +12,8 @@ export async function buildPageIndex(library) {
|
|
|
15
12
|
}
|
|
16
13
|
const pages = [];
|
|
17
14
|
// Read llms.txt to enumerate available pages without downloading them all.
|
|
18
|
-
const
|
|
15
|
+
const baseUrl = getLibraryBaseUrl(library);
|
|
16
|
+
const llmsUrl = `${baseUrl}/llms.txt`;
|
|
19
17
|
const txt = await fetchText(llmsUrl);
|
|
20
18
|
const re = /^\s*-\s*\[([^\]]+)\]\(([^)]+)\)(?:\s*:\s*(.*))?\s*$/;
|
|
21
19
|
for (const line of txt.split(/\r?\n/)) {
|
|
@@ -29,9 +27,10 @@ export async function buildPageIndex(library) {
|
|
|
29
27
|
if (!href || !/\.md$/i.test(href)) {
|
|
30
28
|
continue;
|
|
31
29
|
}
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
30
|
+
const hrefWithoutExt = href.replace(/\.md$/i, '').replace(/\\/g, '/');
|
|
31
|
+
const key = `${library}/${hrefWithoutExt}`;
|
|
32
|
+
const name = display || path.basename(hrefWithoutExt);
|
|
33
|
+
const filePath = `${baseUrl}/${hrefWithoutExt}.md`;
|
|
35
34
|
const info = { key, name, description, filePath, sections: [] };
|
|
36
35
|
pages.push(info);
|
|
37
36
|
pageCache.set(info.key, info);
|
|
@@ -56,6 +55,7 @@ export async function resolvePageRef(library, pageName) {
|
|
|
56
55
|
if (pageCache.has(pageName)) {
|
|
57
56
|
return pageCache.get(pageName);
|
|
58
57
|
}
|
|
58
|
+
const baseUrl = getLibraryBaseUrl(library);
|
|
59
59
|
if (pageName.includes('/')) {
|
|
60
60
|
const normalized = pageName.replace(/\\/g, '/');
|
|
61
61
|
const prefix = normalized.split('/', 1)[0];
|
|
@@ -66,8 +66,14 @@ export async function resolvePageRef(library, pageName) {
|
|
|
66
66
|
if (maybe) {
|
|
67
67
|
return maybe;
|
|
68
68
|
}
|
|
69
|
-
const filePath = `${
|
|
70
|
-
const stub = {
|
|
69
|
+
const filePath = `${baseUrl}/${normalized}.md`;
|
|
70
|
+
const stub = {
|
|
71
|
+
key: normalized,
|
|
72
|
+
name: path.basename(normalized),
|
|
73
|
+
description: undefined,
|
|
74
|
+
filePath,
|
|
75
|
+
sections: []
|
|
76
|
+
};
|
|
71
77
|
pageCache.set(stub.key, stub);
|
|
72
78
|
return stub;
|
|
73
79
|
}
|
|
@@ -76,7 +82,7 @@ export async function resolvePageRef(library, pageName) {
|
|
|
76
82
|
if (maybe) {
|
|
77
83
|
return maybe;
|
|
78
84
|
}
|
|
79
|
-
const filePath = `${
|
|
85
|
+
const filePath = `${baseUrl}/${key}.md`;
|
|
80
86
|
const stub = { key, name: pageName, description: undefined, filePath, sections: [] };
|
|
81
87
|
pageCache.set(stub.key, stub);
|
|
82
88
|
return stub;
|
|
@@ -4,11 +4,22 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { parseSectionsFromMarkdown } from './parser.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
export async function startServer(library, version,
|
|
7
|
+
export async function startServer(library, version, options = {}) {
|
|
8
8
|
const server = new McpServer({
|
|
9
9
|
name: library === 's2' ? 's2-docs-server' : 'react-aria-docs-server',
|
|
10
10
|
version
|
|
11
11
|
});
|
|
12
|
+
const libraries = [library, ...(options.additionalLibraries ?? [])];
|
|
13
|
+
for (const lib of libraries) {
|
|
14
|
+
await registerLibraryDocsTools(server, lib);
|
|
15
|
+
}
|
|
16
|
+
if (options.registerAdditionalTools) {
|
|
17
|
+
await options.registerAdditionalTools(server);
|
|
18
|
+
}
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
}
|
|
22
|
+
async function registerLibraryDocsTools(server, library) {
|
|
12
23
|
// Build page index at startup.
|
|
13
24
|
try {
|
|
14
25
|
await buildPageIndex(library);
|
|
@@ -18,9 +29,12 @@ export async function startServer(library, version, registerAdditionalTools) {
|
|
|
18
29
|
}
|
|
19
30
|
const toolPrefix = library === 's2' ? 's2' : 'react_aria';
|
|
20
31
|
server.registerTool(`list_${toolPrefix}_pages`, {
|
|
21
|
-
title: library === 's2'
|
|
32
|
+
title: library === 's2'
|
|
33
|
+
? 'List React Spectrum (@react-spectrum/s2) docs pages'
|
|
34
|
+
: 'List React Aria docs pages',
|
|
22
35
|
description: `Returns a list of available pages in the ${library} docs.`,
|
|
23
|
-
inputSchema: { includeDescription: z.boolean().optional() }
|
|
36
|
+
inputSchema: { includeDescription: z.boolean().optional() },
|
|
37
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
24
38
|
}, async ({ includeDescription }) => {
|
|
25
39
|
const pages = await buildPageIndex(library);
|
|
26
40
|
const items = pages
|
|
@@ -33,7 +47,8 @@ export async function startServer(library, version, registerAdditionalTools) {
|
|
|
33
47
|
server.registerTool(`get_${toolPrefix}_page_info`, {
|
|
34
48
|
title: 'Get page info',
|
|
35
49
|
description: 'Returns page description and list of sections for a given page.',
|
|
36
|
-
inputSchema: { page_name: z.string() }
|
|
50
|
+
inputSchema: { page_name: z.string() },
|
|
51
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
37
52
|
}, async ({ page_name }) => {
|
|
38
53
|
const ref = await resolvePageRef(library, page_name);
|
|
39
54
|
const info = await ensureParsedPage(ref);
|
|
@@ -47,7 +62,8 @@ export async function startServer(library, version, registerAdditionalTools) {
|
|
|
47
62
|
server.registerTool(`get_${toolPrefix}_page`, {
|
|
48
63
|
title: 'Get page markdown',
|
|
49
64
|
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() }
|
|
65
|
+
inputSchema: { page_name: z.string(), section_name: z.string().optional() },
|
|
66
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
51
67
|
}, async ({ page_name, section_name }) => {
|
|
52
68
|
const ref = await resolvePageRef(library, page_name);
|
|
53
69
|
let text;
|
|
@@ -68,9 +84,4 @@ export async function startServer(library, version, registerAdditionalTools) {
|
|
|
68
84
|
const snippet = lines.slice(section.startLine, section.endLine).join('\n');
|
|
69
85
|
return { content: [{ type: 'text', text: snippet }] };
|
|
70
86
|
});
|
|
71
|
-
if (registerAdditionalTools) {
|
|
72
|
-
await registerAdditionalTools(server);
|
|
73
|
-
}
|
|
74
|
-
const transport = new StdioServerTransport();
|
|
75
|
-
await server.connect(transport);
|
|
76
87
|
}
|
package/dist/shared/src/utils.js
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
+
import { dirname, join } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
// Resolves the version from the nearest package.json above the CLI entry point.
|
|
5
|
+
// Both the npm-published layout (dist/<lib>/src/index.js) and the .mcpb bundle
|
|
6
|
+
// layout (server/<lib>/src/index.js) place package.json three levels up.
|
|
7
|
+
export function readPackageVersion(entryUrl) {
|
|
8
|
+
const entryDir = dirname(fileURLToPath(entryUrl));
|
|
9
|
+
const pkgPath = join(entryDir, '..', '..', '..', 'package.json');
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
12
|
+
if (typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
13
|
+
return pkg.version;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall through
|
|
18
|
+
}
|
|
19
|
+
return '0.0.0';
|
|
20
|
+
}
|
|
1
21
|
export function errorToString(err) {
|
|
2
22
|
if (err && typeof err === 'object' && 'stack' in err && typeof err.stack === 'string') {
|
|
3
23
|
return err.stack;
|
|
4
24
|
}
|
|
5
|
-
if (err &&
|
|
25
|
+
if (err &&
|
|
26
|
+
typeof err === 'object' &&
|
|
27
|
+
'message' in err &&
|
|
28
|
+
typeof err.message === 'string') {
|
|
6
29
|
return err.message;
|
|
7
30
|
}
|
|
8
31
|
try {
|
|
@@ -12,8 +35,15 @@ export function errorToString(err) {
|
|
|
12
35
|
return String(err);
|
|
13
36
|
}
|
|
14
37
|
}
|
|
15
|
-
//
|
|
16
|
-
|
|
38
|
+
// Default base URLs for each library
|
|
39
|
+
const DEFAULT_S2_BASE = 'https://react-spectrum.adobe.com';
|
|
40
|
+
const DEFAULT_REACT_ARIA_BASE = 'https://react-aria.adobe.com';
|
|
41
|
+
export function getLibraryBaseUrl(library) {
|
|
42
|
+
if (process.env.DOCS_CDN_BASE) {
|
|
43
|
+
return process.env.DOCS_CDN_BASE;
|
|
44
|
+
}
|
|
45
|
+
return library === 's2' ? DEFAULT_S2_BASE : DEFAULT_REACT_ARIA_BASE;
|
|
46
|
+
}
|
|
17
47
|
export async function fetchText(url, timeoutMs = 15000) {
|
|
18
48
|
const ctrl = new AbortController();
|
|
19
49
|
const id = setTimeout(() => ctrl.abort(), timeoutMs).unref?.();
|
package/package.json
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-spectrum/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server for React Spectrum (S2) documentation",
|
|
5
|
-
"
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/adobe/react-spectrum"
|
|
9
|
+
},
|
|
6
10
|
"bin": "dist/s2/src/index.js",
|
|
11
|
+
"source": "src/index.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"*.css"
|
|
19
|
+
],
|
|
20
|
+
"main": "dist/main.js",
|
|
21
|
+
"module": "dist/module.js",
|
|
22
|
+
"types": "dist/types.d.ts",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
7
26
|
"scripts": {
|
|
8
27
|
"prepublishOnly": "yarn build",
|
|
9
28
|
"build": "node ./scripts/build-data.mjs && tsc -p tsconfig.json",
|
|
@@ -22,24 +41,5 @@
|
|
|
22
41
|
"engines": {
|
|
23
42
|
"node": ">=18"
|
|
24
43
|
},
|
|
25
|
-
"
|
|
26
|
-
"publishConfig": {
|
|
27
|
-
"access": "public"
|
|
28
|
-
},
|
|
29
|
-
"repository": {
|
|
30
|
-
"type": "git",
|
|
31
|
-
"url": "https://github.com/adobe/react-spectrum"
|
|
32
|
-
},
|
|
33
|
-
"files": [
|
|
34
|
-
"dist",
|
|
35
|
-
"src"
|
|
36
|
-
],
|
|
37
|
-
"sideEffects": [
|
|
38
|
-
"*.css"
|
|
39
|
-
],
|
|
40
|
-
"main": "dist/main.js",
|
|
41
|
-
"module": "dist/module.js",
|
|
42
|
-
"types": "dist/types.d.ts",
|
|
43
|
-
"source": "src/index.ts",
|
|
44
|
-
"gitHead": "4d838da5bfe36abb35aed166995a9ef63825370f"
|
|
44
|
+
"gitHead": "3577a20859b9585a000010c720f6ee39c1c588cc"
|
|
45
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import {errorToString} from '../../shared/src/utils.js';
|
|
4
|
-
import {
|
|
3
|
+
import {errorToString, readPackageVersion} from '../../shared/src/utils.js';
|
|
4
|
+
import {
|
|
5
|
+
listIconNames,
|
|
6
|
+
listIllustrationNames,
|
|
7
|
+
loadIconAliases,
|
|
8
|
+
loadIllustrationAliases,
|
|
9
|
+
loadStyleMacroPropertyValues
|
|
10
|
+
} from './s2-data.js';
|
|
5
11
|
import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
12
|
import {startServer} from '../../shared/src/server.js';
|
|
7
13
|
import {z} from 'zod';
|
|
@@ -11,115 +17,182 @@ import {z} from 'zod';
|
|
|
11
17
|
try {
|
|
12
18
|
const arg = (process.argv[2] || '').trim();
|
|
13
19
|
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
14
|
-
console.log(
|
|
20
|
+
console.log(
|
|
21
|
+
'Usage: npx @react-spectrum/mcp@latest\n\nStarts the MCP server for React Spectrum (S2) documentation.'
|
|
22
|
+
);
|
|
15
23
|
process.exit(0);
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
await startServer('s2',
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
await startServer('s2', readPackageVersion(import.meta.url), {
|
|
27
|
+
additionalLibraries: ['react-aria'],
|
|
28
|
+
registerAdditionalTools: (server: McpServer) => {
|
|
29
|
+
server.registerTool(
|
|
30
|
+
'search_s2_icons',
|
|
31
|
+
{
|
|
32
|
+
title: 'Search S2 icons',
|
|
33
|
+
description:
|
|
34
|
+
'Searches the S2 workflow icon set by one or more terms; returns matching icon names.',
|
|
35
|
+
inputSchema: {terms: z.union([z.string(), z.array(z.string())])},
|
|
36
|
+
annotations: {readOnlyHint: true, openWorldHint: true}
|
|
37
|
+
},
|
|
38
|
+
async ({terms}) => {
|
|
39
|
+
const allNames = listIconNames();
|
|
40
|
+
const nameSet = new Set(allNames);
|
|
41
|
+
const aliases = await loadIconAliases();
|
|
42
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
43
|
+
const normalized = Array.from(
|
|
44
|
+
new Set(
|
|
45
|
+
rawTerms
|
|
46
|
+
.map(t =>
|
|
47
|
+
String(t ?? '')
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
)
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
if (normalized.length === 0) {
|
|
55
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
56
|
+
}
|
|
57
|
+
// direct name matches
|
|
58
|
+
const results = new Set(
|
|
59
|
+
allNames.filter(name => {
|
|
60
|
+
const nameLower = name.toLowerCase();
|
|
61
|
+
return normalized.some(term => nameLower.includes(term));
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
// alias matches
|
|
65
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
66
|
+
if (!targets || targets.length === 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
70
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
71
|
+
for (const t of targets) {
|
|
72
|
+
const n = String(t);
|
|
73
|
+
if (nameSet.has(n)) {
|
|
74
|
+
results.add(n);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
48
77
|
}
|
|
49
78
|
}
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: JSON.stringify(
|
|
84
|
+
Array.from(results).sort((a, b) => a.localeCompare(b)),
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
50
91
|
}
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
);
|
|
92
|
+
);
|
|
54
93
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (normalized.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
server.registerTool(
|
|
95
|
+
'search_s2_illustrations',
|
|
96
|
+
{
|
|
97
|
+
title: 'Search S2 illustrations',
|
|
98
|
+
description:
|
|
99
|
+
'Searches the S2 illustrations set by one or more terms; returns matching illustration names.',
|
|
100
|
+
inputSchema: {terms: z.union([z.string(), z.array(z.string())])},
|
|
101
|
+
annotations: {readOnlyHint: true, openWorldHint: true}
|
|
102
|
+
},
|
|
103
|
+
async ({terms}) => {
|
|
104
|
+
const allNames = listIllustrationNames();
|
|
105
|
+
const nameSet = new Set(allNames);
|
|
106
|
+
const aliases = await loadIllustrationAliases();
|
|
107
|
+
const rawTerms = Array.isArray(terms) ? terms : [terms];
|
|
108
|
+
const normalized = Array.from(
|
|
109
|
+
new Set(
|
|
110
|
+
rawTerms
|
|
111
|
+
.map(t =>
|
|
112
|
+
String(t ?? '')
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
)
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
if (normalized.length === 0) {
|
|
120
|
+
throw new Error('Provide at least one non-empty search term.');
|
|
121
|
+
}
|
|
122
|
+
// direct name matches
|
|
123
|
+
const results = new Set(
|
|
124
|
+
allNames.filter(name => {
|
|
125
|
+
const nameLower = name.toLowerCase();
|
|
126
|
+
return normalized.some(term => nameLower.includes(term));
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
// alias matches
|
|
130
|
+
for (const [aliasKey, targets] of Object.entries(aliases)) {
|
|
131
|
+
if (!targets || targets.length === 0) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const aliasLower = aliasKey.toLowerCase();
|
|
135
|
+
if (normalized.some(term => aliasLower.includes(term) || term.includes(aliasLower))) {
|
|
136
|
+
for (const t of targets) {
|
|
137
|
+
const n = String(t);
|
|
138
|
+
if (nameSet.has(n)) {
|
|
139
|
+
results.add(n);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
84
142
|
}
|
|
85
143
|
}
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: 'text',
|
|
148
|
+
text: JSON.stringify(
|
|
149
|
+
Array.from(results).sort((a, b) => a.localeCompare(b)),
|
|
150
|
+
null,
|
|
151
|
+
2
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
};
|
|
86
156
|
}
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
);
|
|
157
|
+
);
|
|
90
158
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
159
|
+
server.registerTool(
|
|
160
|
+
'get_style_macro_property_values',
|
|
161
|
+
{
|
|
162
|
+
title: 'Get style macro property values',
|
|
163
|
+
description:
|
|
164
|
+
'Returns the allowed values for a given S2 style macro property (including expanded color/spacing value lists where applicable).',
|
|
165
|
+
inputSchema: {propertyName: z.string()},
|
|
166
|
+
annotations: {readOnlyHint: true, openWorldHint: true}
|
|
167
|
+
},
|
|
168
|
+
async ({propertyName}) => {
|
|
169
|
+
const name = String(propertyName ?? '').trim();
|
|
170
|
+
if (!name) {
|
|
171
|
+
throw new Error('Provide a non-empty propertyName.');
|
|
172
|
+
}
|
|
103
173
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
174
|
+
const all = loadStyleMacroPropertyValues();
|
|
175
|
+
let def = all[name];
|
|
176
|
+
if (!def) {
|
|
177
|
+
// fallback to case-insensitive lookup
|
|
178
|
+
const lower = name.toLowerCase();
|
|
179
|
+
const matchKey = Object.keys(all).find(k => k.toLowerCase() === lower);
|
|
180
|
+
if (matchKey) {
|
|
181
|
+
def = all[matchKey];
|
|
182
|
+
}
|
|
112
183
|
}
|
|
113
|
-
}
|
|
114
184
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
if (!def) {
|
|
186
|
+
const available = Object.keys(all).sort((a, b) => a.localeCompare(b));
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Unknown style macro property '${name}'. Available properties: ${available.join(', ')}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
119
191
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
192
|
+
return {content: [{type: 'text', text: JSON.stringify(def, null, 2)}]};
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
123
196
|
});
|
|
124
197
|
} catch (err) {
|
|
125
198
|
console.error(errorToString(err));
|
package/src/s2-data.ts
CHANGED
|
@@ -9,13 +9,18 @@ let iconIdCache: string[] | null = null;
|
|
|
9
9
|
let illustrationIdCache: string[] | null = null;
|
|
10
10
|
let iconAliasesCache: Record<string, string[]> | null = null;
|
|
11
11
|
let illustrationAliasesCache: Record<string, string[]> | null = null;
|
|
12
|
-
let styleMacroPropertyValuesCache: Record<
|
|
12
|
+
let styleMacroPropertyValuesCache: Record<
|
|
13
|
+
string,
|
|
14
|
+
{values: string[]; additionalTypes?: string[]}
|
|
15
|
+
> | null = null;
|
|
13
16
|
|
|
14
17
|
function readBundledJson(filename: string): any | null {
|
|
15
18
|
try {
|
|
16
19
|
// Go up from s2/src/ to dist/, then to data/
|
|
17
20
|
const p = path.resolve(__dirname, '..', '..', 'data', filename);
|
|
18
|
-
if (!fs.existsSync(p)) {
|
|
21
|
+
if (!fs.existsSync(p)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
19
24
|
const txt = fs.readFileSync(p, 'utf8');
|
|
20
25
|
return JSON.parse(txt);
|
|
21
26
|
} catch {
|
|
@@ -24,31 +29,48 @@ function readBundledJson(filename: string): any | null {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export function listIconNames(): string[] {
|
|
27
|
-
if (iconIdCache) {
|
|
32
|
+
if (iconIdCache) {
|
|
33
|
+
return iconIdCache;
|
|
34
|
+
}
|
|
28
35
|
const bundled = readBundledJson('icons.json');
|
|
29
|
-
return (iconIdCache = Array.isArray(bundled)
|
|
36
|
+
return (iconIdCache = Array.isArray(bundled)
|
|
37
|
+
? bundled.slice().sort((a, b) => a.localeCompare(b))
|
|
38
|
+
: []);
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export function listIllustrationNames(): string[] {
|
|
33
|
-
if (illustrationIdCache) {
|
|
42
|
+
if (illustrationIdCache) {
|
|
43
|
+
return illustrationIdCache;
|
|
44
|
+
}
|
|
34
45
|
const bundled = readBundledJson('illustrations.json');
|
|
35
|
-
return (illustrationIdCache = Array.isArray(bundled)
|
|
46
|
+
return (illustrationIdCache = Array.isArray(bundled)
|
|
47
|
+
? bundled.slice().sort((a, b) => a.localeCompare(b))
|
|
48
|
+
: []);
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export async function loadIconAliases(): Promise<Record<string, string[]>> {
|
|
39
|
-
if (iconAliasesCache) {
|
|
52
|
+
if (iconAliasesCache) {
|
|
53
|
+
return iconAliasesCache;
|
|
54
|
+
}
|
|
40
55
|
const bundled = readBundledJson('iconAliases.json');
|
|
41
|
-
return (iconAliasesCache =
|
|
56
|
+
return (iconAliasesCache = bundled && typeof bundled === 'object' ? bundled : {});
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export async function loadIllustrationAliases(): Promise<Record<string, string[]>> {
|
|
45
|
-
if (illustrationAliasesCache) {
|
|
60
|
+
if (illustrationAliasesCache) {
|
|
61
|
+
return illustrationAliasesCache;
|
|
62
|
+
}
|
|
46
63
|
const bundled = readBundledJson('illustrationAliases.json');
|
|
47
|
-
return (illustrationAliasesCache =
|
|
64
|
+
return (illustrationAliasesCache = bundled && typeof bundled === 'object' ? bundled : {});
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
export function loadStyleMacroPropertyValues(): Record<
|
|
51
|
-
|
|
67
|
+
export function loadStyleMacroPropertyValues(): Record<
|
|
68
|
+
string,
|
|
69
|
+
{values: string[]; additionalTypes?: string[]}
|
|
70
|
+
> {
|
|
71
|
+
if (styleMacroPropertyValuesCache) {
|
|
72
|
+
return styleMacroPropertyValuesCache;
|
|
73
|
+
}
|
|
52
74
|
const bundled = readBundledJson('styleMacroPropertyValues.json');
|
|
53
75
|
if (!bundled || typeof bundled !== 'object' || Array.isArray(bundled)) {
|
|
54
76
|
return (styleMacroPropertyValuesCache = {});
|