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
43 changes: 39 additions & 4 deletions packages/b2c-dx-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ npm install -g @salesforce/b2c-dx-mcp

### Workspace Auto-Discovery

When neither `--toolsets` nor `--tools` are provided, the MCP server automatically detects your project type and enables the appropriate toolsets.
The MCP server automatically detects your project type and enables appropriate toolsets when:
1. Neither `--toolsets` nor `--tools` are provided
2. All provided `--toolsets` or `--tools` are invalid (typos, unknown names)

**How it works:**

Expand All @@ -77,7 +79,7 @@ The **SCAPI** toolset is always enabled, providing API discovery and custom API
| Project Type | Detection | Toolsets Enabled |
|--------------|-----------|------------------|
| **PWA Kit v3** | `@salesforce/pwa-kit-*`, `@salesforce/retail-react-app`, or `ccExtensibility` | PWAV3, MRT, SCAPI |
| **Storefront Next** | `@salesforce/storefront-next-*` packages in package.json | STOREFRONTNEXT, MRT, SCAPI |
| **Storefront Next** | `@salesforce/storefront-next-*` packages in package.json | STOREFRONTNEXT, MRT, CARTRIDGES, SCAPI |
| **Cartridges** | Any cartridge with `.project` file (detected via `findCartridges`) | CARTRIDGES, SCAPI |
| **No project detected** | No B2C project markers found | SCAPI (base toolset only) |

