From 90191da6e4e19582f8c62f6435588f6ee32f5063 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 11 Dec 2025 13:43:55 -0600 Subject: [PATCH] add get_style_macro_property_values to s2 mcp server --- packages/dev/mcp/s2/scripts/build-data.mjs | 138 +++++++++++++++++++++ packages/dev/mcp/s2/src/index.ts | 35 +++++- packages/dev/mcp/s2/src/s2-data.ts | 10 ++ 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/packages/dev/mcp/s2/scripts/build-data.mjs b/packages/dev/mcp/s2/scripts/build-data.mjs index 8145ee77aa6..bec0322c838 100644 --- a/packages/dev/mcp/s2/scripts/build-data.mjs +++ b/packages/dev/mcp/s2/scripts/build-data.mjs @@ -1,8 +1,11 @@ #!/usr/bin/env node +import {createRequire} from 'module'; import fg from 'fast-glob'; import {fileURLToPath, pathToFileURL} from 'url'; import fs from 'fs'; import path from 'path'; +// eslint-disable-next-line rulesdir/imports +import * as ts from 'typescript'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -13,6 +16,7 @@ const ICONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/s2wf-icon const ILLUSTRATIONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/spectrum-illustrations/linear'); const ICON_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/iconAliases.js'); const ILLUSTRATION_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/illustrationAliases.js'); +const STYLE_PROPERTIES_TS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/styleProperties.ts'); function ensureDir(p) { fs.mkdirSync(p, {recursive: true}); @@ -24,6 +28,23 @@ function writeJson(file, data) { console.log('Wrote', path.relative(REPO_ROOT, file)); } +async function importTsModule(tsFilePath) { + if (!fs.existsSync(tsFilePath)) { + throw new Error(`TS module not found: ${tsFilePath}`); + } + const sourceText = fs.readFileSync(tsFilePath, 'utf8'); + const result = ts.transpileModule(sourceText, { + fileName: tsFilePath, + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + esModuleInterop: true + } + }); + const url = `data:text/javascript;base64,${Buffer.from(result.outputText, 'utf8').toString('base64')}`; + return import(url); +} + function buildIconNames() { if (!fs.existsSync(ICONS_DIR)) { throw new Error(`Icons directory not found: ${ICONS_DIR}`); @@ -61,6 +82,107 @@ async function loadAliases(modPath, exportName) { return mod[exportName] ?? {}; } +function buildBaseColorKeysFromSpectrumTokens(tokens) { + const keys = new Set(); + + // Matches spectrum-theme.ts + keys.add('transparent'); + keys.add('black'); + keys.add('white'); + + const addScale = (scale) => { + const re = new RegExp(`^${scale}-\\d+$`); + for (const tokenName of Object.keys(tokens)) { + if (re.test(tokenName)) { + // Match @react-spectrum/s2/style/tokens.ts behavior: strip "-color" in the middle. + keys.add(tokenName.replace('-color', '')); + } + } + }; + + // Global color scales + for (const scale of [ + 'gray', 'blue', 'red', 'orange', 'yellow', 'chartreuse', 'celery', 'green', + 'seafoam', 'cyan', 'indigo', 'purple', 'fuchsia', 'magenta', 'pink', + 'turquoise', 'brown', 'silver', 'cinnamon' + ]) { + addScale(scale); + } + + // Semantic color scales + for (const scale of ['accent-color', 'informative-color', 'negative-color', 'notice-color', 'positive-color']) { + addScale(scale); + } + + // Simple transparent scales (names remain unchanged) + for (const scale of ['transparent-white', 'transparent-black']) { + const re = new RegExp(`^${scale}-\\d+$`); + for (const tokenName of Object.keys(tokens)) { + if (re.test(tokenName)) { + keys.add(tokenName); + } + } + } + + // Overlay scale keys (derived in tokens.ts, we only need the names here) + for (const n of [25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]) { + keys.add(`transparent-overlay-${n}`); + } + + // High contrast keywords (matches spectrum-theme.ts) + for (const k of ['Background', 'ButtonBorder', 'ButtonFace', 'ButtonText', 'Field', 'Highlight', 'HighlightText', 'GrayText', 'Mark', 'LinkText']) { + keys.add(k); + } + + return Array.from(keys).sort((a, b) => a.localeCompare(b)); +} + +function buildExpandedStyleMacroPropertyValues(styleProperties, spacingTypeValues, baseColorKeys) { + const out = {}; + + for (const [propertyName, def] of Object.entries(styleProperties)) { + const values = []; + const seen = new Set(); + + const pushUnique = (items) => { + for (const v of items) { + const s = String(v); + if (!seen.has(s)) { + seen.add(s); + values.push(s); + } + } + }; + + // Expand 'baseColors' placeholder into actual color token names. + const expandedBase = []; + for (const v of def.values ?? []) { + if (v === 'baseColors') { + expandedBase.push(...baseColorKeys); + } else { + expandedBase.push(v); + } + } + pushUnique(expandedBase); + + // Expand spacing type placeholders into the actual numeric values shown in docs. + const additionalTypes = Array.isArray(def.additionalTypes) ? def.additionalTypes : []; + if (additionalTypes.includes('baseSpacing')) { + pushUnique(spacingTypeValues?.baseSpacing ?? []); + } + if (additionalTypes.includes('negativeSpacing')) { + pushUnique(spacingTypeValues?.negativeSpacing ?? []); + } + + out[propertyName] = { + values, + additionalTypes + }; + } + + return out; +} + async function main() { const icons = buildIconNames(); const illustrations = buildIllustrationNames(); @@ -71,6 +193,22 @@ async function main() { writeJson(path.join(OUT_DIR, 'illustrations.json'), illustrations); writeJson(path.join(OUT_DIR, 'iconAliases.json'), iconAliases); writeJson(path.join(OUT_DIR, 'illustrationAliases.json'), illustrationAliases); + + // Style macro property definitions + const stylePropsMod = await importTsModule(STYLE_PROPERTIES_TS); + const propertyCategories = ['color', 'dimensions', 'text', 'effects', 'layout', 'misc', 'conditions']; + const styleProperties = {}; + for (const category of propertyCategories) { + Object.assign(styleProperties, stylePropsMod.getPropertyDefinitions(category)); + } + Object.assign(styleProperties, stylePropsMod.getShorthandDefinitions()); + writeJson(path.join(OUT_DIR, 'styleProperties.json'), styleProperties); + + const require = createRequire(import.meta.url); + const spectrumTokens = require('@adobe/spectrum-tokens/dist/json/variables.json'); + const baseColorKeys = buildBaseColorKeysFromSpectrumTokens(spectrumTokens); + const expanded = buildExpandedStyleMacroPropertyValues(styleProperties, stylePropsMod.spacingTypeValues, baseColorKeys); + writeJson(path.join(OUT_DIR, 'styleMacroPropertyValues.json'), expanded); } main().catch((err) => { diff --git a/packages/dev/mcp/s2/src/index.ts b/packages/dev/mcp/s2/src/index.ts index e71b5ad9c0c..b8a8418cd76 100644 --- a/packages/dev/mcp/s2/src/index.ts +++ b/packages/dev/mcp/s2/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /// import {errorToString} from '../../shared/src/utils.js'; -import {listIconNames, listIllustrationNames, loadIconAliases, loadIllustrationAliases} from './s2-data.js'; +import {listIconNames, listIllustrationNames, loadIconAliases, loadIllustrationAliases, loadStyleMacroPropertyValues} from './s2-data.js'; import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; import {startServer} from '../../shared/src/server.js'; import {z} from 'zod'; @@ -87,6 +87,39 @@ import {z} from 'zod'; return {content: [{type: 'text', text: JSON.stringify(Array.from(results).sort((a, b) => a.localeCompare(b)), null, 2)}]}; } ); + + server.registerTool( + 'get_style_macro_property_values', + { + title: 'Get style macro property values', + description: 'Returns the allowed values for a given S2 style macro property (including expanded color/spacing value lists where applicable).', + inputSchema: {propertyName: z.string()} + }, + async ({propertyName}) => { + const name = String(propertyName ?? '').trim(); + if (!name) { + throw new Error('Provide a non-empty propertyName.'); + } + + const all = loadStyleMacroPropertyValues(); + let def = all[name]; + if (!def) { + // fallback to case-insensitive lookup + const lower = name.toLowerCase(); + const matchKey = Object.keys(all).find(k => k.toLowerCase() === lower); + if (matchKey) { + def = all[matchKey]; + } + } + + if (!def) { + const available = Object.keys(all).sort((a, b) => a.localeCompare(b)); + throw new Error(`Unknown style macro property '${name}'. Available properties: ${available.join(', ')}`); + } + + return {content: [{type: 'text', text: JSON.stringify(def, null, 2)}]}; + } + ); }); } catch (err) { console.error(errorToString(err)); diff --git a/packages/dev/mcp/s2/src/s2-data.ts b/packages/dev/mcp/s2/src/s2-data.ts index 8b37337cf18..7e3fdcc5a87 100644 --- a/packages/dev/mcp/s2/src/s2-data.ts +++ b/packages/dev/mcp/s2/src/s2-data.ts @@ -9,6 +9,7 @@ let iconIdCache: string[] | null = null; let illustrationIdCache: string[] | null = null; let iconAliasesCache: Record | null = null; let illustrationAliasesCache: Record | null = null; +let styleMacroPropertyValuesCache: Record | null = null; function readBundledJson(filename: string): any | null { try { @@ -45,3 +46,12 @@ export async function loadIllustrationAliases(): Promise { + if (styleMacroPropertyValuesCache) {return styleMacroPropertyValuesCache;} + const bundled = readBundledJson('styleMacroPropertyValues.json'); + if (!bundled || typeof bundled !== 'object' || Array.isArray(bundled)) { + return (styleMacroPropertyValuesCache = {}); + } + return (styleMacroPropertyValuesCache = bundled as any); +}