diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000000..7ad48fed88 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,16 @@ +export default { + testEnvironment: 'node', + testMatch: [ + '**/__tests__/**/*.js', + '**/?(*.)+(spec|test).js' + ], + collectCoverageFrom: [ + 'src/**/*.js', + '!src/**/*.test.js', + '!src/**/*.spec.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 10000, + extensionsToTreatAsEsm: ['.js'] +} \ No newline at end of file diff --git a/packages/pwa-storefront-mcp/.eslintrc.js b/packages/pwa-storefront-mcp/.eslintrc.cjs similarity index 100% rename from packages/pwa-storefront-mcp/.eslintrc.js rename to packages/pwa-storefront-mcp/.eslintrc.cjs diff --git a/packages/pwa-storefront-mcp/README.md b/packages/pwa-storefront-mcp/README.md index 309a0adba5..60f363c391 100644 --- a/packages/pwa-storefront-mcp/README.md +++ b/packages/pwa-storefront-mcp/README.md @@ -11,13 +11,15 @@ The Model Context Protocol (MCP) is an open protocol that enables secure connect ## Features This MCP server provides: + - `development_guidelines`: Help developers to understand and follow PWA Storefront developer guidelines and best practices - `create_new_component`: Help developers to create a new PWA Storefront component. It will guide developers through a few simple questions and then generate code for the component based on the commerce data model used, layouts, etc. - `submit_pwa_kit_project_answers`: Help developers to generate a new PWA Storefront project ## Setup -1. Install dependencies: +Install dependencies: + ```bash npm install ``` @@ -25,6 +27,7 @@ npm install ## Run the MCP Server ### Method 1: Run MCP Server From Cursor + Open Cursor Application Go to Cursor Menu on top menu bar, then *Settings* > *Cursor Settings...* @@ -36,7 +39,8 @@ Select Tools & Integrations > MCP Tools > New MCP Server Cursor MCP Tools Screenshot You will be led to mcp.json file. Add this to your mcp.json: -``` json + +```json { "mcpServers": { @@ -50,6 +54,7 @@ You will be led to mcp.json file. Add this to your mcp.json: ``` Cursor will: + - Start the MCP server - Connect to it as a client - List available tools @@ -59,12 +64,14 @@ You can go back to MCP Tools choose to enable/disable any MCP Server or tools. ### Method 2: Run MCP Server from Claude #### Using Claude Desktop + 1. Go to Claude menu on top menu bar then "Developer" > "Edit Config" This will lead you to "claude_desktop_config.json" file. Claude MCP Config Screenshot 2. Add this server to your claude_desktop_config.json: + ```json { "mcpServers": { @@ -78,6 +85,7 @@ This will lead you to "claude_desktop_config.json" file. ``` Claude will: + - Start the MCP server - Connect to it as a client - List available tools @@ -129,36 +137,23 @@ The server will output debug information to stderr and handle MCP protocol messa - mcp.json - claude_desktop_config.json /src - /components - - index.js - - PrimaryButton.jsx - ... (other components) /server - server.js /utils - - AddComponentTool.js - pwa-developer-guideline-tool.js - /scripts - - create-button.js - - demo.js + - utils.js /tests - /images + - test-mcp.js + /docs + /images - claude-config.png - claude-list-tools.png - cursor-list-tools.png - cursor-settings.pnb - - test-mcp.js - /docs - cursor-integration-guide.md /node_modules - /.cursor ``` -- All React components are in `src/components/`. - Server code is in `src/server/`. - Utilities/tools are in `src/utils/`. -- Scripts are in `src/scripts/`. -- Tests are in `src/tests/`. - Documentation is in `docs/`. - -Update your import paths accordingly. \ No newline at end of file diff --git a/packages/pwa-storefront-mcp/babel.config.js b/packages/pwa-storefront-mcp/babel.config.cjs similarity index 58% rename from packages/pwa-storefront-mcp/babel.config.js rename to packages/pwa-storefront-mcp/babel.config.cjs index 458a4a983a..571a665a67 100644 --- a/packages/pwa-storefront-mcp/babel.config.js +++ b/packages/pwa-storefront-mcp/babel.config.cjs @@ -4,4 +4,9 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -module.exports = require('@salesforce/pwa-kit-dev/configs/babel/babel-config') +const parentConfig = require('@salesforce/pwa-kit-dev/configs/babel/babel-config').default + +module.exports = { + ...parentConfig, + sourceType: 'module' +} diff --git a/packages/pwa-storefront-mcp/demo.js b/packages/pwa-storefront-mcp/demo.js deleted file mode 100644 index 05f865f69c..0000000000 --- a/packages/pwa-storefront-mcp/demo.js +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env node -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ - -import {spawn} from 'child_process' - -// Example React code to analyze and modify -const exampleCode = `import React from 'react'; -import './App.css'; - -function App() { - return ( -
-
-

My PWA Storefront

-
-
- ); -} - -export default App;` - -async function demonstrateMCPTools() { - console.log('šŸš€ Demonstrating MCP Server Tools...\n') - - // Start the MCP server process - const serverProcess = spawn('node', ['src/server/server.js'], { - stdio: ['pipe', 'pipe', 'pipe'] - }) - - let responseData = '' - - serverProcess.stdout.on('data', (data) => { - responseData += data.toString() - }) - - serverProcess.stderr.on('data', (data) => { - console.log('Server:', data.toString().trim()) - }) - - try { - // Initialize connection - const initRequest = { - jsonrpc: '2.0', - id: 1, - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: {name: 'demo-client', version: '1.0.0'} - } - } - serverProcess.stdin.write(JSON.stringify(initRequest) + '\n') - await wait(500) - - // Test 1: Analyze code structure - console.log('šŸ“‹ 1. Analyzing code structure...') - const analyzeRequest = { - jsonrpc: '2.0', - id: 2, - method: 'tools/call', - params: { - name: 'analyze_code_structure', - arguments: {code: exampleCode} - } - } - serverProcess.stdin.write(JSON.stringify(analyzeRequest) + '\n') - await wait(1000) - - // Test 2: Insert a Product Card component - console.log('\nšŸ›ļø 2. Inserting a ProductCard component...') - const insertRequest = { - jsonrpc: '2.0', - id: 3, - method: 'tools/call', - params: { - name: 'insert_react_component', - arguments: { - code: exampleCode, - componentType: 'product', - options: { - name: 'ProductCard', - styling: 'tailwind', - showPrice: true, - showRating: true - } - } - } - } - serverProcess.stdin.write(JSON.stringify(insertRequest) + '\n') - await wait(1000) - - // Test 3: Create a new Button component file - console.log('\nšŸ”˜ 3. Creating a new Button component file...') - const createRequest = { - jsonrpc: '2.0', - id: 4, - method: 'tools/call', - params: { - name: 'create_component_file', - arguments: { - componentName: 'PrimaryButton', - componentType: 'button', - options: { - variant: 'primary', - size: 'medium', - styling: 'tailwind' - } - } - } - } - serverProcess.stdin.write(JSON.stringify(createRequest) + '\n') - await wait(1000) - - // Parse and display results - console.log('\nšŸ“Ø Results:') - parseAndDisplayResults(responseData) - } catch (error) { - console.error('āŒ Error:', error) - } finally { - serverProcess.kill() - } -} - -function parseAndDisplayResults(responseData) { - if (!responseData.trim()) { - console.log('No responses received') - return - } - - const responses = responseData - .trim() - .split('\n') - .filter((line) => line.trim()) - - responses.forEach((response, index) => { - try { - const parsed = JSON.parse(response) - if (parsed.id === 1) { - console.log('āœ… Server initialized') - } else if (parsed.id === 2 && parsed.result) { - const data = JSON.parse(parsed.result.content[0].text) - console.log('\nšŸ“Š Code Analysis:') - console.log(`- Components found: ${data.summary.totalComponents}`) - console.log(`- Imports found: ${data.summary.totalImports}`) - console.log(`- Has React: ${data.summary.hasReact}`) - console.log(`- Has Tailwind: ${data.summary.hasTailwind}`) - console.log(`- Insertion points: ${data.summary.insertionPoints}`) - } else if (parsed.id === 3 && parsed.result) { - const data = JSON.parse(parsed.result.content[0].text) - if (data.success) { - console.log('\nāœ… Component inserted successfully!') - console.log('Modified code preview:') - console.log('```javascript') - console.log(data.modifiedCode.substring(0, 500) + '...') - console.log('```') - } - } else if (parsed.id === 4 && parsed.result) { - const data = JSON.parse(parsed.result.content[0].text) - if (data.success) { - console.log('\nāœ… New component file created!') - console.log(`Component: ${data.componentName}`) - console.log('Generated code preview:') - console.log('```javascript') - console.log(data.code.substring(0, 300) + '...') - console.log('```') - } - } - } catch (e) { - console.log(`Response ${index + 1}:`, response) - } - }) -} - -function wait(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -demonstrateMCPTools().catch(console.error) diff --git a/packages/pwa-storefront-mcp/jest-setup.js b/packages/pwa-storefront-mcp/jest-setup.cjs similarity index 100% rename from packages/pwa-storefront-mcp/jest-setup.js rename to packages/pwa-storefront-mcp/jest-setup.cjs diff --git a/packages/pwa-storefront-mcp/jest.config.cjs b/packages/pwa-storefront-mcp/jest.config.cjs new file mode 100644 index 0000000000..f1cc1ad554 --- /dev/null +++ b/packages/pwa-storefront-mcp/jest.config.cjs @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +const parentConfig = require('@salesforce/pwa-kit-dev/configs/jest/jest.config.js') + +module.exports = { + ...parentConfig, + testEnvironment: 'node', + testMatch: [ + '**/__tests__/**/*.js', + '**/?(*.)+(spec|test).js' + ], + collectCoverageFrom: [ + 'src/**/*.js', + '!src/**/*.test.js', + '!src/**/*.spec.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 10000 +} \ No newline at end of file diff --git a/packages/pwa-storefront-mcp/jest.config.js b/packages/pwa-storefront-mcp/jest.config.js deleted file mode 100644 index 0a164b66dd..0000000000 --- a/packages/pwa-storefront-mcp/jest.config.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024, salesforce.com, inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const path = require('path') -// eslint-disable-next-line @typescript-eslint/no-var-requires -const base = require('@salesforce/pwa-kit-dev/configs/jest/jest.config.js') - -module.exports = { - ...base, - moduleNameMapper: { - ...base.moduleNameMapper, - '^react$': '/node_modules/react/index.js', - '^react-router-dom(.*)$': '/node_modules/react-router-dom/index.js' - // Add more mappings as needed for this package - }, - transformIgnorePatterns: ['/node_modules/'], - setupFilesAfterEnv: [path.join(__dirname, 'jest-setup.js')], - collectCoverageFrom: ['src/**/*.{js,jsx}', '!node_modules/**'], - coverageThreshold: { - global: { - statements: 73, - branches: 60, - functions: 65, - lines: 74 - } - }, - ...(process.env.CI ? {testTimeout: 30000} : {}) -} diff --git a/packages/pwa-storefront-mcp/mcp.json b/packages/pwa-storefront-mcp/mcp.json index acbd938de4..c159f1d294 100644 --- a/packages/pwa-storefront-mcp/mcp.json +++ b/packages/pwa-storefront-mcp/mcp.json @@ -1,9 +1,9 @@ { "mcpServers": { - "pwa-storefront-mcp-server": { - "command": "node", - "args": ["src/server/server.js"], - "cwd": "." + "pwa-storefront-mcp": { + "command": "node {{parent-dir-to-mcp}}/pwa-storefront-mcp/src/server/server.js", + "transport": "stdio", + "args": [] } } } \ No newline at end of file diff --git a/packages/pwa-storefront-mcp/package.json b/packages/pwa-storefront-mcp/package.json index 93c3f29fed..333225f201 100644 --- a/packages/pwa-storefront-mcp/package.json +++ b/packages/pwa-storefront-mcp/package.json @@ -4,6 +4,7 @@ "private": true, "description": "MCP server that helps you build Salesforce Commerce Cloud PWA Kit Composable Storefront", "main": "src/server/server.js", + "type": "module", "scripts": { "build": "echo 'No build step required for MCP server'", "format": "pwa-kit-dev format \"**/*.{js,jsx}\"", diff --git a/packages/pwa-storefront-mcp/src/server/server.js b/packages/pwa-storefront-mcp/src/server/server.js index 4f6efb823a..0421f0eade 100644 --- a/packages/pwa-storefront-mcp/src/server/server.js +++ b/packages/pwa-storefront-mcp/src/server/server.js @@ -5,15 +5,8 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import {McpServer, ResourceTemplate} from '@modelcontextprotocol/sdk/server/mcp.js' +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' -import {z} from 'zod' -import {AddComponentTool} from '../utils/AddComponentTool.js' -import {InsertExistingComponentTool} from '../utils/InsertExistingComponentTool.js' -import {CreateNewComponentTool} from '../utils/CreateNewComponentTool.js' -import fs from 'fs/promises' -import path from 'path' -import {fileURLToPath} from 'url' import {DeveloperGuidelinesTool} from '../utils/pwa-developer-guideline-tool.js' class PwaStorefrontMCPServerHighLevel { @@ -31,9 +24,6 @@ class PwaStorefrontMCPServerHighLevel { } ) - this.addComponentTool = new AddComponentTool() - this.insertExistingComponentTool = new InsertExistingComponentTool() - this.CreateNewComponentTool = new CreateNewComponentTool() this.setupTools() } @@ -45,220 +35,6 @@ class PwaStorefrontMCPServerHighLevel { DeveloperGuidelinesTool.inputSchema, DeveloperGuidelinesTool.fn ) - - this.server.tool( - 'analyze_code_structure', - 'Analyze JavaScript/React code structure to identify components, imports, and insertion points', - { - code: z.string().describe('The JavaScript/React code to analyze') - }, - async (args) => { - try { - const analysis = this.addComponentTool.analyzeCodeStructure(args.code) - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - analysis, - summary: { - totalImports: analysis.imports.length, - totalComponents: analysis.components.length, - hasReact: analysis.hasReact, - hasNextJs: analysis.hasNextJs, - hasTailwind: analysis.hasTailwind, - insertionPoints: analysis.insertionPoints.length - } - }, - null, - 2 - ) - } - ] - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: JSON.stringify({error: error.message}, null, 2) - } - ], - isError: true - } - } - } - ) - - this.server.tool( - 'insert_existing_component', - 'Insert an existing React component into an existing page', - { - componentName: z.string().describe('Component name'), - targetPage: z.string().describe('Target page name or path'), - options: z - .object({ - beforeComponentName: z - .string() - .optional() - .describe('Insert before Component name'), - afterComponentName: z - .string() - .optional() - .describe('Insert after Component name') - }) - .optional() - }, - async (args) => { - try { - const modifiedCode = this.insertExistingComponentTool.insertComponentIntoPage( - args.targetPage, - args.componentName - ) - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - success: true, - modifiedCode, - componentType: args.componentType, - options: args.options - }, - null, - 2 - ) - } - ] - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: JSON.stringify({error: error.message}, null, 2) - } - ], - isError: true - } - } - } - ) - - this.server.tool( - 'create_new_component', - 'Create a new React component file based on the provided code or a new component', - { - componentName: z.string().describe('Name of the component to create'), - componentCode: z.string().optional().describe('Code of the component to create'), - projectDir: z.string().optional().describe('Directory of Retail React App') - }, - async (args) => { - try { - const componentCode = this.CreateNewComponentTool.createNewComponent( - args.componentName, - args.componentCode, - args.projectDir - ) - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - success: true, - componentName: args.componentName, - code: componentCode - }, - null, - 2 - ) - } - ] - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: JSON.stringify({error: error.message}, null, 2) - } - ], - isError: true - } - } - } - ) - - this.server.resource( - 'data-model', - new ResourceTemplate('data://data-models/{modelName}', {}), - { - title: 'Commerce Cloud Data Model', - description: 'Commerce Cloud Data Model, such as Product, Category, Order, etc.' - }, - async (uri, {modelName}) => { - return this.getDataModelDocument(modelName, uri.href) - } - ) - - this.server.tool( - 'get_data_model', - 'Get the schema of a data model', - { - modelName: z - .string() - .describe('The name of the data model (e.g., Product, Category, etc.)') - }, - async ({modelName}) => { - const uriHref = `data://data-models/${modelName}` - const result = await this.getDataModelDocument(modelName, uriHref) - return { - content: result.contents.map((item) => ({ - type: 'text', - text: item.text - })) - } - } - ) - } - - async getDataModelDocument(modelName, uriHref) { - try { - const __filename = fileURLToPath(import.meta.url) - const __dirname = path.dirname(__filename) - const dataDir = path.join(__dirname, '..', 'data') - const filePath = path.join(dataDir, `${modelName}Document.json`) - let fileContent - try { - fileContent = await fs.readFile(filePath, 'utf8') - } catch (err) { - if (err.code === 'ENOENT') { - fileContent = JSON.stringify({message: `No document found for ${modelName}`}) - } else { - throw err - } - } - return { - contents: [ - { - uri: uriHref, - text: fileContent - } - ] - } - } catch (error) { - return { - contents: [ - { - uri: uriHref, - text: JSON.stringify({error: error.message}, null, 2) - } - ] - } - } } async run() { diff --git a/packages/pwa-storefront-mcp/src/server/server.test.js b/packages/pwa-storefront-mcp/src/server/server.test.js new file mode 100644 index 0000000000..9cf70af400 --- /dev/null +++ b/packages/pwa-storefront-mcp/src/server/server.test.js @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import {spawn} from 'child_process' + +function sendJsonRpcRequest(child, request) { + return new Promise((resolve, reject) => { + let data = '' + const onData = (chunk) => { + data += chunk.toString() + // MCP server sends each message as a line-delimited JSON + if (data.includes('\n')) { + child.stdout.off('data', onData) + try { + // Only parse the first line (response) + const line = data.split('\n').find((l) => l.trim().length > 0) + resolve(JSON.parse(line)) + } catch (e) { + reject(e) + } + } + } + child.stdout.on('data', onData) + child.stdin.write(JSON.stringify(request) + '\n') + }) +} + +describe('PwaStorefrontMCPServerHighLevel integration', () => { + it('should list registered tools via stdio', async () => { + const child = spawn('node', ['src/server/server.js'], { + cwd: process.cwd(), + stdio: ['pipe', 'pipe', 'inherit'] + }) + + // Wait a moment for the server to start + await new Promise((r) => setTimeout(r, 500)) + + // Send the list tools request (JSON-RPC 2.0) + const request = { + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + params: {} + } + const response = await sendJsonRpcRequest(child, request) + expect(response).toHaveProperty('result') + expect(response.result).toHaveProperty('tools') + // Check that at least the DeveloperGuidelinesTool is present + const toolNames = response.result.tools.map((t) => t.name) + expect(toolNames).toContain('development_guidelines') + + child.kill() + }, 10000) +}) diff --git a/packages/pwa-storefront-mcp/src/tests/test-mcp.js b/packages/pwa-storefront-mcp/src/tests/test-mcp.js deleted file mode 100644 index a9cbece6b8..0000000000 --- a/packages/pwa-storefront-mcp/src/tests/test-mcp.js +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env node -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import {spawn} from 'child_process' - -async function testMCPServer() { - console.log('šŸš€ Testing MCP Server...\n') - - // Start the MCP server process - const serverProcess = spawn('node', ['src/server/server.js'], { - stdio: ['pipe', 'pipe', 'pipe'] - }) - - // Handle server stderr output - serverProcess.stderr.on('data', (data) => { - console.log('Server:', data.toString().trim()) - }) - - let responseData = '' - - // Collect server responses - serverProcess.stdout.on('data', (data) => { - responseData += data.toString() - }) - - try { - console.log('āœ… Started MCP server') - - // Test 1: Initialize the connection - console.log('\nšŸ”— Initializing connection...') - const initRequest = { - jsonrpc: '2.0', - id: 1, - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { - name: 'test-client', - version: '1.0.0' - } - } - } - - serverProcess.stdin.write(JSON.stringify(initRequest) + '\n') - - // Wait a bit for response - await new Promise((resolve) => setTimeout(resolve, 1000)) - - // Test 2: List available tools - console.log('\nšŸ“‹ Listing available tools...') - const toolsRequest = { - jsonrpc: '2.0', - id: 2, - method: 'tools/list', - params: {} - } - - serverProcess.stdin.write(JSON.stringify(toolsRequest) + '\n') - - // Wait for response - await new Promise((resolve) => setTimeout(resolve, 1000)) - - // Test 3: Call the create_new_component tool - console.log('\nšŸ• Calling create_new_component tool...') - const toolCallRequest = { - jsonrpc: '2.0', - id: 3, - method: 'tools/call', - params: { - name: 'create_new_component', - arguments: { - componentName: 'my-new-component' - } - } - } - - serverProcess.stdin.write(JSON.stringify(toolCallRequest) + '\n') - - // Wait for final response - await new Promise((resolve) => setTimeout(resolve, 1000)) - - console.log('\nšŸ“Ø Server responses:') - if (responseData.trim()) { - // Split by lines and parse each JSON response - const responses = responseData - .trim() - .split('\n') - .filter((line) => line.trim()) - responses.forEach((response, index) => { - try { - const parsed = JSON.parse(response) - console.log(`Response ${index + 1}:`, JSON.stringify(parsed, null, 2)) - } catch (e) { - console.log(`Raw response ${index + 1}:`, response) - } - }) - } else { - console.log('No responses received from server') - } - - console.log('\nāœ… Test completed!') - } catch (error) { - console.error('āŒ Error during testing:', error) - } finally { - // Clean up - serverProcess.kill() - process.exit(0) - } -} - -testMCPServer().catch(console.error) diff --git a/packages/pwa-storefront-mcp/src/utils/AddComponentTool.js b/packages/pwa-storefront-mcp/src/utils/AddComponentTool.js deleted file mode 100644 index f2e0b4db82..0000000000 --- a/packages/pwa-storefront-mcp/src/utils/AddComponentTool.js +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -export class AddComponentTool { - constructor() { - this.componentTemplates = { - button: this.createButtonComponent, - card: this.createCardComponent, - modal: this.createModalComponent, - form: this.createFormComponent, - list: this.createListComponent, - header: this.createHeaderComponent, - footer: this.createFooterComponent, - product: this.createProductComponent, - cart: this.createCartComponent - } - } - - /** - * Analyze JavaScript/React code and determine insertion points - */ - analyzeCodeStructure(code) { - const analysis = { - imports: [], - components: [], - exports: [], - insertionPoints: [], - hasReact: false, - hasNextJs: false, - hasTailwind: false - } - - const lines = code.split('\n') - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim() - - // Detect imports - if (line.startsWith('import ')) { - analysis.imports.push({ - line: i, - content: line, - type: this.getImportType(line) - }) - - if (line.includes('react')) analysis.hasReact = true - if (line.includes('next/')) analysis.hasNextJs = true - } - - // Detect component definitions - if (this.isComponentDefinition(line)) { - analysis.components.push({ - line: i, - name: this.extractComponentName(line), - type: this.getComponentType(line) - }) - } - - // Detect exports - if (line.startsWith('export ')) { - analysis.exports.push({ - line: i, - content: line, - isDefault: line.includes('default') - }) - } - - // Find potential insertion points - if (this.isInsertionPoint(line, lines, i)) { - analysis.insertionPoints.push({ - line: i, - type: this.getInsertionType(line, lines, i), - context: this.getInsertionContext(lines, i) - }) - } - } - - // Check for Tailwind classes - analysis.hasTailwind = /className\s*=\s*["'][^"']*\b(bg-|text-|p-|m-|flex|grid)/.test(code) - - return analysis - } - - /** - * Insert a new React component into existing code - */ - insertComponent(code, componentType, options = {}) { - const analysis = this.analyzeCodeStructure(code) - const componentGenerator = this.componentTemplates[componentType.toLowerCase()] - - if (!componentGenerator) { - throw new Error(`Unknown component type: ${componentType}`) - } - - const newComponent = componentGenerator.call(this, options, analysis) - const insertionPoint = this.findBestInsertionPoint(analysis, options) - - return this.performInsertion(code, newComponent, insertionPoint, analysis) - } - - /** - * Create a complete React component file - */ - createComponentFile(componentName, componentType, options = {}) { - const componentGenerator = this.componentTemplates[componentType.toLowerCase()] - - if (!componentGenerator) { - throw new Error(`Unknown component type: ${componentType}`) - } - - const mockAnalysis = { - hasReact: true, - hasNextJs: options.framework === 'nextjs', - hasTailwind: options.styling === 'tailwind' - } - - const component = componentGenerator.call( - this, - { - name: componentName, - ...options - }, - mockAnalysis - ) - - const imports = this.generateImports(component.dependencies, mockAnalysis) - - return `${imports}\n\n${component.code}\n\nexport default ${componentName};` - } - - // Component generators - createButtonComponent(options, analysis) { - const {name = 'CustomButton', variant = 'primary', size = 'medium'} = options - const useTailwind = analysis.hasTailwind || options.styling === 'tailwind' - - const baseClasses = useTailwind ? this.getTailwindButtonClasses(variant, size) : 'button' - - return { - code: `const ${name} = ({ children, onClick, disabled = false, className = '', ...props }) => { - return ( - - ); - };`, - dependencies: ['react'] - } - } - - createCardComponent(options, analysis) { - const {name = 'Card', showHeader = true, showFooter = false} = options - const useTailwind = analysis.hasTailwind || options.styling === 'tailwind' - - const cardClasses = useTailwind ? 'bg-white shadow-md rounded-lg overflow-hidden' : 'card' - - return { - code: `const ${name} = ({ title, children, footer, className = '', ...props }) => { - return ( -
- ${ - showHeader - ? `{title && ( -
-

{title}

-
- )}` - : '' - } -
- {children} -
- ${ - showFooter - ? `{footer && ( -
- {footer} -
- )}` - : '' - } -
- ); - };`, - dependencies: ['react'] - } - } - - createProductComponent(options, analysis) { - const {name = 'ProductCard', showPrice = true, showRating = true} = options - const useTailwind = analysis.hasTailwind || options.styling === 'tailwind' - - return { - code: `const ${name} = ({ product, onAddToCart, className = '', ...props }) => { - const { id, name: productName, price, image, rating, description } = product; - - return ( -
- {image && ( -
- {productName} -
- )} -
-

{productName}

- {description && ( -

{description}

- )} -
- ${ - showPrice - ? `\${price}` - : '' - } - ${ - showRating - ? `{rating && ( -
- ā˜… - {rating} -
- )}` - : '' - } -
- {onAddToCart && ( - - )} -
-
- ); - };`, - dependencies: ['react'] - } - } - - createModalComponent(options, analysis) { - const {name = 'Modal', closeOnOverlay = true} = options - const useTailwind = analysis.hasTailwind || options.styling === 'tailwind' - - return { - code: `const ${name} = ({ isOpen, onClose, title, children, className = '', ...props }) => { - useEffect(() => { - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - - return () => { - document.body.style.overflow = 'unset'; - }; - }, [isOpen]); - - if (!isOpen) return null; - - return ( -
-
-
e.stopPropagation()} - {...props} - > - {title && ( -
-

{title}

- -
- )} -
- {children} -
-
-
- ); - };`, - dependencies: ['react'] - } - } - - // Helper methods - isComponentDefinition(line) { - return ( - /^(const|function|class)\s+[A-Z]\w*/.test(line) || - /^export\s+(const|function|class)\s+[A-Z]\w*/.test(line) - ) - } - - extractComponentName(line) { - const match = line.match(/(?:const|function|class)\s+([A-Z]\w*)/) - return match ? match[1] : null - } - - getComponentType(line) { - if (line.includes('class ') && line.includes('extends')) return 'class' - if (line.includes('function ')) return 'function' - if (line.includes('const ') && line.includes('=>')) return 'arrow' - return 'unknown' - } - - getImportType(line) { - if (line.includes("from 'react'")) return 'react' - if (line.includes("from 'next/")) return 'nextjs' - if (line.includes('.css') || line.includes('.scss')) return 'styles' - if (line.startsWith('import ') && line.includes('./')) return 'local' - return 'external' - } - - isInsertionPoint(line, lines, index) { - // After imports - if ( - line.startsWith('import ') && - index + 1 < lines.length && - !lines[index + 1].trim().startsWith('import ') - ) { - return true - } - - // Before export default - if (line.startsWith('export default') && index > 0) return true - - // End of file - if (index === lines.length - 1) return true - - return false - } - - getInsertionType(line, lines, index) { - if (line.startsWith('import ')) return 'after-imports' - if (line.startsWith('export default')) return 'before-export' - if (index === lines.length - 1) return 'end-of-file' - return 'general' - } - - getInsertionContext(lines, index) { - const start = Math.max(0, index - 2) - const end = Math.min(lines.length, index + 3) - return lines.slice(start, end) - } - - findBestInsertionPoint(analysis, options) { - // Prefer after imports - const afterImports = analysis.insertionPoints.find((p) => p.type === 'after-imports') - if (afterImports) return afterImports - - // Fallback to before export - const beforeExport = analysis.insertionPoints.find((p) => p.type === 'before-export') - if (beforeExport) return beforeExport - - // Last resort: end of file - return ( - analysis.insertionPoints.find((p) => p.type === 'end-of-file') || { - line: 0, - type: 'start' - } - ) - } - - performInsertion(code, component, insertionPoint, analysis) { - const lines = code.split('\n') - const insertLine = - insertionPoint.type === 'before-export' ? insertionPoint.line : insertionPoint.line + 1 - - // Add necessary imports - const imports = this.generateImports(component.dependencies, analysis) - const importsToAdd = this.filterNewImports(imports, analysis) - - // Insert component code - lines.splice(insertLine, 0, '', component.code, '') - - // Insert imports at the top - if (importsToAdd) { - const importInsertLine = - analysis.imports.length > 0 - ? analysis.imports[analysis.imports.length - 1].line + 1 - : 0 - lines.splice(importInsertLine, 0, importsToAdd) - } - - return lines.join('\n') - } - - generateImports(dependencies, analysis) { - const imports = [] - - if (dependencies.includes('react') && !analysis.hasReact) { - imports.push("import React, { useEffect } from 'react';") - } else if ( - dependencies.includes('useEffect') && - !analysis.imports.some((imp) => imp.content.includes('useEffect')) - ) { - // Need to update existing React import to include useEffect - imports.push('// Update React import to include useEffect') - } - - return imports.join('\n') - } - - filterNewImports(imports, analysis) { - // Filter out imports that already exist - return imports - } - - getTailwindButtonClasses(variant, size) { - const variants = { - primary: 'bg-blue-600 text-white hover:bg-blue-700', - secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300', - danger: 'bg-red-600 text-white hover:bg-red-700', - success: 'bg-green-600 text-white hover:bg-green-700' - } - - const sizes = { - small: 'px-3 py-1 text-sm', - medium: 'px-4 py-2', - large: 'px-6 py-3 text-lg' - } - - return `${variants[variant] || variants.primary} ${ - sizes[size] || sizes.medium - } rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed` - } -} diff --git a/packages/pwa-storefront-mcp/src/utils/AnalyzeCodeStructureTool.js b/packages/pwa-storefront-mcp/src/utils/AnalyzeCodeStructureTool.js deleted file mode 100644 index bed7c0d6c8..0000000000 --- a/packages/pwa-storefront-mcp/src/utils/AnalyzeCodeStructureTool.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -export class InsertExistingComponentTool { - /** - * Analyze JavaScript/React code and determine insertion points - */ - analyzeCodeStructure(code) { - const analysis = { - imports: [], - components: [], - exports: [], - insertionPoints: [], - hasReact: false, - hasNextJs: false, - hasTailwind: false - } - - const lines = code.split('\n') - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim() - - // Detect imports - if (line.startsWith('import ')) { - analysis.imports.push({ - line: i, - content: line, - type: this.getImportType(line) - }) - - if (line.includes('react')) analysis.hasReact = true - if (line.includes('next/')) analysis.hasNextJs = true - } - - // Detect component definitions - if (this.isComponentDefinition(line)) { - analysis.components.push({ - line: i, - name: this.extractComponentName(line), - type: this.getComponentType(line) - }) - } - - // Detect exports - if (line.startsWith('export ')) { - analysis.exports.push({ - line: i, - content: line, - isDefault: line.includes('default') - }) - } - - // Find potential insertion points - if (this.isInsertionPoint(line, lines, i)) { - analysis.insertionPoints.push({ - line: i, - type: this.getInsertionType(line, lines, i), - context: this.getInsertionContext(lines, i) - }) - } - } - - // Check for Tailwind classes - analysis.hasTailwind = /className\s*=\s*["'][^"']*\b(bg-|text-|p-|m-|flex|grid)/.test(code) - - return analysis - } - - // Helper methods - isComponentDefinition(line) { - return ( - /^(const|function|class)\s+[A-Z]\w*/.test(line) || - /^export\s+(const|function|class)\s+[A-Z]\w*/.test(line) - ) - } - - extractComponentName(line) { - const match = line.match(/(?:const|function|class)\s+([A-Z]\w*)/) - return match ? match[1] : null - } - - getComponentType(line) { - if (line.includes('class ') && line.includes('extends')) return 'class' - if (line.includes('function ')) return 'function' - if (line.includes('const ') && line.includes('=>')) return 'arrow' - return 'unknown' - } - - getImportType(line) { - if (line.includes("from 'react'")) return 'react' - if (line.includes("from 'next/")) return 'nextjs' - if (line.includes('.css') || line.includes('.scss')) return 'styles' - if (line.startsWith('import ') && line.includes('./')) return 'local' - return 'external' - } - - isInsertionPoint(line, lines, index) { - // After imports - if ( - line.startsWith('import ') && - index + 1 < lines.length && - !lines[index + 1].trim().startsWith('import ') - ) { - return true - } - - // Before export default - if (line.startsWith('export default') && index > 0) return true - - // End of file - if (index === lines.length - 1) return true - - return false - } - - getInsertionType(line, lines, index) { - if (line.startsWith('import ')) return 'after-imports' - if (line.startsWith('export default')) return 'before-export' - if (index === lines.length - 1) return 'end-of-file' - return 'general' - } - - getInsertionContext(lines, index) { - const start = Math.max(0, index - 2) - const end = Math.min(lines.length, index + 3) - return lines.slice(start, end) - } - - findBestInsertionPoint(analysis, options) { - // Prefer after imports - const afterImports = analysis.insertionPoints.find((p) => p.type === 'after-imports') - if (afterImports) return afterImports - - // Fallback to before export - const beforeExport = analysis.insertionPoints.find((p) => p.type === 'before-export') - if (beforeExport) return beforeExport - - // Last resort: end of file - return ( - analysis.insertionPoints.find((p) => p.type === 'end-of-file') || { - line: 0, - type: 'start' - } - ) - } -} diff --git a/packages/pwa-storefront-mcp/src/utils/CreateNewComponentTool.js b/packages/pwa-storefront-mcp/src/utils/CreateNewComponentTool.js deleted file mode 100644 index 399ecaa22a..0000000000 --- a/packages/pwa-storefront-mcp/src/utils/CreateNewComponentTool.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import fs from 'fs/promises' -import path from 'path' - -export class CreateNewComponentTool { - /** - * Create a React component file under app/components - * @param {string} componentName - The name of the component - * @param {string} [componentCode] - The code to use for the component (optional) - * @param {string} [componentsDir='retail-react-app/app/components'] - The directory to create the component in - */ - createNewComponent(componentName, code, projectDir = 'template-retail-react-app') { - const componentDir = path.join(projectDir, '/app/components', componentName) - return fs.mkdir(componentDir, {recursive: true}).then(() => { - const filePath = path.join(componentDir, 'index.jsx') - const codeToWrite = code - ? code - : `import React from 'react'; - -const ${componentName} = () => { - return ( -
${componentName} component
- ); -}; - -export default ${componentName}; -` - return fs.writeFile(filePath, codeToWrite, 'utf-8').then(() => `āœ… Created ${filePath}`) - }) - } -} diff --git a/packages/pwa-storefront-mcp/src/utils/InsertExistingComponentTool.js b/packages/pwa-storefront-mcp/src/utils/InsertExistingComponentTool.js deleted file mode 100644 index 44a2c2c735..0000000000 --- a/packages/pwa-storefront-mcp/src/utils/InsertExistingComponentTool.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import fs from 'fs/promises' -import path from 'path' - -export class InsertExistingComponentTool { - /** - * Insert a new React component into existing code - */ - insertComponent(code, componentType, options = {}) { - const analysis = this.analyzeCodeStructure(code) - const componentGenerator = this.componentTemplates[componentType.toLowerCase()] - - if (!componentGenerator) { - throw new Error(`Unknown component type: ${componentType}`) - } - - const newComponent = componentGenerator.call(this, options, analysis) - const insertionPoint = this.findBestInsertionPoint(analysis, options) - - return this.performInsertion(code, newComponent, insertionPoint, analysis) - } - - /** - * Insert an existing component by name into a page by name. - * @param {string} pageName - The name of the page file (without extension) - * @param {string} componentName - The name of the component to insert - * @param {string} [pagesDir='src/pages'] - * @param {string} [componentsDir='src/components'] - */ - async insertComponentIntoPage( - pageName, - componentName, - projectDir = 'template-retail-react-app', - pagesDir = 'app/pages', - componentsDir = 'app/components' - ) { - // Find the page file (support .js and .jsx) - let pageFile = path.join(projectDir, pagesDir, `${pageName}.js`) - componentsDir = path.join(projectDir, componentsDir) - let pageFileAlt = path.join(projectDir, pagesDir, `${pageName}.jsx`) - let pagePath = null - try { - await fs.access(pageFile) - pagePath = pageFile - } catch { - try { - await fs.access(pageFileAlt) - pagePath = pageFileAlt - } catch { - throw new Error(`Page file not found: ${pageFile} or ${pageFileAlt}`) - } - } - console.log('==== pagePath', pagePath) - - // Find the component file (support .js and .jsx) - let compFile = `${componentsDir}/${componentName}/index.js` - let compFileAlt = `${componentsDir}/${componentName}/index.jsx` - - try { - await fs.access(compFile) - } catch { - try { - await fs.access(compFileAlt) - } catch { - throw new Error(`Component file not found: ${compFile} or ${compFileAlt}`) - } - } - - // Read the page file - let code = await fs.readFile(pagePath, 'utf-8') - let lines = code.split('\n') - let importStatement = `import ${componentName} from '../components/${componentName}';` - let hasImport = lines.some( - (line) => - line.includes(importStatement) || - (line.startsWith('import') && - line.includes(componentName) && - line.includes('components')) - ) - - // Insert import if not present (after last import) - if (!hasImport) { - let lastImportIdx = lines - .map((l) => l.trim()) - .reduce((acc, l, i) => (l.startsWith('import') ? i : acc), -1) - lines.splice(lastImportIdx + 1, 0, importStatement) - } - - // Find the main component render output (assume function or arrow function component) - let insertIdx = lines.findIndex((line) => /return \(/.test(line)) - if (insertIdx === -1) { - throw new Error('Could not find a React return statement in the page.') - } - // Insert the component usage after the return ( - let indent = lines[insertIdx].match(/^\s*/)[0] + ' ' - lines.splice(insertIdx + 1, 0, `${indent}<${componentName} />`) - - // Write back the modified file - await fs.writeFile(pagePath, lines.join('\n'), 'utf-8') - return `āœ… Inserted <${componentName} /> into ${pagePath}` - } -} diff --git a/packages/pwa-storefront-mcp/src/utils/InsertionUtils.js b/packages/pwa-storefront-mcp/src/utils/InsertionUtils.js deleted file mode 100644 index 0100cca8af..0000000000 --- a/packages/pwa-storefront-mcp/src/utils/InsertionUtils.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -export class InsertionUtils { - generateImports(dependencies, analysis) { - const imports = [] - - if (dependencies.includes('react') && !analysis.hasReact) { - imports.push("import React, { useEffect } from 'react';") - } else if ( - dependencies.includes('useEffect') && - !analysis.imports.some((imp) => imp.content.includes('useEffect')) - ) { - // Need to update existing React import to include useEffect - imports.push('// Update React import to include useEffect') - } - - return imports.join('\n') - } - - filterNewImports(imports) { - // Filter out imports that already exist - return imports - } -}