Skip to content

Commit 20e2512

Browse files
authored
@W-20926594 MCP Server Flag Configuration and SDK Refactoring (#65)
1 parent 6859880 commit 20e2512

14 files changed

Lines changed: 548 additions & 264 deletions

File tree

packages/b2c-dx-mcp/README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ npm install -g @salesforce/b2c-dx-mcp
6060

6161
### Workspace Auto-Discovery
6262

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

6567
**How it works:**
6668

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

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

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

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

509-
> **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.
511+
> **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.
510512
513+
**Cursor** (supports `${workspaceFolder}`):
514+
```json
515+
{
516+
"mcpServers": {
517+
"b2c-dx": {
518+
"command": "/path/to/packages/b2c-dx-mcp/bin/dev.js",
519+
"args": [
520+
"--toolsets", "CARTRIDGES",
521+
"--working-directory", "${workspaceFolder}",
522+
"--allow-non-ga-tools"
523+
]
524+
}
525+
}
526+
}
527+
```
528+
529+
**Claude Desktop** (use explicit path):
530+
```json
531+
{
532+
"mcpServers": {
533+
"b2c-dx": {
534+
"command": "/path/to/packages/b2c-dx-mcp/bin/dev.js",
535+
"args": [
536+
"--toolsets", "CARTRIDGES",
537+
"--working-directory", "/path/to/your/project",
538+
"--allow-non-ga-tools"
539+
]
540+
}
541+
}
542+
}
543+
```
544+
545+
**Example dw.json:**
511546
```json
512547
{
513548
"hostname": "your-sandbox.demandware.net",

packages/b2c-dx-mcp/src/commands/mcp.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,16 @@
128128
*/
129129

130130
import {Flags} from '@oclif/core';
131-
import {BaseCommand, MrtCommand, InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
131+
import {
132+
BaseCommand,
133+
MrtCommand,
134+
InstanceCommand,
135+
loadConfig,
136+
extractInstanceFlags,
137+
extractMrtFlags,
138+
} from '@salesforce/b2c-tooling-sdk/cli';
139+
import type {LoadConfigOptions} from '@salesforce/b2c-tooling-sdk/cli';
140+
import type {ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config';
132141
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
133142
import {B2CDxMcpServer} from '../server.js';
134143
import {Services} from '../services.js';
@@ -202,14 +211,46 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
202211
}),
203212
};
204213

214+
/**
215+
* Loads configuration from flags, environment variables, and config files.
216+
*
217+
* Combines configuration from both InstanceCommand (B2C instance) and MrtCommand (MRT)
218+
* since this command supports both B2C instance tools and MRT tools.
219+
*
220+
* Uses SDK helper functions for flag extraction:
221+
* - extractInstanceFlags() - B2C instance flags (--server, --username, etc.)
222+
* - extractMrtFlags() - MRT flags (--api-key, --project, etc.) and loading options
223+
*
224+
* Priority (highest to lowest):
225+
* 1. CLI flags (--server, --username, --api-key, etc.)
226+
* 2. Environment variables (SFCC_SERVER, SFCC_USERNAME, SFCC_MRT_API_KEY, etc.)
227+
* 3. dw.json file (via --config flag or auto-discovered from --working-directory)
228+
* 4. ~/.mobify file (for MRT API key)
229+
*/
230+
protected override loadConfiguration(): ResolvedB2CConfig {
231+
const mrt = extractMrtFlags(this.flags as Record<string, unknown>);
232+
const options: LoadConfigOptions = {
233+
...this.getBaseConfigOptions(),
234+
...mrt.options,
235+
};
236+
237+
// Combine B2C instance flags and MRT config flags
238+
const flagConfig = {
239+
...extractInstanceFlags(this.flags as Record<string, unknown>),
240+
...mrt.config,
241+
};
242+
243+
return loadConfig(flagConfig, options);
244+
}
245+
205246
/**
206247
* Main entry point - starts the MCP server.
207248
*
208249
* Execution flow:
209250
* 1. BaseCommand.init() parses flags and loads config
210251
* 2. Filter and validate toolsets (invalid ones are skipped with warning)
211252
* 3. Create B2CDxMcpServer instance
212-
* 4. Create Services via Services.create() which resolves B2C instance and MRT config
253+
* 4. Create Services via Services.fromResolvedConfig() using already-resolved config
213254
* 5. Register tools based on --toolsets and --tools flags
214255
* 6. Connect to stdio transport (JSON-RPC over stdin/stdout)
215256
* 7. Log startup message to stderr
@@ -268,24 +309,8 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
268309
},
269310
);
270311

271-
// Create services with config resolved from flags (which have env var fallbacks via oclif)
272-
const services = Services.create({
273-
b2cInstance: {
274-
configPath: this.flags.config,
275-
hostname: this.flags.server,
276-
codeVersion: this.flags['code-version'],
277-
username: this.flags.username,
278-
password: this.flags.password,
279-
clientId: this.flags['client-id'],
280-
clientSecret: this.flags['client-secret'],
281-
},
282-
mrt: {
283-
apiKey: this.flags['api-key'],
284-
cloudOrigin: this.flags['cloud-origin'],
285-
project: this.flags.project,
286-
environment: this.flags.environment,
287-
},
288-
});
312+
// Create services from already-resolved config (BaseCommand.init() already resolved it)
313+
const services = Services.fromResolvedConfig(this.resolvedConfig);
289314

290315
// Register toolsets
291316
await registerToolsets(startupFlags, server, services);

packages/b2c-dx-mcp/src/registry.ts

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const BASE_TOOLSET: Toolset = 'SCAPI';
2929
const PROJECT_TYPE_TOOLSETS: Record<ProjectType, Toolset[]> = {
3030
cartridges: ['CARTRIDGES'],
3131
'pwa-kit-v3': ['PWAV3', 'MRT'],
32-
'storefront-next': ['STOREFRONTNEXT', 'MRT'],
32+
'storefront-next': ['STOREFRONTNEXT', 'MRT', 'CARTRIDGES'],
3333
};
3434

3535
/**
@@ -107,14 +107,60 @@ export function createToolRegistry(services: Services): ToolRegistry {
107107
return registry;
108108
}
109109

110+
/**
111+
* Performs workspace auto-discovery and returns appropriate toolsets.
112+
* Always includes BASE_TOOLSET even if no project types are detected.
113+
*
114+
* @param flags - Startup flags containing workingDirectory
115+
* @param reason - Reason for triggering auto-discovery (for logging)
116+
* @returns Array of toolsets to enable
117+
*/
118+
async function performAutoDiscovery(flags: StartupFlags, reason: string): Promise<Toolset[]> {
119+
const logger = getLogger();
120+
121+
// Working directory from --working-directory flag or SFCC_WORKING_DIRECTORY env var
122+
const workingDirectory = flags.workingDirectory ?? process.cwd();
123+
124+
// Warn if working directory wasn't explicitly configured
125+
if (!flags.workingDirectory) {
126+
logger.warn(
127+
{cwd: workingDirectory},
128+
'No --working-directory flag or SFCC_WORKING_DIRECTORY env var provided. ' +
129+
'MCP clients like Cursor and Claude Desktop often spawn servers from ~ instead of the project directory. ' +
130+
'Set --working-directory or SFCC_WORKING_DIRECTORY for reliable auto-discovery.',
131+
);
132+
}
133+
134+
const detectionResult = await detectWorkspaceType(workingDirectory);
135+
136+
// Map all detected project types to MCP toolsets (union)
137+
// Note: getToolsetsForProjectTypes always includes BASE_TOOLSET
138+
const mappedToolsets = getToolsetsForProjectTypes(detectionResult.projectTypes);
139+
140+
logger.info(
141+
{
142+
reason,
143+
projectTypes: detectionResult.projectTypes,
144+
matchedPatterns: detectionResult.matchedPatterns,
145+
enabledToolsets: mappedToolsets,
146+
},
147+
`Auto-discovery (${reason}): project types: ${detectionResult.projectTypes.join(', ') || 'none'}`,
148+
);
149+
150+
return mappedToolsets;
151+
}
152+
110153
/**
111154
* Register tools with the MCP server based on startup flags.
112155
*
113156
* Tool selection logic:
114-
* 1. If neither --toolsets nor --tools are provided, perform auto-discovery
157+
* 1. If no valid tools result from --toolsets and --tools, perform auto-discovery
115158
* 2. Start with all tools from --toolsets (or auto-discovered toolsets)
116159
* 3. Add individual tools from --tools (can be from any toolset)
117160
*
161+
* Auto-discovery always enables at least the BASE_TOOLSET (SCAPI), even if no
162+
* project types are detected in the workspace.
163+
*
118164
* Example:
119165
* --toolsets STOREFRONTNEXT,MRT --tools cartridge_deploy
120166
* This enables STOREFRONTNEXT and MRT toolsets, plus adds cartridge_deploy from CARTRIDGES.
@@ -124,44 +170,11 @@ export function createToolRegistry(services: Services): ToolRegistry {
124170
* @param services - Services instance
125171
*/
126172
export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, services: Services): Promise<void> {
127-
let toolsets = flags.toolsets ?? [];
173+
const toolsets = flags.toolsets ?? [];
128174
const individualTools = flags.tools ?? [];
129175
const allowNonGaTools = flags.allowNonGaTools ?? false;
130176
const logger = getLogger();
131177

132-
// Auto-discovery: When no --toolsets or --tools flags are provided,
133-
// detect project type and enable appropriate toolsets automatically.
134-
if (toolsets.length === 0 && individualTools.length === 0) {
135-
// Working directory from --working-directory flag or SFCC_WORKING_DIRECTORY env var
136-
const workingDirectory = flags.workingDirectory ?? process.cwd();
137-
138-
// Warn if working directory wasn't explicitly configured
139-
if (!flags.workingDirectory) {
140-
logger.warn(
141-
{cwd: workingDirectory},
142-
'No --working-directory flag or SFCC_WORKING_DIRECTORY env var provided. ' +
143-
'MCP clients like Cursor and Claude Desktop often spawn servers from ~ instead of the project directory. ' +
144-
'Set --working-directory or SFCC_WORKING_DIRECTORY for reliable auto-discovery.',
145-
);
146-
}
147-
148-
const detectionResult = await detectWorkspaceType(workingDirectory);
149-
150-
// Map all detected project types to MCP toolsets (union)
151-
const mappedToolsets = getToolsetsForProjectTypes(detectionResult.projectTypes);
152-
153-
logger.info(
154-
{
155-
projectTypes: detectionResult.projectTypes,
156-
matchedPatterns: detectionResult.matchedPatterns,
157-
enabledToolsets: mappedToolsets,
158-
},
159-
`Auto-discovered project types: ${detectionResult.projectTypes.join(', ') || 'none'}`,
160-
);
161-
162-
toolsets = mappedToolsets;
163-
}
164-
165178
// Create the tool registry (all available tools)
166179
const toolRegistry = createToolRegistry(services);
167180

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

173-
// Warn about invalid --tools names (but continue with valid ones)
186+
// Determine valid individual tools
174187
const invalidTools = individualTools.filter((name) => !existingToolNames.has(name));
188+
const validIndividualTools = individualTools.filter((name) => existingToolNames.has(name));
189+
190+
// Warn about invalid --tools names (but continue with valid ones)
175191
if (invalidTools.length > 0) {
176192
logger.warn(
177193
{invalidTools, validTools: [...existingToolNames]},
@@ -194,6 +210,17 @@ export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServ
194210
const validToolsets = toolsets.filter((t): t is Toolset => TOOLSETS.includes(t as Toolset));
195211
const toolsetsToEnable = new Set<Toolset>(toolsets.includes(ALL_TOOLSETS) ? TOOLSETS : validToolsets);
196212

213+
// Auto-discovery: If no valid toolsets AND no valid individual tools, detect workspace type.
214+
// This handles both: (1) no flags provided, and (2) all provided flags are invalid.
215+
// Auto-discovery enables appropriate toolsets based on workspace type,
216+
// or at minimum BASE_TOOLSET if no project types are detected.
217+
if (toolsetsToEnable.size === 0 && validIndividualTools.length === 0) {
218+
const discoveredToolsets = await performAutoDiscovery(flags, 'no valid toolsets or tools');
219+
for (const toolset of discoveredToolsets) {
220+
toolsetsToEnable.add(toolset);
221+
}
222+
}
223+
197224
// Build the set of tools to register:
198225
// 1. Start with tools from enabled toolsets
199226
// 2. Add individual tools from --tools
@@ -211,7 +238,7 @@ export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServ
211238
}
212239

213240
// Step 2: Add individual tools from --tools (can be from any toolset)
214-
for (const toolName of individualTools) {
241+
for (const toolName of validIndividualTools) {
215242
const tool = allToolsByName.get(toolName);
216243
if (tool && !registeredToolNames.has(toolName)) {
217244
toolsToRegister.push(tool);

0 commit comments

Comments
 (0)