Skip to content

Commit 0c0dc6b

Browse files
@W-21199544 MCP respect working-directory flag in cartidge and mrt tools (#160)
* @W-21199544 MCP respect working-directory flag in cartidge and mrt tools * @W-21199544 MCP reload config before every tool call * @W-21199544 aggressively rename startDir as workingDirectory
1 parent a09e257 commit 0c0dc6b

File tree

31 files changed

+499
-360
lines changed

31 files changed

+499
-360
lines changed

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,20 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
277277
return loadConfig(flagConfig, options);
278278
}
279279

280+
/**
281+
* Loads configuration and creates a new Services instance.
282+
*
283+
* This method loads configuration files (dw.json, ~/.mobify) on each call,
284+
* allowing tools to pick up changes to configuration between invocations.
285+
* Flags remain the same (parsed once at startup).
286+
*
287+
* @returns A new Services instance with loaded configuration
288+
*/
289+
protected loadServices(): Services {
290+
const config = this.loadConfiguration();
291+
return Services.fromResolvedConfig(config);
292+
}
293+
280294
/**
281295
* Main entry point - starts the MCP server.
282296
*
@@ -336,11 +350,9 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
336350
},
337351
);
338352

339-
// Create services from already-resolved config (BaseCommand.init() already resolved it)
340-
const services = Services.fromResolvedConfig(this.resolvedConfig);
341-
342-
// Register toolsets
343-
await registerToolsets(startupFlags, server, services);
353+
// Register toolsets with loader function that loads config and creates Services on each tool call
354+
// This allows tools to pick up changes to config files (dw.json, ~/.mobify) between invocations
355+
await registerToolsets(startupFlags, server, this.loadServices.bind(this));
344356

345357
// Connect to stdio transport
346358
const transport = new StdioServerTransport();

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ export type ToolRegistry = Record<Toolset, McpTool[]>;
7676
* Tools are organized by their declared `toolsets` array, allowing
7777
* a single tool to appear in multiple toolsets.
7878
*
79-
* @param services - Services instance for dependency injection
79+
* @param loadServices - Function that loads configuration and returns Services instance
8080
* @returns Complete tool registry
8181
*/
82-
export function createToolRegistry(services: Services): ToolRegistry {
82+
export function createToolRegistry(loadServices: () => Services): ToolRegistry {
8383
const registry: ToolRegistry = {
8484
CARTRIDGES: [],
8585
MRT: [],
@@ -90,11 +90,11 @@ export function createToolRegistry(services: Services): ToolRegistry {
9090

9191
// Collect all tools from all factories
9292
const allTools: McpTool[] = [
93-
...createCartridgesTools(services),
94-
...createMrtTools(services),
95-
...createPwav3Tools(services),
96-
...createScapiTools(services),
97-
...createStorefrontNextTools(services),
93+
...createCartridgesTools(loadServices),
94+
...createMrtTools(loadServices),
95+
...createPwav3Tools(loadServices),
96+
...createScapiTools(loadServices),
97+
...createStorefrontNextTools(loadServices),
9898
];
9999

100100
// Organize tools by their declared toolsets (supports multi-toolset)
@@ -167,16 +167,20 @@ async function performAutoDiscovery(flags: StartupFlags, reason: string): Promis
167167
*
168168
* @param flags - Startup flags from CLI
169169
* @param server - B2CDxMcpServer instance
170-
* @param services - Services instance
170+
* @param loadServices - Function that loads configuration and returns Services instance
171171
*/
172-
export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, services: Services): Promise<void> {
172+
export async function registerToolsets(
173+
flags: StartupFlags,
174+
server: B2CDxMcpServer,
175+
loadServices: () => Services,
176+
): Promise<void> {
173177
const toolsets = flags.toolsets ?? [];
174178
const individualTools = flags.tools ?? [];
175179
const allowNonGaTools = flags.allowNonGaTools ?? false;
176180
const logger = getLogger();
177181

178182
// Create the tool registry (all available tools)
179-
const toolRegistry = createToolRegistry(services);
183+
const toolRegistry = createToolRegistry(loadServices);
180184

181185
// Build flat list of all tools for lookup
182186
const allTools = Object.values(toolRegistry).flat();

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,19 @@ export class Services {
313313
return this.b2cInstance.webdav;
314314
}
315315

316+
/**
317+
* Get the project working directory.
318+
* Falls back to process.cwd() if not explicitly set.
319+
*
320+
* This is the directory where the project is located, which may differ from process.cwd()
321+
* when MCP clients spawn servers from a different location (e.g., home directory).
322+
*
323+
* @returns Project working directory path
324+
*/
325+
public getWorkingDirectory(): string {
326+
return this.resolvedConfig.values.workingDirectory ?? process.cwd();
327+
}
328+
316329
/**
317330
* Join path segments.
318331
*

packages/b2c-dx-mcp/src/tools/adapter.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@
99
*
1010
* This module provides utilities for creating standardized MCP tools that:
1111
* - Validate input using Zod schemas
12-
* - Inject pre-resolved B2CInstance for WebDAV/OCAPI operations (requiresInstance)
13-
* - Inject pre-resolved MRT auth for MRT API operations (requiresMrtAuth)
12+
* - Inject loaded B2CInstance for WebDAV/OCAPI operations (requiresInstance)
13+
* - Inject loaded MRT auth for MRT API operations (requiresMrtAuth)
1414
* - Format output consistently (textResult, jsonResult, errorResult)
1515
*
1616
* ## Configuration Resolution
1717
*
18-
* Both B2C instance and MRT auth are resolved once at server startup via
19-
* {@link Services.fromResolvedConfig} and reused for all tool calls:
18+
* Both B2C instance and MRT auth are loaded before each tool call via
19+
* a loader function that calls {@link Services.fromResolvedConfig}:
2020
*
21-
* - **B2CInstance**: Resolved from flags + dw.json. Available when `requiresInstance: true`.
22-
* - **MRT Auth**: Resolved from --api-key → SFCC_MRT_API_KEY → ~/.mobify. Available when `requiresMrtAuth: true`.
21+
* - **B2CInstance**: Loaded from flags + dw.json on each call. Available when `requiresInstance: true`.
22+
* - **MRT Auth**: Loaded from --api-key → SFCC_MRT_API_KEY → ~/.mobify on each call. Available when `requiresMrtAuth: true`.
2323
*
24-
* This "resolve eagerly at startup" pattern provides:
25-
* - Fail-fast behavior (configuration errors surface at startup)
26-
* - Consistent mental model (both resolved the same way)
27-
* - Better performance (no resolution on each tool call)
24+
* This "load on each call" pattern provides:
25+
* - Fresh configuration on each tool invocation (picks up changes to config files)
26+
* - Consistent mental model (both loaded the same way)
27+
* - Tools can respond to configuration changes without server restart
2828
*
2929
* @module tools/adapter
3030
*
@@ -48,8 +48,11 @@
4848
*
4949
* @example MRT tool (MRT API)
5050
* ```typescript
51-
* // Services created from already-resolved config at startup
52-
* const services = Services.fromResolvedConfig(this.resolvedConfig);
51+
* // Loader function that loads config and creates Services on each tool call
52+
* const loadServices = () => {
53+
* const config = this.loadConfiguration();
54+
* return Services.fromResolvedConfig(config);
55+
* };
5356
*
5457
* const mrtTool = createToolAdapter({
5558
* name: 'mrt_bundle_push',
@@ -59,12 +62,12 @@
5962
* inputSchema: {
6063
* projectSlug: z.string().describe('MRT project slug'),
6164
* },
62-
* execute: async (args, { mrtAuth }) => {
63-
* const result = await pushBundle({ projectSlug: args.projectSlug }, mrtAuth);
65+
* execute: async (args, { mrtConfig }) => {
66+
* const result = await pushBundle({ projectSlug: args.projectSlug }, mrtConfig.auth);
6467
* return result;
6568
* },
6669
* formatOutput: (output) => jsonResult(output),
67-
* }, services);
70+
* }, loadServices);
6871
* ```
6972
*/
7073

@@ -87,7 +90,7 @@ export interface ToolExecutionContext {
8790

8891
/**
8992
* MRT configuration (auth, project, environment, origin).
90-
* Pre-resolved at server startup.
93+
* Loaded before each tool call.
9194
* Only populated when requiresMrtAuth is true.
9295
*/
9396
mrtConfig?: MrtConfig;
@@ -222,31 +225,36 @@ function formatZodErrors(error: z.ZodError): string {
222225
* @template TInput - The validated input type (inferred from inputSchema)
223226
* @template TOutput - The output type from the execute function
224227
* @param options - Tool adapter configuration
225-
* @param services - Services instance for dependency injection
228+
* @param loadServices - Function that loads configuration and returns Services instance
226229
* @returns An McpTool ready for registration
227230
*
228231
* @example
229232
* ```typescript
230233
* import { z } from 'zod';
231234
* import { createToolAdapter, jsonResult, errorResult } from './adapter.js';
232235
*
236+
* const loadServices = () => {
237+
* const config = this.loadConfiguration();
238+
* return Services.fromResolvedConfig(config);
239+
* };
240+
*
233241
* const listCodeVersionsTool = createToolAdapter({
234242
* name: 'code_version_list',
235243
* description: 'List all code versions on the instance',
236244
* toolsets: ['CARTRIDGES'],
237245
* inputSchema: {},
238-
* execute: async (_args, { instance }) => {
239-
* const result = await instance.ocapi.GET('/code_versions', {});
246+
* execute: async (_args, { b2cInstance }) => {
247+
* const result = await b2cInstance.ocapi.GET('/code_versions', {});
240248
* if (result.error) throw new Error(result.error.message);
241249
* return result.data;
242250
* },
243251
* formatOutput: (data) => jsonResult(data),
244-
* }, services);
252+
* }, loadServices);
245253
* ```
246254
*/
247255
export function createToolAdapter<TInput, TOutput>(
248256
options: ToolAdapterOptions<TInput, TOutput>,
249-
services: Services,
257+
loadServices: () => Services,
250258
): McpTool {
251259
const {
252260
name,
@@ -279,7 +287,10 @@ export function createToolAdapter<TInput, TOutput>(
279287
const args = parseResult.data as TInput;
280288

281289
try {
282-
// 2. Get B2CInstance if required (pre-resolved at startup)
290+
// 2. Load Services to get fresh configuration (re-reads config files)
291+
const services = loadServices();
292+
293+
// 3. Get B2CInstance if required (loaded on each call)
283294
let b2cInstance: B2CInstance | undefined;
284295
if (requiresInstance) {
285296
if (!services.b2cInstance) {
@@ -290,7 +301,7 @@ export function createToolAdapter<TInput, TOutput>(
290301
b2cInstance = services.b2cInstance;
291302
}
292303

293-
// 3. Get MRT config if required (pre-resolved at startup)
304+
// 4. Get MRT config if required (loaded on each call)
294305
let mrtConfig: ToolExecutionContext['mrtConfig'];
295306
if (requiresMrtAuth) {
296307
if (!services.mrtConfig.auth) {
@@ -306,15 +317,15 @@ export function createToolAdapter<TInput, TOutput>(
306317
};
307318
}
308319

309-
// 4. Execute the operation
320+
// 5. Execute the operation
310321
const context: ToolExecutionContext = {
311322
b2cInstance,
312323
mrtConfig,
313324
services,
314325
};
315326
const output = await execute(args, context);
316327

317-
// 5. Format output
328+
// 6. Format output
318329
return formatOutput(output);
319330
} catch (error) {
320331
// Handle execution errors

packages/b2c-dx-mcp/src/tools/cartridges/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ interface CartridgeToolInjections {
5252
* 3. Uploads the zip to WebDAV and triggers server-side unzip
5353
* 4. Optionally reloads the code version after deploy
5454
*
55-
* @param services - MCP services
55+
* @param loadServices - Function that loads configuration and returns Services instance
5656
* @param injections - Optional dependency injections for testing
5757
* @returns The cartridge_deploy tool
5858
*/
59-
function createCartridgeDeployTool(services: Services, injections?: CartridgeToolInjections): McpTool {
59+
function createCartridgeDeployTool(loadServices: () => Services, injections?: CartridgeToolInjections): McpTool {
6060
const findAndDeployCartridgesFn = injections?.findAndDeployCartridges || findAndDeployCartridges;
6161
return createToolAdapter<CartridgeDeployInput, DeployResult>(
6262
{
@@ -106,7 +106,7 @@ function createCartridgeDeployTool(services: Services, injections?: CartridgeToo
106106
const instance = context.b2cInstance!;
107107

108108
// Default directory to current directory
109-
const directory = args.directory || '.';
109+
const directory = args.directory || context.services.getWorkingDirectory();
110110

111111
// Parse options
112112
const options: DeployOptions = {
@@ -134,17 +134,17 @@ function createCartridgeDeployTool(services: Services, injections?: CartridgeToo
134134
},
135135
formatOutput: (output) => jsonResult(output),
136136
},
137-
services,
137+
loadServices,
138138
);
139139
}
140140

141141
/**
142142
* Creates all tools for the CARTRIDGES toolset.
143143
*
144-
* @param services - MCP services
144+
* @param loadServices - Function that loads configuration and returns Services instance
145145
* @param injections - Optional dependency injections for testing
146146
* @returns Array of MCP tools
147147
*/
148-
export function createCartridgesTools(services: Services, injections?: CartridgeToolInjections): McpTool[] {
149-
return [createCartridgeDeployTool(services, injections)];
148+
export function createCartridgesTools(loadServices: () => Services, injections?: CartridgeToolInjections): McpTool[] {
149+
return [createCartridgeDeployTool(loadServices, injections)];
150150
}

packages/b2c-dx-mcp/src/tools/mrt/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @module tools/mrt
1313
*/
1414

15+
import path from 'node:path';
1516
import {z} from 'zod';
1617
import type {McpTool} from '../../utils/index.js';
1718
import type {Services} from '../../services.js';
@@ -51,11 +52,11 @@ interface MrtToolInjections {
5152
* Expects the project to already be built (e.g., `npm run build` completed).
5253
* Shared across MRT, PWAV3, and STOREFRONTNEXT toolsets.
5354
*
54-
* @param services - MCP services
55+
* @param loadServices - Function that loads configuration and returns Services instance
5556
* @param injections - Optional dependency injections for testing
5657
* @returns The mrt_bundle_push tool
5758
*/
58-
function createMrtBundlePushTool(services: Services, injections?: MrtToolInjections): McpTool {
59+
function createMrtBundlePushTool(loadServices: () => Services, injections?: MrtToolInjections): McpTool {
5960
const pushBundleFn = injections?.pushBundle || pushBundle;
6061
return createToolAdapter<MrtBundlePushInput, PushResult>(
6162
{
@@ -96,7 +97,7 @@ function createMrtBundlePushTool(services: Services, injections?: MrtToolInjecti
9697
// Parse comma-separated glob patterns (same as CLI defaults)
9798
const ssrOnly = (args.ssrOnly || 'ssr.js,ssr.mjs,server/**/*').split(',').map((s) => s.trim());
9899
const ssrShared = (args.ssrShared || 'static/**/*,client/**/*').split(',').map((s) => s.trim());
99-
const buildDirectory = args.buildDirectory || './build';
100+
const buildDirectory = args.buildDirectory || path.join(context.services.getWorkingDirectory(), 'build');
100101

101102
// Log all computed variables before pushing bundle
102103
const logger = getLogger();
@@ -132,17 +133,17 @@ function createMrtBundlePushTool(services: Services, injections?: MrtToolInjecti
132133
},
133134
formatOutput: (output) => jsonResult(output),
134135
},
135-
services,
136+
loadServices,
136137
);
137138
}
138139

139140
/**
140141
* Creates all tools for the MRT toolset.
141142
*
142-
* @param services - MCP services
143+
* @param loadServices - Function that loads configuration and returns Services instance
143144
* @param injections - Optional dependency injections for testing
144145
* @returns Array of MCP tools
145146
*/
146-
export function createMrtTools(services: Services, injections?: MrtToolInjections): McpTool[] {
147-
return [createMrtBundlePushTool(services, injections)];
147+
export function createMrtTools(loadServices: () => Services, injections?: MrtToolInjections): McpTool[] {
148+
return [createMrtBundlePushTool(loadServices, injections)];
148149
}

0 commit comments

Comments
 (0)