diff --git a/packages/pwa-storefront-mcp/CHANGELOG.md b/packages/pwa-storefront-mcp/CHANGELOG.md new file mode 100644 index 0000000000..1c8f752106 --- /dev/null +++ b/packages/pwa-storefront-mcp/CHANGELOG.md @@ -0,0 +1,2 @@ +## v1.0.0-dev.0 (Jun 26, 2025) +- Add `pwa-kit-create-app` guideline tool [#2663](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2663) diff --git a/packages/pwa-storefront-mcp/package-lock.json b/packages/pwa-storefront-mcp/package-lock.json index 94b49a1be0..3a3391b471 100644 --- a/packages/pwa-storefront-mcp/package-lock.json +++ b/packages/pwa-storefront-mcp/package-lock.json @@ -11,12 +11,12 @@ "dependencies": { "@babel/runtime": "^7.21.0", "@modelcontextprotocol/sdk": "^1.0.0", + "node-pty": "1.0.0", "zod": "^3.25.56" }, "devDependencies": { "@axe-core/playwright": "^4.10.1", "@playwright/test": "^1.49.0", - "@types/node": "^16.0.3", "axe-core": "^4.10.3", "cross-env": "^5.2.1", "nodemon": "^2.0.22", @@ -44,9 +44,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.0.tgz", - "integrity": "sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.2.tgz", + "integrity": "sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==", "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -70,7 +70,7 @@ "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", "dev": true, "dependencies": { - "playwright": "1.49.0" + "playwright": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -79,59 +79,6 @@ "node": ">=18" } }, - "node_modules/@playwright/test/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", - "dev": true, - "dependencies": { - "playwright-core": "1.49.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", - "dev": true, - "dependencies": { - "undici-types": "~7.8.0" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -541,11 +488,11 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", - "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/express": { @@ -590,9 +537,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "engines": { "node": ">= 16" }, @@ -600,7 +547,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, "node_modules/fast-deep-equal": { @@ -941,6 +888,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -955,6 +907,15 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", @@ -1465,12 +1426,6 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1523,9 +1478,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "peerDependencies": { "zod": "^3.24.1" } diff --git a/packages/pwa-storefront-mcp/package.json b/packages/pwa-storefront-mcp/package.json index d2af59f2b8..3978cb8f61 100644 --- a/packages/pwa-storefront-mcp/package.json +++ b/packages/pwa-storefront-mcp/package.json @@ -32,6 +32,7 @@ "dependencies": { "@babel/runtime": "^7.21.0", "@modelcontextprotocol/sdk": "^1.0.0", + "node-pty": "1.0.0", "zod": "^3.25.56" }, "devDependencies": { diff --git a/packages/pwa-storefront-mcp/src/server/server.js b/packages/pwa-storefront-mcp/src/server/server.js index 8e0b28e5c3..500799a3b9 100644 --- a/packages/pwa-storefront-mcp/src/server/server.js +++ b/packages/pwa-storefront-mcp/src/server/server.js @@ -8,8 +8,11 @@ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' import {z} from 'zod' -import {DeveloperGuidelinesTool} from '../utils/pwa-developer-guideline-tool.js' -import {CreateNewComponentTool} from '../utils/create-new-component-tool.js' +import { + CreateAppGuidelinesTool, + CreateNewComponentTool, + DeveloperGuidelinesTool +} from '../utils/index.js' import {HookRecommenderTool} from '../utils/hook-recommender-tool.js' import productDocument from '../data/ProductDocument.json' with { type: 'json' } import categoryDocument from '../data/CategoryDocument.json' with { type: 'json' } @@ -42,6 +45,14 @@ class PwaStorefrontMCPServerHighLevel { } setupTools() { + // Register CreateProjectTool + this.server.tool( + CreateAppGuidelinesTool.name, + CreateAppGuidelinesTool.description, + CreateAppGuidelinesTool.inputSchema, + CreateAppGuidelinesTool.fn + ) + // Register DeveloperGuidelinesTool this.server.tool( DeveloperGuidelinesTool.name, diff --git a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js index 6d75e0a986..7ad0012066 100644 --- a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js +++ b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js @@ -29,7 +29,7 @@ function inferEntityFromComponentName(componentName) { return null } -export class CreateNewComponentTool { +class CreateNewComponentTool { constructor() { this.currentStep = 0 this.componentData = { @@ -498,3 +498,5 @@ export default ${pascalComponentName}; return 'Selected hook/entity not supported for presentational generation.' } } + +export default CreateNewComponentTool diff --git a/packages/pwa-storefront-mcp/src/utils/index.js b/packages/pwa-storefront-mcp/src/utils/index.js new file mode 100644 index 0000000000..bcffca9b22 --- /dev/null +++ b/packages/pwa-storefront-mcp/src/utils/index.js @@ -0,0 +1,11 @@ +/* + * 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 CreateAppGuidelinesTool from './pwa-create-app-guideline-tool.js' +import CreateNewComponentTool from './create-new-component-tool.js' +import DeveloperGuidelinesTool from './pwa-developer-guideline-tool.js' + +export {CreateAppGuidelinesTool, CreateNewComponentTool, DeveloperGuidelinesTool} diff --git a/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js new file mode 100644 index 0000000000..cad608a429 --- /dev/null +++ b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js @@ -0,0 +1,112 @@ +/* + * 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 path from 'path' + +// Project dependencies +import {EmptyJsonSchema, isMonoRepo, runWithPty} from './utils.js' + +// const CREATE_APP_VERSION = 'latest' +const CREATE_APP_VERSION = '3.11.0-nightly-20250630080227' +const CREATE_APP_COMMAND = isMonoRepo() + ? path.resolve( + `${process.env.WORKSPACE_FOLDER_PATHS}/packages/pwa-kit-create-app/scripts/create-mobify-app.js` + ) + : `@salesforce/pwa-kit-create-app@${CREATE_APP_VERSION}` +const DISPLAY_PROGRAM_COMMAND = '--displayProgram' +const NPX_COMMAND = 'npx' + +const guidelinesText = ` +# PWA Kit Create App — Agent Usage Guidelines + +## Overview + +This document defines the behavior agents must follow when using the \`@salesforce/pwa-kit-create-app\` CLI tool to generate new PWA Kit projects. The CLI supports both **presets** and **templates** for project creation, and agents must clearly distinguish between these two modes of operation. + +--- + +## General Rules + +- Always use this tool to initiate project creation. Never attempt to manually create a project outside of this process. +- Ask one question at a time when gathering information from the user. +- Do not mix presets and templates. Only show or ask about one based on the user's intent. +- Never proceed with project generation unless all required information has been collected. + +--- + +## Creating a Project Using a Preset + +If the user requests a project using a **preset**: + +- List only the available presets. +- If a preset is provided, use the \`--preset\` flag with the CLI. +- Do not ask for or display any template options. + +--- + +## Creating a Project Using a Template + +If the user requests a project using a **template**: + +- List only the available templates. +- If a template is provided: + - Use its associated questions to prompt the user, one at a time. + - Do not proceed with project generation until all required answers have been collected. +- Do not ask for or display any preset options. + +--- + +## Important Reminders + +- Never attempt to create a project without using this tool. +- When gathering answers for a template, ask questions one at a time to maintain clarity. +- Presets and templates are mutually exclusive paths. Do not offer both options unless explicitly requested. +- Use the \`${NPX_COMMAND}\` command to run the \`${CREATE_APP_COMMAND}\` CLI tool when creating a new project. +` + +export default { + name: 'create-app_guidelines', + description: `This tool is used to provide the agent with the instructions on how to use the @salesforce/pwa-kit-create-app CLI tool to create a new PWA Kit projects. Do not attempt to create a project without using this tool first.`, + inputSchema: EmptyJsonSchema, + fn: async () => { + let programOutput = '' + + // Run the display program and get the output. + try { + programOutput = await runWithPty(NPX_COMMAND, [ + CREATE_APP_COMMAND, + DISPLAY_PROGRAM_COMMAND + ]) + } catch (err) { + console.error('Failed to run display program:', err) + } + + // Parse the output and get the data, metadata, and schemas. + const { + data, + metadata: {description: cli}, + schemas + } = JSON.parse(programOutput) + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + guidelines: guidelinesText, + cli, + schemas, + data + }, + null, + 2 + ) + } + ] + } + } +} diff --git a/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.js b/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.js index dd08060ead..c54376a9d3 100644 --- a/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.js +++ b/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.js @@ -105,7 +105,7 @@ This document offers guidelines in the development of Salesforce Commerce Compos - Follow security best practices for all code. ` -export const DeveloperGuidelinesTool = { +export default { name: 'development_guidelines', description: `You must follow this development guidelines before attempting to analyze/ generate / refactor / modify / fix code. - e.g. "Create a customer service Chat component", "Find bugs in my_script.jsx", "Refactor my_script.jsx to use React Hooks"`, diff --git a/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.test.js b/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.test.js index d11e79c234..2ea26b3458 100644 --- a/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.test.js +++ b/packages/pwa-storefront-mcp/src/utils/pwa-developer-guideline-tool.test.js @@ -4,7 +4,7 @@ * 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 {DeveloperGuidelinesTool} from './pwa-developer-guideline-tool.js' +import DeveloperGuidelinesTool from './pwa-developer-guideline-tool.js' import {EmptyJsonSchema} from './utils.js' describe('PWA Development Guidelines', () => { diff --git a/packages/pwa-storefront-mcp/src/utils/utils.js b/packages/pwa-storefront-mcp/src/utils/utils.js index cc0cb2f5de..164a2264c0 100644 --- a/packages/pwa-storefront-mcp/src/utils/utils.js +++ b/packages/pwa-storefront-mcp/src/utils/utils.js @@ -4,9 +4,17 @@ * 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' +import path from 'path' +import pty from 'node-pty' +import {fileURLToPath} from 'url' import {zodToJsonSchema} from 'zod-to-json-schema' import {z} from 'zod' +// Emulate __dirname +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + // Private schema used to generate the JSON schema const emptySchema = z.object({}).strict() @@ -27,3 +35,50 @@ export function toKebabCase(str) { */ export const toPascalCase = (str) => str.replace(/(^\w|[-_\s]\w)/g, (match) => match.replace(/[-_\s]/, '').toUpperCase()) + +/** + * Spawns a pseudo-terminal process and captures its output. + * + * This is useful for running CLI tools that emit different output + * when run in a TTY environment (e.g., with color, formatting, or paging). + * + * @param {string} command - The command to execute (e.g., 'npx'). + * @param {string[]} [args=[]] - An array of arguments to pass to the command. + * @returns {Promise} Resolves with the full output from the process if it exits with code 0, + * otherwise rejects with an Error containing the exit code. + * + * @example + * const output = await runWithPty('npx', ['some-cli', '--help']) + */ +export const runWithPty = (command, args = []) => { + return new Promise((resolve, reject) => { + const ptyProcess = pty.spawn(command, args, { + name: 'xterm-color', + env: process.env + }) + + let output = '' + + ptyProcess.onData((data) => { + output += data + }) + + ptyProcess.onExit(({exitCode}) => { + if (exitCode === 0) { + resolve(output) + } else { + reject(new Error(`PTY exited with code ${exitCode}`)) + } + }) + }) +} + +/** + * Checks if the project is a monorepo by verifying the existence of lerna.json in the root directory. + * + * @returns {boolean} True if lerna.json exists in the '../../../..' folder, false otherwise. + */ +export function isMonoRepo() { + const lernaPath = path.resolve(__dirname, '../../../..', 'lerna.json') + return fs.existsSync(lernaPath) +} diff --git a/packages/pwa-storefront-mcp/src/utils/utils.test.js b/packages/pwa-storefront-mcp/src/utils/utils.test.js index 61eb2f5aa7..22d28412b5 100644 --- a/packages/pwa-storefront-mcp/src/utils/utils.test.js +++ b/packages/pwa-storefront-mcp/src/utils/utils.test.js @@ -4,7 +4,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 {EmptyJsonSchema} from './utils' +import {EmptyJsonSchema, isMonoRepo} from './utils' +import fs from 'fs' describe('Utils', () => { describe('EmptyJsonSchema', () => { @@ -28,4 +29,29 @@ describe('Utils', () => { expect(EmptyJsonSchema.properties).toEqual({}) }) }) + + // TODO: The test run failed due to a SyntaxError: Cannot use 'import.meta' outside a module. This error occurs because the utils.js file uses ESM syntax (import.meta.url), but Jest (by default) runs in CommonJS mode and does not support ESM features out of the box. + describe.skip('isMonoRepo', () => { + /* + * DO NOT REMOVE THIS COMMENT! This test was generated by Cursor + * This test was generated with the following model: GPT-4.1 + */ + test('returns true if lerna.json exists', () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true) + expect(isMonoRepo()).toBe(true) + }) + + /* + * DO NOT REMOVE THIS COMMENT! This test was generated by Cursor + * This test was generated with the following model: GPT-4.1 + */ + test('returns false if lerna.json does not exist', () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false) + expect(isMonoRepo()).toBe(false) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + }) })