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
5 changes: 5 additions & 0 deletions .changeset/agent-plugins-standard-manifests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/b2c-agent-plugins': minor
---

Package the plugins to the open Agent Plugins standard (agent-plugins.org v1.0.0). Each plugin now has a root `plugin.json` manifest with its Codex display metadata under `extensions."com.openai"`, and the MCP server plugin ships a standard `mcp.json`. This lets Codex, Cursor, GitHub Copilot, VS Code, and Kiro consume the plugins directly; Claude Code continues to install from its marketplace as before. The legacy `.codex-plugin/plugin.json` manifests are retained during the transition so existing Codex users on older CLI versions are unaffected.
5 changes: 5 additions & 0 deletions .changeset/cli-inspect-mask-jwt-passphrase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/b2c-cli': patch
---

`b2c setup inspect` now redacts the JWT private key passphrase (`jwtPassphrase`) by default, matching the other secret fields. Use `--unmask` to show it.
6 changes: 6 additions & 0 deletions .changeset/mcp-config-inspect-project-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-dx-mcp': minor
'@salesforce/b2c-tooling-sdk': minor
---

Add a `config_inspect` MCP tool that reports the resolved configuration (instance, auth, SCAPI/MRT settings) with the source of each value and the effective project directory — secrets are redacted by default. Filesystem tools now resolve the project directory with explicit precedence (per-call argument, then `--project-directory`/`SFCC_PROJECT_DIRECTORY`, then the process working directory) and echo the resolved directory back in their output, so agents can override it per call and see which directory was used across MCP clients that spawn the server from inconsistent working directories.
4 changes: 3 additions & 1 deletion docs/guide/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ description: Agentic B2C Developer Toolkit — AI agent skills and plugins that

Turn your coding agent into a B2C Commerce specialist. Skills cover the full platform — storefront and headless development, operational workflows, and everything in between — so your agent knows both how B2C Commerce works and which CLI commands to run.

