Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/b2c-dx-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Discover schema metadata and fetch OpenAPI specs for both standard and custom SC

**Custom API Scaffold (tool: `scapi_customapi_scaffold`):**

Generate a new custom SCAPI endpoint in an existing cartridge (OAS 3.0 schema.yaml, api.json, script.js with example GET endpoints). Requires **apiName** (kebab-case). Optional: **cartridgeName** (omit to use the first cartridge found under the working directory), **apiType** (shopper | admin; default shopper), **apiDescription**, **projectRoot**, **outputDir**. Set `--working-directory` (or SFCC_WORKING_DIRECTORY) so the server discovers cartridges in your project. Files are always generated (no dry run) and existing files are never overwritten.
Generate a new custom SCAPI endpoint in an existing cartridge (OAS 3.0 schema.yaml, api.json, script.js with example GET endpoints). Requires **apiName** (kebab-case). Optional: **cartridgeName** (omit to use the first cartridge found under the working directory), **apiType** (shopper | admin; default shopper), **apiDescription**, **projectRoot**, **outputDir**. Set `--project-directory` (or SFCC_PROJECT_DIRECTORY) so the server discovers cartridges in your project. Files are always generated (no dry run) and existing files are never overwritten.

- ✅ "Use the MCP tool to scaffold a new custom API named my-products."
- ✅ "Use the MCP tool to create a custom admin API called customer-trips."
Expand Down
33 changes: 20 additions & 13 deletions packages/b2c-dx-mcp/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,19 +313,6 @@ export class Services {
return this.b2cInstance.webdav;
}

/**
* Get the project project directory.
* Falls back to process.cwd() if not explicitly set.
*
* This is the directory where the project is located, which may differ from process.cwd()
* when MCP clients spawn servers from a different location (e.g., home directory).
*
* @returns Project project directory path
*/
public getWorkingDirectory(): string {
return this.resolvedConfig.values.projectDirectory ?? process.cwd();
}

/**
* Join path segments.
*
Expand Down Expand Up @@ -371,6 +358,26 @@ export class Services {
return path.resolve(...segments);
}

/**
* Resolve a path relative to the project directory.
* If path is not supplied, returns the project directory.
* If path is absolute, returns it as-is.
* If path is relative, resolves it relative to the project directory.
*
* @param pathArg - Optional path to resolve
* @returns Resolved absolute path
*/
public resolveWithProjectDirectory(pathArg?: string): string {
const projectDir = this.resolvedConfig.values.projectDirectory ?? process.cwd();
if (!pathArg) {
return projectDir;
}
if (path.isAbsolute(pathArg)) {
return pathArg;
}
return path.resolve(projectDir, pathArg);
}

/**
* Get file or directory stats.
*
Expand Down
7 changes: 1 addition & 6 deletions packages/b2c-dx-mcp/src/tools/cartridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* @module tools/cartridges
*/

import path from 'node:path';
import {z} from 'zod';
import type {McpTool} from '../../utils/index.js';
import type {Services} from '../../services.js';
Expand Down Expand Up @@ -129,11 +128,7 @@ function createCartridgeDeployTool(loadServices: () => Services, injections?: Ca
}

// Resolve directory path: relative paths are resolved relative to project directory, absolute paths are used as-is
const directory = args.directory
? path.isAbsolute(args.directory)
? args.directory
: path.resolve(context.services.getWorkingDirectory(), args.directory)
: context.services.getWorkingDirectory();
const directory = context.services.resolveWithProjectDirectory(args.directory);

