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
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.
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 (
-
-
-
- );
-}
-
-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 (
-
- {children}
-
- );
- };`,
- 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}
- {description && (
-
{description}
- )}
-
- ${
- showPrice
- ? `
\${price} `
- : ''
- }
- ${
- showRating
- ? `{rating && (
-
- ā
- {rating}
-
- )}`
- : ''
- }
-
- {onAddToCart && (
-
onAddToCart(product)}
- className="${
- useTailwind
- ? 'w-full mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors'
- : 'add-to-cart-btn'
- }"
- >
- Add to Cart
-
- )}
-
-
- );
- };`,
- 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
- }
-}