Skills follow the open [Agent Skills](https://agentskills.io/home) standard and work with Agentforce Vibes, Claude Code, Cursor, GitHub Copilot (VS Code and CLI), Codex, OpenCode, and others. Install from your IDE's plugin marketplace or the B2C CLI (`b2c setup skills`).
Skills follow the open [Agent Skills](https://agentskills.io/home) standard and work with Agentforce Vibes, Claude Code, Cursor, GitHub Copilot (VS Code and CLI), Codex, Kiro, OpenCode, and others. Install from your IDE's plugin marketplace or the B2C CLI (`b2c setup skills`).

These plugins are packaged to the open [Agent Plugins](https://agent-plugins.org/) standard: each plugin has a root `plugin.json` (targeting `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`) with its skills under `skills/`, and the MCP server plugin ships an `mcp.json`. Clients that read this standard — **Codex/ChatGPT, Cursor, GitHub Copilot, VS Code, and Kiro** — consume these manifests directly. **Claude Code** is the one exception: it installs via its own marketplace (`.claude-plugin/marketplace.json`), documented below.

## Quick Start

Expand Down
18 changes: 18 additions & 0 deletions docs/mcp/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ MRT tools require an API key. You can include `mrtApiKey`, `mrtProject`, and `mr

If both `dw.json` and `~/.mobify` contain an API key, `dw.json` takes precedence. For complete setup instructions, see the [Authentication Guide](../guide/authentication#managed-runtime-api-key).

## Project Directory {#project-directory}

Several tools operate on files in your project — for example, `cartridge_deploy` searches for cartridges and `scapi_custom_api_generate_scaffold` writes generated files. These tools need to know which directory is your project root.

The server resolves the project directory in this order:

1. **Per-call tool argument** (highest) — tools that touch the filesystem accept an explicit `projectDirectory` (or an equivalent like `directory` / `projectRoot`). This is the reliable outlet when the agent knows the project path.
2. **`--project-directory` flag / `SFCC_PROJECT_DIRECTORY` env var** — set once in `mcp.json` for the whole server.
3. **Process working directory** (`cwd`) — the fallback, but **MCP clients disagree on what the working directory is**. Claude Code and GitHub Copilot set it to the project root; Cursor user-level config (`~/.cursor/mcp.json`) sets it to your home directory. Because it's inconsistent, don't rely on it alone.

For reliable behavior, either set `--project-directory "${workspaceFolder}"` (or your client's project-path variable) in `mcp.json`, or let the agent pass `projectDirectory` per call. Tools echo the resolved directory back in their output, so you can confirm which path was used when none was passed explicitly.

::: tip Diagnosing configuration
Run the `config_inspect` tool (ask your agent to "inspect the B2C MCP configuration") to see the resolved configuration — instance, auth, SCAPI/MRT settings, and which source provided each value — along with the effective project directory and how it was resolved. Secrets are redacted by default.
:::

This is the [Agent Plugins](https://agent-plugins.org/plugin-authors/mcp-servers) `cwd` model: when a plugin declares an MCP server without an explicit `cwd`, the working directory defaults to the plugin root rather than your open project — which is exactly why the explicit outlets above matter.

## Configuration Priority

When the same setting is provided in multiple places, the server resolves values in this order:
Expand Down
1 change: 1 addition & 0 deletions docs/mcp/toolsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Cartridge deployment and code version management. **Auto-enabled for** cartridge

Script debugger, runtime log inspection, and multi-corpus documentation search. **Always enabled.** The debugger and log tools also appear in `CARTRIDGES` and `SCAPI`; the documentation tools appear in every toolset.

- `config_inspect` — show the resolved configuration (instance, auth, SCAPI/MRT settings) with the contributing source for each value, plus the effective [project directory](./configuration#project-directory) and how it was resolved. Secrets are redacted by default. Use it first when configuration seems wrong or the server appears to target the wrong instance or directory.
- [Script Debugger](./tools/diagnostics) — `debug_*` tools: manage SDAPI sessions, set breakpoints, step execution, inspect stack/variables, and capture at a breakpoint
- [Instance logs](./tools/logs#instance-logs) — `logs_*` tools: list files, fetch recent entries, and run buffered watches
- [MRT logs](./tools/logs#mrt-logs) — `mrt_logs_*` tools: buffered tail of Managed Runtime application logs over a WebSocket
Expand Down
47 changes: 9 additions & 38 deletions packages/b2c-cli/src/commands/setup/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,16 @@ import {Flags, ux} from '@oclif/core';
import cliui from 'cliui';
import {BaseCommand, loadConfig} from '@salesforce/b2c-tooling-sdk/cli';
import type {NormalizedConfig, ConfigSourceInfo, ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config';
import {EnvSource} from '@salesforce/b2c-tooling-sdk/config';
import {
EnvSource,
isSensitiveConfigField,
maskConfigValue,
redactConfigValues,
} from '@salesforce/b2c-tooling-sdk/config';
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '@salesforce/b2c-tooling-sdk';
import {DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients';
import {withDocs} from '../../i18n/index.js';

/**
* Sensitive fields that should be masked by default.
*/
const SENSITIVE_FIELDS = new Set<keyof NormalizedConfig>([
'certificatePassphrase',
'clientSecret',
'mrtApiKey',
'password',
'slasClientSecret',
]);

/**
* JSON output structure for the inspect command.
*/
Expand All @@ -32,24 +26,6 @@ interface SetupInspectResponse {
warnings?: string[];
}

/**
* Mask a sensitive value, showing first 4 characters.
* Matches the pattern used in the logger for consistency.
*/
function maskValue(value: string): string {
if (value.length > 10) {
return `${value.slice(0, 4)}...REDACTED`;
}
return 'REDACTED';
}

/**
* Check if a field is sensitive and should be masked.
*/
function isSensitiveField(field: string): boolean {
return SENSITIVE_FIELDS.has(field as keyof NormalizedConfig);
}

/**
* Get the display value for a config field, applying masking if needed.
*/
Expand All @@ -64,8 +40,8 @@ function getDisplayValue(field: string, value: unknown, unmask: boolean): string

const strValue = String(value);

if (!unmask && isSensitiveField(field)) {
return maskValue(strValue);
if (!unmask && isSensitiveConfigField(field)) {
return maskConfigValue(strValue);
}

return strValue;
Expand Down Expand Up @@ -135,12 +111,7 @@ export default class SetupInspect extends BaseCommand<typeof SetupInspect> {
const unmask = this.flags.unmask;

// Build output config with masking applied
const outputConfig: Record<string, unknown> = {};
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
outputConfig[key] = isSensitiveField(key) && !unmask ? maskValue(String(value)) : value;
}
}
const outputConfig = redactConfigValues(values, {unmask});

const result: SetupInspectResponse = {
config: outputConfig,
Expand Down
56 changes: 54 additions & 2 deletions packages/b2c-dx-mcp/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,19 @@ export class Services {
return os.platform();
}

/**
* Get the resolved configuration (values, sources, warnings).
*
* Exposed for the `config_inspect` tool so agents can see the effective,
* source-attributed configuration the server resolved. Callers displaying
* these values must redact secrets (see `redactConfigValues`).
*
* @returns The resolved B2C configuration
*/
public getResolvedConfig(): ResolvedB2CConfig {
return this.resolvedConfig;
}

/**
* Get SCAPI Schemas client for discovering available SCAPI APIs.
* Requires shortCode, tenantId, and OAuth credentials to be configured.
Expand Down Expand Up @@ -398,17 +411,56 @@ export class Services {
return path.resolve(...segments);
}

/**
* Resolve the effective project directory for a tool call, reporting which
* source it came from.
*
* MCP clients disagree on the working directory a stdio server is spawned
* with (Claude Code / Cursor often use the user's home directory rather than
* the open project — see https://agent-plugins.org/plugin-authors/mcp-servers),
* so the resolved value is deliberately explicit. Precedence:
*
* 1. `override` — a per-call `projectDirectory` tool argument (highest)
* 2. `projectDirectory` from `--project-directory` / `SFCC_PROJECT_DIRECTORY`
* 3. `process.cwd()` (fallback; unreliable across clients)
*
* Tools should surface the returned `{path, source}` in their output so the
* agent can see which directory was used when it did not pass one explicitly.
*
* The `override` and configured values are returned as-supplied (not
* re-resolved against cwd); callers pass absolute paths, and `path.resolve`
* would otherwise drive-prefix a POSIX-style path on Windows.
*
* @param override - Optional explicit project directory from a tool argument
* @returns The project directory and the source it was resolved from
*/
public resolveProjectDirectory(override?: string): {path: string; source: 'argument' | 'config' | 'cwd'} {
if (override) {
return {path: override, source: 'argument'};
}
const configured = this.resolvedConfig.values.projectDirectory;
if (configured) {
return {path: configured, source: 'config'};
}
return {path: process.cwd(), source: 'cwd'};
}

/**
* Resolve a path relative to the project directory.
* If path is not supplied, returns the project directory.
* If path is absolute, returns it as-is.
* If path is relative, resolves it relative to the project directory.
*
* An optional explicit project-directory override (typically a per-call
* `projectDirectory` tool argument) takes precedence over the configured
* project directory and cwd — see {@link Services.resolveProjectDirectory}.
*
* @param pathArg - Optional path to resolve
* @param projectDirectoryOverride - Optional explicit project directory to resolve against
* @returns Resolved absolute path
*/
public resolveWithProjectDirectory(pathArg?: string): string {
const projectDir = this.resolvedConfig.values.projectDirectory ?? process.cwd();
public resolveWithProjectDirectory(pathArg?: string, projectDirectoryOverride?: string): string {
const projectDir = this.resolveProjectDirectory(projectDirectoryOverride).path;
if (!pathArg) {
return projectDir;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/b2c-dx-mcp/src/tools/cartridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ interface CartridgeDeployInput {

/** Output type: deploy result plus reminder to update site cartridge path. */
interface CartridgeDeployOutput extends DeployResult {
/**
* The absolute directory that was searched for cartridges. Reflected back so
* the agent can confirm which location was used when `directory` was omitted
* and the server fell back to the project directory or process cwd.
*/
resolvedDirectory: string;
/** Reminder to add deployed cartridges to the site cartridge path in Business Manager. */
postInstructions?: string;
}
Expand Down Expand Up @@ -169,6 +175,7 @@ function createCartridgeDeployTool(

return {
...result,
resolvedDirectory: directory,
postInstructions: CARTRIDGE_PATH_REMINDER,
};
} catch (error) {
Expand Down
81 changes: 81 additions & 0 deletions packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* SPDX-License-Identifier: Apache-2
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/

import {z} from 'zod';
import {redactConfigValues, type ConfigSourceInfo} from '@salesforce/b2c-tooling-sdk/config';
import type {McpTool} from '../../utils/index.js';
import type {Services} from '../../services.js';
import {createToolAdapter, jsonResult} from '../adapter.js';

interface ConfigInspectInput {
unmask?: boolean;
}

/**
* The project directory the server resolved, and which source it came from.
* Mirrors {@link Services.resolveProjectDirectory}.
*/
interface ProjectDirectoryInfo {
path: string;
source: 'argument' | 'config' | 'cwd';
}

interface ConfigInspectOutput {
/** Resolved configuration values (secrets masked unless `unmask` was set). */
config: Record<string, unknown>;
/** The effective project directory and how it was resolved. */
projectDirectory: ProjectDirectoryInfo;
/** Configuration sources that contributed, in precedence order. */
sources: ConfigSourceInfo[];
/** Resolution warnings, if any. */
warnings?: string[];
}

/**
* Creates the `config_inspect` tool — the MCP equivalent of the CLI
* `b2c setup inspect` command. Reports the resolved configuration (with secrets
* redacted by default), the sources that contributed, and — importantly for
* agents — the effective project directory and how it was resolved.
*
* @param loadServices - Function that loads configuration and returns Services instance
* @returns The config_inspect MCP tool
*/
export function createConfigInspectTool(loadServices: () => Promise<Services> | Services): McpTool {
return createToolAdapter<ConfigInspectInput, ConfigInspectOutput>(
{
name: 'config_inspect',
description:
'Inspect the resolved B2C Commerce configuration the MCP server is using — instance hostname, auth, SCAPI, MRT, and other settings — along with which source (dw.json, environment variables, flags) provided each value. ' +
'Secrets (passwords, client secrets, API keys) are redacted by default. ' +
'The output includes the effective projectDirectory and how it was resolved (explicit --project-directory / SFCC_PROJECT_DIRECTORY vs. the process working directory), which is useful for diagnosing why the server targets the wrong instance or cannot find a project. ' +
'Use this first when configuration seems wrong, auth is failing, or the server appears to be operating in the wrong directory.',
toolsets: ['DIAGNOSTICS'],
isGA: true,
requiresInstance: false,
inputSchema: {
unmask: z
.boolean()
.optional()
.describe(
'Show sensitive values (passwords, secrets, API keys) unmasked. Defaults to false — secrets are redacted. Only set this when the user explicitly needs the raw secret values.',
),
},
async execute(args, {services}) {
const resolved = services.getResolvedConfig();
const projectDirectory = services.resolveProjectDirectory();

return {
config: redactConfigValues(resolved.values, {unmask: args.unmask ?? false}),
projectDirectory,
sources: resolved.sources,
warnings: resolved.warnings.length > 0 ? resolved.warnings.map((w) => w.message) : undefined,
};
},
formatOutput: (output) => jsonResult(output),
},
loadServices,
);
}
2 changes: 2 additions & 0 deletions packages/b2c-dx-mcp/src/tools/diagnostics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type {McpTool} from '../../utils/index.js';
import type {Services} from '../../services.js';
import type {ServerContext} from '../../server-context.js';
import {createConfigInspectTool} from './config-inspect.js';
import {createDebugListSessionsTool} from './debug-list-sessions.js';
import {createDebugStartSessionTool} from './debug-start-session.js';
import {createDebugEndSessionTool} from './debug-end-session.js';
Expand Down Expand Up @@ -38,6 +39,7 @@ export function createDiagnosticsTools(
injections?: DiagnosticsToolInjections,
): McpTool[] {
return [
createConfigInspectTool(loadServices),
createDebugListSessionsTool(loadServices, serverContext),
createDebugStartSessionTool(loadServices, serverContext),
createDebugEndSessionTool(loadServices, serverContext),
Expand Down
44 changes: 44 additions & 0 deletions packages/b2c-dx-mcp/test/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,50 @@ describe('services', () => {
// rooted POSIX-style path produces a drive-prefixed, backslash-separated path).
expect(services.resolveWithProjectDirectory('subdir')).to.equal(path.resolve(projectDir, 'subdir'));
});

it('should honor an explicit project-directory override argument', () => {
const config = createMockResolvedConfig({projectDirectory: '/path/to/project'});
const services = new Services({resolvedConfig: config});

// Override wins over the configured project directory for path resolution.
expect(services.resolveWithProjectDirectory('subdir', '/other/root')).to.equal(
path.resolve('/other/root', 'subdir'),
);
});
});

describe('resolveProjectDirectory', () => {
it('reports source "argument" when an override is provided', () => {
const config = createMockResolvedConfig({projectDirectory: '/path/to/project'});
const services = new Services({resolvedConfig: config});

// Returned as-supplied (not re-resolved against cwd) so a POSIX-style
// absolute path is not drive-prefixed on Windows.
expect(services.resolveProjectDirectory('/override/root')).to.deep.equal({
path: '/override/root',
source: 'argument',
});
});

it('reports source "config" when only the configured project directory is set', () => {
const config = createMockResolvedConfig({projectDirectory: '/path/to/project'});
const services = new Services({resolvedConfig: config});

expect(services.resolveProjectDirectory()).to.deep.equal({
path: '/path/to/project',
source: 'config',
});
});

it('reports source "cwd" when nothing is configured', () => {
const config = createMockResolvedConfig();
const services = new Services({resolvedConfig: config});

expect(services.resolveProjectDirectory()).to.deep.equal({
path: process.cwd(),
source: 'cwd',
});
});
});

describe('getHomeDir', () => {
Expand Down
Loading
Loading