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
21 changes: 20 additions & 1 deletion packages/b2c-dx-mcp/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import headerPlugin from 'eslint-plugin-header';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

import {copyrightHeader, sharedRules, oclifRules, prettierPlugin} from '../../eslint.config.mjs';
import {copyrightHeader, sharedRules, oclifRules, chaiTestRules, prettierPlugin} from '../../eslint.config.mjs';

const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore');
headerPlugin.rules.header.meta.schema = false;
Expand All @@ -28,4 +28,23 @@ export default [
...oclifRules,
},
},
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for adding it.

files: ['test/**/*.ts'],
rules: {
...chaiTestRules,
// Tests use stubbing patterns that intentionally return undefined
'unicorn/no-useless-undefined': 'off',
// Some tests use void 0 to satisfy TS stub typings; allow it in tests
'no-void': 'off',
// Helper functions in tests are commonly declared within suites for clarity
'unicorn/consistent-function-scoping': 'off',
// Sinon default import is intentional and idiomatic in tests
'import/no-named-as-default-member': 'off',
// import/namespace behaves inconsistently across environments when parsing CJS modules
'import/namespace': 'off',
// Disable for tests: ESLint import resolver doesn't understand conditional exports (development condition)
// but Node.js resolves them correctly at runtime
'import/no-unresolved': 'off',
},
},
];
12 changes: 10 additions & 2 deletions packages/b2c-dx-mcp/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
* 1. `--api-key` flag (oclif also checks `SFCC_MRT_API_KEY` env var)
* 2. `~/.mobify` config file (or `~/.mobify--[hostname]` if `--cloud-origin` is set)
*
* **MRT Origin** (for Managed Runtime API URL):
* 1. `--cloud-origin` flag (oclif also checks `SFCC_MRT_CLOUD_ORIGIN` env var)
* 2. `mrtOrigin` field in dw.json
* 3. Default: `https://cloud.mobify.com`
*
* @module services
*/

