From a26b82d70f4dc779d30ba5aa63d40599ad419b29 Mon Sep 17 00:00:00 2001 From: garthdb Date: Tue, 10 Feb 2026 09:30:49 -0700 Subject: [PATCH 01/15] feat(mcp): add agent skills for component building and token discovery --- .changeset/agent-skills-feature.md | 5 + llms.txt | 21 + tools/spectrum-design-data-mcp/README.md | 51 ++- .../agent-skills/README.md | 153 +++++++ .../agent-skills/component-builder.md | 190 ++++++++ .../agent-skills/token-finder.md | 280 ++++++++++++ tools/spectrum-design-data-mcp/src/index.js | 11 +- .../src/tools/workflows.js | 431 ++++++++++++++++++ .../test/tools/workflows.test.js | 260 +++++++++++ 9 files changed, 1395 insertions(+), 7 deletions(-) create mode 100644 .changeset/agent-skills-feature.md create mode 100644 tools/spectrum-design-data-mcp/agent-skills/README.md create mode 100644 tools/spectrum-design-data-mcp/agent-skills/component-builder.md create mode 100644 tools/spectrum-design-data-mcp/agent-skills/token-finder.md create mode 100644 tools/spectrum-design-data-mcp/src/tools/workflows.js create mode 100644 tools/spectrum-design-data-mcp/test/tools/workflows.test.js diff --git a/.changeset/agent-skills-feature.md b/.changeset/agent-skills-feature.md new file mode 100644 index 00000000..946b88f5 --- /dev/null +++ b/.changeset/agent-skills-feature.md @@ -0,0 +1,5 @@ +--- +"@adobe/spectrum-design-data-mcp": minor +--- + +Added Agent Skills for component building and token discovery. Agent Skills are markdown guides that help AI agents orchestrate MCP tools into complete workflows. Includes Component Builder and Token Finder skills, plus new workflow-oriented MCP tools. diff --git a/llms.txt b/llms.txt index 6b01c13a..07c47d8b 100644 --- a/llms.txt +++ b/llms.txt @@ -210,6 +210,26 @@ pnpm generateDiffResult 3. **Token Diff Analysis**: Compare token changes between versions 4. **Documentation Generation**: Create docs from schemas 5. **Migration Assistance**: Help with S1 → S2 migrations +6. **Component Building**: Use Agent Skills to build Spectrum components correctly + +### Agent Skills for AI Agents + +The Spectrum Design Data MCP includes **Agent Skills** - markdown guides that help AI agents work with Spectrum design data: + +- **[Component Builder](tools/spectrum-design-data-mcp/agent-skills/component-builder.md)**: Guides agents through building Spectrum components by orchestrating MCP tools to discover schemas, find tokens, and validate configurations +- **[Token Finder](tools/spectrum-design-data-mcp/agent-skills/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling + +**How to Use Agent Skills:** +1. When a user asks about building components or finding tokens, read the relevant Agent Skill +2. Follow the step-by-step workflow provided in the skill +3. Call the MCP tools as directed (e.g., `get-component-schema`, `find-tokens-by-use-case`) +4. Combine tool outputs into a complete solution + +**Workflow Pattern:** +- Component Building: `get-component-schema` → `get-component-tokens` → `find-tokens-by-use-case` → `validate-component-props` +- Token Discovery: `get-design-recommendations` → `find-tokens-by-use-case` → `get-token-details` + +See `tools/spectrum-design-data-mcp/agent-skills/README.md` for complete documentation. ### File Patterns to Understand @@ -217,6 +237,7 @@ pnpm generateDiffResult - `packages/component-schemas/schemas/components/*.json` - Component schemas - `docs/*/src/**/*.ts` - Visualization tool source code - `tools/*/src/**/*.js` - Utility tool implementations +- `tools/spectrum-design-data-mcp/agent-skills/*.md` - Agent Skills documentation ### Important Data Structures diff --git a/tools/spectrum-design-data-mcp/README.md b/tools/spectrum-design-data-mcp/README.md index 96f5fb99..1668bf6b 100644 --- a/tools/spectrum-design-data-mcp/README.md +++ b/tools/spectrum-design-data-mcp/README.md @@ -66,6 +66,44 @@ The server runs locally and communicates via stdio with MCP-compatible AI client * **`validate-component-props`**: Validate props against schema * **`search-components-by-feature`**: Find components by property name +#### Workflow Tools + +* **`build-component-config`**: Generate a complete component configuration with recommended tokens and props +* **`suggest-component-improvements`**: Analyze existing component configuration and suggest improvements + +## Agent Skills + +Agent Skills are markdown guides that help AI agents use the Spectrum Design Data MCP tools effectively. They orchestrate multiple MCP tools into complete workflows for common design system tasks. + +### Available Agent Skills + +* **[Component Builder](agent-skills/component-builder.md)**: Guides agents through building Spectrum components correctly by discovering schemas, finding tokens, and validating configurations +* **[Token Finder](agent-skills/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling + +### How Agent Skills Work + +Agent Skills are documentation files that: +- Guide AI agents through multi-step workflows +- Orchestrate existing MCP tools into complete tasks +- Provide examples and best practices +- Help agents discover the right tools for specific use cases + +Unlike MCP tools (which are executable functions), Agent Skills are **guidance documents** that tell agents how to combine tools to accomplish complex tasks. + +### Using Agent Skills + +For AI agents working with Spectrum components: +1. Read the relevant Agent Skill when a user asks about a covered task +2. Follow the step-by-step workflow provided +3. Call the MCP tools as directed by the skill +4. Combine tool outputs into a complete solution + +See the [Agent Skills README](agent-skills/README.md) for more details. + +### Related Resources + +This implementation follows the pattern established by [React Spectrum's AI integration](https://react-spectrum.adobe.com/ai), which also uses MCP and Agent Skills to help AI agents work with design systems. + ## Configuration ### MCP Setup @@ -253,10 +291,15 @@ src/ ├── cli.js # CLI interface ├── tools/ # MCP tool implementations │ ├── tokens.js # Token-related tools -│ └── schemas.js # Schema-related tools -└── data/ # Data access layer - ├── tokens.js # Token data access - └── schemas.js # Schema data access +│ ├── schemas.js # Schema-related tools +│ └── workflows.js # Workflow-oriented tools +├── data/ # Data access layer +│ ├── tokens.js # Token data access +│ └── schemas.js # Schema data access +└── agent-skills/ # Agent Skills documentation + ├── component-builder.md + ├── token-finder.md + └── README.md ``` ## Security diff --git a/tools/spectrum-design-data-mcp/agent-skills/README.md b/tools/spectrum-design-data-mcp/agent-skills/README.md new file mode 100644 index 00000000..82580dab --- /dev/null +++ b/tools/spectrum-design-data-mcp/agent-skills/README.md @@ -0,0 +1,153 @@ +# Spectrum Design Data Agent Skills + +## Overview + +Agent Skills are markdown guides that help AI agents use the Spectrum Design Data MCP tools effectively. They orchestrate multiple MCP tools into complete workflows for common design system tasks. + +## What are Agent Skills? + +Agent Skills are documentation files that: +- Guide AI agents through multi-step workflows +- Orchestrate existing MCP tools into complete tasks +- Provide examples and best practices +- Help agents discover the right tools for specific use cases + +Unlike MCP tools (which are executable functions), Agent Skills are **guidance documents** that tell agents how to combine tools to accomplish complex tasks. + +## Available Skills + +### [Component Builder](component-builder.md) + +Helps agents build Spectrum components correctly by: +- Discovering component schemas +- Finding appropriate design tokens +- Validating component configurations +- Following Spectrum design patterns + +**Use when**: Building, creating, or implementing Spectrum components + +### [Token Finder](token-finder.md) + +Helps agents discover the right design tokens for: +- Color decisions (semantic, component-specific) +- Spacing and layout +- Typography +- Component styling + +**Use when**: Finding tokens for design decisions or styling components + +## How Agent Skills Work + +Agent Skills don't execute code directly. Instead, they: + +1. **Guide tool selection**: Tell agents which MCP tools to use +2. **Orchestrate workflows**: Show how to combine multiple tools +3. **Provide examples**: Demonstrate real-world usage patterns +4. **Share best practices**: Help agents make better decisions + +### Example Workflow + +When an agent needs to build a button: + +1. Agent reads `component-builder.md` +2. Skill guides agent to use `get-component-schema` first +3. Then use `get-component-tokens` to find related tokens +4. Use `find-tokens-by-use-case` for specific styling needs +5. Finally use `validate-component-props` to ensure correctness + +The skill provides the **workflow**, while the MCP tools provide the **data**. + +## Integration with MCP Tools + +Agent Skills work alongside the Spectrum Design Data MCP tools: + +### Token Tools +- `query-tokens` - Search tokens +- `find-tokens-by-use-case` ⭐ - Find tokens for use cases +- `get-component-tokens` ⭐ - Get component-specific tokens +- `get-design-recommendations` ⭐ - Get semantic recommendations +- `get-token-categories` - List categories +- `get-token-details` - Get token details + +### Schema Tools +- `query-component-schemas` - Search schemas +- `get-component-schema` ⭐ - Get component API +- `list-components` - List all components +- `validate-component-props` ⭐ - Validate configurations +- `get-type-schemas` - Get type definitions +- `get-component-options` ⭐ - User-friendly property discovery +- `search-components-by-feature` ⭐ - Find components by feature + +⭐ = Frequently used in Agent Skills + +## Using Agent Skills + +### For AI Agents + +1. **Read the skill**: When a user asks about a task covered by a skill, read the relevant skill file +2. **Follow the workflow**: Use the step-by-step guidance +3. **Call MCP tools**: Execute the MCP tools as directed +4. **Combine results**: Synthesize tool outputs into a complete solution + +### For Developers + +1. **Reference in prompts**: Mention Agent Skills when asking agents to work with Spectrum +2. **Link in documentation**: Reference skills in your project documentation +3. **Extend skills**: Create new skills for additional workflows + +## Creating New Agent Skills + +To create a new Agent Skill: + +1. **Identify the workflow**: What multi-step task needs guidance? +2. **Map to MCP tools**: Which existing tools support this workflow? +3. **Write the guide**: Create a markdown file with: + - Overview and when to use + - Step-by-step workflow + - Examples with real use cases + - Best practices + - Related tools reference +4. **Add to this README**: Document the new skill + +### Skill Template + +```markdown +# [Skill Name] Agent Skill + +## Overview +Brief description of what this skill helps with. + +## When to Use +List scenarios when this skill should be activated. + +## Workflow +Step-by-step guidance on using MCP tools. + +## Example +Real-world example showing the workflow. + +## Best Practices +Tips for using this skill effectively. + +## Related Tools +List of MCP tools used by this skill. +``` + +## Relationship to React Spectrum AI + +This implementation follows the pattern established by [React Spectrum's AI integration](https://react-spectrum.adobe.com/ai), which also uses MCP and Agent Skills to help AI agents work with design systems. + +## Contributing + +When adding new Agent Skills: +- Follow the existing markdown format +- Include clear examples +- Reference specific MCP tools +- Test workflows with real scenarios +- Update this README + +## Resources + +- [Spectrum Design Data MCP README](../README.md) +- [React Spectrum AI](https://react-spectrum.adobe.com/ai) +- [MCP Specification](https://modelcontextprotocol.io) diff --git a/tools/spectrum-design-data-mcp/agent-skills/component-builder.md b/tools/spectrum-design-data-mcp/agent-skills/component-builder.md new file mode 100644 index 00000000..3919021c --- /dev/null +++ b/tools/spectrum-design-data-mcp/agent-skills/component-builder.md @@ -0,0 +1,190 @@ +# Component Builder Agent Skill + +## Overview + +This Agent Skill helps AI agents build Spectrum components correctly by orchestrating multiple MCP tools to discover component schemas, find appropriate tokens, and validate configurations. + +## When to Use + +Activate this skill when: +- User asks to build, create, or implement a Spectrum component +- User needs help with component props, variants, or configuration +- User wants to validate component usage +- User asks about component structure or API + +## Workflow + +### Step 1: Discover Component Schema + +Use `get-component-schema` to understand the component's API: + +```json +{ + "component": "action-button" +} +``` + +This returns: +- Available properties +- Required properties +- Property types and enums +- Default values +- Component description + +### Step 2: Get Component-Specific Tokens + +Use `get-component-tokens` to find all tokens related to this component: + +```json +{ + "componentName": "action-button" +} +``` + +This returns tokens organized by category (color, layout, typography, etc.). + +### Step 3: Find Tokens for Specific Use Cases + +For each visual aspect (background, text, border, spacing), use `find-tokens-by-use-case`: + +```json +{ + "useCase": "button background", + "componentType": "action-button" +} +``` + +Common use cases: +- "button background" - for background colors +- "text color" - for text/foreground colors +- "border" - for border colors and styles +- "spacing" - for padding and margins +- "error state" - for error/negative states +- "hover state" - for interactive states + +### Step 4: Get Design Recommendations + +For semantic decisions, use `get-design-recommendations`: + +```json +{ + "intent": "primary", + "state": "default", + "context": "button" +} +``` + +Common intents: "primary", "secondary", "accent", "negative", "positive", "notice", "informative" +Common states: "default", "hover", "focus", "active", "disabled", "selected" + +### Step 5: Validate Component Configuration + +Before finalizing, use `validate-component-props` to ensure correctness: + +```json +{ + "component": "action-button", + "props": { + "variant": "accent", + "size": "m", + "isDisabled": false + } +} +``` + +## Example: Building an Action Button + +### User Request +"Create a primary action button with medium size" + +### Agent Workflow + +1. **Get schema**: `get-component-schema` with `{"component": "action-button"}` + - Discover available props: variant, size, isDisabled, etc. + +2. **Get tokens**: `get-component-tokens` with `{"componentName": "action-button"}` + - Find all button-related tokens + +3. **Find background token**: `find-tokens-by-use-case` with `{"useCase": "button background", "componentType": "action-button"}` + - Get recommended background colors + +4. **Get recommendations**: `get-design-recommendations` with `{"intent": "primary", "context": "button"}` + - Get semantic color recommendations + +5. **Build config**: Combine schema props with token recommendations: + ```json + { + "variant": "accent", + "size": "m", + "style": { + "backgroundColor": "accent-color-100", + "color": "text-color-primary" + } + } + ``` + +6. **Validate**: `validate-component-props` to ensure correctness + +## Example: Building a Text Field + +### User Request +"Create a text input with error state" + +### Agent Workflow + +1. **Get schema**: `get-component-schema` with `{"component": "text-field"}` + - Discover validationError, isRequired, etc. + +2. **Get tokens**: `get-component-tokens` with `{"componentName": "text-field"}` + +3. **Find error tokens**: `find-tokens-by-use-case` with `{"useCase": "error state", "componentType": "input"}` + - Get negative/error color tokens + +4. **Get error recommendations**: `get-design-recommendations` with `{"intent": "negative", "context": "input"}` + - Get semantic error colors + +5. **Build config**: + ```json + { + "validationState": "invalid", + "errorMessage": "Please enter a valid value", + "style": { + "borderColor": "negative-border-color", + "textColor": "negative-color-100" + } + } + ``` + +## Best Practices + +1. **Always validate**: Use `validate-component-props` before finalizing any component configuration +2. **Use semantic tokens**: Prefer `get-design-recommendations` for semantic decisions (primary, error, etc.) +3. **Check component options**: Use `get-component-options` for a user-friendly view of available props +4. **Combine multiple tools**: Don't rely on a single tool - combine schema + tokens + recommendations +5. **Handle states**: For interactive components, consider all states (default, hover, focus, disabled) + +## Related Tools + +- `get-component-schema` - Get complete component API +- `get-component-tokens` - Find component-specific tokens +- `find-tokens-by-use-case` - Find tokens for specific use cases +- `get-design-recommendations` - Get semantic token recommendations +- `validate-component-props` - Validate component configuration +- `get-component-options` - User-friendly property discovery +- `search-components-by-feature` - Find components with specific features + +## Common Components + +- **Actions**: `action-button`, `button`, `action-group`, `action-bar` +- **Inputs**: `text-field`, `text-area`, `checkbox`, `radio-group`, `select-box` +- **Containers**: `card`, `popover`, `tray`, `dialog`, `alert-dialog` +- **Navigation**: `breadcrumbs`, `tabs`, `menu`, `side-navigation` +- **Feedback**: `alert-banner`, `toast`, `in-line-alert`, `status-light` + +## Notes + +- Always check if a component exists using `list-components` before building +- Use `get-component-options` with `detailed: true` for comprehensive property information +- For complex components, break down into smaller parts (container, content, actions) +- Consider accessibility: check for required ARIA props in the schema +- Follow Spectrum design patterns: use recommended tokens, not arbitrary values diff --git a/tools/spectrum-design-data-mcp/agent-skills/token-finder.md b/tools/spectrum-design-data-mcp/agent-skills/token-finder.md new file mode 100644 index 00000000..e2fe917b --- /dev/null +++ b/tools/spectrum-design-data-mcp/agent-skills/token-finder.md @@ -0,0 +1,280 @@ +# Token Finder Agent Skill + +## Overview + +This Agent Skill helps AI agents discover the right Spectrum design tokens for design decisions, component styling, and visual design tasks. It orchestrates token discovery tools to find appropriate tokens based on use cases, design intent, and component context. + +## When to Use + +Activate this skill when: +- User asks about colors, spacing, typography, or other design tokens +- User needs to find tokens for a specific design decision +- User wants recommendations for styling components +- User asks "what token should I use for..." +- User needs help with design system values + +## Workflow + +### Step 1: Understand the Design Intent + +Determine the semantic intent: +- **Intent**: primary, secondary, accent, negative, positive, notice, informative +- **State**: default, hover, focus, active, disabled, selected +- **Context**: button, input, text, background, border, icon + +### Step 2: Get Design Recommendations + +Use `get-design-recommendations` for semantic decisions: + +```json +{ + "intent": "primary", + "state": "hover", + "context": "button" +} +``` + +This returns high-confidence token recommendations organized by: +- Colors (semantic and component) +- Layout (spacing, sizing) +- Typography (if text context) + +### Step 3: Find Tokens by Use Case + +For specific use cases, use `find-tokens-by-use-case`: + +```json +{ + "useCase": "button background", + "componentType": "button" +} +``` + +Common use cases: +- **Colors**: "button background", "text color", "border color", "icon color" +- **Spacing**: "spacing", "padding", "margin", "gap" +- **Typography**: "font", "heading", "body text", "label" +- **States**: "error state", "hover state", "disabled state", "selected state" +- **Components**: "button", "input", "card", "modal" + +### Step 4: Get Token Details + +Once you've identified candidate tokens, use `get-token-details` for complete information: + +```json +{ + "tokenPath": "accent-color-100", + "category": "semantic-color-palette" +} +``` + +This returns: +- Token value +- Description +- Deprecation status +- Related tokens +- Usage information + +### Step 5: Explore Component Tokens + +For component-specific styling, use `get-component-tokens`: + +```json +{ + "componentName": "button" +} +``` + +This returns all tokens related to a specific component, organized by category. + +## Example: Finding Button Colors + +### User Request +"What colors should I use for a primary button?" + +### Agent Workflow + +1. **Get recommendations**: `get-design-recommendations` with `{"intent": "primary", "context": "button"}` + - Returns: `accent-color-100`, `accent-color-200`, etc. + +2. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "button background", "componentType": "button"}` + - Returns component-specific background tokens + +3. **Get details**: `get-token-details` for each recommended token + - Verify values and check for deprecation + +4. **Combine results**: Present both semantic and component-specific options + +### Result +```json +{ + "recommended": { + "default": "accent-color-100", + "hover": "accent-color-200", + "pressed": "accent-color-300" + }, + "textColor": "text-color-primary", + "borderColor": "accent-border-color" +} +``` + +## Example: Finding Spacing Tokens + +### User Request +"What spacing should I use between form fields?" + +### Agent Workflow + +1. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "spacing", "componentType": "input"}` + - Returns layout and spacing tokens + +2. **Get component tokens**: `get-component-tokens` with `{"componentName": "text-field"}` + - Find field-specific spacing tokens + +3. **Get recommendations**: `get-design-recommendations` with `{"context": "spacing"}` + - Get semantic spacing recommendations + +### Result +```json +{ + "recommended": { + "fieldSpacing": "spacing-300", + "fieldPadding": "spacing-200", + "labelSpacing": "spacing-100" + } +} +``` + +## Example: Finding Error State Tokens + +### User Request +"What tokens should I use for error messaging?" + +### Agent Workflow + +1. **Get recommendations**: `get-design-recommendations` with `{"intent": "negative", "context": "text"}` + - Returns semantic negative/error colors + +2. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "error state"}` + - Returns error-specific tokens + +3. **Get details**: `get-token-details` for key tokens + - Verify error color values + +### Result +```json +{ + "recommended": { + "textColor": "negative-color-100", + "backgroundColor": "negative-background-color-default", + "borderColor": "negative-border-color", + "iconColor": "negative-color-100" + } +} +``` + +## Decision Trees + +### Color Selection + +``` +Is it semantic? (primary, error, success, etc.) + → Use get-design-recommendations + → Intent: primary/secondary/negative/positive/notice + → Context: button/input/text/background/border + +Is it component-specific? + → Use get-component-tokens + → Then find-tokens-by-use-case with componentType + +Is it a specific use case? + → Use find-tokens-by-use-case + → UseCase: button background, text color, border, etc. +``` + +### Spacing Selection + +``` +Is it component-specific? + → Use get-component-tokens + → Look for layout-component tokens + +Is it a general spacing need? + → Use find-tokens-by-use-case + → UseCase: spacing, padding, margin + +Is it for a specific component part? + → Use get-component-tokens + → Then get-token-details for specific tokens +``` + +### Typography Selection + +``` +Is it for headings? + → Use find-tokens-by-use-case + → UseCase: heading, font + +Is it for body text? + → Use find-tokens-by-use-case + → UseCase: body text, font + +Is it component-specific? + → Use get-component-tokens + → Look for typography tokens +``` + +## Best Practices + +1. **Start with semantics**: Use `get-design-recommendations` for semantic decisions (primary, error, etc.) +2. **Narrow with use cases**: Use `find-tokens-by-use-case` to narrow down options +3. **Verify details**: Always use `get-token-details` to check values and deprecation +4. **Consider states**: For interactive elements, get tokens for all states (default, hover, focus, disabled) +5. **Check component context**: Use `get-component-tokens` when styling specific components +6. **Avoid deprecated tokens**: Check `deprecated` flag in token details +7. **Use aliases**: Check for `renamed` property if a token is deprecated + +## Token Categories + +- **Color**: `color-palette`, `color-component`, `semantic-color-palette`, `color-aliases` +- **Layout**: `layout`, `layout-component` +- **Typography**: `typography` +- **Icons**: `icons` + +## Related Tools + +- `get-design-recommendations` - Semantic token recommendations +- `find-tokens-by-use-case` - Find tokens for specific use cases +- `get-component-tokens` - Get component-specific tokens +- `get-token-details` - Get detailed token information +- `query-tokens` - Search tokens by name/type/category +- `get-token-categories` - List all token categories + +## Common Use Cases + +### Colors +- "button background" → Background colors for buttons +- "text color" → Text/foreground colors +- "border color" → Border colors +- "error state" → Error/negative colors +- "hover state" → Hover state colors + +### Spacing +- "spacing" → General spacing tokens +- "padding" → Padding tokens +- "margin" → Margin tokens +- "gap" → Gap tokens for flex/grid + +### Typography +- "heading" → Heading font tokens +- "body text" → Body text tokens +- "label" → Label tokens +- "font" → Font family tokens + +## Notes + +- Always prefer semantic tokens (`semantic-color-palette`) over raw palette tokens +- Check for `private: true` - these are internal tokens not for public use +- Use `renamed` property to find replacement tokens for deprecated ones +- Token values may be references to other tokens (aliases) +- Some tokens are component-specific and should only be used with those components diff --git a/tools/spectrum-design-data-mcp/src/index.js b/tools/spectrum-design-data-mcp/src/index.js index 3c7aba1c..5563086d 100644 --- a/tools/spectrum-design-data-mcp/src/index.js +++ b/tools/spectrum-design-data-mcp/src/index.js @@ -19,6 +19,7 @@ import { import { createTokenTools } from "./tools/tokens.js"; import { createSchemaTools } from "./tools/schemas.js"; +import { createWorkflowTools } from "./tools/workflows.js"; /** * Create and configure the Spectrum Design Data MCP server @@ -28,7 +29,7 @@ export function createMCPServer() { const server = new Server( { name: "spectrum-design-data", - version: "0.1.0", + version: "0.2.0", }, { capabilities: { @@ -38,7 +39,11 @@ export function createMCPServer() { ); // Combine all available tools - const allTools = [...createTokenTools(), ...createSchemaTools()]; + const allTools = [ + ...createTokenTools(), + ...createSchemaTools(), + ...createWorkflowTools(), + ]; // Register list_tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { @@ -91,4 +96,4 @@ export async function startServer() { } // Export for testing -export { createTokenTools, createSchemaTools }; +export { createTokenTools, createSchemaTools, createWorkflowTools }; diff --git a/tools/spectrum-design-data-mcp/src/tools/workflows.js b/tools/spectrum-design-data-mcp/src/tools/workflows.js new file mode 100644 index 00000000..1a75fc15 --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/tools/workflows.js @@ -0,0 +1,431 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { getTokenData } from "../data/tokens.js"; +import { getSchemaData } from "../data/schemas.js"; + +/** + * Create workflow-oriented MCP tools that orchestrate multiple operations + * @returns {Array} Array of workflow tools + */ +export function createWorkflowTools() { + return [ + { + name: "build-component-config", + description: + "Generate a complete component configuration with recommended tokens and props. This tool orchestrates multiple MCP tools to provide a ready-to-use component configuration.", + inputSchema: { + type: "object", + properties: { + component: { + type: "string", + description: + 'Component name (e.g., "action-button", "text-field", "card")', + required: true, + }, + variant: { + type: "string", + description: + 'Component variant (e.g., "accent", "primary", "secondary")', + }, + intent: { + type: "string", + description: + 'Design intent (e.g., "primary", "secondary", "accent", "negative", "positive")', + }, + useCase: { + type: "string", + description: + 'Use case description (e.g., "primary action button", "error input field")', + }, + includeTokens: { + type: "boolean", + description: + "Include recommended design tokens in the configuration (default: true)", + default: true, + }, + }, + required: ["component"], + }, + handler: async (args) => { + const { + component, + variant, + intent, + useCase, + includeTokens = true, + } = args; + + const schemaData = await getSchemaData(); + const tokenData = await getTokenData(); + + // Get component schema + const fileName = `${component}.json`; + const schema = schemaData.components[fileName]; + + if (!schema) { + throw new Error( + `Component not found: ${component}. Use list-components to see available components.`, + ); + } + + const config = { + component, + schema: { + title: schema.title, + description: schema.description, + properties: {}, + }, + recommendedProps: {}, + tokens: includeTokens ? {} : undefined, + validation: {}, + }; + + // Build recommended props based on schema + if (schema.properties) { + Object.entries(schema.properties).forEach(([propName, propDef]) => { + config.schema.properties[propName] = { + type: propDef.type, + description: propDef.description, + required: schema.required?.includes(propName) || false, + }; + + // Set default values if available + if (propDef.default !== undefined) { + config.recommendedProps[propName] = propDef.default; + } + + // Apply variant if it's a variant property + if (propName === "variant" && variant) { + if (propDef.enum && propDef.enum.includes(variant)) { + config.recommendedProps[propName] = variant; + } + } + }); + } + + // Get component tokens if requested + if (includeTokens) { + const componentTokens = []; + const componentLower = component.toLowerCase(); + + // Search for component-specific tokens + Object.entries(tokenData).forEach(([category, tokens]) => { + if (!tokens) return; + + Object.entries(tokens).forEach(([name, token]) => { + if (name.toLowerCase().includes(componentLower)) { + componentTokens.push({ + name, + category, + value: token.value, + description: token.description, + }); + } + }); + }); + + // Get design recommendations if intent is provided + if (intent) { + const semanticColors = tokenData["semantic-color-palette.json"] || {}; + const recommendations = []; + + Object.entries(semanticColors).forEach(([name, token]) => { + const nameLower = name.toLowerCase(); + const intentLower = intent.toLowerCase(); + + if ( + nameLower.includes(intentLower) || + (intentLower === "error" && nameLower.includes("negative")) || + (intentLower === "success" && nameLower.includes("positive")) || + (intentLower === "warning" && nameLower.includes("notice")) + ) { + recommendations.push({ + name, + value: token.value, + category: "semantic-color-palette", + type: "semantic", + }); + } + }); + + config.tokens.colors = recommendations.slice(0, 5); + } + + // Find tokens by use case if provided + if (useCase) { + const useCaseLower = useCase.toLowerCase(); + const useCaseTokens = []; + + Object.entries(tokenData).forEach(([category, tokens]) => { + if (!tokens) return; + + Object.entries(tokens).forEach(([name, token]) => { + const nameMatch = + name.toLowerCase().includes(useCaseLower) || + (token.description && + token.description.toLowerCase().includes(useCaseLower)); + + if (nameMatch && !token.private) { + useCaseTokens.push({ + name, + category, + value: token.value, + description: token.description, + }); + } + }); + }); + + if (useCaseTokens.length > 0) { + config.tokens.useCaseTokens = useCaseTokens.slice(0, 10); + } + } + + // Group component tokens by category + if (componentTokens.length > 0) { + config.tokens.componentTokens = componentTokens.reduce( + (acc, token) => { + if (!acc[token.category]) acc[token.category] = []; + acc[token.category].push(token); + return acc; + }, + {}, + ); + } + } + + // Basic validation of recommended props + const validationErrors = []; + const required = schema.required || []; + for (const requiredProp of required) { + if (!(requiredProp in config.recommendedProps)) { + validationErrors.push(`Missing required property: ${requiredProp}`); + } + } + + config.validation = { + valid: validationErrors.length === 0, + errors: validationErrors, + warnings: [], + }; + + return config; + }, + }, + { + name: "suggest-component-improvements", + description: + "Analyze an existing component configuration and suggest improvements including token recommendations, validation fixes, and best practices.", + inputSchema: { + type: "object", + properties: { + component: { + type: "string", + description: "Component name to analyze", + required: true, + }, + props: { + type: "object", + description: "Current component properties to analyze", + required: true, + }, + includeTokenSuggestions: { + type: "boolean", + description: + "Include token recommendations for styling (default: true)", + default: true, + }, + }, + required: ["component", "props"], + }, + handler: async (args) => { + const { component, props, includeTokenSuggestions = true } = args; + + const schemaData = await getSchemaData(); + const tokenData = await getTokenData(); + + // Get component schema + const fileName = `${component}.json`; + const schema = schemaData.components[fileName]; + + if (!schema) { + throw new Error( + `Component not found: ${component}. Use list-components to see available components.`, + ); + } + + const suggestions = { + component, + currentProps: props, + validation: { + valid: true, + errors: [], + warnings: [], + }, + improvements: [], + tokenRecommendations: includeTokenSuggestions ? {} : undefined, + bestPractices: [], + }; + + // Validate props against schema + const schemaProps = schema.properties || {}; + const required = schema.required || []; + + // Check required properties + for (const requiredProp of required) { + if (!(requiredProp in props)) { + suggestions.validation.valid = false; + suggestions.validation.errors.push( + `Missing required property: ${requiredProp}`, + ); + suggestions.improvements.push({ + type: "missing_required", + property: requiredProp, + message: `Add required property: ${requiredProp}`, + suggestion: schemaProps[requiredProp]?.default || "See schema", + }); + } + } + + // Check for unknown properties + for (const propName of Object.keys(props)) { + if (!schemaProps[propName]) { + suggestions.validation.warnings.push( + `Unknown property: ${propName}`, + ); + suggestions.improvements.push({ + type: "unknown_property", + property: propName, + message: `Property "${propName}" is not defined in the schema`, + suggestion: "Remove or check spelling", + }); + } + } + + // Check property types + for (const [propName, propValue] of Object.entries(props)) { + const propSchema = schemaProps[propName]; + if (!propSchema) continue; + + if (propSchema.type) { + const expectedType = propSchema.type; + const actualType = Array.isArray(propValue) + ? "array" + : typeof propValue; + + if (expectedType !== actualType) { + suggestions.validation.valid = false; + suggestions.validation.errors.push( + `Property ${propName} should be ${expectedType}, got ${actualType}`, + ); + suggestions.improvements.push({ + type: "type_mismatch", + property: propName, + message: `Type mismatch: expected ${expectedType}, got ${actualType}`, + suggestion: `Change to ${expectedType}`, + }); + } + } + + // Check enum values + if (propSchema.enum && !propSchema.enum.includes(propValue)) { + suggestions.validation.warnings.push( + `Property ${propName} value "${propValue}" is not in allowed enum values`, + ); + suggestions.improvements.push({ + type: "invalid_enum", + property: propName, + message: `Invalid value: "${propValue}"`, + suggestion: `Use one of: ${propSchema.enum.join(", ")}`, + }); + } + } + + // Get token recommendations if requested + if (includeTokenSuggestions) { + const componentTokens = []; + const componentLower = component.toLowerCase(); + + // Find component-specific tokens + Object.entries(tokenData).forEach(([category, tokens]) => { + if (!tokens) return; + + Object.entries(tokens).forEach(([name, token]) => { + if ( + name.toLowerCase().includes(componentLower) && + !token.private && + !token.deprecated + ) { + componentTokens.push({ + name, + category, + value: token.value, + description: token.description, + }); + } + }); + }); + + // Group by category + if (componentTokens.length > 0) { + suggestions.tokenRecommendations.componentTokens = + componentTokens.reduce((acc, token) => { + if (!acc[token.category]) acc[token.category] = []; + acc[token.category].push(token); + return acc; + }, {}); + } + + // Suggest semantic tokens based on variant/intent + if (props.variant) { + const variantLower = props.variant.toLowerCase(); + const semanticColors = tokenData["semantic-color-palette.json"] || + {}; + const semanticTokens = []; + + Object.entries(semanticColors).forEach(([name, token]) => { + const nameLower = name.toLowerCase(); + if ( + nameLower.includes(variantLower) || + (variantLower === "accent" && nameLower.includes("accent")) || + (variantLower === "negative" && nameLower.includes("negative")) + ) { + semanticTokens.push({ + name, + value: token.value, + category: "semantic-color-palette", + type: "semantic", + }); + } + }); + + if (semanticTokens.length > 0) { + suggestions.tokenRecommendations.semanticColors = + semanticTokens.slice(0, 5); + } + } + } + + // Add best practices + suggestions.bestPractices.push( + "Use semantic tokens (semantic-color-palette) over raw palette tokens", + "Check for deprecated tokens and use renamed alternatives", + "Validate all props against the component schema", + "Use get-component-options for a user-friendly view of available props", + ); + + return suggestions; + }, + }, + ]; +} diff --git a/tools/spectrum-design-data-mcp/test/tools/workflows.test.js b/tools/spectrum-design-data-mcp/test/tools/workflows.test.js new file mode 100644 index 00000000..77a6057b --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/tools/workflows.test.js @@ -0,0 +1,260 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { createWorkflowTools } from "../../src/tools/workflows.js"; + +test("createWorkflowTools returns array of tools", (t) => { + const tools = createWorkflowTools(); + t.true(Array.isArray(tools)); + t.true(tools.length > 0); +}); + +test("workflow tools have required properties", (t) => { + const tools = createWorkflowTools(); + + for (const tool of tools) { + t.is(typeof tool.name, "string"); + t.is(typeof tool.description, "string"); + t.is(typeof tool.inputSchema, "object"); + t.is(typeof tool.handler, "function"); + } +}); + +test("build-component-config tool exists", (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + t.truthy(buildTool); + t.is(buildTool.name, "build-component-config"); + t.true(buildTool.description.includes("component configuration")); + t.true(buildTool.inputSchema.required.includes("component")); +}); + +test("suggest-component-improvements tool exists", (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + t.truthy(suggestTool); + t.is(suggestTool.name, "suggest-component-improvements"); + t.true(suggestTool.description.includes("improvements")); + t.true(suggestTool.inputSchema.required.includes("component")); + t.true(suggestTool.inputSchema.required.includes("props")); +}); + +test("build-component-config handles valid component", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const result = await buildTool.handler({ + component: "action-button", + includeTokens: false, + }); + + t.truthy(result); + t.is(result.component, "action-button"); + t.truthy(result.schema); + t.truthy(result.recommendedProps); + t.truthy(result.validation); +}); + +test("build-component-config throws error for invalid component", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const error = await t.throwsAsync(async () => { + await buildTool.handler({ + component: "non-existent-component", + }); + }); + + t.true(error.message.includes("not found")); +}); + +test("build-component-config includes tokens when requested", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const result = await buildTool.handler({ + component: "action-button", + includeTokens: true, + }); + + t.truthy(result); + t.truthy(result.tokens); +}); + +test("build-component-config excludes tokens when not requested", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const result = await buildTool.handler({ + component: "action-button", + includeTokens: false, + }); + + t.truthy(result); + t.is(result.tokens, undefined); +}); + +test("build-component-config applies variant when provided", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const result = await buildTool.handler({ + component: "action-button", + variant: "accent", + includeTokens: false, + }); + + t.truthy(result); + // Variant should be applied if it's a valid enum value + if (result.recommendedProps.variant) { + t.is(result.recommendedProps.variant, "accent"); + } +}); + +test("suggest-component-improvements handles valid component", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const result = await suggestTool.handler({ + component: "action-button", + props: { + variant: "accent", + }, + includeTokenSuggestions: false, + }); + + t.truthy(result); + t.is(result.component, "action-button"); + t.deepEqual(result.currentProps, { variant: "accent" }); + t.truthy(result.validation); + t.truthy(result.improvements); + t.truthy(result.bestPractices); +}); + +test("suggest-component-improvements throws error for invalid component", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const error = await t.throwsAsync(async () => { + await suggestTool.handler({ + component: "non-existent-component", + props: {}, + }); + }); + + t.true(error.message.includes("not found")); +}); + +test("suggest-component-improvements includes token suggestions when requested", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const result = await suggestTool.handler({ + component: "action-button", + props: { + variant: "accent", + }, + includeTokenSuggestions: true, + }); + + t.truthy(result); + t.truthy(result.tokenRecommendations); +}); + +test("suggest-component-improvements excludes token suggestions when not requested", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const result = await suggestTool.handler({ + component: "action-button", + props: { + variant: "accent", + }, + includeTokenSuggestions: false, + }); + + t.truthy(result); + t.is(result.tokenRecommendations, undefined); +}); + +test("suggest-component-improvements detects missing required props", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const result = await suggestTool.handler({ + component: "action-button", + props: {}, + includeTokenSuggestions: false, + }); + + t.truthy(result); + // If the component has required props, validation should catch missing ones + if (result.validation.errors.length > 0) { + t.true( + result.validation.errors.some((error) => + error.includes("Missing required"), + ), + ); + } +}); + +test("suggest-component-improvements detects unknown properties", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const result = await suggestTool.handler({ + component: "action-button", + props: { + unknownProperty: "value", + }, + includeTokenSuggestions: false, + }); + + t.truthy(result); + // Should detect unknown properties + if (result.validation.warnings.length > 0) { + t.true( + result.validation.warnings.some((warning) => + warning.includes("Unknown property"), + ), + ); + } +}); From 9f3dcff85175b5566146a1aa3e2140cc54d3aa9b Mon Sep 17 00:00:00 2001 From: garthdb Date: Tue, 10 Feb 2026 09:55:30 -0700 Subject: [PATCH 02/15] fix(changeset): fix line length in changeset description --- .changeset/agent-skills-feature.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/agent-skills-feature.md b/.changeset/agent-skills-feature.md index 946b88f5..de3ab975 100644 --- a/.changeset/agent-skills-feature.md +++ b/.changeset/agent-skills-feature.md @@ -2,4 +2,7 @@ "@adobe/spectrum-design-data-mcp": minor --- -Added Agent Skills for component building and token discovery. Agent Skills are markdown guides that help AI agents orchestrate MCP tools into complete workflows. Includes Component Builder and Token Finder skills, plus new workflow-oriented MCP tools. +Added Agent Skills for component building and token discovery. +Agent Skills are markdown guides that help AI agents orchestrate +MCP tools into complete workflows. Includes Component Builder and +Token Finder skills, plus new workflow-oriented MCP tools. From 25b4c0b9fbc4b321c35e42e6c1e8263702d6140e Mon Sep 17 00:00:00 2001 From: garthdb Date: Tue, 10 Feb 2026 10:31:39 -0700 Subject: [PATCH 03/15] docs(mcp): add SKILL.md for agent skills documentation --- .../agent-skills/SKILL.md | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tools/spectrum-design-data-mcp/agent-skills/SKILL.md diff --git a/tools/spectrum-design-data-mcp/agent-skills/SKILL.md b/tools/spectrum-design-data-mcp/agent-skills/SKILL.md new file mode 100644 index 00000000..281c05b4 --- /dev/null +++ b/tools/spectrum-design-data-mcp/agent-skills/SKILL.md @@ -0,0 +1,183 @@ +# Spectrum Design Data Agent Skills + +## Overview + +This skill package provides AI agents with the ability to build Spectrum components and discover design tokens through orchestrated workflows using the Spectrum Design Data MCP server. + +## Capabilities + +### Component Builder Skill + +**Purpose**: Helps AI agents build Spectrum components correctly by orchestrating MCP tools to discover schemas, find tokens, and validate configurations. + +**When to Use**: + +* Building, creating, or implementing Spectrum components +* Need help with component props, variants, or configuration +* Validating component usage +* Understanding component structure or API + +**Workflow**: + +1. Discover component schema using `get-component-schema` +2. Get component-specific tokens using `get-component-tokens` +3. Find tokens for specific use cases using `find-tokens-by-use-case` +4. Get design recommendations using `get-design-recommendations` +5. Validate component configuration using `validate-component-props` + +**Example**: + +``` +User: "Create a primary action button with medium size" + +Agent workflow: +1. get-component-schema({"component": "action-button"}) +2. get-component-tokens({"componentName": "action-button"}) +3. find-tokens-by-use-case({"useCase": "button background", "componentType": "action-button"}) +4. get-design-recommendations({"intent": "primary", "context": "button"}) +5. validate-component-props({"component": "action-button", "props": {...}}) +``` + +**Related Tools**: + +* `get-component-schema` +* `get-component-tokens` +* `find-tokens-by-use-case` +* `get-design-recommendations` +* `validate-component-props` +* `get-component-options` +* `search-components-by-feature` + +### Token Finder Skill + +**Purpose**: Helps AI agents discover the right Spectrum design tokens for design decisions, component styling, and visual design tasks. + +**When to Use**: + +* Finding tokens for colors, spacing, typography, or other design decisions +* Need recommendations for styling components +* Asking "what token should I use for..." +* Need help with design system values + +**Workflow**: + +1. Understand design intent (primary, error, success, etc.) +2. Get design recommendations using `get-design-recommendations` +3. Find tokens by use case using `find-tokens-by-use-case` +4. Get token details using `get-token-details` +5. Explore component tokens using `get-component-tokens` + +**Example**: + +``` +User: "What colors should I use for a primary button?" + +Agent workflow: +1. get-design-recommendations({"intent": "primary", "context": "button"}) +2. find-tokens-by-use-case({"useCase": "button background", "componentType": "button"}) +3. get-token-details({"tokenPath": "accent-color-100"}) +``` + +**Related Tools**: + +* `get-design-recommendations` +* `find-tokens-by-use-case` +* `get-component-tokens` +* `get-token-details` +* `query-tokens` +* `get-token-categories` + +## MCP Tools Available + +### Token Tools + +* `query-tokens` - Search and retrieve design tokens +* `find-tokens-by-use-case` - Find tokens for specific use cases +* `get-component-tokens` - Get component-specific tokens +* `get-design-recommendations` - Get semantic token recommendations +* `get-token-categories` - List all token categories +* `get-token-details` - Get detailed token information + +### Schema Tools + +* `query-component-schemas` - Search component API schemas +* `get-component-schema` - Get complete component schema +* `list-components` - List all available components +* `validate-component-props` - Validate component properties +* `get-type-schemas` - Get type definitions +* `get-component-options` - User-friendly property discovery +* `search-components-by-feature` - Find components by feature + +### Workflow Tools + +* `build-component-config` - Generate complete component configuration +* `suggest-component-improvements` - Analyze and suggest improvements + +## Integration + +### MCP Server Configuration + +Add to your MCP configuration (e.g., `.cursor/mcp.json`): + +```json +{ + "mcpServers": { + "spectrum-design-data": { + "command": "npx", + "args": ["@adobe/spectrum-design-data-mcp"] + } + } +} +``` + +### Using with AI Agents + +1. **Read the skill documentation**: When a user asks about a task covered by a skill, read the relevant skill file (`component-builder.md` or `token-finder.md`) +2. **Follow the workflow**: Use the step-by-step guidance provided +3. **Call MCP tools**: Execute the MCP tools as directed by the skill +4. **Combine results**: Synthesize tool outputs into a complete solution + +## Resources + +* **Component Builder Guide**: [component-builder.md](component-builder.md) +* **Token Finder Guide**: [token-finder.md](token-finder.md) +* **Agent Skills README**: [README.md](README.md) +* **MCP Server README**: [../README.md](../README.md) +* **React Spectrum AI**: + +## Examples + +### Building a Button Component + +``` +User: "Create a primary action button" + +Agent uses Component Builder skill: +1. Gets action-button schema +2. Finds button-related tokens +3. Gets primary color recommendations +4. Validates final configuration +``` + +### Finding Error State Tokens + +``` +User: "What tokens should I use for error messaging?" + +Agent uses Token Finder skill: +1. Gets negative/error color recommendations +2. Finds error state tokens +3. Gets token details for verification +``` + +## Best Practices + +1. **Always validate**: Use `validate-component-props` before finalizing component configurations +2. **Use semantic tokens**: Prefer `get-design-recommendations` for semantic decisions +3. **Check component options**: Use `get-component-options` for user-friendly property discovery +4. **Combine multiple tools**: Don't rely on a single tool - combine schema + tokens + recommendations +5. **Handle states**: For interactive components, consider all states (default, hover, focus, disabled) + +## Related Projects + +This implementation follows the pattern established by [React Spectrum's AI integration](https://react-spectrum.adobe.com/ai), which also uses MCP and Agent Skills to help AI agents work with design systems. From 7c091cbf0d6601c25e08a38565b45e4a4cf04a18 Mon Sep 17 00:00:00 2001 From: garthdb Date: Tue, 10 Feb 2026 10:36:22 -0700 Subject: [PATCH 04/15] fix(mcp): update SKILL.md to follow Agent Skills specification format --- tools/spectrum-design-data-mcp/agent-skills/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/spectrum-design-data-mcp/agent-skills/SKILL.md b/tools/spectrum-design-data-mcp/agent-skills/SKILL.md index 281c05b4..f67d4adb 100644 --- a/tools/spectrum-design-data-mcp/agent-skills/SKILL.md +++ b/tools/spectrum-design-data-mcp/agent-skills/SKILL.md @@ -1,3 +1,14 @@ +*** + +name: agent-skills +description: Build Spectrum components and discover design tokens through orchestrated workflows using the Spectrum Design Data MCP server. Use when building UI components, finding design tokens, validating component configurations, or working with Adobe Spectrum design system data. +license: Apache-2.0 +compatibility: Requires MCP client with access to [**@adobe/spectrum-design-data-mcp**](https://github.com/adobe/spectrum-design-data-mcp) server. Works with Cursor, VS Code, Claude Code, and other MCP-compatible clients. +metadata: +author: Adobe +version: "1.0" +-------------- + # Spectrum Design Data Agent Skills ## Overview From 7d8a44b9671c3dead9c96101f7e047f788588a3c Mon Sep 17 00:00:00 2001 From: Garth Braithwaite Date: Tue, 10 Feb 2026 11:04:50 -0700 Subject: [PATCH 05/15] feat(spectrum-design-data-mcp): refactor code quality, add agent skills tests, wire moon ci - Add constants, intent-mappings config, validation and token/component helpers - Refactor workflows, tokens, schemas to use shared utilities and constants - Rename agent-skills to build-spectrum-components; use --- delimiter in SKILL.md - Add integration tests for Component Builder and Token Finder workflows - Add unit tests for validation, token-helpers, component-helpers - Add moon test inputs and ci task for GitHub Actions Co-authored-by: Cursor --- tools/spectrum-design-data-mcp/README.md | 18 +- .../README.md | 82 +-- .../SKILL.md | 5 +- .../component-builder.md | 83 +-- .../token-finder.md | 130 ++--- tools/spectrum-design-data-mcp/moon.yml | 8 + .../src/config/intent-mappings.js | 43 ++ .../spectrum-design-data-mcp/src/constants.js | 32 ++ .../src/tools/schemas.js | 385 +++++++++++--- .../src/tools/tokens.js | 487 +++++++++++++++--- .../src/tools/workflows.js | 381 ++++---------- .../src/utils/component-helpers.js | 162 ++++++ .../src/utils/token-helpers.js | 193 +++++++ .../src/utils/validation.js | 69 +++ .../skills/agent-skills-integration.test.js | 317 ++++++++++++ .../test/tools/workflows.test.js | 32 ++ .../test/utils/component-helpers.test.js | 104 ++++ .../test/utils/token-helpers.test.js | 116 +++++ .../test/utils/validation.test.js | 91 ++++ 19 files changed, 2193 insertions(+), 545 deletions(-) rename tools/spectrum-design-data-mcp/{agent-skills => build-spectrum-components}/README.md (68%) rename tools/spectrum-design-data-mcp/{agent-skills => build-spectrum-components}/SKILL.md (99%) rename tools/spectrum-design-data-mcp/{agent-skills => build-spectrum-components}/component-builder.md (66%) rename tools/spectrum-design-data-mcp/{agent-skills => build-spectrum-components}/token-finder.md (66%) create mode 100644 tools/spectrum-design-data-mcp/src/config/intent-mappings.js create mode 100644 tools/spectrum-design-data-mcp/src/constants.js create mode 100644 tools/spectrum-design-data-mcp/src/utils/component-helpers.js create mode 100644 tools/spectrum-design-data-mcp/src/utils/token-helpers.js create mode 100644 tools/spectrum-design-data-mcp/src/utils/validation.js create mode 100644 tools/spectrum-design-data-mcp/test/skills/agent-skills-integration.test.js create mode 100644 tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js create mode 100644 tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js create mode 100644 tools/spectrum-design-data-mcp/test/utils/validation.test.js diff --git a/tools/spectrum-design-data-mcp/README.md b/tools/spectrum-design-data-mcp/README.md index 1668bf6b..c5bffce5 100644 --- a/tools/spectrum-design-data-mcp/README.md +++ b/tools/spectrum-design-data-mcp/README.md @@ -77,28 +77,30 @@ Agent Skills are markdown guides that help AI agents use the Spectrum Design Dat ### Available Agent Skills -* **[Component Builder](agent-skills/component-builder.md)**: Guides agents through building Spectrum components correctly by discovering schemas, finding tokens, and validating configurations -* **[Token Finder](agent-skills/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling +* **[Component Builder](build-spectrum-components/component-builder.md)**: Guides agents through building Spectrum components correctly by discovering schemas, finding tokens, and validating configurations +* **[Token Finder](build-spectrum-components/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling ### How Agent Skills Work Agent Skills are documentation files that: -- Guide AI agents through multi-step workflows -- Orchestrate existing MCP tools into complete tasks -- Provide examples and best practices -- Help agents discover the right tools for specific use cases + +* Guide AI agents through multi-step workflows +* Orchestrate existing MCP tools into complete tasks +* Provide examples and best practices +* Help agents discover the right tools for specific use cases Unlike MCP tools (which are executable functions), Agent Skills are **guidance documents** that tell agents how to combine tools to accomplish complex tasks. ### Using Agent Skills For AI agents working with Spectrum components: + 1. Read the relevant Agent Skill when a user asks about a covered task 2. Follow the step-by-step workflow provided 3. Call the MCP tools as directed by the skill 4. Combine tool outputs into a complete solution -See the [Agent Skills README](agent-skills/README.md) for more details. +See the [Agent Skills README](build-spectrum-components/README.md) for more details. ### Related Resources @@ -296,7 +298,7 @@ src/ ├── data/ # Data access layer │ ├── tokens.js # Token data access │ └── schemas.js # Schema data access -└── agent-skills/ # Agent Skills documentation +└── build-spectrum-components/ # Agent Skills documentation ├── component-builder.md ├── token-finder.md └── README.md diff --git a/tools/spectrum-design-data-mcp/agent-skills/README.md b/tools/spectrum-design-data-mcp/build-spectrum-components/README.md similarity index 68% rename from tools/spectrum-design-data-mcp/agent-skills/README.md rename to tools/spectrum-design-data-mcp/build-spectrum-components/README.md index 82580dab..ca724e6f 100644 --- a/tools/spectrum-design-data-mcp/agent-skills/README.md +++ b/tools/spectrum-design-data-mcp/build-spectrum-components/README.md @@ -7,10 +7,11 @@ Agent Skills are markdown guides that help AI agents use the Spectrum Design Dat ## What are Agent Skills? Agent Skills are documentation files that: -- Guide AI agents through multi-step workflows -- Orchestrate existing MCP tools into complete tasks -- Provide examples and best practices -- Help agents discover the right tools for specific use cases + +* Guide AI agents through multi-step workflows +* Orchestrate existing MCP tools into complete tasks +* Provide examples and best practices +* Help agents discover the right tools for specific use cases Unlike MCP tools (which are executable functions), Agent Skills are **guidance documents** that tell agents how to combine tools to accomplish complex tasks. @@ -19,20 +20,22 @@ Unlike MCP tools (which are executable functions), Agent Skills are **guidance d ### [Component Builder](component-builder.md) Helps agents build Spectrum components correctly by: -- Discovering component schemas -- Finding appropriate design tokens -- Validating component configurations -- Following Spectrum design patterns + +* Discovering component schemas +* Finding appropriate design tokens +* Validating component configurations +* Following Spectrum design patterns **Use when**: Building, creating, or implementing Spectrum components ### [Token Finder](token-finder.md) Helps agents discover the right design tokens for: -- Color decisions (semantic, component-specific) -- Spacing and layout -- Typography -- Component styling + +* Color decisions (semantic, component-specific) +* Spacing and layout +* Typography +* Component styling **Use when**: Finding tokens for design decisions or styling components @@ -62,21 +65,23 @@ The skill provides the **workflow**, while the MCP tools provide the **data**. Agent Skills work alongside the Spectrum Design Data MCP tools: ### Token Tools -- `query-tokens` - Search tokens -- `find-tokens-by-use-case` ⭐ - Find tokens for use cases -- `get-component-tokens` ⭐ - Get component-specific tokens -- `get-design-recommendations` ⭐ - Get semantic recommendations -- `get-token-categories` - List categories -- `get-token-details` - Get token details + +* `query-tokens` - Search tokens +* `find-tokens-by-use-case` ⭐ - Find tokens for use cases +* `get-component-tokens` ⭐ - Get component-specific tokens +* `get-design-recommendations` ⭐ - Get semantic recommendations +* `get-token-categories` - List categories +* `get-token-details` - Get token details ### Schema Tools -- `query-component-schemas` - Search schemas -- `get-component-schema` ⭐ - Get component API -- `list-components` - List all components -- `validate-component-props` ⭐ - Validate configurations -- `get-type-schemas` - Get type definitions -- `get-component-options` ⭐ - User-friendly property discovery -- `search-components-by-feature` ⭐ - Find components by feature + +* `query-component-schemas` - Search schemas +* `get-component-schema` ⭐ - Get component API +* `list-components` - List all components +* `validate-component-props` ⭐ - Validate configurations +* `get-type-schemas` - Get type definitions +* `get-component-options` ⭐ - User-friendly property discovery +* `search-components-by-feature` ⭐ - Find components by feature ⭐ = Frequently used in Agent Skills @@ -102,11 +107,11 @@ To create a new Agent Skill: 1. **Identify the workflow**: What multi-step task needs guidance? 2. **Map to MCP tools**: Which existing tools support this workflow? 3. **Write the guide**: Create a markdown file with: - - Overview and when to use - - Step-by-step workflow - - Examples with real use cases - - Best practices - - Related tools reference + * Overview and when to use + * Step-by-step workflow + * Examples with real use cases + * Best practices + * Related tools reference 4. **Add to this README**: Document the new skill ### Skill Template @@ -140,14 +145,15 @@ This implementation follows the pattern established by [React Spectrum's AI inte ## Contributing When adding new Agent Skills: -- Follow the existing markdown format -- Include clear examples -- Reference specific MCP tools -- Test workflows with real scenarios -- Update this README + +* Follow the existing markdown format +* Include clear examples +* Reference specific MCP tools +* Test workflows with real scenarios +* Update this README ## Resources -- [Spectrum Design Data MCP README](../README.md) -- [React Spectrum AI](https://react-spectrum.adobe.com/ai) -- [MCP Specification](https://modelcontextprotocol.io) +* [Spectrum Design Data MCP README](../README.md) +* [React Spectrum AI](https://react-spectrum.adobe.com/ai) +* [MCP Specification](https://modelcontextprotocol.io) diff --git a/tools/spectrum-design-data-mcp/agent-skills/SKILL.md b/tools/spectrum-design-data-mcp/build-spectrum-components/SKILL.md similarity index 99% rename from tools/spectrum-design-data-mcp/agent-skills/SKILL.md rename to tools/spectrum-design-data-mcp/build-spectrum-components/SKILL.md index f67d4adb..37111774 100644 --- a/tools/spectrum-design-data-mcp/agent-skills/SKILL.md +++ b/tools/spectrum-design-data-mcp/build-spectrum-components/SKILL.md @@ -1,13 +1,14 @@ *** -name: agent-skills +name: build-spectrum-components description: Build Spectrum components and discover design tokens through orchestrated workflows using the Spectrum Design Data MCP server. Use when building UI components, finding design tokens, validating component configurations, or working with Adobe Spectrum design system data. license: Apache-2.0 compatibility: Requires MCP client with access to [**@adobe/spectrum-design-data-mcp**](https://github.com/adobe/spectrum-design-data-mcp) server. Works with Cursor, VS Code, Claude Code, and other MCP-compatible clients. metadata: author: Adobe version: "1.0" --------------- + +*** # Spectrum Design Data Agent Skills diff --git a/tools/spectrum-design-data-mcp/agent-skills/component-builder.md b/tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md similarity index 66% rename from tools/spectrum-design-data-mcp/agent-skills/component-builder.md rename to tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md index 3919021c..45efd54e 100644 --- a/tools/spectrum-design-data-mcp/agent-skills/component-builder.md +++ b/tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md @@ -7,10 +7,11 @@ This Agent Skill helps AI agents build Spectrum components correctly by orchestr ## When to Use Activate this skill when: -- User asks to build, create, or implement a Spectrum component -- User needs help with component props, variants, or configuration -- User wants to validate component usage -- User asks about component structure or API + +* User asks to build, create, or implement a Spectrum component +* User needs help with component props, variants, or configuration +* User wants to validate component usage +* User asks about component structure or API ## Workflow @@ -25,11 +26,12 @@ Use `get-component-schema` to understand the component's API: ``` This returns: -- Available properties -- Required properties -- Property types and enums -- Default values -- Component description + +* Available properties +* Required properties +* Property types and enums +* Default values +* Component description ### Step 2: Get Component-Specific Tokens @@ -55,12 +57,13 @@ For each visual aspect (background, text, border, spacing), use `find-tokens-by- ``` Common use cases: -- "button background" - for background colors -- "text color" - for text/foreground colors -- "border" - for border colors and styles -- "spacing" - for padding and margins -- "error state" - for error/negative states -- "hover state" - for interactive states + +* "button background" - for background colors +* "text color" - for text/foreground colors +* "border" - for border colors and styles +* "spacing" - for padding and margins +* "error state" - for error/negative states +* "hover state" - for interactive states ### Step 4: Get Design Recommendations @@ -95,21 +98,22 @@ Before finalizing, use `validate-component-props` to ensure correctness: ## Example: Building an Action Button ### User Request + "Create a primary action button with medium size" ### Agent Workflow 1. **Get schema**: `get-component-schema` with `{"component": "action-button"}` - - Discover available props: variant, size, isDisabled, etc. + * Discover available props: variant, size, isDisabled, etc. 2. **Get tokens**: `get-component-tokens` with `{"componentName": "action-button"}` - - Find all button-related tokens + * Find all button-related tokens 3. **Find background token**: `find-tokens-by-use-case` with `{"useCase": "button background", "componentType": "action-button"}` - - Get recommended background colors + * Get recommended background colors 4. **Get recommendations**: `get-design-recommendations` with `{"intent": "primary", "context": "button"}` - - Get semantic color recommendations + * Get semantic color recommendations 5. **Build config**: Combine schema props with token recommendations: ```json @@ -128,20 +132,21 @@ Before finalizing, use `validate-component-props` to ensure correctness: ## Example: Building a Text Field ### User Request + "Create a text input with error state" ### Agent Workflow 1. **Get schema**: `get-component-schema` with `{"component": "text-field"}` - - Discover validationError, isRequired, etc. + * Discover validationError, isRequired, etc. 2. **Get tokens**: `get-component-tokens` with `{"componentName": "text-field"}` 3. **Find error tokens**: `find-tokens-by-use-case` with `{"useCase": "error state", "componentType": "input"}` - - Get negative/error color tokens + * Get negative/error color tokens 4. **Get error recommendations**: `get-design-recommendations` with `{"intent": "negative", "context": "input"}` - - Get semantic error colors + * Get semantic error colors 5. **Build config**: ```json @@ -165,26 +170,26 @@ Before finalizing, use `validate-component-props` to ensure correctness: ## Related Tools -- `get-component-schema` - Get complete component API -- `get-component-tokens` - Find component-specific tokens -- `find-tokens-by-use-case` - Find tokens for specific use cases -- `get-design-recommendations` - Get semantic token recommendations -- `validate-component-props` - Validate component configuration -- `get-component-options` - User-friendly property discovery -- `search-components-by-feature` - Find components with specific features +* `get-component-schema` - Get complete component API +* `get-component-tokens` - Find component-specific tokens +* `find-tokens-by-use-case` - Find tokens for specific use cases +* `get-design-recommendations` - Get semantic token recommendations +* `validate-component-props` - Validate component configuration +* `get-component-options` - User-friendly property discovery +* `search-components-by-feature` - Find components with specific features ## Common Components -- **Actions**: `action-button`, `button`, `action-group`, `action-bar` -- **Inputs**: `text-field`, `text-area`, `checkbox`, `radio-group`, `select-box` -- **Containers**: `card`, `popover`, `tray`, `dialog`, `alert-dialog` -- **Navigation**: `breadcrumbs`, `tabs`, `menu`, `side-navigation` -- **Feedback**: `alert-banner`, `toast`, `in-line-alert`, `status-light` +* **Actions**: `action-button`, `button`, `action-group`, `action-bar` +* **Inputs**: `text-field`, `text-area`, `checkbox`, `radio-group`, `select-box` +* **Containers**: `card`, `popover`, `tray`, `dialog`, `alert-dialog` +* **Navigation**: `breadcrumbs`, `tabs`, `menu`, `side-navigation` +* **Feedback**: `alert-banner`, `toast`, `in-line-alert`, `status-light` ## Notes -- Always check if a component exists using `list-components` before building -- Use `get-component-options` with `detailed: true` for comprehensive property information -- For complex components, break down into smaller parts (container, content, actions) -- Consider accessibility: check for required ARIA props in the schema -- Follow Spectrum design patterns: use recommended tokens, not arbitrary values +* Always check if a component exists using `list-components` before building +* Use `get-component-options` with `detailed: true` for comprehensive property information +* For complex components, break down into smaller parts (container, content, actions) +* Consider accessibility: check for required ARIA props in the schema +* Follow Spectrum design patterns: use recommended tokens, not arbitrary values diff --git a/tools/spectrum-design-data-mcp/agent-skills/token-finder.md b/tools/spectrum-design-data-mcp/build-spectrum-components/token-finder.md similarity index 66% rename from tools/spectrum-design-data-mcp/agent-skills/token-finder.md rename to tools/spectrum-design-data-mcp/build-spectrum-components/token-finder.md index e2fe917b..e8c7b38f 100644 --- a/tools/spectrum-design-data-mcp/agent-skills/token-finder.md +++ b/tools/spectrum-design-data-mcp/build-spectrum-components/token-finder.md @@ -7,20 +7,22 @@ This Agent Skill helps AI agents discover the right Spectrum design tokens for d ## When to Use Activate this skill when: -- User asks about colors, spacing, typography, or other design tokens -- User needs to find tokens for a specific design decision -- User wants recommendations for styling components -- User asks "what token should I use for..." -- User needs help with design system values + +* User asks about colors, spacing, typography, or other design tokens +* User needs to find tokens for a specific design decision +* User wants recommendations for styling components +* User asks "what token should I use for..." +* User needs help with design system values ## Workflow ### Step 1: Understand the Design Intent Determine the semantic intent: -- **Intent**: primary, secondary, accent, negative, positive, notice, informative -- **State**: default, hover, focus, active, disabled, selected -- **Context**: button, input, text, background, border, icon + +* **Intent**: primary, secondary, accent, negative, positive, notice, informative +* **State**: default, hover, focus, active, disabled, selected +* **Context**: button, input, text, background, border, icon ### Step 2: Get Design Recommendations @@ -35,9 +37,10 @@ Use `get-design-recommendations` for semantic decisions: ``` This returns high-confidence token recommendations organized by: -- Colors (semantic and component) -- Layout (spacing, sizing) -- Typography (if text context) + +* Colors (semantic and component) +* Layout (spacing, sizing) +* Typography (if text context) ### Step 3: Find Tokens by Use Case @@ -51,11 +54,12 @@ For specific use cases, use `find-tokens-by-use-case`: ``` Common use cases: -- **Colors**: "button background", "text color", "border color", "icon color" -- **Spacing**: "spacing", "padding", "margin", "gap" -- **Typography**: "font", "heading", "body text", "label" -- **States**: "error state", "hover state", "disabled state", "selected state" -- **Components**: "button", "input", "card", "modal" + +* **Colors**: "button background", "text color", "border color", "icon color" +* **Spacing**: "spacing", "padding", "margin", "gap" +* **Typography**: "font", "heading", "body text", "label" +* **States**: "error state", "hover state", "disabled state", "selected state" +* **Components**: "button", "input", "card", "modal" ### Step 4: Get Token Details @@ -69,11 +73,12 @@ Once you've identified candidate tokens, use `get-token-details` for complete in ``` This returns: -- Token value -- Description -- Deprecation status -- Related tokens -- Usage information + +* Token value +* Description +* Deprecation status +* Related tokens +* Usage information ### Step 5: Explore Component Tokens @@ -90,22 +95,24 @@ This returns all tokens related to a specific component, organized by category. ## Example: Finding Button Colors ### User Request + "What colors should I use for a primary button?" ### Agent Workflow 1. **Get recommendations**: `get-design-recommendations` with `{"intent": "primary", "context": "button"}` - - Returns: `accent-color-100`, `accent-color-200`, etc. + * Returns: `accent-color-100`, `accent-color-200`, etc. 2. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "button background", "componentType": "button"}` - - Returns component-specific background tokens + * Returns component-specific background tokens 3. **Get details**: `get-token-details` for each recommended token - - Verify values and check for deprecation + * Verify values and check for deprecation 4. **Combine results**: Present both semantic and component-specific options ### Result + ```json { "recommended": { @@ -121,20 +128,22 @@ This returns all tokens related to a specific component, organized by category. ## Example: Finding Spacing Tokens ### User Request + "What spacing should I use between form fields?" ### Agent Workflow 1. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "spacing", "componentType": "input"}` - - Returns layout and spacing tokens + * Returns layout and spacing tokens 2. **Get component tokens**: `get-component-tokens` with `{"componentName": "text-field"}` - - Find field-specific spacing tokens + * Find field-specific spacing tokens 3. **Get recommendations**: `get-design-recommendations` with `{"context": "spacing"}` - - Get semantic spacing recommendations + * Get semantic spacing recommendations ### Result + ```json { "recommended": { @@ -148,20 +157,22 @@ This returns all tokens related to a specific component, organized by category. ## Example: Finding Error State Tokens ### User Request + "What tokens should I use for error messaging?" ### Agent Workflow 1. **Get recommendations**: `get-design-recommendations` with `{"intent": "negative", "context": "text"}` - - Returns semantic negative/error colors + * Returns semantic negative/error colors 2. **Find by use case**: `find-tokens-by-use-case` with `{"useCase": "error state"}` - - Returns error-specific tokens + * Returns error-specific tokens 3. **Get details**: `get-token-details` for key tokens - - Verify error color values + * Verify error color values ### Result + ```json { "recommended": { @@ -236,45 +247,48 @@ Is it component-specific? ## Token Categories -- **Color**: `color-palette`, `color-component`, `semantic-color-palette`, `color-aliases` -- **Layout**: `layout`, `layout-component` -- **Typography**: `typography` -- **Icons**: `icons` +* **Color**: `color-palette`, `color-component`, `semantic-color-palette`, `color-aliases` +* **Layout**: `layout`, `layout-component` +* **Typography**: `typography` +* **Icons**: `icons` ## Related Tools -- `get-design-recommendations` - Semantic token recommendations -- `find-tokens-by-use-case` - Find tokens for specific use cases -- `get-component-tokens` - Get component-specific tokens -- `get-token-details` - Get detailed token information -- `query-tokens` - Search tokens by name/type/category -- `get-token-categories` - List all token categories +* `get-design-recommendations` - Semantic token recommendations +* `find-tokens-by-use-case` - Find tokens for specific use cases +* `get-component-tokens` - Get component-specific tokens +* `get-token-details` - Get detailed token information +* `query-tokens` - Search tokens by name/type/category +* `get-token-categories` - List all token categories ## Common Use Cases ### Colors -- "button background" → Background colors for buttons -- "text color" → Text/foreground colors -- "border color" → Border colors -- "error state" → Error/negative colors -- "hover state" → Hover state colors + +* "button background" → Background colors for buttons +* "text color" → Text/foreground colors +* "border color" → Border colors +* "error state" → Error/negative colors +* "hover state" → Hover state colors ### Spacing -- "spacing" → General spacing tokens -- "padding" → Padding tokens -- "margin" → Margin tokens -- "gap" → Gap tokens for flex/grid + +* "spacing" → General spacing tokens +* "padding" → Padding tokens +* "margin" → Margin tokens +* "gap" → Gap tokens for flex/grid ### Typography -- "heading" → Heading font tokens -- "body text" → Body text tokens -- "label" → Label tokens -- "font" → Font family tokens + +* "heading" → Heading font tokens +* "body text" → Body text tokens +* "label" → Label tokens +* "font" → Font family tokens ## Notes -- Always prefer semantic tokens (`semantic-color-palette`) over raw palette tokens -- Check for `private: true` - these are internal tokens not for public use -- Use `renamed` property to find replacement tokens for deprecated ones -- Token values may be references to other tokens (aliases) -- Some tokens are component-specific and should only be used with those components +* Always prefer semantic tokens (`semantic-color-palette`) over raw palette tokens +* Check for `private: true` - these are internal tokens not for public use +* Use `renamed` property to find replacement tokens for deprecated ones +* Token values may be references to other tokens (aliases) +* Some tokens are component-specific and should only be used with those components diff --git a/tools/spectrum-design-data-mcp/moon.yml b/tools/spectrum-design-data-mcp/moon.yml index fce3657a..d6cbfb26 100644 --- a/tools/spectrum-design-data-mcp/moon.yml +++ b/tools/spectrum-design-data-mcp/moon.yml @@ -20,7 +20,15 @@ tasks: command: - pnpm - ava + - test platform: node + inputs: + - "src/**/*" + - "test/**/*" + - "ava.config.js" + - "package.json" + ci: + deps: [test] test-watch: command: - ava diff --git a/tools/spectrum-design-data-mcp/src/config/intent-mappings.js b/tools/spectrum-design-data-mcp/src/config/intent-mappings.js new file mode 100644 index 00000000..bbb6efd7 --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/config/intent-mappings.js @@ -0,0 +1,43 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/** Maps user-facing intent names to semantic token name substrings */ +export const INTENT_SEMANTIC_MAPPINGS = { + error: ["negative"], + success: ["positive"], + warning: ["notice"], +}; + +/** Maps use case keywords to token categories to search */ +export const USE_CASE_PATTERNS = { + background: ["color-component", "semantic-color-palette", "color-palette"], + text: ["color-component", "semantic-color-palette", "typography"], + border: ["color-component", "semantic-color-palette"], + spacing: ["layout", "layout-component"], + padding: ["layout", "layout-component"], + margin: ["layout", "layout-component"], + font: ["typography"], + icon: ["icons", "layout"], + error: ["semantic-color-palette", "color-component"], + success: ["semantic-color-palette", "color-component"], + warning: ["semantic-color-palette", "color-component"], + accent: ["semantic-color-palette", "color-component"], + button: ["color-component", "layout-component"], + input: ["color-component", "layout-component"], + card: ["color-component", "layout-component"], +}; + +/** Maps variant names to semantic token name substrings */ +export const VARIANT_MAPPINGS = { + accent: ["accent"], + negative: ["negative"], +}; diff --git a/tools/spectrum-design-data-mcp/src/constants.js b/tools/spectrum-design-data-mcp/src/constants.js new file mode 100644 index 00000000..bd46c868 --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/constants.js @@ -0,0 +1,32 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +export const RESULT_LIMITS = { + DEFAULT_TOKEN_LIMIT: 50, + DEFAULT_SCHEMA_LIMIT: 20, + MAX_COLOR_RECOMMENDATIONS: 5, + MAX_USE_CASE_TOKENS: 10, + MAX_SEMANTIC_COLORS: 5, + MAX_DESIGN_RECOMMENDATIONS: 10, + MAX_TOKENS_BY_USE_CASE: 20, +}; + +export const TOKEN_FILES = [ + "color-aliases.json", + "color-component.json", + "color-palette.json", + "icons.json", + "layout-component.json", + "layout.json", + "semantic-color-palette.json", + "typography.json", +]; diff --git a/tools/spectrum-design-data-mcp/src/tools/schemas.js b/tools/spectrum-design-data-mcp/src/tools/schemas.js index 19d370c7..59a42310 100644 --- a/tools/spectrum-design-data-mcp/src/tools/schemas.js +++ b/tools/spectrum-design-data-mcp/src/tools/schemas.js @@ -11,6 +11,13 @@ governing permissions and limitations under the License. */ import { getSchemaData } from "../data/schemas.js"; +import { RESULT_LIMITS } from "../constants.js"; +import { + validateComponentName, + validateLimit, + validatePropsObject, + validateStringParam, +} from "../utils/validation.js"; /** * Create schema-related MCP tools @@ -18,6 +25,88 @@ import { getSchemaData } from "../data/schemas.js"; */ export function createSchemaTools() { return [ + { + name: "query-component-schemas", + description: "Search and retrieve Spectrum component API schemas", + inputSchema: { + type: "object", + properties: { + component: { + type: "string", + description: + 'Component name to search for (e.g., "button", "action-button")', + }, + query: { + type: "string", + description: + "Search query to filter schemas (searches component names, descriptions)", + }, + limit: { + type: "number", + description: "Maximum number of schemas to return (default: 20)", + default: 20, + }, + }, + }, + handler: async (args) => { + const component = validateStringParam(args?.component, "component"); + const query = validateStringParam(args?.query, "query"); + const limit = validateLimit( + args?.limit, + RESULT_LIMITS.DEFAULT_SCHEMA_LIMIT, + 100, + ); + + const schemaData = await getSchemaData(); + let results = []; + + const components = + schemaData?.components != null && + typeof schemaData.components === "object" + ? schemaData.components + : {}; + + for (const [fileName, schema] of Object.entries(components)) { + if (!schema || typeof schema !== "object") continue; + + const componentName = String(fileName).replace(".json", ""); + + if ( + component != null && + component !== "" && + !componentName.toLowerCase().includes(component.toLowerCase()) + ) { + continue; + } + + if ( + query != null && + query !== "" && + !matchesSchemaQuery(componentName, schema, query) + ) { + continue; + } + + results.push({ + component: componentName, + fileName, + title: schema.title, + description: schema.description, + properties: Object.keys(schema.properties || {}), + required: schema.required || [], + schema, + }); + } + + results = results.slice(0, limit); + + return { + total: results.length, + schemas: results, + query: { component, query, limit }, + }; + }, + }, { name: "get-component-schema", description: "Get full JSON schema for one component.", @@ -33,13 +122,16 @@ export function createSchemaTools() { required: ["component"], }, handler: async (args) => { - const { component } = args; - const schemaData = await getSchemaData(); + const component = validateComponentName(args?.component); + const schemaData = await getSchemaData(); const fileName = `${component}.json`; - const schema = schemaData.components[fileName]; + const schema = + schemaData?.components != null + ? schemaData.components[fileName] + : undefined; - if (!schema) { + if (!schema || typeof schema !== "object") { throw new Error(`Component schema not found: ${component}`); } @@ -61,22 +153,33 @@ export function createSchemaTools() { inputSchema: { type: "object", properties: {} }, handler: async () => { const schemaData = await getSchemaData(); - const components = Object.keys(schemaData.components).map( - (fileName) => { - const componentName = fileName.replace(".json", ""); - const schema = schemaData.components[fileName]; - const entry = { - name: componentName, - propertyCount: Object.keys(schema.properties || {}).length, - }; - if (schema.title) entry.title = schema.title; - if (schema.description) entry.description = schema.description; - return entry; - }, - ); + const components = + schemaData?.components != null && + typeof schemaData.components === "object" + ? schemaData.components + : {}; + + const list = Object.keys(components).map((fileName) => { + const componentName = String(fileName).replace(".json", ""); + const schema = components[fileName]; + const props = schema?.properties; + const required = schema?.required; + + return { + name: componentName, + title: schema?.title, + description: schema?.description, + propertyCount: + props && typeof props === "object" + ? Object.keys(props).length + : 0, + hasRequired: Array.isArray(required) && required.length > 0, + }; + }); + return { - total: components.length, - components: components.sort((a, b) => a.name.localeCompare(b.name)), + total: list.length, + components: list.sort((a, b) => a.name.localeCompare(b.name)), }; }, }, @@ -100,17 +203,20 @@ export function createSchemaTools() { required: ["component", "props"], }, handler: async (args) => { - const { component, props } = args; - const schemaData = await getSchemaData(); + const component = validateComponentName(args?.component); + const props = validatePropsObject(args?.props); + const schemaData = await getSchemaData(); const fileName = `${component}.json`; - const schema = schemaData.components[fileName]; + const schema = + schemaData?.components != null + ? schemaData.components[fileName] + : undefined; - if (!schema) { + if (!schema || typeof schema !== "object") { throw new Error(`Component schema not found: ${component}`); } - // Basic validation logic const validationResults = validateProps(props, schema); return { @@ -122,6 +228,150 @@ export function createSchemaTools() { }; }, }, + { + name: "get-type-schemas", + description: "Get type definitions used in component schemas", + inputSchema: { + type: "object", + properties: { + type: { + type: "string", + description: 'Specific type to retrieve (e.g., "hex-color")', + }, + }, + }, + handler: async (args) => { + const type = validateStringParam(args?.type, "type"); + const schemaData = await getSchemaData(); + + if (type != null && type !== "") { + const typeSchema = + schemaData?.types != null + ? schemaData.types[`${type}.json`] + : undefined; + if (!typeSchema || typeof typeSchema !== "object") { + throw new Error(`Type schema not found: ${type}`); + } + + return { + type, + schema: typeSchema, + }; + } + + const typesData = + schemaData?.types != null && typeof schemaData.types === "object" + ? schemaData.types + : {}; + const types = Object.keys(typesData).map((fileName) => { + const typeName = String(fileName).replace(".json", ""); + const typeSchema = typesData[fileName]; + return { + name: typeName, + title: typeSchema?.title, + description: typeSchema?.description, + type: typeSchema?.type, + }; + }); + + return { + total: types.length, + types: types.sort((a, b) => a.name.localeCompare(b.name)), + }; + }, + }, + { + name: "get-component-options", + description: + "Get all available options/properties for a component in a user-friendly format - perfect for discovering what options a component supports", + inputSchema: { + type: "object", + properties: { + component: { + type: "string", + description: + 'Component name (e.g., "action-button", "text-field", "menu")', + required: true, + }, + detailed: { + type: "boolean", + description: + "Include detailed property information like enums, default values, etc.", + default: false, + }, + }, + required: ["component"], + }, + handler: async (args) => { + const component = validateComponentName(args?.component); + const detailed = args?.detailed === true; + + const schemaData = await getSchemaData(); + const fileName = `${component}.json`; + const schema = + schemaData?.components != null + ? schemaData.components[fileName] + : undefined; + + if (!schema || typeof schema !== "object") { + throw new Error( + `Component not found: ${component}. Use list-components to see available components.`, + ); + } + + const componentInfo = { + name: component, + title: schema.title ?? component, + description: schema.description, + totalProperties: 0, + properties: [], + }; + + const props = schema.properties; + if (props && typeof props === "object") { + componentInfo.totalProperties = Object.keys(props).length; + const required = schema.required || []; + + for (const [propName, propDef] of Object.entries(props)) { + if (!propDef || typeof propDef !== "object") continue; + + const propInfo = { + name: propName, + type: propDef.type ?? "object", + required: required.includes(propName), + description: propDef.description, + }; + + if (detailed) { + if (Array.isArray(propDef.enum)) { + propInfo.possibleValues = propDef.enum; + } + if (propDef.default !== undefined) { + propInfo.defaultValue = propDef.default; + } + if ( + propDef.properties && + typeof propDef.properties === "object" + ) { + propInfo.nestedProperties = Object.keys(propDef.properties); + } + if (propDef.$ref != null) { + propInfo.reference = propDef.$ref; + } + } + + componentInfo.properties.push(propInfo); + } + + componentInfo.properties.sort((a, b) => { + if (a.required !== b.required) return a.required ? -1 : 1; + return a.name.localeCompare(b.name); + }); + } + + return componentInfo; + }, + }, { name: "search-components-by-feature", description: @@ -138,31 +388,47 @@ export function createSchemaTools() { required: ["feature"], }, handler: async (args) => { - const { feature } = args; + const feature = + args?.feature != null ? String(args.feature) : undefined; + if (!feature || feature.trim() === "") { + throw new Error("feature is required"); + } + const schemaData = await getSchemaData(); const matchingComponents = []; + const components = + schemaData?.components != null && + typeof schemaData.components === "object" + ? schemaData.components + : {}; + const featureLower = feature.toLowerCase(); + + for (const [fileName, schema] of Object.entries(components)) { + if (!schema || typeof schema !== "object") continue; + + const componentName = String(fileName).replace(".json", ""); + const props = schema.properties; + + if (!props || typeof props !== "object") continue; + + const hasFeature = Object.keys(props).some((prop) => + prop.toLowerCase().includes(featureLower), + ); - Object.entries(schemaData.components).forEach(([fileName, schema]) => { - const componentName = fileName.replace(".json", ""); - if (schema.properties) { - const hasFeature = Object.keys(schema.properties).some((prop) => - prop.toLowerCase().includes(feature.toLowerCase()), + if (hasFeature) { + const matchingProps = Object.keys(props).filter((prop) => + prop.toLowerCase().includes(featureLower), ); - if (hasFeature) { - const matchingProps = Object.keys(schema.properties).filter( - (prop) => prop.toLowerCase().includes(feature.toLowerCase()), - ); - const entry = { - name: componentName, - matchingProperties: matchingProps, - totalProperties: Object.keys(schema.properties).length, - }; - if (schema.title) entry.title = schema.title; - if (schema.description) entry.description = schema.description; - matchingComponents.push(entry); - } + + matchingComponents.push({ + name: componentName, + title: schema.title ?? componentName, + description: schema.description, + matchingProperties: matchingProps, + totalProperties: Object.keys(props).length, + }); } - }); + } return { totalMatches: matchingComponents.length, @@ -183,29 +449,29 @@ export function createSchemaTools() { * @returns {boolean} Whether the schema matches */ function matchesSchemaQuery(componentName, schema, query) { - const searchText = query.toLowerCase(); + const searchText = String(query).toLowerCase(); - // Search in component name if (componentName.toLowerCase().includes(searchText)) { return true; } - // Search in title - if (schema.title && schema.title.toLowerCase().includes(searchText)) { + if ( + schema?.title != null && + String(schema.title).toLowerCase().includes(searchText) + ) { return true; } - // Search in description if ( - schema.description && - schema.description.toLowerCase().includes(searchText) + schema?.description != null && + String(schema.description).toLowerCase().includes(searchText) ) { return true; } - // Search in property names - if (schema.properties) { - for (const propName of Object.keys(schema.properties)) { + const props = schema?.properties; + if (props && typeof props === "object") { + for (const propName of Object.keys(props)) { if (propName.toLowerCase().includes(searchText)) { return true; } @@ -217,33 +483,30 @@ function matchesSchemaQuery(componentName, schema, query) { /** * Basic validation of properties against schema - * @param {Object} props - Properties to validate + * @param {Record} props - Properties to validate * @param {Object} schema - Schema to validate against * @returns {Object} Validation results */ function validateProps(props, schema) { const errors = []; const warnings = []; + const required = schema?.required || []; + const schemaProps = schema?.properties || {}; - // Check required properties - const required = schema.required || []; for (const requiredProp of required) { if (!(requiredProp in props)) { errors.push(`Missing required property: ${requiredProp}`); } } - // Check property types (basic validation) - const schemaProps = schema.properties || {}; for (const [propName, propValue] of Object.entries(props)) { const propSchema = schemaProps[propName]; - if (!propSchema) { + if (!propSchema || typeof propSchema !== "object") { warnings.push(`Unknown property: ${propName}`); continue; } - // Basic type checking if (propSchema.type) { const expectedType = propSchema.type; const actualType = Array.isArray(propValue) ? "array" : typeof propValue; diff --git a/tools/spectrum-design-data-mcp/src/tools/tokens.js b/tools/spectrum-design-data-mcp/src/tools/tokens.js index 293c6eb4..e1cbe622 100644 --- a/tools/spectrum-design-data-mcp/src/tools/tokens.js +++ b/tools/spectrum-design-data-mcp/src/tools/tokens.js @@ -11,6 +11,10 @@ governing permissions and limitations under the License. */ import { getTokenData, getFlatTokenMap } from "../data/tokens.js"; +import { RESULT_LIMITS } from "../constants.js"; +import { USE_CASE_PATTERNS } from "../config/intent-mappings.js"; +import { validateLimit, validateStringParam } from "../utils/validation.js"; +import { tokenNameMatchesIntent } from "../utils/token-helpers.js"; /** * Create token-related MCP tools @@ -39,19 +43,38 @@ export function createTokenTools() { }, }, handler: async (args) => { - const { query, category, type, limit = 50 } = args; + const query = validateStringParam(args?.query, "query"); + const category = validateStringParam(args?.category, "category"); + const type = validateStringParam(args?.type, "type"); + const limit = validateLimit( + args?.limit, + RESULT_LIMITS.DEFAULT_TOKEN_LIMIT, + 100, + ); + const tokenData = await getTokenData(); let results = []; - for (const [fileName, tokens] of Object.entries(tokenData)) { - if ( - category && - !fileName.toLowerCase().includes(category.toLowerCase()) - ) { - continue; + + if (tokenData && typeof tokenData === "object") { + for (const [fileName, tokens] of Object.entries(tokenData)) { + if ( + category && + !String(fileName).toLowerCase().includes(category.toLowerCase()) + ) { + continue; + } + if (!tokens || typeof tokens !== "object") continue; + + const processedTokens = processTokens( + tokens, + fileName, + query ?? "", + type, + ); + results.push(...processedTokens); } - const processedTokens = processTokens(tokens, fileName, query, type); - results.push(...processedTokens); } + results = results.slice(0, limit); return { total: results.length, tokens: results }; }, @@ -143,6 +166,29 @@ export function createTokenTools() { return { total: sliced.length, tokens: sliced }; }, }, + { + name: "get-token-categories", + description: + "Get all available token categories in the Spectrum design system", + inputSchema: { + type: "object", + properties: {}, + }, + handler: async () => { + const tokenData = await getTokenData(); + const categories = + tokenData && typeof tokenData === "object" + ? Object.keys(tokenData).map((fileName) => + String(fileName).replace(".json", ""), + ) + : []; + + return { + categories, + total: categories.length, + }; + }, + }, { name: "get-token-details", description: "Get full token data by path (flat token name).", @@ -159,23 +205,32 @@ export function createTokenTools() { required: ["tokenPath"], }, handler: async (args) => { - const { tokenPath, category } = args; - const tokenData = await getTokenData(); + const tokenPath = + args?.tokenPath != null ? String(args.tokenPath) : undefined; + const category = validateStringParam(args?.category, "category"); + + if (!tokenPath || tokenPath.trim() === "") { + throw new Error("tokenPath is required"); + } - // Search for the token in all categories or specific category - const categoriesToSearch = category - ? [category] - : Object.keys(tokenData); + const tokenData = await getTokenData(); + const categoriesToSearch = + category != null && category !== "" + ? [category] + : tokenData && typeof tokenData === "object" + ? Object.keys(tokenData) + : []; for (const cat of categoriesToSearch) { - const categoryData = tokenData[cat + ".json"] || tokenData[cat]; - if (!categoryData) continue; + const key = cat.endsWith(".json") ? cat : `${cat}.json`; + const categoryData = tokenData?.[key] ?? tokenData?.[cat]; + if (!categoryData || typeof categoryData !== "object") continue; const token = findTokenByPath(categoryData, tokenPath); if (token) { return { path: tokenPath, - category: cat, + category: cat.replace(".json", ""), token, }; } @@ -184,6 +239,116 @@ export function createTokenTools() { throw new Error(`Token not found: ${tokenPath}`); }, }, + { + name: "find-tokens-by-use-case", + description: + 'Find appropriate design tokens for specific component use cases (e.g., "button background", "text color", "border", "spacing")', + inputSchema: { + type: "object", + properties: { + useCase: { + type: "string", + description: + 'The use case or purpose (e.g., "button background", "text color", "border", "spacing", "error state")', + }, + componentType: { + type: "string", + description: + 'Optional: Type of component being built (e.g., "button", "input", "card")', + }, + }, + required: ["useCase"], + }, + handler: async (args) => { + const useCase = + args?.useCase != null ? String(args.useCase) : undefined; + const componentType = validateStringParam( + args?.componentType, + "componentType", + ); + + if (!useCase || useCase.trim() === "") { + throw new Error("useCase is required"); + } + + const data = await getTokenData(); + const recommendations = []; + const useCaseLower = useCase.toLowerCase(); + const compTypeLower = (componentType ?? "").toLowerCase(); + + const relevantCategories = []; + for (const [pattern, categories] of Object.entries(USE_CASE_PATTERNS)) { + if ( + useCaseLower.includes(pattern) || + compTypeLower.includes(pattern) + ) { + relevantCategories.push(...categories); + } + } + + const categoriesToSearch = + relevantCategories.length > 0 + ? [...new Set(relevantCategories)] + : data && typeof data === "object" + ? Object.keys(data) + : []; + + for (const category of categoriesToSearch) { + const filename = category.includes(".json") + ? category + : `${category}.json`; + const tokens = data?.[filename]; + if (!tokens || typeof tokens !== "object") continue; + + for (const [name, token] of Object.entries(tokens)) { + if (!token || typeof token !== "object") continue; + + const nameMatch = + name.toLowerCase().includes(useCaseLower) || + (componentType != null && + name.toLowerCase().includes(compTypeLower)); + const descMatch = + token.description != null && + String(token.description).toLowerCase().includes(useCaseLower); + + if (nameMatch || descMatch) { + recommendations.push({ + name, + category: filename, + value: token.value, + description: token.description, + schema: token.$schema, + uuid: token.uuid, + private: token.private === true, + deprecated: token.deprecated === true, + deprecated_comment: token.deprecated_comment, + renamed: token.renamed, + relevanceReason: nameMatch ? "name match" : "description match", + }); + } + } + } + + recommendations.sort((a, b) => { + if (a.private !== b.private) return a.private ? 1 : -1; + if (a.relevanceReason !== b.relevanceReason) { + return a.relevanceReason === "name match" ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + + return { + useCase, + componentType: componentType ?? undefined, + recommendations: recommendations.slice( + 0, + RESULT_LIMITS.MAX_TOKENS_BY_USE_CASE, + ), + totalFound: recommendations.length, + searchedCategories: categoriesToSearch, + }; + }, + }, { name: "get-component-tokens", description: @@ -198,29 +363,47 @@ export function createTokenTools() { }, required: ["componentName"], }, - handler: async ({ componentName }) => { + handler: async (args) => { + const componentName = + args?.componentName != null ? String(args.componentName) : undefined; + if (!componentName || componentName.trim() === "") { + throw new Error("componentName is required"); + } + const data = await getTokenData(); const componentTokens = []; const componentLower = componentName.toLowerCase(); - Object.entries(data).forEach(([category, tokens]) => { - if (!tokens) return; - Object.entries(tokens).forEach(([name, token]) => { - if (name.toLowerCase().includes(componentLower)) { - const entry = { name, category, value: token.value }; - if (token.description) entry.description = token.description; - if (token.deprecated) entry.deprecated = true; - if (token.private) entry.private = true; - componentTokens.push(entry); + if (data && typeof data === "object") { + for (const [cat, tokens] of Object.entries(data)) { + if (!tokens || typeof tokens !== "object") continue; + + for (const [name, token] of Object.entries(tokens)) { + if (!token || typeof token !== "object") continue; + if (!name.toLowerCase().includes(componentLower)) continue; + + componentTokens.push({ + name, + category: cat, + value: token.value, + description: token.description, + schema: token.$schema, + uuid: token.uuid, + private: token.private === true, + deprecated: token.deprecated === true, + deprecated_comment: token.deprecated_comment, + renamed: token.renamed, + }); } - }); - }); + } + } const groupedTokens = componentTokens.reduce((acc, token) => { - if (!acc[token.category]) acc[token.category] = []; - acc[token.category].push(token); + const category = token.category; + if (!acc[category]) acc[category] = []; + acc[category].push(token); return acc; - }, {}); + }, /** @type {Record>} */ ({})); return { tokensByCategory: groupedTokens, @@ -228,6 +411,175 @@ export function createTokenTools() { }; }, }, + { + name: "get-design-recommendations", + description: + "Get design token recommendations for common design decisions and component states", + inputSchema: { + type: "object", + properties: { + intent: { + type: "string", + description: + 'Design intent (e.g., "primary", "secondary", "accent", "negative", "notice", "positive", "informative")', + }, + state: { + type: "string", + description: + 'Component state (e.g., "default", "hover", "focus", "active", "disabled", "selected")', + }, + context: { + type: "string", + description: + 'Usage context (e.g., "button", "input", "text", "background", "border", "icon")', + }, + }, + required: ["intent"], + }, + handler: async (args) => { + const intent = args?.intent != null ? String(args.intent) : undefined; + const state = validateStringParam(args?.state, "state"); + const context = validateStringParam(args?.context, "context"); + + if (!intent || intent.trim() === "") { + throw new Error("intent is required"); + } + + const data = await getTokenData(); + const recommendations = { + colors: [], + layout: [], + typography: [], + }; + + const intentLower = intent.toLowerCase(); + const stateLower = (state ?? "").toLowerCase(); + const contextLower = (context ?? "").toLowerCase(); + + const semanticColors = data?.["semantic-color-palette.json"] ?? {}; + if (semanticColors && typeof semanticColors === "object") { + for (const [name, token] of Object.entries(semanticColors)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + + const intentMatch = tokenNameMatchesIntent(nameLower, intentLower); + const stateMatch = + !state || state === "" || nameLower.includes(stateLower); + const contextMatch = + !context || context === "" || nameLower.includes(contextLower); + + if (intentMatch && stateMatch && contextMatch) { + recommendations.colors.push({ + name, + value: token.value, + category: "semantic-color-palette", + type: "semantic", + confidence: "high", + }); + } + } + } + + if (recommendations.colors.length < 3) { + const componentColors = data?.["color-component.json"] ?? {}; + if (componentColors && typeof componentColors === "object") { + for (const [name, token] of Object.entries(componentColors)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + + const intentMatch = nameLower.includes(intentLower); + const contextMatch = + !context || context === "" || nameLower.includes(contextLower); + const stateMatch = + !state || state === "" || nameLower.includes(stateLower); + + if ((intentMatch || contextMatch) && stateMatch) { + recommendations.colors.push({ + name, + value: token.value, + category: "color-component", + type: "component", + confidence: "medium", + }); + } + } + } + } + + const layoutContexts = [ + "button", + "input", + "spacing", + "padding", + "margin", + ]; + if (context && layoutContexts.some((c) => contextLower.includes(c))) { + const layoutComponent = data?.["layout-component.json"] ?? {}; + if (layoutComponent && typeof layoutComponent === "object") { + for (const [name, token] of Object.entries(layoutComponent)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + + if ( + contextLower && + nameLower.includes(contextLower) && + nameLower.includes(stateLower || "size") + ) { + recommendations.layout.push({ + name, + value: token.value, + category: "layout-component", + type: "spacing", + confidence: "high", + }); + } + } + } + } + + const textContexts = ["text", "label", "heading", "body"]; + if (context && textContexts.some((c) => contextLower.includes(c))) { + const typography = data?.["typography.json"] ?? {}; + if (typography && typeof typography === "object") { + for (const [name, token] of Object.entries(typography)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + + if (nameLower.includes(contextLower)) { + recommendations.typography.push({ + name, + value: token.value, + category: "typography", + type: "text", + confidence: "high", + }); + } + } + } + } + + const confidenceOrder = { high: 0, medium: 1, low: 2 }; + for (const category of ["colors", "layout", "typography"]) { + recommendations[category] = recommendations[category] + .sort( + (a, b) => + confidenceOrder[a.confidence] - confidenceOrder[b.confidence], + ) + .slice(0, RESULT_LIMITS.MAX_DESIGN_RECOMMENDATIONS); + } + + return { + intent, + state: state ?? undefined, + context: context ?? undefined, + recommendations, + totalFound: + recommendations.colors.length + + recommendations.layout.length + + recommendations.typography.length, + }; + }, + }, ]; } @@ -236,47 +588,50 @@ export function createTokenTools() { * @param {Object} tokens - Token data structure * @param {string} fileName - Name of the token file * @param {string} query - Search query - * @param {string} type - Type filter + * @param {string|undefined} type - Type filter * @returns {Array} Processed tokens */ function processTokens(tokens, fileName, query, type) { const results = []; - const category = fileName.replace(".json", ""); + if (!tokens || typeof tokens !== "object") return results; + + const category = String(fileName).replace(".json", ""); function traverse(obj, path = "") { + if (!obj || typeof obj !== "object") return; for (const [key, value] of Object.entries(obj)) { const currentPath = path ? `${path}.${key}` : key; - if (value && typeof value === "object") { + if (value != null && typeof value === "object") { if (value.$value !== undefined || value.value !== undefined) { - // This is a token - const tokenType = value.$type || value.type || "unknown"; + const tokenType = value.$type ?? value.type ?? "unknown"; - // Apply type filter - if (type && tokenType !== type) { + if (type != null && type !== "" && tokenType !== type) { continue; } - // Apply query filter - if (query && !matchesQuery(currentPath, value, query)) { + if ( + query != null && + query !== "" && + !matchesQuery(currentPath, value, query) + ) { continue; } - const entry = { + results.push({ name: key, category, - value: value.$value || value.value, - }; - if (value.$description || value.description) - entry.description = value.$description || value.description; - if (value.deprecated) entry.deprecated = true; - if (value.private) entry.private = true; - if (value.deprecated_comment) - entry.deprecated_comment = value.deprecated_comment; - if (value.renamed) entry.renamed = value.renamed; - results.push(entry); + type: tokenType, + value: value.$value ?? value.value, + description: value.$description ?? value.description, + extensions: value.$extensions ?? value.extensions, + uuid: value.uuid, + private: value.private === true, + deprecated: value.deprecated === true, + deprecated_comment: value.deprecated_comment, + renamed: value.renamed, + }); } else { - // Recurse into nested objects traverse(value, currentPath); } } @@ -294,17 +649,22 @@ function processTokens(tokens, fileName, query, type) { * @returns {Object|null} Token object or null if not found */ function findTokenByPath(tokens, path) { - const parts = path.split("."); + if (!tokens || typeof tokens !== "object" || !path) return null; + const parts = String(path).split("."); let current = tokens; for (const part of parts) { - if (current[part] === undefined) { + if ( + current == null || + typeof current !== "object" || + current[part] === undefined + ) { return null; } current = current[part]; } - return current; + return current != null && typeof current === "object" ? current : null; } /** @@ -315,21 +675,18 @@ function findTokenByPath(tokens, path) { * @returns {boolean} Whether the token matches */ function matchesQuery(path, token, query) { - const searchText = query.toLowerCase(); + const searchText = String(query).toLowerCase(); - // Search in path - if (path.toLowerCase().includes(searchText)) { + if (String(path).toLowerCase().includes(searchText)) { return true; } - // Search in description - const description = token.$description || token.description || ""; - if (description.toLowerCase().includes(searchText)) { + const description = token?.$description ?? token?.description ?? ""; + if (String(description).toLowerCase().includes(searchText)) { return true; } - // Search in value (for string values) - const value = token.$value || token.value || ""; + const value = token?.$value ?? token?.value ?? ""; if (typeof value === "string" && value.toLowerCase().includes(searchText)) { return true; } diff --git a/tools/spectrum-design-data-mcp/src/tools/workflows.js b/tools/spectrum-design-data-mcp/src/tools/workflows.js index 1a75fc15..b758e926 100644 --- a/tools/spectrum-design-data-mcp/src/tools/workflows.js +++ b/tools/spectrum-design-data-mcp/src/tools/workflows.js @@ -12,6 +12,24 @@ governing permissions and limitations under the License. import { getTokenData } from "../data/tokens.js"; import { getSchemaData } from "../data/schemas.js"; +import { RESULT_LIMITS } from "../constants.js"; +import { + validateComponentName, + validatePropsObject, + validateStringParam, +} from "../utils/validation.js"; +import { + buildRecommendedProps, + validateComponentConfig, + validatePropsWithImprovements, +} from "../utils/component-helpers.js"; +import { + findComponentTokens, + findSemanticColorsByIntent, + findSemanticColorsByVariant, + findTokensByUseCase, + groupTokensByCategory, +} from "../utils/token-helpers.js"; /** * Create workflow-oriented MCP tools that orchestrate multiple operations @@ -57,166 +75,86 @@ export function createWorkflowTools() { required: ["component"], }, handler: async (args) => { - const { - component, - variant, - intent, - useCase, - includeTokens = true, - } = args; + const rawComponent = args?.component; + const variant = validateStringParam(args?.variant, "variant"); + const intent = validateStringParam(args?.intent, "intent"); + const useCase = validateStringParam(args?.useCase, "useCase"); + const includeTokens = args?.includeTokens !== false; + + const component = validateComponentName(rawComponent); const schemaData = await getSchemaData(); const tokenData = await getTokenData(); - // Get component schema const fileName = `${component}.json`; - const schema = schemaData.components[fileName]; + const schema = + schemaData?.components != null + ? schemaData.components[fileName] + : undefined; - if (!schema) { + if (!schema || typeof schema !== "object") { throw new Error( `Component not found: ${component}. Use list-components to see available components.`, ); } + const { recommendedProps, schemaProperties } = buildRecommendedProps( + schema, + variant, + ); + const config = { component, schema: { title: schema.title, description: schema.description, - properties: {}, + properties: schemaProperties, }, - recommendedProps: {}, + recommendedProps, tokens: includeTokens ? {} : undefined, validation: {}, }; - // Build recommended props based on schema - if (schema.properties) { - Object.entries(schema.properties).forEach(([propName, propDef]) => { - config.schema.properties[propName] = { - type: propDef.type, - description: propDef.description, - required: schema.required?.includes(propName) || false, - }; - - // Set default values if available - if (propDef.default !== undefined) { - config.recommendedProps[propName] = propDef.default; - } - - // Apply variant if it's a variant property - if (propName === "variant" && variant) { - if (propDef.enum && propDef.enum.includes(variant)) { - config.recommendedProps[propName] = variant; - } - } - }); - } - - // Get component tokens if requested - if (includeTokens) { - const componentTokens = []; - const componentLower = component.toLowerCase(); - - // Search for component-specific tokens - Object.entries(tokenData).forEach(([category, tokens]) => { - if (!tokens) return; - - Object.entries(tokens).forEach(([name, token]) => { - if (name.toLowerCase().includes(componentLower)) { - componentTokens.push({ - name, - category, - value: token.value, - description: token.description, - }); - } - }); - }); + if (includeTokens && tokenData && typeof tokenData === "object") { + const componentTokens = findComponentTokens(tokenData, component); + if (componentTokens.length > 0) { + config.tokens.componentTokens = + groupTokensByCategory(componentTokens); + } - // Get design recommendations if intent is provided if (intent) { - const semanticColors = tokenData["semantic-color-palette.json"] || {}; - const recommendations = []; - - Object.entries(semanticColors).forEach(([name, token]) => { - const nameLower = name.toLowerCase(); - const intentLower = intent.toLowerCase(); - - if ( - nameLower.includes(intentLower) || - (intentLower === "error" && nameLower.includes("negative")) || - (intentLower === "success" && nameLower.includes("positive")) || - (intentLower === "warning" && nameLower.includes("notice")) - ) { - recommendations.push({ - name, - value: token.value, - category: "semantic-color-palette", - type: "semantic", - }); - } - }); - - config.tokens.colors = recommendations.slice(0, 5); + const semanticColors = + tokenData["semantic-color-palette.json"] ?? {}; + const recommendations = findSemanticColorsByIntent( + semanticColors, + intent, + RESULT_LIMITS.MAX_COLOR_RECOMMENDATIONS, + ); + if (recommendations.length > 0) { + config.tokens.colors = recommendations; + } } - // Find tokens by use case if provided if (useCase) { - const useCaseLower = useCase.toLowerCase(); - const useCaseTokens = []; - - Object.entries(tokenData).forEach(([category, tokens]) => { - if (!tokens) return; - - Object.entries(tokens).forEach(([name, token]) => { - const nameMatch = - name.toLowerCase().includes(useCaseLower) || - (token.description && - token.description.toLowerCase().includes(useCaseLower)); - - if (nameMatch && !token.private) { - useCaseTokens.push({ - name, - category, - value: token.value, - description: token.description, - }); - } - }); - }); - + const useCaseTokens = findTokensByUseCase( + tokenData, + useCase, + RESULT_LIMITS.MAX_USE_CASE_TOKENS, + ); if (useCaseTokens.length > 0) { - config.tokens.useCaseTokens = useCaseTokens.slice(0, 10); + config.tokens.useCaseTokens = useCaseTokens; } } - - // Group component tokens by category - if (componentTokens.length > 0) { - config.tokens.componentTokens = componentTokens.reduce( - (acc, token) => { - if (!acc[token.category]) acc[token.category] = []; - acc[token.category].push(token); - return acc; - }, - {}, - ); - } - } - - // Basic validation of recommended props - const validationErrors = []; - const required = schema.required || []; - for (const requiredProp of required) { - if (!(requiredProp in config.recommendedProps)) { - validationErrors.push(`Missing required property: ${requiredProp}`); - } } + const validationResult = validateComponentConfig( + config.recommendedProps, + schema, + ); config.validation = { - valid: validationErrors.length === 0, - errors: validationErrors, - warnings: [], + valid: validationResult.valid, + errors: validationResult.errors, + warnings: validationResult.warnings, }; return config; @@ -249,181 +187,76 @@ export function createWorkflowTools() { required: ["component", "props"], }, handler: async (args) => { - const { component, props, includeTokenSuggestions = true } = args; + const rawComponent = args?.component; + const props = validatePropsObject(args?.props); + const includeTokenSuggestions = args?.includeTokenSuggestions !== false; + + const component = validateComponentName(rawComponent); const schemaData = await getSchemaData(); const tokenData = await getTokenData(); - // Get component schema const fileName = `${component}.json`; - const schema = schemaData.components[fileName]; + const schema = + schemaData?.components != null + ? schemaData.components[fileName] + : undefined; - if (!schema) { + if (!schema || typeof schema !== "object") { throw new Error( `Component not found: ${component}. Use list-components to see available components.`, ); } + const validationResult = validatePropsWithImprovements(props, schema); + const suggestions = { component, currentProps: props, validation: { - valid: true, - errors: [], - warnings: [], + valid: validationResult.valid, + errors: validationResult.errors, + warnings: validationResult.warnings, }, - improvements: [], + improvements: validationResult.improvements, tokenRecommendations: includeTokenSuggestions ? {} : undefined, - bestPractices: [], + bestPractices: [ + "Use semantic tokens (semantic-color-palette) over raw palette tokens", + "Check for deprecated tokens and use renamed alternatives", + "Validate all props against the component schema", + "Use get-component-options for a user-friendly view of available props", + ], }; - // Validate props against schema - const schemaProps = schema.properties || {}; - const required = schema.required || []; - - // Check required properties - for (const requiredProp of required) { - if (!(requiredProp in props)) { - suggestions.validation.valid = false; - suggestions.validation.errors.push( - `Missing required property: ${requiredProp}`, - ); - suggestions.improvements.push({ - type: "missing_required", - property: requiredProp, - message: `Add required property: ${requiredProp}`, - suggestion: schemaProps[requiredProp]?.default || "See schema", - }); - } - } - - // Check for unknown properties - for (const propName of Object.keys(props)) { - if (!schemaProps[propName]) { - suggestions.validation.warnings.push( - `Unknown property: ${propName}`, - ); - suggestions.improvements.push({ - type: "unknown_property", - property: propName, - message: `Property "${propName}" is not defined in the schema`, - suggestion: "Remove or check spelling", - }); - } - } - - // Check property types - for (const [propName, propValue] of Object.entries(props)) { - const propSchema = schemaProps[propName]; - if (!propSchema) continue; - - if (propSchema.type) { - const expectedType = propSchema.type; - const actualType = Array.isArray(propValue) - ? "array" - : typeof propValue; - - if (expectedType !== actualType) { - suggestions.validation.valid = false; - suggestions.validation.errors.push( - `Property ${propName} should be ${expectedType}, got ${actualType}`, - ); - suggestions.improvements.push({ - type: "type_mismatch", - property: propName, - message: `Type mismatch: expected ${expectedType}, got ${actualType}`, - suggestion: `Change to ${expectedType}`, - }); - } - } - - // Check enum values - if (propSchema.enum && !propSchema.enum.includes(propValue)) { - suggestions.validation.warnings.push( - `Property ${propName} value "${propValue}" is not in allowed enum values`, - ); - suggestions.improvements.push({ - type: "invalid_enum", - property: propName, - message: `Invalid value: "${propValue}"`, - suggestion: `Use one of: ${propSchema.enum.join(", ")}`, - }); - } - } - - // Get token recommendations if requested - if (includeTokenSuggestions) { - const componentTokens = []; - const componentLower = component.toLowerCase(); - - // Find component-specific tokens - Object.entries(tokenData).forEach(([category, tokens]) => { - if (!tokens) return; - - Object.entries(tokens).forEach(([name, token]) => { - if ( - name.toLowerCase().includes(componentLower) && - !token.private && - !token.deprecated - ) { - componentTokens.push({ - name, - category, - value: token.value, - description: token.description, - }); - } - }); + if ( + includeTokenSuggestions && + tokenData && + typeof tokenData === "object" + ) { + const componentTokens = findComponentTokens(tokenData, component, { + excludePrivate: true, + excludeDeprecated: true, }); - - // Group by category if (componentTokens.length > 0) { suggestions.tokenRecommendations.componentTokens = - componentTokens.reduce((acc, token) => { - if (!acc[token.category]) acc[token.category] = []; - acc[token.category].push(token); - return acc; - }, {}); + groupTokensByCategory(componentTokens); } - // Suggest semantic tokens based on variant/intent - if (props.variant) { - const variantLower = props.variant.toLowerCase(); - const semanticColors = tokenData["semantic-color-palette.json"] || - {}; - const semanticTokens = []; - - Object.entries(semanticColors).forEach(([name, token]) => { - const nameLower = name.toLowerCase(); - if ( - nameLower.includes(variantLower) || - (variantLower === "accent" && nameLower.includes("accent")) || - (variantLower === "negative" && nameLower.includes("negative")) - ) { - semanticTokens.push({ - name, - value: token.value, - category: "semantic-color-palette", - type: "semantic", - }); - } - }); - + const variant = props.variant; + if (variant != null && typeof variant === "string") { + const semanticColors = + tokenData["semantic-color-palette.json"] ?? {}; + const semanticTokens = findSemanticColorsByVariant( + semanticColors, + String(variant), + RESULT_LIMITS.MAX_SEMANTIC_COLORS, + ); if (semanticTokens.length > 0) { - suggestions.tokenRecommendations.semanticColors = - semanticTokens.slice(0, 5); + suggestions.tokenRecommendations.semanticColors = semanticTokens; } } } - // Add best practices - suggestions.bestPractices.push( - "Use semantic tokens (semantic-color-palette) over raw palette tokens", - "Check for deprecated tokens and use renamed alternatives", - "Validate all props against the component schema", - "Use get-component-options for a user-friendly view of available props", - ); - return suggestions; }, }, diff --git a/tools/spectrum-design-data-mcp/src/utils/component-helpers.js b/tools/spectrum-design-data-mcp/src/utils/component-helpers.js new file mode 100644 index 00000000..c1285602 --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/utils/component-helpers.js @@ -0,0 +1,162 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/** + * Build recommended props from schema (defaults + variant when applicable) + * @param {Object} schema - Component schema + * @param {string} [variant] - Optional variant to apply to variant property + * @returns {{ recommendedProps: Record, schemaProperties: Record }} + */ +export function buildRecommendedProps(schema, variant) { + const recommendedProps = /** @type {Record} */ ({}); + const schemaProperties = + /** @type {Record} */ ({}); + + if (!schema?.properties || typeof schema.properties !== "object") { + return { recommendedProps, schemaProperties }; + } + + const requiredSet = new Set(schema.required || []); + + for (const [propName, propDef] of Object.entries(schema.properties)) { + if (!propDef || typeof propDef !== "object") continue; + + schemaProperties[propName] = { + type: propDef.type, + description: propDef.description, + required: requiredSet.has(propName), + }; + + if (propDef.default !== undefined) { + recommendedProps[propName] = propDef.default; + } + + if (propName === "variant" && variant) { + const enumValues = propDef.enum; + if (Array.isArray(enumValues) && enumValues.includes(variant)) { + recommendedProps[propName] = variant; + } + } + } + + return { recommendedProps, schemaProperties }; +} + +/** + * Validate that required props are present + * @param {Record} props - Current props + * @param {Object} schema - Component schema + * @returns {{ valid: boolean, errors: string[], warnings: string[] }} + */ +export function validateComponentConfig(props, schema) { + const errors = []; + const required = schema?.required || []; + const schemaProps = schema?.properties || {}; + + for (const requiredProp of required) { + if (!(requiredProp in props)) { + errors.push(`Missing required property: ${requiredProp}`); + } + } + + return { + valid: errors.length === 0, + errors, + warnings: [], + }; +} + +/** + * Validate props against schema and collect improvements for suggest-component-improvements + * @param {Record} props - User props + * @param {Object} schema - Component schema + * @returns {{ valid: boolean, errors: string[], warnings: string[], improvements: Array<{ type: string, property: string, message: string, suggestion: string }> }} + */ +export function validatePropsWithImprovements(props, schema) { + const errors = []; + const warnings = []; + const improvements = []; + const schemaProps = schema?.properties || {}; + const required = schema?.required || []; + + for (const requiredProp of required) { + if (!(requiredProp in props)) { + errors.push(`Missing required property: ${requiredProp}`); + const propSchema = schemaProps[requiredProp]; + improvements.push({ + type: "missing_required", + property: requiredProp, + message: `Add required property: ${requiredProp}`, + suggestion: + propSchema?.default != null + ? String(propSchema.default) + : "See schema", + }); + } + } + + for (const propName of Object.keys(props)) { + if (!schemaProps[propName]) { + warnings.push(`Unknown property: ${propName}`); + improvements.push({ + type: "unknown_property", + property: propName, + message: `Property "${propName}" is not defined in the schema`, + suggestion: "Remove or check spelling", + }); + } + } + + for (const [propName, propValue] of Object.entries(props)) { + const propSchema = schemaProps[propName]; + if (!propSchema) continue; + + if (propSchema.type) { + const expectedType = propSchema.type; + const actualType = Array.isArray(propValue) ? "array" : typeof propValue; + + if (expectedType !== actualType) { + errors.push( + `Property ${propName} should be ${expectedType}, got ${actualType}`, + ); + improvements.push({ + type: "type_mismatch", + property: propName, + message: `Type mismatch: expected ${expectedType}, got ${actualType}`, + suggestion: `Change to ${expectedType}`, + }); + } + } + + if ( + Array.isArray(propSchema.enum) && + !propSchema.enum.includes(propValue) + ) { + warnings.push( + `Property ${propName} value "${propValue}" is not in allowed enum values`, + ); + improvements.push({ + type: "invalid_enum", + property: propName, + message: `Invalid value: "${propValue}"`, + suggestion: `Use one of: ${propSchema.enum.join(", ")}`, + }); + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + improvements, + }; +} diff --git a/tools/spectrum-design-data-mcp/src/utils/token-helpers.js b/tools/spectrum-design-data-mcp/src/utils/token-helpers.js new file mode 100644 index 00000000..3df9102b --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/utils/token-helpers.js @@ -0,0 +1,193 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { + INTENT_SEMANTIC_MAPPINGS, + VARIANT_MAPPINGS, +} from "../config/intent-mappings.js"; + +/** + * Find tokens that match a component name across all categories + * @param {Record>} tokenData - Token data by category + * @param {string} componentName - Component name to match + * @param {Object} [options] - Options + * @param {boolean} [options.excludePrivate=false] - Exclude private tokens + * @param {boolean} [options.excludeDeprecated=false] - Exclude deprecated tokens + * @returns {Array<{ name: string, category: string, value: unknown, description?: string }>} + */ +export function findComponentTokens(tokenData, componentName, options = {}) { + const { excludePrivate = false, excludeDeprecated = false } = options; + const componentTokens = []; + const componentLower = componentName.toLowerCase(); + + for (const [category, tokens] of Object.entries(tokenData)) { + if (!tokens || typeof tokens !== "object") continue; + + for (const [name, token] of Object.entries(tokens)) { + if (!token || typeof token !== "object") continue; + if (excludePrivate && token.private) continue; + if (excludeDeprecated && token.deprecated) continue; + if (!name.toLowerCase().includes(componentLower)) continue; + + componentTokens.push({ + name, + category, + value: token.value, + description: token.description, + }); + } + } + + return componentTokens; +} + +/** + * Check if a token name matches the given intent (including semantic mappings) + * @param {string} nameLower - Token name lowercased + * @param {string} intentLower - Intent lowercased + * @returns {boolean} + */ +export function tokenNameMatchesIntent(nameLower, intentLower) { + if (nameLower.includes(intentLower)) return true; + const mapping = INTENT_SEMANTIC_MAPPINGS[intentLower]; + if (mapping) { + return mapping.some((sub) => nameLower.includes(sub)); + } + return false; +} + +/** + * Find semantic color tokens matching an intent + * @param {Record} semanticColors - Semantic color palette tokens + * @param {string} intent - Design intent (e.g. primary, error, success) + * @param {number} limit - Max results to return + * @returns {Array<{ name: string, value: unknown, category: string, type: string }>} + */ +export function findSemanticColorsByIntent(semanticColors, intent, limit) { + if (!semanticColors || typeof semanticColors !== "object") return []; + const recommendations = []; + const intentLower = intent.toLowerCase(); + + for (const [name, token] of Object.entries(semanticColors)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + if (!tokenNameMatchesIntent(nameLower, intentLower)) continue; + + recommendations.push({ + name, + value: token.value, + category: "semantic-color-palette", + type: "semantic", + }); + } + + return recommendations.slice(0, limit); +} + +/** + * Check if a token name matches the given variant (using variant mappings) + * @param {string} nameLower - Token name lowercased + * @param {string} variantLower - Variant lowercased + * @returns {boolean} + */ +export function tokenNameMatchesVariant(nameLower, variantLower) { + if (nameLower.includes(variantLower)) return true; + const mapping = VARIANT_MAPPINGS[variantLower]; + if (mapping) { + return mapping.some((sub) => nameLower.includes(sub)); + } + return false; +} + +/** + * Find semantic color tokens matching a variant + * @param {Record} semanticColors - Semantic color palette tokens + * @param {string} variant - Variant name (e.g. accent, negative) + * @param {number} limit - Max results to return + * @returns {Array<{ name: string, value: unknown, category: string, type: string }>} + */ +export function findSemanticColorsByVariant(semanticColors, variant, limit) { + if (!semanticColors || typeof semanticColors !== "object") return []; + const semanticTokens = []; + const variantLower = variant.toLowerCase(); + + for (const [name, token] of Object.entries(semanticColors)) { + if (!token || typeof token !== "object") continue; + const nameLower = name.toLowerCase(); + if (!tokenNameMatchesVariant(nameLower, variantLower)) continue; + + semanticTokens.push({ + name, + value: token.value, + category: "semantic-color-palette", + type: "semantic", + }); + } + + return semanticTokens.slice(0, limit); +} + +/** + * Find tokens matching a use case string (name or description) + * @param {Record>} tokenData - Token data by category + * @param {string} useCase - Use case search string + * @param {number} limit - Max results + * @param {Object} [options] - Options + * @param {boolean} [options.excludePrivate=true] - Exclude private tokens + * @returns {Array<{ name: string, category: string, value: unknown, description?: string }>} + */ +export function findTokensByUseCase(tokenData, useCase, limit, options = {}) { + const { excludePrivate = true } = options; + const useCaseLower = useCase.toLowerCase(); + const useCaseTokens = []; + + for (const [category, tokens] of Object.entries(tokenData)) { + if (!tokens || typeof tokens !== "object") continue; + + for (const [name, token] of Object.entries(tokens)) { + if (!token || typeof token !== "object") continue; + if (excludePrivate && token.private) continue; + + const nameMatch = name.toLowerCase().includes(useCaseLower); + const descMatch = + token.description && + String(token.description).toLowerCase().includes(useCaseLower); + + if (nameMatch || descMatch) { + useCaseTokens.push({ + name, + category, + value: token.value, + description: token.description, + }); + } + } + } + + return useCaseTokens.slice(0, limit); +} + +/** + * Group an array of tokens by their category + * @param {Array<{ category: string, [key: string]: unknown }>} tokens - Tokens with category + * @returns {Record>} + */ +export function groupTokensByCategory(tokens) { + if (!Array.isArray(tokens)) return {}; + return tokens.reduce((acc, token) => { + const category = token?.category; + if (category == null) return acc; + if (!acc[category]) acc[category] = []; + acc[category].push(token); + return acc; + }, /** @type {Record>} */ ({})); +} diff --git a/tools/spectrum-design-data-mcp/src/utils/validation.js b/tools/spectrum-design-data-mcp/src/utils/validation.js new file mode 100644 index 00000000..00eda8dc --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/utils/validation.js @@ -0,0 +1,69 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/** + * Validate component name is a non-empty string without path separators + * @param {string} component - Component name to validate + * @returns {string} Trimmed component name (lowercased for lookup) + * @throws {Error} If component is invalid + */ +export function validateComponentName(component) { + if (!component || typeof component !== "string") { + throw new Error("Component name must be a non-empty string"); + } + if (component.includes("/") || component.includes("\\")) { + throw new Error("Component name cannot contain path separators"); + } + return component.trim(); +} + +/** + * Validate and clamp limit parameter + * @param {number|undefined} limit - Requested limit + * @param {number} defaultLimit - Default when limit is invalid + * @param {number} maxLimit - Maximum allowed value + * @returns {number} Valid limit + */ +export function validateLimit(limit, defaultLimit, maxLimit = 100) { + const parsedLimit = Number(limit); + if (Number.isNaN(parsedLimit) || parsedLimit < 1) { + return defaultLimit; + } + return Math.min(parsedLimit, maxLimit); +} + +/** + * Validate props is a plain object (not array or null) + * @param {unknown} props - Props to validate + * @returns {Record} The props object + * @throws {Error} If props is invalid + */ +export function validatePropsObject(props) { + if (!props || typeof props !== "object" || Array.isArray(props)) { + throw new Error("Props must be a valid object"); + } + return /** @type {Record} */ (props); +} + +/** + * Validate optional string parameter + * @param {unknown} param - Parameter value + * @param {string} paramName - Name for error messages + * @returns {string|undefined} The param if valid, undefined if not provided + * @throws {Error} If param is provided but not a string + */ +export function validateStringParam(param, paramName) { + if (param !== undefined && param !== null && typeof param !== "string") { + throw new Error(`${paramName} must be a string`); + } + return param !== undefined && param !== null ? String(param) : undefined; +} diff --git a/tools/spectrum-design-data-mcp/test/skills/agent-skills-integration.test.js b/tools/spectrum-design-data-mcp/test/skills/agent-skills-integration.test.js new file mode 100644 index 00000000..1dc3122f --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/skills/agent-skills-integration.test.js @@ -0,0 +1,317 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { createWorkflowTools } from "../../src/tools/workflows.js"; +import { createTokenTools } from "../../src/tools/tokens.js"; +import { createSchemaTools } from "../../src/tools/schemas.js"; + +const workflowTools = createWorkflowTools(); +const tokenTools = createTokenTools(); +const schemaTools = createSchemaTools(); + +function getTool(tools, name) { + return tools.find((t) => t.name === name); +} + +async function getComponentSchema(component) { + const tool = getTool(schemaTools, "get-component-schema"); + return await tool.handler({ component }); +} + +async function getComponentTokens(componentName) { + const tool = getTool(tokenTools, "get-component-tokens"); + return await tool.handler({ componentName }); +} + +async function findTokensByUseCase(useCase, componentType) { + const tool = getTool(tokenTools, "find-tokens-by-use-case"); + return await tool.handler({ useCase, componentType }); +} + +async function getDesignRecommendations(intent, state, context) { + const tool = getTool(tokenTools, "get-design-recommendations"); + return await tool.handler({ intent, state, context }); +} + +async function validateProps(component, props) { + const tool = getTool(schemaTools, "validate-component-props"); + return await tool.handler({ component, props }); +} + +async function getTokenDetails(tokenPath, category) { + const tool = getTool(tokenTools, "get-token-details"); + return await tool.handler({ tokenPath, category }); +} + +async function buildComponentConfig(args) { + const tool = getTool(workflowTools, "build-component-config"); + return await tool.handler(args); +} + +// --- Component Builder Workflows (from component-builder.md) --- + +test("Component Builder: action-button primary medium", async (t) => { + const schema = await getComponentSchema("action-button"); + t.truthy(schema.schema); + t.truthy(schema.schema.properties); + + const tokens = await getComponentTokens("action-button"); + t.truthy(tokens.tokensByCategory); + + const bgTokens = await findTokensByUseCase( + "button background", + "action-button", + ); + t.true(Array.isArray(bgTokens.recommendations)); + + const recommendations = await getDesignRecommendations( + "primary", + undefined, + "button", + ); + t.truthy(recommendations.recommendations.colors); + + const validation = await validateProps("action-button", { + variant: "accent", + size: "m", + }); + t.true(validation.valid); +}); + +test("Component Builder: text-field error state", async (t) => { + const schema = await getComponentSchema("text-field"); + t.truthy(schema.schema); + + const tokens = await getComponentTokens("text-field"); + t.truthy(tokens.tokensByCategory); + + const errorTokens = await findTokensByUseCase("error state", "input"); + t.true(Array.isArray(errorTokens.recommendations)); + + const recommendations = await getDesignRecommendations( + "negative", + undefined, + "input", + ); + t.truthy(recommendations.recommendations.colors); + t.true(recommendations.recommendations.colors.length >= 0); + + const validation = await validateProps("text-field", { + validationState: "invalid", + errorMessage: "Please enter a valid value", + }); + t.truthy(validation); +}); + +// --- Token Finder Workflows (from token-finder.md) --- + +test("Token Finder: primary button colors", async (t) => { + const recommendations = await getDesignRecommendations( + "primary", + undefined, + "button", + ); + t.truthy(recommendations.recommendations.colors); + t.true(Array.isArray(recommendations.recommendations.colors)); + + t.true( + Array.isArray(recommendations.recommendations.colors), + "should return colors array", + ); + + const bgTokens = await findTokensByUseCase("button background", "button"); + t.true(Array.isArray(bgTokens.recommendations)); + + if (recommendations.recommendations.colors.length > 0) { + const firstColor = recommendations.recommendations.colors[0]; + const details = await getTokenDetails(firstColor.name); + t.truthy(details.token); + } +}); + +test("Token Finder: form field spacing", async (t) => { + const spacingTokens = await findTokensByUseCase("spacing", "input"); + t.true(Array.isArray(spacingTokens.recommendations)); + + const componentTokens = await getComponentTokens("text-field"); + t.truthy(componentTokens.tokensByCategory); + + const recommendations = await getDesignRecommendations( + "informative", + undefined, + "spacing", + ); + t.truthy(recommendations.recommendations.layout); + t.true(Array.isArray(recommendations.recommendations.layout)); +}); + +test("Token Finder: error messaging tokens", async (t) => { + const recommendations = await getDesignRecommendations( + "negative", + undefined, + "text", + ); + t.truthy(recommendations.recommendations.colors); + t.true(Array.isArray(recommendations.recommendations.colors)); + + const errorTokens = await findTokensByUseCase("error state"); + t.true(Array.isArray(errorTokens.recommendations)); + + if (recommendations.recommendations.colors.length > 0) { + const details = await getTokenDetails( + recommendations.recommendations.colors[0].name, + ); + t.truthy(details.token); + } +}); + +// --- SKILL.md example validation --- + +test("SKILL.md example: Component Builder workflow", async (t) => { + const schema = await getComponentSchema("action-button"); + t.truthy(schema.schema); + + const tokens = await getComponentTokens("action-button"); + t.truthy(tokens.tokensByCategory); + + const useCaseTokens = await findTokensByUseCase( + "button background", + "action-button", + ); + t.true(Array.isArray(useCaseTokens.recommendations)); + + const recommendations = await getDesignRecommendations( + "primary", + undefined, + "button", + ); + t.truthy(recommendations.recommendations); + + const props = { variant: "accent", size: "m" }; + const validation = await validateProps("action-button", props); + t.true(validation.valid); +}); + +test("SKILL.md example: Token Finder workflow", async (t) => { + const recommendations = await getDesignRecommendations( + "primary", + undefined, + "button", + ); + t.truthy(recommendations.recommendations); + + const bgTokens = await findTokensByUseCase("button background", "button"); + t.true(Array.isArray(bgTokens.recommendations)); + + const categoriesTool = getTool(tokenTools, "get-token-categories"); + const categoriesResult = await categoriesTool.handler({}); + t.truthy(categoriesResult.categories); + t.true(categoriesResult.categories.length > 0); +}); + +// --- Workflow validation --- + +test("Workflow validation: Component Builder steps execute in sequence", async (t) => { + const executionLog = []; + + const schema = await getComponentSchema("action-button"); + executionLog.push("get-component-schema"); + t.truthy(schema.schema); + + const tokens = await getComponentTokens("action-button"); + executionLog.push("get-component-tokens"); + t.truthy(tokens.tokensByCategory); + + await findTokensByUseCase("button background", "action-button"); + executionLog.push("find-tokens-by-use-case"); + + await getDesignRecommendations("primary", undefined, "button"); + executionLog.push("get-design-recommendations"); + + await validateProps("action-button", { variant: "accent" }); + executionLog.push("validate-component-props"); + + t.deepEqual(executionLog, [ + "get-component-schema", + "get-component-tokens", + "find-tokens-by-use-case", + "get-design-recommendations", + "validate-component-props", + ]); +}); + +test("Workflow performance: button builder completes in reasonable time", async (t) => { + const start = Date.now(); + + await getComponentSchema("action-button"); + await getComponentTokens("action-button"); + await findTokensByUseCase("button background", "action-button"); + await getDesignRecommendations("primary", undefined, "button"); + await validateProps("action-button", { variant: "accent", size: "m" }); + + const duration = Date.now() - start; + t.true(duration < 10000, "Workflow should complete in under 10 seconds"); +}); + +// --- Error handling --- + +test("Error handling: invalid component name for get-component-schema", async (t) => { + const error = await t.throwsAsync(async () => { + await getComponentSchema("non-existent-component"); + }); + t.true(error.message.includes("not found")); +}); + +test("Error handling: invalid token path for get-token-details", async (t) => { + const error = await t.throwsAsync(async () => { + await getTokenDetails("invalid.token.path.that.does.not.exist"); + }); + t.true(error.message.includes("not found")); +}); + +test("Error handling: build-component-config with invalid component", async (t) => { + const error = await t.throwsAsync(async () => { + await buildComponentConfig({ component: "non-existent-component" }); + }); + t.true(error.message.includes("not found")); +}); + +test("Error handling: get-component-schema with empty component", async (t) => { + const error = await t.throwsAsync(async () => { + await getComponentSchema(""); + }); + t.truthy(error.message); +}); + +test("Error handling: find-tokens-by-use-case with missing useCase", async (t) => { + const tool = getTool(tokenTools, "find-tokens-by-use-case"); + const error = await t.throwsAsync(async () => { + await tool.handler({}); + }); + t.truthy(error.message); +}); + +test("Error handling: get-design-recommendations with missing intent", async (t) => { + const tool = getTool(tokenTools, "get-design-recommendations"); + const error = await t.throwsAsync(async () => { + await tool.handler({}); + }); + t.truthy(error.message); +}); + +test("Error handling: validate-component-props with invalid component", async (t) => { + const error = await t.throwsAsync(async () => { + await validateProps("non-existent-component", { size: "m" }); + }); + t.true(error.message.includes("not found")); +}); diff --git a/tools/spectrum-design-data-mcp/test/tools/workflows.test.js b/tools/spectrum-design-data-mcp/test/tools/workflows.test.js index 77a6057b..c3104e9d 100644 --- a/tools/spectrum-design-data-mcp/test/tools/workflows.test.js +++ b/tools/spectrum-design-data-mcp/test/tools/workflows.test.js @@ -258,3 +258,35 @@ test("suggest-component-improvements detects unknown properties", async (t) => { ); } }); + +test("build-component-config throws for invalid component name", async (t) => { + const tools = createWorkflowTools(); + const buildTool = tools.find( + (tool) => tool.name === "build-component-config", + ); + + const error = await t.throwsAsync(async () => { + await buildTool.handler({ component: "" }); + }); + t.true(error.message.includes("non-empty string")); + + const error2 = await t.throwsAsync(async () => { + await buildTool.handler({ component: "foo/bar" }); + }); + t.true(error2.message.includes("path separators")); +}); + +test("suggest-component-improvements throws for invalid props", async (t) => { + const tools = createWorkflowTools(); + const suggestTool = tools.find( + (tool) => tool.name === "suggest-component-improvements", + ); + + const error = await t.throwsAsync(async () => { + await suggestTool.handler({ + component: "action-button", + props: "not-an-object", + }); + }); + t.true(error.message.includes("valid object")); +}); diff --git a/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js b/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js new file mode 100644 index 00000000..bdb1cc76 --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js @@ -0,0 +1,104 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { + buildRecommendedProps, + validateComponentConfig, + validatePropsWithImprovements, +} from "../../src/utils/component-helpers.js"; + +test("buildRecommendedProps returns defaults from schema", (t) => { + const schema = { + properties: { + size: { type: "string", default: "m" }, + variant: { type: "string", enum: ["accent", "primary"] }, + }, + }; + const { recommendedProps, schemaProperties } = buildRecommendedProps(schema); + t.is(recommendedProps.size, "m"); + t.truthy(schemaProperties.size); +}); + +test("buildRecommendedProps applies variant when valid enum", (t) => { + const schema = { + properties: { + variant: { + type: "string", + enum: ["accent", "primary"], + default: "primary", + }, + }, + }; + const { recommendedProps } = buildRecommendedProps(schema, "accent"); + t.is(recommendedProps.variant, "accent"); +}); + +test("buildRecommendedProps ignores variant when not in enum", (t) => { + const schema = { + properties: { + variant: { + type: "string", + enum: ["accent", "primary"], + default: "primary", + }, + }, + }; + const { recommendedProps } = buildRecommendedProps(schema, "invalid"); + t.is(recommendedProps.variant, "primary"); +}); + +test("validateComponentConfig reports missing required", (t) => { + const schema = { required: ["size"] }; + const result = validateComponentConfig({}, schema); + t.false(result.valid); + t.true(result.errors.some((e) => e.includes("size"))); +}); + +test("validateComponentConfig valid when required present", (t) => { + const schema = { required: ["size"] }; + const result = validateComponentConfig({ size: "m" }, schema); + t.true(result.valid); +}); + +test("validatePropsWithImprovements returns improvements for missing required", (t) => { + const schema = { + required: ["size"], + properties: { size: { type: "string", default: "m" } }, + }; + const result = validatePropsWithImprovements({}, schema); + t.false(result.valid); + t.is(result.improvements.length, 1); + t.is(result.improvements[0].type, "missing_required"); +}); + +test("validatePropsWithImprovements returns improvements for unknown property", (t) => { + const schema = { properties: {} }; + const result = validatePropsWithImprovements({ unknown: 1 }, schema); + t.true(result.warnings.some((w) => w.includes("Unknown"))); + t.true(result.improvements.some((i) => i.type === "unknown_property")); +}); + +test("validatePropsWithImprovements returns improvements for type mismatch", (t) => { + const schema = { properties: { size: { type: "string" } } }; + const result = validatePropsWithImprovements({ size: 123 }, schema); + t.false(result.valid); + t.true(result.improvements.some((i) => i.type === "type_mismatch")); +}); + +test("validatePropsWithImprovements returns improvements for invalid enum", (t) => { + const schema = { + properties: { variant: { type: "string", enum: ["a", "b"] } }, + }; + const result = validatePropsWithImprovements({ variant: "c" }, schema); + t.true(result.improvements.some((i) => i.type === "invalid_enum")); +}); diff --git a/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js b/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js new file mode 100644 index 00000000..41d58690 --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js @@ -0,0 +1,116 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { + findComponentTokens, + findSemanticColorsByIntent, + findSemanticColorsByVariant, + findTokensByUseCase, + groupTokensByCategory, + tokenNameMatchesIntent, + tokenNameMatchesVariant, +} from "../../src/utils/token-helpers.js"; + +test("findComponentTokens returns tokens matching component name", (t) => { + const tokenData = { + "color-component.json": { + "action-button-background": { value: "#fff", description: "Bg" }, + "other-token": { value: "#000" }, + }, + }; + const result = findComponentTokens(tokenData, "action-button"); + t.is(result.length, 1); + t.is(result[0].name, "action-button-background"); +}); + +test("findComponentTokens excludes private when option set", (t) => { + const tokenData = { + "color.json": { + "button-secret": { value: "#fff", private: true }, + "button-public": { value: "#000" }, + }, + }; + const result = findComponentTokens(tokenData, "button", { + excludePrivate: true, + }); + t.is(result.length, 1); + t.is(result[0].name, "button-public"); +}); + +test("findComponentTokens skips invalid token data", (t) => { + const tokenData = { + "a.json": null, + "b.json": { x: { value: 1 } }, + }; + const result = findComponentTokens(tokenData, "x"); + t.is(result.length, 1); +}); + +test("tokenNameMatchesIntent uses direct match", (t) => { + t.true(tokenNameMatchesIntent("primary-background", "primary")); +}); + +test("tokenNameMatchesIntent uses semantic mapping for error->negative", (t) => { + t.true(tokenNameMatchesIntent("negative-color", "error")); +}); + +test("findSemanticColorsByIntent returns limited results", (t) => { + const semantic = { + "primary-100": { value: "#111" }, + "primary-200": { value: "#222" }, + "primary-300": { value: "#333" }, + "primary-400": { value: "#444" }, + "primary-500": { value: "#555" }, + }; + const result = findSemanticColorsByIntent(semantic, "primary", 2); + t.is(result.length, 2); +}); + +test("findSemanticColorsByVariant uses variant mappings", (t) => { + const semantic = { + "accent-fill": { value: "#blue" }, + }; + const result = findSemanticColorsByVariant(semantic, "accent", 5); + t.is(result.length, 1); + t.is(result[0].name, "accent-fill"); +}); + +test("tokenNameMatchesVariant uses mapping", (t) => { + t.true(tokenNameMatchesVariant("accent-fill", "accent")); +}); + +test("findTokensByUseCase matches name and description", (t) => { + const tokenData = { + "color.json": { + "button-bg": { value: "#fff", description: "Button background color" }, + }, + }; + const result = findTokensByUseCase(tokenData, "button", 10); + t.is(result.length, 1); +}); + +test("groupTokensByCategory groups by category", (t) => { + const tokens = [ + { category: "a", name: "1" }, + { category: "a", name: "2" }, + { category: "b", name: "3" }, + ]; + const grouped = groupTokensByCategory(tokens); + t.deepEqual(Object.keys(grouped).sort(), ["a", "b"]); + t.is(grouped.a.length, 2); + t.is(grouped.b.length, 1); +}); + +test("groupTokensByCategory returns empty object for non-array", (t) => { + t.deepEqual(groupTokensByCategory(null), {}); +}); diff --git a/tools/spectrum-design-data-mcp/test/utils/validation.test.js b/tools/spectrum-design-data-mcp/test/utils/validation.test.js new file mode 100644 index 00000000..252e928e --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/utils/validation.test.js @@ -0,0 +1,91 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { + validateComponentName, + validateLimit, + validatePropsObject, + validateStringParam, +} from "../../src/utils/validation.js"; + +test("validateComponentName accepts non-empty string and trims", (t) => { + t.is(validateComponentName("action-button"), "action-button"); + t.is(validateComponentName(" text-field "), "text-field"); +}); + +test("validateComponentName throws for empty string", (t) => { + const err = t.throws(() => validateComponentName("")); + t.true(err.message.includes("non-empty string")); +}); + +test("validateComponentName throws for non-string", (t) => { + t.throws(() => validateComponentName(null)); + t.throws(() => validateComponentName(undefined)); + t.throws(() => validateComponentName(123)); + t.throws(() => validateComponentName({})); +}); + +test("validateComponentName throws for path separators", (t) => { + const err1 = t.throws(() => validateComponentName("foo/bar")); + t.true(err1.message.includes("path separators")); + const err2 = t.throws(() => validateComponentName("foo\\bar")); + t.true(err2.message.includes("path separators")); +}); + +test("validateLimit returns default for invalid input", (t) => { + t.is(validateLimit(undefined, 50), 50); + t.is(validateLimit(NaN, 20), 20); + t.is(validateLimit(0, 50), 50); + t.is(validateLimit(-1, 50), 50); +}); + +test("validateLimit clamps to maxLimit", (t) => { + t.is(validateLimit(200, 50, 100), 100); + t.is(validateLimit(50, 50, 100), 50); +}); + +test("validateLimit accepts valid number", (t) => { + t.is(validateLimit(25, 50), 25); + t.is(validateLimit(1, 50), 1); +}); + +test("validatePropsObject accepts plain object", (t) => { + const obj = { a: 1 }; + t.is(validatePropsObject(obj), obj); +}); + +test("validatePropsObject throws for non-object", (t) => { + t.throws(() => validatePropsObject(null)); + t.throws(() => validatePropsObject(undefined)); + t.throws(() => validatePropsObject("string")); +}); + +test("validatePropsObject throws for array", (t) => { + const err = t.throws(() => validatePropsObject([])); + t.true(err.message.includes("valid object")); +}); + +test("validateStringParam returns undefined for missing param", (t) => { + t.is(validateStringParam(undefined, "foo"), undefined); + t.is(validateStringParam(null, "foo"), undefined); +}); + +test("validateStringParam returns string for valid param", (t) => { + t.is(validateStringParam("hello", "foo"), "hello"); +}); + +test("validateStringParam throws for non-string when provided", (t) => { + const err = t.throws(() => validateStringParam(123, "paramName")); + t.true(err.message.includes("paramName")); + t.true(err.message.includes("string")); +}); From 2ca6d60c7a079c31502b43344a104b7d41580a0d Mon Sep 17 00:00:00 2001 From: Garth Braithwaite Date: Tue, 10 Feb 2026 11:20:00 -0700 Subject: [PATCH 06/15] test(mcp): add code coverage with c8 Add c8 code coverage instrumentation to spectrum-design-data-mcp to match other tools in the monorepo. Initial coverage report shows 77.33% overall coverage with excellent coverage (99%+) in the refactored utils modules. - Add c8 ^10.1.3 as devDependency - Update test script from "ava" to "c8 ava" - Coverage directory already ignored via root .gitignore Co-authored-by: Cursor --- pnpm-lock.yaml | 3 +++ tools/spectrum-design-data-mcp/package.json | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1203d46e..9b2e359a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -608,6 +608,9 @@ importers: ava: specifier: ^6.0.1 version: 6.4.0(@ava/typescript@6.0.0)(rollup@4.44.1) + c8: + specifier: ^10.1.3 + version: 10.1.3 tools/spectrum-diff-core: dependencies: diff --git a/tools/spectrum-design-data-mcp/package.json b/tools/spectrum-design-data-mcp/package.json index 418d4eac..8ac952c9 100644 --- a/tools/spectrum-design-data-mcp/package.json +++ b/tools/spectrum-design-data-mcp/package.json @@ -26,7 +26,7 @@ "LICENSE" ], "scripts": { - "test": "ava" + "test": "c8 ava" }, "engines": { "node": ">=20.12.0", @@ -56,6 +56,7 @@ "commander": "^13.1.0" }, "devDependencies": { - "ava": "^6.0.1" + "ava": "^6.0.1", + "c8": "^10.1.3" } } From a2f37475297621177bab2e00e7d73097584bbe31 Mon Sep 17 00:00:00 2001 From: Garth Braithwaite Date: Wed, 11 Feb 2026 13:50:00 -0700 Subject: [PATCH 07/15] feat(mcp): add implementation mapping tools and refactor workflows - Add token-to-implementation mapping tools (resolve, reverse-lookup, list) - Add react-spectrum-token-map.json with 109+ token mappings (PoC) - Add comprehensive test suite for implementation map tools - Refactor release workflows to use reusable publish-packages workflow - Update MCP config to remove unused doc servers - Add devEngines to package.json for proto v0.55.0+ compatibility Co-authored-by: Cursor --- .github/workflows/publish-packages.yml | 77 ++++++++ .github/workflows/release-snapshot.yml | 52 ++--- .github/workflows/release.yml | 37 +--- package.json | 6 +- tools/spectrum-design-data-mcp/README.md | 10 + .../data/react-spectrum-token-map.json | 158 ++++++++++++++++ .../src/data/react-spectrum-map.js | 73 +++++++ tools/spectrum-design-data-mcp/src/index.js | 9 +- .../src/tools/implementation-map.js | 179 ++++++++++++++++++ .../test/tools/implementation-map.test.js | 177 +++++++++++++++++ 10 files changed, 707 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/publish-packages.yml create mode 100644 tools/spectrum-design-data-mcp/data/react-spectrum-token-map.json create mode 100644 tools/spectrum-design-data-mcp/src/data/react-spectrum-map.js create mode 100644 tools/spectrum-design-data-mcp/src/tools/implementation-map.js create mode 100644 tools/spectrum-design-data-mcp/test/tools/implementation-map.test.js diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 00000000..f325d9ef --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,77 @@ +name: Publish Packages (Reusable) + +on: + workflow_call: + inputs: + snapshot-tag: + description: "Optional snapshot tag for prerelease versions" + required: false + type: string + secrets: + GH_TOKEN: + required: true + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write # Required for npm trusted publishing (OIDC) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get last author info + id: author + run: | + echo "authorName=$(git log -1 --pretty=format:'%an')" >> $GITHUB_OUTPUT + echo "authorEmail=$(git log -1 --pretty=format:'%ae')" >> $GITHUB_OUTPUT + + - uses: moonrepo/setup-toolchain@v0 + with: + auto-install: true + + - run: moon setup + - run: moon run :build --query "projectSource~packages/*" + + # Validate OIDC prerequisites before publishing + - name: Validate Publishing Prerequisites + uses: GarthDB/changesets-publish-validator@v1 + with: + auth-method: oidc + + # Install npm CLI with OIDC support for snapshot releases + - name: Install npm with OIDC support + if: inputs.snapshot-tag != '' + run: npm install -g npm@11.6.2 + + # Snapshot release + - name: Snapshot release + if: inputs.snapshot-tag != '' + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + SNAPSHOT_TAG: ${{ inputs.snapshot-tag }} + USERNAME: ${{ steps.author.outputs.authorName }} + EMAIL: ${{ steps.author.outputs.authorEmail }} + run: | + pnpm changeset version --snapshot $SNAPSHOT_TAG + git config --global user.name "$USERNAME" + git config --global user.email "$EMAIL" + git add . + git commit -m "chore: snapshot release $SNAPSHOT_TAG" + pnpm changeset publish --tag $SNAPSHOT_TAG + git push origin HEAD + git push --tags + + # Standard release + - name: Create Release Pull Request or Publish to npm + if: inputs.snapshot-tag == '' + uses: GarthDB/changesets-action@v1.6.8 + with: + commit: "chore: release" + publish: pnpm release + oidcAuth: true + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index 13c219ef..099ad86a 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -11,42 +11,24 @@ on: jobs: get-snapshot-tag: runs-on: ubuntu-latest - permissions: - contents: write - id-token: write + outputs: + snapshot-tag: ${{ steps.compute-tag.outputs.tag }} steps: - - name: Split branch name - id: split + - name: Compute snapshot tag + id: compute-tag env: BRANCH: ${{ github.ref_name }} - run: echo "fragment=${BRANCH##*snapshot-}" >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Get last author info - id: author run: | - echo "authorName=$(git log -1 --pretty=format:'%an')" >> $GITHUB_OUTPUT - echo "authorEmail=$(git log -1 --pretty=format:'%ae')" >> $GITHUB_OUTPUT - - uses: moonrepo/setup-toolchain@v0 - with: - auto-install: true - - run: moon setup - - run: moon run :build --query "projectSource~packages/*" - - name: Snapshot release - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - SNAPSHOT_TAG: ${{ inputs.tag || steps.split.outputs.fragment }} - USERNAME: ${{ steps.author.outputs.authorName }} - EMAIL: ${{ steps.author.outputs.authorEmail }} - run: | - pnpm changeset version --snapshot $SNAPSHOT_TAG - git config --global user.name "$USERNAME" - git config --global user.email "$EMAIL" - git add . - git commit -m "chore: snapshot release $SNAPSHOT_TAG" - npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN - pnpm changeset publish --tag $SNAPSHOT_TAG - git push origin HEAD - git push --tags + if [ -n "${{ inputs.tag }}" ]; then + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "tag=${BRANCH##*snapshot-}" >> $GITHUB_OUTPUT + fi + + publish: + needs: get-snapshot-tag + uses: ./.github/workflows/publish-packages.yml + with: + snapshot-tag: ${{ needs.get-snapshot-tag.outputs.snapshot-tag }} + secrets: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b327ed4a..3f788883 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,37 +10,6 @@ concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: release: - name: Release - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write # Required for npm trusted publishing (OIDC) - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - # Set up Node.js without proto to test OIDC compatibility - - uses: actions/setup-node@v4 - with: - node-version: "20.17.0" - # Install npm 11.6.2 (required for OIDC) - bypassing proto - - run: npm install -g npm@11.6.2 - # Install pnpm directly - - run: npm install -g pnpm@10.17.1 - # Install dependencies - - run: pnpm install --frozen-lockfile - # Build packages directly (bypassing moon - only tokens package has build tasks) - - name: Build tokens package - run: | - cd packages/tokens - node tasks/buildSpectrumTokens.js - node tasks/buildManifest.js - - name: Create Release Pull Request or Publish to npm - id: changesets - uses: GarthDB/changesets-action@v1.6.8 - with: - commit: "chore: release" - publish: pnpm release - oidcAuth: true # Test OIDC without proto shims - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + uses: ./.github/workflows/publish-packages.yml + secrets: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/package.json b/package.json index 2d9bf0bc..3ce085bb 100644 --- a/package.json +++ b/package.json @@ -44,5 +44,9 @@ "engines": { "node": "20.17.0" }, - "packageManager": "pnpm@10.17.1" + "packageManager": "pnpm@10.17.1", + "devEngines": { + "runtime": "20.17.0", + "packageManager": "pnpm@10.17.1" + } } diff --git a/tools/spectrum-design-data-mcp/README.md b/tools/spectrum-design-data-mcp/README.md index c5bffce5..0e3d4d99 100644 --- a/tools/spectrum-design-data-mcp/README.md +++ b/tools/spectrum-design-data-mcp/README.md @@ -71,6 +71,16 @@ The server runs locally and communicates via stdio with MCP-compatible AI client * **`build-component-config`**: Generate a complete component configuration with recommended tokens and props * **`suggest-component-improvements`**: Analyze existing component configuration and suggest improvements +#### Implementation Map Tools (PoC) + +Token-to-implementation mapping for translating Spectrum design tokens into platform-specific style APIs (e.g. React Spectrum S2 style macro). See [RFC: Design Token Sourcemaps and Traceability](https://github.com/adobe/spectrum-design-data/discussions/626). + +* **`resolve-implementation`**: Resolve a Spectrum token name to the equivalent style macro property and value for a platform (e.g. `accent-background-color-default` → `backgroundColor: 'accent'` in React Spectrum) +* **`reverse-lookup-implementation`**: Find Spectrum token name(s) that map to a given platform style macro property and value +* **`list-implementation-mappings`**: List token names that have a known mapping for a platform (useful to see PoC coverage) + +Supported platform: **react-spectrum**. Mapping data: `data/react-spectrum-token-map.json`. + ## Agent Skills Agent Skills are markdown guides that help AI agents use the Spectrum Design Data MCP tools effectively. They orchestrate multiple MCP tools into complete workflows for common design system tasks. diff --git a/tools/spectrum-design-data-mcp/data/react-spectrum-token-map.json b/tools/spectrum-design-data-mcp/data/react-spectrum-token-map.json new file mode 100644 index 00000000..83b96175 --- /dev/null +++ b/tools/spectrum-design-data-mcp/data/react-spectrum-token-map.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://opensource.adobe.com/spectrum-design-data/token-sourcemap-poc", + "version": 1, + "platform": "react-spectrum", + "description": "Proof-of-concept mapping from @adobe/spectrum-tokens to React Spectrum (S2) style macro. Extracted from @react-spectrum/s2 style/tokens.ts and style/spectrum-theme.ts.", + "mappings": { + "accent-content-color-default": { + "styleMacro": { "property": "color", "value": "accent" } + }, + "neutral-content-color-default": { + "styleMacro": { "property": "color", "value": "neutral" } + }, + "neutral-subdued-content-color-default": { + "styleMacro": { "property": "color", "value": "neutral-subdued" } + }, + "accent-background-color-default": { + "styleMacro": { "property": "backgroundColor", "value": "accent" } + }, + "accent-subtle-background-color-default": { + "styleMacro": { "property": "backgroundColor", "value": "accent-subtle" } + }, + "neutral-background-color-default": { + "styleMacro": { "property": "backgroundColor", "value": "neutral" } + }, + "neutral-subdued-background-color-default": { + "styleMacro": { + "property": "backgroundColor", + "value": "neutral-subdued" + } + }, + "neutral-subtle-background-color-default": { + "styleMacro": { "property": "backgroundColor", "value": "neutral-subtle" } + }, + "informative-background-color-default": { + "styleMacro": { "property": "backgroundColor", "value": "informative" } + }, + "informative-subtle-background-color-default": { + "styleMacro": { + "property": "backgroundColor", + "value": "informative-subtle" + } + }, + "background-base-color": { + "styleMacro": { "property": "backgroundColor", "value": "base" } + }, + "background-layer-1-color": { + "styleMacro": { "property": "backgroundColor", "value": "layer-1" } + }, + "background-layer-2-color": { + "styleMacro": { "property": "backgroundColor", "value": "layer-2" } + }, + "background-pasteboard-color": { + "styleMacro": { "property": "backgroundColor", "value": "pasteboard" } + }, + "background-elevated-color": { + "styleMacro": { "property": "backgroundColor", "value": "elevated" } + }, + "disabled-background-color": { + "styleMacro": { "property": "backgroundColor", "value": "disabled" } + }, + "focus-indicator-color": { + "styleMacro": { "property": "outlineColor", "value": "focus-ring" } + }, + "negative-border-color-default": { + "styleMacro": { "property": "borderColor", "value": "negative" } + }, + "disabled-border-color": { + "styleMacro": { "property": "borderColor", "value": "disabled" } + }, + "accent-visual-color": { + "styleMacro": { "property": "fill", "value": "accent" } + }, + "neutral-visual-color": { + "styleMacro": { "property": "fill", "value": "neutral" } + }, + "informative-visual-color": { + "styleMacro": { "property": "fill", "value": "informative" } + }, + "negative-visual-color": { + "styleMacro": { "property": "fill", "value": "negative" } + }, + "positive-visual-color": { + "styleMacro": { "property": "fill", "value": "positive" } + }, + "notice-visual-color": { + "styleMacro": { "property": "fill", "value": "notice" } + }, + "font-size-50": { + "styleMacro": { "property": "fontSize", "value": "ui-xs" } + }, + "font-size-75": { + "styleMacro": { "property": "fontSize", "value": "ui-sm" } + }, + "font-size-100": { + "styleMacro": { "property": "fontSize", "value": "ui" } + }, + "font-size-200": { + "styleMacro": { "property": "fontSize", "value": "ui-lg" } + }, + "font-size-300": { + "styleMacro": { "property": "fontSize", "value": "ui-xl" } + }, + "font-size-400": { + "styleMacro": { "property": "fontSize", "value": "ui-2xl" } + }, + "font-size-500": { + "styleMacro": { "property": "fontSize", "value": "ui-3xl" } + }, + "heading-size-xxs": { + "styleMacro": { "property": "fontSize", "value": "heading-2xs" } + }, + "heading-size-xs": { + "styleMacro": { "property": "fontSize", "value": "heading-xs" } + }, + "heading-size-s": { + "styleMacro": { "property": "fontSize", "value": "heading-sm" } + }, + "heading-size-m": { + "styleMacro": { "property": "fontSize", "value": "heading" } + }, + "heading-size-l": { + "styleMacro": { "property": "fontSize", "value": "heading-lg" } + }, + "heading-size-xl": { + "styleMacro": { "property": "fontSize", "value": "heading-xl" } + }, + "heading-size-xxl": { + "styleMacro": { "property": "fontSize", "value": "heading-2xl" } + }, + "heading-size-xxxl": { + "styleMacro": { "property": "fontSize", "value": "heading-3xl" } + }, + "body-size-xs": { + "styleMacro": { "property": "fontSize", "value": "body-xs" } + }, + "body-size-s": { + "styleMacro": { "property": "fontSize", "value": "body-sm" } + }, + "body-size-m": { + "styleMacro": { "property": "fontSize", "value": "body" } + }, + "body-size-l": { + "styleMacro": { "property": "fontSize", "value": "body-lg" } + }, + "body-size-xl": { + "styleMacro": { "property": "fontSize", "value": "body-xl" } + }, + "detail-size-s": { + "styleMacro": { "property": "fontSize", "value": "detail-sm" } + }, + "detail-size-m": { + "styleMacro": { "property": "fontSize", "value": "detail" } + }, + "detail-size-l": { + "styleMacro": { "property": "fontSize", "value": "detail-lg" } + } + } +} diff --git a/tools/spectrum-design-data-mcp/src/data/react-spectrum-map.js b/tools/spectrum-design-data-mcp/src/data/react-spectrum-map.js new file mode 100644 index 00000000..c46f725a --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/data/react-spectrum-map.js @@ -0,0 +1,73 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use it except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +let mapCache = null; + +/** + * Load the React Spectrum token-to-style-macro mapping (PoC). + * @returns {{ version: number, platform: string, mappings: Record }} + */ +export function loadReactSpectrumMap() { + if (mapCache) return mapCache; + const path = join(__dirname, "../../data/react-spectrum-token-map.json"); + const raw = readFileSync(path, "utf-8"); + mapCache = JSON.parse(raw); + return mapCache; +} + +/** + * Resolve a Spectrum token name to React Spectrum style macro property and value. + * @param {string} tokenName - Token name (e.g. "accent-background-color-default", "font-size-100") + * @returns {{ property: string, value: string } | null} + */ +export function resolveTokenToReactSpectrum(tokenName) { + if (!tokenName || typeof tokenName !== "string") return null; + const normalized = String(tokenName).trim(); + if (!normalized) return null; + const { mappings } = loadReactSpectrumMap(); + const entry = mappings[normalized]; + return entry?.styleMacro ?? null; +} + +/** + * Reverse lookup: find Spectrum token name(s) that map to the given style macro property and value. + * @param {string} property - Style macro property (e.g. "backgroundColor", "fontSize") + * @param {string} value - Style macro value (e.g. "accent", "ui") + * @returns {string[]} + */ +export function reverseLookupReactSpectrum(property, value) { + if (!property || !value) return []; + const { mappings } = loadReactSpectrumMap(); + const tokenNames = []; + for (const [tokenName, entry] of Object.entries(mappings)) { + const sm = entry?.styleMacro; + if (sm && sm.property === property && sm.value === value) { + tokenNames.push(tokenName); + } + } + return tokenNames; +} + +/** + * List all token names that have a React Spectrum mapping. + * @returns {string[]} + */ +export function listMappedTokenNames() { + const { mappings } = loadReactSpectrumMap(); + return Object.keys(mappings); +} diff --git a/tools/spectrum-design-data-mcp/src/index.js b/tools/spectrum-design-data-mcp/src/index.js index 5563086d..fbabe2b7 100644 --- a/tools/spectrum-design-data-mcp/src/index.js +++ b/tools/spectrum-design-data-mcp/src/index.js @@ -20,6 +20,7 @@ import { import { createTokenTools } from "./tools/tokens.js"; import { createSchemaTools } from "./tools/schemas.js"; import { createWorkflowTools } from "./tools/workflows.js"; +import { createImplementationMapTools } from "./tools/implementation-map.js"; /** * Create and configure the Spectrum Design Data MCP server @@ -43,6 +44,7 @@ export function createMCPServer() { ...createTokenTools(), ...createSchemaTools(), ...createWorkflowTools(), + ...createImplementationMapTools(), ]; // Register list_tools handler @@ -96,4 +98,9 @@ export async function startServer() { } // Export for testing -export { createTokenTools, createSchemaTools, createWorkflowTools }; +export { + createTokenTools, + createSchemaTools, + createWorkflowTools, + createImplementationMapTools, +}; diff --git a/tools/spectrum-design-data-mcp/src/tools/implementation-map.js b/tools/spectrum-design-data-mcp/src/tools/implementation-map.js new file mode 100644 index 00000000..1be6d52a --- /dev/null +++ b/tools/spectrum-design-data-mcp/src/tools/implementation-map.js @@ -0,0 +1,179 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use it except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { + loadReactSpectrumMap, + resolveTokenToReactSpectrum, + reverseLookupReactSpectrum, + listMappedTokenNames, +} from "../data/react-spectrum-map.js"; + +const SUPPORTED_PLATFORMS = ["react-spectrum"]; + +/** + * Create implementation-mapping MCP tools (token → platform style macro). + * @returns {Array<{ name: string, description: string, inputSchema: object, handler: Function }>} + */ +export function createImplementationMapTools() { + return [ + { + name: "resolve-implementation", + description: + "Resolve a Spectrum token name to the equivalent style macro property and value for a given platform (e.g. React Spectrum). Use when you need to know what to use in code for a given design token.", + inputSchema: { + type: "object", + properties: { + platform: { + type: "string", + description: `Target platform. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + enum: SUPPORTED_PLATFORMS, + }, + tokenName: { + type: "string", + description: + "Spectrum token name (e.g. accent-background-color-default, font-size-100)", + }, + }, + required: ["platform", "tokenName"], + }, + handler: async (args) => { + const platform = args?.platform; + const tokenName = args?.tokenName; + if (!platform || !tokenName) { + return { + ok: false, + error: "platform and tokenName are required", + }; + } + if (platform !== "react-spectrum") { + return { + ok: false, + error: `Unsupported platform: ${platform}. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + }; + } + const styleMacro = resolveTokenToReactSpectrum( + String(tokenName).trim(), + ); + if (!styleMacro) { + return { + ok: false, + platform: "react-spectrum", + tokenName: String(tokenName).trim(), + message: + "No mapping found for this token. It may not be used in React Spectrum style macro or is not yet in the PoC map.", + }; + } + return { + ok: true, + platform: "react-spectrum", + tokenName: String(tokenName).trim(), + styleMacro: { + property: styleMacro.property, + value: styleMacro.value, + }, + usage: `In React Spectrum style macro, set ${styleMacro.property}: '${styleMacro.value}'`, + }; + }, + }, + { + name: "reverse-lookup-implementation", + description: + "Find Spectrum token name(s) that map to a given platform style macro property and value. Use when you have a React Spectrum style value and want to know the source token.", + inputSchema: { + type: "object", + properties: { + platform: { + type: "string", + description: `Platform. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + enum: SUPPORTED_PLATFORMS, + }, + property: { + type: "string", + description: + "Style macro property (e.g. backgroundColor, fontSize, outlineColor)", + }, + value: { + type: "string", + description: "Style macro value (e.g. accent, ui, focus-ring)", + }, + }, + required: ["platform", "property", "value"], + }, + handler: async (args) => { + const platform = args?.platform; + const property = args?.property; + const value = args?.value; + if (!platform || !property || !value) { + return { + ok: false, + error: "platform, property, and value are required", + }; + } + if (platform !== "react-spectrum") { + return { + ok: false, + error: `Unsupported platform: ${platform}. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + }; + } + const tokenNames = reverseLookupReactSpectrum( + String(property).trim(), + String(value).trim(), + ); + return { + ok: true, + platform: "react-spectrum", + property: String(property).trim(), + value: String(value).trim(), + tokenNames, + message: + tokenNames.length === 0 + ? "No tokens in the PoC map match this style macro." + : undefined, + }; + }, + }, + { + name: "list-implementation-mappings", + description: + "List token names that have a known mapping to a given platform (e.g. React Spectrum). Useful to see what is covered by the PoC map.", + inputSchema: { + type: "object", + properties: { + platform: { + type: "string", + description: `Platform. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + enum: SUPPORTED_PLATFORMS, + }, + }, + required: ["platform"], + }, + handler: async (args) => { + const platform = args?.platform; + if (!platform || platform !== "react-spectrum") { + return { + ok: false, + error: `Unsupported platform: ${platform ?? "missing"}. Supported: ${SUPPORTED_PLATFORMS.join(", ")}`, + }; + } + const { version, mappings } = loadReactSpectrumMap(); + const tokenNames = listMappedTokenNames(); + return { + ok: true, + platform: "react-spectrum", + version, + count: tokenNames.length, + tokenNames, + }; + }, + }, + ]; +} diff --git a/tools/spectrum-design-data-mcp/test/tools/implementation-map.test.js b/tools/spectrum-design-data-mcp/test/tools/implementation-map.test.js new file mode 100644 index 00000000..6646ba6a --- /dev/null +++ b/tools/spectrum-design-data-mcp/test/tools/implementation-map.test.js @@ -0,0 +1,177 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use it except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import test from "ava"; +import { createImplementationMapTools } from "../../src/tools/implementation-map.js"; + +test("createImplementationMapTools returns array of tools", (t) => { + const tools = createImplementationMapTools(); + t.true(Array.isArray(tools)); + t.true(tools.length >= 3); +}); + +test("implementation map tools have required properties", (t) => { + const tools = createImplementationMapTools(); + + for (const tool of tools) { + t.is(typeof tool.name, "string"); + t.is(typeof tool.description, "string"); + t.is(typeof tool.inputSchema, "object"); + t.is(typeof tool.handler, "function"); + } +}); + +test("resolve-implementation tool exists", (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + t.truthy(resolve); + t.true(resolve.inputSchema.required.includes("platform")); + t.true(resolve.inputSchema.required.includes("tokenName")); +}); + +test("resolve-implementation returns style macro for known token", async (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + + const result = await resolve.handler({ + platform: "react-spectrum", + tokenName: "accent-background-color-default", + }); + + t.true(result.ok); + t.is(result.platform, "react-spectrum"); + t.is(result.tokenName, "accent-background-color-default"); + t.deepEqual(result.styleMacro, { + property: "backgroundColor", + value: "accent", + }); + t.true(result.usage.includes("backgroundColor")); + t.true(result.usage.includes("accent")); +}); + +test("resolve-implementation returns style macro for font-size token", async (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + + const result = await resolve.handler({ + platform: "react-spectrum", + tokenName: "font-size-100", + }); + + t.true(result.ok); + t.deepEqual(result.styleMacro, { property: "fontSize", value: "ui" }); +}); + +test("resolve-implementation returns error for unknown token", async (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + + const result = await resolve.handler({ + platform: "react-spectrum", + tokenName: "nonexistent-token-name", + }); + + t.false(result.ok); + t.truthy(result.message); +}); + +test("resolve-implementation returns error for missing params", async (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + + const result = await resolve.handler({ platform: "react-spectrum" }); + t.false(result.ok); + t.truthy(result.error); +}); + +test("resolve-implementation returns error for unsupported platform", async (t) => { + const tools = createImplementationMapTools(); + const resolve = tools.find((tool) => tool.name === "resolve-implementation"); + + const result = await resolve.handler({ + platform: "ios", + tokenName: "accent-background-color-default", + }); + + t.false(result.ok); + t.true(result.error.includes("Unsupported platform")); +}); + +test("reverse-lookup-implementation tool exists", (t) => { + const tools = createImplementationMapTools(); + const reverse = tools.find( + (tool) => tool.name === "reverse-lookup-implementation", + ); + t.truthy(reverse); + t.true(reverse.inputSchema.required.includes("platform")); + t.true(reverse.inputSchema.required.includes("property")); + t.true(reverse.inputSchema.required.includes("value")); +}); + +test("reverse-lookup-implementation finds token for style macro", async (t) => { + const tools = createImplementationMapTools(); + const reverse = tools.find( + (tool) => tool.name === "reverse-lookup-implementation", + ); + + const result = await reverse.handler({ + platform: "react-spectrum", + property: "backgroundColor", + value: "accent", + }); + + t.true(result.ok); + t.is(result.property, "backgroundColor"); + t.is(result.value, "accent"); + t.true(result.tokenNames.includes("accent-background-color-default")); +}); + +test("reverse-lookup-implementation returns empty for unknown style value", async (t) => { + const tools = createImplementationMapTools(); + const reverse = tools.find( + (tool) => tool.name === "reverse-lookup-implementation", + ); + + const result = await reverse.handler({ + platform: "react-spectrum", + property: "backgroundColor", + value: "nonexistent-value", + }); + + t.true(result.ok); + t.is(result.tokenNames.length, 0); +}); + +test("list-implementation-mappings tool exists", (t) => { + const tools = createImplementationMapTools(); + const list = tools.find( + (tool) => tool.name === "list-implementation-mappings", + ); + t.truthy(list); + t.true(list.inputSchema.required.includes("platform")); +}); + +test("list-implementation-mappings returns token names for react-spectrum", async (t) => { + const tools = createImplementationMapTools(); + const list = tools.find( + (tool) => tool.name === "list-implementation-mappings", + ); + + const result = await list.handler({ platform: "react-spectrum" }); + + t.true(result.ok); + t.is(result.platform, "react-spectrum"); + t.true(Array.isArray(result.tokenNames)); + t.true(result.count > 0); + t.true(result.tokenNames.includes("accent-background-color-default")); + t.true(result.tokenNames.includes("font-size-100")); +}); From e6c16340db0b99c20b20604bbf272bfcecd95090 Mon Sep 17 00:00:00 2001 From: Garth Braithwaite Date: Wed, 11 Feb 2026 20:51:39 -0700 Subject: [PATCH 08/15] fix(mcp): correct copyright year from 2026 to 2024 Updates copyright headers in newly added files from 2026 to 2024 to match the actual year of creation. Files updated: - src/config/intent-mappings.js - src/constants.js - src/utils/component-helpers.js - src/utils/token-helpers.js - src/utils/validation.js - test/utils/component-helpers.test.js - test/utils/token-helpers.test.js - test/utils/validation.test.js All tests continue to pass (93/93). Co-authored-by: Cursor --- tools/spectrum-design-data-mcp/src/config/intent-mappings.js | 2 +- tools/spectrum-design-data-mcp/src/constants.js | 2 +- tools/spectrum-design-data-mcp/src/utils/component-helpers.js | 2 +- tools/spectrum-design-data-mcp/src/utils/token-helpers.js | 2 +- tools/spectrum-design-data-mcp/src/utils/validation.js | 2 +- .../test/utils/component-helpers.test.js | 2 +- tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js | 2 +- tools/spectrum-design-data-mcp/test/utils/validation.test.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/spectrum-design-data-mcp/src/config/intent-mappings.js b/tools/spectrum-design-data-mcp/src/config/intent-mappings.js index bbb6efd7..3ae4ef54 100644 --- a/tools/spectrum-design-data-mcp/src/config/intent-mappings.js +++ b/tools/spectrum-design-data-mcp/src/config/intent-mappings.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/src/constants.js b/tools/spectrum-design-data-mcp/src/constants.js index bd46c868..5ea713b2 100644 --- a/tools/spectrum-design-data-mcp/src/constants.js +++ b/tools/spectrum-design-data-mcp/src/constants.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/src/utils/component-helpers.js b/tools/spectrum-design-data-mcp/src/utils/component-helpers.js index c1285602..1cc2ef40 100644 --- a/tools/spectrum-design-data-mcp/src/utils/component-helpers.js +++ b/tools/spectrum-design-data-mcp/src/utils/component-helpers.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/src/utils/token-helpers.js b/tools/spectrum-design-data-mcp/src/utils/token-helpers.js index 3df9102b..34f887e2 100644 --- a/tools/spectrum-design-data-mcp/src/utils/token-helpers.js +++ b/tools/spectrum-design-data-mcp/src/utils/token-helpers.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/src/utils/validation.js b/tools/spectrum-design-data-mcp/src/utils/validation.js index 00eda8dc..d4960dc5 100644 --- a/tools/spectrum-design-data-mcp/src/utils/validation.js +++ b/tools/spectrum-design-data-mcp/src/utils/validation.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js b/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js index bdb1cc76..c886a38c 100644 --- a/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js +++ b/tools/spectrum-design-data-mcp/test/utils/component-helpers.test.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js b/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js index 41d58690..5340c25f 100644 --- a/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js +++ b/tools/spectrum-design-data-mcp/test/utils/token-helpers.test.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/tools/spectrum-design-data-mcp/test/utils/validation.test.js b/tools/spectrum-design-data-mcp/test/utils/validation.test.js index 252e928e..6d5ddd45 100644 --- a/tools/spectrum-design-data-mcp/test/utils/validation.test.js +++ b/tools/spectrum-design-data-mcp/test/utils/validation.test.js @@ -1,5 +1,5 @@ /* -Copyright 2026 Adobe. All rights reserved. +Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 From b49da40f2e3f254d3ff840b59397f1da55628fb9 Mon Sep 17 00:00:00 2001 From: Garth Braithwaite Date: Thu, 12 Feb 2026 11:38:56 -0700 Subject: [PATCH 09/15] docs(mcp): agent-skills folder rename, docs, and error messages Co-authored-by: Cursor --- .changeset/agent-skills-enhancements.md | 5 + tools/spectrum-design-data-mcp/README.md | 70 +++++- .../README.md | 81 +++++++ .../SKILL.md | 0 .../component-builder.md | 76 +++++++ .../agent-skills/guides/state-management.md | 214 ++++++++++++++++++ .../token-finder.md | 0 .../src/tools/schemas.js | 26 ++- .../src/tools/tokens.js | 34 ++- .../src/tools/workflows.js | 4 +- .../src/utils/validation.js | 23 +- 11 files changed, 515 insertions(+), 18 deletions(-) create mode 100644 .changeset/agent-skills-enhancements.md rename tools/spectrum-design-data-mcp/{build-spectrum-components => agent-skills}/README.md (74%) rename tools/spectrum-design-data-mcp/{build-spectrum-components => agent-skills}/SKILL.md (100%) rename tools/spectrum-design-data-mcp/{build-spectrum-components => agent-skills}/component-builder.md (75%) create mode 100644 tools/spectrum-design-data-mcp/agent-skills/guides/state-management.md rename tools/spectrum-design-data-mcp/{build-spectrum-components => agent-skills}/token-finder.md (100%) diff --git a/.changeset/agent-skills-enhancements.md b/.changeset/agent-skills-enhancements.md new file mode 100644 index 00000000..18ac76c7 --- /dev/null +++ b/.changeset/agent-skills-enhancements.md @@ -0,0 +1,5 @@ +--- +"@adobe/spectrum-design-data-mcp": patch +--- + +Improved Agent Skills with better folder naming (agent-skills/), comprehensive documentation including quick start examples, state management guide, accessibility checklist, and enhanced error messages with actionable next steps. diff --git a/tools/spectrum-design-data-mcp/README.md b/tools/spectrum-design-data-mcp/README.md index 0e3d4d99..6d1e5b20 100644 --- a/tools/spectrum-design-data-mcp/README.md +++ b/tools/spectrum-design-data-mcp/README.md @@ -87,8 +87,8 @@ Agent Skills are markdown guides that help AI agents use the Spectrum Design Dat ### Available Agent Skills -* **[Component Builder](build-spectrum-components/component-builder.md)**: Guides agents through building Spectrum components correctly by discovering schemas, finding tokens, and validating configurations -* **[Token Finder](build-spectrum-components/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling +* **[Component Builder](agent-skills/component-builder.md)**: Guides agents through building Spectrum components correctly by discovering schemas, finding tokens, and validating configurations +* **[Token Finder](agent-skills/token-finder.md)**: Helps agents discover the right design tokens for colors, spacing, typography, and component styling ### How Agent Skills Work @@ -110,12 +110,74 @@ For AI agents working with Spectrum components: 3. Call the MCP tools as directed by the skill 4. Combine tool outputs into a complete solution -See the [Agent Skills README](build-spectrum-components/README.md) for more details. +See the [Agent Skills README](agent-skills/README.md) for more details. ### Related Resources This implementation follows the pattern established by [React Spectrum's AI integration](https://react-spectrum.adobe.com/ai), which also uses MCP and Agent Skills to help AI agents work with design systems. +## Quick Start + +### Building a Component with Agent Skills + +The fastest way to build a Spectrum component is to use the workflow tools: + +#### One-Shot Component Configuration + +```javascript +// Generate complete config with recommended tokens +await buildComponentConfig({ + component: "action-button", + variant: "accent", + intent: "primary", + includeTokens: true, +}); +// Returns: complete config with props, tokens, and validation +``` + +#### Step-by-Step Workflow (following Component Builder skill) + +```javascript +// 1. Get component schema +const schema = await getComponentSchema({ component: "action-button" }); + +// 2. Find related tokens +const tokens = await getComponentTokens({ componentName: "action-button" }); + +// 3. Get color recommendations +const colors = await getDesignRecommendations({ + intent: "primary", + context: "button", +}); + +// 4. Validate configuration +const validation = await validateComponentProps({ + component: "action-button", + props: { variant: "accent", size: "m" }, +}); +``` + +### Finding Design Tokens + +```javascript +// Find tokens by use case +const bgTokens = await findTokensByUseCase({ + useCase: "button background", + componentType: "button", +}); + +// Get semantic color recommendations +const errorColors = await getDesignRecommendations({ + intent: "negative", + context: "text", +}); + +// Get all tokens for a component +const buttonTokens = await getComponentTokens({ + componentName: "action-button", +}); +``` + ## Configuration ### MCP Setup @@ -308,7 +370,7 @@ src/ ├── data/ # Data access layer │ ├── tokens.js # Token data access │ └── schemas.js # Schema data access -└── build-spectrum-components/ # Agent Skills documentation +└── agent-skills/ # Agent Skills documentation ├── component-builder.md ├── token-finder.md └── README.md diff --git a/tools/spectrum-design-data-mcp/build-spectrum-components/README.md b/tools/spectrum-design-data-mcp/agent-skills/README.md similarity index 74% rename from tools/spectrum-design-data-mcp/build-spectrum-components/README.md rename to tools/spectrum-design-data-mcp/agent-skills/README.md index ca724e6f..f32bfcc4 100644 --- a/tools/spectrum-design-data-mcp/build-spectrum-components/README.md +++ b/tools/spectrum-design-data-mcp/agent-skills/README.md @@ -39,6 +39,10 @@ Helps agents discover the right design tokens for: **Use when**: Finding tokens for design decisions or styling components +### Guides + +* **[State Management](guides/state-management.md)**: Handling component states (default, hover, focus, disabled, selected) and token recommendations per state + ## How Agent Skills Work Agent Skills don't execute code directly. Instead, they: @@ -85,6 +89,83 @@ Agent Skills work alongside the Spectrum Design Data MCP tools: ⭐ = Frequently used in Agent Skills +## Common Usage Patterns + +### Pattern: Multi-State Component + +When building components with multiple interactive states: + +```javascript +// 1. Get base recommendations +const baseTokens = await getDesignRecommendations({ + intent: "primary", + state: "default", + context: "button", +}); + +// 2. Get hover state +const hoverTokens = await getDesignRecommendations({ + intent: "primary", + state: "hover", + context: "button", +}); + +// 3. Get disabled state +const disabledTokens = await getDesignRecommendations({ + intent: "primary", + state: "disabled", + context: "button", +}); +``` + +### Pattern: Form Validation + +Building form fields with validation: + +```javascript +// 1. Get field schema +const schema = await getComponentSchema({ component: "text-field" }); + +// 2. Get error state tokens +const errorTokens = await findTokensByUseCase({ + useCase: "error state", + componentType: "input", +}); + +// 3. Validate configuration +const validation = await validateComponentProps({ + component: "text-field", + props: { + validationState: "invalid", + errorMessage: "Required field", + }, +}); +``` + +### Pattern: Semantic Color Selection + +Choosing colors based on intent: + +```javascript +// For success messages +const successColors = await getDesignRecommendations({ + intent: "positive", + context: "text", +}); + +// For warnings +const warningColors = await getDesignRecommendations({ + intent: "notice", + context: "background", +}); + +// For errors +const errorColors = await getDesignRecommendations({ + intent: "negative", + context: "border", +}); +``` + ## Using Agent Skills ### For AI Agents diff --git a/tools/spectrum-design-data-mcp/build-spectrum-components/SKILL.md b/tools/spectrum-design-data-mcp/agent-skills/SKILL.md similarity index 100% rename from tools/spectrum-design-data-mcp/build-spectrum-components/SKILL.md rename to tools/spectrum-design-data-mcp/agent-skills/SKILL.md diff --git a/tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md b/tools/spectrum-design-data-mcp/agent-skills/component-builder.md similarity index 75% rename from tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md rename to tools/spectrum-design-data-mcp/agent-skills/component-builder.md index 45efd54e..4f89f4a4 100644 --- a/tools/spectrum-design-data-mcp/build-spectrum-components/component-builder.md +++ b/tools/spectrum-design-data-mcp/agent-skills/component-builder.md @@ -168,6 +168,51 @@ Before finalizing, use `validate-component-props` to ensure correctness: 4. **Combine multiple tools**: Don't rely on a single tool - combine schema + tokens + recommendations 5. **Handle states**: For interactive components, consider all states (default, hover, focus, disabled) +## Common Validation Errors & Solutions + +### Missing Required Props + +**Error:** + +``` +Missing required property: label +``` + +**Solution:** + +* Check schema with `get-component-schema` to see required props +* Use `get-component-options` for user-friendly property list +* Add the required prop to your configuration + +### Invalid Enum Value + +**Error:** + +``` +Property variant value "primary" is not in allowed enum values +``` + +**Solution:** + +* Check available values: `get-component-schema` shows enum options +* Common fix: "primary" → "accent" for accent buttons +* Use `suggest-component-improvements` for recommendations + +### Unknown Property + +**Error:** + +``` +Unknown property: color +``` + +**Solution:** + +* Property might be named differently (e.g., `variant` instead of `color`) +* Check spelling with `get-component-options` +* Remove custom properties not in schema +* Use tokens through style props instead + ## Related Tools * `get-component-schema` - Get complete component API @@ -186,6 +231,37 @@ Before finalizing, use `validate-component-props` to ensure correctness: * **Navigation**: `breadcrumbs`, `tabs`, `menu`, `side-navigation` * **Feedback**: `alert-banner`, `toast`, `in-line-alert`, `status-light` +## Accessibility Checklist + +When building components, ensure: + +### Required Props + +* [ ] Check schema for required ARIA props (`aria-label`, `aria-labelledby`, etc.) +* [ ] Verify keyboard navigation support (focus props, tab order) +* [ ] Include proper role attributes where needed + +### States & Feedback + +* [ ] Disabled state has appropriate ARIA attributes +* [ ] Error states include `aria-invalid` and error message association +* [ ] Loading states use `aria-busy` or `aria-live` regions +* [ ] Selected/checked states properly indicated + +### Visual Design + +* [ ] Color contrast meets WCAG AA standards (use semantic tokens) +* [ ] Focus indicators are clearly visible +* [ ] Interactive elements have adequate touch targets (44x44px minimum) +* [ ] Text sizing follows typography tokens (minimum 16px for body) + +### Testing + +* [ ] Test with screen reader (VoiceOver, NVDA, JAWS) +* [ ] Verify keyboard-only navigation works +* [ ] Check color contrast with tools +* [ ] Validate against component schema with `validate-component-props` + ## Notes * Always check if a component exists using `list-components` before building diff --git a/tools/spectrum-design-data-mcp/agent-skills/guides/state-management.md b/tools/spectrum-design-data-mcp/agent-skills/guides/state-management.md new file mode 100644 index 00000000..74239f01 --- /dev/null +++ b/tools/spectrum-design-data-mcp/agent-skills/guides/state-management.md @@ -0,0 +1,214 @@ +# State Management Guide + +This guide helps AI agents handle component states correctly when building Spectrum components. Use `get-design-recommendations` with the `state` parameter to get token recommendations for each interactive state. + +## Component States + +Spectrum components support these common states: + +| State | Description | When to use | +| -------- | ------------------------------ | -------------------------------- | +| default | Resting state, no interaction | Initial render | +| hover | Pointer over the component | Mouse/touch hover | +| focus | Keyboard or programmatic focus | Focus ring, tab navigation | +| active | Pressed / in progress | Click/tap in progress, loading | +| disabled | Not interactive | `isDisabled: true` | +| selected | Toggle or selection state | Checkboxes, tabs, selected items | + +## Token Recommendations by State + +Use `get-design-recommendations` with the appropriate `state` and `context`: + +```json +{ + "intent": "primary", + "state": "default", + "context": "button" +} +``` + +### Buttons + +For action buttons and buttons, request tokens for each state: + +```json +// Default +{ "intent": "accent", "state": "default", "context": "button" } + +// Hover +{ "intent": "accent", "state": "hover", "context": "button" } + +// Focus (focus ring) +{ "intent": "accent", "state": "focus", "context": "button" } + +// Active / pressed +{ "intent": "accent", "state": "active", "context": "button" } + +// Disabled +{ "intent": "accent", "state": "disabled", "context": "button" } +``` + +### Inputs + +For text fields and other inputs: + +```json +// Default +{ "intent": "primary", "state": "default", "context": "input" } + +// Focus (focused field) +{ "intent": "primary", "state": "focus", "context": "input" } + +// Error / invalid +{ "intent": "negative", "state": "default", "context": "input" } + +// Disabled +{ "intent": "primary", "state": "disabled", "context": "input" } +``` + +### Selection Components + +For checkboxes, radio groups, tabs: + +```json +// Unselected +{ "intent": "primary", "state": "default", "context": "button" } + +// Selected +{ "intent": "primary", "state": "selected", "context": "button" } + +// Selected + hover +{ "intent": "primary", "state": "hover", "context": "button" } +``` + +## Interaction Patterns and State Transitions + +### Typical Button Flow + +1. **default** → user hovers → **hover** +2. **hover** → user presses → **active** +3. **active** → user releases → **hover** or **default** +4. **default** → user tabs to element → **focus** +5. **focus** → user blurs → **default** + +Always provide tokens for default, hover, focus, and disabled. Add active and selected when the component supports them. + +### Form Field Flow + +1. **default** → user focuses → **focus** +2. **focus** → validation fails → **default** with error styling (use intent `negative`) +3. **default** → field disabled → **disabled** + +### Best Practices for State Combinations + +1. **Cover all interactive states**: For buttons and links, include default, hover, focus, and disabled at minimum. +2. **Use semantic intents**: Use `intent: "negative"` for errors, `intent: "positive"` for success, `intent: "accent"` for primary actions. +3. **Consistent context**: Keep `context` aligned with the component type (button, input, text, background, border). +4. **Validate with schema**: After building stateful configs, use `validate-component-props` to ensure props like `isDisabled` and `variant` are valid. +5. **One recommendation call per state**: Call `get-design-recommendations` once per state you need; combine results into a single config object. + +## Examples + +### Example: Action Button with All States + +```javascript +// 1. Get recommendations for each state +const defaultTokens = await getDesignRecommendations({ + intent: "accent", + state: "default", + context: "button", +}); +const hoverTokens = await getDesignRecommendations({ + intent: "accent", + state: "hover", + context: "button", +}); +const focusTokens = await getDesignRecommendations({ + intent: "accent", + state: "focus", + context: "button", +}); +const disabledTokens = await getDesignRecommendations({ + intent: "accent", + state: "disabled", + context: "button", +}); + +// 2. Build config with schema props + state tokens +const config = { + component: "action-button", + props: { variant: "accent", size: "m" }, + states: { + default: defaultTokens, + hover: hoverTokens, + focus: focusTokens, + disabled: disabledTokens, + }, +}; + +// 3. Validate +await validateComponentProps({ + component: "action-button", + props: config.props, +}); +``` + +### Example: Text Field with Error State + +```javascript +// Default and focus +const defaultInput = await getDesignRecommendations({ + intent: "primary", + state: "default", + context: "input", +}); +const errorInput = await getDesignRecommendations({ + intent: "negative", + state: "default", + context: "input", +}); + +// Component supports validationState: "invalid" +const config = { + component: "text-field", + props: { + label: "Email", + validationState: "invalid", + errorMessage: "Please enter a valid email", + }, + tokens: { + default: defaultInput, + error: errorInput, + }, +}; +``` + +### Example: Card with Hover + +For containers like cards, use `context: "background"` or component-specific tokens: + +```javascript +const defaultCard = await getDesignRecommendations({ + intent: "secondary", + state: "default", + context: "background", +}); +const hoverCard = await getDesignRecommendations({ + intent: "secondary", + state: "hover", + context: "background", +}); +``` + +## Related Tools + +* `get-design-recommendations` – primary tool for state-based token recommendations +* `find-tokens-by-use-case` – e.g. "hover state", "disabled state", "error state" +* `get-component-tokens` – component-specific tokens that may include state variants +* `get-component-schema` – required props and enums (e.g. `isDisabled`, `validationState`) +* `validate-component-props` – validate final configuration + +## See Also + +* [Component Builder](../component-builder.md) – full workflow for building components +* [Token Finder](../token-finder.md) – discovering tokens by use case and intent diff --git a/tools/spectrum-design-data-mcp/build-spectrum-components/token-finder.md b/tools/spectrum-design-data-mcp/agent-skills/token-finder.md similarity index 100% rename from tools/spectrum-design-data-mcp/build-spectrum-components/token-finder.md rename to tools/spectrum-design-data-mcp/agent-skills/token-finder.md diff --git a/tools/spectrum-design-data-mcp/src/tools/schemas.js b/tools/spectrum-design-data-mcp/src/tools/schemas.js index 59a42310..67743b66 100644 --- a/tools/spectrum-design-data-mcp/src/tools/schemas.js +++ b/tools/spectrum-design-data-mcp/src/tools/schemas.js @@ -132,7 +132,11 @@ export function createSchemaTools() { : undefined; if (!schema || typeof schema !== "object") { - throw new Error(`Component schema not found: ${component}`); + throw new Error( + `Component schema not found: ${component}. ` + + "Use list-components to see all available components, or " + + "check https://spectrum.adobe.com/page/components for documentation.", + ); } return { @@ -214,7 +218,14 @@ export function createSchemaTools() { : undefined; if (!schema || typeof schema !== "object") { - throw new Error(`Component schema not found: ${component}`); + throw new Error( + `Component schema not found: ${component}. ` + + "This might mean: " + + "1. The component name is misspelled (use list-components to verify), " + + "2. The component doesn't have a schema yet, " + + "3. The component is from a different package. " + + "Try: get-component-options to explore available options.", + ); } const validationResults = validateProps(props, schema); @@ -250,7 +261,10 @@ export function createSchemaTools() { ? schemaData.types[`${type}.json`] : undefined; if (!typeSchema || typeof typeSchema !== "object") { - throw new Error(`Type schema not found: ${type}`); + throw new Error( + `Type schema not found: ${type}. ` + + "Use get-type-schemas to list all available type definitions.", + ); } return { @@ -391,7 +405,11 @@ export function createSchemaTools() { const feature = args?.feature != null ? String(args.feature) : undefined; if (!feature || feature.trim() === "") { - throw new Error("feature is required"); + throw new Error( + "feature is required for component search. " + + "Provide a feature to search for (e.g., 'validation', 'icon', 'selection'). " + + "Common features: validation, disabled, icon, selection, loading, error.", + ); } const schemaData = await getSchemaData(); diff --git a/tools/spectrum-design-data-mcp/src/tools/tokens.js b/tools/spectrum-design-data-mcp/src/tools/tokens.js index e1cbe622..d8462f7f 100644 --- a/tools/spectrum-design-data-mcp/src/tools/tokens.js +++ b/tools/spectrum-design-data-mcp/src/tools/tokens.js @@ -210,7 +210,11 @@ export function createTokenTools() { const category = validateStringParam(args?.category, "category"); if (!tokenPath || tokenPath.trim() === "") { - throw new Error("tokenPath is required"); + throw new Error( + "tokenPath is required to get token details. " + + "Provide the token name (e.g., 'accent-color-100'). " + + "Use query-tokens or find-tokens-by-use-case to discover token names.", + ); } const tokenData = await getTokenData(); @@ -236,7 +240,15 @@ export function createTokenTools() { } } - throw new Error(`Token not found: ${tokenPath}`); + throw new Error( + `Token not found: ${tokenPath}. ` + + "This might mean: " + + "1. The token name is misspelled, " + + "2. The token is in a different category than specified, " + + "3. The token has been deprecated or renamed. " + + "Try: query-tokens to search for similar tokens, or " + + "get-token-categories to see all available categories.", + ); }, }, { @@ -268,7 +280,11 @@ export function createTokenTools() { ); if (!useCase || useCase.trim() === "") { - throw new Error("useCase is required"); + throw new Error( + "useCase is required to find tokens. " + + "Describe what you're looking for (e.g., 'button background', 'error state', 'spacing'). " + + "Common use cases: button background, text color, border, spacing, error state, hover state.", + ); } const data = await getTokenData(); @@ -367,7 +383,11 @@ export function createTokenTools() { const componentName = args?.componentName != null ? String(args.componentName) : undefined; if (!componentName || componentName.trim() === "") { - throw new Error("componentName is required"); + throw new Error( + "componentName is required to get component tokens. " + + "Provide the component name (e.g., 'action-button', 'text-field'). " + + "Use list-components to see all available components.", + ); } const data = await getTokenData(); @@ -442,7 +462,11 @@ export function createTokenTools() { const context = validateStringParam(args?.context, "context"); if (!intent || intent.trim() === "") { - throw new Error("intent is required"); + throw new Error( + "intent is required for design recommendations. " + + "Provide a design intent (e.g., 'primary', 'negative', 'positive'). " + + "Common intents: primary, secondary, accent, negative, positive, notice, informative.", + ); } const data = await getTokenData(); diff --git a/tools/spectrum-design-data-mcp/src/tools/workflows.js b/tools/spectrum-design-data-mcp/src/tools/workflows.js index b758e926..b2628065 100644 --- a/tools/spectrum-design-data-mcp/src/tools/workflows.js +++ b/tools/spectrum-design-data-mcp/src/tools/workflows.js @@ -204,7 +204,9 @@ export function createWorkflowTools() { if (!schema || typeof schema !== "object") { throw new Error( - `Component not found: ${component}. Use list-components to see available components.`, + `Component not found: ${component}. ` + + "Use list-components to see available components. " + + "Or use build-component-config to generate a complete configuration from scratch.", ); } diff --git a/tools/spectrum-design-data-mcp/src/utils/validation.js b/tools/spectrum-design-data-mcp/src/utils/validation.js index d4960dc5..dd266c83 100644 --- a/tools/spectrum-design-data-mcp/src/utils/validation.js +++ b/tools/spectrum-design-data-mcp/src/utils/validation.js @@ -18,10 +18,17 @@ governing permissions and limitations under the License. */ export function validateComponentName(component) { if (!component || typeof component !== "string") { - throw new Error("Component name must be a non-empty string"); + throw new Error( + "Component name must be a non-empty string. " + + "Example: 'action-button', 'text-field', or 'card'. " + + "Use list-components to see all available components.", + ); } if (component.includes("/") || component.includes("\\")) { - throw new Error("Component name cannot contain path separators"); + throw new Error( + "Component name cannot contain path separators (/ or \\). " + + "Use the component name only, e.g., 'action-button' instead of 'components/action-button'.", + ); } return component.trim(); } @@ -49,7 +56,11 @@ export function validateLimit(limit, defaultLimit, maxLimit = 100) { */ export function validatePropsObject(props) { if (!props || typeof props !== "object" || Array.isArray(props)) { - throw new Error("Props must be a valid object"); + throw new Error( + "Props must be a valid object (not an array or null). " + + "Example: { variant: 'accent', size: 'm' }. " + + "Use get-component-schema to see available properties.", + ); } return /** @type {Record} */ (props); } @@ -63,7 +74,11 @@ export function validatePropsObject(props) { */ export function validateStringParam(param, paramName) { if (param !== undefined && param !== null && typeof param !== "string") { - throw new Error(`${paramName} must be a string`); + throw new Error( + `${paramName} must be a string. ` + + `Received: ${typeof param}. ` + + "Check your input and try again.", + ); } return param !== undefined && param !== null ? String(param) : undefined; } From 73907039014010a1d6685082982197eb67095c87 Mon Sep 17 00:00:00 2001 From: garthdb Date: Fri, 27 Feb 2026 12:58:44 -0700 Subject: [PATCH 10/15] fix(mcp): update test assertions for new tool counts after rebase Made-with: Cursor --- tools/spectrum-design-data-mcp/test/tools/schemas.test.js | 4 ++-- tools/spectrum-design-data-mcp/test/tools/tokens.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/spectrum-design-data-mcp/test/tools/schemas.test.js b/tools/spectrum-design-data-mcp/test/tools/schemas.test.js index c628e11c..238c7c42 100644 --- a/tools/spectrum-design-data-mcp/test/tools/schemas.test.js +++ b/tools/spectrum-design-data-mcp/test/tools/schemas.test.js @@ -13,10 +13,10 @@ governing permissions and limitations under the License. import test from "ava"; import { createSchemaTools } from "../../src/tools/schemas.js"; -test("createSchemaTools returns array of 4 tools", (t) => { +test("createSchemaTools returns array of 7 tools", (t) => { const tools = createSchemaTools(); t.true(Array.isArray(tools)); - t.is(tools.length, 4); + t.is(tools.length, 7); }); test("schema tools have required properties", (t) => { diff --git a/tools/spectrum-design-data-mcp/test/tools/tokens.test.js b/tools/spectrum-design-data-mcp/test/tools/tokens.test.js index 193e2bd2..b9145c67 100644 --- a/tools/spectrum-design-data-mcp/test/tools/tokens.test.js +++ b/tools/spectrum-design-data-mcp/test/tools/tokens.test.js @@ -13,10 +13,10 @@ governing permissions and limitations under the License. import test from "ava"; import { createTokenTools } from "../../src/tools/tokens.js"; -test("createTokenTools returns array of 4 tools", (t) => { +test("createTokenTools returns array of 7 tools", (t) => { const tools = createTokenTools(); t.true(Array.isArray(tools)); - t.is(tools.length, 4); + t.is(tools.length, 7); }); test("token tools have required properties", (t) => { From b3731eb8edfd891ca33519fee0bc228ddf378b1c Mon Sep 17 00:00:00 2001 From: garthdb Date: Thu, 26 Mar 2026 14:34:27 -0600 Subject: [PATCH 11/15] fix(ci): pin node 20.17.0 in .prototools and update ai.md for agent skills - Add node = "20.17.0" to .prototools so proto satisfies npm's node requirement (moonrepo/setup-toolchain@v0 now checks .prototools exclusively before reading .moon/toolchain.yml) - Update docs/site/src/pages/ai.md with full tool inventory for @adobe/spectrum-design-data-mcp (workflow, implementation, and expanded token/schema tools) and add Agent Skills section Co-Authored-By: Claude Sonnet 4.6 --- .prototools | 1 + docs/site/src/pages/ai.md | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.prototools b/.prototools index 0e208b6e..d1a2f06d 100644 --- a/.prototools +++ b/.prototools @@ -1,2 +1,3 @@ +node = "20.17.0" moon = "1.39.1" npm = "11.6.2" diff --git a/docs/site/src/pages/ai.md b/docs/site/src/pages/ai.md index 3498d231..7c7f918e 100644 --- a/docs/site/src/pages/ai.md +++ b/docs/site/src/pages/ai.md @@ -16,9 +16,13 @@ Design tokens and component API schemas. Enables AI to look up token values, fin **npm:** `@adobe/spectrum-design-data-mcp` -**Token tools:** `query-tokens`, `query-tokens-by-value`, `get-token-details`, `get-component-tokens` +**Token tools:** `query-tokens`, `query-tokens-by-value`, `get-token-details`, `get-token-categories`, `get-component-tokens`, `find-tokens-by-use-case`, `get-design-recommendations` -**Schema tools:** `list-components`, `get-component-schema`, `validate-component-props`, `search-components-by-feature` +**Schema tools:** `query-component-schemas`, `list-components`, `get-component-schema`, `get-component-options`, `get-type-schemas`, `validate-component-props`, `search-components-by-feature` + +**Workflow tools:** `build-component-config`, `suggest-component-improvements` + +**Implementation tools:** `resolve-implementation`, `reverse-lookup-implementation`, `list-implementation-mappings` **Cursor config (single server):** @@ -56,6 +60,24 @@ Spectrum 2 component documentation and design guidelines. Use when the AI needs If you run from source, point `args` to `tools/s2-docs-mcp/src/cli.js` inside your clone. +## Agent Skills + +Agent Skills are markdown guides that orchestrate MCP tools into complete workflows for common design system tasks. They don't execute code directly — instead they tell AI agents which tools to call, in what order, and how to combine the results. + +The top-level entry point is [`SKILL.md`](https://github.com/adobe/spectrum-design-data/blob/main/tools/spectrum-design-data-mcp/agent-skills/SKILL.md), which follows a standard skill metadata format recognized by MCP-compatible clients (Cursor, Claude Code, VS Code, etc.). MCP clients that support Agent Skills will auto-discover it from the server. + +### Available skills + +* **[Component Builder](https://github.com/adobe/spectrum-design-data/blob/main/tools/spectrum-design-data-mcp/agent-skills/component-builder.md)** — Guides agents through discovering component schemas, finding design tokens, and validating configurations. Use when building or implementing Spectrum components. + +* **[Token Finder](https://github.com/adobe/spectrum-design-data/blob/main/tools/spectrum-design-data-mcp/agent-skills/token-finder.md)** — Helps agents find the right tokens for colors, spacing, typography, and component styling. Use when asking "what token should I use for…" + +* **[State Management guide](https://github.com/adobe/spectrum-design-data/blob/main/tools/spectrum-design-data-mcp/agent-skills/guides/state-management.md)** — Covers token recommendations per interactive state (default, hover, focus, disabled, selected). + +### Using skills + +For AI agents, read the relevant skill file when a task matches its scope and follow the step-by-step workflow. For developers, reference skill files in your project docs or system prompts to steer agents toward correct Spectrum patterns. + ## Cursor IDE setup Add both servers to `.cursor/mcp.json` so the AI can use tokens, schemas, and S2 docs in the same session: From d3a19bbb61f7859dbdd83abf48158d36efebdd26 Mon Sep 17 00:00:00 2001 From: garthdb Date: Thu, 26 Mar 2026 14:38:22 -0600 Subject: [PATCH 12/15] fix(ci): add pnpm 10.17.1 to .prototools New cache key (from pinning node) was built without pnpm since it's only configured in .moon/toolchain.yml. Pin it explicitly so proto installs it in all workflows. Co-Authored-By: Claude Sonnet 4.6 --- .prototools | 1 + 1 file changed, 1 insertion(+) diff --git a/.prototools b/.prototools index d1a2f06d..004c40c3 100644 --- a/.prototools +++ b/.prototools @@ -1,3 +1,4 @@ node = "20.17.0" moon = "1.39.1" npm = "11.6.2" +pnpm = "10.17.1" From 4da64b2c860264b4fc3c47bf5234cf4c7faf596e Mon Sep 17 00:00:00 2001 From: garthdb Date: Thu, 26 Mar 2026 14:44:41 -0600 Subject: [PATCH 13/15] fix(changeset): wrap long line in agent-skills-enhancements.md Line exceeded 100 char limit; changeset linter runs with --fail-on-warnings. Co-Authored-By: Claude Sonnet 4.6 --- .changeset/agent-skills-enhancements.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/agent-skills-enhancements.md b/.changeset/agent-skills-enhancements.md index 18ac76c7..c4c46d67 100644 --- a/.changeset/agent-skills-enhancements.md +++ b/.changeset/agent-skills-enhancements.md @@ -2,4 +2,6 @@ "@adobe/spectrum-design-data-mcp": patch --- -Improved Agent Skills with better folder naming (agent-skills/), comprehensive documentation including quick start examples, state management guide, accessibility checklist, and enhanced error messages with actionable next steps. +Improved Agent Skills with better folder naming (`agent-skills/`), comprehensive documentation including +quick start examples, state management guide, accessibility checklist, and enhanced error messages with +actionable next steps. From 8cb0827a2287c2d489bceea60f93604da6612113 Mon Sep 17 00:00:00 2001 From: garthdb Date: Thu, 26 Mar 2026 14:47:40 -0600 Subject: [PATCH 14/15] fix(changeset): rewrap lines to stay under 100 chars Co-Authored-By: Claude Sonnet 4.6 --- .changeset/agent-skills-enhancements.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/agent-skills-enhancements.md b/.changeset/agent-skills-enhancements.md index c4c46d67..1126accd 100644 --- a/.changeset/agent-skills-enhancements.md +++ b/.changeset/agent-skills-enhancements.md @@ -2,6 +2,6 @@ "@adobe/spectrum-design-data-mcp": patch --- -Improved Agent Skills with better folder naming (`agent-skills/`), comprehensive documentation including -quick start examples, state management guide, accessibility checklist, and enhanced error messages with -actionable next steps. +Improved Agent Skills with better folder naming (`agent-skills/`), comprehensive documentation +including quick start examples, state management guide, accessibility checklist, and enhanced +error messages with actionable next steps. From f605411ba2a25d961936d863c175cbc75e9f2427 Mon Sep 17 00:00:00 2001 From: garthdb Date: Thu, 26 Mar 2026 18:12:55 -0600 Subject: [PATCH 15/15] chore: migrate from Cursor to Claude Code configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace .cursorrules and .cursor/ config with Claude Code equivalents: - .cursorrules → CLAUDE.md (root) - tools/component-options-editor/.cursorrules → CLAUDE.md - .cursor/skills/ → .claude/skills/ - .cursor/plans/ → .claude/plans/ - Add .claude/settings.local.json to .gitignore Co-Authored-By: Claude Sonnet 4.6 --- .../plans/size-property-analysis.md | 0 .../plans/token-options-mapping-analysis.md | 0 .../skills/enhance-sync-pr/SKILL.md | 20 +- .cursor/mcp.json | 3 - .cursor/rules | 265 ------------- .cursorrules | 271 -------------- .gitignore | 3 + CLAUDE.md | 347 ++++++++++++++++++ tools/component-options-editor/.cursorrules | 259 ------------- tools/component-options-editor/CLAUDE.md | 283 ++++++++++++++ 10 files changed, 643 insertions(+), 808 deletions(-) rename {.cursor => .claude}/plans/size-property-analysis.md (100%) rename {.cursor => .claude}/plans/token-options-mapping-analysis.md (100%) rename {.cursor => .claude}/skills/enhance-sync-pr/SKILL.md (90%) delete mode 100644 .cursor/mcp.json delete mode 100644 .cursor/rules delete mode 100644 .cursorrules create mode 100644 CLAUDE.md delete mode 100644 tools/component-options-editor/.cursorrules create mode 100644 tools/component-options-editor/CLAUDE.md diff --git a/.cursor/plans/size-property-analysis.md b/.claude/plans/size-property-analysis.md similarity index 100% rename from .cursor/plans/size-property-analysis.md rename to .claude/plans/size-property-analysis.md diff --git a/.cursor/plans/token-options-mapping-analysis.md b/.claude/plans/token-options-mapping-analysis.md similarity index 100% rename from .cursor/plans/token-options-mapping-analysis.md rename to .claude/plans/token-options-mapping-analysis.md diff --git a/.cursor/skills/enhance-sync-pr/SKILL.md b/.claude/skills/enhance-sync-pr/SKILL.md similarity index 90% rename from .cursor/skills/enhance-sync-pr/SKILL.md rename to .claude/skills/enhance-sync-pr/SKILL.md index 806f9ab1..79a1c0a1 100644 --- a/.cursor/skills/enhance-sync-pr/SKILL.md +++ b/.claude/skills/enhance-sync-pr/SKILL.md @@ -58,14 +58,14 @@ Or full graph: `moon ci` (same as GitHub Actions). ### Failure map (what to suggest) -| Symptom | Likely cause | Suggestion | | | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | --------------- | -| `checkComponentProps` lists token names | Missing or wrong `component` on entries in `color-component.json`, `layout-component.json`, or `icons.json` | Set `component` to match token name prefix (see [`packages/tokens/test/checkComponentProps.js`](../../../packages/tokens/test/checkComponentProps.js)). | | | -| Schema / `color-set` errors | `sets` missing `light`, `dark`, or **`wireframe`** | Align with [`schemas/token-types/color-set.json`](../../../packages/tokens/schemas/token-types/color-set.json). | | | -| `verifyDesignDataSnapshot` / snapshot mismatch | Validation report changed vs golden file | Paths in the snapshot are relative to `packages/tokens/`. After `moon run sdk:build`, run **`migrate snapshot` from `packages/tokens/`** so paths match `moon run tokens:verifyDesignDataSnapshot`: `cd packages/tokens && ../../sdk/target/debug/design-data migrate snapshot ./src --output ./snapshots/validation-snapshot.json --schema-path ./schemas --exceptions-path ./naming-exceptions.json`. | | | -| Token file / `$schema` / `uuid` | Invalid token shape | Fix per [`token-file.json`](../../../packages/tokens/schemas/token-file.json) and failing AVA tests. | | | -| Alias / renamed / manifest | Broken refs or manifest drift | Fix aliases; run `moon run tokens:buildManifest`. | | | -| Changeset lint on PR | Bad frontmatter | Valid `---` / \`"[**@adobe/spectrum-tokens**](https://github.com/adobe/spectrum-tokens)": patch | minor | major\` / body. | +| Symptom | Likely cause | Suggestion | | | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | - | +| `checkComponentProps` lists token names | Missing or wrong `component` on entries in `color-component.json`, `layout-component.json`, or `icons.json` | Set `component` to match token name prefix (see [`packages/tokens/test/checkComponentProps.js`](../../../packages/tokens/test/checkComponentProps.js)). | | | +| Schema / `color-set` errors | `sets` missing `light`, `dark`, or **`wireframe`** | Align with [`schemas/token-types/color-set.json`](../../../packages/tokens/schemas/token-types/color-set.json). | | | +| `verifyDesignDataSnapshot` / snapshot mismatch | Validation report changed vs golden file | Paths in the snapshot are relative to `packages/tokens/`. After `moon run sdk:build`, run **`migrate snapshot` from `packages/tokens/`** so paths match `moon run tokens:verifyDesignDataSnapshot`: `cd packages/tokens && ../../sdk/target/debug/design-data migrate snapshot ./src --output ./snapshots/validation-snapshot.json --schema-path ./schemas --exceptions-path ./naming-exceptions.json`. | | | +| Token file / `$schema` / `uuid` | Invalid token shape | Fix per [`token-file.json`](../../../packages/tokens/schemas/token-file.json) and failing AVA tests. | | | +| Alias / renamed / manifest | Broken refs or manifest drift | Fix aliases; run `moon run tokens:buildManifest`. | | | +| Changeset lint on PR | Bad frontmatter | Valid `---` / `"@adobe/spectrum-tokens": patch \| minor \| major` / body. | | | Re-run targeted tasks after each fix until green. @@ -108,7 +108,7 @@ GITHUB_TOKEN=... node src/cli.js generate \ Bump type is inferred from diff (e.g. deletions → major, additions → minor). -Commit using the **source implementer’s** git identity so changelog “Thanks @…” matches design work: +Commit using the **source implementer's** git identity so changelog "Thanks @…" matches design work: ```bash git config user.email '' @@ -132,4 +132,4 @@ Push the branch; confirm CI and changeset-bot on the PR. ## Reference: prior manual PRs -Well-formed examples: merged sync PRs with `token-changeset-generator`-style changesets and enriched bodies (e.g. [#615](https://github.com/adobe/spectrum-design-data/issues/615), [#593](https://github.com/adobe/spectrum-design-data/issues/593)). Early PRs used hand-written “Design motivation” + “Token diff” in the changeset body — the generator now standardizes that shape. +Well-formed examples: merged sync PRs with `token-changeset-generator`-style changesets and enriched bodies (e.g. [#615](https://github.com/adobe/spectrum-design-data/issues/615), [#593](https://github.com/adobe/spectrum-design-data/issues/593)). Early PRs used hand-written "Design motivation" + "Token diff" in the changeset body — the generator now standardizes that shape. diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index da39e4ff..00000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "mcpServers": {} -} diff --git a/.cursor/rules b/.cursor/rules deleted file mode 100644 index 0d86e18c..00000000 --- a/.cursor/rules +++ /dev/null @@ -1,265 +0,0 @@ -# Spectrum Tokens - Cursor Rules - -## Project Overview -This is the Spectrum Tokens project, a monorepo containing design tokens, component schemas, and related tooling for Adobe's Spectrum Design System. - -## Required Tools & Standards -- **Testing**: AVA (required for all JavaScript/TypeScript testing) -- **Package Management**: pnpm@10.17.1 (never use npm or yarn) -- **Monorepo Management**: moon (for task management and CI/CD) -- **Release Management**: changesets (for version bumps and releases) -- **Commit Messages**: commitlint with conventional commits (required for all git commits) -- **Node.js Version**: ~20.12 - -## Architecture -- **Monorepo Structure**: Uses pnpm workspaces with packages/, docs/, and tools/ -- **Task Management**: All tasks defined in moon.yml files -- **Testing**: AVA with specific configuration requirements -- **ESM**: Project uses ES modules (type: "module") - -## Coding Standards - -### JavaScript/TypeScript -- Use ES modules (import/export syntax) -- Prefer const over let, never use var -- Use async/await over Promise chains -- Use template literals for string interpolation -- Follow ESLint configuration where present - -### Testing Guidelines -- All tests must use AVA framework -- Test files must follow pattern: `test/**/*.test.js` -- Each package must have `ava.config.js` configuration -- Use descriptive test names -- Set up proper test environment variables - -### Commit Message Guidelines -- All commits must follow conventional commit format -- Use format: `type(scope): description` -- Available types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` -- Scope is optional but recommended for clarity -- Description should be present tense and lowercase -- Breaking changes must include `!` after type/scope or `BREAKING CHANGE:` in footer -- Examples: `feat(tokens): add new color palette`, `fix(diff): handle edge case` - -### Package Management -- Always use pnpm commands (never npm or yarn) -- Use exact versions for critical dependencies -- Keep pnpm-lock.yaml in version control -- Add new packages to pnpm-workspace.yaml when needed - -### File Structure -- Follow existing patterns in packages/, docs/, tools/ -- Include moon.yml for task definitions -- Include proper package.json with correct fields -- Include commitlint.config.cjs for commit message validation -- Use conventional file naming - -## Development Workflow - -### Adding New Packages -1. Create package directory under packages/, docs/, or tools/ -2. Add package.json with proper configuration -3. Add moon.yml with task definitions -4. Add ava.config.js for testing -5. Update pnpm-workspace.yaml if needed - -### Testing -- Use `moon run test` to run tests -- Tests should be thorough and descriptive -- Mock external dependencies appropriately -- Use AVA's built-in assertions - -### Version Management -- Use changesets for all version bumps -- Create changesets with `pnpm changeset` -- Never manually edit version numbers -- Follow semantic versioning - -### Commit Message Management -- Use conventional commit format for all commits -- Commit messages are validated by commitlint -- Use appropriate commit types and scopes -- Include breaking change indicators when needed -- Follow the format: `type(scope): description` - -### Task Execution -- Use moon for all defined tasks -- Define tasks in moon.yml files -- Use pnpm commands within moon tasks -- Set appropriate platform (node) for Node.js tasks - -## Code Suggestions - -### Preferred Patterns -```javascript -// Preferred: ES modules -import { readFileSync } from 'fs'; -export default function myFunction() {} - -// Preferred: Async/await -async function fetchData() { - try { - const response = await fetch(url); - return await response.json(); - } catch (error) { - console.error('Error fetching data:', error); - } -} - -// Preferred: Template literals -const message = `Hello, ${name}!`; - -// Preferred: Destructuring -const { name, version } = packageJson; -``` - -### AVA Test Patterns -```javascript -// Preferred test structure -import test from 'ava'; -import { myFunction } from '../src/index.js'; - -test('myFunction should return expected result', t => { - const result = myFunction(input); - t.is(result, expected); -}); - -test('myFunction should handle errors gracefully', async t => { - const error = await t.throwsAsync(async () => { - await myFunction(invalidInput); - }); - t.is(error.message, 'Expected error message'); -}); -``` - -### Moon Task Definitions -```yaml -# Preferred task structure -tasks: - test: - command: [pnpm, ava, test] - platform: node - build: - command: [pnpm, build] - platform: node - deps: [test] -``` - -### Commitlint Configuration -```javascript -// commitlint.config.cjs -module.exports = { - extends: ["@commitlint/config-conventional"], - ignores: [ - (message) => message.includes("[create-pull-request] automated change"), - ], -}; -``` - -## File-Specific Guidelines - -### package.json -- Include "type": "module" for ESM -- Specify Node.js version in engines -- Use pnpm in packageManager field -- Include proper repository, author, license fields - -### ava.config.js -- Use export default syntax -- Include standard configuration options -- Set NODE_ENV to "test" -- Use verbose output for debugging - -### moon.yml -- Define clear task names -- Use pnpm commands -- Set platform: node for Node.js tasks -- Include proper dependencies between tasks - -## Anti-Patterns to Avoid - -### Prohibited Commands -- `npm install` or `yarn install` (use `pnpm install`) -- `npm run` or `yarn run` (use `pnpm run` or `moon run`) -- Manual version bumps (use changesets) -- Non-conventional commit messages (use proper format) - -### Prohibited Patterns -```javascript -// Avoid: CommonJS in new code -const fs = require('fs'); -module.exports = function() {}; - -// Avoid: var declarations -var message = 'Hello'; - -// Avoid: Promise chains when async/await is cleaner -fetch(url) - .then(response => response.json()) - .then(data => console.log(data)) - .catch(error => console.error(error)); -``` - -## Dependencies -- Manage all tool dependencies at root level -- Use specific versions for critical tools -- Keep dependencies up to date -- Prefer established, well-maintained packages - -## Documentation -- Include README.md for each package -- Document complex functions and modules -- Keep CHANGELOG.md updated via changesets -- Reference TOOLING_STANDARDS.md for tool usage - -## JSON Schema -- Use proper JSON schema validation -- Include $schema references where applicable -- Follow established schema patterns in the project -- Validate schemas in tests - -## License & Copyright -- All files must include proper Adobe copyright -- Use Apache-2.0 license -- Include copyright headers in source files -- Follow existing copyright patterns -- **New files** must use the **current calendar year** (the year the file is created) in the copyright line, e.g. `Copyright YYYY Adobe. All rights reserved.` Use the same comment style as neighboring files (`//` in Rust, `#` in YAML/moon.yml, block or line comments in JS/TS, etc.) - -## Performance Considerations -- Use efficient algorithms for token processing -- Cache expensive operations when possible -- Minimize file I/O operations -- Use streaming for large datasets - -## Security -- Validate all inputs -- Use secure defaults -- Avoid eval() and similar dangerous functions -- Keep dependencies updated for security fixes - -## Error Handling -- Use descriptive error messages -- Handle edge cases gracefully -- Log errors appropriately -- Use try/catch blocks for async operations - -## Code Style -- Use 2-space indentation -- Use single quotes for strings (follow existing patterns) -- Include trailing commas in multiline objects/arrays -- Use meaningful variable and function names -- Keep functions focused and small - -## When Making Changes -1. Run tests with `moon run test` -2. Use conventional commit message format (e.g., `feat(tokens): add new color system`) -3. Create changesets for version bumps -4. Update documentation as needed -5. Follow the established patterns in the codebase - -## IDE Integration -- Use the project's ESLint configuration -- Enable Prettier for consistent formatting -- Use the project's TypeScript configuration where applicable -- Respect the project's .gitignore patterns \ No newline at end of file diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index da308a1b..00000000 --- a/.cursorrules +++ /dev/null @@ -1,271 +0,0 @@ -# Spectrum Tokens - Cursor Rules - -## Project Overview -This is the Spectrum Tokens project, a monorepo containing design tokens, component schemas, and related tooling for Adobe's Spectrum Design System. - -## Required Tools & Standards -- **Testing**: AVA (required for all JavaScript/TypeScript testing) -- **Package Management**: pnpm@10.17.1 (never use npm or yarn) -- **Monorepo Management**: moon (for task management and CI/CD) -- **Release Management**: changesets (for version bumps and releases) -- **Commit Messages**: commitlint with conventional commits (required for all git commits) -- **Node.js Version**: ~20.12 - -## Architecture -- **Monorepo Structure**: Uses pnpm workspaces with packages/, docs/, and tools/ -- **Task Management**: All tasks defined in moon.yml files -- **Testing**: AVA with specific configuration requirements -- **ESM**: Project uses ES modules (type: "module") - -## Coding Standards - -### JavaScript/TypeScript -- Use ES modules (import/export syntax) -- Prefer const over let, never use var -- Use async/await over Promise chains -- Use template literals for string interpolation -- Follow ESLint configuration where present - -### Testing Guidelines -- All tests must use AVA framework -- Test files must follow pattern: `test/**/*.test.js` -- Each package must have `ava.config.js` configuration -- Use descriptive test names -- Set up proper test environment variables - -### Commit Message Guidelines -- All commits must follow conventional commit format -- Use format: `type(scope): description` -- Available types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` -- Scope is optional but recommended for clarity -- Description should be present tense and lowercase -- Breaking changes must include `!` after type/scope or `BREAKING CHANGE:` in footer -- Examples: `feat(tokens): add new color palette`, `fix(diff): handle edge case` - -### Package Management -- Always use pnpm commands (never npm or yarn) -- Use exact versions for critical dependencies -- Keep pnpm-lock.yaml in version control -- Add new packages to pnpm-workspace.yaml when needed - -### File Structure -- Follow existing patterns in packages/, docs/, tools/ -- Include moon.yml for task definitions -- Include proper package.json with correct fields -- Include commitlint.config.cjs for commit message validation -- Use conventional file naming - -## Development Workflow - -### Adding New Packages -1. Create package directory under packages/, docs/, or tools/ -2. Add package.json with proper configuration -3. Add moon.yml with task definitions -4. Add ava.config.js for testing -5. Update pnpm-workspace.yaml if needed - -### Testing -- Use `moon run test` to run tests -- Tests should be thorough and descriptive -- Mock external dependencies appropriately -- Use AVA's built-in assertions - -### Version Management -- Use changesets for all version bumps -- Create changesets with `pnpm changeset` -- Never manually edit version numbers -- Follow semantic versioning - -### Commit Message Management -- Use conventional commit format for all commits -- Commit messages are validated by commitlint -- Use appropriate commit types and scopes -- Include breaking change indicators when needed -- Follow the format: `type(scope): description` - -### Task Execution -- Use moon for all defined tasks -- Define tasks in moon.yml files -- Use pnpm commands within moon tasks -- Set appropriate platform (node) for Node.js tasks - -## Code Suggestions - -### Preferred Patterns -```javascript -// Preferred: ES modules -import { readFileSync } from 'fs'; -export default function myFunction() {} - -// Preferred: Async/await -async function fetchData() { - try { - const response = await fetch(url); - return await response.json(); - } catch (error) { - console.error('Error fetching data:', error); - } -} - -// Preferred: Template literals -const message = `Hello, ${name}!`; - -// Preferred: Destructuring -const { name, version } = packageJson; -``` - -### AVA Test Patterns -```javascript -// Preferred test structure -import test from 'ava'; -import { myFunction } from '../src/index.js'; - -test('myFunction should return expected result', t => { - const result = myFunction(input); - t.is(result, expected); -}); - -test('myFunction should handle errors gracefully', async t => { - const error = await t.throwsAsync(async () => { - await myFunction(invalidInput); - }); - t.is(error.message, 'Expected error message'); -}); -``` - -### Moon Task Definitions -```yaml -# Preferred task structure -tasks: - test: - command: [pnpm, ava, test] - platform: node - build: - command: [pnpm, build] - platform: node - deps: [test] -``` - -### Commitlint Configuration -```javascript -// commitlint.config.cjs -module.exports = { - extends: ["@commitlint/config-conventional"], - ignores: [ - (message) => message.includes("[create-pull-request] automated change"), - ], -}; -``` - -## File-Specific Guidelines - -### package.json -- Include "type": "module" for ESM -- Specify Node.js version in engines -- Use pnpm in packageManager field -- Include proper repository, author, license fields - -### ava.config.js -- Use export default syntax -- Include standard configuration options -- Set NODE_ENV to "test" -- Use verbose output for debugging - -### moon.yml -- Define clear task names -- Use pnpm commands -- Set platform: node for Node.js tasks -- Include proper dependencies between tasks - -## Anti-Patterns to Avoid - -### Prohibited Commands -- `npm install` or `yarn install` (use `pnpm install`) -- `npm run` or `yarn run` (use `pnpm run` or `moon run`) -- Manual version bumps (use changesets) -- Non-conventional commit messages (use proper format) - -### Prohibited Patterns -```javascript -// Avoid: CommonJS in new code -const fs = require('fs'); -module.exports = function() {}; - -// Avoid: var declarations -var message = 'Hello'; - -// Avoid: Promise chains when async/await is cleaner -fetch(url) - .then(response => response.json()) - .then(data => console.log(data)) - .catch(error => console.error(error)); -``` - -## Dependencies -- Manage all tool dependencies at root level -- Use specific versions for critical tools -- Keep dependencies up to date -- Prefer established, well-maintained packages - -## Documentation -- Include README.md for each package -- Document complex functions and modules -- Keep CHANGELOG.md updated via changesets -- Reference TOOLING_STANDARDS.md for tool usage - -## JSON Schema -- Use proper JSON schema validation -- Include $schema references where applicable -- Follow established schema patterns in the project -- Validate schemas in tests - -## License & Copyright -- Use Apache-2.0 license for this project (see root `LICENSE`). -- **Copyright headers (Adobe OSPO / open-source practice):** Add the standard Apache 2.0 file header to **human-authored source files** you create or substantially edit—not to every file in the repo. -- **Include copyright headers in** (examples): - - Code: `.js`, `.ts`, `.jsx`, `.tsx`, and similar - - Scripts: `.sh`, `.bash`, etc. - - Human-edited config/templates: `.yaml`/`.yml`, `.json` (when not machine-generated dumps) -- **Do not add copyright headers to**: - - Markdown: `.md` (READMEs, changesets, docs, `CONTRIBUTING.md`, etc.) - - Clearly auto-generated or bundled output (lockfiles, build artifacts) - - Top-level project metadata (`LICENSE`, `.gitignore`, CI config, etc.) unless the project’s own standards say otherwise -- Follow existing header patterns in nearby files when editing. - -## Performance Considerations -- Use efficient algorithms for token processing -- Cache expensive operations when possible -- Minimize file I/O operations -- Use streaming for large datasets - -## Security -- Validate all inputs -- Use secure defaults -- Avoid eval() and similar dangerous functions -- Keep dependencies updated for security fixes - -## Error Handling -- Use descriptive error messages -- Handle edge cases gracefully -- Log errors appropriately -- Use try/catch blocks for async operations - -## Code Style -- Use 2-space indentation -- Use single quotes for strings (follow existing patterns) -- Include trailing commas in multiline objects/arrays -- Use meaningful variable and function names -- Keep functions focused and small - -## When Making Changes -1. Run tests with `moon run test` -2. Use conventional commit message format (e.g., `feat(tokens): add new color system`) -3. Create changesets for version bumps -4. Update documentation as needed -5. Follow the established patterns in the codebase - -## IDE Integration -- Use the project's ESLint configuration -- Enable Prettier for consistent formatting -- Use the project's TypeScript configuration where applicable -- Respect the project's .gitignore patterns \ No newline at end of file diff --git a/.gitignore b/.gitignore index cbef6e01..f0fb4cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,9 @@ sdk/target/ # visual studio code .vscode/ +# Claude Code local settings (machine-specific) +.claude/settings.local.json + # test temporary directories temp-* **/test/temp-* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7c7f19fc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,347 @@ +# Spectrum Tokens - Claude Code Rules + +## Project Overview + +This is the Spectrum Tokens project, a monorepo containing design tokens, component schemas, and related tooling for Adobe's Spectrum Design System. + +## Required Tools & Standards + +* **Testing**: AVA (required for all JavaScript/TypeScript testing) +* **Package Management**: pnpm\@10.17.1 (never use npm or yarn) +* **Monorepo Management**: moon (for task management and CI/CD) +* **Release Management**: changesets (for version bumps and releases) +* **Commit Messages**: commitlint with conventional commits (required for all git commits) +* **Node.js Version**: \~20.12 + +## Architecture + +* **Monorepo Structure**: Uses pnpm workspaces with packages/, docs/, and tools/ +* **Task Management**: All tasks defined in moon.yml files +* **Testing**: AVA with specific configuration requirements +* **ESM**: Project uses ES modules (type: "module") + +## Coding Standards + +### JavaScript/TypeScript + +* Use ES modules (import/export syntax) +* Prefer const over let, never use var +* Use async/await over Promise chains +* Use template literals for string interpolation +* Follow ESLint configuration where present + +### Testing Guidelines + +* All tests must use AVA framework +* Test files must follow pattern: `test/**/*.test.js` +* Each package must have `ava.config.js` configuration +* Use descriptive test names +* Set up proper test environment variables + +### Commit Message Guidelines + +* All commits must follow conventional commit format +* Use format: `type(scope): description` +* Available types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` +* Scope is optional but recommended for clarity +* Description should be present tense and lowercase +* Breaking changes must include `!` after type/scope or `BREAKING CHANGE:` in footer +* Examples: `feat(tokens): add new color palette`, `fix(diff): handle edge case` + +### Package Management + +* Always use pnpm commands (never npm or yarn) +* Use exact versions for critical dependencies +* Keep pnpm-lock.yaml in version control +* Add new packages to pnpm-workspace.yaml when needed + +### File Structure + +* Follow existing patterns in packages/, docs/, tools/ +* Include moon.yml for task definitions +* Include proper package.json with correct fields +* Include commitlint.config.cjs for commit message validation +* Use conventional file naming + +## Development Workflow + +### Adding New Packages + +1. Create package directory under packages/, docs/, or tools/ +2. Add package.json with proper configuration +3. Add moon.yml with task definitions +4. Add ava.config.js for testing +5. Update pnpm-workspace.yaml if needed + +### Testing + +* Use `moon run test` to run tests +* Tests should be thorough and descriptive +* Mock external dependencies appropriately +* Use AVA's built-in assertions + +### Version Management + +* Use changesets for all version bumps +* Create changesets with `pnpm changeset` +* Never manually edit version numbers +* Follow semantic versioning + +### Commit Message Management + +* Use conventional commit format for all commits +* Commit messages are validated by commitlint +* Use appropriate commit types and scopes +* Include breaking change indicators when needed +* Follow the format: `type(scope): description` + +### Task Execution + +* Use moon for all defined tasks +* Define tasks in moon.yml files +* Use pnpm commands within moon tasks +* Set appropriate platform (node) for Node.js tasks + +## Code Suggestions + +### Preferred Patterns + +```javascript +// Preferred: ES modules +import { readFileSync } from 'fs'; +export default function myFunction() {} + +// Preferred: Async/await +async function fetchData() { + try { + const response = await fetch(url); + return await response.json(); + } catch (error) { + console.error('Error fetching data:', error); + } +} + +// Preferred: Template literals +const message = `Hello, ${name}!`; + +// Preferred: Destructuring +const { name, version } = packageJson; +``` + +### AVA Test Patterns + +```javascript +// Preferred test structure +import test from 'ava'; +import { myFunction } from '../src/index.js'; + +test('myFunction should return expected result', t => { + const result = myFunction(input); + t.is(result, expected); +}); + +test('myFunction should handle errors gracefully', async t => { + const error = await t.throwsAsync(async () => { + await myFunction(invalidInput); + }); + t.is(error.message, 'Expected error message'); +}); +``` + +### Moon Task Definitions + +```yaml +# Preferred task structure +tasks: + test: + command: [pnpm, ava, test] + platform: node + build: + command: [pnpm, build] + platform: node + deps: [test] +``` + +### Commitlint Configuration + +```javascript +// commitlint.config.cjs +module.exports = { + extends: ["@commitlint/config-conventional"], + ignores: [ + (message) => message.includes("[create-pull-request] automated change"), + ], +}; +``` + +## File-Specific Guidelines + +### package.json + +* Include "type": "module" for ESM +* Specify Node.js version in engines +* Use pnpm in packageManager field +* Include proper repository, author, license fields + +### ava.config.js + +* Use export default syntax +* Include standard configuration options +* Set NODE\_ENV to "test" +* Use verbose output for debugging + +### moon.yml + +* Define clear task names +* Use pnpm commands +* Set platform: node for Node.js tasks +* Include proper dependencies between tasks + +## Anti-Patterns to Avoid + +### Prohibited Commands + +* `npm install` or `yarn install` (use `pnpm install`) +* `npm run` or `yarn run` (use `pnpm run` or `moon run`) +* Manual version bumps (use changesets) +* Non-conventional commit messages (use proper format) + +### Prohibited Patterns + +```javascript +// Avoid: CommonJS in new code +const fs = require('fs'); +module.exports = function() {}; + +// Avoid: var declarations +var message = 'Hello'; + +// Avoid: Promise chains when async/await is cleaner +fetch(url) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => console.error(error)); +``` + +## Dependencies + +* Manage all tool dependencies at root level +* Use specific versions for critical tools +* Keep dependencies up to date +* Prefer established, well-maintained packages + +## Documentation + +* Include README.md for each package +* Document complex functions and modules +* Keep CHANGELOG.md updated via changesets +* Reference TOOLING\_STANDARDS.md for tool usage + +## JSON Schema + +* Use proper JSON schema validation +* Include $schema references where applicable +* Follow established schema patterns in the project +* Validate schemas in tests + +## License & Copyright + +* All files must include proper Adobe copyright +* Use Apache-2.0 license +* Include copyright headers in source files +* Follow existing copyright patterns +* **New files** must use the **current calendar year** (the year the file is created) in the copyright line, e.g. `Copyright YYYY Adobe. All rights reserved.` Use the same comment style as neighboring files (`//` in Rust, `#` in YAML/moon.yml, block or line comments in JS/TS, etc.) + +## Performance Considerations + +* Use efficient algorithms for token processing +* Cache expensive operations when possible +* Minimize file I/O operations +* Use streaming for large datasets + +## Security + +* Validate all inputs +* Use secure defaults +* Avoid eval() and similar dangerous functions +* Keep dependencies updated for security fixes + +## Error Handling + +* Use descriptive error messages +* Handle edge cases gracefully +* Log errors appropriately +* Use try/catch blocks for async operations + +## Code Style + +* Use 2-space indentation +* Use single quotes for strings (follow existing patterns) +* Include trailing commas in multiline objects/arrays +* Use meaningful variable and function names +* Keep functions focused and small + +## When Making Changes + +1. Run tests with `moon run test` +2. Use conventional commit message format (e.g., `feat(tokens): add new color system`) +3. Create changesets for version bumps +4. Update documentation as needed +5. Follow the established patterns in the codebase + +## IDE Integration + +* Use the project's ESLint configuration +* Enable Prettier for consistent formatting +* Use the project's TypeScript configuration where applicable +* Respect the project's .gitignore patterns + + + +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work +bd close # Complete work +``` + +### Rules + +* Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +* Run `bd prime` for detailed command reference and session close protocol +* Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +## Session Completion + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd dolt push + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** + +* Work is NOT complete until `git push` succeeds +* NEVER stop before pushing - that leaves work stranded locally +* NEVER say "ready to push when you are" - YOU must push +* If push fails, resolve and retry until it succeeds + + diff --git a/tools/component-options-editor/.cursorrules b/tools/component-options-editor/.cursorrules deleted file mode 100644 index 0563e789..00000000 --- a/tools/component-options-editor/.cursorrules +++ /dev/null @@ -1,259 +0,0 @@ -# Component Options Editor - Cursor Rules - -## Project Context -This is a Figma plugin built with TypeScript, Lit, and Spectrum Web Components. The plugin helps author component option schemas for Adobe Spectrum Design System components. - -## Technology Stack -- **Framework**: Lit (Web Components) -- **UI Library**: Spectrum Web Components (@spectrum-web-components/*) -- **Language**: TypeScript -- **Build**: Webpack -- **Figma**: Plugin API -- **Version Control**: Git with GitHub CLI - -## Code Quality Standards - -### TypeScript -- Always use explicit types, avoid `any` -- Use interfaces for data structures (see `src/index.d.ts`) -- Enable strict mode compliance -- Use type guards where appropriate -- Document complex types with JSDoc comments - -### Lit Components -- Use `@customElement` decorator for all custom elements -- Use `@property` and `@query` decorators appropriately -- Implement reactive properties with proper types -- Use `html` template literals for rendering -- Follow Lit lifecycle methods (connectedCallback, disconnectedCallback) -- Clean up event listeners in disconnectedCallback - -### Spectrum Web Components -- **ALWAYS use Spectrum Web Components for UI elements** -- Prefer Spectrum components over native HTML elements -- Import from `@spectrum-web-components/*` packages -- Common components to use: - - `sp-button`, `sp-button-group` - - `sp-textfield`, `sp-field-label`, `sp-field-group` - - `sp-picker`, `sp-menu`, `sp-menu-item` - - `sp-checkbox`, `sp-radio`, `sp-radio-group` - - `sp-table` and related table elements - - `sp-action-button`, `sp-action-group` - - `sp-tabs`, `sp-tab`, `sp-tab-panel` - - `sp-icon` with workflow icons -- Follow Spectrum design patterns and sizing (s, m, l, xl) -- Use Spectrum color tokens and theming - -### Code Organization -- One component per file -- Place components in appropriate directories: - - `src/ui/app/` - main app components - - `src/ui/app/templates/` - reusable template components - - `src/ui/app/events/` - custom events - - `src/plugin/` - Figma plugin backend code -- Keep files under 300 lines when possible -- Extract reusable logic into helper functions - -### Naming Conventions -- Components: PascalCase (e.g., `OptionForm`, `LitAppElement`) -- Files: camelCase.ts (e.g., `optionForm.ts`, `litAppElement.ts`) -- Properties: camelCase (e.g., `componentName`, `optionType`) -- Events: kebab-case (e.g., `save-option`, `update-component`) -- Constants: UPPER_SNAKE_CASE - -### Event Handling -- Create custom events extending Event class (see `SaveOptionEvent`) -- Use type-safe event dispatching -- Document event payloads with interfaces -- Clean up listeners properly - -### State Management -- Use Lit reactive properties (`@property`) -- Keep state as high as needed, as low as possible -- Communicate between components via events -- Store plugin data in Figma's plugin storage (`figma.currentPage.getPluginData`) - -## GitHub Integration - -### Using GitHub CLI -- **ALWAYS use `gh` CLI for GitHub operations** -- Update issues: `gh issue edit --body "..."` -- List issues: `gh issue list` -- View issue: `gh issue view ` -- Comment on issues: `gh issue comment --body "..."` -- Close issues: `gh issue close ` -- Create PRs: `gh pr create` - -### Issue Management -- **Update issue descriptions/checklists, NOT just comments** -- When completing a task, use: `gh issue edit --body "$(gh issue view -c | sed 's/- \[ \] Task/- [x] Task/')"` -- Keep issue bodies as single source of truth -- Use tasklist format: `- [ ]` for tasks -- Link related issues with `#` in descriptions -- Use labels appropriately: `enhancement`, `bug`, `documentation` - -### Commit Messages -- **REQUIRED**: Follow conventional commits format (enforced by commitlint) -- Format: `(): ` -- Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore` -- Scopes: `metadata`, `options`, `preview`, `json`, `edit`, `variants`, `ui`, `plugin`, `build`, `deps` -- Subject must be sentence-case (capital first letter) -- Reference issues: `feat(metadata): Add category picker (#1)` -- Keep first line under 72 characters -- Add detailed description in body when needed - -### Pre-commit Automation -- **Husky pre-commit hook** runs automatically before each commit -- **lint-staged** runs ESLint and Prettier on staged files only -- Auto-fixes issues when possible -- Commit will be rejected if linting fails -- Commit message format is validated by commitlint - -### CI/CD & Branch Protection -- **GitHub Actions CI** runs on all pushes and PRs - - Checks: ESLint, Prettier, TypeScript compilation, Build, Copyright - - Must pass before merging to main -- **PR title validation** enforces conventional format -- **Branch protection active on main and dev**: - - Main: Requires status checks, conversation resolution, linear history - - Dev: Requires status checks, allows force pushes for rebasing - -## Plugin-Specific Guidelines - -### Figma Plugin Backend (src/plugin/) -- Keep plugin code minimal and focused on data persistence -- Use `figma.ui.postMessage` for plugin → UI communication -- Use `figma.ui.onmessage` for UI → plugin communication -- Handle errors gracefully with try-catch -- Log important operations with `cout` helper - -### UI Code (src/ui/) -- All UI logic runs in iframe sandbox -- Use `parent.postMessage` to communicate with plugin -- Listen for messages with `window.addEventListener('message')` -- Keep UI responsive and reactive -- Show loading states when appropriate - -### Data Validation -- Validate component metadata (required fields, URL format) -- Validate option data before saving -- Provide clear error messages to users -- Match JSON schema from `component-options.json` - -### Build Process -- Run `npm run build` before testing in Figma -- Build outputs to `dist/` directory -- Manifest paths in dist are relative (not `dist/...`) -- Test changes in Figma's development mode - -## Development Workflow - -### Git Flow Strategy -``` -main (protected, production) - └── dev (protected, development) - └── feature/issue-X-description (feature branches) -``` - -1. **Before starting work**: - - Check issue details: `gh issue view ` - - Create feature branch from dev: `git checkout dev && git pull && git checkout -b feature/issue--description` - -2. **During development**: - - Write clean, typed TypeScript - - Use Spectrum components - - Commit frequently with conventional commits (hooks will auto-fix formatting) - - Test in Figma frequently - - Keep commits atomic and well-described - -3. **Committing (automated checks run)**: - - Stage files: `git add ` - - Commit: `git commit -m "feat(scope): Description with capital letter"` - - **Pre-commit hook automatically runs**: ESLint --fix, Prettier --write - - **Commit-msg hook validates**: Conventional commit format - - Commit rejected if checks fail - fix issues and try again - - No need to run lint/prettier manually - hooks handle it! - -4. **Before pushing**: - - Optional: Run full validation: `npm run validate` - - Optional: Test build: `npm run build` - - Test plugin in Figma - - Push: `git push origin feature/issue-X-description` - -5. **Creating PRs**: - - Use `gh pr create --base dev` (merge to dev first, not main) - - PR title must follow conventional format: `feat(scope): Description` - - Reference issue: "Closes #" - - Describe changes and testing done - - Wait for CI checks to pass (automated) - - Squash and merge when ready - -6. **After completing tasks**: - - Update issue checklist using `gh issue edit ` - - Delete feature branch after merge - -7. **Releases to Main**: - - Create PR from dev to main when ready for release - - More stringent checks apply on main branch - - Tag releases appropriately - -## npm Scripts Reference - -### Quality & Validation -- `npm run lint` - Check TypeScript files with ESLint -- `npm run lint:fix` - Auto-fix ESLint issues -- `npm run prettier` - Check code formatting -- `npm run prettier:fix` - Auto-format code -- `npm run type-check` - Run TypeScript compiler without emitting files -- `npm run validate` - Run all checks (type-check, lint, prettier) -- `npm test` - Full validation + build (used in CI) -- `npm run lint-staged` - Run on staged files (used by Husky) - -### Build & Development -- `npm run build` - Full build (pack + assets) -- `npm run pack` - Webpack build + inline -- `npm run assets` - Copy assets to dist -- `npm run clean` - Remove build/dist directories -- `npm start` - Watch mode for development - -### Other -- `npm run copyright` - Check copyright headers -- `npm run format` - Run copyright + prettier + lint fixes - -## File References -- Main app: `src/ui/app/litAppElement.ts` -- Plugin backend: `src/plugin/plugin.ts` -- Type definitions: `src/index.d.ts` -- Schema reference: `component-options.json` -- Build config: `webpack.config.js` -- Package info: `package.json` -- Cursor rules: `.cursorrules` (this file) -- Branch protection plan: `.github/BRANCH_PROTECTION_PLAN.md` - -## Testing Checklist -- [ ] TypeScript compiles without errors -- [ ] Linter passes -- [ ] Plugin builds successfully -- [ ] Plugin loads in Figma without errors -- [ ] UI renders correctly -- [ ] Data persists after closing plugin -- [ ] All Spectrum components function properly -- [ ] Responsive to user interactions - -## Common Pitfalls to Avoid -- ❌ Using native HTML inputs instead of Spectrum components -- ❌ Forgetting to build before testing in Figma -- ❌ Not cleaning up event listeners -- ❌ Using `any` type -- ❌ Commenting on issues instead of updating descriptions -- ❌ Not referencing issues in commits -- ❌ Hardcoding values that should be configurable -- ❌ Forgetting to handle edge cases (empty arrays, null values) - -## Resources -- [Lit Documentation](https://lit.dev/) -- [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/) -- [Figma Plugin API](https://www.figma.com/plugin-docs/) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/) -- [GitHub CLI Manual](https://cli.github.com/manual/) - diff --git a/tools/component-options-editor/CLAUDE.md b/tools/component-options-editor/CLAUDE.md new file mode 100644 index 00000000..0c579ee2 --- /dev/null +++ b/tools/component-options-editor/CLAUDE.md @@ -0,0 +1,283 @@ +# Component Options Editor - Claude Code Rules + +## Project Context + +This is a Figma plugin built with TypeScript, Lit, and Spectrum Web Components. The plugin helps author component option schemas for Adobe Spectrum Design System components. + +## Technology Stack + +* **Framework**: Lit (Web Components) +* **UI Library**: Spectrum Web Components (@spectrum-web-components/\*) +* **Language**: TypeScript +* **Build**: Webpack +* **Figma**: Plugin API +* **Version Control**: Git with GitHub CLI + +## Code Quality Standards + +### TypeScript + +* Always use explicit types, avoid `any` +* Use interfaces for data structures (see `src/index.d.ts`) +* Enable strict mode compliance +* Use type guards where appropriate +* Document complex types with JSDoc comments + +### Lit Components + +* Use `@customElement` decorator for all custom elements +* Use `@property` and `@query` decorators appropriately +* Implement reactive properties with proper types +* Use `html` template literals for rendering +* Follow Lit lifecycle methods (connectedCallback, disconnectedCallback) +* Clean up event listeners in disconnectedCallback + +### Spectrum Web Components + +* **ALWAYS use Spectrum Web Components for UI elements** +* Prefer Spectrum components over native HTML elements +* Import from `@spectrum-web-components/*` packages +* Common components to use: + * `sp-button`, `sp-button-group` + * `sp-textfield`, `sp-field-label`, `sp-field-group` + * `sp-picker`, `sp-menu`, `sp-menu-item` + * `sp-checkbox`, `sp-radio`, `sp-radio-group` + * `sp-table` and related table elements + * `sp-action-button`, `sp-action-group` + * `sp-tabs`, `sp-tab`, `sp-tab-panel` + * `sp-icon` with workflow icons +* Follow Spectrum design patterns and sizing (s, m, l, xl) +* Use Spectrum color tokens and theming + +### Code Organization + +* One component per file +* Place components in appropriate directories: + * `src/ui/app/` - main app components + * `src/ui/app/templates/` - reusable template components + * `src/ui/app/events/` - custom events + * `src/plugin/` - Figma plugin backend code +* Keep files under 300 lines when possible +* Extract reusable logic into helper functions + +### Naming Conventions + +* Components: PascalCase (e.g., `OptionForm`, `LitAppElement`) +* Files: camelCase.ts (e.g., `optionForm.ts`, `litAppElement.ts`) +* Properties: camelCase (e.g., `componentName`, `optionType`) +* Events: kebab-case (e.g., `save-option`, `update-component`) +* Constants: UPPER\_SNAKE\_CASE + +### Event Handling + +* Create custom events extending Event class (see `SaveOptionEvent`) +* Use type-safe event dispatching +* Document event payloads with interfaces +* Clean up listeners properly + +### State Management + +* Use Lit reactive properties (`@property`) +* Keep state as high as needed, as low as possible +* Communicate between components via events +* Store plugin data in Figma's plugin storage (`figma.currentPage.getPluginData`) + +## GitHub Integration + +### Using GitHub CLI + +* **ALWAYS use `gh` CLI for GitHub operations** +* Update issues: `gh issue edit --body "..."` +* List issues: `gh issue list` +* View issue: `gh issue view ` +* Comment on issues: `gh issue comment --body "..."` +* Close issues: `gh issue close ` +* Create PRs: `gh pr create` + +### Issue Management + +* **Update issue descriptions/checklists, NOT just comments** +* When completing a task, use: `gh issue edit --body "$(gh issue view -c | sed 's/- \[ \] Task/- [x] Task/')"` +* Keep issue bodies as single source of truth +* Use tasklist format: `- [ ]` for tasks +* Link related issues with `#` in descriptions +* Use labels appropriately: `enhancement`, `bug`, `documentation` + +### Commit Messages + +* **REQUIRED**: Follow conventional commits format (enforced by commitlint) +* Format: `(): ` +* Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore` +* Scopes: `metadata`, `options`, `preview`, `json`, `edit`, `variants`, `ui`, `plugin`, `build`, `deps` +* Subject must be sentence-case (capital first letter) +* Reference issues: `feat(metadata): Add category picker (#1)` +* Keep first line under 72 characters +* Add detailed description in body when needed + +### Pre-commit Automation + +* **Husky pre-commit hook** runs automatically before each commit +* **lint-staged** runs ESLint and Prettier on staged files only +* Auto-fixes issues when possible +* Commit will be rejected if linting fails +* Commit message format is validated by commitlint + +### CI/CD & Branch Protection + +* **GitHub Actions CI** runs on all pushes and PRs + * Checks: ESLint, Prettier, TypeScript compilation, Build, Copyright + * Must pass before merging to main +* **PR title validation** enforces conventional format +* **Branch protection active on main and dev**: + * Main: Requires status checks, conversation resolution, linear history + * Dev: Requires status checks, allows force pushes for rebasing + +## Plugin-Specific Guidelines + +### Figma Plugin Backend (src/plugin/) + +* Keep plugin code minimal and focused on data persistence +* Use `figma.ui.postMessage` for plugin → UI communication +* Use `figma.ui.onmessage` for UI → plugin communication +* Handle errors gracefully with try-catch +* Log important operations with `cout` helper + +### UI Code (src/ui/) + +* All UI logic runs in iframe sandbox +* Use `parent.postMessage` to communicate with plugin +* Listen for messages with `window.addEventListener('message')` +* Keep UI responsive and reactive +* Show loading states when appropriate + +### Data Validation + +* Validate component metadata (required fields, URL format) +* Validate option data before saving +* Provide clear error messages to users +* Match JSON schema from `component-options.json` + +### Build Process + +* Run `npm run build` before testing in Figma +* Build outputs to `dist/` directory +* Manifest paths in dist are relative (not `dist/...`) +* Test changes in Figma's development mode + +## Development Workflow + +### Git Flow Strategy + +``` +main (protected, production) + └── dev (protected, development) + └── feature/issue-X-description (feature branches) +``` + +1. **Before starting work**: + * Check issue details: `gh issue view ` + * Create feature branch from dev: `git checkout dev && git pull && git checkout -b feature/issue--description` + +2. **During development**: + * Write clean, typed TypeScript + * Use Spectrum components + * Commit frequently with conventional commits (hooks will auto-fix formatting) + * Test in Figma frequently + * Keep commits atomic and well-described + +3. **Committing (automated checks run)**: + * Stage files: `git add ` + * Commit: `git commit -m "feat(scope): Description with capital letter"` + * **Pre-commit hook automatically runs**: ESLint --fix, Prettier --write + * **Commit-msg hook validates**: Conventional commit format + * Commit rejected if checks fail - fix issues and try again + * No need to run lint/prettier manually - hooks handle it! + +4. **Before pushing**: + * Optional: Run full validation: `npm run validate` + * Optional: Test build: `npm run build` + * Test plugin in Figma + * Push: `git push origin feature/issue-X-description` + +5. **Creating PRs**: + * Use `gh pr create --base dev` (merge to dev first, not main) + * PR title must follow conventional format: `feat(scope): Description` + * Reference issue: "Closes #" + * Describe changes and testing done + * Wait for CI checks to pass (automated) + * Squash and merge when ready + +6. **After completing tasks**: + * Update issue checklist using `gh issue edit ` + * Delete feature branch after merge + +7. **Releases to Main**: + * Create PR from dev to main when ready for release + * More stringent checks apply on main branch + * Tag releases appropriately + +## npm Scripts Reference + +### Quality & Validation + +* `npm run lint` - Check TypeScript files with ESLint +* `npm run lint:fix` - Auto-fix ESLint issues +* `npm run prettier` - Check code formatting +* `npm run prettier:fix` - Auto-format code +* `npm run type-check` - Run TypeScript compiler without emitting files +* `npm run validate` - Run all checks (type-check, lint, prettier) +* `npm test` - Full validation + build (used in CI) +* `npm run lint-staged` - Run on staged files (used by Husky) + +### Build & Development + +* `npm run build` - Full build (pack + assets) +* `npm run pack` - Webpack build + inline +* `npm run assets` - Copy assets to dist +* `npm run clean` - Remove build/dist directories +* `npm start` - Watch mode for development + +### Other + +* `npm run copyright` - Check copyright headers +* `npm run format` - Run copyright + prettier + lint fixes + +## File References + +* Main app: `src/ui/app/litAppElement.ts` +* Plugin backend: `src/plugin/plugin.ts` +* Type definitions: `src/index.d.ts` +* Schema reference: `component-options.json` +* Build config: `webpack.config.js` +* Package info: `package.json` +* Branch protection plan: `.github/BRANCH_PROTECTION_PLAN.md` + +## Testing Checklist + +* [ ] TypeScript compiles without errors +* [ ] Linter passes +* [ ] Plugin builds successfully +* [ ] Plugin loads in Figma without errors +* [ ] UI renders correctly +* [ ] Data persists after closing plugin +* [ ] All Spectrum components function properly +* [ ] Responsive to user interactions + +## Common Pitfalls to Avoid + +* Using native HTML inputs instead of Spectrum components +* Forgetting to build before testing in Figma +* Not cleaning up event listeners +* Using `any` type +* Commenting on issues instead of updating descriptions +* Not referencing issues in commits +* Hardcoding values that should be configurable +* Forgetting to handle edge cases (empty arrays, null values) + +## Resources + +* [Lit Documentation](https://lit.dev/) +* [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/) +* [Figma Plugin API](https://www.figma.com/plugin-docs/) +* [TypeScript Handbook](https://www.typescriptlang.org/docs/) +* [GitHub CLI Manual](https://cli.github.com/manual/)