From 1340563761627d049304248f1af813ed11ef52f9 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Fri, 13 Feb 2026 14:07:06 -0800 Subject: [PATCH 01/14] add page-designer-decorator tool for Storefront Next --- packages/b2c-dx-mcp/package.json | 5 +- .../tools/page-designer-decorator/README.md | 236 ++++++ .../tools/page-designer-decorator/analyzer.ts | 638 +++++++++++++++ .../tools/page-designer-decorator/index.ts | 746 ++++++++++++++++++ .../tools/page-designer-decorator/rules.ts | 87 ++ .../rules/1-mode-selection.ts | 73 ++ .../rules/2a-auto-mode.ts | 104 +++ .../rules/2b-0-interactive-overview.ts | 55 ++ .../rules/2b-1-interactive-analyze.ts | 144 ++++ .../rules/2b-2-interactive-select-props.ts | 86 ++ .../rules/2b-3-interactive-configure-attrs.ts | 101 +++ .../2b-4-interactive-configure-regions.ts | 69 ++ .../2b-5-interactive-confirm-generation.ts | 103 +++ .../templates/decorator-generator.ts | 413 ++++++++++ .../src/tools/storefrontnext/README.md | 62 ++ .../src/tools/storefrontnext/index.ts | 2 + .../tools/page-designer-decorator/README.md | 120 +++ .../page-designer-decorator/index.test.ts | 739 +++++++++++++++++ pnpm-lock.yaml | 36 + pnpm-workspace.yaml | 10 +- 20 files changed, 3825 insertions(+), 4 deletions(-) create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/analyzer.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/1-mode-selection.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2a-auto-mode.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-0-interactive-overview.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-1-interactive-analyze.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-2-interactive-select-props.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts create mode 100644 packages/b2c-dx-mcp/src/tools/page-designer-decorator/templates/decorator-generator.ts create mode 100644 packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md create mode 100644 packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index cfdf39a42..d8ca42ff7 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -94,7 +94,10 @@ "@modelcontextprotocol/sdk": "1.26.0", "@oclif/core": "4.8.0", "@salesforce/b2c-tooling-sdk": "workspace:*", - "zod": "3.25.76" + "glob": "^11.0.0", + "ts-morph": "^24.0.0", + "zod": "3.25.76", + "zod-to-json-schema": "^3.24.1" }, "devDependencies": { "@eslint/compat": "^1", diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md new file mode 100644 index 000000000..931e2dec3 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -0,0 +1,236 @@ +# Page Designer Decorator Tool + +Tool for adding Page Designer decorators to React components using native TypeScript template literals. + +## 🎯 Overview + +This tool analyzes React components and generates Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to make components available in Page Designer for Storefront Next. + +## ✨ Key Features + +- **Name-Based Lookup**: Find components by name (e.g., "ProductCard") without knowing paths +- **Auto-Discovery**: Automatically searches common component directories +- **Type-Safe**: Full TypeScript type inference for all contexts +- **Fast**: Direct function execution, no file I/O or compilation overhead +- **Flexible Input**: Supports component names or file paths +- **Two Modes**: Auto mode for quick setup, Interactive mode for fine-tuned control + +## 📁 File Structure + +``` +page-designer-decorator/ +├── analyzer.ts # Component parsing and analysis +├── rules.ts # Rule loader and exports +├── index.ts # Main tool implementation +├── rules/ +│ ├── 1-mode-selection.ts # Entry point +│ ├── 2a-auto-mode.ts # Auto mode workflow +│ ├── 2b-0-interactive-overview.ts # Interactive workflow overview +│ ├── 2b-1-interactive-analyze.ts # Step 1: Analysis +│ ├── 2b-2-interactive-select-props.ts # Step 2: Selection +│ ├── 2b-3-interactive-configure-attrs.ts # Step 3: Configuration +│ ├── 2b-4-interactive-configure-regions.ts # Step 4: Regions +│ └── 2b-5-interactive-confirm-generation.ts # Step 5: Generation +└── templates/ + └── decorator-generator.ts # Decorator code generation +``` + +## 🚀 Usage + +### Basic Usage (Name-Based - Recommended) + +```bash +# By component name (automatically finds the file) +add_page_designer_decorator({ + component: "ProductCard", + autoMode: true +}) + +# Interactive mode +add_page_designer_decorator({ + component: "Hero", + conversationContext: { step: "analyze" } +}) + +# With custom search paths (for unusual locations) +add_page_designer_decorator({ + component: "ProductCard", + searchPaths: ["packages/retail/src", "app/features"], + autoMode: true +}) +``` + +### Path-Based Usage + +```bash +# If you prefer to specify the exact path +add_page_designer_decorator({ + component: "src/components/ProductCard.tsx", + autoMode: true +}) +``` + +### Workflow + +1. **Component Discovery**: Provide name (e.g., "ProductCard") or path +2. **Mode Selection**: Choose Auto or Interactive mode +3. **Analysis** (Interactive only): Review component props +4. **Selection** (Interactive only): Select which props to expose +5. **Configuration** (Interactive only): Configure types and defaults +6. **Regions** (Interactive only): Configure nested content areas +7. **Generation**: Get decorator code + +### Component Discovery + +The tool automatically searches for components in these locations (in order): + +1. `src/components/**` (PascalCase and kebab-case) +2. `app/components/**` +3. `components/**` +4. `src/**` (broader search) +5. Custom paths (if provided via `searchPaths`) + +**Examples:** + +- `"ProductCard"` → finds `src/components/product-tile/ProductCard.tsx` +- `"Hero"` → finds `src/components/hero/Hero.tsx` or `app/components/hero.tsx` +- `"product-card"` → finds `src/components/product-card.tsx` or `product-card/index.tsx` + +**Tips:** + +- Use component name for portability +- Use path for unusual locations +- Add `searchPaths` for monorepos or non-standard structures + +## 🏗️ Architecture + +### Rule Rendering + +Rules are pure TypeScript functions that return strings: + +```typescript +${context.hasEditableProps + ? context.editableProps.map(prop => + `- \`${prop.name}\` (${prop.type})` + ).join('\n') + : '' +} +``` + +### Type Safety + +Every rule has a strongly-typed context interface: + +```typescript +export interface AnalyzeStepContext { + componentName: string; + file: string; + hasEditableProps: boolean; + editableProps: PropInfo[]; + // ... more fields +} + +export function renderAnalyzeStep(context: AnalyzeStepContext): string { + // TypeScript checks all variable access at compile time +} +``` + +### Template Generation + +Code generation uses pure functions: + +```typescript +export function generateDecoratorCode(context: MetadataContext): string { + const imports = generateImports(context); + const decorator = generateComponentDecorator(context); + const attributes = generateAttributes(context); + + return `${imports}${decorator}\nexport class ${context.metadataClassName} {\n${attributes}\n}`; +} +``` + +## 📦 Build Process + +All rules and templates are compiled into the JavaScript output: + +```json +{ + "scripts": { + "build": "tsc" + } +} +``` + +## 🎯 When to Use This Tool + +Use this tool when: + +- ✅ You need to add Page Designer support to React components +- ✅ You want automatic component discovery by name +- ✅ You prefer type-safe decorator generation +- ✅ You need both quick auto-mode and detailed interactive workflows + +## 🔧 Development + +### Adding a New Rule + +1. Create a new file in `rules/`: + +```typescript +// rules/my-new-rule.ts +export interface MyRuleContext { + message: string; +} + +export function renderMyRule(context: MyRuleContext): string { + return `# My Rule\n\n${context.message}`; +} +``` + +2. Export it from `rules.ts`: + +```typescript +import {renderMyRule, type MyRuleContext} from './rules/my-new-rule.js'; + +export const pageDesignerDecoratorRules = { + // ... existing rules + getMyRule(context: MyRuleContext): string { + return renderMyRule(context); + }, +}; +``` + +3. Use it in `index.ts`: + +```typescript +const instructions = pageDesignerDecoratorRules.getMyRule({ + message: 'Hello World', +}); +``` + +### Modifying Code Generation + +Edit `templates/decorator-generator.ts` directly. Changes require recompilation. + +## 📊 Performance + +The tool uses direct function execution with no file I/O or compilation overhead. Typical tool invocations complete in under 1ms. + +## ✅ Testing + +```bash +pnpm build +pnpm test +``` + +Comprehensive test suite covers all workflow modes, component discovery, and error handling. + +## 🎓 Learning Resources + +- [Template Literals (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [MCP Tools Documentation](https://modelcontextprotocol.io/docs) + +## 📝 License + +Apache-2.0 - Copyright (c) 2025, Salesforce, Inc. diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/analyzer.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/analyzer.ts new file mode 100644 index 000000000..72aa3647d --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/analyzer.ts @@ -0,0 +1,638 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {existsSync, readFileSync} from 'node:fs'; +import path from 'node:path'; +import {globSync} from 'glob'; +import {Project, InterfaceDeclaration, PropertySignature} from 'ts-morph'; + +// ============================================================================ +// TYPE DEFINITIONS +// ============================================================================ + +/** + * Component analysis result + */ +export interface ComponentInfo { + componentName: string; + interfaceName: null | string; + hasDecorators: boolean; + props: PropInfo[]; + exportType: 'default' | 'named'; + filePath: string; +} + +/** + * Property information extracted from component interface + */ +export interface PropInfo { + name: string; + type: string; + optional: boolean; + isComplex: boolean; // Can't be used directly in Page Designer + isUIOnly: boolean; // Styling/layout props not suitable for PD +} + +/** + * Type suggestion for attribute configuration + */ +export interface TypeSuggestion { + type: string; + reason: string; + priority: 'high' | 'low' | 'medium'; +} + +// ============================================================================ +// TYPE INFERENCE +// ============================================================================ + +/** + * Type mapping from TypeScript to SFCC Page Designer attribute types + */ +const TYPE_MAPPING: Record = { + String: 'string', + string: 'string', + Number: 'integer', + number: 'integer', + Boolean: 'boolean', + boolean: 'boolean', + Date: 'string', + URL: 'url', + CMSRecord: 'cms_record', +}; + +/** + * Valid SFCC Page Designer attribute types + */ +export const VALID_ATTRIBUTE_TYPES = [ + 'string', + 'text', + 'markup', + 'integer', + 'boolean', + 'product', + 'category', + 'file', + 'page', + 'image', + 'url', + 'enum', + 'custom', + 'cms_record', +] as const; + +/** + * Infer Page Designer attribute type from TypeScript type + */ +export function inferPageDesignerType(tsType: string): string { + if (TYPE_MAPPING[tsType]) { + return TYPE_MAPPING[tsType]; + } + + if (tsType.includes('|')) { + const firstType = tsType.split('|')[0].trim(); + return inferPageDesignerType(firstType); + } + + if (tsType.includes('[]') || tsType.includes('Array<')) { + return 'string'; + } + + return 'string'; +} + +/** + * Check if TypeScript type can be auto-inferred + */ +export function isAutoInferredType(tsType: string): boolean { + return Boolean(TYPE_MAPPING[tsType]); +} + +/** + * Check if type is too complex for Page Designer + */ +export function isComplexType(tsType: string): boolean { + return ( + tsType.includes('{') || + tsType.includes('<') || + tsType.includes('.') || + tsType.includes('=>') || + tsType.includes('React.') || + tsType.startsWith('(') + ); +} + +/** + * Check if property is UI-only + */ +export function isUIOnlyProp(propName: string): boolean { + const uiPatterns = [ + 'classname', + 'style', + 'theme', + 'variant', + 'size', + 'color', + 'loading', + 'disabled', + 'readonly', + 'onclick', + 'onchange', + 'onsubmit', + 'children', + 'key', + 'ref', + ]; + const nameLower = propName.toLowerCase(); + return uiPatterns.some((pattern) => nameLower.includes(pattern)); +} + +/** + * Generate Page Designer attribute type suggestions for a component prop + * + * **Inference Strategy:** + * Uses naming patterns and TypeScript types to suggest appropriate Page Designer types. + * This reduces manual configuration by auto-detecting common patterns. + * + * **Page Designer Types:** + * - `string`: Default text input + * - `url`: URL/link inputs (validates URL format) + * - `image`: Image asset picker + * - `html`: Rich text editor + * - `markup`: HTML/markdown editor + * - `enum`: Dropdown with predefined values + * - `boolean`: Checkbox + * - `number`: Numeric input + * - `product`: Product picker (SFCC-specific) + * - `category`: Category picker (SFCC-specific) + * + * **Heuristics (by priority):** + * 1. **High Priority**: Strong patterns (url, image, product) + * 2. **Medium Priority**: Contextual patterns (html, markup) + * 3. **Low Priority**: Weak signals (description → markup) + * + * Multiple suggestions allow developers to choose the best fit. + * + * @param propName - Property name from component interface + * @param tsType - TypeScript type string + * @returns Array of type suggestions with reasoning and priority + * + * @example + * // URL detection: + * generateTypeSuggestions('imageUrl', 'string') + * // => [{ type: 'url', reason: '...', priority: 'high' }] + * + * @example + * // Image detection: + * generateTypeSuggestions('heroImage', 'string') + * // => [{ type: 'image', reason: '...', priority: 'high' }] + * + * @example + * // Multiple suggestions: + * generateTypeSuggestions('description', 'string') + * // => [ + * // { type: 'markup', reason: '...', priority: 'low' }, + * // { type: 'html', reason: '...', priority: 'medium' } + * // ] + * + * @example + * // Product reference: + * generateTypeSuggestions('product', 'string') + * // => [{ type: 'product', reason: '...', priority: 'high' }] + * + * @public + */ +export function generateTypeSuggestions(propName: string, tsType: string): TypeSuggestion[] { + const suggestions: TypeSuggestion[] = []; + const nameLower = propName.toLowerCase(); + + // URL patterns + if (nameLower.includes('url') || nameLower.includes('link') || nameLower.includes('href')) { + suggestions.push({ + type: 'url', + reason: 'Property name suggests URL/link', + priority: 'high', + }); + } + + // Image patterns + if ( + nameLower.includes('image') || + nameLower.includes('img') || + nameLower.includes('picture') || + nameLower.includes('background') + ) { + suggestions.push({ + type: 'image', + reason: 'Property name suggests image asset', + priority: 'high', + }); + } + + // Rich text patterns + if ( + nameLower.includes('html') || + nameLower.includes('richtext') || + nameLower.includes('content') || + nameLower.includes('body') + ) { + suggestions.push({ + type: 'markup', + reason: 'Property name suggests rich content', + priority: 'medium', + }); + } + + // Multi-line text patterns + if (nameLower.includes('description') || nameLower.includes('bio') || nameLower.includes('message')) { + suggestions.push({ + type: 'text', + reason: 'Property name suggests multi-line text', + priority: 'medium', + }); + } + + // Array patterns + if (tsType.includes('[]') || tsType.includes('Array<')) { + suggestions.push({ + type: 'enum', + reason: 'Array types work best as enums for selection in Page Designer', + priority: 'high', + }); + } + + // Product/Category references + if (nameLower.includes('product') && !nameLower.includes('products')) { + suggestions.push({ + type: 'product', + reason: 'Property name suggests product reference', + priority: 'high', + }); + } + + if (nameLower.includes('category')) { + suggestions.push({ + type: 'category', + reason: 'Property name suggests category reference', + priority: 'high', + }); + } + + return suggestions; +} + +// ============================================================================ +// COMPONENT FILE PARSING +// ============================================================================ + +/** + * Extract component name from file content + */ +function extractComponentName(content: string): string { + const defaultFunctionMatch = content.match(/export\s+default\s+function\s+(\w+)/); + if (defaultFunctionMatch) { + return defaultFunctionMatch[1]; + } + + const namedFunctionMatch = content.match(/export\s+function\s+(\w+)/); + if (namedFunctionMatch) { + return namedFunctionMatch[1]; + } + + const namedConstMatch = content.match(/export\s+const\s+(\w+)\s*=/); + if (namedConstMatch) { + return namedConstMatch[1]; + } + + return 'Component'; +} + +/** + * Detect export type + */ +function detectExportType(content: string): 'default' | 'named' { + return content.includes('export default') ? 'default' : 'named'; +} + +/** + * Parse component file and extract structure + */ +function parseComponentFile(filePath: string): ComponentInfo { + const content = readFileSync(filePath, 'utf8'); + + const hasDecorators = content.includes('@Component') || content.includes('@PageType'); + + if (hasDecorators) { + return { + componentName: extractComponentName(content), + interfaceName: null, + hasDecorators: true, + props: [], + exportType: detectExportType(content), + filePath, + }; + } + + const project = new Project({ + useInMemoryFileSystem: true, + skipAddingFilesFromTsConfig: true, + }); + + const sourceFile = project.createSourceFile(filePath, content); + const interfaces = sourceFile.getInterfaces(); + const propsInterface = interfaces.find((i: InterfaceDeclaration) => i.getName().includes('Props')); + + if (!propsInterface) { + return { + componentName: extractComponentName(content), + interfaceName: null, + hasDecorators: false, + props: [], + exportType: detectExportType(content), + filePath, + }; + } + + const props: PropInfo[] = propsInterface.getProperties().map((prop: PropertySignature) => { + const name = prop.getName(); + const type = prop.getType().getText(); + const optional = prop.hasQuestionToken(); + + return { + name, + type, + optional, + isComplex: isComplexType(type), + isUIOnly: isUIOnlyProp(name), + }; + }); + + return { + componentName: extractComponentName(content), + interfaceName: propsInterface.getName(), + hasDecorators: false, + props, + exportType: detectExportType(content), + filePath, + }; +} + +// ============================================================================ +// COMPONENT ANALYZER +// ============================================================================ + +/** + * Component analyzer for Page Designer decorator generation + */ +class ComponentAnalyzer { + private cache: Map = new Map(); + + analyzeComponent(filePath: string): ComponentInfo { + const cached = this.cache.get(filePath); + if (cached) { + return cached; + } + + const analysis = parseComponentFile(filePath); + this.cache.set(filePath, analysis); + + return analysis; + } + + clearCache() { + this.cache.clear(); + } +} + +export const componentAnalyzer = new ComponentAnalyzer(); + +// ============================================================================ +// COMPONENT RESOLUTION (Name-Based Lookup) +// ============================================================================ + +/** + * Convert PascalCase or camelCase to kebab-case + * + * Used for finding components with different naming conventions. + * React components are typically PascalCase, but file names may be kebab-case. + * + * @param str - String to convert (e.g., "ProductCard", "myComponent") + * @returns Kebab-case string (e.g., "product-card", "my-component") + * + * @example + * toKebabCase('ProductCard') // => 'product-card' + * toKebabCase('MyButtonComponent') // => 'my-button-component' + * toKebabCase('heroSection') // => 'hero-section' + * + * @internal + */ +function toKebabCase(str: string): string { + return str + .replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2') + .replaceAll(/([A-Z])([A-Z][a-z])/g, '$1-$2') + .toLowerCase(); +} + +/** + * Search for component file by name using smart discovery patterns + * + * **Search Strategy (in priority order):** + * 1. Common component directories with exact name (PascalCase) + * 2. Kebab-case variants of the name + * 3. Index file patterns (for directory-based components) + * 4. Broader search in src/ + * 5. Custom search paths (if provided) + * + * **Why this order:** + * - Most projects follow conventions (src/components/) + * - PascalCase is React standard, checked first + * - Kebab-case is common for file names + * - Index files are common for complex components + * - Fallback to broader search if not in standard locations + * + * **Disambiguation:** + * If multiple files match, prefers the shortest path (closest to root). + * This typically selects the main component over similar named test/story files. + * + * @param componentName - Component name without extension (e.g., "ProductCard", "Hero") + * @param workspaceRoot - Absolute path to workspace root + * @param customPaths - Additional directories to search (e.g., ["packages/retail/src"]) + * @returns Absolute file path or null if not found + * + * @example + * // Finds: src/components/product-tile/ProductCard.tsx + * findComponentByName('ProductCard', '/workspace', undefined) + * + * @example + * // Finds: src/components/hero.tsx or src/components/hero/index.tsx + * findComponentByName('hero', '/workspace', undefined) + * + * @example + * // Searches in custom paths first + * findComponentByName('ProductCard', '/workspace', ['packages/retail/src']) + * + * @internal + */ +function findComponentByName(componentName: string, workspaceRoot: string, customPaths?: string[]): null | string { + // Normalize component name (remove file extensions) + const cleanName = componentName.replace(/\.(tsx?|jsx?)$/, ''); + const kebabName = toKebabCase(cleanName); + + // Search patterns (in order of priority) + const searchPatterns = [ + // Common component directories (PascalCase) + `src/components/**/${cleanName}.tsx`, + `src/components/**/${cleanName}.ts`, + `app/components/**/${cleanName}.tsx`, + `components/**/${cleanName}.tsx`, + + // Kebab-case variants + `src/components/**/${kebabName}.tsx`, + `app/components/**/${kebabName}.tsx`, + `components/**/${kebabName}.tsx`, + + // Index file patterns + `src/components/**/${kebabName}/index.tsx`, + `app/components/**/${kebabName}/index.tsx`, + + // Anywhere in src/ (broader search) + `src/**/${cleanName}.tsx`, + `src/**/${cleanName}.ts`, + `src/**/${kebabName}.tsx`, + + // Custom search paths (if provided) + ...(customPaths?.flatMap((path) => [ + `${path}/**/${cleanName}.tsx`, + `${path}/**/${cleanName}.ts`, + `${path}/**/${kebabName}.tsx`, + `${path}/**/${kebabName}/index.tsx`, + ]) || []), + ]; + + // Search with glob + for (const pattern of searchPatterns) { + try { + const matches = globSync(pattern, { + cwd: workspaceRoot, + absolute: true, + ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/out/**'], + }); + + if (matches.length > 0) { + // If multiple matches, prefer shortest path (closest to root) + const sorted = matches.sort((a, b) => a.length - b.length); + return sorted[0]; + } + } catch { + // Ignore glob errors and try next pattern + continue; + } + } + + return null; +} + +/** + * Resolve component input (name or path) to absolute file path + * + * **This is the main entry point for component discovery.** + * + * Supports two input modes: + * 1. **Name-based** (recommended): Just provide the component name + * 2. **Path-based** (backward compatible): Provide relative path from workspace + * + * **Name-based detection:** + * Input is treated as a name if it: + * - Does NOT contain path separators (/ or \) + * - Does NOT have a file extension (.tsx, .ts, etc.) + * + * **Path-based detection:** + * Input is treated as a path if it: + * - Contains / or \ + * - Has a file extension + * + * @param input - Component name or relative path + * @param workspaceRoot - Absolute path to workspace root + * @param searchPaths - Additional directories to search (only used for name-based) + * @returns Absolute file path to component + * @throws {Error} If component cannot be found, with detailed search information + * + * @example + * // Name-based (finds automatically): + * resolveComponent('ProductCard', '/workspace') + * // => '/workspace/src/components/product-tile/ProductCard.tsx' + * + * @example + * // Path-based (backward compatible): + * resolveComponent('src/components/ProductCard.tsx', '/workspace') + * // => '/workspace/src/components/ProductCard.tsx' + * + * @example + * // With custom search paths (for monorepos): + * resolveComponent('Hero', '/workspace', ['packages/retail/src', 'packages/shared']) + * // => '/workspace/packages/retail/src/components/Hero.tsx' + * + * @example + * // Error handling: + * try { + * resolveComponent('NonExistent', '/workspace') + * } catch (err) { + * // Error includes: + * // - List of searched locations + * // - Tried name variations + * // - Helpful tips for resolution + * } + * + * @public + */ +export function resolveComponent(input: string, workspaceRoot: string, searchPaths?: string[]): string { + // Check if input looks like a path (has / or \ or file extension) + const looksLikePath = input.includes('/') || input.includes('\\') || input.match(/\.(tsx?|jsx?|mjs|cjs|js)$/); + + if (looksLikePath) { + // Treat as path (backward compatible) + const fullPath = path.join(workspaceRoot, input); + if (existsSync(fullPath)) { + return fullPath; + } + throw new Error( + `Component file not found at path: ${input}\n\n` + + `Full path checked: ${fullPath}\n\n` + + `Tips:\n` + + ` 1. Use component name instead (e.g., "ProductCard") for automatic discovery\n` + + ` 2. If components are in a different repo, set: SFCC_WORKING_DIRECTORY=/path/to/storefront-next`, + ); + } + + // Treat as component name - search for it + const found = findComponentByName(input, workspaceRoot, searchPaths); + + if (!found) { + const searchLocations = [ + 'src/components/**', + 'app/components/**', + 'components/**', + 'src/**', + ...(searchPaths || []), + ]; + + throw new Error( + `Component "${input}" not found.\n\n` + + `Searched in:\n${searchLocations.map((loc) => ` - ${loc}`).join('\n')}\n\n` + + `Tried variations:\n` + + ` - ${input}.tsx\n` + + ` - ${toKebabCase(input)}.tsx\n` + + ` - ${toKebabCase(input)}/index.tsx\n\n` + + `Tips:\n` + + ` 1. Provide full path: component: "src/components/ProductCard.tsx"\n` + + ` 2. Add custom search: searchPaths: ["packages/retail/src"]\n` + + ` 3. Check component name spelling and casing\n` + + ` 4. If components are in a different repo, set: SFCC_WORKING_DIRECTORY=/path/to/storefront-next`, + ); + } + + return found; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts new file mode 100644 index 000000000..629daa2fe --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts @@ -0,0 +1,746 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {z, type ZodRawShape} from 'zod'; +import {componentAnalyzer, generateTypeSuggestions, resolveComponent, type TypeSuggestion} from './analyzer.js'; +import {generateDecoratorCode, type AttributeContext, type MetadataContext} from './templates/decorator-generator.js'; +import {pageDesignerDecoratorRules} from './rules.js'; +import type {McpTool} from '../../utils/index.js'; +import type {Services} from '../../services.js'; + +// ============================================================================ +// SCHEMA DEFINITION +// ============================================================================ + +export const pageDesignerDecoratorSchema = z + .object({ + component: z + .string() + .describe( + 'Component name (e.g., "ProductCard", "Hero") or file path (e.g., "src/components/ProductCard.tsx"). ' + + 'When a name is provided, the tool automatically searches common component directories. ' + + 'For backward compatibility, file paths are also supported.', + ), + + searchPaths: z + .array(z.string()) + .optional() + .describe( + 'Additional directories to search for components (e.g., ["packages/retail/src", "app/features"]). ' + + 'Only used when component is specified by name (not path).', + ), + + autoMode: z + .boolean() + .optional() + .describe( + 'Auto-generate all configurations with sensible defaults (skip interactive workflow). When enabled, automatically selects suitable props, infers types, and generates decorators without user confirmation.', + ), + + componentId: z.string().optional().describe('Override component ID (default: auto-generated from component name)'), + + conversationContext: z + .object({ + step: z + .enum(['analyze', 'select_props', 'configure_attrs', 'configure_regions', 'confirm_generation']) + .optional() + .describe('Current step in the conversation workflow'), + + componentInfo: z + .record(z.string(), z.any()) + .optional() + .describe('Cached component analysis from previous step'), + + selectedProps: z + .array(z.string()) + .optional() + .describe('Props from component interface selected to expose in Page Designer'), + + newAttributes: z + .array( + z.object({ + name: z.string(), + description: z.string().optional(), + required: z.boolean().optional(), + }), + ) + .optional() + .describe('New attributes to add (not in existing props)'), + + attributeConfig: z + .record( + z.string(), + z.object({ + type: z.string().optional(), + name: z.string().optional(), + defaultValue: z.any().optional(), + values: z.array(z.string()).optional(), + }), + ) + .optional() + .describe('Configuration for each attribute (explicit types, names, etc.)'), + + componentMetadata: z + .object({ + id: z.string(), + name: z.string(), + description: z.string(), + group: z.string().optional(), + }) + .optional() + .describe('Component decorator configuration'), + + regionConfig: z + .object({ + enabled: z.boolean().describe('Whether to include @RegionDefinition decorator'), + regions: z + .array( + z.object({ + id: z.string().describe('Region identifier (e.g., "main", "sidebar")'), + name: z.string().describe('Display name for the region'), + description: z.string().optional().describe('Description of the region purpose'), + maxComponents: z.number().optional().describe('Maximum number of components allowed in region'), + componentTypeInclusions: z + .array(z.string()) + .optional() + .describe('Allowed component types (whitelist)'), + componentTypeExclusions: z + .array(z.string()) + .optional() + .describe('Disallowed component types (blacklist)'), + }), + ) + .optional() + .describe('Array of region definitions'), + }) + .optional() + .describe('Region configuration for nested content areas'), + }) + .optional() + .describe('Conversation state for multi-turn interaction'), + }) + .strict(); + +export type PageDesignerDecoratorInput = z.infer; + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Convert component name to kebab-case for use as component ID + * + * Page Designer component IDs should be lowercase with hyphens. + * + * @param name - PascalCase or camelCase name + * @returns kebab-case identifier + * + * @example + * toKebabCase('ProductCard') // => 'product-card' + * toKebabCase('TwoColumnLayout') // => 'two-column-layout' + * + * @internal + */ +function toKebabCase(name: string): string { + return name + .replaceAll(/([a-z])([A-Z])/g, '$1-$2') + .replaceAll(/[\s_]+/g, '-') + .toLowerCase(); +} + +/** + * Convert camelCase prop name to human-readable display name + * + * Used for attribute names shown to merchants in Page Designer UI. + * + * @param fieldName - camelCase field name + * @returns Human-readable name with proper capitalization + * + * @example + * toHumanReadableName('imageUrl') // => 'Image Url' + * toHumanReadableName('ctaButtonText') // => 'Cta Button Text' + * + * @internal + */ +function toHumanReadableName(fieldName: string): string { + return fieldName + .replaceAll(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase()) + .trim(); +} + +// ============================================================================ +// WORKFLOW STEP HANDLERS +// ============================================================================ + +/** + * Handle Interactive Mode - Step 1: Analyze + * + * Parses the component file and provides analysis to the LLM: + * - Component name and structure + * - All props with types + * - Categorization (editable, complex, UI-only) + * - Suggested component ID and name + * + * **LLM should then:** + * - Present findings to user + * - Ask which props to expose in Page Designer + * - Collect component metadata (ID, name, description, group) + * - Call next step with selectedProps and componentMetadata + * + * @internal + */ +function handleAnalyzeStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { + const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + const editableProps = componentInfo.props.filter((p) => !p.isComplex && !p.isUIOnly); + const complexProps = componentInfo.props.filter((p) => p.isComplex); + const uiProps = componentInfo.props.filter((p) => p.isUIOnly && !p.isComplex); + + const suggestedComponentId = args.componentId || toKebabCase(componentInfo.componentName); + const suggestedComponentName = toHumanReadableName(componentInfo.componentName); + + const instructions = pageDesignerDecoratorRules.getAnalyzeInstructions({ + componentName: componentInfo.componentName, + file: args.component, + hasDecorators: componentInfo.hasDecorators, + interfaceName: componentInfo.interfaceName || 'None found', + totalProps: componentInfo.props.length, + exportType: componentInfo.exportType, + hasEditableProps: editableProps.length > 0, + editableProps, + hasComplexProps: complexProps.length > 0, + complexProps, + hasUIProps: uiProps.length > 0, + uiProps, + suggestedComponentId, + suggestedComponentName, + }); + + return { + content: [ + { + type: 'text' as const, + text: instructions, + }, + ], + }; +} + +function handleSelectPropsStep(args: PageDesignerDecoratorInput, _workspaceRoot: string) { + const selectedProps = args.conversationContext?.selectedProps || []; + const newAttributes = args.conversationContext?.newAttributes || []; + const componentMetadata = args.conversationContext?.componentMetadata; + + if (!componentMetadata) { + return { + content: [ + { + type: 'text' as const, + text: '⚠️ Missing component metadata. Please provide component ID, name, description, and group from the analyze step.', + }, + ], + isError: true, + }; + } + + const confirmation = pageDesignerDecoratorRules.getSelectPropsConfirmation({ + componentMetadata: { + id: componentMetadata.id, + name: componentMetadata.name, + description: componentMetadata.description, + group: componentMetadata.group || 'odyssey_base', + }, + selectedProps, + newAttributes, + selectedPropsCount: selectedProps.length, + newAttributesCount: newAttributes.length, + totalAttributeCount: selectedProps.length + newAttributes.length, + hasSelectedProps: selectedProps.length > 0, + hasNewAttributes: newAttributes.length > 0, + }); + + return { + content: [ + { + type: 'text' as const, + text: confirmation, + }, + ], + }; +} + +function handleConfigureAttrsStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { + const selectedProps = args.conversationContext?.selectedProps || []; + const newAttributes = args.conversationContext?.newAttributes || []; + + const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + const attributeAnalysis: Array<{ + name: string; + source: 'existing' | 'new'; + tsType: string; + autoInferred: boolean; + suggestions: TypeSuggestion[]; + }> = []; + + for (const propName of selectedProps) { + const prop = componentInfo.props.find((p) => p.name === propName); + if (!prop) continue; + + const suggestions = generateTypeSuggestions(propName, prop.type); + + attributeAnalysis.push({ + name: propName, + source: 'existing', + tsType: prop.type, + autoInferred: suggestions.length === 0, + suggestions, + }); + } + + for (const attr of newAttributes) { + const suggestions = generateTypeSuggestions(attr.name, 'string'); + + attributeAnalysis.push({ + name: attr.name, + source: 'new', + tsType: 'string', + autoInferred: suggestions.length === 0, + suggestions, + }); + } + + const autoInferredAttrs = attributeAnalysis.filter((a) => a.autoInferred); + const needsConfigAttrs = attributeAnalysis.filter((a) => !a.autoInferred); + + const instructions = pageDesignerDecoratorRules.getConfigureAttrsInstructions({ + totalAttributes: attributeAnalysis.length, + autoInferredCount: autoInferredAttrs.length, + needsConfigCount: needsConfigAttrs.length, + hasAutoInferred: autoInferredAttrs.length > 0, + autoInferredAttrs: autoInferredAttrs.map((a) => ({name: a.name, tsType: a.tsType})), + hasNeedsConfig: needsConfigAttrs.length > 0, + needsConfigAttrs: needsConfigAttrs.map((attr) => ({ + name: attr.name, + tsType: attr.tsType, + source: attr.source === 'existing' ? 'Existing prop' : 'New attribute', + hasSuggestions: attr.suggestions.length > 0, + suggestions: attr.suggestions, + suggestedTypes: attr.suggestions.map((s) => s.type).join(', ') || 'string', + humanReadableName: toHumanReadableName(attr.name), + hasEnumSuggestion: attr.suggestions.some((s) => s.type === 'enum'), + })), + }); + + return { + content: [ + { + type: 'text' as const, + text: instructions, + }, + ], + }; +} + +function handleConfigureRegionsStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { + const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + const instructions = pageDesignerDecoratorRules.getConfigureRegionsInstructions({ + componentName: componentInfo.componentName, + }); + + return { + content: [ + { + type: 'text' as const, + text: instructions, + }, + ], + }; +} + +function handleConfirmGenerationStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { + const { + componentMetadata, + selectedProps = [], + newAttributes = [], + attributeConfig = {}, + } = args.conversationContext || {}; + + if (!componentMetadata) { + return { + content: [ + { + type: 'text' as const, + text: 'Error: Missing component metadata. Please start from the beginning.', + }, + ], + isError: true, + }; + } + + const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + const attributes: AttributeContext[] = []; + + for (const propName of selectedProps) { + const prop = componentInfo.props.find((p) => p.name === propName); + if (!prop) continue; + + const config = attributeConfig[propName]; + const hasConfig = config && Object.keys(config).length > 0; + + attributes.push({ + name: propName, + tsType: prop.type, + optional: prop.optional, + hasConfig, + config, + }); + } + + for (const attr of newAttributes) { + const config = attributeConfig[attr.name]; + const hasConfig = config && Object.keys(config).length > 0; + + attributes.push({ + name: attr.name, + tsType: 'string', + optional: !attr.required, + hasConfig, + config, + }); + } + + const regionConfig = args.conversationContext?.regionConfig; + const hasRegions = regionConfig?.enabled && regionConfig.regions && regionConfig.regions.length > 0; + + const context: MetadataContext = { + needsImports: true, + componentId: componentMetadata.id, + componentName: componentMetadata.name, + componentDescription: componentMetadata.description, + componentGroup: componentMetadata.group || 'odyssey_base', + metadataClassName: `${componentInfo.componentName}Metadata`, + hasAttributes: attributes.length > 0, + hasRegions: hasRegions || false, + hasLoader: false, + regions: hasRegions ? regionConfig.regions || [] : [], + attributes, + }; + + const decoratorCode = generateDecoratorCode(context); + + const userResponse = pageDesignerDecoratorRules.getConfirmGenerationInstructions({ + decoratorCode, + componentName: componentInfo.componentName, + componentId: componentMetadata.id, + componentGroup: componentMetadata.group || 'odyssey_base', + file: args.component, + attributeCount: attributes.length, + hasRegions: hasRegions || false, + regionCount: hasRegions && regionConfig.regions ? regionConfig.regions.length : 0, + }); + + return { + content: [ + { + type: 'text' as const, + text: userResponse, + }, + ], + }; +} + +/** + * Handle Auto Mode - Single-step decorator generation + * + * **Fully automated workflow:** + * 1. Analyzes component + * 2. Auto-selects suitable props (excludes complex and UI-only) + * 3. Auto-infers Page Designer types from naming patterns + * 4. Generates decorator code immediately + * 5. NO user interaction required + * + * **Selection criteria:** + * - ✅ Simple types (string, number, boolean) + * - ❌ Complex types (objects, functions, React nodes) + * - ❌ UI-only props (className, style, onClick, etc.) + * + * **Auto-configuration:** + * - High-confidence patterns get explicit types (url, image, enum) + * - Others use auto-inferred types + * - Human-readable names auto-generated + * - No regions configured (interactive mode for advanced features) + * + * **Use cases:** + * - Quick setup for standard components + * - Batch processing multiple components + * - Getting started quickly + * + * @internal + */ +function handleAutoMode(args: PageDesignerDecoratorInput, workspaceRoot: string) { + const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + if (componentInfo.hasDecorators) { + return { + content: [ + { + type: 'text' as const, + text: `# ⚠️ Component Already Decorated\n\nThe component \`${componentInfo.componentName}\` already has Page Designer decorators.\n\nWould you like to modify the existing decorators instead?`, + }, + ], + }; + } + + const selectedProps = componentInfo.props.filter((p) => !p.isComplex && !p.isUIOnly).map((p) => p.name); + + const attributeConfig: Record = {}; + const attributes: AttributeContext[] = []; + + for (const propName of selectedProps) { + const prop = componentInfo.props.find((p) => p.name === propName); + if (!prop) continue; + + const suggestions = generateTypeSuggestions(propName, prop.type); + const config: {name?: string; type?: string; values?: string[]; defaultValue?: unknown} = { + name: toHumanReadableName(propName), + }; + + const highPrioritySuggestion = suggestions.find((s) => s.priority === 'high'); + if (highPrioritySuggestion) { + config.type = highPrioritySuggestion.type; + + if (highPrioritySuggestion.type === 'enum') { + if (propName.toLowerCase().includes('size')) { + config.values = ['sm', 'default', 'lg']; + config.defaultValue = 'default'; + } else if (propName.toLowerCase().includes('variant')) { + config.values = ['default', 'primary', 'secondary']; + config.defaultValue = 'default'; + } + } + + if (highPrioritySuggestion.type === 'boolean') { + config.defaultValue = false; + } + } + + if (Object.keys(config).length > 1) { + attributeConfig[propName] = config; + } + + attributes.push({ + name: propName, + tsType: prop.type, + optional: prop.optional, + hasConfig: Object.keys(config).length > 1, + config: Object.keys(config).length > 1 ? config : undefined, + }); + } + + const componentId = args.componentId || toKebabCase(componentInfo.componentName); + const componentName = toHumanReadableName(componentInfo.componentName); + const componentDescription = `${componentName} component for Page Designer`; + + const context: MetadataContext = { + needsImports: true, + componentId, + componentName, + componentDescription, + componentGroup: 'odyssey_base', + metadataClassName: `${componentInfo.componentName}Metadata`, + hasAttributes: attributes.length > 0, + hasRegions: false, + hasLoader: false, + regions: [], + attributes, + }; + + const decoratorCode = generateDecoratorCode(context); + + const response = pageDesignerDecoratorRules.getAutoModeInstructions({ + componentName: componentInfo.componentName, + file: args.component, + componentId, + selectedPropCount: selectedProps.length, + autoConfigCount: Object.keys(attributeConfig).length, + autoInferredCount: selectedProps.length - Object.keys(attributeConfig).length, + hasNoSuitableProps: selectedProps.length === 0, + selectedProps: selectedProps.length > 0 ? selectedProps.map((p) => `\`${p}\``).join(', ') : 'None', + decoratorCode, + componentGroup: 'odyssey_base', + }); + + return { + content: [ + { + type: 'text' as const, + text: response, + }, + ], + }; +} + +// ============================================================================ +// TOOL EXPORT +// ============================================================================ + +/** + * Creates the Page Designer decorator tool for Storefront Next. + * + * @param _services - MCP services (not used by this tool) + * @returns The configured MCP tool + */ +export function createPageDesignerDecoratorTool(_services: Services): McpTool { + return { + name: 'add_page_designer_decorator', + + description: `⚠️ MANDATORY: Add Page Designer decorators to an existing component (Template Literals Version). + + Analyzes component structure and guides through adding @Component, @AttributeDefinition, and @RegionDefinition decorators. + + ENVIRONMENT SETUP: + Set SFCC_WORKING_DIRECTORY environment variable to the Storefront Next repository path. + If not set, uses current working directory. + + INITIAL CALL - MODE SELECTION: + When called with ONLY the 'component' parameter (no autoMode, no conversationContext), the tool will: + - Analyze the component (automatically finds it by name) + - Present mode selection options to the user + - WAIT for user to choose between Auto Mode or Interactive Mode + - DO NOT proceed until user selects a mode + + MODES: + + **AUTO MODE** (set autoMode: true): + - Automatically analyzes component and generates decorators with sensible defaults + - Skips all interactive steps + - Auto-selects suitable props (excludes complex and UI-only props) + - Auto-infers types based on naming patterns + - Immediately generates code - no confirmation needed + - Best for: Quick setup, standard components, batch processing + + **INTERACTIVE MODE** (set conversationContext.step: "analyze"): + - Multi-step workflow with user confirmation at each stage + - Allows fine-tuned control over all settings + - MUST ask questions and WAIT for responses + - DO NOT generate code until user confirms configuration + - Best for: Complex components, custom requirements, learning + + WORKFLOW STEPS (Interactive Mode): + 1. analyze: Parse component, identify props, provide recommendations + 2. select_props: Confirm user's selections and prepare for configuration + 3. configure_attrs: Set explicit types, names, defaults where needed + 4. configure_regions: Configure nested content areas (optional) + 5. confirm_generation: Generate final decorator code + + INTELLIGENT FEATURES: + - Auto-infers types when possible (string, number, boolean) + - Suggests explicit types when needed (url, image, enum, markup) + - Detects unsuitable props (complex types, UI-only props) + - Provides smart defaults based on naming patterns + + Use conversationContext parameter for multi-turn conversation (interactive mode only).`, + + inputSchema: pageDesignerDecoratorSchema.shape as ZodRawShape, + toolsets: ['STOREFRONTNEXT'], + isGA: false, + + async handler(args: Record) { + try { + // Validate and parse input + const validatedArgs = pageDesignerDecoratorSchema.parse(args) as PageDesignerDecoratorInput; + // Workspace resolution: + // 1. SFCC_WORKING_DIRECTORY (Storefront Next convention, recommended) + // 2. process.cwd() (fallback) + const workspaceRoot = process.env.SFCC_WORKING_DIRECTORY || process.cwd(); + + if (validatedArgs.autoMode === undefined && !validatedArgs.conversationContext) { + const fullPath = resolveComponent(validatedArgs.component, workspaceRoot, validatedArgs.searchPaths); + const componentInfo = componentAnalyzer.analyzeComponent(fullPath); + + const instructions = pageDesignerDecoratorRules.getModeSelectionInstructions({ + componentName: componentInfo.componentName, + file: validatedArgs.component, + }); + + return { + content: [ + { + type: 'text' as const, + text: instructions, + }, + ], + }; + } + + if (validatedArgs.autoMode) { + return handleAutoMode(validatedArgs, workspaceRoot); + } + + const step = validatedArgs.conversationContext?.step || 'analyze'; + + switch (step) { + case 'analyze': { + return handleAnalyzeStep(validatedArgs, workspaceRoot); + } + + case 'configure_attrs': { + return handleConfigureAttrsStep(validatedArgs, workspaceRoot); + } + + case 'configure_regions': { + return handleConfigureRegionsStep(validatedArgs, workspaceRoot); + } + + case 'confirm_generation': { + return handleConfirmGenerationStep(validatedArgs, workspaceRoot); + } + + case 'select_props': { + return handleSelectPropsStep(validatedArgs, workspaceRoot); + } + + default: { + const unknownStep: string = step; + throw new Error(`Unknown step: ${unknownStep}`); + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + // Check if it's a Zod validation error + if (error instanceof Error && error.name === 'ZodError') { + return { + content: [ + { + type: 'text' as const, + text: `# Error: Invalid Input\n\n${errorMessage}\n\nPlease check your input parameters and try again.`, + }, + ], + isError: true, + }; + } + return { + content: [ + { + type: 'text' as const, + text: `# Error Adding Page Designer Support\n\n${errorMessage}`, + }, + ], + isError: true, + }; + } + }, + }; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules.ts new file mode 100644 index 000000000..6ced5cdb1 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Import all rule renderers +import {renderModeSelection, type ModeSelectionContext} from './rules/1-mode-selection.js'; +import {renderInteractiveOverview} from './rules/2b-0-interactive-overview.js'; +import {renderAnalyzeStep, type AnalyzeStepContext} from './rules/2b-1-interactive-analyze.js'; +import {renderSelectPropsConfirmation, type SelectPropsContext} from './rules/2b-2-interactive-select-props.js'; +import {renderConfigureAttrs, type ConfigureAttrsContext} from './rules/2b-3-interactive-configure-attrs.js'; +import {renderConfigureRegions, type ConfigureRegionsContext} from './rules/2b-4-interactive-configure-regions.js'; +import {renderConfirmGeneration, type ConfirmGenerationContext} from './rules/2b-5-interactive-confirm-generation.js'; +import {renderAutoMode, type AutoModeContext} from './rules/2a-auto-mode.js'; + +/** + * Page Designer decorator rules - type-safe, zero dependencies + */ +export const pageDesignerDecoratorRules = { + /** + * Renders the mode selection prompt + */ + getModeSelectionInstructions(context: ModeSelectionContext): string { + return renderModeSelection(context); + }, + + /** + * Renders the interactive mode workflow overview + */ + getInteractiveOverview(): string { + return renderInteractiveOverview(); + }, + + /** + * Renders Interactive Analyze step instructions + */ + getAnalyzeInstructions(context: AnalyzeStepContext): string { + const workflow = this.getInteractiveOverview(); + const stepContent = renderAnalyzeStep(context); + return `${workflow}\n\n${stepContent}`; + }, + + /** + * Renders Interactive Select Props step confirmation + */ + getSelectPropsConfirmation(context: SelectPropsContext): string { + return renderSelectPropsConfirmation(context); + }, + + /** + * Renders Interactive Configure Attributes step instructions + */ + getConfigureAttrsInstructions(context: ConfigureAttrsContext): string { + return renderConfigureAttrs(context); + }, + + /** + * Renders Interactive Configure Regions step instructions + */ + getConfigureRegionsInstructions(context: ConfigureRegionsContext): string { + return renderConfigureRegions(context); + }, + + /** + * Renders Interactive Confirm Generation step (final code presentation) + */ + getConfirmGenerationInstructions(context: ConfirmGenerationContext): string { + return renderConfirmGeneration(context); + }, + + /** + * Renders Auto Mode instructions + */ + getAutoModeInstructions(context: AutoModeContext): string { + return renderAutoMode(context); + }, +}; + +// Re-export types for convenience +export type {ModeSelectionContext} from './rules/1-mode-selection.js'; +export type {AutoModeContext} from './rules/2a-auto-mode.js'; +export type {AnalyzeStepContext} from './rules/2b-1-interactive-analyze.js'; +export type {SelectPropsContext} from './rules/2b-2-interactive-select-props.js'; +export type {ConfigureAttrsContext} from './rules/2b-3-interactive-configure-attrs.js'; +export type {ConfigureRegionsContext} from './rules/2b-4-interactive-configure-regions.js'; +export type {ConfirmGenerationContext} from './rules/2b-5-interactive-confirm-generation.js'; diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/1-mode-selection.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/1-mode-selection.ts new file mode 100644 index 000000000..3f458ce8a --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/1-mode-selection.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Mode selection rule - Entry point for Page Designer decorator tool + */ +export interface ModeSelectionContext { + componentName: string; + file: string; +} + +export function renderModeSelection(context: ModeSelectionContext): string { + return `# 🎯 Choose Page Designer Setup Mode + +I need to know which mode you'd like to use for adding Page Designer support to **\`${context.componentName}\`**. + +## Available Modes + +### 🤖 Auto Mode (Quick & Automatic) +- **Best for**: Quick setup, standard components, batch processing +- **What happens**: + - Automatically analyzes the component + - Auto-selects suitable props (excludes complex types) + - Auto-infers types based on naming patterns + - Generates decorators immediately with sensible defaults + - **No confirmation needed** - code generated instantly +- **Time**: ~1 step +- **Control**: Low (uses smart defaults) + +### 👤 Interactive Mode (Step-by-Step) +- **Best for**: Complex components, custom requirements, learning the process +- **What happens**: + - Multi-step workflow with your input at each stage + - Review and approve prop selections + - Configure attribute types, names, and defaults + - Configure regions for nested content (optional) + - **Requires confirmation** before generating code +- **Time**: ~4-5 steps +- **Control**: High (you decide everything) + +## ⚡ How to Proceed + +**⚠️ IMPORTANT: WAIT for the user to choose a mode. DO NOT proceed automatically.** + +Please ask the user: **"Which mode would you like to use: Auto Mode or Interactive Mode?"** + +Once the user responds: + +**For Auto Mode**, call the tool again with: +\`\`\`json +{ + "file": "${context.file}", + "autoMode": true +} +\`\`\` + +**For Interactive Mode**, call the tool again with: +\`\`\`json +{ + "file": "${context.file}", + "conversationContext": { + "step": "analyze" + } +} +\`\`\` + +--- + +💡 **Tip**: If unsure, try **Auto Mode** first. You can always modify the generated decorators later or rerun in Interactive Mode for more control.`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2a-auto-mode.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2a-auto-mode.ts new file mode 100644 index 000000000..75962971f --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2a-auto-mode.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface AutoModeContext { + componentName: string; + file: string; + componentId: string; + selectedPropCount: number; + autoConfigCount: number; + autoInferredCount: number; + hasNoSuitableProps: boolean; + selectedProps: string; + decoratorCode: string; + componentGroup: string; +} + +export function renderAutoMode(context: AutoModeContext): string { + return `# Auto Mode - Page Designer Decorator Generation + +## LLM INSTRUCTIONS + +### Auto Mode Behavior + +**Single-Step Execution:** +- NO user interaction required +- NO questions to ask +- Analyze component automatically +- Auto-select all suitable props +- Auto-infer types based on naming patterns +- Generate code immediately + +**Auto-Selection Criteria:** +- ✅ Include: Simple props (string, number, boolean) +- ❌ Exclude: Complex types (objects, arrays, functions) +- ❌ Exclude: UI-only props (className, style, etc.) + +**Auto-Inference Patterns:** +- \`*Url\`, \`*Link\` → \`url\` type +- \`*Image\`, \`*Icon\` → \`image\` type +- \`is*\`, \`has*\`, \`enable*\`, \`show*\` → \`boolean\` type (default: false) +- \`*Size\` → \`enum\` type (values: ['default', 'primary', 'secondary']) +- \`*Variant\` → \`enum\` type (values: ['default', 'primary', 'secondary']) + +**Regions:** +- NOT configured in auto mode +- User must use interactive mode for regions + +### Component Analysis Summary + +**Component Name**: ${context.componentName} +**File**: ${context.file} +**Component ID**: ${context.componentId} +**Selected Props**: ${context.selectedPropCount} +**Auto-configured**: ${context.autoConfigCount} +**Auto-inferred**: ${context.autoInferredCount} + +${ + context.hasNoSuitableProps + ? `⚠️ **No suitable props found**. The component has only complex or UI-only props. +Consider adding new attributes manually or using interactive mode.` + : '' +} + +--- + +# USER-FACING RESPONSE + +# ✅ Page Designer Decorators Generated (Auto Mode) + +## Auto-Configuration Summary + +- **Component**: \`${context.componentName}\` +- **Component ID**: \`${context.componentId}\` +- **File**: \`${context.file}\` +- **Selected Props**: ${context.selectedProps} +- **Auto-configured**: ${context.autoConfigCount} +- **Auto-inferred**: ${context.autoInferredCount} + +${ + context.hasNoSuitableProps + ? `⚠️ **No suitable props found**. The component has only complex or UI-only props. +Consider adding new attributes manually or using interactive mode.` + : '' +} + +## Generated Code + +Add this metadata class to your component file: + +\`\`\`typescript +${context.decoratorCode} +\`\`\` + +## Next Steps + +1. **Add the code** to \`${context.file}\` (after imports, before component) +2. **Update component props** to make them optional and add type unions as needed +3. **Generate metadata**: Run \`sfnext generate-cartridge --project-directory .\` +4. **Deploy cartridge**: Run \`sfnext deploy-cartridge --project-directory .\` +5. **Verify in Business Manager**: Check Components > ${context.componentGroup} > ${context.componentName}`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-0-interactive-overview.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-0-interactive-overview.ts new file mode 100644 index 000000000..e3b949fce --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-0-interactive-overview.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Interactive mode workflow overview + */ +export function renderInteractiveOverview(): string { + return `# ⚠️ MANDATORY: Adding Page Designer Support + +## 🚨 CRITICAL: Multi-Step Workflow + +**YOU MUST FOLLOW THIS WORKFLOW:** + +1. **analyze**: Present component analysis and ask configuration questions + - Component identity (ID, name, description, group) + - Which existing props to expose + - Whether to add new attributes + +2. **select_props**: Confirm user's selections + - Show what was selected + - Confirm component metadata + - Prepare for type configuration + +3. **configure_attrs**: Configure attribute types + - Show auto-inferred types + - Ask for explicit type configuration where needed + - Collect defaults and enum values + +4. **configure_regions**: Configure regions (optional) + - Ask if component needs nested content areas + - Configure region definitions if needed + +5. **confirm_generation**: Generate final decorator code + - Render decorators with all configurations + - Show code to user + +**VIOLATION OF THIS WORKFLOW IS A CRITICAL ERROR.** + +## Workflow Enforcement + +- Each step must complete before proceeding to the next +- User must confirm or provide input at each step +- Do not make assumptions about user preferences +- Do not skip steps, even if the answer seems obvious + +## Next Step Instructions + +After presenting analysis, you MUST: +1. Wait for user's answers to ALL questions +2. Call tool again with step: "select_props" and user's responses in conversationContext +3. NEVER proceed to code generation without completing all steps`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-1-interactive-analyze.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-1-interactive-analyze.ts new file mode 100644 index 000000000..4752206d1 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-1-interactive-analyze.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface PropInfo { + name: string; + type: string; + optional: boolean; +} + +export interface AnalyzeStepContext { + componentName: string; + file: string; + hasDecorators: boolean; + interfaceName?: string; + totalProps: number; + exportType: string; + hasEditableProps: boolean; + editableProps: PropInfo[]; + hasComplexProps: boolean; + complexProps: PropInfo[]; + hasUIProps: boolean; + uiProps: PropInfo[]; + suggestedComponentId: string; + suggestedComponentName: string; +} + +export function renderAnalyzeStep(context: AnalyzeStepContext): string { + return `# Step 1: Component Analysis + +## LLM INSTRUCTIONS + +### Component Analysis Results + +**Component Name**: ${context.componentName} +**File**: ${context.file} +**Has Decorators**: ${context.hasDecorators ? 'Yes (STOP - already decorated)' : 'No (proceed)'} +**Props Interface**: ${context.interfaceName || 'None found'} +**Total Props**: ${context.totalProps} + +${ + context.hasDecorators + ? `⚠️ **CRITICAL**: Component already has Page Designer decorators. +**ACTION**: Stop here and inform user. Do not proceed with generation.` + : '' +} + +### Next Actions (LLM) + +1. Present the analysis to the user +2. Ask all configuration questions (component identity, props selection, new properties) +3. Wait for user's complete response +4. THEN call tool again with step: "select_props" with collected answers + +--- + +# USER-FACING RESPONSE + +${ + context.hasDecorators + ? `# Analysis: ${context.componentName} + +✅ **This component already has Page Designer support.** + +The component has existing decorators (@Component, @AttributeDefinition, etc.). + +Would you like to modify the existing decorators instead?` + : `# Analysis: ${context.componentName} + +## Current State + +- **Component**: \`${context.componentName}\` +- **File**: \`${context.file}\` +- **Props Interface**: \`${context.interfaceName || 'None found'}\` +- **Export Type**: ${context.exportType} + +## Existing Properties Analysis + +${ + context.hasEditableProps + ? `### ✅ Suitable for Page Designer: + +${context.editableProps.map((prop) => `- \`${prop.name}\` (${prop.type})${prop.optional ? ' - optional' : ''}`).join('\n')}` + : '### ⚠️ No suitable properties found' +} + +${ + context.hasComplexProps + ? `### ⚠️ Complex (needs simplification): + +${context.complexProps.map((prop) => `- \`${prop.name}\` (${prop.type}) - Too complex for Page Designer`).join('\n')} + +These complex types cannot be used directly. Consider creating simpler alternatives.` + : '' +} + +${ + context.hasUIProps + ? `### 🎨 UI Props (typically not exposed): + +${context.uiProps.map((prop) => `- \`${prop.name}\` (${prop.type})`).join('\n')} + +These are styling/layout props, usually not exposed to Page Designer.` + : '' +} + +## Configuration Questions + +### 1️⃣ Component Identity + +I suggest: +- **ID**: \`${context.suggestedComponentId}\` +- **Name**: "${context.suggestedComponentName}" +- **Description**: *[Please provide a description]* +- **Group**: \`odyssey_base\` (default) or specify custom group + +✏️ Are these acceptable, or would you like to change them? + +### 2️⃣ Existing Properties + +${ + context.hasEditableProps + ? `**Which properties should be editable in Page Designer?** + +${context.editableProps.map((prop) => `- [ ] \`${prop.name}\` - ${prop.type}`).join('\n')}` + : '⚠️ No existing properties are suitable.' +} + +### 3️⃣ New Properties + +**Should I add any new properties** that don't exist in the component interface? + +Examples: +- Button text, labels, or headings +- Toggle flags (show/hide elements) +- Configuration options + +--- + +**Please answer these questions to proceed.**` +}`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-2-interactive-select-props.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-2-interactive-select-props.ts new file mode 100644 index 000000000..7f7f4e98b --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-2-interactive-select-props.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface NewAttribute { + name: string; + description?: string; + required?: boolean; +} + +export interface SelectPropsContext { + componentMetadata: { + id: string; + name: string; + description: string; + group?: string; + }; + selectedProps: string[]; + newAttributes: NewAttribute[]; + selectedPropsCount: number; + newAttributesCount: number; + totalAttributeCount: number; + hasSelectedProps: boolean; + hasNewAttributes: boolean; +} + +export function renderSelectPropsConfirmation(context: SelectPropsContext): string { + return `# Step 2: Selection Confirmation + +## LLM INSTRUCTIONS + +### Purpose +Present a clear confirmation of the user's selections from Step 1 (analyze). + +### Context Provided +- Component identity (id, name, description, group) +- Array of selected existing prop names +- Array of new attributes to add +- Counts and flags + +### Next Actions +After showing confirmation, instruct user to confirm proceeding to type configuration. + +--- + +# USER-FACING RESPONSE + +# ✅ Selection Confirmed + +## Component Configuration + +- **ID**: \`${context.componentMetadata.id}\` +- **Name**: "${context.componentMetadata.name}" +- **Description**: "${context.componentMetadata.description}" +- **Group**: \`${context.componentMetadata.group || 'odyssey_base'}\` + +${ + context.hasSelectedProps + ? `## 📋 Selected Existing Props (${context.selectedPropsCount}) + +${context.selectedProps.map((prop) => `- \`${prop}\``).join('\n')}` + : `## 📋 Selected Existing Props + +None selected.` +} + +${ + context.hasNewAttributes + ? `## ➕ New Attributes to Add (${context.newAttributesCount}) + +${context.newAttributes.map((attr) => `- \`${attr.name}\`${attr.description ? ` - ${attr.description}` : ''}${attr.required ? ' (required)' : ''}`).join('\n')}` + : `## ➕ New Attributes to Add + +None requested.` +} + +--- + +## 🎯 Next Step: Attribute Configuration + +Now I'll analyze the types for these ${context.totalAttributeCount} attribute(s) and help you configure them for Page Designer. + +**Please confirm**: Ready to proceed with type configuration? (Say "yes" or "proceed")`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts new file mode 100644 index 000000000..03a06d707 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface TypeSuggestion { + type: string; + priority: string; + reason: string; +} + +export interface ConfigureAttrsContext { + totalAttributes: number; + autoInferredCount: number; + needsConfigCount: number; + hasAutoInferred: boolean; + autoInferredAttrs: Array<{name: string; tsType: string}>; + hasNeedsConfig: boolean; + needsConfigAttrs: Array<{ + name: string; + tsType: string; + source: string; + hasSuggestions: boolean; + suggestions: TypeSuggestion[]; + suggestedTypes: string; + humanReadableName: string; + hasEnumSuggestion: boolean; + }>; +} + +export function renderConfigureAttrs(context: ConfigureAttrsContext): string { + return `# Step 2: Attribute Configuration + +## LLM INSTRUCTIONS + +### Analysis Complete + +Total attributes to configure: ${context.totalAttributes} +Auto-inferred: ${context.autoInferredCount} +Need configuration: ${context.needsConfigCount} + +### Next Steps for LLM + +**WAIT for user to:** +1. Provide explicit type overrides (if desired) +2. Provide custom names/descriptions (if desired) +3. Provide enum values (if applicable) +4. Or confirm "use defaults" + +**THEN** call tool again with step: "configure_regions" + +--- + +# USER-FACING RESPONSE + +# Attribute Configuration + +${ + context.hasAutoInferred + ? `## ✅ Auto-Configured Attributes + +These attributes will use auto-inferred types (no explicit configuration needed): + +${context.autoInferredAttrs.map((attr) => `- **${attr.name}** (${attr.tsType}) → Auto-inferred as Page Designer type`).join('\n')}` + : '' +} + +${ + context.hasNeedsConfig + ? `## ⚙️ Attributes Needing Configuration + +${context.needsConfigAttrs + .map( + (attr, index) => `### ${index + 1}. \`${attr.name}\` + +- **TypeScript Type**: \`${attr.tsType}\` +- **Source**: ${attr.source} + +${ + attr.hasSuggestions + ? `**Recommendations**: + +${attr.suggestions.map((s) => `- **${s.type}** (${s.priority} priority): ${s.reason}`).join('\n')}` + : '' +} + +**Questions**: +- What Page Designer type should this be? (${attr.suggestedTypes}) +- Custom display name? (default: "${attr.humanReadableName}") +- Default value? +${attr.hasEnumSuggestion ? '- Enum values? (e.g., ["option1", "option2", "option3"])' : ''}`, + ) + .join('\n\n')}` + : '' +} + +--- + +**Please provide configuration for attributes that need it, or say "use defaults" to proceed.**`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts new file mode 100644 index 000000000..6e7025819 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface ConfigureRegionsContext { + componentName: string; +} + +export function renderConfigureRegions(context: ConfigureRegionsContext): string { + return `# Step 3: Region Configuration + +## LLM INSTRUCTIONS + +### 🚨 CRITICAL: Ask User About Regions + +**YOU MUST:** +1. Ask user if component needs regions for nested content +2. If YES, ask for region configurations: + - Region ID (e.g., "main", "sidebar", "footer") + - Region name (display name) + - Description (optional) + - Max components (optional) + - Component type filters (optional) +3. Wait for user response +4. THEN call tool again with step: "confirm_generation" and regionConfig filled + +### Region Context + +Regions allow business users to nest other components inside this component. +Examples: Hero with content slots, Layout containers, Section wrappers + +--- + +# USER-FACING RESPONSE + +# Step 3: Region Configuration + +**Component**: ${context.componentName} + +## About Regions + +Regions define areas where business users can insert other components in Page Designer. +Use regions for: +- Layout containers (e.g., grid, flex layouts) +- Content areas with multiple components +- Sections that need nested content + +**Does this component need regions for nested content?** + +Examples: +- ✅ **YES** for: Layout, Container, Section, Grid +- ❌ **NO** for: Button, Image, Text, ProductCarousel + +--- + +**Please answer:** +1. Does this component need regions? (yes/no) +2. If yes, provide region configuration(s) + +**Region Configuration Example:** +\`\`\` +- id: "main" + name: "Main Content" + description: "Primary content area" + maxComponents: 10 +\`\`\``; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts new file mode 100644 index 000000000..331af2738 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface ConfirmGenerationContext { + decoratorCode: string; + componentName: string; + componentId: string; + componentGroup: string; + file: string; + attributeCount: number; + hasRegions: boolean; + regionCount?: number; +} + +export function renderConfirmGeneration(context: ConfirmGenerationContext): string { + return `# Step 5: Code Generation + +## LLM INSTRUCTIONS + +### Purpose +Present the generated Page Designer decorator code and next steps. + +### Context Provided +- Complete generated metadata class with decorators +- Component name, ID, group +- File path, attribute count, region info + +### Next Actions +1. Show the generated code +2. Provide clear next steps for deployment +3. Optionally offer to add code to the file + +--- + +# USER-FACING RESPONSE + +# ✅ Page Designer Decorators Generated + +## Generated Code + +Add this metadata class to \`${context.file}\`: + +\`\`\`typescript +${context.decoratorCode} +\`\`\` + +## Summary + +- **Component**: ${context.componentName} +- **Component ID**: \`${context.componentId}\` +- **Group**: \`${context.componentGroup}\` +- **Attributes**: ${context.attributeCount} +${context.hasRegions ? `- **Regions**: ${context.regionCount} configured` : ''} + +## Next Steps + +### 1. Add the Code + +Add the generated metadata class to \`${context.file}\`: +- Place it **after imports** +- Place it **before the component definition** + +### 2. Update Component Props (if needed) + +Make decorated props optional in your component interface: + +\`\`\`typescript +interface ${context.componentName}Props { + title?: string; // Add ? if attribute is not required + // ... other props +} +\`\`\` + +### 3. Generate Cartridge Metadata + +\`\`\`bash +cd packages/template-retail-rsc-app +pnpm sfnext generate-cartridge --project-directory . +\`\`\` + +This creates JSON files in \`cartridges/app_storefrontnext_base/cartridge/experience/components/\`. + +### 4. Deploy Cartridge + +\`\`\`bash +pnpm sfnext deploy-cartridge --project-directory . +\`\`\` + +### 5. Verify in Business Manager + +1. Log into Business Manager +2. Navigate to: **Merchant Tools > Site > Page Designer** +3. Find your component: **Components > ${context.componentGroup} > ${context.componentName}** +4. Verify all attributes appear correctly +5. Test editing a page with your component + +--- + +**Would you like me to add this code to your component file?**`; +} diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/templates/decorator-generator.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/templates/decorator-generator.ts new file mode 100644 index 000000000..ac3b6384d --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/templates/decorator-generator.ts @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface AttributeContext { + name: string; + tsType: string; + optional: boolean; + hasConfig: boolean; + config?: { + id?: string; + type?: string; + name?: string; + description?: string; + defaultValue?: unknown; + required?: boolean; + values?: string[]; + }; +} + +export interface RegionContext { + id: string; + name: string; + description?: string; + maxComponents?: number; + componentTypeInclusions?: string[]; + componentTypeExclusions?: string[]; +} + +export interface MetadataContext { + needsImports: boolean; + componentId: string; + componentName: string; + componentDescription: string; + componentGroup?: string; + metadataClassName: string; + hasAttributes: boolean; + hasRegions: boolean; + hasLoader: boolean; + regions: RegionContext[]; + attributes: AttributeContext[]; +} + +/** + * Generate a simple attribute decorator with auto-inferred type + * + * **Simple attributes** use default Page Designer behavior: + * - Type is inferred from TypeScript type + * - No custom configuration needed + * - Minimal decorator syntax + * + * **When to use:** + * - Basic string, number, or boolean props + * - No special validation or defaults needed + * - Standard field naming is acceptable + * + * @param attr - Attribute context + * @returns TypeScript code string for the attribute + * + * @example + * // Input: + * { name: 'title', tsType: 'string', optional: false, hasConfig: false } + * + * // Output: + * `@AttributeDefinition() + * title!: string;` + * + * @internal + */ +function generateSimpleAttribute(attr: AttributeContext): string { + return ` @AttributeDefinition() + ${attr.name}${attr.optional ? '?' : '!'}: ${attr.tsType};`; +} + +/** + * Generate a configured attribute decorator with explicit settings + * + * **Configured attributes** specify custom Page Designer behavior: + * - Explicit `type` (url, image, enum, etc.) + * - Custom `name` for display in Page Designer UI + * - `description` for merchant guidance + * - `defaultValue` for new instances + * - `required` flag for validation + * - `values` array for enum types + * + * **When to use:** + * - URL, image, or rich text fields (need specific editors) + * - Enum fields with predefined options + * - Fields with default values + * - Fields with merchant-friendly names + * + * @param attr - Attribute context with configuration + * @returns TypeScript code string for the configured attribute + * + * @example + * // Input (URL field): + * { + * name: 'ctaUrl', + * tsType: 'string', + * optional: false, + * hasConfig: true, + * config: { + * type: 'url', + * name: 'CTA Button URL', + * description: 'Destination URL for the call-to-action button' + * } + * } + * + * // Output: + * `@AttributeDefinition({ + * type: 'url', + * name: 'CTA Button URL', + * description: 'Destination URL for the call-to-action button', + * }) + * ctaUrl!: string;` + * + * @example + * // Input (Enum field): + * { + * name: 'variant', + * tsType: 'string', + * optional: false, + * hasConfig: true, + * config: { + * type: 'enum', + * name: 'Button Variant', + * values: ['primary', 'secondary', 'outline'], + * defaultValue: 'primary' + * } + * } + * + * // Output: + * `@AttributeDefinition({ + * type: 'enum', + * name: 'Button Variant', + * defaultValue: 'primary', + * values: ['primary', 'secondary', 'outline'], + * }) + * variant!: string;` + * + * @internal + */ +function generateConfiguredAttribute(attr: AttributeContext): string { + if (!attr.config) { + return generateSimpleAttribute(attr); + } + + const config = attr.config; + const configLines: string[] = []; + + if (config.id) { + configLines.push(` id: '${config.id}',`); + } + if (config.name) { + configLines.push(` name: '${config.name}',`); + } + if (config.type) { + configLines.push(` type: '${config.type}',`); + } + if (config.description) { + configLines.push(` description: '${config.description}',`); + } + if (config.defaultValue !== undefined) { + const valueStr = + typeof config.defaultValue === 'string' ? `'${config.defaultValue}'` : JSON.stringify(config.defaultValue); + configLines.push(` defaultValue: ${valueStr},`); + } + if (config.required !== undefined) { + configLines.push(` required: ${config.required},`); + } + if (config.values && config.values.length > 0) { + configLines.push(` values: [${config.values.map((v) => `'${v}'`).join(', ')}],`); + } + + return ` @AttributeDefinition({ +${configLines.join('\n')} + }) + ${attr.name}${attr.optional ? '?' : '!'}: ${attr.tsType};`; +} + +/** + * Generate import statements for Page Designer decorators + * + * **Decision logic:** + * - Always imports `Component` (required for all decorated components) + * - Conditionally imports `AttributeDefinition` (if component has editable props) + * - Conditionally imports `RegionDefinition` (if component has nested content areas) + * + * **Why conditional:** + * Avoids unused imports that would trigger linting warnings. + * + * @param context - Metadata context indicating what's needed + * @returns TypeScript import statements with trailing newlines + * + * @example + * // Component with attributes only: + * generateImports({ needsImports: true, hasAttributes: true, hasRegions: false }) + * // => `import { Component } from '@/lib/decorators/component'; + * // import { AttributeDefinition } from '@/lib/decorators/attribute-definition';\n\n` + * + * @example + * // Component with regions: + * generateImports({ needsImports: true, hasAttributes: true, hasRegions: true }) + * // => All three decorators imported + * + * @internal + */ +function generateImports(context: MetadataContext): string { + if (!context.needsImports) { + return ''; + } + + const imports: string[] = [`import { Component } from '@/lib/decorators/component';`]; + + if (context.hasAttributes) { + imports.push(`import { AttributeDefinition } from '@/lib/decorators/attribute-definition';`); + } + + if (context.hasRegions) { + imports.push(`import { RegionDefinition } from '@/lib/decorators';`); + } + + return `${imports.join('\n')}\n\n`; +} + +/** + * Generate @RegionDefinition decorator for nested content areas + * + * **Regions** define areas where merchants can add nested components in Page Designer. + * Common use cases: + * - Layout containers (grid cells, columns) + * - Content sections (header, body, footer) + * - Tab panels, accordion items + * + * **Configuration options:** + * - `id`: Unique identifier for the region + * - `name`: Display name in Page Designer + * - `description`: Merchant guidance + * - `maxComponents`: Limit number of nested components + * - `componentTypeInclusions`: Whitelist of allowed component types + * - `componentTypeExclusions`: Blacklist of disallowed component types + * + * @param context - Metadata context with region definitions + * @returns TypeScript code for @RegionDefinition decorator or empty string + * + * @example + * // Simple region: + * { + * hasRegions: true, + * regions: [{ + * id: 'main', + * name: 'Main Content Area', + * description: 'Add content components here' + * }] + * } + * // => `@RegionDefinition([ + * // { + * // id: 'main', + * // name: 'Main Content Area', + * // description: 'Add content components here', + * // } + * // ])\n` + * + * @example + * // Constrained region: + * { + * hasRegions: true, + * regions: [{ + * id: 'grid', + * name: 'Product Grid', + * maxComponents: 12, + * componentTypeInclusions: ['product-tile', 'product-card'] + * }] + * } + * + * @internal + */ +function generateRegionDefinition(context: MetadataContext): string { + if (!context.hasRegions || context.regions.length === 0) { + return ''; + } + + const regionsDef = context.regions + .map((region) => { + const lines: string[] = [` {`, ` id: '${region.id}',`, ` name: '${region.name}',`]; + + if (region.description) { + lines.push(` description: '${region.description}',`); + } + if (region.maxComponents !== undefined) { + lines.push(` maxComponents: ${region.maxComponents},`); + } + if (region.componentTypeInclusions && region.componentTypeInclusions.length > 0) { + lines.push( + ` componentTypeInclusions: [${region.componentTypeInclusions.map((t) => `'${t}'`).join(', ')}],`, + ); + } + if (region.componentTypeExclusions && region.componentTypeExclusions.length > 0) { + lines.push( + ` componentTypeExclusions: [${region.componentTypeExclusions.map((t) => `'${t}'`).join(', ')}],`, + ); + } + + lines.push(` }`); + return lines.join('\n'); + }) + .join(',\n'); + + return `@RegionDefinition([\n${regionsDef}\n])\n`; +} + +/** + * Generate complete Page Designer decorator code for a React component + * + * **This is the main code generation function.** + * + * Produces a TypeScript class with decorators that: + * 1. Registers the component in Page Designer + * 2. Defines editable attributes (props) + * 3. Optionally defines nested content regions + * + * **Output structure:** + * ```typescript + * import { Component } from '...'; + * import { AttributeDefinition } from '...'; + * + * @Component('component-id', { + * name: 'Component Name', + * description: '...', + * group: 'category' + * }) + * @RegionDefinition([...]) // Optional + * export class ComponentMetadata { + * @AttributeDefinition({ ... }) + * prop1!: string; + * + * @AttributeDefinition() + * prop2?: number; + * } + * ``` + * + * **Generated code must be:** + * - Added to the component file (after imports, before component) + * - Compiled with TypeScript + * - Used by `generate_page_designer_metadata` tool to create JSON metadata + * + * @param context - Complete metadata context + * @returns TypeScript code string ready to paste into component file + * + * @example + * // Simple component with attributes: + * generateDecoratorCode({ + * needsImports: true, + * componentId: 'hero-banner', + * componentName: 'Hero Banner', + * componentDescription: 'Large hero section with image and CTA', + * componentGroup: 'content', + * metadataClassName: 'HeroBannerMetadata', + * hasAttributes: true, + * hasRegions: false, + * hasLoader: false, + * regions: [], + * attributes: [ + * { name: 'title', tsType: 'string', optional: false, hasConfig: false }, + * { name: 'imageUrl', tsType: 'string', optional: false, hasConfig: true, + * config: { type: 'image', name: 'Background Image' } } + * ] + * }) + * + * @example + * // Layout component with regions: + * generateDecoratorCode({ + * needsImports: true, + * componentId: 'two-column', + * componentName: 'Two Column Layout', + * componentDescription: 'Side-by-side content layout', + * componentGroup: 'layout', + * metadataClassName: 'TwoColumnMetadata', + * hasAttributes: false, + * hasRegions: true, + * hasLoader: false, + * regions: [ + * { id: 'left', name: 'Left Column' }, + * { id: 'right', name: 'Right Column' } + * ], + * attributes: [] + * }) + * + * @public + */ +export function generateDecoratorCode(context: MetadataContext): string { + const imports = generateImports(context); + + const componentDecorator = `@Component('${context.componentId}', { + name: '${context.componentName}', + description: '${context.componentDescription}',${context.componentGroup ? `\n group: '${context.componentGroup}',` : ''} +})`; + + const regionDefinition = generateRegionDefinition(context); + + const attributes = context.attributes + .map((attr) => { + return attr.hasConfig ? generateConfiguredAttribute(attr) : generateSimpleAttribute(attr); + }) + .join('\n\n'); + + return `${imports}${componentDecorator} +${regionDefinition}export class ${context.metadataClassName} { +${attributes} +}`; +} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md index 9ef193128..213f96a34 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md @@ -80,6 +80,68 @@ MCP tools for Storefront Next development with React Server Components. } ``` +### `add_page_designer_decorator` + +Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to existing React components for Storefront Next. + +**Status**: ✅ Implemented (non-GA - use `--allow-non-ga-tools` flag) + +**Use cases**: + +- Add Page Designer support to new components +- Convert existing components to be Page Designer-compatible +- Generate decorator code automatically or interactively +- Configure component attributes and regions for Page Designer + +**Key features**: + +- **Name-Based Lookup**: Find components by name (e.g., "ProductCard") without knowing paths +- **Auto Mode**: Automatically generates decorators with sensible defaults +- **Interactive Mode**: Step-by-step workflow for fine-tuned control +- **Auto-Discovery**: Searches common component directories automatically +- **Type Inference**: Automatically infers Page Designer types from prop names + +**Parameters**: + +- `component` (required, string): Component name (e.g., "ProductCard") or file path +- `autoMode` (optional, boolean): Enable auto mode for quick setup +- `searchPaths` (optional, array): Additional directories to search for components +- `componentId` (optional, string): Override component ID +- `conversationContext` (optional, object): For interactive mode workflow steps + +**Modes**: + +- **Auto Mode**: Generates decorators immediately with sensible defaults +- **Interactive Mode**: Multi-step workflow with user confirmation at each stage + +**Returns**: Generated decorator code and instructions for adding to component file + +**Example usage**: + +```json +// Auto mode (quick setup) +{ + "name": "add_page_designer_decorator", + "arguments": { + "component": "ProductCard", + "autoMode": true + } +} + +// Interactive mode (step-by-step) +{ + "name": "add_page_designer_decorator", + "arguments": { + "component": "Hero", + "conversationContext": { + "step": "analyze" + } + } +} +``` + +**See also**: [Detailed documentation](./page-designer-decorator/README.md) for complete usage guide, architecture details, and examples. + ## Implementation Details ### Architecture diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts index e243a0226..a3f21c049 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts @@ -28,6 +28,7 @@ import type {McpTool} from '../../utils/index.js'; import type {Services} from '../../services.js'; import {createToolAdapter, jsonResult} from '../adapter.js'; import {createDeveloperGuidelinesTool} from './developer-guidelines.js'; +import {createPageDesignerDecoratorTool} from '../page-designer-decorator/index.js'; /** * Common input type for placeholder tools. @@ -100,6 +101,7 @@ function createPlaceholderTool(name: string, description: string, services: Serv export function createStorefrontNextTools(services: Services): McpTool[] { return [ createDeveloperGuidelinesTool(services), + createPageDesignerDecoratorTool(services), createPlaceholderTool( 'storefront_next_site_theming', 'Configure and manage site theming for Storefront Next', diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md new file mode 100644 index 000000000..00b707aa0 --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md @@ -0,0 +1,120 @@ +# Testing Page Designer Decorator Tool + +## Test Status + +The page-designer-decorator tool has comprehensive unit tests covering: +- ✅ Tool metadata (name, description, toolsets, isGA) +- ✅ Mode selection flow +- ✅ Error handling +- ✅ Input validation + +Some integration tests may require actual component files in a real workspace to fully validate component resolution. + +## Testing Approaches + +### 1. Unit Tests (Automated) + +Run the test suite: + +```bash +cd packages/b2c-dx-mcp +pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts +``` + +### 2. MCP Inspector (Interactive Testing) + +Use the MCP Inspector to test the tool interactively: + +```bash +cd packages/b2c-dx-mcp +pnpm run inspect:dev +``` + +Then in the inspector: +1. Click **Connect** +2. Click **List Tools** - you should see `add_page_designer_decorator` +3. Click on the tool to test it with real inputs + +### 3. CLI Testing + +Test via command line: + +```bash +# List all tools (should include add_page_designer_decorator) +npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools --method tools/list + +# Call the tool +npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ + --method tools/call \ + --tool-name add_page_designer_decorator \ + --args '{"component": "MyComponent"}' +``` + +### 4. Manual Testing with Real Components + +1. Set up a Storefront Next project (or use an existing one) +2. Create a test component: + +```tsx +// src/components/TestComponent.tsx +export interface TestComponentProps { + title: string; + description?: string; +} + +export default function TestComponent({title, description}: TestComponentProps) { + return
{title}
; +} +``` + +3. Set environment variable: +```bash +export SFCC_WORKING_DIRECTORY=/path/to/storefront-next +``` + +4. Use the tool via MCP Inspector or your IDE's MCP integration + +### 5. Test Scenarios + +#### Mode Selection +```json +{ + "component": "TestComponent" +} +``` +Expected: Returns mode selection instructions + +#### Auto Mode +```json +{ + "component": "src/components/TestComponent.tsx", + "autoMode": true +} +``` +Expected: Generates decorators automatically + +#### Interactive Mode - Analyze Step +```json +{ + "component": "src/components/TestComponent.tsx", + "conversationContext": { + "step": "analyze" + } +} +``` +Expected: Returns component analysis + +## Troubleshooting + +### Component Not Found Errors + +If you get "Component not found" errors: +1. Verify `SFCC_WORKING_DIRECTORY` is set correctly +2. Check that the component file exists at the expected path +3. Try using the full relative path: `"component": "src/components/MyComponent.tsx"` + +### Validation Errors + +If you get Zod validation errors: +- Check that all required fields are provided +- Verify field types match the schema (e.g., `component` must be a string) diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts new file mode 100644 index 000000000..d738c71b0 --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -0,0 +1,739 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import {createPageDesignerDecoratorTool} from '../../../src/tools/page-designer-decorator/index.js'; +import {Services} from '../../../src/services.js'; +import type {ToolResult} from '../../../src/utils/types.js'; +import {existsSync, mkdirSync, writeFileSync, rmSync} from 'node:fs'; +import path from 'node:path'; +import {tmpdir} from 'node:os'; + +/** + * Helper to extract text from a ToolResult. + * Throws if the first content item is not a text type. + * + * @param result - The ToolResult to extract text from + * @returns The text content from the first content item + * @throws {Error} If the first content item is not a text type + */ +function getResultText(result: ToolResult): string { + const content = result.content[0]; + if (content.type !== 'text') { + throw new Error(`Expected text content, got ${content.type}`); + } + return content.text; +} + +/** + * Create a mock services instance for testing. + * + * @returns A new Services instance with empty configuration + */ +function createMockServices(): Services { + return new Services({}); +} + +/** + * Create a temporary test component file. + * Creates components in the standard location that the tool searches for (`src/components/`). + * + * The component will have: + * - A Props interface with the specified props + * - A default export function component + * - Proper copyright header + * + * @param dir - The test directory root where the component should be created + * @param componentName - The name of the component (e.g., "TestComponent") + * @param props - Optional props string in the format "propName: type; propName2: type;" + * If not provided, defaults to "title: string;" + * @returns The absolute path to the created component file + * + * @example + * ```typescript + * const path = createTestComponent(testDir, 'MyComponent', 'title: string; count: number;'); + * // Creates: {testDir}/src/components/MyComponent.tsx + * ``` + */ +function createTestComponent(dir: string, componentName: string, props?: string): string { + // Create in src/components/ which is the standard search location + const componentPath = path.join(dir, 'src', 'components', `${componentName}.tsx`); + mkdirSync(path.dirname(componentPath), {recursive: true}); + + // Extract prop names for the component function + const propNames = props + ? props + .split(';') + .map((p) => p.trim()) + .filter((p) => p.length > 0) + .map((p) => p.split(':')[0].trim()) + .join(', ') + : 'title'; + + const componentContent = `/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +export interface ${componentName}Props { + ${props || 'title: string;'} +} + +export default function ${componentName}({${propNames}}: ${componentName}Props) { + return
{${propNames.split(',')[0].trim()}}
; +} +`; + + writeFileSync(componentPath, componentContent, 'utf8'); + return componentPath; +} + +/** + * Tests for the page-designer-decorator MCP tool. + * + * This test suite covers: + * - Tool metadata (name, description, toolsets, isGA) + * - Mode selection workflow + * - Auto mode decorator generation + * - Interactive mode workflow (all steps) + * - Component resolution (by name, path, custom searchPaths) + * - Input validation + * - Error handling + * - Output format validation + * + * Tests use temporary directories and mock components to avoid dependencies + * on real project files. + */ +describe('tools/page-designer-decorator', () => { + let services: Services; + let testDir: string; + let originalCwd: string; + + beforeEach(() => { + services = createMockServices(); + // Create a temporary directory for test components + testDir = path.join(tmpdir(), `b2c-mcp-test-${Date.now()}`); + mkdirSync(testDir, {recursive: true}); + originalCwd = process.cwd(); + process.chdir(testDir); + // Set SFCC_WORKING_DIRECTORY to the test directory + process.env.SFCC_WORKING_DIRECTORY = testDir; + }); + + afterEach(() => { + process.chdir(originalCwd); + if (existsSync(testDir)) { + rmSync(testDir, {recursive: true, force: true}); + } + delete process.env.SFCC_WORKING_DIRECTORY; + }); + + describe('tool metadata', () => { + it('should have correct tool name', () => { + const tool = createPageDesignerDecoratorTool(services); + expect(tool.name).to.equal('add_page_designer_decorator'); + }); + + it('should have comprehensive description', () => { + const tool = createPageDesignerDecoratorTool(services); + const desc = tool.description; + + // Should mention Page Designer + expect(desc).to.include('Page Designer'); + expect(desc).to.include('decorator'); + + // Should mention modes + expect(desc).to.match(/AUTO MODE|auto mode/i); + expect(desc).to.match(/INTERACTIVE MODE|interactive mode/i); + + // Should mention key features + expect(desc).to.include('@Component'); + expect(desc).to.include('@AttributeDefinition'); + }); + + it('should be in STOREFRONTNEXT toolset', () => { + const tool = createPageDesignerDecoratorTool(services); + expect(tool.toolsets).to.include('STOREFRONTNEXT'); + expect(tool.toolsets).to.have.lengthOf(1); + }); + + it('should not be GA (generally available)', () => { + const tool = createPageDesignerDecoratorTool(services); + expect(tool.isGA).to.be.false; + }); + }); + + describe('mode selection', () => { + it('should show mode selection when called with only component name', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'TestComponent'); + + const result = await tool.handler({ + component: 'TestComponent', + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should present mode selection options + expect(text).to.match(/mode|Mode/i); + expect(text).to.match(/auto|Auto/i); + expect(text).to.match(/interactive|Interactive/i); + expect(text).to.include('TestComponent'); + }); + + it('should use SFCC_WORKING_DIRECTORY if set', async () => { + const tool = createPageDesignerDecoratorTool(services); + const customDir = path.join(tmpdir(), `b2c-mcp-test-custom-${Date.now()}`); + mkdirSync(customDir, {recursive: true}); + createTestComponent(customDir, 'CustomComponent'); + + process.env.SFCC_WORKING_DIRECTORY = customDir; + + const result = await tool.handler({ + component: 'CustomComponent', + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.match(/mode|Mode/i); + + rmSync(customDir, {recursive: true, force: true}); + }); + }); + + describe('auto mode', () => { + it('should generate decorators in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'AutoComponent', 'title: string;'); + + const result = await tool.handler({ + component: 'AutoComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should generate decorator code + expect(text).to.include('@Component'); + expect(text).to.include('@AttributeDefinition'); + expect(text).to.include('AutoComponent'); + expect(text).to.include('title'); + }); + + it('should handle component with multiple props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'MultiPropComponent', + `title: string; +description: string; +imageUrl: string;`, + ); + + const result = await tool.handler({ + component: 'MultiPropComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should include decorators for multiple props + expect(text).to.include('@Component'); + expect(text).to.include('title'); + expect(text).to.include('description'); + expect(text).to.include('imageUrl'); + }); + + it('should exclude complex props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'ComplexPropsComponent', + `title: string; +onClick: () => void; +config: { key: string; value: number };`, + ); + + const result = await tool.handler({ + component: 'ComplexPropsComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should include simple props in generated decorators + expect(text).to.include('title'); + // Complex props should not appear in @AttributeDefinition decorators + // (they might appear in instructions, but not in the actual decorator code) + const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); + if (decoratorCodeMatch) { + const decoratorCode = decoratorCodeMatch.join('\n'); + expect(decoratorCode).to.not.include('onClick'); + expect(decoratorCode).to.not.include('config'); + } + }); + + it('should exclude UI-only props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'UIPropsComponent', + `title: string; +className: string; +style: React.CSSProperties;`, + ); + + const result = await tool.handler({ + component: 'UIPropsComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should include content props in generated decorators + expect(text).to.include('title'); + // UI-only props should not appear in @AttributeDefinition decorators + // (they might appear in instructions, but not in the actual decorator code) + const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); + if (decoratorCodeMatch) { + const decoratorCode = decoratorCodeMatch.join('\n'); + expect(decoratorCode).to.not.include('className'); + expect(decoratorCode).to.not.include('style'); + } + }); + }); + + describe('interactive mode', () => { + describe('analyze step', () => { + it('should analyze component in analyze step', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'AnalyzeComponent', 'title: string;'); + + const result = await tool.handler({ + component: 'AnalyzeComponent', + conversationContext: { + step: 'analyze', + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should show analysis results + expect(text).to.match(/component|Component/i); + expect(text).to.match(/prop|Prop|attribute|Attribute/i); + expect(text).to.include('AnalyzeComponent'); + expect(text).to.include('title'); + }); + + it('should categorize props correctly', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'CategorizedComponent', + `title: string; +onClick: () => void; +className: string;`, + ); + + const result = await tool.handler({ + component: 'CategorizedComponent', + conversationContext: { + step: 'analyze', + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should identify editable props + expect(text).to.include('title'); + // Should mention props analysis (may use different terminology) + expect(text).to.match(/prop|Prop|attribute|Attribute|editable|suitable/i); + // Should mention complex or UI props (may be described differently) + expect(text).to.match(/complex|Complex|UI|ui|exclude|skip/i); + }); + }); + + describe('select_props step', () => { + it('should confirm selected props', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'SelectPropsComponent', 'title: string; description: string;'); + + const result = await tool.handler({ + component: 'SelectPropsComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title', 'description'], + componentMetadata: { + id: 'select-props-component', + name: 'Select Props Component', + description: 'Test component', + }, + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should confirm selections + expect(text).to.include('title'); + expect(text).to.include('description'); + expect(text).to.include('Select Props Component'); + }); + + it('should require component metadata', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'MissingMetadataComponent'); + + const result = await tool.handler({ + component: 'MissingMetadataComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title'], + // Missing componentMetadata + }, + }); + + // Should return error when metadata is missing + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.match(/metadata|Metadata/i); + }); + }); + + describe('configure_attrs step', () => { + it('should provide attribute configuration instructions', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'ConfigureAttrsComponent', 'imageUrl: string; description: string;'); + + const result = await tool.handler({ + component: 'ConfigureAttrsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'description'], + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should provide configuration guidance + expect(text).to.match(/attribute|Attribute|configure|Configure/i); + expect(text).to.include('imageUrl'); + expect(text).to.include('description'); + }); + + it('should suggest types for props', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'TypeSuggestionsComponent', 'imageUrl: string; productId: string;'); + + const result = await tool.handler({ + component: 'TypeSuggestionsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'productId'], + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should suggest appropriate types + expect(text).to.match(/url|image|product/i); + }); + }); + + describe('configure_regions step', () => { + it('should provide region configuration instructions', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'RegionsComponent'); + + const result = await tool.handler({ + component: 'RegionsComponent', + conversationContext: { + step: 'configure_regions', + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should provide region configuration guidance + expect(text).to.match(/region|Region/i); + }); + }); + + describe('confirm_generation step', () => { + it('should generate decorator code when all context provided', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'ConfirmComponent', 'title: string;'); + + const result = await tool.handler({ + component: 'ConfirmComponent', + conversationContext: { + step: 'confirm_generation', + selectedProps: ['title'], + componentMetadata: { + id: 'confirm-component', + name: 'Confirm Component', + description: 'Test component', + }, + attributeConfig: { + title: { + type: 'string', + name: 'Title', + }, + }, + }, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should generate complete decorator code + expect(text).to.include('@Component'); + expect(text).to.include('@AttributeDefinition'); + expect(text).to.include('ConfirmComponent'); + expect(text).to.include('title'); + }); + + it('should require component metadata', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'MissingMetadataConfirmComponent'); + + const result = await tool.handler({ + component: 'MissingMetadataConfirmComponent', + conversationContext: { + step: 'confirm_generation', + // Missing componentMetadata + }, + }); + + // Should return error when metadata is missing + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.match(/metadata|Metadata/i); + }); + }); + }); + + describe('error handling', () => { + it('should handle non-existent component gracefully', async () => { + const tool = createPageDesignerDecoratorTool(services); + + const result = await tool.handler({ + component: 'NonExistentComponent', + }); + + // Should return an error result + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.match(/not found|error|Error/i); + }); + + it('should handle invalid input gracefully', async () => { + const tool = createPageDesignerDecoratorTool(services); + + // Invalid input should be caught by zod validation + const result = await tool.handler({ + component: 123, // Invalid type + } as unknown as Record); + + // Should return an error result + expect(result.isError).to.be.true; + }); + }); + + describe('component resolution', () => { + it('should find component by name in standard location', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'StandardLocationComponent'); + + const result = await tool.handler({ + component: 'StandardLocationComponent', + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('StandardLocationComponent'); + }); + + it('should find component by path', async () => { + const tool = createPageDesignerDecoratorTool(services); + const componentPath = createTestComponent(testDir, 'PathComponent'); + + const result = await tool.handler({ + component: path.relative(testDir, componentPath), + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('PathComponent'); + }); + + it('should use searchPaths when provided', async () => { + const tool = createPageDesignerDecoratorTool(services); + // Create component in a custom location + const customDir = path.join(testDir, 'custom', 'components'); + mkdirSync(customDir, {recursive: true}); + const componentPath = path.join(customDir, 'CustomLocationComponent.tsx'); + writeFileSync( + componentPath, + `export interface CustomLocationComponentProps { + title: string; +} + +export default function CustomLocationComponent({title}: CustomLocationComponentProps) { + return
{title}
; +} +`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'CustomLocationComponent', + searchPaths: ['custom/components'], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('CustomLocationComponent'); + }); + }); + + describe('input validation', () => { + it('should accept valid component name', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'ValidComponent'); + + const result = await tool.handler({ + component: 'ValidComponent', + }); + + // Should not error on valid input + expect(result.isError).to.be.undefined; + }); + + it('should accept component path', async () => { + const tool = createPageDesignerDecoratorTool(services); + const componentPath = createTestComponent(testDir, 'PathComponent'); + + const result = await tool.handler({ + component: path.relative(testDir, componentPath), + }); + + // Should not error on valid path + expect(result.isError).to.be.undefined; + }); + + it('should accept optional searchPaths', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'SearchComponent'); + + const result = await tool.handler({ + component: 'SearchComponent', + searchPaths: ['src/components'], + }); + + // Should not error with searchPaths + expect(result.isError).to.be.undefined; + }); + + it('should accept optional autoMode flag', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'AutoModeComponent'); + + const result = await tool.handler({ + component: 'AutoModeComponent', + autoMode: true, + }); + + // Should not error with autoMode + expect(result.isError).to.be.undefined; + }); + + it('should accept optional componentId', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'CustomIdComponent'); + + const result = await tool.handler({ + component: 'CustomIdComponent', + componentId: 'custom-component-id', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('custom-component-id'); + }); + + it('should accept conversationContext with all steps', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'ConversationComponent'); + + const steps = ['analyze', 'select_props', 'configure_attrs', 'configure_regions', 'confirm_generation']; + + const results = await Promise.all( + steps.map((step) => + tool.handler({ + component: 'ConversationComponent', + conversationContext: { + step: step as 'analyze' | 'configure_attrs' | 'configure_regions' | 'confirm_generation' | 'select_props', + }, + }), + ), + ); + + // Should not error on valid step + for (const [i, step] of steps.entries()) { + const result = results[i]; + if (step === 'select_props' || step === 'confirm_generation') { + // These steps require metadata, so they'll error without it + // But the step itself should be accepted + expect(result).to.exist; + } else { + expect(result.isError).to.be.undefined; + } + } + }); + }); + + describe('output format', () => { + it('should return text content in ToolResult format', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'FormatComponent'); + + const result = await tool.handler({ + component: 'FormatComponent', + }); + + expect(result).to.have.property('content'); + expect(result.content).to.be.an('array'); + expect(result.content.length).to.be.greaterThan(0); + expect(result.content[0]).to.have.property('type', 'text'); + expect(result.content[0]).to.have.property('text'); + }); + + it('should return error format when component not found', async () => { + const tool = createPageDesignerDecoratorTool(services); + + const result = await tool.handler({ + component: 'NonExistentComponent', + }); + + expect(result.isError).to.be.true; + expect(result.content).to.be.an('array'); + expect(result.content[0]).to.have.property('type', 'text'); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 772f9a208..3068614b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,9 +176,18 @@ importers: '@salesforce/b2c-tooling-sdk': specifier: workspace:* version: link:../b2c-tooling-sdk + glob: + specifier: ^11.0.0 + version: 11.1.0 + ts-morph: + specifier: ^24.0.0 + version: 24.0.0 zod: specifier: 3.25.76 version: 3.25.76 + zod-to-json-schema: + specifier: ^3.24.1 + version: 3.25.1(zod@3.25.76) devDependencies: '@eslint/compat': specifier: ^1 @@ -2796,6 +2805,9 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@ts-morph/common@0.25.0': + resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -3803,6 +3815,9 @@ packages: resolution: {integrity: sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==} engines: {node: '>=16'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -6093,6 +6108,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-case@3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} @@ -7048,6 +7066,9 @@ packages: peerDependencies: typescript: '>=4.0.0' + ts-morph@24.0.0: + resolution: {integrity: sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -10607,6 +10628,12 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + '@ts-morph/common@0.25.0': + dependencies: + minimatch: 9.0.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -11771,6 +11798,8 @@ snapshots: cockatiel@3.2.1: {} + code-block-writer@13.0.3: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -14275,6 +14304,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-browserify@1.0.1: {} + path-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -15377,6 +15408,11 @@ snapshots: picomatch: 4.0.3 typescript: 5.9.3 + ts-morph@24.0.0: + dependencies: + '@ts-morph/common': 0.25.0 + code-block-writer: 13.0.3 + ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7ef38bb45..dfe829bfd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,10 +7,12 @@ minimumReleaseAgeExclude: [] nodeLinker: hoisted -# Pin exact versions by default when adding dependencies (no ^ or ~ prefix) -savePrefix: '' - onlyBuiltDependencies: + - '@vscode/vsce-sign' + - esbuild + - keytar + - msw + - protobufjs - unrs-resolver - yarn @@ -28,6 +30,8 @@ overrides: preact@>=10.27.0 <10.27.3: '>=10.27.3' qs@<6.14.1: '>=6.14.1' +savePrefix: '' + trustPolicy: no-downgrade trustPolicyExclude: [] From a693fe16dc3faba03414c2ccd7c17d7163921c41 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Fri, 13 Feb 2026 15:23:37 -0800 Subject: [PATCH 02/14] Add manual test runner for page-designer-decorator tool- Add automated test runner script (index.test.mjs) with 24 test cases- Support both temporary directory mode (default) and real Storefront Next project mode- Update test documentation with usage instructions for both modes- Update tool README with manual test runner sectionThe test runner covers component discovery, auto mode, interactive mode,error handling, edge cases, and environment variable scenarios. --- .../tools/page-designer-decorator/README.md | 19 + .../tools/page-designer-decorator/README.md | 74 ++- .../page-designer-decorator/index.test.mjs | 601 ++++++++++++++++++ 3 files changed, 692 insertions(+), 2 deletions(-) create mode 100755 packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md index 931e2dec3..c5ac7e6d0 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -218,6 +218,8 @@ The tool uses direct function execution with no file I/O or compilation overhead ## ✅ Testing +### Automated Tests + ```bash pnpm build pnpm test @@ -225,6 +227,23 @@ pnpm test Comprehensive test suite covers all workflow modes, component discovery, and error handling. +### Manual Test Runner + +For quick manual verification, use the test runner script: + +```bash +cd packages/b2c-dx-mcp +pnpm build +node test/tools/page-designer-decorator/index.test.mjs all +``` + +Run a specific test case: +```bash +node test/tools/page-designer-decorator/index.test.mjs TC-1.1 +``` + +See [`test/tools/page-designer-decorator/README.md`](../../../test/tools/page-designer-decorator/README.md) for detailed testing instructions. + ## 🎓 Learning Resources - [Template Literals (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md index 00b707aa0..8a8595ca9 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md @@ -50,7 +50,77 @@ npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga --args '{"component": "MyComponent"}' ``` -### 4. Manual Testing with Real Components +### 4. Automated Manual Test Runner + +A test runner script is available to quickly verify the tool with various test scenarios: + +```bash +cd packages/b2c-dx-mcp +pnpm build # Build the package first +node test/tools/page-designer-decorator/index.test.mjs all +``` + +Run a specific test case: +```bash +node test/tools/page-designer-decorator/index.test.mjs TC-1.1 +``` + +The script covers 24 automated test cases including: +- Component discovery (name-based, path-based, nested, custom paths) +- Auto mode (basic, type inference, complex props exclusion) +- Interactive mode (mode selection, analyze step) +- Error handling (invalid input, missing parameters) +- Edge cases (no props, optional props, union types, collisions) +- Environment variables (SFCC_WORKING_DIRECTORY) + +See the script's JSDoc header for a complete list of available test cases. + +#### Running Tests Against a Local Storefront Next Installation + +The test runner script supports two modes: + +**1. Temporary Directory Mode (Default)** +Creates isolated test environments with temporary directories. Ideal for CI/CD and regression testing. + +```bash +cd packages/b2c-dx-mcp +pnpm build +node test/tools/page-designer-decorator/index.test.mjs all +``` + +**2. Real Storefront Next Project Mode** +Test against an existing Storefront Next installation by setting `SFCC_WORKING_DIRECTORY`: + +```bash +cd packages/b2c-dx-mcp +pnpm build +SFCC_WORKING_DIRECTORY=/path/to/storefront-next \ + node test/tools/page-designer-decorator/index.test.mjs all +``` + +Or set it as an environment variable: +```bash +export SFCC_WORKING_DIRECTORY=/path/to/storefront-next +cd packages/b2c-dx-mcp +pnpm build +node test/tools/page-designer-decorator/index.test.mjs TC-1.1 +``` + +**Important Notes for Real Project Mode**: +- Component discovery searches in your real Storefront Next project (`SFCC_WORKING_DIRECTORY`) +- Test components created by the script are placed in a temporary directory (not in your real project) +- The script will **not** modify your real project files (read-only) +- Tests will use existing components from your real project if they exist +- If a test component doesn't exist in your real project, the test will show a "component not found" result (this is expected) +- The real project directory is preserved after testing +- To test with specific components, ensure they exist in your real project's `src/components/` directory + +**Alternative Testing Methods**: +- **MCP Inspector**: Interactive UI testing (see section 2 above) +- **CLI Testing**: Command-line testing (see section 3 above) +- **Manual Test Plan**: Full integration testing including Business Manager and Page Designer (see [manual test plan](../../../../../Documents/page-designer-decorator-manual-test-plan.md) for TC-7.x tests) + +### 5. Manual Testing with Real Components 1. Set up a Storefront Next project (or use an existing one) 2. Create a test component: @@ -74,7 +144,7 @@ export SFCC_WORKING_DIRECTORY=/path/to/storefront-next 4. Use the tool via MCP Inspector or your IDE's MCP integration -### 5. Test Scenarios +### 6. Test Scenarios #### Mode Selection ```json diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs new file mode 100755 index 000000000..dc3124bda --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs @@ -0,0 +1,601 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Automated test runner for page-designer-decorator tool + * + * This script executes automated test cases by: + * 1. Creating test components in temporary directories + * 2. Invoking the tool with various test scenarios + * 3. Validating outputs + * + * **Modes**: + * - Default: Creates temporary directories and test components (isolated testing) + * - Real Project: Set SFCC_WORKING_DIRECTORY to use an existing Storefront Next project + * + * **Note**: This script covers automated test cases (TC-1.1 through TC-6.2). + * Test Category 7 (TC-7.1 through TC-7.5) requires a real Storefront Next + * installation and manual steps (Business Manager, Page Designer, cartridge + * deployment) and cannot be automated. See manual test plan for those tests. + * + * Usage: + * node test/tools/page-designer-decorator/index.test.mjs [test-case-id] + * + * Examples: + * # Run all tests with temporary directories (default) + * node test/tools/page-designer-decorator/index.test.mjs all + * + * # Run a specific test case + * node test/tools/page-designer-decorator/index.test.mjs TC-1.1 + * + * # Run tests against a real Storefront Next installation + * SFCC_WORKING_DIRECTORY=/path/to/storefront-next \ + * node test/tools/page-designer-decorator/index.test.mjs all + * + * Available automated test cases: + * Component Discovery: + * - TC-1.1: Name-Based Discovery (PascalCase) + * - TC-1.2: Name-Based Discovery (Kebab-Case) + * - TC-1.3: Nested Component Discovery + * - TC-1.4: Path-Based Input + * - TC-1.5: Custom Search Paths + * - TC-1.6: Component Not Found + * Auto Mode: + * - TC-2.1: Basic Auto Mode + * - TC-2.2: Auto Mode - Excludes Complex Props + * - TC-2.3: Auto Mode - Excludes UI-Only Props + * - TC-2.4: Auto Mode - Type Inference + * - TC-2.5: Auto Mode - Component Already Decorated + * - TC-2.6: Auto Mode - Custom Component ID + * Interactive Mode: + * - TC-3.1: Mode Selection + * - TC-3.2: Interactive Mode - Analyze Step + * Error Handling: + * - TC-4.1: Invalid Input Type + * - TC-4.2: Invalid Step Name + * - TC-4.3: Missing Required Parameter + * Edge Cases: + * - TC-5.1: Component with No Props + * - TC-5.2: Component with Only Complex Props + * - TC-5.3: Component with Optional Props + * - TC-5.4: Component with Union Types + * - TC-5.5: Component Name Collision + * Environment Variables: + * - TC-6.1: SFCC_WORKING_DIRECTORY Set + * - TC-6.2: SFCC_WORKING_DIRECTORY Not Set + * + * For real Storefront Next project tests (TC-7.x), see manual test plan: + * ~/Documents/page-designer-decorator-manual-test-plan.md + */ + +import {createPageDesignerDecoratorTool} from '../../../dist/tools/page-designer-decorator/index.js'; +import {Services} from '../../../dist/services.js'; +import {mkdirSync, writeFileSync, rmSync, existsSync, readFileSync} from 'node:fs'; +import path from 'node:path'; +import {tmpdir} from 'node:os'; + +const services = new Services({}); +const tool = createPageDesignerDecoratorTool(services); + +// Test directory - use SFCC_WORKING_DIRECTORY if set, otherwise create temporary directory +const useRealProject = Boolean(process.env.SFCC_WORKING_DIRECTORY); +const realProjectDir = useRealProject ? process.env.SFCC_WORKING_DIRECTORY : null; +const testDir = useRealProject + ? path.join(tmpdir(), `page-designer-test-${Date.now()}`) + : path.join(tmpdir(), `page-designer-test-${Date.now()}`); + +/** + * Create a test component file + */ +function createTestComponent(name, props = 'title: string;') { + const componentPath = path.join(testDir, 'src', 'components', `${name}.tsx`); + mkdirSync(path.dirname(componentPath), {recursive: true}); + + const propNames = props + .split(';') + .map((p) => p.trim()) + .filter((p) => p.length > 0) + .map((p) => p.split(':')[0].trim()) + .join(', '); + + const content = `/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + */ + +export interface ${name}Props { + ${props} +} + +export default function ${name}({${propNames}}: ${name}Props) { + return
{${propNames.split(',')[0].trim()}}
; +} +`; + + writeFileSync(componentPath, content, 'utf8'); + return componentPath; +} + +/** + * Run a test case and display results + */ +async function runTest(testId, description, testFn) { + console.log(`\n${'='.repeat(80)}`); + console.log(`Test Case: ${testId}`); + console.log(`Description: ${description}`); + console.log(`${'='.repeat(80)}\n`); + + try { + // Set working directory - use real project if available, otherwise use test directory + const workingDir = useRealProject ? realProjectDir : testDir; + process.env.SFCC_WORKING_DIRECTORY = workingDir; + process.chdir(workingDir); + + const result = await testFn(); + + console.log('✅ Test PASSED'); + console.log('\nTool Output:'); + console.log('-'.repeat(80)); + if (result.content && result.content[0]) { + const text = result.content[0].text || JSON.stringify(result.content[0], null, 2); + console.log(text.slice(0, 1000)); // Limit output + if (text.length > 1000) { + console.log(`\n... (output truncated, ${text.length} total characters)`); + } + } + console.log('-'.repeat(80)); + console.log(`\nIs Error: ${result.isError ? 'Yes' : 'No'}`); + + return {testId, status: 'PASS', result}; + } catch (error) { + console.log('❌ Test FAILED'); + console.error('Error:', error.message); + return {testId, status: 'FAIL', error: error.message}; + } +} + +/** + * Test Cases + */ +const testCases = { + 'TC-1.1': { + description: 'Name-Based Discovery (PascalCase)', + async run() { + createTestComponent('ProductCard', 'title: string; price: number;'); + return tool.handler({component: 'ProductCard'}); + }, + }, + + 'TC-1.2': { + description: 'Name-Based Discovery (Kebab-Case)', + async run() { + const kebabPath = path.join(testDir, 'src', 'components', 'product-card.tsx'); + mkdirSync(path.dirname(kebabPath), {recursive: true}); + writeFileSync( + kebabPath, + `export interface ProductCardProps { title: string; } +export default function ProductCard({title}: ProductCardProps) { return
{title}
; }`, + 'utf8', + ); + return tool.handler({component: 'product-card'}); + }, + }, + + 'TC-1.3': { + description: 'Nested Component Discovery', + async run() { + const nestedPath = path.join(testDir, 'src', 'components', 'hero', 'Hero.tsx'); + mkdirSync(path.dirname(nestedPath), {recursive: true}); + writeFileSync( + nestedPath, + `export interface HeroProps { title: string; } +export default function Hero({title}: HeroProps) { return
{title}
; }`, + 'utf8', + ); + return tool.handler({component: 'Hero'}); + }, + }, + + 'TC-1.4': { + description: 'Path-Based Input', + async run() { + createTestComponent('PathComponent', 'title: string;'); + const componentPath = path.join(testDir, 'src', 'components', 'PathComponent.tsx'); + const relativePath = path.relative(testDir, componentPath); + return tool.handler({component: relativePath}); + }, + }, + + 'TC-1.5': { + description: 'Custom Search Paths', + async run() { + const customPath = path.join(testDir, 'custom', 'components', 'CustomComponent.tsx'); + mkdirSync(path.dirname(customPath), {recursive: true}); + writeFileSync( + customPath, + `export interface CustomComponentProps { title: string; } +export default function CustomComponent({title}: CustomComponentProps) { return
{title}
; }`, + 'utf8', + ); + return tool.handler({ + component: 'CustomComponent', + searchPaths: ['custom/components'], + }); + }, + }, + + 'TC-1.6': { + description: 'Component Not Found', + async run() { + return tool.handler({component: 'NonExistentComponent'}); + }, + }, + + 'TC-2.1': { + description: 'Basic Auto Mode', + async run() { + createTestComponent('SimpleComponent', 'title: string; count: number;'); + return tool.handler({ + component: 'SimpleComponent', + autoMode: true, + }); + }, + }, + + 'TC-2.2': { + description: 'Auto Mode - Excludes Complex Props', + async run() { + createTestComponent( + 'MixedComponent', + `title: string; +onClick: () => void; +config: { key: string };`, + ); + return tool.handler({ + component: 'MixedComponent', + autoMode: true, + }); + }, + }, + + 'TC-2.3': { + description: 'Auto Mode - Excludes UI-Only Props', + async run() { + createTestComponent( + 'UIComponent', + `title: string; +className: string; +style: React.CSSProperties;`, + ); + return tool.handler({ + component: 'UIComponent', + autoMode: true, + }); + }, + }, + + 'TC-2.4': { + description: 'Auto Mode - Type Inference', + async run() { + createTestComponent( + 'MediaComponent', + `imageUrl: string; +ctaUrl: string; +productId: string;`, + ); + return tool.handler({ + component: 'MediaComponent', + autoMode: true, + }); + }, + }, + + 'TC-2.5': { + description: 'Auto Mode - Component Already Decorated', + async run() { + const decoratedPath = path.join(testDir, 'src', 'components', 'DecoratedComponent.tsx'); + mkdirSync(path.dirname(decoratedPath), {recursive: true}); + writeFileSync( + decoratedPath, + `import {Component} from '@salesforce/retail-react-app/app/components/page-designer'; + +@Component({ + id: 'existing-component', + name: 'Existing Component', +}) +export class DecoratedComponentMetadata { + @AttributeDefinition() + title!: string; +} + +export interface DecoratedComponentProps { + title: string; +} + +export default function DecoratedComponent({title}: DecoratedComponentProps) { + return
{title}
; +}`, + 'utf8', + ); + return tool.handler({ + component: 'DecoratedComponent', + autoMode: true, + }); + }, + }, + + 'TC-2.6': { + description: 'Auto Mode - Custom Component ID', + async run() { + createTestComponent('CustomIdComponent', 'title: string;'); + return tool.handler({ + component: 'CustomIdComponent', + autoMode: true, + componentId: 'custom-my-component-id', + }); + }, + }, + + 'TC-3.1': { + description: 'Mode Selection', + async run() { + createTestComponent('TestComponent'); + return tool.handler({component: 'TestComponent'}); + }, + }, + + 'TC-3.2': { + description: 'Interactive Mode - Analyze Step', + async run() { + createTestComponent( + 'AnalyzeComponent', + `title: string; +onClick: () => void; +className: string;`, + ); + return tool.handler({ + component: 'AnalyzeComponent', + conversationContext: {step: 'analyze'}, + }); + }, + }, + + 'TC-4.1': { + description: 'Invalid Input Type', + async run() { + return tool.handler({component: 123}); + }, + }, + + 'TC-4.2': { + description: 'Invalid Step Name', + async run() { + createTestComponent('TestComponent'); + return tool.handler({ + component: 'TestComponent', + conversationContext: {step: 'invalid_step'}, + }); + }, + }, + + 'TC-4.3': { + description: 'Missing Required Parameter', + async run() { + return tool.handler({}); + }, + }, + + 'TC-5.1': { + description: 'Component with No Props', + async run() { + const emptyPath = path.join(testDir, 'src', 'components', 'EmptyProps.tsx'); + mkdirSync(path.dirname(emptyPath), {recursive: true}); + writeFileSync( + emptyPath, + `export interface EmptyPropsProps {} +export default function EmptyProps({}: EmptyPropsProps) { return
Empty
; }`, + 'utf8', + ); + return tool.handler({ + component: 'EmptyProps', + autoMode: true, + }); + }, + }, + + 'TC-5.2': { + description: 'Component with Only Complex Props', + async run() { + createTestComponent( + 'ComplexOnlyComponent', + `onClick: () => void; +config: { key: string }; +data: Array<{id: number}>;`, + ); + return tool.handler({ + component: 'ComplexOnlyComponent', + autoMode: true, + }); + }, + }, + + 'TC-5.3': { + description: 'Component with Optional Props', + async run() { + createTestComponent( + 'OptionalPropsComponent', + `title?: string; +count?: number;`, + ); + return tool.handler({ + component: 'OptionalPropsComponent', + autoMode: true, + }); + }, + }, + + 'TC-5.4': { + description: 'Component with Union Types', + async run() { + createTestComponent( + 'UnionTypesComponent', + `status: 'active' | 'inactive'; +value: string | number;`, + ); + return tool.handler({ + component: 'UnionTypesComponent', + autoMode: true, + }); + }, + }, + + 'TC-5.5': { + description: 'Component Name Collision', + async run() { + // Create component in src/components/ + createTestComponent('CollisionComponent', 'title: string;'); + // Create component with same name in app/components/ + const appPath = path.join(testDir, 'app', 'components', 'CollisionComponent.tsx'); + mkdirSync(path.dirname(appPath), {recursive: true}); + writeFileSync( + appPath, + `export interface CollisionComponentProps { title: string; } +export default function CollisionComponent({title}: CollisionComponentProps) { return
{title}
; }`, + 'utf8', + ); + return tool.handler({component: 'CollisionComponent'}); + }, + }, + + 'TC-6.1': { + description: 'SFCC_WORKING_DIRECTORY Set', + async run() { + const customDir = path.join(tmpdir(), `page-designer-test-custom-${Date.now()}`); + mkdirSync(customDir, {recursive: true}); + mkdirSync(path.join(customDir, 'src', 'components'), {recursive: true}); + createTestComponent('CustomDirComponent', 'title: string;'); + // Move component to custom directory + const customComponentPath = path.join(customDir, 'src', 'components', 'CustomDirComponent.tsx'); + const originalPath = path.join(testDir, 'src', 'components', 'CustomDirComponent.tsx'); + if (existsSync(originalPath)) { + writeFileSync(customComponentPath, readFileSync(originalPath, 'utf8'), 'utf8'); + } + process.env.SFCC_WORKING_DIRECTORY = customDir; + const result = await tool.handler({component: 'CustomDirComponent'}); + delete process.env.SFCC_WORKING_DIRECTORY; + rmSync(customDir, {recursive: true, force: true}); + return result; + }, + }, + + 'TC-6.2': { + description: 'SFCC_WORKING_DIRECTORY Not Set', + async run() { + // Unset SFCC_WORKING_DIRECTORY + const originalEnv = process.env.SFCC_WORKING_DIRECTORY; + delete process.env.SFCC_WORKING_DIRECTORY; + createTestComponent('CwdComponent', 'title: string;'); + const result = await tool.handler({component: 'CwdComponent'}); + // Restore original env + if (originalEnv) { + process.env.SFCC_WORKING_DIRECTORY = originalEnv; + } + return result; + }, + }, +}; + +/** + * Main execution + */ +async function main() { + const testId = process.argv[2] || 'all'; + + console.log('🧪 Page Designer Decorator Tool - Manual Test Runner'); + if (useRealProject) { + console.log(`Real Project: ${realProjectDir}`); + console.log(`Test Components: ${testDir}`); + } else { + console.log(`Test Directory: ${testDir}`); + } + console.log(`Mode: ${useRealProject ? 'Real Storefront Next Project' : 'Temporary Test Directory'}`); + console.log(`Running: ${testId === 'all' ? 'All tests' : testId}\n`); + + // Setup - always create test directory for test components + mkdirSync(testDir, {recursive: true}); + mkdirSync(path.join(testDir, 'src', 'components'), {recursive: true}); + + if (useRealProject) { + // Verify the real project directory exists + if (!existsSync(realProjectDir)) { + throw new Error(`Storefront Next directory does not exist: ${realProjectDir}`); + } + console.log(`✅ Using real Storefront Next project: ${realProjectDir}`); + console.log(`📝 Test components will be created in temporary directory: ${testDir}`); + console.log(`⚠️ Note: Component discovery will search in the real project\n`); + } + + const results = []; + + try { + if (testId === 'all') { + // Run all tests + const testEntries = Object.entries(testCases); + const testResults = await Promise.all( + testEntries.map(([id, testCase]) => runTest(id, testCase.description, testCase.run)), + ); + results.push(...testResults); + } else if (testCases[testId]) { + // Run single test + const testCase = testCases[testId]; + const result = await runTest(testId, testCase.description, testCase.run); + results.push(result); + } else { + console.error(`❌ Unknown test case: ${testId}`); + console.log('\nAvailable test cases:'); + for (const id of Object.keys(testCases)) { + console.log(` - ${id}: ${testCases[id].description}`); + } + throw new Error(`Unknown test case: ${testId}`); + } + + // Summary + console.log(`\n${'='.repeat(80)}`); + console.log('📊 Test Summary'); + console.log(`${'='.repeat(80)}`); + const passed = results.filter((r) => r.status === 'PASS').length; + const failed = results.filter((r) => r.status === 'FAIL').length; + console.log(`Total: ${results.length}`); + console.log(`✅ Passed: ${passed}`); + console.log(`❌ Failed: ${failed}`); + + if (failed > 0) { + console.log('\nFailed Tests:'); + for (const r of results.filter((r) => r.status === 'FAIL')) { + console.log(` - ${r.testId}: ${r.error}`); + } + } + } finally { + // Cleanup - always remove temporary test directory, preserve real project + if (existsSync(testDir)) { + rmSync(testDir, {recursive: true, force: true}); + if (useRealProject) { + console.log(`\n🧹 Cleaned up temporary test directory: ${testDir}`); + console.log(`✅ Real project directory preserved: ${realProjectDir}`); + } else { + console.log(`\n🧹 Cleaned up test directory: ${testDir}`); + } + } + } +} + +try { + await main(); +} catch (error) { + console.error('Fatal error:', error); + throw error; +} From 63f5f8bf66cf631caedae2a3ac3e4d63390b2048 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Fri, 13 Feb 2026 17:36:09 -0800 Subject: [PATCH 03/14] Remove unused zod-to-json-schema dependency --- packages/b2c-dx-mcp/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index d8ca42ff7..f71badab8 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -96,8 +96,7 @@ "@salesforce/b2c-tooling-sdk": "workspace:*", "glob": "^11.0.0", "ts-morph": "^24.0.0", - "zod": "3.25.76", - "zod-to-json-schema": "^3.24.1" + "zod": "3.25.76" }, "devDependencies": { "@eslint/compat": "^1", From 9034319142bb27021cc94456ee29faf126b5e9cd Mon Sep 17 00:00:00 2001 From: cboscenco Date: Fri, 13 Feb 2026 18:06:59 -0800 Subject: [PATCH 04/14] refactor(b2c-dx-mcp): migrate page-designer-decorator tests from custom runner to Mocha Remove custom test runner (index.test.mjs) and consolidate all test cases into the standard Mocha test suite (index.test.ts) for consistency with the monorepo's test framework. --- .../tools/page-designer-decorator/README.md | 18 +- .../tools/page-designer-decorator/README.md | 61 +- .../page-designer-decorator/index.test.mjs | 601 ------------------ .../page-designer-decorator/index.test.ts | 235 ++++++- 4 files changed, 255 insertions(+), 660 deletions(-) delete mode 100755 packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md index c5ac7e6d0..90da4001d 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -227,20 +227,22 @@ pnpm test Comprehensive test suite covers all workflow modes, component discovery, and error handling. -### Manual Test Runner +### Running Tests -For quick manual verification, use the test runner script: +Run the comprehensive Mocha test suite: ```bash cd packages/b2c-dx-mcp -pnpm build -node test/tools/page-designer-decorator/index.test.mjs all +pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts ``` -Run a specific test case: -```bash -node test/tools/page-designer-decorator/index.test.mjs TC-1.1 -``` +The test suite covers: +- Component discovery (name-based, kebab-case, nested, path-based, custom paths, name collisions) +- Auto mode (basic, type inference, complex props exclusion, UI-only props exclusion, edge cases) +- Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) +- Error handling (invalid input, invalid step name, missing parameters) +- Edge cases (no props, only complex props, optional props, union types, already decorated components) +- Environment variables (SFCC_WORKING_DIRECTORY) See [`test/tools/page-designer-decorator/README.md`](../../../test/tools/page-designer-decorator/README.md) for detailed testing instructions. diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md index 8a8595ca9..05d221f4b 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md @@ -5,10 +5,15 @@ The page-designer-decorator tool has comprehensive unit tests covering: - ✅ Tool metadata (name, description, toolsets, isGA) - ✅ Mode selection flow -- ✅ Error handling +- ✅ Auto mode (basic, type inference, complex/UI props exclusion, edge cases) +- ✅ Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) +- ✅ Component resolution (name-based, kebab-case, nested, path-based, custom searchPaths, name collisions) +- ✅ Error handling (invalid input, invalid step name, missing parameters) - ✅ Input validation +- ✅ Edge cases (no props, only complex props, optional props, union types, already decorated components) +- ✅ Environment variables (SFCC_WORKING_DIRECTORY) -Some integration tests may require actual component files in a real workspace to fully validate component resolution. +All tests use the standard Mocha test framework and run with `pnpm test`. ## Testing Approaches @@ -50,68 +55,28 @@ npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga --args '{"component": "MyComponent"}' ``` -### 4. Automated Manual Test Runner +### 4. Running Tests Against a Local Storefront Next Installation -A test runner script is available to quickly verify the tool with various test scenarios: +The Mocha test suite supports testing against a real Storefront Next installation by setting `SFCC_WORKING_DIRECTORY`: ```bash cd packages/b2c-dx-mcp -pnpm build # Build the package first -node test/tools/page-designer-decorator/index.test.mjs all -``` - -Run a specific test case: -```bash -node test/tools/page-designer-decorator/index.test.mjs TC-1.1 -``` - -The script covers 24 automated test cases including: -- Component discovery (name-based, path-based, nested, custom paths) -- Auto mode (basic, type inference, complex props exclusion) -- Interactive mode (mode selection, analyze step) -- Error handling (invalid input, missing parameters) -- Edge cases (no props, optional props, union types, collisions) -- Environment variables (SFCC_WORKING_DIRECTORY) - -See the script's JSDoc header for a complete list of available test cases. - -#### Running Tests Against a Local Storefront Next Installation - -The test runner script supports two modes: - -**1. Temporary Directory Mode (Default)** -Creates isolated test environments with temporary directories. Ideal for CI/CD and regression testing. - -```bash -cd packages/b2c-dx-mcp -pnpm build -node test/tools/page-designer-decorator/index.test.mjs all -``` - -**2. Real Storefront Next Project Mode** -Test against an existing Storefront Next installation by setting `SFCC_WORKING_DIRECTORY`: - -```bash -cd packages/b2c-dx-mcp -pnpm build SFCC_WORKING_DIRECTORY=/path/to/storefront-next \ - node test/tools/page-designer-decorator/index.test.mjs all + pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts ``` Or set it as an environment variable: ```bash export SFCC_WORKING_DIRECTORY=/path/to/storefront-next cd packages/b2c-dx-mcp -pnpm build -node test/tools/page-designer-decorator/index.test.mjs TC-1.1 +pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts ``` **Important Notes for Real Project Mode**: - Component discovery searches in your real Storefront Next project (`SFCC_WORKING_DIRECTORY`) -- Test components created by the script are placed in a temporary directory (not in your real project) -- The script will **not** modify your real project files (read-only) +- Tests create temporary directories for test components (not in your real project) +- Tests will **not** modify your real project files (read-only) - Tests will use existing components from your real project if they exist -- If a test component doesn't exist in your real project, the test will show a "component not found" result (this is expected) - The real project directory is preserved after testing - To test with specific components, ensure they exist in your real project's `src/components/` directory diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs deleted file mode 100755 index dc3124bda..000000000 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.mjs +++ /dev/null @@ -1,601 +0,0 @@ -#!/usr/bin/env node -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Automated test runner for page-designer-decorator tool - * - * This script executes automated test cases by: - * 1. Creating test components in temporary directories - * 2. Invoking the tool with various test scenarios - * 3. Validating outputs - * - * **Modes**: - * - Default: Creates temporary directories and test components (isolated testing) - * - Real Project: Set SFCC_WORKING_DIRECTORY to use an existing Storefront Next project - * - * **Note**: This script covers automated test cases (TC-1.1 through TC-6.2). - * Test Category 7 (TC-7.1 through TC-7.5) requires a real Storefront Next - * installation and manual steps (Business Manager, Page Designer, cartridge - * deployment) and cannot be automated. See manual test plan for those tests. - * - * Usage: - * node test/tools/page-designer-decorator/index.test.mjs [test-case-id] - * - * Examples: - * # Run all tests with temporary directories (default) - * node test/tools/page-designer-decorator/index.test.mjs all - * - * # Run a specific test case - * node test/tools/page-designer-decorator/index.test.mjs TC-1.1 - * - * # Run tests against a real Storefront Next installation - * SFCC_WORKING_DIRECTORY=/path/to/storefront-next \ - * node test/tools/page-designer-decorator/index.test.mjs all - * - * Available automated test cases: - * Component Discovery: - * - TC-1.1: Name-Based Discovery (PascalCase) - * - TC-1.2: Name-Based Discovery (Kebab-Case) - * - TC-1.3: Nested Component Discovery - * - TC-1.4: Path-Based Input - * - TC-1.5: Custom Search Paths - * - TC-1.6: Component Not Found - * Auto Mode: - * - TC-2.1: Basic Auto Mode - * - TC-2.2: Auto Mode - Excludes Complex Props - * - TC-2.3: Auto Mode - Excludes UI-Only Props - * - TC-2.4: Auto Mode - Type Inference - * - TC-2.5: Auto Mode - Component Already Decorated - * - TC-2.6: Auto Mode - Custom Component ID - * Interactive Mode: - * - TC-3.1: Mode Selection - * - TC-3.2: Interactive Mode - Analyze Step - * Error Handling: - * - TC-4.1: Invalid Input Type - * - TC-4.2: Invalid Step Name - * - TC-4.3: Missing Required Parameter - * Edge Cases: - * - TC-5.1: Component with No Props - * - TC-5.2: Component with Only Complex Props - * - TC-5.3: Component with Optional Props - * - TC-5.4: Component with Union Types - * - TC-5.5: Component Name Collision - * Environment Variables: - * - TC-6.1: SFCC_WORKING_DIRECTORY Set - * - TC-6.2: SFCC_WORKING_DIRECTORY Not Set - * - * For real Storefront Next project tests (TC-7.x), see manual test plan: - * ~/Documents/page-designer-decorator-manual-test-plan.md - */ - -import {createPageDesignerDecoratorTool} from '../../../dist/tools/page-designer-decorator/index.js'; -import {Services} from '../../../dist/services.js'; -import {mkdirSync, writeFileSync, rmSync, existsSync, readFileSync} from 'node:fs'; -import path from 'node:path'; -import {tmpdir} from 'node:os'; - -const services = new Services({}); -const tool = createPageDesignerDecoratorTool(services); - -// Test directory - use SFCC_WORKING_DIRECTORY if set, otherwise create temporary directory -const useRealProject = Boolean(process.env.SFCC_WORKING_DIRECTORY); -const realProjectDir = useRealProject ? process.env.SFCC_WORKING_DIRECTORY : null; -const testDir = useRealProject - ? path.join(tmpdir(), `page-designer-test-${Date.now()}`) - : path.join(tmpdir(), `page-designer-test-${Date.now()}`); - -/** - * Create a test component file - */ -function createTestComponent(name, props = 'title: string;') { - const componentPath = path.join(testDir, 'src', 'components', `${name}.tsx`); - mkdirSync(path.dirname(componentPath), {recursive: true}); - - const propNames = props - .split(';') - .map((p) => p.trim()) - .filter((p) => p.length > 0) - .map((p) => p.split(':')[0].trim()) - .join(', '); - - const content = `/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - */ - -export interface ${name}Props { - ${props} -} - -export default function ${name}({${propNames}}: ${name}Props) { - return
{${propNames.split(',')[0].trim()}}
; -} -`; - - writeFileSync(componentPath, content, 'utf8'); - return componentPath; -} - -/** - * Run a test case and display results - */ -async function runTest(testId, description, testFn) { - console.log(`\n${'='.repeat(80)}`); - console.log(`Test Case: ${testId}`); - console.log(`Description: ${description}`); - console.log(`${'='.repeat(80)}\n`); - - try { - // Set working directory - use real project if available, otherwise use test directory - const workingDir = useRealProject ? realProjectDir : testDir; - process.env.SFCC_WORKING_DIRECTORY = workingDir; - process.chdir(workingDir); - - const result = await testFn(); - - console.log('✅ Test PASSED'); - console.log('\nTool Output:'); - console.log('-'.repeat(80)); - if (result.content && result.content[0]) { - const text = result.content[0].text || JSON.stringify(result.content[0], null, 2); - console.log(text.slice(0, 1000)); // Limit output - if (text.length > 1000) { - console.log(`\n... (output truncated, ${text.length} total characters)`); - } - } - console.log('-'.repeat(80)); - console.log(`\nIs Error: ${result.isError ? 'Yes' : 'No'}`); - - return {testId, status: 'PASS', result}; - } catch (error) { - console.log('❌ Test FAILED'); - console.error('Error:', error.message); - return {testId, status: 'FAIL', error: error.message}; - } -} - -/** - * Test Cases - */ -const testCases = { - 'TC-1.1': { - description: 'Name-Based Discovery (PascalCase)', - async run() { - createTestComponent('ProductCard', 'title: string; price: number;'); - return tool.handler({component: 'ProductCard'}); - }, - }, - - 'TC-1.2': { - description: 'Name-Based Discovery (Kebab-Case)', - async run() { - const kebabPath = path.join(testDir, 'src', 'components', 'product-card.tsx'); - mkdirSync(path.dirname(kebabPath), {recursive: true}); - writeFileSync( - kebabPath, - `export interface ProductCardProps { title: string; } -export default function ProductCard({title}: ProductCardProps) { return
{title}
; }`, - 'utf8', - ); - return tool.handler({component: 'product-card'}); - }, - }, - - 'TC-1.3': { - description: 'Nested Component Discovery', - async run() { - const nestedPath = path.join(testDir, 'src', 'components', 'hero', 'Hero.tsx'); - mkdirSync(path.dirname(nestedPath), {recursive: true}); - writeFileSync( - nestedPath, - `export interface HeroProps { title: string; } -export default function Hero({title}: HeroProps) { return
{title}
; }`, - 'utf8', - ); - return tool.handler({component: 'Hero'}); - }, - }, - - 'TC-1.4': { - description: 'Path-Based Input', - async run() { - createTestComponent('PathComponent', 'title: string;'); - const componentPath = path.join(testDir, 'src', 'components', 'PathComponent.tsx'); - const relativePath = path.relative(testDir, componentPath); - return tool.handler({component: relativePath}); - }, - }, - - 'TC-1.5': { - description: 'Custom Search Paths', - async run() { - const customPath = path.join(testDir, 'custom', 'components', 'CustomComponent.tsx'); - mkdirSync(path.dirname(customPath), {recursive: true}); - writeFileSync( - customPath, - `export interface CustomComponentProps { title: string; } -export default function CustomComponent({title}: CustomComponentProps) { return
{title}
; }`, - 'utf8', - ); - return tool.handler({ - component: 'CustomComponent', - searchPaths: ['custom/components'], - }); - }, - }, - - 'TC-1.6': { - description: 'Component Not Found', - async run() { - return tool.handler({component: 'NonExistentComponent'}); - }, - }, - - 'TC-2.1': { - description: 'Basic Auto Mode', - async run() { - createTestComponent('SimpleComponent', 'title: string; count: number;'); - return tool.handler({ - component: 'SimpleComponent', - autoMode: true, - }); - }, - }, - - 'TC-2.2': { - description: 'Auto Mode - Excludes Complex Props', - async run() { - createTestComponent( - 'MixedComponent', - `title: string; -onClick: () => void; -config: { key: string };`, - ); - return tool.handler({ - component: 'MixedComponent', - autoMode: true, - }); - }, - }, - - 'TC-2.3': { - description: 'Auto Mode - Excludes UI-Only Props', - async run() { - createTestComponent( - 'UIComponent', - `title: string; -className: string; -style: React.CSSProperties;`, - ); - return tool.handler({ - component: 'UIComponent', - autoMode: true, - }); - }, - }, - - 'TC-2.4': { - description: 'Auto Mode - Type Inference', - async run() { - createTestComponent( - 'MediaComponent', - `imageUrl: string; -ctaUrl: string; -productId: string;`, - ); - return tool.handler({ - component: 'MediaComponent', - autoMode: true, - }); - }, - }, - - 'TC-2.5': { - description: 'Auto Mode - Component Already Decorated', - async run() { - const decoratedPath = path.join(testDir, 'src', 'components', 'DecoratedComponent.tsx'); - mkdirSync(path.dirname(decoratedPath), {recursive: true}); - writeFileSync( - decoratedPath, - `import {Component} from '@salesforce/retail-react-app/app/components/page-designer'; - -@Component({ - id: 'existing-component', - name: 'Existing Component', -}) -export class DecoratedComponentMetadata { - @AttributeDefinition() - title!: string; -} - -export interface DecoratedComponentProps { - title: string; -} - -export default function DecoratedComponent({title}: DecoratedComponentProps) { - return
{title}
; -}`, - 'utf8', - ); - return tool.handler({ - component: 'DecoratedComponent', - autoMode: true, - }); - }, - }, - - 'TC-2.6': { - description: 'Auto Mode - Custom Component ID', - async run() { - createTestComponent('CustomIdComponent', 'title: string;'); - return tool.handler({ - component: 'CustomIdComponent', - autoMode: true, - componentId: 'custom-my-component-id', - }); - }, - }, - - 'TC-3.1': { - description: 'Mode Selection', - async run() { - createTestComponent('TestComponent'); - return tool.handler({component: 'TestComponent'}); - }, - }, - - 'TC-3.2': { - description: 'Interactive Mode - Analyze Step', - async run() { - createTestComponent( - 'AnalyzeComponent', - `title: string; -onClick: () => void; -className: string;`, - ); - return tool.handler({ - component: 'AnalyzeComponent', - conversationContext: {step: 'analyze'}, - }); - }, - }, - - 'TC-4.1': { - description: 'Invalid Input Type', - async run() { - return tool.handler({component: 123}); - }, - }, - - 'TC-4.2': { - description: 'Invalid Step Name', - async run() { - createTestComponent('TestComponent'); - return tool.handler({ - component: 'TestComponent', - conversationContext: {step: 'invalid_step'}, - }); - }, - }, - - 'TC-4.3': { - description: 'Missing Required Parameter', - async run() { - return tool.handler({}); - }, - }, - - 'TC-5.1': { - description: 'Component with No Props', - async run() { - const emptyPath = path.join(testDir, 'src', 'components', 'EmptyProps.tsx'); - mkdirSync(path.dirname(emptyPath), {recursive: true}); - writeFileSync( - emptyPath, - `export interface EmptyPropsProps {} -export default function EmptyProps({}: EmptyPropsProps) { return
Empty
; }`, - 'utf8', - ); - return tool.handler({ - component: 'EmptyProps', - autoMode: true, - }); - }, - }, - - 'TC-5.2': { - description: 'Component with Only Complex Props', - async run() { - createTestComponent( - 'ComplexOnlyComponent', - `onClick: () => void; -config: { key: string }; -data: Array<{id: number}>;`, - ); - return tool.handler({ - component: 'ComplexOnlyComponent', - autoMode: true, - }); - }, - }, - - 'TC-5.3': { - description: 'Component with Optional Props', - async run() { - createTestComponent( - 'OptionalPropsComponent', - `title?: string; -count?: number;`, - ); - return tool.handler({ - component: 'OptionalPropsComponent', - autoMode: true, - }); - }, - }, - - 'TC-5.4': { - description: 'Component with Union Types', - async run() { - createTestComponent( - 'UnionTypesComponent', - `status: 'active' | 'inactive'; -value: string | number;`, - ); - return tool.handler({ - component: 'UnionTypesComponent', - autoMode: true, - }); - }, - }, - - 'TC-5.5': { - description: 'Component Name Collision', - async run() { - // Create component in src/components/ - createTestComponent('CollisionComponent', 'title: string;'); - // Create component with same name in app/components/ - const appPath = path.join(testDir, 'app', 'components', 'CollisionComponent.tsx'); - mkdirSync(path.dirname(appPath), {recursive: true}); - writeFileSync( - appPath, - `export interface CollisionComponentProps { title: string; } -export default function CollisionComponent({title}: CollisionComponentProps) { return
{title}
; }`, - 'utf8', - ); - return tool.handler({component: 'CollisionComponent'}); - }, - }, - - 'TC-6.1': { - description: 'SFCC_WORKING_DIRECTORY Set', - async run() { - const customDir = path.join(tmpdir(), `page-designer-test-custom-${Date.now()}`); - mkdirSync(customDir, {recursive: true}); - mkdirSync(path.join(customDir, 'src', 'components'), {recursive: true}); - createTestComponent('CustomDirComponent', 'title: string;'); - // Move component to custom directory - const customComponentPath = path.join(customDir, 'src', 'components', 'CustomDirComponent.tsx'); - const originalPath = path.join(testDir, 'src', 'components', 'CustomDirComponent.tsx'); - if (existsSync(originalPath)) { - writeFileSync(customComponentPath, readFileSync(originalPath, 'utf8'), 'utf8'); - } - process.env.SFCC_WORKING_DIRECTORY = customDir; - const result = await tool.handler({component: 'CustomDirComponent'}); - delete process.env.SFCC_WORKING_DIRECTORY; - rmSync(customDir, {recursive: true, force: true}); - return result; - }, - }, - - 'TC-6.2': { - description: 'SFCC_WORKING_DIRECTORY Not Set', - async run() { - // Unset SFCC_WORKING_DIRECTORY - const originalEnv = process.env.SFCC_WORKING_DIRECTORY; - delete process.env.SFCC_WORKING_DIRECTORY; - createTestComponent('CwdComponent', 'title: string;'); - const result = await tool.handler({component: 'CwdComponent'}); - // Restore original env - if (originalEnv) { - process.env.SFCC_WORKING_DIRECTORY = originalEnv; - } - return result; - }, - }, -}; - -/** - * Main execution - */ -async function main() { - const testId = process.argv[2] || 'all'; - - console.log('🧪 Page Designer Decorator Tool - Manual Test Runner'); - if (useRealProject) { - console.log(`Real Project: ${realProjectDir}`); - console.log(`Test Components: ${testDir}`); - } else { - console.log(`Test Directory: ${testDir}`); - } - console.log(`Mode: ${useRealProject ? 'Real Storefront Next Project' : 'Temporary Test Directory'}`); - console.log(`Running: ${testId === 'all' ? 'All tests' : testId}\n`); - - // Setup - always create test directory for test components - mkdirSync(testDir, {recursive: true}); - mkdirSync(path.join(testDir, 'src', 'components'), {recursive: true}); - - if (useRealProject) { - // Verify the real project directory exists - if (!existsSync(realProjectDir)) { - throw new Error(`Storefront Next directory does not exist: ${realProjectDir}`); - } - console.log(`✅ Using real Storefront Next project: ${realProjectDir}`); - console.log(`📝 Test components will be created in temporary directory: ${testDir}`); - console.log(`⚠️ Note: Component discovery will search in the real project\n`); - } - - const results = []; - - try { - if (testId === 'all') { - // Run all tests - const testEntries = Object.entries(testCases); - const testResults = await Promise.all( - testEntries.map(([id, testCase]) => runTest(id, testCase.description, testCase.run)), - ); - results.push(...testResults); - } else if (testCases[testId]) { - // Run single test - const testCase = testCases[testId]; - const result = await runTest(testId, testCase.description, testCase.run); - results.push(result); - } else { - console.error(`❌ Unknown test case: ${testId}`); - console.log('\nAvailable test cases:'); - for (const id of Object.keys(testCases)) { - console.log(` - ${id}: ${testCases[id].description}`); - } - throw new Error(`Unknown test case: ${testId}`); - } - - // Summary - console.log(`\n${'='.repeat(80)}`); - console.log('📊 Test Summary'); - console.log(`${'='.repeat(80)}`); - const passed = results.filter((r) => r.status === 'PASS').length; - const failed = results.filter((r) => r.status === 'FAIL').length; - console.log(`Total: ${results.length}`); - console.log(`✅ Passed: ${passed}`); - console.log(`❌ Failed: ${failed}`); - - if (failed > 0) { - console.log('\nFailed Tests:'); - for (const r of results.filter((r) => r.status === 'FAIL')) { - console.log(` - ${r.testId}: ${r.error}`); - } - } - } finally { - // Cleanup - always remove temporary test directory, preserve real project - if (existsSync(testDir)) { - rmSync(testDir, {recursive: true, force: true}); - if (useRealProject) { - console.log(`\n🧹 Cleaned up temporary test directory: ${testDir}`); - console.log(`✅ Real project directory preserved: ${realProjectDir}`); - } else { - console.log(`\n🧹 Cleaned up test directory: ${testDir}`); - } - } - } -} - -try { - await main(); -} catch (error) { - console.error('Fatal error:', error); - throw error; -} diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts index d738c71b0..364a22e99 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -98,11 +98,11 @@ export default function ${componentName}({${propNames}}: ${componentName}Props) * This test suite covers: * - Tool metadata (name, description, toolsets, isGA) * - Mode selection workflow - * - Auto mode decorator generation + * - Auto mode decorator generation (including edge cases: no props, only complex props, optional props, union types, already decorated) * - Interactive mode workflow (all steps) - * - Component resolution (by name, path, custom searchPaths) + * - Component resolution (by name, kebab-case, nested paths, path, custom searchPaths, name collisions) * - Input validation - * - Error handling + * - Error handling (invalid input, invalid step name, missing parameters) * - Output format validation * * Tests use temporary directories and mock components to avoid dependencies @@ -310,6 +310,149 @@ style: React.CSSProperties;`, expect(decoratorCode).to.not.include('style'); } }); + + it('should handle component already decorated in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + const decoratedPath = path.join(testDir, 'src', 'components', 'DecoratedComponent.tsx'); + mkdirSync(path.dirname(decoratedPath), {recursive: true}); + writeFileSync( + decoratedPath, + `import {Component} from '@salesforce/retail-react-app/app/components/page-designer'; + +@Component({ + id: 'existing-component', + name: 'Existing Component', +}) +export class DecoratedComponentMetadata { + @AttributeDefinition() + title!: string; +} + +export interface DecoratedComponentProps { + title: string; +} + +export default function DecoratedComponent({title}: DecoratedComponentProps) { + return
{title}
; +}`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'DecoratedComponent', + autoMode: true, + }); + + // Should handle already-decorated components gracefully + // May return an error or provide guidance + expect(result).to.exist; + const text = getResultText(result); + // Should mention the component is already decorated or provide appropriate guidance + expect(text).to.match(/decorated|already|existing|Component/i); + }); + + it('should handle component with no props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + const emptyPath = path.join(testDir, 'src', 'components', 'EmptyProps.tsx'); + mkdirSync(path.dirname(emptyPath), {recursive: true}); + writeFileSync( + emptyPath, + `export interface EmptyPropsProps {} +export default function EmptyProps({}: EmptyPropsProps) { return
Empty
; }`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'EmptyProps', + autoMode: true, + }); + + // Should handle components with no props gracefully + expect(result).to.exist; + const text = getResultText(result); + // Should generate decorator code even with no props (just @Component, no @AttributeDefinition) + expect(text).to.include('@Component'); + expect(text).to.include('EmptyProps'); + }); + + it('should handle component with only complex props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'ComplexOnlyComponent', + `onClick: () => void; +config: { key: string }; +data: Array<{id: number}>;`, + ); + + const result = await tool.handler({ + component: 'ComplexOnlyComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should generate decorator code with just @Component (no @AttributeDefinition since all props are complex) + expect(text).to.include('@Component'); + expect(text).to.include('ComplexOnlyComponent'); + // Should not include complex props in decorators + const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); + if (decoratorCodeMatch) { + const decoratorCode = decoratorCodeMatch.join('\n'); + expect(decoratorCode).to.not.include('onClick'); + expect(decoratorCode).to.not.include('config'); + expect(decoratorCode).to.not.include('data'); + } + }); + + it('should handle component with optional props in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'OptionalPropsComponent', + `title?: string; +count?: number;`, + ); + + const result = await tool.handler({ + component: 'OptionalPropsComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should include optional props in generated decorators + expect(text).to.include('@Component'); + expect(text).to.include('OptionalPropsComponent'); + expect(text).to.include('title'); + expect(text).to.include('count'); + }); + + it('should handle component with union types in auto mode', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent( + testDir, + 'UnionTypesComponent', + `status: 'active' | 'inactive'; +value: string | number;`, + ); + + const result = await tool.handler({ + component: 'UnionTypesComponent', + autoMode: true, + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should handle union types appropriately + expect(text).to.include('@Component'); + expect(text).to.include('UnionTypesComponent'); + expect(text).to.include('status'); + expect(text).to.include('value'); + }); }); describe('interactive mode', () => { @@ -552,6 +695,28 @@ className: string;`, // Should return an error result expect(result.isError).to.be.true; }); + + it('should handle invalid step name', async () => { + const tool = createPageDesignerDecoratorTool(services); + createTestComponent(testDir, 'TestComponent'); + + const result = await tool.handler({ + component: 'TestComponent', + conversationContext: {step: 'invalid_step'}, + } as unknown as Record); + + // Should return an error result for invalid step + expect(result.isError).to.be.true; + }); + + it('should handle missing required parameter', async () => { + const tool = createPageDesignerDecoratorTool(services); + + const result = await tool.handler({} as unknown as Record); + + // Should return an error result + expect(result.isError).to.be.true; + }); }); describe('component resolution', () => { @@ -568,6 +733,46 @@ className: string;`, expect(text).to.include('StandardLocationComponent'); }); + it('should find component by kebab-case name', async () => { + const tool = createPageDesignerDecoratorTool(services); + const kebabPath = path.join(testDir, 'src', 'components', 'product-card.tsx'); + mkdirSync(path.dirname(kebabPath), {recursive: true}); + writeFileSync( + kebabPath, + `export interface ProductCardProps { title: string; } +export default function ProductCard({title}: ProductCardProps) { return
{title}
; }`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'product-card', + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.match(/ProductCard|product-card/i); + }); + + it('should find nested component by name', async () => { + const tool = createPageDesignerDecoratorTool(services); + const nestedPath = path.join(testDir, 'src', 'components', 'hero', 'Hero.tsx'); + mkdirSync(path.dirname(nestedPath), {recursive: true}); + writeFileSync( + nestedPath, + `export interface HeroProps { title: string; } +export default function Hero({title}: HeroProps) { return
{title}
; }`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'Hero', + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('Hero'); + }); + it('should find component by path', async () => { const tool = createPageDesignerDecoratorTool(services); const componentPath = createTestComponent(testDir, 'PathComponent'); @@ -609,6 +814,30 @@ export default function CustomLocationComponent({title}: CustomLocationComponent const text = getResultText(result); expect(text).to.include('CustomLocationComponent'); }); + + it('should handle component name collision', async () => { + const tool = createPageDesignerDecoratorTool(services); + // Create component in src/components/ + createTestComponent(testDir, 'CollisionComponent', 'title: string;'); + // Create component with same name in app/components/ + const appPath = path.join(testDir, 'app', 'components', 'CollisionComponent.tsx'); + mkdirSync(path.dirname(appPath), {recursive: true}); + writeFileSync( + appPath, + `export interface CollisionComponentProps { title: string; } +export default function CollisionComponent({title}: CollisionComponentProps) { return
{title}
; }`, + 'utf8', + ); + + const result = await tool.handler({ + component: 'CollisionComponent', + }); + + // Should find one of the components (likely the first one found) + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.include('CollisionComponent'); + }); }); describe('input validation', () => { From 00be5dff60de3680c0593844cad907c96935667d Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 09:53:40 -0800 Subject: [PATCH 05/14] refactor(mcp): use Services.workingDirectory for component discovery Add workingDirectory property to Services and update page-designer-decorator tool to use it instead of reading process.env.SFCC_WORKING_DIRECTORY directly. This ensures component searches start from the correct project directory when MCP clients spawn servers from the home directory. - Add workingDirectory to Services (resolved from --working-directory flag/env) - Update page-designer-decorator to use services.workingDirectory - Simplify tool description for LLM consumption - Update tests and documentation accordingly --- packages/b2c-dx-mcp/src/commands/mcp.ts | 3 +- packages/b2c-dx-mcp/src/services.ts | 18 +++++- .../tools/page-designer-decorator/README.md | 6 +- .../tools/page-designer-decorator/analyzer.ts | 4 +- .../tools/page-designer-decorator/index.ts | 63 ++++--------------- .../page-designer-decorator/index.test.ts | 18 +++--- 6 files changed, 45 insertions(+), 67 deletions(-) diff --git a/packages/b2c-dx-mcp/src/commands/mcp.ts b/packages/b2c-dx-mcp/src/commands/mcp.ts index 9696cf2a5..a6be41a91 100644 --- a/packages/b2c-dx-mcp/src/commands/mcp.ts +++ b/packages/b2c-dx-mcp/src/commands/mcp.ts @@ -337,7 +337,8 @@ export default class McpServerCommand extends BaseCommand { let originalCwd: string; beforeEach(() => { - services = createMockServices(); // Create a temporary directory for test components testDir = path.join(tmpdir(), `b2c-mcp-test-${Date.now()}`); mkdirSync(testDir, {recursive: true}); originalCwd = process.cwd(); process.chdir(testDir); - // Set SFCC_WORKING_DIRECTORY to the test directory - process.env.SFCC_WORKING_DIRECTORY = testDir; + // Create services with workingDirectory set to test directory + services = createMockServices(testDir); }); afterEach(() => { @@ -129,7 +129,6 @@ describe('tools/page-designer-decorator', () => { if (existsSync(testDir)) { rmSync(testDir, {recursive: true, force: true}); } - delete process.env.SFCC_WORKING_DIRECTORY; }); describe('tool metadata', () => { @@ -186,13 +185,14 @@ describe('tools/page-designer-decorator', () => { expect(text).to.include('TestComponent'); }); - it('should use SFCC_WORKING_DIRECTORY if set', async () => { - const tool = createPageDesignerDecoratorTool(services); + it('should use workingDirectory from Services', async () => { const customDir = path.join(tmpdir(), `b2c-mcp-test-custom-${Date.now()}`); mkdirSync(customDir, {recursive: true}); createTestComponent(customDir, 'CustomComponent'); - process.env.SFCC_WORKING_DIRECTORY = customDir; + // Create services with custom workingDirectory + const customServices = createMockServices(customDir); + const tool = createPageDesignerDecoratorTool(customServices); const result = await tool.handler({ component: 'CustomComponent', From d40463e6252c8b157358005f3bc364ac8039a9eb Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 10:53:32 -0800 Subject: [PATCH 06/14] refactor(mcp): rename page designer decorator tool and reorganize docs - Rename tool from to - Remove conflicting placeholder - Reorganize storefrontnext README to scatter tool documentation throughout file - Update all references in code, tests, and documentation This ensures consistent naming with other Storefront Next tools and improves documentation organization by distributing tool details across appropriate sections. --- packages/b2c-dx-mcp/README.md | 4 +- packages/b2c-dx-mcp/content/page-designer.md | 8 +-- .../tools/page-designer-decorator/README.md | 8 +-- .../tools/page-designer-decorator/index.ts | 2 +- .../src/tools/storefrontnext/README.md | 68 +++++++++++++------ .../src/tools/storefrontnext/index.ts | 6 -- .../tools/page-designer-decorator/README.md | 6 +- .../page-designer-decorator/index.test.ts | 2 +- 8 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/b2c-dx-mcp/README.md b/packages/b2c-dx-mcp/README.md index a01ca4ae9..726d54444 100644 --- a/packages/b2c-dx-mcp/README.md +++ b/packages/b2c-dx-mcp/README.md @@ -290,7 +290,7 @@ Storefront Next development tools for building modern storefronts. | `storefront_next_figma_to_component_workflow` | Convert Figma designs to Storefront Next components | | `storefront_next_generate_component` | Generate a new Storefront Next component | | `storefront_next_map_tokens_to_theme` | Map design tokens to Storefront Next theme configuration | -| `storefront_next_design_decorator` | Apply design decorators to Storefront Next components | +| `storefront_next_page_designer_decorator` | Add Page Designer decorators to Storefront Next components | | `storefront_next_generate_page_designer_metadata` | Generate Page Designer metadata for Storefront Next components | | `scapi_discovery` | Discover available SCAPI endpoints and capabilities | | `scapi_custom_api_discovery` | Discover custom SCAPI API endpoints | @@ -393,7 +393,7 @@ npx mcp-inspector --cli node bin/dev.js --toolsets all --allow-non-ga-tools --me # Call a specific tool npx mcp-inspector --cli node bin/dev.js --toolsets all --allow-non-ga-tools \ --method tools/call \ - --tool-name storefront_next_design_decorator + --tool-name storefront_next_page_designer_decorator ``` #### 2. IDE Integration diff --git a/packages/b2c-dx-mcp/content/page-designer.md b/packages/b2c-dx-mcp/content/page-designer.md index 022f523a0..74021bf43 100644 --- a/packages/b2c-dx-mcp/content/page-designer.md +++ b/packages/b2c-dx-mcp/content/page-designer.md @@ -35,7 +35,7 @@ To add a new content page: define a page type and ID in Commerce Cloud, then in - **Add a metadata class** with `@Component('typeId', { name, description })` and `@AttributeDefinition()` (and optionally `@AttributeDefinition({ type: 'image' })`, `type: 'url'`, etc.) for each prop you want editable in Page Designer. Use `@RegionDefinition([...])` if the component has nested regions (e.g. a grid with slots). - **Implement the React component** so it accepts those props (and strips Page Designer–only props like `component`, `page`, `componentData`, `designMetadata` before spreading to the DOM). If the component needs server data (e.g. products for a carousel), export a `loader({ componentData, context })` and optionally a `fallback` component; the registry calls the loader during `collectComponentDataPromises` and passes resolved data as the `data` prop. -- **Use the MCP tool `storefront_next_design_decorator`** to generate decorators instead of writing them by hand. Example components: `components/hero/index.tsx`, `components/content-card/index.tsx`, `components/product-carousel/index.tsx`. +- **Use the MCP tool `storefront_next_page_designer_decorator`** to generate decorators instead of writing them by hand. Example components: `components/hero/index.tsx`, `components/content-card/index.tsx`, `components/product-carousel/index.tsx`. ### After changes @@ -50,7 +50,7 @@ To add a new content page: define a page type and ID in Commerce Cloud, then in Use the **B2C DX MCP server** for Page Designer work instead of hand-writing decorators and metadata. Configure the B2C DX MCP server in your IDE (e.g. in MCP settings) so these tools are available. -### 1. `storefront_next_design_decorator` (STOREFRONTNEXT toolset) +### 1. `storefront_next_page_designer_decorator` (STOREFRONTNEXT toolset) Adds Page Designer decorators to an existing React component so it can be used in Business Manager. The tool analyzes the component, picks suitable props, infers types (e.g. `*Url`/`*Link` → url, `*Image` → image, `is*`/`show*` → boolean), and generates `@Component('typeId', { name, description })`, `@AttributeDefinition()` on a metadata class, and optionally `@RegionDefinition([...])` for nested regions. It skips complex or UI-only props (e.g. className, style, callbacks). @@ -71,7 +71,7 @@ Packages the cartridge, uploads it to Commerce Cloud via WebDAV, and unpacks it ### Typical workflow -1. **`storefront_next_design_decorator`** — Add decorators to the component (use autoMode for a quick first pass). +1. **`storefront_next_page_designer_decorator`** — Add decorators to the component (use autoMode for a quick first pass). 2. **`storefront_next_generate_page_designer_metadata`** — Generate metadata JSON so the component and regions appear in Page Designer. 3. **`cartridge_deploy`** — Deploy to Commerce Cloud so merchants can use the component in Business Manager. @@ -81,6 +81,6 @@ Packages the cartridge, uploads it to Commerce Cloud via WebDAV, and unpacks it 2. **Use registry for components**: Register all Page Designer components with proper `typeId` 3. **Handle design mode**: Adapt UI when `pageDesignerMode` is `'EDIT'` or `'PREVIEW'` 4. **Rebuild after registry changes**: Static registry is generated at build time -5. **Use MCP tools**: Leverage `storefront_next_design_decorator` and `storefront_next_generate_page_designer_metadata` for faster development +5. **Use MCP tools**: Leverage `storefront_next_page_designer_decorator` and `storefront_next_generate_page_designer_metadata` for faster development **Reference:** See README.md for complete Page Designer documentation and MCP tool setup. diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md index f56ae077f..c56462460 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -41,19 +41,19 @@ page-designer-decorator/ ```bash # By component name (automatically finds the file) -add_page_designer_decorator({ +storefront_next_page_designer_decorator({ component: "ProductCard", autoMode: true }) # Interactive mode -add_page_designer_decorator({ +storefront_next_page_designer_decorator({ component: "Hero", conversationContext: { step: "analyze" } }) # With custom search paths (for unusual locations) -add_page_designer_decorator({ +storefront_next_page_designer_decorator({ component: "ProductCard", searchPaths: ["packages/retail/src", "app/features"], autoMode: true @@ -64,7 +64,7 @@ add_page_designer_decorator({ ```bash # If you prefer to specify the exact path -add_page_designer_decorator({ +storefront_next_page_designer_decorator({ component: "src/components/ProductCard.tsx", autoMode: true }) diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts index dc2880b6c..dcfb06d5b 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts @@ -604,7 +604,7 @@ function handleAutoMode(args: PageDesignerDecoratorInput, workspaceRoot: string) */ export function createPageDesignerDecoratorTool(services: Services): McpTool { return { - name: 'add_page_designer_decorator', + name: 'storefront_next_page_designer_decorator', description: 'Adds Page Designer decorators (@Component, @AttributeDefinition, @RegionDefinition) to React components. ' + diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md index 213f96a34..cf45a994a 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md @@ -80,7 +80,7 @@ MCP tools for Storefront Next development with React Server Components. } ``` -### `add_page_designer_decorator` +### `storefront_next_page_designer_decorator` Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to existing React components for Storefront Next. @@ -93,14 +93,6 @@ Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefi - Generate decorator code automatically or interactively - Configure component attributes and regions for Page Designer -**Key features**: - -- **Name-Based Lookup**: Find components by name (e.g., "ProductCard") without knowing paths -- **Auto Mode**: Automatically generates decorators with sensible defaults -- **Interactive Mode**: Step-by-step workflow for fine-tuned control -- **Auto-Discovery**: Searches common component directories automatically -- **Type Inference**: Automatically infers Page Designer types from prop names - **Parameters**: - `component` (required, string): Component name (e.g., "ProductCard") or file path @@ -109,11 +101,6 @@ Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefi - `componentId` (optional, string): Override component ID - `conversationContext` (optional, object): For interactive mode workflow steps -**Modes**: - -- **Auto Mode**: Generates decorators immediately with sensible defaults -- **Interactive Mode**: Multi-step workflow with user confirmation at each stage - **Returns**: Generated decorator code and instructions for adding to component file **Example usage**: @@ -121,7 +108,7 @@ Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefi ```json // Auto mode (quick setup) { - "name": "add_page_designer_decorator", + "name": "storefront_next_page_designer_decorator", "arguments": { "component": "ProductCard", "autoMode": true @@ -130,7 +117,7 @@ Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefi // Interactive mode (step-by-step) { - "name": "add_page_designer_decorator", + "name": "storefront_next_page_designer_decorator", "arguments": { "component": "Hero", "conversationContext": { @@ -140,12 +127,12 @@ Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefi } ``` -**See also**: [Detailed documentation](./page-designer-decorator/README.md) for complete usage guide, architecture details, and examples. - ## Implementation Details ### Architecture +#### `storefront_next_development_guidelines` + The tool loads content from markdown files in the `content/` directory: - **Content source**: Markdown files loaded at runtime from `packages/b2c-dx-mcp/content/*.md` @@ -153,7 +140,7 @@ The tool loads content from markdown files in the `content/` directory: - **Section-Based**: Individual markdown files per topic (~100-200 lines each) - **Default behavior**: Returns 4 sections by default for comprehensive coverage -### Content Structure +**Content Structure**: Each section markdown file includes: @@ -162,14 +149,14 @@ Each section markdown file includes: - Quick reference snippets - Framework-specific patterns for React Server Components -### Behavior +**Behavior**: - **No sections specified**: Returns default comprehensive set (`quick-reference`, `data-fetching`, `components`, `testing`) - **Single section**: Returns content directly without separators - **Multiple sections**: Combines content with `---` separators and includes instructions for full content display - **Empty array**: Returns empty string -### Benefits +**Benefits**: ✅ **Token Efficient**: Returns only relevant content (200-500 lines vs 20K+ full doc) ✅ **Modular**: Access specific sections as needed @@ -177,6 +164,44 @@ Each section markdown file includes: ✅ **Always Current**: Content loaded from markdown files (easy to update) ✅ **Comprehensive Default**: Returns key sections by default for immediate value +#### `storefront_next_page_designer_decorator` + +The tool uses a rule-based architecture with TypeScript template literals for generating Page Designer decorators: + +- **Rule Rendering**: Pure TypeScript functions that return strings based on typed context +- **Type Safety**: Every rule has a strongly-typed context interface checked at compile time +- **Template Generation**: Code generation uses pure functions for decorator creation +- **Component Discovery**: Automatically searches common component directories (e.g., `src/components/**`, `app/components/**`) + +**Key Features**: + +- **Name-Based Lookup**: Find components by name (e.g., "ProductCard") without knowing paths +- **Auto-Discovery**: Searches common component directories automatically +- **Type-Safe**: Full TypeScript type inference for all contexts +- **Fast**: Direct function execution, no file I/O or compilation overhead +- **Flexible Input**: Supports component names or file paths + +**Modes**: + +- **Auto Mode**: Generates decorators immediately with sensible defaults +- **Interactive Mode**: Multi-step workflow with user confirmation at each stage + +**Component Discovery**: + +The tool automatically searches for components in these locations (in order): + +1. `src/components/**` (PascalCase and kebab-case) +2. `app/components/**` +3. `components/**` +4. `src/**` (broader search) +5. Custom paths (if provided via `searchPaths`) + +**Working Directory**: + +Component discovery uses the working directory resolved from `--working-directory` flag or `SFCC_WORKING_DIRECTORY` environment variable (via Services). This ensures searches start from the correct project directory, especially when MCP clients spawn servers from the home directory. + +**See also**: [Detailed documentation](./page-designer-decorator/README.md) for complete usage guide, architecture details, and examples. + ## Placeholder Tools The following tools are placeholders awaiting implementation: @@ -185,7 +210,6 @@ The following tools are placeholders awaiting implementation: - `storefront_next_figma_to_component_workflow` - Convert Figma designs to Storefront Next components - `storefront_next_generate_component` - Generate a new Storefront Next component - `storefront_next_map_tokens_to_theme` - Map design tokens to Storefront Next theme configuration -- `storefront_next_design_decorator` - Apply design decorators to Storefront Next components - `storefront_next_generate_page_designer_metadata` - Generate Page Designer metadata for Storefront Next components Use `--allow-non-ga-tools` flag to enable placeholder tools. diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts index a3f21c049..effc145a5 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts @@ -17,7 +17,6 @@ * - `storefront_next_figma_to_component_workflow` - Convert Figma to components * - `storefront_next_generate_component` - Generate new components * - `storefront_next_map_tokens_to_theme` - Map design tokens - * - `storefront_next_design_decorator` - Apply design decorators * - `storefront_next_generate_page_designer_metadata` - Generate Page Designer metadata * * @module tools/storefrontnext @@ -118,11 +117,6 @@ export function createStorefrontNextTools(services: Services): McpTool[] { 'Map design tokens to Storefront Next theme configuration', services, ), - createPlaceholderTool( - 'storefront_next_design_decorator', - 'Apply design decorators to Storefront Next components', - services, - ), createPlaceholderTool( 'storefront_next_generate_page_designer_metadata', 'Generate Page Designer metadata for Storefront Next components', diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md index 05d221f4b..14e35137d 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/README.md @@ -37,7 +37,7 @@ pnpm run inspect:dev Then in the inspector: 1. Click **Connect** -2. Click **List Tools** - you should see `add_page_designer_decorator` +2. Click **List Tools** - you should see `storefront_next_page_designer_decorator` 3. Click on the tool to test it with real inputs ### 3. CLI Testing @@ -45,13 +45,13 @@ Then in the inspector: Test via command line: ```bash -# List all tools (should include add_page_designer_decorator) +# List all tools (should include storefront_next_page_designer_decorator) npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools --method tools/list # Call the tool npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ --method tools/call \ - --tool-name add_page_designer_decorator \ + --tool-name storefront_next_page_designer_decorator \ --args '{"component": "MyComponent"}' ``` diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts index 3f4127411..b3a88164d 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -134,7 +134,7 @@ describe('tools/page-designer-decorator', () => { describe('tool metadata', () => { it('should have correct tool name', () => { const tool = createPageDesignerDecoratorTool(services); - expect(tool.name).to.equal('add_page_designer_decorator'); + expect(tool.name).to.equal('storefront_next_page_designer_decorator'); }); it('should have comprehensive description', () => { From 6b057699f5114c1048b32094ca06adc018e72ad5 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 14:08:13 -0800 Subject: [PATCH 07/14] fix(page-designer-decorator): use lazy Services loading and correct API Update createPageDesignerDecoratorTool to accept loadServices function for consistency with other tools. Fix Services API usage to call getWorkingDirectory() method instead of accessing workingDirectory property. Update test mocks to properly construct Services with ResolvedB2CConfig. This fixes build errors and test failures after recent refactoring. --- .../tools/page-designer-decorator/index.ts | 7 +- .../page-designer-decorator/index.test.ts | 88 ++++++++++--------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts index dcfb06d5b..e94ba0472 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts @@ -599,10 +599,10 @@ function handleAutoMode(args: PageDesignerDecoratorInput, workspaceRoot: string) /** * Creates the Page Designer decorator tool for Storefront Next. * - * @param services - MCP services (provides workingDirectory for component search) + * @param loadServices - Function that loads configuration and returns Services instance * @returns The configured MCP tool */ -export function createPageDesignerDecoratorTool(services: Services): McpTool { +export function createPageDesignerDecoratorTool(loadServices: () => Services): McpTool { return { name: 'storefront_next_page_designer_decorator', @@ -623,7 +623,8 @@ export function createPageDesignerDecoratorTool(services: Services): McpTool { const validatedArgs = pageDesignerDecoratorSchema.parse(args) as PageDesignerDecoratorInput; // Use workingDirectory from services to ensure we search in the correct project directory // This prevents searches in the home folder when MCP clients spawn servers from ~ - const workspaceRoot = services.workingDirectory; + const services = loadServices(); + const workspaceRoot = services.getWorkingDirectory(); if (validatedArgs.autoMode === undefined && !validatedArgs.conversationContext) { const fullPath = resolveComponent(validatedArgs.component, workspaceRoot, validatedArgs.searchPaths); diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts index b3a88164d..53b1ee76b 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -11,6 +11,7 @@ import type {ToolResult} from '../../../src/utils/types.js'; import {existsSync, mkdirSync, writeFileSync, rmSync} from 'node:fs'; import path from 'node:path'; import {tmpdir} from 'node:os'; +import {createMockResolvedConfig} from '../../test-helpers.js'; /** * Helper to extract text from a ToolResult. @@ -35,7 +36,8 @@ function getResultText(result: ToolResult): string { * @returns A new Services instance with empty configuration */ function createMockServices(workingDirectory?: string): Services { - return new Services({workingDirectory}); + const config = createMockResolvedConfig({workingDirectory}); + return new Services({resolvedConfig: config}); } /** @@ -133,12 +135,12 @@ describe('tools/page-designer-decorator', () => { describe('tool metadata', () => { it('should have correct tool name', () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); expect(tool.name).to.equal('storefront_next_page_designer_decorator'); }); it('should have comprehensive description', () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const desc = tool.description; // Should mention Page Designer @@ -155,20 +157,20 @@ describe('tools/page-designer-decorator', () => { }); it('should be in STOREFRONTNEXT toolset', () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); expect(tool.toolsets).to.include('STOREFRONTNEXT'); expect(tool.toolsets).to.have.lengthOf(1); }); it('should not be GA (generally available)', () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); expect(tool.isGA).to.be.false; }); }); describe('mode selection', () => { it('should show mode selection when called with only component name', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); const result = await tool.handler({ @@ -192,7 +194,7 @@ describe('tools/page-designer-decorator', () => { // Create services with custom workingDirectory const customServices = createMockServices(customDir); - const tool = createPageDesignerDecoratorTool(customServices); + const tool = createPageDesignerDecoratorTool(() => customServices); const result = await tool.handler({ component: 'CustomComponent', @@ -208,7 +210,7 @@ describe('tools/page-designer-decorator', () => { describe('auto mode', () => { it('should generate decorators in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoComponent', 'title: string;'); const result = await tool.handler({ @@ -227,7 +229,7 @@ describe('tools/page-designer-decorator', () => { }); it('should handle component with multiple props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'MultiPropComponent', @@ -252,7 +254,7 @@ imageUrl: string;`, }); it('should exclude complex props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'ComplexPropsComponent', @@ -282,7 +284,7 @@ config: { key: string; value: number };`, }); it('should exclude UI-only props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'UIPropsComponent', @@ -312,7 +314,7 @@ style: React.CSSProperties;`, }); it('should handle component already decorated in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const decoratedPath = path.join(testDir, 'src', 'components', 'DecoratedComponent.tsx'); mkdirSync(path.dirname(decoratedPath), {recursive: true}); writeFileSync( @@ -352,7 +354,7 @@ export default function DecoratedComponent({title}: DecoratedComponentProps) { }); it('should handle component with no props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const emptyPath = path.join(testDir, 'src', 'components', 'EmptyProps.tsx'); mkdirSync(path.dirname(emptyPath), {recursive: true}); writeFileSync( @@ -376,7 +378,7 @@ export default function EmptyProps({}: EmptyPropsProps) { return
Empty
{ - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'ComplexOnlyComponent', @@ -407,7 +409,7 @@ data: Array<{id: number}>;`, }); it('should handle component with optional props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'OptionalPropsComponent', @@ -431,7 +433,7 @@ count?: number;`, }); it('should handle component with union types in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'UnionTypesComponent', @@ -458,7 +460,7 @@ value: string | number;`, describe('interactive mode', () => { describe('analyze step', () => { it('should analyze component in analyze step', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AnalyzeComponent', 'title: string;'); const result = await tool.handler({ @@ -479,7 +481,7 @@ value: string | number;`, }); it('should categorize props correctly', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent( testDir, 'CategorizedComponent', @@ -509,7 +511,7 @@ className: string;`, describe('select_props step', () => { it('should confirm selected props', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SelectPropsComponent', 'title: string; description: string;'); const result = await tool.handler({ @@ -535,7 +537,7 @@ className: string;`, }); it('should require component metadata', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataComponent'); const result = await tool.handler({ @@ -556,7 +558,7 @@ className: string;`, describe('configure_attrs step', () => { it('should provide attribute configuration instructions', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfigureAttrsComponent', 'imageUrl: string; description: string;'); const result = await tool.handler({ @@ -577,7 +579,7 @@ className: string;`, }); it('should suggest types for props', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TypeSuggestionsComponent', 'imageUrl: string; productId: string;'); const result = await tool.handler({ @@ -598,7 +600,7 @@ className: string;`, describe('configure_regions step', () => { it('should provide region configuration instructions', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'RegionsComponent'); const result = await tool.handler({ @@ -618,7 +620,7 @@ className: string;`, describe('confirm_generation step', () => { it('should generate decorator code when all context provided', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfirmComponent', 'title: string;'); const result = await tool.handler({ @@ -651,7 +653,7 @@ className: string;`, }); it('should require component metadata', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataConfirmComponent'); const result = await tool.handler({ @@ -672,7 +674,7 @@ className: string;`, describe('error handling', () => { it('should handle non-existent component gracefully', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const result = await tool.handler({ component: 'NonExistentComponent', @@ -685,7 +687,7 @@ className: string;`, }); it('should handle invalid input gracefully', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); // Invalid input should be caught by zod validation const result = await tool.handler({ @@ -697,7 +699,7 @@ className: string;`, }); it('should handle invalid step name', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); const result = await tool.handler({ @@ -710,7 +712,7 @@ className: string;`, }); it('should handle missing required parameter', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const result = await tool.handler({} as unknown as Record); @@ -721,7 +723,7 @@ className: string;`, describe('component resolution', () => { it('should find component by name in standard location', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'StandardLocationComponent'); const result = await tool.handler({ @@ -734,7 +736,7 @@ className: string;`, }); it('should find component by kebab-case name', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const kebabPath = path.join(testDir, 'src', 'components', 'product-card.tsx'); mkdirSync(path.dirname(kebabPath), {recursive: true}); writeFileSync( @@ -754,7 +756,7 @@ export default function ProductCard({title}: ProductCardProps) { return
{ti }); it('should find nested component by name', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const nestedPath = path.join(testDir, 'src', 'components', 'hero', 'Hero.tsx'); mkdirSync(path.dirname(nestedPath), {recursive: true}); writeFileSync( @@ -774,7 +776,7 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` }); it('should find component by path', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); const result = await tool.handler({ @@ -787,7 +789,7 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` }); it('should use searchPaths when provided', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); // Create component in a custom location const customDir = path.join(testDir, 'custom', 'components'); mkdirSync(customDir, {recursive: true}); @@ -816,7 +818,7 @@ export default function CustomLocationComponent({title}: CustomLocationComponent }); it('should handle component name collision', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); // Create component in src/components/ createTestComponent(testDir, 'CollisionComponent', 'title: string;'); // Create component with same name in app/components/ @@ -842,7 +844,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r describe('input validation', () => { it('should accept valid component name', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ValidComponent'); const result = await tool.handler({ @@ -854,7 +856,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should accept component path', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); const result = await tool.handler({ @@ -866,7 +868,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should accept optional searchPaths', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SearchComponent'); const result = await tool.handler({ @@ -879,7 +881,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should accept optional autoMode flag', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoModeComponent'); const result = await tool.handler({ @@ -892,7 +894,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should accept optional componentId', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'CustomIdComponent'); const result = await tool.handler({ @@ -907,7 +909,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should accept conversationContext with all steps', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConversationComponent'); const steps = ['analyze', 'select_props', 'configure_attrs', 'configure_regions', 'confirm_generation']; @@ -939,7 +941,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r describe('output format', () => { it('should return text content in ToolResult format', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'FormatComponent'); const result = await tool.handler({ @@ -954,7 +956,7 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r }); it('should return error format when component not found', async () => { - const tool = createPageDesignerDecoratorTool(services); + const tool = createPageDesignerDecoratorTool(() => services); const result = await tool.handler({ component: 'NonExistentComponent', From 2fb636ad76baae61478c7bf16ea3d05229703fb2 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 14:25:55 -0800 Subject: [PATCH 08/14] refactor(mcp): add RequestHandlerExtra context parameter to tool handlers Update tool handler signatures to match MCP SDK's expected format by accepting RequestHandlerExtra context parameter. Context is currently unused but available for future protocol-level features. --- packages/b2c-dx-mcp/package.json | 1 + packages/b2c-dx-mcp/src/registry.ts | 2 +- packages/b2c-dx-mcp/src/server.ts | 9 ++++++--- packages/b2c-dx-mcp/src/tools/adapter.ts | 7 ++++++- .../src/tools/page-designer-decorator/index.ts | 4 +++- packages/b2c-dx-mcp/src/utils/types.ts | 8 ++++++-- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index 9c0f1cb70..b2e630884 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -96,6 +96,7 @@ "@modelcontextprotocol/sdk": "1.26.0", "@oclif/core": "catalog:", "@salesforce/b2c-tooling-sdk": "workspace:*", + "glob": "catalog:", "ts-morph": "^27.0.0", "yaml": "2.8.1", "zod": "3.25.76" diff --git a/packages/b2c-dx-mcp/src/registry.ts b/packages/b2c-dx-mcp/src/registry.ts index e24863a98..02a677366 100644 --- a/packages/b2c-dx-mcp/src/registry.ts +++ b/packages/b2c-dx-mcp/src/registry.ts @@ -266,6 +266,6 @@ async function registerTools(tools: McpTool[], server: B2CDxMcpServer, allowNonG // Register the tool // TODO: Telemetry - Tool registration includes timing/error tracking - server.addTool(tool.name, tool.description, tool.inputSchema, async (args) => tool.handler(args)); + server.addTool(tool.name, tool.description, tool.inputSchema, async (args, context) => tool.handler(args, context)); } } diff --git a/packages/b2c-dx-mcp/src/server.ts b/packages/b2c-dx-mcp/src/server.ts index 146c73513..7a2b37d80 100644 --- a/packages/b2c-dx-mcp/src/server.ts +++ b/packages/b2c-dx-mcp/src/server.ts @@ -72,15 +72,18 @@ export class B2CDxMcpServer extends McpServer { name: string, description: string, inputSchema: ZodRawShape, - handler: (args: Record) => Promise, + handler: ( + args: Record, + context: RequestHandlerExtra, + ) => Promise, ): void { const wrappedHandler = async ( args: Record, - _extra: RequestHandlerExtra, + extra: RequestHandlerExtra, ): Promise => { const startTime = Date.now(); try { - const result = await handler(args); + const result = await handler(args, extra); const runTimeMs = Date.now() - startTime; await this.telemetry diff --git a/packages/b2c-dx-mcp/src/tools/adapter.ts b/packages/b2c-dx-mcp/src/tools/adapter.ts index 55a9b4120..7accb0980 100644 --- a/packages/b2c-dx-mcp/src/tools/adapter.ts +++ b/packages/b2c-dx-mcp/src/tools/adapter.ts @@ -73,6 +73,8 @@ import {z, type ZodRawShape, type ZodObject, type ZodType} from 'zod'; import type {B2CInstance} from '@salesforce/b2c-tooling-sdk'; +import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; +import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; import type {McpTool, ToolResult, Toolset} from '../utils/index.js'; import type {Services, MrtConfig} from '../services.js'; @@ -278,7 +280,10 @@ export function createToolAdapter( toolsets, isGA, - async handler(rawArgs: Record): Promise { + async handler( + rawArgs: Record, + _mcpContext: RequestHandlerExtra, + ): Promise { // 1. Validate input with Zod const parseResult = zodSchema.safeParse(rawArgs); if (!parseResult.success) { diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts index e94ba0472..dc07c2424 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts @@ -5,6 +5,8 @@ */ import {z, type ZodRawShape} from 'zod'; +import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; +import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; import {componentAnalyzer, generateTypeSuggestions, resolveComponent, type TypeSuggestion} from './analyzer.js'; import {generateDecoratorCode, type AttributeContext, type MetadataContext} from './templates/decorator-generator.js'; import {pageDesignerDecoratorRules} from './rules.js'; @@ -617,7 +619,7 @@ export function createPageDesignerDecoratorTool(loadServices: () => Services): M toolsets: ['STOREFRONTNEXT'], isGA: false, - async handler(args: Record) { + async handler(args: Record, _context: RequestHandlerExtra) { try { // Validate and parse input const validatedArgs = pageDesignerDecoratorSchema.parse(args) as PageDesignerDecoratorInput; diff --git a/packages/b2c-dx-mcp/src/utils/types.ts b/packages/b2c-dx-mcp/src/utils/types.ts index 254be6aca..ffaba59c0 100644 --- a/packages/b2c-dx-mcp/src/utils/types.ts +++ b/packages/b2c-dx-mcp/src/utils/types.ts @@ -5,7 +5,8 @@ */ import type {z, ZodRawShape} from 'zod'; -import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; +import type {CallToolResult, ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; +import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; import type {Toolset} from './constants.js'; /** @@ -35,7 +36,10 @@ export interface McpToolConfig { */ export interface McpTool extends McpToolConfig { /** Handler function that executes the tool */ - handler: (args: z.infer>) => Promise; + handler: ( + args: z.infer>, + context: RequestHandlerExtra, + ) => Promise; } /** From 3828ba80011384d7912916f0c86f2efd26c3218e Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 14:35:44 -0800 Subject: [PATCH 09/14] Updated the lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca0287d26..524170d1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,9 @@ importers: '@salesforce/b2c-tooling-sdk': specifier: workspace:* version: link:../b2c-tooling-sdk + glob: + specifier: 'catalog:' + version: 13.0.0 ts-morph: specifier: ^27.0.0 version: 27.0.2 From eb1f0af3248487d3b6ac317a833c3c1f19ae3e99 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 17 Feb 2026 14:57:13 -0800 Subject: [PATCH 10/14] fix: update test handlers to include context parameter Update all test files to pass the new context parameter to tool.handler() calls. The handler signature was updated to accept a second parameter (RequestHandlerExtra) for MCP context, but tests were still calling handlers with a single argument. Changes: - Add {} as any as second argument to all tool.handler() calls in tests - Add eslint-disable comments for @typescript-eslint/no-explicit-any - Fix prettier formatting issues - Remove duplicate variable declaration in developer-guidelines test - Fix no-await-in-loop lint errors with proper disable comments --- .../tools/page-designer-decorator/README.md | 1 + .../b2c-dx-mcp/test/tools/adapter.test.ts | 88 ++-- .../test/tools/cartridges/index.test.ts | 42 +- .../b2c-dx-mcp/test/tools/mrt/index.test.ts | 26 +- .../page-designer-decorator/index.test.ts | 488 ++++++++++++------ .../scapi/scapi-custom-apis-status.test.ts | 42 +- .../tools/scapi/scapi-schemas-list.test.ts | 146 ++++-- .../developer-guidelines.test.ts | 161 +++--- 8 files changed, 646 insertions(+), 348 deletions(-) diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md index c56462460..a874aa7f6 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -241,6 +241,7 @@ pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts ``` The test suite covers: + - Component discovery (name-based, kebab-case, nested, path-based, custom paths, name collisions) - Auto mode (basic, type inference, complex props exclusion, UI-only props exclusion, edge cases) - Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) diff --git a/packages/b2c-dx-mcp/test/tools/adapter.test.ts b/packages/b2c-dx-mcp/test/tools/adapter.test.ts index ab636e53d..48a82971d 100644 --- a/packages/b2c-dx-mcp/test/tools/adapter.test.ts +++ b/packages/b2c-dx-mcp/test/tools/adapter.test.ts @@ -183,18 +183,21 @@ describe('tools/adapter', () => { ); // Test with valid input - const validResult = await tool.handler({name: 'test', count: 5}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const validResult = await tool.handler({name: 'test', count: 5}, {} as any); expect(validResult.isError).to.be.undefined; expect(getResultText(validResult)).to.equal('Received: test, 5'); // Test with missing required field - const missingResult = await tool.handler({count: 5}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const missingResult = await tool.handler({count: 5}, {} as any); expect(missingResult.isError).to.be.true; expect(getResultText(missingResult)).to.include('Invalid input'); expect(getResultText(missingResult)).to.include('name'); // Test with invalid type - const invalidTypeResult = await tool.handler({name: 'test', count: 'not-a-number'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const invalidTypeResult = await tool.handler({name: 'test', count: 'not-a-number'}, {} as any); expect(invalidTypeResult.isError).to.be.true; expect(getResultText(invalidTypeResult)).to.include('Invalid input'); }); @@ -217,7 +220,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({email: 'not-an-email'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({email: 'not-an-email'}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Invalid input'); @@ -242,7 +246,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Execution error'); @@ -267,7 +272,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Execution error'); @@ -294,7 +300,8 @@ describe('tools/adapter', () => { loadServices, ); - await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({}, {} as any); const services = loadServices(); expect(receivedServices).to.equal(services); @@ -322,7 +329,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({projectName: 'my-storefront'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({projectName: 'my-storefront'}, {} as any); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.equal('Created project: my-storefront'); @@ -349,7 +357,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; const parsed = JSON.parse(getResultText(result)); @@ -398,12 +407,14 @@ describe('tools/adapter', () => { ); // Without optional field - const result1 = await tool.handler({required: 'value'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result1 = await tool.handler({required: 'value'}, {} as any); expect(result1.isError).to.be.undefined; expect(getResultText(result1)).to.equal('required: value, optional: not provided'); // With optional field - const result2 = await tool.handler({required: 'value', optional: 'present'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result2 = await tool.handler({required: 'value', optional: 'present'}, {} as any); expect(result2.isError).to.be.undefined; expect(getResultText(result2)).to.equal('required: value, optional: present'); }); @@ -426,7 +437,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({items: ['a', 'b', 'c']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({items: ['a', 'b', 'c']}, {} as any); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.equal('a, b, c'); @@ -452,7 +464,8 @@ describe('tools/adapter', () => { ); // Test with too short name - const result = await tool.handler({name: 'ab', age: 25}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({name: 'ab', age: 25}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Name must be at least 3 characters'); }); @@ -478,7 +491,8 @@ describe('tools/adapter', () => { ); // Default is now false, so tool should execute without instance - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.b2cInstance).to.be.undefined; @@ -500,7 +514,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('B2C instance error'); @@ -534,7 +549,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.be.undefined; @@ -563,7 +579,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -595,7 +612,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -629,7 +647,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -657,7 +676,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(contextReceived?.b2cInstance).to.be.undefined; @@ -684,7 +704,8 @@ describe('tools/adapter', () => { loadServices, ); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('MRT auth error'); @@ -723,16 +744,21 @@ describe('tools/adapter', () => { ); // Empty list - const emptyResult = await tool.handler({items: []}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emptyResult = await tool.handler({items: []}, {} as any); expect(getResultText(emptyResult)).to.equal('No items found.'); // With items - const itemsResult = await tool.handler({ - items: [ - {id: 1, name: 'First'}, - {id: 2, name: 'Second'}, - ], - }); + const itemsResult = await tool.handler( + { + items: [ + {id: 1, name: 'First'}, + {id: 2, name: 'Second'}, + ], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(getResultText(itemsResult)).to.include('Found 2 items:'); expect(getResultText(itemsResult)).to.include('1: First'); expect(getResultText(itemsResult)).to.include('2: Second'); @@ -768,11 +794,13 @@ describe('tools/adapter', () => { loadServices, ); - const successResult = await tool.handler({operation: 'succeed'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const successResult = await tool.handler({operation: 'succeed'}, {} as any); expect(successResult.isError).to.be.undefined; expect(getResultText(successResult)).to.equal('Operation succeeded'); - const failResult = await tool.handler({operation: 'fail'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const failResult = await tool.handler({operation: 'fail'}, {} as any); expect(failResult.isError).to.be.true; expect(getResultText(failResult)).to.equal('Operation failed'); }); diff --git a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts index 335a6819b..2aa7644a9 100644 --- a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts @@ -139,7 +139,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(findAndDeployCartridgesStub.calledOnce).to.be.true; @@ -175,7 +176,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - const result = await tool.handler({directory}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({directory}, {} as any); expect(result.isError).to.be.undefined; expect(findAndDeployCartridgesStub.calledOnce).to.be.true; @@ -204,7 +206,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - await tool.handler({cartridges}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({cartridges}, {} as any); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -228,7 +231,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - await tool.handler({exclude}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({exclude}, {} as any); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -250,7 +254,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - await tool.handler({reload: true}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({reload: true}, {} as any); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -277,12 +282,16 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - await tool.handler({ - directory, - cartridges, - exclude, - reload, - }); + await tool.handler( + { + directory, + cartridges, + exclude, + reload, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const [, dir, options] = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -309,7 +318,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; const jsonResult = getResultJson(result); @@ -328,7 +338,8 @@ describe('tools/cartridges', () => { }); const tool = createCartridgesTools(loadServices)[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -347,7 +358,8 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -369,7 +381,7 @@ describe('tools/cartridges', () => { it('should validate input schema', async () => { // Test that invalid input is rejected by the adapter // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({directory: 123} as any); + const result = await tool.handler({directory: 123} as any, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); diff --git a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts index b5dc9339b..6d2760b4e 100644 --- a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts @@ -163,7 +163,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - const result = await tool.handler({buildDirectory: buildDir}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({buildDirectory: buildDir}, {} as any); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -194,7 +195,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - const result = await tool.handler({buildDirectory: buildDir}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({buildDirectory: buildDir}, {} as any); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -225,7 +227,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - const result = await tool.handler({buildDirectory: buildDir}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({buildDirectory: buildDir}, {} as any); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -253,7 +256,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - await tool.handler({buildDirectory: buildDir, message: 'Custom deployment message'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({buildDirectory: buildDir, message: 'Custom deployment message'}, {} as any); expect(pushBundleStub.calledOnce).to.be.true; const [options] = pushBundleStub.firstCall.args as [PushOptions]; @@ -277,7 +281,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - const result = await tool.handler({buildDirectory: buildDir, message: 'Release v1.0.0'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({buildDirectory: buildDir, message: 'Release v1.0.0'}, {} as any); expect(result.isError).to.be.undefined; const jsonResult = getResultJson(result); @@ -296,7 +301,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices)[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -312,7 +318,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices)[0]; - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -333,7 +340,8 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - const result = await tool.handler({buildDirectory: buildDir}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({buildDirectory: buildDir}, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -354,7 +362,7 @@ describe('tools/mrt', () => { it('should validate input schema', async () => { // Test that invalid input is rejected by the adapter // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: 123} as any); + const result = await tool.handler({buildDirectory: 123} as any, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts index 53b1ee76b..e2aef28a2 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -173,9 +173,13 @@ describe('tools/page-designer-decorator', () => { const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); - const result = await tool.handler({ - component: 'TestComponent', - }); + const result = await tool.handler( + { + component: 'TestComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -196,9 +200,13 @@ describe('tools/page-designer-decorator', () => { const customServices = createMockServices(customDir); const tool = createPageDesignerDecoratorTool(() => customServices); - const result = await tool.handler({ - component: 'CustomComponent', - }); + const result = await tool.handler( + { + component: 'CustomComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -213,10 +221,14 @@ describe('tools/page-designer-decorator', () => { const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoComponent', 'title: string;'); - const result = await tool.handler({ - component: 'AutoComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'AutoComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -238,10 +250,14 @@ description: string; imageUrl: string;`, ); - const result = await tool.handler({ - component: 'MultiPropComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'MultiPropComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -263,10 +279,14 @@ onClick: () => void; config: { key: string; value: number };`, ); - const result = await tool.handler({ - component: 'ComplexPropsComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'ComplexPropsComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -293,10 +313,14 @@ className: string; style: React.CSSProperties;`, ); - const result = await tool.handler({ - component: 'UIPropsComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'UIPropsComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -340,10 +364,14 @@ export default function DecoratedComponent({title}: DecoratedComponentProps) { 'utf8', ); - const result = await tool.handler({ - component: 'DecoratedComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'DecoratedComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should handle already-decorated components gracefully // May return an error or provide guidance @@ -364,10 +392,14 @@ export default function EmptyProps({}: EmptyPropsProps) { return
Empty
;`, ); - const result = await tool.handler({ - component: 'ComplexOnlyComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'ComplexOnlyComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -417,10 +453,14 @@ data: Array<{id: number}>;`, count?: number;`, ); - const result = await tool.handler({ - component: 'OptionalPropsComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'OptionalPropsComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -441,10 +481,14 @@ count?: number;`, value: string | number;`, ); - const result = await tool.handler({ - component: 'UnionTypesComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'UnionTypesComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -463,12 +507,16 @@ value: string | number;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AnalyzeComponent', 'title: string;'); - const result = await tool.handler({ - component: 'AnalyzeComponent', - conversationContext: { - step: 'analyze', + const result = await tool.handler( + { + component: 'AnalyzeComponent', + conversationContext: { + step: 'analyze', + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -490,12 +538,16 @@ onClick: () => void; className: string;`, ); - const result = await tool.handler({ - component: 'CategorizedComponent', - conversationContext: { - step: 'analyze', + const result = await tool.handler( + { + component: 'CategorizedComponent', + conversationContext: { + step: 'analyze', + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -514,18 +566,22 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SelectPropsComponent', 'title: string; description: string;'); - const result = await tool.handler({ - component: 'SelectPropsComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title', 'description'], - componentMetadata: { - id: 'select-props-component', - name: 'Select Props Component', - description: 'Test component', + const result = await tool.handler( + { + component: 'SelectPropsComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title', 'description'], + componentMetadata: { + id: 'select-props-component', + name: 'Select Props Component', + description: 'Test component', + }, }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -540,14 +596,18 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataComponent'); - const result = await tool.handler({ - component: 'MissingMetadataComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title'], - // Missing componentMetadata + const result = await tool.handler( + { + component: 'MissingMetadataComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title'], + // Missing componentMetadata + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should return error when metadata is missing expect(result.isError).to.be.true; @@ -561,13 +621,17 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfigureAttrsComponent', 'imageUrl: string; description: string;'); - const result = await tool.handler({ - component: 'ConfigureAttrsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'description'], + const result = await tool.handler( + { + component: 'ConfigureAttrsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'description'], + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -582,13 +646,17 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TypeSuggestionsComponent', 'imageUrl: string; productId: string;'); - const result = await tool.handler({ - component: 'TypeSuggestionsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'productId'], + const result = await tool.handler( + { + component: 'TypeSuggestionsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'productId'], + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -603,12 +671,16 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'RegionsComponent'); - const result = await tool.handler({ - component: 'RegionsComponent', - conversationContext: { - step: 'configure_regions', + const result = await tool.handler( + { + component: 'RegionsComponent', + conversationContext: { + step: 'configure_regions', + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -623,24 +695,28 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfirmComponent', 'title: string;'); - const result = await tool.handler({ - component: 'ConfirmComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'confirm-component', - name: 'Confirm Component', - description: 'Test component', - }, - attributeConfig: { - title: { - type: 'string', - name: 'Title', + const result = await tool.handler( + { + component: 'ConfirmComponent', + conversationContext: { + step: 'confirm_generation', + selectedProps: ['title'], + componentMetadata: { + id: 'confirm-component', + name: 'Confirm Component', + description: 'Test component', + }, + attributeConfig: { + title: { + type: 'string', + name: 'Title', + }, }, }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -656,13 +732,17 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataConfirmComponent'); - const result = await tool.handler({ - component: 'MissingMetadataConfirmComponent', - conversationContext: { - step: 'confirm_generation', - // Missing componentMetadata + const result = await tool.handler( + { + component: 'MissingMetadataConfirmComponent', + conversationContext: { + step: 'confirm_generation', + // Missing componentMetadata + }, }, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should return error when metadata is missing expect(result.isError).to.be.true; @@ -676,9 +756,13 @@ className: string;`, it('should handle non-existent component gracefully', async () => { const tool = createPageDesignerDecoratorTool(() => services); - const result = await tool.handler({ - component: 'NonExistentComponent', - }); + const result = await tool.handler( + { + component: 'NonExistentComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should return an error result expect(result.isError).to.be.true; @@ -690,9 +774,13 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); // Invalid input should be caught by zod validation - const result = await tool.handler({ - component: 123, // Invalid type - } as unknown as Record); + const result = await tool.handler( + { + component: 123, // Invalid type + } as unknown as Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should return an error result expect(result.isError).to.be.true; @@ -702,10 +790,14 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); - const result = await tool.handler({ - component: 'TestComponent', - conversationContext: {step: 'invalid_step'}, - } as unknown as Record); + const result = await tool.handler( + { + component: 'TestComponent', + conversationContext: {step: 'invalid_step'}, + } as unknown as Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should return an error result for invalid step expect(result.isError).to.be.true; @@ -714,7 +806,8 @@ className: string;`, it('should handle missing required parameter', async () => { const tool = createPageDesignerDecoratorTool(() => services); - const result = await tool.handler({} as unknown as Record); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({} as unknown as Record, {} as any); // Should return an error result expect(result.isError).to.be.true; @@ -726,9 +819,13 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'StandardLocationComponent'); - const result = await tool.handler({ - component: 'StandardLocationComponent', - }); + const result = await tool.handler( + { + component: 'StandardLocationComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -746,9 +843,13 @@ export default function ProductCard({title}: ProductCardProps) { return
{ti 'utf8', ); - const result = await tool.handler({ - component: 'product-card', - }); + const result = await tool.handler( + { + component: 'product-card', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -766,9 +867,13 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` 'utf8', ); - const result = await tool.handler({ - component: 'Hero', - }); + const result = await tool.handler( + { + component: 'Hero', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -779,9 +884,13 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); - const result = await tool.handler({ - component: path.relative(testDir, componentPath), - }); + const result = await tool.handler( + { + component: path.relative(testDir, componentPath), + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -807,10 +916,14 @@ export default function CustomLocationComponent({title}: CustomLocationComponent 'utf8', ); - const result = await tool.handler({ - component: 'CustomLocationComponent', - searchPaths: ['custom/components'], - }); + const result = await tool.handler( + { + component: 'CustomLocationComponent', + searchPaths: ['custom/components'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -831,9 +944,13 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r 'utf8', ); - const result = await tool.handler({ - component: 'CollisionComponent', - }); + const result = await tool.handler( + { + component: 'CollisionComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should find one of the components (likely the first one found) expect(result.isError).to.be.undefined; @@ -847,9 +964,13 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ValidComponent'); - const result = await tool.handler({ - component: 'ValidComponent', - }); + const result = await tool.handler( + { + component: 'ValidComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should not error on valid input expect(result.isError).to.be.undefined; @@ -859,9 +980,13 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); - const result = await tool.handler({ - component: path.relative(testDir, componentPath), - }); + const result = await tool.handler( + { + component: path.relative(testDir, componentPath), + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should not error on valid path expect(result.isError).to.be.undefined; @@ -871,10 +996,14 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SearchComponent'); - const result = await tool.handler({ - component: 'SearchComponent', - searchPaths: ['src/components'], - }); + const result = await tool.handler( + { + component: 'SearchComponent', + searchPaths: ['src/components'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should not error with searchPaths expect(result.isError).to.be.undefined; @@ -884,10 +1013,14 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoModeComponent'); - const result = await tool.handler({ - component: 'AutoModeComponent', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'AutoModeComponent', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); // Should not error with autoMode expect(result.isError).to.be.undefined; @@ -897,11 +1030,15 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'CustomIdComponent'); - const result = await tool.handler({ - component: 'CustomIdComponent', - componentId: 'custom-component-id', - autoMode: true, - }); + const result = await tool.handler( + { + component: 'CustomIdComponent', + componentId: 'custom-component-id', + autoMode: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -916,12 +1053,21 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const results = await Promise.all( steps.map((step) => - tool.handler({ - component: 'ConversationComponent', - conversationContext: { - step: step as 'analyze' | 'configure_attrs' | 'configure_regions' | 'confirm_generation' | 'select_props', + tool.handler( + { + component: 'ConversationComponent', + conversationContext: { + step: step as + | 'analyze' + | 'configure_attrs' + | 'configure_regions' + | 'confirm_generation' + | 'select_props', + }, }, - }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ), ), ); @@ -944,9 +1090,13 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'FormatComponent'); - const result = await tool.handler({ - component: 'FormatComponent', - }); + const result = await tool.handler( + { + component: 'FormatComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result).to.have.property('content'); expect(result.content).to.be.an('array'); @@ -958,9 +1108,13 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r it('should return error format when component not found', async () => { const tool = createPageDesignerDecoratorTool(() => services); - const result = await tool.handler({ - component: 'NonExistentComponent', - }); + const result = await tool.handler( + { + component: 'NonExistentComponent', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.true; expect(result.content).to.be.an('array'); diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts index ed314744d..8257260c2 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts @@ -124,7 +124,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints, 'version1')); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -150,7 +151,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse([])); const tool = createScapiCustomApisStatusTool(() => services); - await tool.handler({status: 'active'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await tool.handler({status: 'active'}, {} as any); expect(mockGet.calledOnce).to.be.true; expect(mockGet.firstCall.args[1]?.params?.query).to.deep.equal({status: 'active'}); @@ -164,7 +166,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); const endpoints = parsed?.endpoints as Array<{type?: string; apiName?: string}>; @@ -179,7 +182,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse([], 'v1')); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.endpoints).to.be.an('array').that.is.empty; @@ -195,7 +199,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { }); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.total).to.equal(0); @@ -207,7 +212,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.rejects(new Error('Network error')); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.total).to.equal(0); @@ -222,7 +228,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({groupBy: 'type'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({groupBy: 'type'}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.groups).to.exist; @@ -241,7 +248,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({groupBy: 'site'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({groupBy: 'site'}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.groups).to.exist; @@ -256,7 +264,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({columns: 'type,apiName,status'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({columns: 'type,apiName,status'}, {} as any); const {parsed} = parseResultContent(result); const endpoint = (parsed?.endpoints as Record[])?.[0]; @@ -279,10 +288,14 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({ - columns: - 'type,apiName,apiVersion,cartridgeName,endpointPath,httpMethod,status,siteId,securityScheme,operationId,schemaFile,implementationScript,errorReason,id', - }); + const result = await tool.handler( + { + columns: + 'type,apiName,apiVersion,cartridgeName,endpointPath,httpMethod,status,siteId,securityScheme,operationId,schemaFile,implementationScript,errorReason,id', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); const {parsed} = parseResultContent(result); const endpoint = (parsed?.endpoints as Record[])?.[0]; @@ -309,7 +322,8 @@ describe('tools/scapi/scapi-custom-apis-status', () => { it('should return validation error for invalid status value', async () => { const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler({status: 'invalid'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({status: 'invalid'}, {} as any); expect(result.isError).to.be.true; const first = result.content?.[0] as undefined | {text?: string}; diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts index e82faf81a..465233223 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts @@ -107,7 +107,8 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -140,12 +141,16 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - await tool.handler({ - apiFamily: 'checkout', - apiName: 'shopper-baskets', - apiVersion: 'v1', - status: 'current', - }); + await tool.handler( + { + apiFamily: 'checkout', + apiName: 'shopper-baskets', + apiVersion: 'v1', + status: 'current', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(mockGet.firstCall.args[1]?.params?.query).to.deep.equal({ apiFamily: 'checkout', @@ -163,7 +168,8 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.schemas).to.be.an('array').that.is.empty; @@ -180,7 +186,8 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({apiFamily: 'checkout', status: 'current'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({apiFamily: 'checkout', status: 'current'}, {} as any); const {parsed} = parseResultContent(result); expect(parsed?.message).to.include('No SCAPI schemas match the filters'); @@ -202,7 +209,8 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); const {parsed} = parseResultContent(result); const first = (parsed?.schemas as Record[])?.[0]; @@ -218,7 +226,8 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; @@ -237,12 +246,16 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - }); + const result = await tool.handler( + { + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -277,13 +290,17 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiFamily: 'checkout', - apiName: 'shopper-baskets', - apiVersion: 'v1', - includeSchemas: true, - expandAll: true, - }); + const result = await tool.handler( + { + apiFamily: 'checkout', + apiName: 'shopper-baskets', + apiVersion: 'v1', + includeSchemas: true, + expandAll: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); const {parsed} = parseResultContent(result); expect(parsed?.collapsed).to.be.false; @@ -298,13 +315,17 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - status: 'current', - }); + const result = await tool.handler( + { + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + status: 'current', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); const {parsed} = parseResultContent(result); expect(parsed?.warning).to.include('status'); @@ -319,12 +340,16 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiFamily: 'product', - apiName: 'nonexistent-api', - apiVersion: 'v1', - includeSchemas: true, - }); + const result = await tool.handler( + { + apiFamily: 'product', + apiName: 'nonexistent-api', + apiVersion: 'v1', + includeSchemas: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; @@ -353,12 +378,16 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => servicesWithoutShortCode); - const result = await tool.handler({ - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - }); + const result = await tool.handler( + { + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -369,11 +398,15 @@ describe('tools/scapi/scapi-schemas-list', () => { describe('handler (validation and errors)', () => { it('returns error result when includeSchemas true but missing apiFamily', async () => { const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiName: 'shopper-baskets', - apiVersion: 'v1', - includeSchemas: true, - }); + const result = await tool.handler( + { + apiName: 'shopper-baskets', + apiVersion: 'v1', + includeSchemas: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.true; const {raw} = parseResultContent(result); @@ -383,18 +416,23 @@ describe('tools/scapi/scapi-schemas-list', () => { it('returns error result when includeSchemas true but missing apiVersion', async () => { const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({ - apiFamily: 'checkout', - apiName: 'shopper-baskets', - includeSchemas: true, - }); + const result = await tool.handler( + { + apiFamily: 'checkout', + apiName: 'shopper-baskets', + includeSchemas: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.true; }); it('returns validation error for invalid status value', async () => { const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler({status: 'invalid' as 'current'}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({status: 'invalid' as 'current'}, {} as any); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts index d16a96065..7af55ec4c 100644 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts +++ b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts @@ -91,7 +91,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { // Each section should be valid (tests that SECTIONS_METADATA is complete) for (const section of allSections) { - const result = tool.handler({sections: [section]}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = tool.handler({sections: [section]}, {} as any); expect(result).to.be.a('promise'); } }); @@ -158,7 +159,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { // Each section should be valid and retrievable for (const section of allSections) { - const result = tool.handler({sections: [section]}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = tool.handler({sections: [section]}, {} as any); expect(result).to.be.a('promise'); // Should not throw sync error } }); @@ -169,7 +171,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Should work without providing sections parameter - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.not.be.empty; }); @@ -194,8 +197,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { ]; for (const section of validSections) { - // eslint-disable-next-line no-await-in-loop - const result = await tool.handler({sections: [section]}); + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: [section]}, {} as any); expect(result.isError).to.be.undefined; } }); @@ -204,7 +207,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('default behavior', () => { it('should return comprehensive guidelines by default when no sections specified', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -227,7 +231,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return empty string when sections array is explicitly empty', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: []}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: []}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -261,7 +266,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return quick-reference section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['quick-reference']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -270,7 +276,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return data-fetching section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['data-fetching']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['data-fetching']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -279,7 +286,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return state-management section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['state-management']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['state-management']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -288,7 +296,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return auth section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['auth']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['auth']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -297,7 +306,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return config section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['config']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['config']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -306,7 +316,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return i18n section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['i18n']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['i18n']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -315,7 +326,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return components section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['components']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['components']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -324,7 +336,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return page-designer section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['page-designer']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['page-designer']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -333,7 +346,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return performance section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['performance']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['performance']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -342,7 +356,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return testing section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['testing']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['testing']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -351,7 +366,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return extensions section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['extensions']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['extensions']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -360,7 +376,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return pitfalls section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['pitfalls']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['pitfalls']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -373,9 +390,13 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Test related sections together (as mentioned in description) - const result = await tool.handler({ - sections: ['data-fetching', 'state-management'], - }); + const result = await tool.handler( + { + sections: ['data-fetching', 'state-management'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -393,9 +414,13 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should combine three sections correctly', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: ['auth', 'config', 'i18n'], - }); + const result = await tool.handler( + { + sections: ['auth', 'config', 'i18n'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -413,9 +438,13 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Request sections in specific order - const result = await tool.handler({ - sections: ['auth', 'config'], - }); + const result = await tool.handler( + { + sections: ['auth', 'config'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -439,22 +468,26 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle all sections at once', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ], - }); + const result = await tool.handler( + { + sections: [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -479,7 +512,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject invalid section names', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['invalid-section']} as any); + const result = await tool.handler({sections: ['invalid-section']} as any, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -489,7 +522,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject empty strings in sections array', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['']} as any); + const result = await tool.handler({sections: ['']} as any, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -499,7 +532,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject non-array sections parameter', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: 'quick-reference'} as any); + const result = await tool.handler({sections: 'quick-reference'} as any, {} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -510,7 +543,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('content verification', () => { it('should load actual markdown content from files', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['quick-reference']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -523,8 +557,10 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return different content for different sections', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result1 = await tool.handler({sections: ['data-fetching']}); - const result2 = await tool.handler({sections: ['auth']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result1 = await tool.handler({sections: ['data-fetching']}, {} as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result2 = await tool.handler({sections: ['auth']}, {} as any); expect(result1.isError).to.be.undefined; expect(result2.isError).to.be.undefined; @@ -550,8 +586,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { ]; for (const {section, keywords} of topicTests) { - // eslint-disable-next-line no-await-in-loop - const result = await tool.handler({sections: [section]}); + // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: [section]}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result).toLowerCase(); @@ -564,7 +600,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should provide non-negotiable architecture rules in quick-reference', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['quick-reference']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -583,7 +620,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should emphasize TypeScript-only approach', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['quick-reference']}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -596,7 +634,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('edge cases', () => { it('should handle undefined sections parameter', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: undefined}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: undefined}, {} as any); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -609,7 +648,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle sections parameter explicitly set to null', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: null} as any); + const result = await tool.handler({sections: null} as any, {} as any); // null is not a valid array, should error expect(result.isError).to.be.true; @@ -617,9 +656,13 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle duplicate sections in array', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: ['auth', 'auth'], - }); + const result = await tool.handler( + { + sections: ['auth', 'auth'], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any, + ); expect(result.isError).to.be.undefined; const text = getResultText(result); From 4605c1fc59b453f9eeef5fa3381884116ab1c47c Mon Sep 17 00:00:00 2001 From: cboscenco Date: Wed, 18 Feb 2026 08:21:41 -0800 Subject: [PATCH 11/14] Revert the handler signature change --- packages/b2c-dx-mcp/package.json | 1 - packages/b2c-dx-mcp/src/registry.ts | 2 +- packages/b2c-dx-mcp/src/server.ts | 9 +- packages/b2c-dx-mcp/src/tools/adapter.ts | 7 +- .../tools/page-designer-decorator/README.md | 1 - .../tools/page-designer-decorator/index.ts | 4 +- packages/b2c-dx-mcp/src/utils/types.ts | 8 +- .../b2c-dx-mcp/test/tools/adapter.test.ts | 88 ++-- .../test/tools/cartridges/index.test.ts | 42 +- .../b2c-dx-mcp/test/tools/mrt/index.test.ts | 26 +- .../page-designer-decorator/index.test.ts | 488 ++++++------------ .../scapi/scapi-custom-apis-status.test.ts | 42 +- .../tools/scapi/scapi-schemas-list.test.ts | 146 ++---- .../developer-guidelines.test.ts | 161 +++--- pnpm-lock.yaml | 3 - 15 files changed, 356 insertions(+), 672 deletions(-) diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index b2e630884..9c0f1cb70 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -96,7 +96,6 @@ "@modelcontextprotocol/sdk": "1.26.0", "@oclif/core": "catalog:", "@salesforce/b2c-tooling-sdk": "workspace:*", - "glob": "catalog:", "ts-morph": "^27.0.0", "yaml": "2.8.1", "zod": "3.25.76" diff --git a/packages/b2c-dx-mcp/src/registry.ts b/packages/b2c-dx-mcp/src/registry.ts index 02a677366..e24863a98 100644 --- a/packages/b2c-dx-mcp/src/registry.ts +++ b/packages/b2c-dx-mcp/src/registry.ts @@ -266,6 +266,6 @@ async function registerTools(tools: McpTool[], server: B2CDxMcpServer, allowNonG // Register the tool // TODO: Telemetry - Tool registration includes timing/error tracking - server.addTool(tool.name, tool.description, tool.inputSchema, async (args, context) => tool.handler(args, context)); + server.addTool(tool.name, tool.description, tool.inputSchema, async (args) => tool.handler(args)); } } diff --git a/packages/b2c-dx-mcp/src/server.ts b/packages/b2c-dx-mcp/src/server.ts index 7a2b37d80..146c73513 100644 --- a/packages/b2c-dx-mcp/src/server.ts +++ b/packages/b2c-dx-mcp/src/server.ts @@ -72,18 +72,15 @@ export class B2CDxMcpServer extends McpServer { name: string, description: string, inputSchema: ZodRawShape, - handler: ( - args: Record, - context: RequestHandlerExtra, - ) => Promise, + handler: (args: Record) => Promise, ): void { const wrappedHandler = async ( args: Record, - extra: RequestHandlerExtra, + _extra: RequestHandlerExtra, ): Promise => { const startTime = Date.now(); try { - const result = await handler(args, extra); + const result = await handler(args); const runTimeMs = Date.now() - startTime; await this.telemetry diff --git a/packages/b2c-dx-mcp/src/tools/adapter.ts b/packages/b2c-dx-mcp/src/tools/adapter.ts index 7accb0980..55a9b4120 100644 --- a/packages/b2c-dx-mcp/src/tools/adapter.ts +++ b/packages/b2c-dx-mcp/src/tools/adapter.ts @@ -73,8 +73,6 @@ import {z, type ZodRawShape, type ZodObject, type ZodType} from 'zod'; import type {B2CInstance} from '@salesforce/b2c-tooling-sdk'; -import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; -import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; import type {McpTool, ToolResult, Toolset} from '../utils/index.js'; import type {Services, MrtConfig} from '../services.js'; @@ -280,10 +278,7 @@ export function createToolAdapter( toolsets, isGA, - async handler( - rawArgs: Record, - _mcpContext: RequestHandlerExtra, - ): Promise { + async handler(rawArgs: Record): Promise { // 1. Validate input with Zod const parseResult = zodSchema.safeParse(rawArgs); if (!parseResult.success) { diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md index a874aa7f6..c56462460 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/README.md @@ -241,7 +241,6 @@ pnpm run test:agent -- test/tools/page-designer-decorator/index.test.ts ``` The test suite covers: - - Component discovery (name-based, kebab-case, nested, path-based, custom paths, name collisions) - Auto mode (basic, type inference, complex props exclusion, UI-only props exclusion, edge cases) - Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) diff --git a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts index dc07c2424..e94ba0472 100644 --- a/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts +++ b/packages/b2c-dx-mcp/src/tools/page-designer-decorator/index.ts @@ -5,8 +5,6 @@ */ import {z, type ZodRawShape} from 'zod'; -import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; -import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; import {componentAnalyzer, generateTypeSuggestions, resolveComponent, type TypeSuggestion} from './analyzer.js'; import {generateDecoratorCode, type AttributeContext, type MetadataContext} from './templates/decorator-generator.js'; import {pageDesignerDecoratorRules} from './rules.js'; @@ -619,7 +617,7 @@ export function createPageDesignerDecoratorTool(loadServices: () => Services): M toolsets: ['STOREFRONTNEXT'], isGA: false, - async handler(args: Record, _context: RequestHandlerExtra) { + async handler(args: Record) { try { // Validate and parse input const validatedArgs = pageDesignerDecoratorSchema.parse(args) as PageDesignerDecoratorInput; diff --git a/packages/b2c-dx-mcp/src/utils/types.ts b/packages/b2c-dx-mcp/src/utils/types.ts index ffaba59c0..254be6aca 100644 --- a/packages/b2c-dx-mcp/src/utils/types.ts +++ b/packages/b2c-dx-mcp/src/utils/types.ts @@ -5,8 +5,7 @@ */ import type {z, ZodRawShape} from 'zod'; -import type {CallToolResult, ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js'; -import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js'; +import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; import type {Toolset} from './constants.js'; /** @@ -36,10 +35,7 @@ export interface McpToolConfig { */ export interface McpTool extends McpToolConfig { /** Handler function that executes the tool */ - handler: ( - args: z.infer>, - context: RequestHandlerExtra, - ) => Promise; + handler: (args: z.infer>) => Promise; } /** diff --git a/packages/b2c-dx-mcp/test/tools/adapter.test.ts b/packages/b2c-dx-mcp/test/tools/adapter.test.ts index 48a82971d..ab636e53d 100644 --- a/packages/b2c-dx-mcp/test/tools/adapter.test.ts +++ b/packages/b2c-dx-mcp/test/tools/adapter.test.ts @@ -183,21 +183,18 @@ describe('tools/adapter', () => { ); // Test with valid input - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const validResult = await tool.handler({name: 'test', count: 5}, {} as any); + const validResult = await tool.handler({name: 'test', count: 5}); expect(validResult.isError).to.be.undefined; expect(getResultText(validResult)).to.equal('Received: test, 5'); // Test with missing required field - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const missingResult = await tool.handler({count: 5}, {} as any); + const missingResult = await tool.handler({count: 5}); expect(missingResult.isError).to.be.true; expect(getResultText(missingResult)).to.include('Invalid input'); expect(getResultText(missingResult)).to.include('name'); // Test with invalid type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const invalidTypeResult = await tool.handler({name: 'test', count: 'not-a-number'}, {} as any); + const invalidTypeResult = await tool.handler({name: 'test', count: 'not-a-number'}); expect(invalidTypeResult.isError).to.be.true; expect(getResultText(invalidTypeResult)).to.include('Invalid input'); }); @@ -220,8 +217,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({email: 'not-an-email'}, {} as any); + const result = await tool.handler({email: 'not-an-email'}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Invalid input'); @@ -246,8 +242,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Execution error'); @@ -272,8 +267,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Execution error'); @@ -300,8 +294,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({}, {} as any); + await tool.handler({}); const services = loadServices(); expect(receivedServices).to.equal(services); @@ -329,8 +322,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({projectName: 'my-storefront'}, {} as any); + const result = await tool.handler({projectName: 'my-storefront'}); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.equal('Created project: my-storefront'); @@ -357,8 +349,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; const parsed = JSON.parse(getResultText(result)); @@ -407,14 +398,12 @@ describe('tools/adapter', () => { ); // Without optional field - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result1 = await tool.handler({required: 'value'}, {} as any); + const result1 = await tool.handler({required: 'value'}); expect(result1.isError).to.be.undefined; expect(getResultText(result1)).to.equal('required: value, optional: not provided'); // With optional field - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result2 = await tool.handler({required: 'value', optional: 'present'}, {} as any); + const result2 = await tool.handler({required: 'value', optional: 'present'}); expect(result2.isError).to.be.undefined; expect(getResultText(result2)).to.equal('required: value, optional: present'); }); @@ -437,8 +426,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({items: ['a', 'b', 'c']}, {} as any); + const result = await tool.handler({items: ['a', 'b', 'c']}); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.equal('a, b, c'); @@ -464,8 +452,7 @@ describe('tools/adapter', () => { ); // Test with too short name - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({name: 'ab', age: 25}, {} as any); + const result = await tool.handler({name: 'ab', age: 25}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('Name must be at least 3 characters'); }); @@ -491,8 +478,7 @@ describe('tools/adapter', () => { ); // Default is now false, so tool should execute without instance - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.b2cInstance).to.be.undefined; @@ -514,8 +500,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('B2C instance error'); @@ -549,8 +534,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.be.undefined; @@ -579,8 +563,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -612,8 +595,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -647,8 +629,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined; @@ -676,8 +657,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(contextReceived?.b2cInstance).to.be.undefined; @@ -704,8 +684,7 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; expect(getResultText(result)).to.include('MRT auth error'); @@ -744,21 +723,16 @@ describe('tools/adapter', () => { ); // Empty list - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const emptyResult = await tool.handler({items: []}, {} as any); + const emptyResult = await tool.handler({items: []}); expect(getResultText(emptyResult)).to.equal('No items found.'); // With items - const itemsResult = await tool.handler( - { - items: [ - {id: 1, name: 'First'}, - {id: 2, name: 'Second'}, - ], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const itemsResult = await tool.handler({ + items: [ + {id: 1, name: 'First'}, + {id: 2, name: 'Second'}, + ], + }); expect(getResultText(itemsResult)).to.include('Found 2 items:'); expect(getResultText(itemsResult)).to.include('1: First'); expect(getResultText(itemsResult)).to.include('2: Second'); @@ -794,13 +768,11 @@ describe('tools/adapter', () => { loadServices, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const successResult = await tool.handler({operation: 'succeed'}, {} as any); + const successResult = await tool.handler({operation: 'succeed'}); expect(successResult.isError).to.be.undefined; expect(getResultText(successResult)).to.equal('Operation succeeded'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const failResult = await tool.handler({operation: 'fail'}, {} as any); + const failResult = await tool.handler({operation: 'fail'}); expect(failResult.isError).to.be.true; expect(getResultText(failResult)).to.equal('Operation failed'); }); diff --git a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts index 2aa7644a9..335a6819b 100644 --- a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts @@ -139,8 +139,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(findAndDeployCartridgesStub.calledOnce).to.be.true; @@ -176,8 +175,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({directory}, {} as any); + const result = await tool.handler({directory}); expect(result.isError).to.be.undefined; expect(findAndDeployCartridgesStub.calledOnce).to.be.true; @@ -206,8 +204,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({cartridges}, {} as any); + await tool.handler({cartridges}); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -231,8 +228,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({exclude}, {} as any); + await tool.handler({exclude}); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -254,8 +250,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({reload: true}, {} as any); + await tool.handler({reload: true}); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const args = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -282,16 +277,12 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - await tool.handler( - { - directory, - cartridges, - exclude, - reload, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + await tool.handler({ + directory, + cartridges, + exclude, + reload, + }); expect(findAndDeployCartridgesStub.calledOnce).to.be.true; const [, dir, options] = findAndDeployCartridgesStub.firstCall.args as [B2CInstance, string, DeployOptions]; @@ -318,8 +309,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; const jsonResult = getResultJson(result); @@ -338,8 +328,7 @@ describe('tools/cartridges', () => { }); const tool = createCartridgesTools(loadServices)[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; const text = getResultText(result); @@ -358,8 +347,7 @@ describe('tools/cartridges', () => { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; const text = getResultText(result); @@ -381,7 +369,7 @@ describe('tools/cartridges', () => { it('should validate input schema', async () => { // Test that invalid input is rejected by the adapter // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({directory: 123} as any, {} as any); + const result = await tool.handler({directory: 123} as any); expect(result.isError).to.be.true; const text = getResultText(result); diff --git a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts index 6d2760b4e..b5dc9339b 100644 --- a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts @@ -163,8 +163,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: buildDir}, {} as any); + const result = await tool.handler({buildDirectory: buildDir}); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -195,8 +194,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: buildDir}, {} as any); + const result = await tool.handler({buildDirectory: buildDir}); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -227,8 +225,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: buildDir}, {} as any); + const result = await tool.handler({buildDirectory: buildDir}); expect(result.isError).to.be.undefined; expect(pushBundleStub.calledOnce).to.be.true; @@ -256,8 +253,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({buildDirectory: buildDir, message: 'Custom deployment message'}, {} as any); + await tool.handler({buildDirectory: buildDir, message: 'Custom deployment message'}); expect(pushBundleStub.calledOnce).to.be.true; const [options] = pushBundleStub.firstCall.args as [PushOptions]; @@ -281,8 +277,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: buildDir, message: 'Release v1.0.0'}, {} as any); + const result = await tool.handler({buildDirectory: buildDir, message: 'Release v1.0.0'}); expect(result.isError).to.be.undefined; const jsonResult = getResultJson(result); @@ -301,8 +296,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices)[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; const text = getResultText(result); @@ -318,8 +312,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices)[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; const text = getResultText(result); @@ -340,8 +333,7 @@ describe('tools/mrt', () => { }); const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: buildDir}, {} as any); + const result = await tool.handler({buildDirectory: buildDir}); expect(result.isError).to.be.true; const text = getResultText(result); @@ -362,7 +354,7 @@ describe('tools/mrt', () => { it('should validate input schema', async () => { // Test that invalid input is rejected by the adapter // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({buildDirectory: 123} as any, {} as any); + const result = await tool.handler({buildDirectory: 123} as any); expect(result.isError).to.be.true; const text = getResultText(result); diff --git a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts index e2aef28a2..53b1ee76b 100644 --- a/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/page-designer-decorator/index.test.ts @@ -173,13 +173,9 @@ describe('tools/page-designer-decorator', () => { const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); - const result = await tool.handler( - { - component: 'TestComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'TestComponent', + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -200,13 +196,9 @@ describe('tools/page-designer-decorator', () => { const customServices = createMockServices(customDir); const tool = createPageDesignerDecoratorTool(() => customServices); - const result = await tool.handler( - { - component: 'CustomComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'CustomComponent', + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -221,14 +213,10 @@ describe('tools/page-designer-decorator', () => { const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoComponent', 'title: string;'); - const result = await tool.handler( - { - component: 'AutoComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'AutoComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -250,14 +238,10 @@ description: string; imageUrl: string;`, ); - const result = await tool.handler( - { - component: 'MultiPropComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'MultiPropComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -279,14 +263,10 @@ onClick: () => void; config: { key: string; value: number };`, ); - const result = await tool.handler( - { - component: 'ComplexPropsComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'ComplexPropsComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -313,14 +293,10 @@ className: string; style: React.CSSProperties;`, ); - const result = await tool.handler( - { - component: 'UIPropsComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'UIPropsComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -364,14 +340,10 @@ export default function DecoratedComponent({title}: DecoratedComponentProps) { 'utf8', ); - const result = await tool.handler( - { - component: 'DecoratedComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'DecoratedComponent', + autoMode: true, + }); // Should handle already-decorated components gracefully // May return an error or provide guidance @@ -392,14 +364,10 @@ export default function EmptyProps({}: EmptyPropsProps) { return
Empty
;`, ); - const result = await tool.handler( - { - component: 'ComplexOnlyComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'ComplexOnlyComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -453,14 +417,10 @@ data: Array<{id: number}>;`, count?: number;`, ); - const result = await tool.handler( - { - component: 'OptionalPropsComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'OptionalPropsComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -481,14 +441,10 @@ count?: number;`, value: string | number;`, ); - const result = await tool.handler( - { - component: 'UnionTypesComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'UnionTypesComponent', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -507,16 +463,12 @@ value: string | number;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AnalyzeComponent', 'title: string;'); - const result = await tool.handler( - { - component: 'AnalyzeComponent', - conversationContext: { - step: 'analyze', - }, + const result = await tool.handler({ + component: 'AnalyzeComponent', + conversationContext: { + step: 'analyze', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -538,16 +490,12 @@ onClick: () => void; className: string;`, ); - const result = await tool.handler( - { - component: 'CategorizedComponent', - conversationContext: { - step: 'analyze', - }, + const result = await tool.handler({ + component: 'CategorizedComponent', + conversationContext: { + step: 'analyze', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -566,22 +514,18 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SelectPropsComponent', 'title: string; description: string;'); - const result = await tool.handler( - { - component: 'SelectPropsComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title', 'description'], - componentMetadata: { - id: 'select-props-component', - name: 'Select Props Component', - description: 'Test component', - }, + const result = await tool.handler({ + component: 'SelectPropsComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title', 'description'], + componentMetadata: { + id: 'select-props-component', + name: 'Select Props Component', + description: 'Test component', }, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -596,18 +540,14 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataComponent'); - const result = await tool.handler( - { - component: 'MissingMetadataComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title'], - // Missing componentMetadata - }, + const result = await tool.handler({ + component: 'MissingMetadataComponent', + conversationContext: { + step: 'select_props', + selectedProps: ['title'], + // Missing componentMetadata }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); // Should return error when metadata is missing expect(result.isError).to.be.true; @@ -621,17 +561,13 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfigureAttrsComponent', 'imageUrl: string; description: string;'); - const result = await tool.handler( - { - component: 'ConfigureAttrsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'description'], - }, + const result = await tool.handler({ + component: 'ConfigureAttrsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'description'], }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -646,17 +582,13 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TypeSuggestionsComponent', 'imageUrl: string; productId: string;'); - const result = await tool.handler( - { - component: 'TypeSuggestionsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'productId'], - }, + const result = await tool.handler({ + component: 'TypeSuggestionsComponent', + conversationContext: { + step: 'configure_attrs', + selectedProps: ['imageUrl', 'productId'], }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -671,16 +603,12 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'RegionsComponent'); - const result = await tool.handler( - { - component: 'RegionsComponent', - conversationContext: { - step: 'configure_regions', - }, + const result = await tool.handler({ + component: 'RegionsComponent', + conversationContext: { + step: 'configure_regions', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -695,28 +623,24 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ConfirmComponent', 'title: string;'); - const result = await tool.handler( - { - component: 'ConfirmComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'confirm-component', - name: 'Confirm Component', - description: 'Test component', - }, - attributeConfig: { - title: { - type: 'string', - name: 'Title', - }, + const result = await tool.handler({ + component: 'ConfirmComponent', + conversationContext: { + step: 'confirm_generation', + selectedProps: ['title'], + componentMetadata: { + id: 'confirm-component', + name: 'Confirm Component', + description: 'Test component', + }, + attributeConfig: { + title: { + type: 'string', + name: 'Title', }, }, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -732,17 +656,13 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'MissingMetadataConfirmComponent'); - const result = await tool.handler( - { - component: 'MissingMetadataConfirmComponent', - conversationContext: { - step: 'confirm_generation', - // Missing componentMetadata - }, + const result = await tool.handler({ + component: 'MissingMetadataConfirmComponent', + conversationContext: { + step: 'confirm_generation', + // Missing componentMetadata }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + }); // Should return error when metadata is missing expect(result.isError).to.be.true; @@ -756,13 +676,9 @@ className: string;`, it('should handle non-existent component gracefully', async () => { const tool = createPageDesignerDecoratorTool(() => services); - const result = await tool.handler( - { - component: 'NonExistentComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'NonExistentComponent', + }); // Should return an error result expect(result.isError).to.be.true; @@ -774,13 +690,9 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); // Invalid input should be caught by zod validation - const result = await tool.handler( - { - component: 123, // Invalid type - } as unknown as Record, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 123, // Invalid type + } as unknown as Record); // Should return an error result expect(result.isError).to.be.true; @@ -790,14 +702,10 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'TestComponent'); - const result = await tool.handler( - { - component: 'TestComponent', - conversationContext: {step: 'invalid_step'}, - } as unknown as Record, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'TestComponent', + conversationContext: {step: 'invalid_step'}, + } as unknown as Record); // Should return an error result for invalid step expect(result.isError).to.be.true; @@ -806,8 +714,7 @@ className: string;`, it('should handle missing required parameter', async () => { const tool = createPageDesignerDecoratorTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({} as unknown as Record, {} as any); + const result = await tool.handler({} as unknown as Record); // Should return an error result expect(result.isError).to.be.true; @@ -819,13 +726,9 @@ className: string;`, const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'StandardLocationComponent'); - const result = await tool.handler( - { - component: 'StandardLocationComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'StandardLocationComponent', + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -843,13 +746,9 @@ export default function ProductCard({title}: ProductCardProps) { return
{ti 'utf8', ); - const result = await tool.handler( - { - component: 'product-card', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'product-card', + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -867,13 +766,9 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` 'utf8', ); - const result = await tool.handler( - { - component: 'Hero', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'Hero', + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -884,13 +779,9 @@ export default function Hero({title}: HeroProps) { return
{title}
; }` const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); - const result = await tool.handler( - { - component: path.relative(testDir, componentPath), - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: path.relative(testDir, componentPath), + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -916,14 +807,10 @@ export default function CustomLocationComponent({title}: CustomLocationComponent 'utf8', ); - const result = await tool.handler( - { - component: 'CustomLocationComponent', - searchPaths: ['custom/components'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'CustomLocationComponent', + searchPaths: ['custom/components'], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -944,13 +831,9 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r 'utf8', ); - const result = await tool.handler( - { - component: 'CollisionComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'CollisionComponent', + }); // Should find one of the components (likely the first one found) expect(result.isError).to.be.undefined; @@ -964,13 +847,9 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'ValidComponent'); - const result = await tool.handler( - { - component: 'ValidComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'ValidComponent', + }); // Should not error on valid input expect(result.isError).to.be.undefined; @@ -980,13 +859,9 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); const componentPath = createTestComponent(testDir, 'PathComponent'); - const result = await tool.handler( - { - component: path.relative(testDir, componentPath), - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: path.relative(testDir, componentPath), + }); // Should not error on valid path expect(result.isError).to.be.undefined; @@ -996,14 +871,10 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'SearchComponent'); - const result = await tool.handler( - { - component: 'SearchComponent', - searchPaths: ['src/components'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'SearchComponent', + searchPaths: ['src/components'], + }); // Should not error with searchPaths expect(result.isError).to.be.undefined; @@ -1013,14 +884,10 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'AutoModeComponent'); - const result = await tool.handler( - { - component: 'AutoModeComponent', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'AutoModeComponent', + autoMode: true, + }); // Should not error with autoMode expect(result.isError).to.be.undefined; @@ -1030,15 +897,11 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'CustomIdComponent'); - const result = await tool.handler( - { - component: 'CustomIdComponent', - componentId: 'custom-component-id', - autoMode: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'CustomIdComponent', + componentId: 'custom-component-id', + autoMode: true, + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -1053,21 +916,12 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const results = await Promise.all( steps.map((step) => - tool.handler( - { - component: 'ConversationComponent', - conversationContext: { - step: step as - | 'analyze' - | 'configure_attrs' - | 'configure_regions' - | 'confirm_generation' - | 'select_props', - }, + tool.handler({ + component: 'ConversationComponent', + conversationContext: { + step: step as 'analyze' | 'configure_attrs' | 'configure_regions' | 'confirm_generation' | 'select_props', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ), + }), ), ); @@ -1090,13 +944,9 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r const tool = createPageDesignerDecoratorTool(() => services); createTestComponent(testDir, 'FormatComponent'); - const result = await tool.handler( - { - component: 'FormatComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'FormatComponent', + }); expect(result).to.have.property('content'); expect(result.content).to.be.an('array'); @@ -1108,13 +958,9 @@ export default function CollisionComponent({title}: CollisionComponentProps) { r it('should return error format when component not found', async () => { const tool = createPageDesignerDecoratorTool(() => services); - const result = await tool.handler( - { - component: 'NonExistentComponent', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + component: 'NonExistentComponent', + }); expect(result.isError).to.be.true; expect(result.content).to.be.an('array'); diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts index 8257260c2..ed314744d 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts @@ -124,8 +124,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints, 'version1')); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -151,8 +150,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse([])); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await tool.handler({status: 'active'}, {} as any); + await tool.handler({status: 'active'}); expect(mockGet.calledOnce).to.be.true; expect(mockGet.firstCall.args[1]?.params?.query).to.deep.equal({status: 'active'}); @@ -166,8 +164,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); const endpoints = parsed?.endpoints as Array<{type?: string; apiName?: string}>; @@ -182,8 +179,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse([], 'v1')); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); expect(parsed?.endpoints).to.be.an('array').that.is.empty; @@ -199,8 +195,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { }); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); expect(parsed?.total).to.equal(0); @@ -212,8 +207,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.rejects(new Error('Network error')); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); expect(parsed?.total).to.equal(0); @@ -228,8 +222,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({groupBy: 'type'}, {} as any); + const result = await tool.handler({groupBy: 'type'}); const {parsed} = parseResultContent(result); expect(parsed?.groups).to.exist; @@ -248,8 +241,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({groupBy: 'site'}, {} as any); + const result = await tool.handler({groupBy: 'site'}); const {parsed} = parseResultContent(result); expect(parsed?.groups).to.exist; @@ -264,8 +256,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({columns: 'type,apiName,status'}, {} as any); + const result = await tool.handler({columns: 'type,apiName,status'}); const {parsed} = parseResultContent(result); const endpoint = (parsed?.endpoints as Record[])?.[0]; @@ -288,14 +279,10 @@ describe('tools/scapi/scapi-custom-apis-status', () => { mockGet.resolves(createMockClientResponse(mockEndpoints)); const tool = createScapiCustomApisStatusTool(() => services); - const result = await tool.handler( - { - columns: - 'type,apiName,apiVersion,cartridgeName,endpointPath,httpMethod,status,siteId,securityScheme,operationId,schemaFile,implementationScript,errorReason,id', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + columns: + 'type,apiName,apiVersion,cartridgeName,endpointPath,httpMethod,status,siteId,securityScheme,operationId,schemaFile,implementationScript,errorReason,id', + }); const {parsed} = parseResultContent(result); const endpoint = (parsed?.endpoints as Record[])?.[0]; @@ -322,8 +309,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { it('should return validation error for invalid status value', async () => { const tool = createScapiCustomApisStatusTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({status: 'invalid'}, {} as any); + const result = await tool.handler({status: 'invalid'}); expect(result.isError).to.be.true; const first = result.content?.[0] as undefined | {text?: string}; diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts index 465233223..e82faf81a 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts @@ -107,8 +107,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -141,16 +140,12 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - await tool.handler( - { - apiFamily: 'checkout', - apiName: 'shopper-baskets', - apiVersion: 'v1', - status: 'current', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + await tool.handler({ + apiFamily: 'checkout', + apiName: 'shopper-baskets', + apiVersion: 'v1', + status: 'current', + }); expect(mockGet.firstCall.args[1]?.params?.query).to.deep.equal({ apiFamily: 'checkout', @@ -168,8 +163,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); expect(parsed?.schemas).to.be.an('array').that.is.empty; @@ -186,8 +180,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({apiFamily: 'checkout', status: 'current'}, {} as any); + const result = await tool.handler({apiFamily: 'checkout', status: 'current'}); const {parsed} = parseResultContent(result); expect(parsed?.message).to.include('No SCAPI schemas match the filters'); @@ -209,8 +202,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); const {parsed} = parseResultContent(result); const first = (parsed?.schemas as Record[])?.[0]; @@ -226,8 +218,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; @@ -246,16 +237,12 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + }); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -290,17 +277,13 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiFamily: 'checkout', - apiName: 'shopper-baskets', - apiVersion: 'v1', - includeSchemas: true, - expandAll: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'checkout', + apiName: 'shopper-baskets', + apiVersion: 'v1', + includeSchemas: true, + expandAll: true, + }); const {parsed} = parseResultContent(result); expect(parsed?.collapsed).to.be.false; @@ -315,17 +298,13 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - status: 'current', - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + status: 'current', + }); const {parsed} = parseResultContent(result); expect(parsed?.warning).to.include('status'); @@ -340,16 +319,12 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiFamily: 'product', - apiName: 'nonexistent-api', - apiVersion: 'v1', - includeSchemas: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'product', + apiName: 'nonexistent-api', + apiVersion: 'v1', + includeSchemas: true, + }); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; @@ -378,16 +353,12 @@ describe('tools/scapi/scapi-schemas-list', () => { }); const tool = createScapiSchemasListTool(() => servicesWithoutShortCode); - const result = await tool.handler( - { - apiFamily: 'product', - apiName: 'shopper-products', - apiVersion: 'v1', - includeSchemas: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'product', + apiName: 'shopper-products', + apiVersion: 'v1', + includeSchemas: true, + }); expect(result.isError).to.be.undefined; const {parsed} = parseResultContent(result); @@ -398,15 +369,11 @@ describe('tools/scapi/scapi-schemas-list', () => { describe('handler (validation and errors)', () => { it('returns error result when includeSchemas true but missing apiFamily', async () => { const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiName: 'shopper-baskets', - apiVersion: 'v1', - includeSchemas: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiName: 'shopper-baskets', + apiVersion: 'v1', + includeSchemas: true, + }); expect(result.isError).to.be.true; const {raw} = parseResultContent(result); @@ -416,23 +383,18 @@ describe('tools/scapi/scapi-schemas-list', () => { it('returns error result when includeSchemas true but missing apiVersion', async () => { const tool = createScapiSchemasListTool(() => services); - const result = await tool.handler( - { - apiFamily: 'checkout', - apiName: 'shopper-baskets', - includeSchemas: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + apiFamily: 'checkout', + apiName: 'shopper-baskets', + includeSchemas: true, + }); expect(result.isError).to.be.true; }); it('returns validation error for invalid status value', async () => { const tool = createScapiSchemasListTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({status: 'invalid' as 'current'}, {} as any); + const result = await tool.handler({status: 'invalid' as 'current'}); expect(result.isError).to.be.true; const first = result.content?.[0] as {text?: string}; diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts index 7af55ec4c..d16a96065 100644 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts +++ b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts @@ -91,8 +91,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { // Each section should be valid (tests that SECTIONS_METADATA is complete) for (const section of allSections) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = tool.handler({sections: [section]}, {} as any); + const result = tool.handler({sections: [section]}); expect(result).to.be.a('promise'); } }); @@ -159,8 +158,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { // Each section should be valid and retrievable for (const section of allSections) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = tool.handler({sections: [section]}, {} as any); + const result = tool.handler({sections: [section]}); expect(result).to.be.a('promise'); // Should not throw sync error } }); @@ -171,8 +169,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Should work without providing sections parameter - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; expect(getResultText(result)).to.not.be.empty; }); @@ -197,8 +194,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { ]; for (const section of validSections) { - // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: [section]}, {} as any); + // eslint-disable-next-line no-await-in-loop + const result = await tool.handler({sections: [section]}); expect(result.isError).to.be.undefined; } }); @@ -207,8 +204,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('default behavior', () => { it('should return comprehensive guidelines by default when no sections specified', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({}, {} as any); + const result = await tool.handler({}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -231,8 +227,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return empty string when sections array is explicitly empty', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: []}, {} as any); + const result = await tool.handler({sections: []}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -266,8 +261,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return quick-reference section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['quick-reference']}, {} as any); + const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -276,8 +270,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return data-fetching section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['data-fetching']}, {} as any); + const result = await tool.handler({sections: ['data-fetching']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -286,8 +279,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return state-management section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['state-management']}, {} as any); + const result = await tool.handler({sections: ['state-management']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -296,8 +288,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return auth section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['auth']}, {} as any); + const result = await tool.handler({sections: ['auth']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -306,8 +297,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return config section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['config']}, {} as any); + const result = await tool.handler({sections: ['config']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -316,8 +306,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return i18n section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['i18n']}, {} as any); + const result = await tool.handler({sections: ['i18n']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -326,8 +315,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return components section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['components']}, {} as any); + const result = await tool.handler({sections: ['components']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -336,8 +324,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return page-designer section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['page-designer']}, {} as any); + const result = await tool.handler({sections: ['page-designer']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -346,8 +333,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return performance section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['performance']}, {} as any); + const result = await tool.handler({sections: ['performance']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -356,8 +342,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return testing section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['testing']}, {} as any); + const result = await tool.handler({sections: ['testing']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -366,8 +351,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return extensions section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['extensions']}, {} as any); + const result = await tool.handler({sections: ['extensions']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -376,8 +360,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return pitfalls section', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['pitfalls']}, {} as any); + const result = await tool.handler({sections: ['pitfalls']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -390,13 +373,9 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Test related sections together (as mentioned in description) - const result = await tool.handler( - { - sections: ['data-fetching', 'state-management'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + sections: ['data-fetching', 'state-management'], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -414,13 +393,9 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should combine three sections correctly', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler( - { - sections: ['auth', 'config', 'i18n'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + sections: ['auth', 'config', 'i18n'], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -438,13 +413,9 @@ describe('tools/storefrontnext/developer-guidelines', () => { const tool = createDeveloperGuidelinesTool(() => services); // Request sections in specific order - const result = await tool.handler( - { - sections: ['auth', 'config'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + sections: ['auth', 'config'], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -468,26 +439,22 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle all sections at once', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler( - { - sections: [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + sections: [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -512,7 +479,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject invalid section names', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['invalid-section']} as any, {} as any); + const result = await tool.handler({sections: ['invalid-section']} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -522,7 +489,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject empty strings in sections array', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['']} as any, {} as any); + const result = await tool.handler({sections: ['']} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -532,7 +499,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should reject non-array sections parameter', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: 'quick-reference'} as any, {} as any); + const result = await tool.handler({sections: 'quick-reference'} as any); expect(result.isError).to.be.true; const text = getResultText(result); @@ -543,8 +510,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('content verification', () => { it('should load actual markdown content from files', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['quick-reference']}, {} as any); + const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -557,10 +523,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should return different content for different sections', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result1 = await tool.handler({sections: ['data-fetching']}, {} as any); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result2 = await tool.handler({sections: ['auth']}, {} as any); + const result1 = await tool.handler({sections: ['data-fetching']}); + const result2 = await tool.handler({sections: ['auth']}); expect(result1.isError).to.be.undefined; expect(result2.isError).to.be.undefined; @@ -586,8 +550,8 @@ describe('tools/storefrontnext/developer-guidelines', () => { ]; for (const {section, keywords} of topicTests) { - // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: [section]}, {} as any); + // eslint-disable-next-line no-await-in-loop + const result = await tool.handler({sections: [section]}); expect(result.isError).to.be.undefined; const text = getResultText(result).toLowerCase(); @@ -600,8 +564,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should provide non-negotiable architecture rules in quick-reference', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['quick-reference']}, {} as any); + const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -620,8 +583,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should emphasize TypeScript-only approach', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['quick-reference']}, {} as any); + const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -634,8 +596,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('edge cases', () => { it('should handle undefined sections parameter', async () => { const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: undefined}, {} as any); + const result = await tool.handler({sections: undefined}); expect(result.isError).to.be.undefined; const text = getResultText(result); @@ -648,7 +609,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle sections parameter explicitly set to null', async () => { const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: null} as any, {} as any); + const result = await tool.handler({sections: null} as any); // null is not a valid array, should error expect(result.isError).to.be.true; @@ -656,13 +617,9 @@ describe('tools/storefrontnext/developer-guidelines', () => { it('should handle duplicate sections in array', async () => { const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler( - { - sections: ['auth', 'auth'], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any, - ); + const result = await tool.handler({ + sections: ['auth', 'auth'], + }); expect(result.isError).to.be.undefined; const text = getResultText(result); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 524170d1f..ca0287d26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,9 +261,6 @@ importers: '@salesforce/b2c-tooling-sdk': specifier: workspace:* version: link:../b2c-tooling-sdk - glob: - specifier: 'catalog:' - version: 13.0.0 ts-morph: specifier: ^27.0.0 version: 27.0.2 From 081193c6622beda779fd30a2ed2b7e83d348773c Mon Sep 17 00:00:00 2001 From: cboscenco Date: Wed, 18 Feb 2026 08:34:51 -0800 Subject: [PATCH 12/14] Add missing dependency --- packages/b2c-dx-mcp/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index 9c0f1cb70..b2e630884 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -96,6 +96,7 @@ "@modelcontextprotocol/sdk": "1.26.0", "@oclif/core": "catalog:", "@salesforce/b2c-tooling-sdk": "workspace:*", + "glob": "catalog:", "ts-morph": "^27.0.0", "yaml": "2.8.1", "zod": "3.25.76" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca0287d26..524170d1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,9 @@ importers: '@salesforce/b2c-tooling-sdk': specifier: workspace:* version: link:../b2c-tooling-sdk + glob: + specifier: 'catalog:' + version: 13.0.0 ts-morph: specifier: ^27.0.0 version: 27.0.2 From caa3ae5bd27179aad6bcd141260cc82e122433b6 Mon Sep 17 00:00:00 2001 From: cboscenco Date: Wed, 18 Feb 2026 09:20:05 -0800 Subject: [PATCH 13/14] Revert build dependencies changes --- pnpm-workspace.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fb8919319..3c9019915 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -41,11 +41,6 @@ minimumReleaseAgeExclude: [] nodeLinker: hoisted onlyBuiltDependencies: - - '@vscode/vsce-sign' - - esbuild - - keytar - - msw - - protobufjs - unrs-resolver - yarn From 3459eca93d8cd346de7ce0fb676dbb4d82b08d1f Mon Sep 17 00:00:00 2001 From: cboscenco Date: Wed, 18 Feb 2026 09:26:44 -0800 Subject: [PATCH 14/14] Additional revert for the workspace file --- pnpm-workspace.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3c9019915..90b7adcdb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,6 +40,9 @@ minimumReleaseAgeExclude: [] nodeLinker: hoisted +# Pin exact versions by default when adding dependencies (no ^ or ~ prefix) +savePrefix: '' + onlyBuiltDependencies: - unrs-resolver - yarn @@ -59,8 +62,6 @@ overrides: preact@>=10.27.0 <10.27.3: '>=10.27.3' qs@<6.14.1: '>=6.14.1' -savePrefix: '' - trustPolicy: no-downgrade trustPolicyExclude: []