// Parse options
const options: DeployOptions = {
Expand Down
7 changes: 1 addition & 6 deletions packages/b2c-dx-mcp/src/tools/mrt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* @module tools/mrt
*/

import path from 'node:path';
import {z} from 'zod';
import type {McpTool} from '../../utils/index.js';
import type {Services} from '../../services.js';
Expand Down Expand Up @@ -115,11 +114,7 @@ function createMrtBundlePushTool(loadServices: () => Services, injections?: MrtT
// Parse comma-separated glob patterns (same as CLI defaults)
const ssrOnly = (args.ssrOnly || 'ssr.js,ssr.mjs,server/**/*').split(',').map((s) => s.trim());
const ssrShared = (args.ssrShared || 'static/**/*,client/**/*').split(',').map((s) => s.trim());
const buildDirectory = args.buildDirectory
? path.isAbsolute(args.buildDirectory)
? args.buildDirectory
: path.resolve(context.services.getWorkingDirectory(), args.buildDirectory)
: path.join(context.services.getWorkingDirectory(), 'build');
const buildDirectory = context.services.resolveWithProjectDirectory(args.buildDirectory || 'build');

// Log all computed variables before pushing bundle
const logger = getLogger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* @module tools/scapi/scapi-custom-api-scaffold
*/

import path from 'node:path';
import {z} from 'zod';
import {createToolAdapter, jsonResult, errorResult} from '../adapter.js';
import type {Services} from '../../services.js';
Expand Down Expand Up @@ -48,7 +47,7 @@ interface ScaffoldCustomApiInput {
apiType?: 'admin' | 'shopper';
/** Short description of the API. Default: "A custom B2C Commerce API" */
apiDescription?: string;
/** Project root for cartridge discovery and output. Default: MCP working directory */
/** Project root for cartridge discovery and output. Default: MCP project directory */
projectRoot?: string;
/** Output directory override. Default: scaffold default or project root */
outputDir?: string;
Expand Down Expand Up @@ -79,7 +78,7 @@ export async function executeScaffoldCustomApi(
services: Services,
overrides?: ScaffoldCustomApiExecuteOverrides,
): Promise<ScaffoldCustomApiOutput> {
const projectRoot = path.resolve(args.projectRoot ?? services.getWorkingDirectory());
const projectRoot = services.resolveWithProjectDirectory(args.projectRoot);

const getScaffold =
overrides?.getScaffold ??
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ export function createPageDesignerDecoratorTool(loadServices: () => Services): M
// Use projectDirectory from services to ensure we search in the correct project directory
// This prevents searches in the home folder when MCP clients spawn servers from ~
const services = loadServices();
const workspaceRoot = services.getWorkingDirectory();
const workspaceRoot = services.resolveWithProjectDirectory();

if (validatedArgs.autoMode === undefined && !validatedArgs.conversationContext) {
const fullPath = resolveComponent(validatedArgs.component, workspaceRoot, validatedArgs.searchPaths);
Expand Down
33 changes: 24 additions & 9 deletions packages/b2c-dx-mcp/test/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,35 +172,50 @@ describe('services', () => {
});
});

describe('getWorkingDirectory', () => {
it('should return project directory when provided in config', () => {
describe('resolveWithProjectDirectory', () => {
it('should return project directory when provided in config and no path arg', () => {
const workingDir = '/path/to/project';
const config = createMockResolvedConfig({projectDirectory: workingDir});
const services = new Services({resolvedConfig: config});

expect(services.getWorkingDirectory()).to.equal(workingDir);
expect(services.resolveWithProjectDirectory()).to.equal(workingDir);
});

it('should fall back to process.cwd() when not provided', () => {
it('should fall back to process.cwd() when not provided and no path arg', () => {
const config = createMockResolvedConfig();
const services = new Services({resolvedConfig: config});

expect(services.getWorkingDirectory()).to.equal(process.cwd());
expect(services.resolveWithProjectDirectory()).to.equal(process.cwd());
});

it('should return project directory from fromResolvedConfig when provided in config', () => {
it('should return project directory from fromResolvedConfig when provided in config and no path arg', () => {
const projectDir = '/path/to/project';
const config = createMockResolvedConfig({projectDirectory: projectDir});
const services = Services.fromResolvedConfig(config);

expect(services.getWorkingDirectory()).to.equal(projectDir);
expect(services.resolveWithProjectDirectory()).to.equal(projectDir);
});

it('should fall back to process.cwd() from fromResolvedConfig when not provided in config', () => {
it('should fall back to process.cwd() from fromResolvedConfig when not provided in config and no path arg', () => {
const config = createMockResolvedConfig();
const services = Services.fromResolvedConfig(config);

expect(services.getWorkingDirectory()).to.equal(process.cwd());
expect(services.resolveWithProjectDirectory()).to.equal(process.cwd());
});

it('should return absolute path as-is', () => {
const config = createMockResolvedConfig({projectDirectory: '/path/to/project'});
const services = new Services({resolvedConfig: config});

expect(services.resolveWithProjectDirectory('/absolute/path')).to.equal('/absolute/path');
});

it('should resolve relative path relative to project directory', () => {
const projectDir = '/path/to/project';
const config = createMockResolvedConfig({projectDirectory: projectDir});
const services = new Services({resolvedConfig: config});

expect(services.resolveWithProjectDirectory('subdir')).to.equal('/path/to/project/subdir');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The page-designer-decorator tool has comprehensive unit tests covering:
- ✅ Error handling (invalid input, invalid step name, missing parameters)
- ✅ Input validation
- ✅ Edge cases (no props, only complex props, optional props, union types, already decorated components)
- ✅ Environment variables (SFCC_WORKING_DIRECTORY)
- ✅ Environment variables (SFCC_PROJECT_DIRECTORY)

All tests use the standard Mocha test framework and run with `pnpm test`.

Expand Down Expand Up @@ -57,23 +57,23 @@ npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga

### 4. Running Tests Against a Local Storefront Next Installation

The Mocha test suite supports testing against a real Storefront Next installation by setting `SFCC_WORKING_DIRECTORY`:
The Mocha test suite supports testing against a real Storefront Next installation by setting `SFCC_PROJECT_DIRECTORY`:

```bash
cd packages/b2c-dx-mcp
SFCC_WORKING_DIRECTORY=/path/to/storefront-next \
SFCC_PROJECT_DIRECTORY=/path/to/storefront-next \
pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts
```

Or set it as an environment variable:
```bash
export SFCC_WORKING_DIRECTORY=/path/to/storefront-next
export SFCC_PROJECT_DIRECTORY=/path/to/storefront-next
cd packages/b2c-dx-mcp
pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts
```

**Important Notes for Real Project Mode**:
- Component discovery searches in your real Storefront Next project (`SFCC_WORKING_DIRECTORY`)
- Component discovery searches in your real Storefront Next project (`SFCC_PROJECT_DIRECTORY`)
- Tests create temporary directories for test components (not in your real project)
- Tests will **not** modify your real project files (read-only)
- Tests will use existing components from your real project if they exist
Expand Down Expand Up @@ -104,7 +104,7 @@ export default function TestComponent({title, description}: TestComponentProps)

3. Set environment variable:
```bash
export SFCC_WORKING_DIRECTORY=/path/to/storefront-next
export SFCC_PROJECT_DIRECTORY=/path/to/storefront-next
```

4. Use the tool via MCP Inspector or your IDE's MCP integration
Expand Down Expand Up @@ -144,7 +144,7 @@ Expected: Returns component analysis
### Component Not Found Errors

If you get "Component not found" errors:
1. Verify `SFCC_WORKING_DIRECTORY` is set correctly
1. Verify `SFCC_PROJECT_DIRECTORY` is set correctly
2. Check that the component file exists at the expected path
3. Try using the full relative path: `"component": "src/components/MyComponent.tsx"`

Expand Down
Loading