Expand Down Expand Up @@ -504,10 +506,43 @@ Tools that interact with B2C Commerce instances (e.g., `cartridge_deploy`, SCAPI

**Option E: dw.json with auto-discovery**

When `--config` is not provided, the MCP server searches upward from `~/` for a `dw.json` file.
When `--config` is not provided, the MCP server searches for `dw.json` starting from the `--working-directory` path (or `SFCC_WORKING_DIRECTORY` env var).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the --working-directory suffice? I'm not sure we need to do a search for dw.json at all here. If the file isn't in the working-directory we won't load it. But we may load some other configuration source that is sufficient for the task. The working-directory is the important part here.

Copy link
Copy Markdown
Contributor Author

@yhsieh1 yhsieh1 Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--working-directory should be suffice but --config is also useful if user has different config setup. I will --config to be consist with CLI and de-emphasize it in the doc so that user will not be confused. I'll have a separated PR for README.md update soon to reflect this. PR #78


> **Note:** Auto-discovery starts from the home directory, so it won't find project-level `dw.json` files. Use `--config` with an explicit path instead.
> **Important:** MCP clients like Cursor and Claude Desktop often spawn servers from the home directory (`~`) rather than the project directory. Always set `--working-directory` for reliable configuration loading and auto-discovery.

**Cursor** (supports `${workspaceFolder}`):
```json
{
"mcpServers": {
"b2c-dx": {
"command": "/path/to/packages/b2c-dx-mcp/bin/dev.js",
"args": [
"--toolsets", "CARTRIDGES",
"--working-directory", "${workspaceFolder}",
"--allow-non-ga-tools"
]
}
}
}
```

**Claude Desktop** (use explicit path):
```json
{
"mcpServers": {
"b2c-dx": {
"command": "/path/to/packages/b2c-dx-mcp/bin/dev.js",
"args": [
"--toolsets", "CARTRIDGES",
"--working-directory", "/path/to/your/project",
"--allow-non-ga-tools"
]
}
}
}
```

**Example dw.json:**
```json
{
"hostname": "your-sandbox.demandware.net",
Expand Down
65 changes: 45 additions & 20 deletions packages/b2c-dx-mcp/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,16 @@
*/

import {Flags} from '@oclif/core';
import {BaseCommand, MrtCommand, InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {
BaseCommand,
MrtCommand,
InstanceCommand,
loadConfig,
extractInstanceFlags,
extractMrtFlags,
} from '@salesforce/b2c-tooling-sdk/cli';
import type {LoadConfigOptions} from '@salesforce/b2c-tooling-sdk/cli';
import type {ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {B2CDxMcpServer} from '../server.js';
import {Services} from '../services.js';
Expand Down Expand Up @@ -202,14 +211,46 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
}),
};

/**
* Loads configuration from flags, environment variables, and config files.
*
* Combines configuration from both InstanceCommand (B2C instance) and MrtCommand (MRT)
* since this command supports both B2C instance tools and MRT tools.
*
* Uses SDK helper functions for flag extraction:
* - extractInstanceFlags() - B2C instance flags (--server, --username, etc.)
* - extractMrtFlags() - MRT flags (--api-key, --project, etc.) and loading options
*
* Priority (highest to lowest):
* 1. CLI flags (--server, --username, --api-key, etc.)
* 2. Environment variables (SFCC_SERVER, SFCC_USERNAME, SFCC_MRT_API_KEY, etc.)
* 3. dw.json file (via --config flag or auto-discovered from --working-directory)
* 4. ~/.mobify file (for MRT API key)
*/
protected override loadConfiguration(): ResolvedB2CConfig {
const mrt = extractMrtFlags(this.flags as Record<string, unknown>);
const options: LoadConfigOptions = {
...this.getBaseConfigOptions(),
...mrt.options,
};

// Combine B2C instance flags and MRT config flags
const flagConfig = {
...extractInstanceFlags(this.flags as Record<string, unknown>),
...mrt.config,
};

return loadConfig(flagConfig, options);
}

/**
* Main entry point - starts the MCP server.
*
* Execution flow:
* 1. BaseCommand.init() parses flags and loads config
* 2. Filter and validate toolsets (invalid ones are skipped with warning)
* 3. Create B2CDxMcpServer instance
* 4. Create Services via Services.create() which resolves B2C instance and MRT config
* 4. Create Services via Services.fromResolvedConfig() using already-resolved config
* 5. Register tools based on --toolsets and --tools flags
* 6. Connect to stdio transport (JSON-RPC over stdin/stdout)
* 7. Log startup message to stderr
Expand Down Expand Up @@ -268,24 +309,8 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
},
);

// Create services with config resolved from flags (which have env var fallbacks via oclif)
const services = Services.create({
b2cInstance: {
configPath: this.flags.config,
hostname: this.flags.server,
codeVersion: this.flags['code-version'],
username: this.flags.username,
password: this.flags.password,
clientId: this.flags['client-id'],
clientSecret: this.flags['client-secret'],
},
mrt: {
apiKey: this.flags['api-key'],
cloudOrigin: this.flags['cloud-origin'],
project: this.flags.project,
environment: this.flags.environment,
},
});
// Create services from already-resolved config (BaseCommand.init() already resolved it)
const services = Services.fromResolvedConfig(this.resolvedConfig);

// Register toolsets
await registerToolsets(startupFlags, server, services);
Expand Down
103 changes: 65 additions & 38 deletions packages/b2c-dx-mcp/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const BASE_TOOLSET: Toolset = 'SCAPI';
const PROJECT_TYPE_TOOLSETS: Record<ProjectType, Toolset[]> = {
cartridges: ['CARTRIDGES'],
'pwa-kit-v3': ['PWAV3', 'MRT'],
'storefront-next': ['STOREFRONTNEXT', 'MRT'],
'storefront-next': ['STOREFRONTNEXT', 'MRT', 'CARTRIDGES'],
};

/**
Expand Down Expand Up @@ -107,14 +107,60 @@ export function createToolRegistry(services: Services): ToolRegistry {
return registry;
}

/**
* Performs workspace auto-discovery and returns appropriate toolsets.
* Always includes BASE_TOOLSET even if no project types are detected.
*
* @param flags - Startup flags containing workingDirectory
* @param reason - Reason for triggering auto-discovery (for logging)
* @returns Array of toolsets to enable
*/
async function performAutoDiscovery(flags: StartupFlags, reason: string): Promise<Toolset[]> {
const logger = getLogger();

// Working directory from --working-directory flag or SFCC_WORKING_DIRECTORY env var
const workingDirectory = flags.workingDirectory ?? process.cwd();

// Warn if working directory wasn't explicitly configured
if (!flags.workingDirectory) {
logger.warn(
{cwd: workingDirectory},
'No --working-directory flag or SFCC_WORKING_DIRECTORY env var provided. ' +
'MCP clients like Cursor and Claude Desktop often spawn servers from ~ instead of the project directory. ' +
'Set --working-directory or SFCC_WORKING_DIRECTORY for reliable auto-discovery.',
);
}

const detectionResult = await detectWorkspaceType(workingDirectory);

// Map all detected project types to MCP toolsets (union)
// Note: getToolsetsForProjectTypes always includes BASE_TOOLSET
const mappedToolsets = getToolsetsForProjectTypes(detectionResult.projectTypes);

logger.info(
{
reason,
projectTypes: detectionResult.projectTypes,
matchedPatterns: detectionResult.matchedPatterns,
enabledToolsets: mappedToolsets,
},
`Auto-discovery (${reason}): project types: ${detectionResult.projectTypes.join(', ') || 'none'}`,
);

return mappedToolsets;
}

/**
* Register tools with the MCP server based on startup flags.
*
* Tool selection logic:
* 1. If neither --toolsets nor --tools are provided, perform auto-discovery
* 1. If no valid tools result from --toolsets and --tools, perform auto-discovery
* 2. Start with all tools from --toolsets (or auto-discovered toolsets)
* 3. Add individual tools from --tools (can be from any toolset)
*
* Auto-discovery always enables at least the BASE_TOOLSET (SCAPI), even if no
* project types are detected in the workspace.
*
* Example:
* --toolsets STOREFRONTNEXT,MRT --tools cartridge_deploy
* This enables STOREFRONTNEXT and MRT toolsets, plus adds cartridge_deploy from CARTRIDGES.
Expand All @@ -124,44 +170,11 @@ export function createToolRegistry(services: Services): ToolRegistry {
* @param services - Services instance
*/
export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, services: Services): Promise<void> {
let toolsets = flags.toolsets ?? [];
const toolsets = flags.toolsets ?? [];
const individualTools = flags.tools ?? [];
const allowNonGaTools = flags.allowNonGaTools ?? false;
const logger = getLogger();

// Auto-discovery: When no --toolsets or --tools flags are provided,
// detect project type and enable appropriate toolsets automatically.
if (toolsets.length === 0 && individualTools.length === 0) {
// Working directory from --working-directory flag or SFCC_WORKING_DIRECTORY env var
const workingDirectory = flags.workingDirectory ?? process.cwd();

// Warn if working directory wasn't explicitly configured
if (!flags.workingDirectory) {
logger.warn(
{cwd: workingDirectory},
'No --working-directory flag or SFCC_WORKING_DIRECTORY env var provided. ' +
'MCP clients like Cursor and Claude Desktop often spawn servers from ~ instead of the project directory. ' +
'Set --working-directory or SFCC_WORKING_DIRECTORY for reliable auto-discovery.',
);
}

const detectionResult = await detectWorkspaceType(workingDirectory);

// Map all detected project types to MCP toolsets (union)
const mappedToolsets = getToolsetsForProjectTypes(detectionResult.projectTypes);

logger.info(
{
projectTypes: detectionResult.projectTypes,
matchedPatterns: detectionResult.matchedPatterns,
enabledToolsets: mappedToolsets,
},
`Auto-discovered project types: ${detectionResult.projectTypes.join(', ') || 'none'}`,
);

toolsets = mappedToolsets;
}

// Create the tool registry (all available tools)
const toolRegistry = createToolRegistry(services);

Expand All @@ -170,8 +183,11 @@ export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServ
const allToolsByName = new Map(allTools.map((tool) => [tool.name, tool]));
const existingToolNames = new Set(allToolsByName.keys());

// Warn about invalid --tools names (but continue with valid ones)
// Determine valid individual tools
const invalidTools = individualTools.filter((name) => !existingToolNames.has(name));
const validIndividualTools = individualTools.filter((name) => existingToolNames.has(name));

// Warn about invalid --tools names (but continue with valid ones)
if (invalidTools.length > 0) {
logger.warn(
{invalidTools, validTools: [...existingToolNames]},
Expand All @@ -194,6 +210,17 @@ export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServ
const validToolsets = toolsets.filter((t): t is Toolset => TOOLSETS.includes(t as Toolset));
const toolsetsToEnable = new Set<Toolset>(toolsets.includes(ALL_TOOLSETS) ? TOOLSETS : validToolsets);

// Auto-discovery: If no valid toolsets AND no valid individual tools, detect workspace type.
// This handles both: (1) no flags provided, and (2) all provided flags are invalid.
// Auto-discovery enables appropriate toolsets based on workspace type,
// or at minimum BASE_TOOLSET if no project types are detected.
if (toolsetsToEnable.size === 0 && validIndividualTools.length === 0) {
const discoveredToolsets = await performAutoDiscovery(flags, 'no valid toolsets or tools');
for (const toolset of discoveredToolsets) {
toolsetsToEnable.add(toolset);
}
}

// Build the set of tools to register:
// 1. Start with tools from enabled toolsets
// 2. Add individual tools from --tools
Expand All @@ -211,7 +238,7 @@ export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServ
}

// Step 2: Add individual tools from --tools (can be from any toolset)
for (const toolName of individualTools) {
for (const toolName of validIndividualTools) {
const tool = allToolsByName.get(toolName);
if (tool && !registeredToolNames.has(toolName)) {
toolsToRegister.push(tool);
Expand Down
Loading
Loading