Skip to content

Commit ddf749d

Browse files
feat(mcp): implement MRT Push MCP Tool (#110)
* fix formatting of unrelated README
1 parent 9e3cf14 commit ddf749d

File tree

6 files changed

+468
-59
lines changed

6 files changed

+468
-59
lines changed

packages/b2c-dx-mcp/eslint.config.mjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import headerPlugin from 'eslint-plugin-header';
99
import path from 'node:path';
1010
import {fileURLToPath} from 'node:url';
1111

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

1414
const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore');
1515
headerPlugin.rules.header.meta.schema = false;
@@ -28,4 +28,23 @@ export default [
2828
...oclifRules,
2929
},
3030
},
31+
{
32+
files: ['test/**/*.ts'],
33+
rules: {
34+
...chaiTestRules,
35+
// Tests use stubbing patterns that intentionally return undefined
36+
'unicorn/no-useless-undefined': 'off',
37+
// Some tests use void 0 to satisfy TS stub typings; allow it in tests
38+
'no-void': 'off',
39+
// Helper functions in tests are commonly declared within suites for clarity
40+
'unicorn/consistent-function-scoping': 'off',
41+
// Sinon default import is intentional and idiomatic in tests
42+
'import/no-named-as-default-member': 'off',
43+
// import/namespace behaves inconsistently across environments when parsing CJS modules
44+
'import/namespace': 'off',
45+
// Disable for tests: ESLint import resolver doesn't understand conditional exports (development condition)
46+
// but Node.js resolves them correctly at runtime
47+
'import/no-unresolved': 'off',
48+
},
49+
},
3150
];

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
* 1. `--api-key` flag (oclif also checks `SFCC_MRT_API_KEY` env var)
3636
* 2. `~/.mobify` config file (or `~/.mobify--[hostname]` if `--cloud-origin` is set)
3737
*
38+
* **MRT Origin** (for Managed Runtime API URL):
39+
* 1. `--cloud-origin` flag (oclif also checks `SFCC_MRT_CLOUD_ORIGIN` env var)
40+
* 2. `mrtOrigin` field in dw.json
41+
* 3. Default: `https://cloud.mobify.com`
42+
*
3843
* @module services
3944
*/
4045

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