Expand All @@ -47,7 +52,7 @@ import type {ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config';

/**
* MRT (Managed Runtime) configuration.
* Groups auth, project, and environment settings.
* Groups auth, project, environment, and origin settings.
*/
export interface MrtConfig {
/** Pre-resolved auth strategy for MRT API operations */
Expand All @@ -56,6 +61,8 @@ export interface MrtConfig {
project?: string;
/** MRT environment from --environment flag or SFCC_MRT_ENVIRONMENT env var */
environment?: string;
/** MRT API origin URL from --cloud-origin flag, SFCC_MRT_CLOUD_ORIGIN env var, or mrtOrigin in dw.json */
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the correct sequence to get origin? do we still have issue read from dw.json?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The actual sequence is apparently this, but I'm not sure we need his level of detail?

 /** MRT API origin URL from --cloud-origin flag (oclif also checks SFCC_MRT_CLOUD_ORIGIN env var), mrtOrigin in dw.json, or default */

Copy link
Collaborator

@clavery clavery Feb 6, 2026

Choose a reason for hiding this comment

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

The MCP shouldn't be concerned with reading from dw.json or ~/.mobify ever. It is only concerned with flags and env vars (which are tied to the flags anyway in oclif so just flags). Those are passed into the config resolution logic if we have it and you get origin: this.resolvedConfig.values.mrtOrigin. But this should generally be concerned with the SDK operations not the MCP.

We do have an annoying issue were it's called mrtOrigin and cloudOrigin interchangably. But since this isn't a customer need it's not a huge deal

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also the MRT operations in the SDK are inconsistent in signature patterns (some take a client some create it internally). We identified this in another PR. I'll probably do a refactoring pass on those at some point.

origin?: string;
}

/**
Expand Down Expand Up @@ -94,7 +101,7 @@ export class Services {
public readonly b2cInstance?: B2CInstance;

/**
* Pre-resolved MRT configuration (auth, project, environment).
* Pre-resolved MRT configuration (auth, project, environment, origin).
* Resolved once at server startup from MrtCommand flags and ~/.mobify.
*/
public readonly mrtConfig: MrtConfig;
Expand Down Expand Up @@ -122,6 +129,7 @@ export class Services {
auth: config.hasMrtConfig() ? config.createMrtAuth() : undefined,
project: config.values.mrtProject,
environment: config.values.mrtEnvironment,
origin: config.values.mrtOrigin,
};

// Build B2C instance using factory method
Expand Down
15 changes: 4 additions & 11 deletions packages/b2c-dx-mcp/src/tools/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@

import {z, type ZodRawShape, type ZodObject, type ZodType} from 'zod';
import type {B2CInstance} from '@salesforce/b2c-tooling-sdk';
import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
import type {McpTool, ToolResult, Toolset} from '../utils/index.js';
import type {Services} from '../services.js';
import type {Services, MrtConfig} from '../services.js';

/**
* Context provided to tool execute functions.
Expand All @@ -87,18 +86,11 @@ export interface ToolExecutionContext {
b2cInstance?: B2CInstance;

/**
* MRT configuration (auth, project, environment).
* MRT configuration (auth, project, environment, origin).
* Pre-resolved at server startup.
* Only populated when requiresMrtAuth is true.
*/
mrtConfig?: {
/** Auth strategy for MRT API operations */
auth: AuthStrategy;
/** MRT project slug */
project?: string;
/** MRT environment */
environment?: string;
};
mrtConfig?: MrtConfig;

/**
* Services instance for file system access and other utilities.
Expand Down Expand Up @@ -310,6 +302,7 @@ export function createToolAdapter<TInput, TOutput>(
auth: services.mrtConfig.auth,
project: services.mrtConfig.project,
environment: services.mrtConfig.environment,
origin: services.mrtConfig.origin,
};
}

Expand Down
95 changes: 50 additions & 45 deletions packages/b2c-dx-mcp/src/tools/mrt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@
*
* This toolset provides MCP tools for Managed Runtime operations.
*
* > ⚠️ **PLACEHOLDER - ACTIVE DEVELOPMENT**
* > This tool is a placeholder implementation that returns mock responses.
* > Actual implementation is coming soon. Use `--allow-non-ga-tools` flag to enable.
*
* @module tools/mrt
*/

import {z} from 'zod';
import type {McpTool} from '../../utils/index.js';
import type {Services} from '../../services.js';
import {createToolAdapter, jsonResult} from '../adapter.js';
import {pushBundle} from '@salesforce/b2c-tooling-sdk/operations/mrt';
import type {PushResult, PushOptions} from '@salesforce/b2c-tooling-sdk/operations/mrt';
import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
import {getLogger} from '@salesforce/b2c-tooling-sdk/logging';

/**
Expand All @@ -37,14 +36,11 @@ interface MrtBundlePushInput {
}

/**
* Output type for mrt_bundle_push tool.
* Optional dependency injections for testing.
*/
interface MrtBundlePushOutput {
tool: string;
status: string;
message: string;
input: MrtBundlePushInput;
timestamp: string;
interface MrtToolInjections {
/** Mock pushBundle function for testing */
pushBundle?: (options: PushOptions, auth: AuthStrategy) => Promise<PushResult>;
}

/**
Expand All @@ -56,14 +52,16 @@ interface MrtBundlePushOutput {
* Shared across MRT, PWAV3, and STOREFRONTNEXT toolsets.
*
* @param services - MCP services
* @param injections - Optional dependency injections for testing
* @returns The mrt_bundle_push tool
*/
function createMrtBundlePushTool(services: Services): McpTool {
return createToolAdapter<MrtBundlePushInput, MrtBundlePushOutput>(
function createMrtBundlePushTool(services: Services, injections?: MrtToolInjections): McpTool {
const pushBundleFn = injections?.pushBundle || pushBundle;
return createToolAdapter<MrtBundlePushInput, PushResult>(
{
name: 'mrt_bundle_push',
description:
'[PLACEHOLDER] Bundle a pre-built PWA Kit project and push to Managed Runtime. Optionally deploy to a target environment.',
'Bundle a pre-built PWA Kit project and push to Managed Runtime. Optionally deploy to a target environment.',
toolsets: ['MRT', 'PWAV3', 'STOREFRONTNEXT'],
isGA: false,
// MRT operations use ApiKeyStrategy from SFCC_MRT_API_KEY or ~/.mobify
Expand Down Expand Up @@ -92,39 +90,45 @@ function createMrtBundlePushTool(services: Services): McpTool {
// Get environment from --environment flag (optional)
const environment = context.mrtConfig?.environment;

// Placeholder implementation
const timestamp = new Date().toISOString();
// Get origin from --cloud-origin flag or mrtOrigin config (optional)
const origin = context.mrtConfig?.origin;

// 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 || './build';

// TODO: Remove this log when implementing
// Log all computed variables before pushing bundle
const logger = getLogger();
logger.debug({mrtConfig: context.mrtConfig, project, environment}, 'mrt_bundle_push context');
logger.debug(
{
project,
environment,
origin,
buildDirectory,
message: args.message,
ssrOnly,
ssrShared,
},
'[MRT] Pushing bundle with computed options',
);

// TODO: When implementing, use context.mrtConfig.auth:
//
// import { pushBundle } from '@salesforce/b2c-tooling-sdk/operations/mrt';
//
// // 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 result = await pushBundle({
// project,
// buildDirectory: args.buildDirectory || './build',
// ssrOnly, // files that run only on SSR server (never sent to browser)
// ssrShared, // files served from CDN and also available to SSR
// message: args.message,
// environment,
// }, context.mrtConfig!.auth);
// return result;
// Push bundle to MRT
// Note: auth is guaranteed to be present by the adapter when requiresMrtAuth is true
const result = await pushBundleFn(
{
projectSlug: project,
buildDirectory,
ssrOnly, // files that run only on SSR server (never sent to browser)
ssrShared, // files served from CDN and also available to SSR
message: args.message,
target: environment,
origin, // MRT API origin URL (optional, defaults to https://cloud.mobify.com)
},
context.mrtConfig!.auth!,
);

return {
tool: 'mrt_bundle_push',
status: 'placeholder',
message:
"This is a placeholder implementation for 'mrt_bundle_push'. The actual implementation is coming soon.",
input: {...args, project, environment},
timestamp,
};
return result;
},
formatOutput: (output) => jsonResult(output),
},
Expand All @@ -136,8 +140,9 @@ function createMrtBundlePushTool(services: Services): McpTool {
* Creates all tools for the MRT toolset.
*
* @param services - MCP services
* @param injections - Optional dependency injections for testing
* @returns Array of MCP tools
*/
export function createMrtTools(services: Services): McpTool[] {
return [createMrtBundlePushTool(services)];
export function createMrtTools(services: Services, injections?: MrtToolInjections): McpTool[] {
return [createMrtBundlePushTool(services, injections)];
}
2 changes: 1 addition & 1 deletion packages/b2c-dx-mcp/src/tools/storefrontnext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Each section markdown file includes:
✅ **Modular**: Access specific sections as needed
✅ **Multi-Select**: Combine related sections in a single call for contextual learning
✅ **Always Current**: Content loaded from markdown files (easy to update)
✅ **Comprehensive Default**: Returns key sections by default for immediate value
✅ **Comprehensive Default**: Returns key sections by default for immediate value

## Placeholder Tools

Expand Down
34 changes: 34 additions & 0 deletions packages/b2c-dx-mcp/test/tools/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,40 @@ describe('tools/adapter', () => {

expect(result.isError).to.be.undefined;
expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined;
// Verify origin field is present in mrtConfig (may be undefined if not set)
expect(contextReceived?.mrtConfig).to.have.property('origin');
});

it('should pass mrtOrigin through to mrtConfig.origin in context', async () => {
// Test that mrtOrigin from config is passed through to context.mrtConfig.origin
const config = resolveConfig({
mrtApiKey: 'test-api-key-12345',
mrtOrigin: 'https://custom-cloud.mobify.com',
});
const services = Services.fromResolvedConfig(config);
let contextReceived: ToolExecutionContext | undefined;

const tool = createToolAdapter(
{
name: 'mrt_origin_tool',
description: 'Tests mrtOrigin passthrough',
toolsets: ['MRT'],
requiresMrtAuth: true,
inputSchema: {},
async execute(_args, context) {
contextReceived = context;
return 'success';
},
formatOutput: (output) => textResult(output),
},
services,
);

const result = await tool.handler({});

expect(result.isError).to.be.undefined;
expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined;
expect(contextReceived?.mrtConfig?.origin).to.equal('https://custom-cloud.mobify.com');
});

it('should support both requiresInstance and requiresMrtAuth being false', async () => {
Expand Down
Loading
Loading