4853
/**
4954
* MRT (Managed Runtime) configuration.
50-
* Groups auth, project, and environment settings.
55+
* Groups auth, project, environment, and origin settings.
5156
*/
5257
export interface MrtConfig {
5358
/** Pre-resolved auth strategy for MRT API operations */
@@ -56,6 +61,8 @@ export interface MrtConfig {
5661
project?: string;
5762
/** MRT environment from --environment flag or SFCC_MRT_ENVIRONMENT env var */
5863
environment?: string;
64+
/** MRT API origin URL from --cloud-origin flag, SFCC_MRT_CLOUD_ORIGIN env var, or mrtOrigin in dw.json */
65+
origin?: string;
5966
}
6067

6168
/**
@@ -94,7 +101,7 @@ export class Services {
94101
public readonly b2cInstance?: B2CInstance;
95102

96103
/**
97-
* Pre-resolved MRT configuration (auth, project, environment).
104+
* Pre-resolved MRT configuration (auth, project, environment, origin).
98105
* Resolved once at server startup from MrtCommand flags and ~/.mobify.
99106
*/
100107
public readonly mrtConfig: MrtConfig;
@@ -122,6 +129,7 @@ export class Services {
122129
auth: config.hasMrtConfig() ? config.createMrtAuth() : undefined,
123130
project: config.values.mrtProject,
124131
environment: config.values.mrtEnvironment,
132+
origin: config.values.mrtOrigin,
125133
};
126134

127135
// Build B2C instance using factory method

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@
7070

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

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

8988
/**
90-
* MRT configuration (auth, project, environment).
89+
* MRT configuration (auth, project, environment, origin).
9190
* Pre-resolved at server startup.
9291
* Only populated when requiresMrtAuth is true.
9392
*/
94-
mrtConfig?: {
95-
/** Auth strategy for MRT API operations */
96-
auth: AuthStrategy;
97-
/** MRT project slug */
98-
project?: string;
99-
/** MRT environment */
100-
environment?: string;
101-
};
93+
mrtConfig?: MrtConfig;
10294

10395
/**
10496
* Services instance for file system access and other utilities.
@@ -310,6 +302,7 @@ export function createToolAdapter<TInput, TOutput>(
310302
auth: services.mrtConfig.auth,
311303
project: services.mrtConfig.project,
312304
environment: services.mrtConfig.environment,
305+
origin: services.mrtConfig.origin,
313306
};
314307
}
315308

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

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99
*
1010
* This toolset provides MCP tools for Managed Runtime operations.
1111
*
12-
* > ⚠️ **PLACEHOLDER - ACTIVE DEVELOPMENT**
13-
* > This tool is a placeholder implementation that returns mock responses.
14-
* > Actual implementation is coming soon. Use `--allow-non-ga-tools` flag to enable.
15-
*
1612
* @module tools/mrt
1713
*/
1814

1915
import {z} from 'zod';
2016
import type {McpTool} from '../../utils/index.js';
2117
import type {Services} from '../../services.js';
2218
import {createToolAdapter, jsonResult} from '../adapter.js';
19+
import {pushBundle} from '@salesforce/b2c-tooling-sdk/operations/mrt';
20+
import type {PushResult, PushOptions} from '@salesforce/b2c-tooling-sdk/operations/mrt';
21+
import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth';
2322
import {getLogger} from '@salesforce/b2c-tooling-sdk/logging';
2423

2524
/**
@@ -37,14 +36,11 @@ interface MrtBundlePushInput {
3736
}
3837

3938
/**
40-
* Output type for mrt_bundle_push tool.
39+
* Optional dependency injections for testing.
4140
*/
42-
interface MrtBundlePushOutput {
43-
tool: string;
44-
status: string;
45-
message: string;
46-
input: MrtBundlePushInput;
47-
timestamp: string;
41+
interface MrtToolInjections {
42+
/** Mock pushBundle function for testing */
43+
pushBundle?: (options: PushOptions, auth: AuthStrategy) => Promise<PushResult>;
4844
}
4945

5046
/**
@@ -56,14 +52,16 @@ interface MrtBundlePushOutput {
5652
* Shared across MRT, PWAV3, and STOREFRONTNEXT toolsets.
5753
*
5854
* @param services - MCP services
55+
* @param injections - Optional dependency injections for testing
5956
* @returns The mrt_bundle_push tool
6057
*/
61-
function createMrtBundlePushTool(services: Services): McpTool {
62-
return createToolAdapter<MrtBundlePushInput, MrtBundlePushOutput>(
58+
function createMrtBundlePushTool(services: Services, injections?: MrtToolInjections): McpTool {
59+
const pushBundleFn = injections?.pushBundle || pushBundle;
60+
return createToolAdapter<MrtBundlePushInput, PushResult>(
6361
{
6462
name: 'mrt_bundle_push',
6563
description:
66-
'[PLACEHOLDER] Bundle a pre-built PWA Kit project and push to Managed Runtime. Optionally deploy to a target environment.',
64+
'Bundle a pre-built PWA Kit project and push to Managed Runtime. Optionally deploy to a target environment.',
6765
toolsets: ['MRT', 'PWAV3', 'STOREFRONTNEXT'],
6866
isGA: false,
6967
// MRT operations use ApiKeyStrategy from SFCC_MRT_API_KEY or ~/.mobify
@@ -92,39 +90,45 @@ function createMrtBundlePushTool(services: Services): McpTool {
9290
// Get environment from --environment flag (optional)
9391
const environment = context.mrtConfig?.environment;
9492

95-
// Placeholder implementation
96-
const timestamp = new Date().toISOString();
93+
// Get origin from --cloud-origin flag or mrtOrigin config (optional)
94+
const origin = context.mrtConfig?.origin;
95+
96+
// Parse comma-separated glob patterns (same as CLI defaults)
97+
const ssrOnly = (args.ssrOnly || 'ssr.js,ssr.mjs,server/**/*').split(',').map((s) => s.trim());
98+
const ssrShared = (args.ssrShared || 'static/**/*,client/**/*').split(',').map((s) => s.trim());
99+
const buildDirectory = args.buildDirectory || './build';
97100

98-
// TODO: Remove this log when implementing
101+
// Log all computed variables before pushing bundle
99102
const logger = getLogger();
100-
logger.debug({mrtConfig: context.mrtConfig, project, environment}, 'mrt_bundle_push context');
103+
logger.debug(
104+
{
105+
project,
106+
environment,
107+
origin,
108+
buildDirectory,
109+
message: args.message,
110+
ssrOnly,
111+
ssrShared,
112+
},
113+
'[MRT] Pushing bundle with computed options',
114+
);
101115

102-
// TODO: When implementing, use context.mrtConfig.auth:
103-
//
104-
// import { pushBundle } from '@salesforce/b2c-tooling-sdk/operations/mrt';
105-
//
106-
// // Parse comma-separated glob patterns (same as CLI defaults)
107-
// const ssrOnly = (args.ssrOnly || 'ssr.js,ssr.mjs,server/**/*').split(',').map(s => s.trim());
108-
// const ssrShared = (args.ssrShared || 'static/**/*,client/**/*').split(',').map(s => s.trim());
109-
//
110-
// const result = await pushBundle({
111-
// project,
112-
// buildDirectory: args.buildDirectory || './build',
113-
// ssrOnly, // files that run only on SSR server (never sent to browser)
114-
// ssrShared, // files served from CDN and also available to SSR
115-
// message: args.message,
116-
// environment,
117-
// }, context.mrtConfig!.auth);
118-
// return result;
116+
// Push bundle to MRT
117+
// Note: auth is guaranteed to be present by the adapter when requiresMrtAuth is true
118+
const result = await pushBundleFn(
119+
{
120+
projectSlug: project,
121+
buildDirectory,
122+
ssrOnly, // files that run only on SSR server (never sent to browser)
123+
ssrShared, // files served from CDN and also available to SSR
124+
message: args.message,
125+
target: environment,
126+
origin, // MRT API origin URL (optional, defaults to https://cloud.mobify.com)
127+
},
128+
context.mrtConfig!.auth!,
129+
);
119130

120-
return {
121-
tool: 'mrt_bundle_push',
122-
status: 'placeholder',
123-
message:
124-
"This is a placeholder implementation for 'mrt_bundle_push'. The actual implementation is coming soon.",
125-
input: {...args, project, environment},
126-
timestamp,
127-
};
131+
return result;
128132
},
129133
formatOutput: (output) => jsonResult(output),
130134
},
@@ -136,8 +140,9 @@ function createMrtBundlePushTool(services: Services): McpTool {
136140
* Creates all tools for the MRT toolset.
137141
*
138142
* @param services - MCP services
143+
* @param injections - Optional dependency injections for testing
139144
* @returns Array of MCP tools
140145
*/
141-
export function createMrtTools(services: Services): McpTool[] {
142-
return [createMrtBundlePushTool(services)];
146+
export function createMrtTools(services: Services, injections?: MrtToolInjections): McpTool[] {
147+
return [createMrtBundlePushTool(services, injections)];
143148
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,40 @@ describe('tools/adapter', () => {
589589

590590
expect(result.isError).to.be.undefined;
591591
expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined;
592+
// Verify origin field is present in mrtConfig (may be undefined if not set)
593+
expect(contextReceived?.mrtConfig).to.have.property('origin');
594+
});
595+
596+
it('should pass mrtOrigin through to mrtConfig.origin in context', async () => {
597+
// Test that mrtOrigin from config is passed through to context.mrtConfig.origin
598+
const config = resolveConfig({
599+
mrtApiKey: 'test-api-key-12345',
600+
mrtOrigin: 'https://custom-cloud.mobify.com',
601+
});
602+
const services = Services.fromResolvedConfig(config);
603+
let contextReceived: ToolExecutionContext | undefined;
604+
605+
const tool = createToolAdapter(
606+
{
607+
name: 'mrt_origin_tool',
608+
description: 'Tests mrtOrigin passthrough',
609+
toolsets: ['MRT'],
610+
requiresMrtAuth: true,
611+
inputSchema: {},
612+
async execute(_args, context) {
613+
contextReceived = context;
614+
return 'success';
615+
},
616+
formatOutput: (output) => textResult(output),
617+
},
618+
services,
619+
);
620+
621+
const result = await tool.handler({});
622+
623+
expect(result.isError).to.be.undefined;
624+
expect(contextReceived?.mrtConfig?.auth).to.not.be.undefined;
625+
expect(contextReceived?.mrtConfig?.origin).to.equal('https://custom-cloud.mobify.com');
592626
});
593627

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

0 commit comments

Comments
 (0)