From c3b1eeae6c309b58d1dc8b56b88545e1fd297c1c Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 7 Aug 2026 10:03:34 -0400 Subject: [PATCH 1/2] feat: align plugins with Agent Plugins standard; add MCP config_inspect + projectDirectory outlet Package the plugins to the open Agent Plugins standard (agent-plugins.org v1.0.0): each plugin gains a root plugin.json with Codex display metadata under extensions."com.openai", and the MCP plugin ships a standard mcp.json. The legacy .codex-plugin/plugin.json manifests are retained during a transition window so Codex CLI < 0.146.0 users are unaffected; both layouts are version-stamped in lockstep by sync-plugin-versions.mjs. MCP: add a config_inspect tool (mirrors `b2c setup inspect`; redaction on by default) that reports the resolved configuration, its sources, and the effective project directory + how it was resolved. Filesystem tools now resolve the project directory with explicit precedence (per-call argument > --project-directory/SFCC_PROJECT_DIRECTORY > cwd) via Services.resolveProjectDirectory and echo the resolved directory back, giving agents an outlet across MCP clients that spawn the server from inconsistent working directories. Extract shared config redaction into the SDK so the CLI inspect command and the MCP tool share one masking implementation (also masks jwtPassphrase). --- .../agent-plugins-standard-manifests.md | 5 + .changeset/cli-inspect-mask-jwt-passphrase.md | 5 + .../mcp-config-inspect-project-directory.md | 6 ++ docs/guide/agent-skills.md | 4 +- docs/mcp/configuration.md | 18 ++++ docs/mcp/toolsets.md | 1 + .../b2c-cli/src/commands/setup/inspect.ts | 47 ++------ packages/b2c-dx-mcp/src/services.ts | 52 ++++++++- .../b2c-dx-mcp/src/tools/cartridges/index.ts | 7 ++ .../src/tools/diagnostics/config-inspect.ts | 81 ++++++++++++++ .../b2c-dx-mcp/src/tools/diagnostics/index.ts | 2 + packages/b2c-dx-mcp/test/services.test.ts | 42 ++++++++ .../tools/diagnostics/config-inspect.test.ts | 100 ++++++++++++++++++ packages/b2c-tooling-sdk/src/config/index.ts | 3 + .../b2c-tooling-sdk/src/config/redaction.ts | 73 +++++++++++++ .../test/config/redaction.test.ts | 74 +++++++++++++ plugins/b2c-dx-mcp/mcp.json | 14 +++ plugins/b2c-dx-mcp/plugin.json | 32 ++++++ scripts/sync-plugin-versions.mjs | 55 +++++++--- skills/README.md | 2 +- skills/b2c-cli/plugin.json | 44 ++++++++ skills/b2c/plugin.json | 44 ++++++++ .../figma-to-sfnext-pagedesigner/plugin.json | 44 ++++++++ skills/storefront-next-figma/plugin.json | 44 ++++++++ skills/storefront-next/plugin.json | 43 ++++++++ 25 files changed, 783 insertions(+), 59 deletions(-) create mode 100644 .changeset/agent-plugins-standard-manifests.md create mode 100644 .changeset/cli-inspect-mask-jwt-passphrase.md create mode 100644 .changeset/mcp-config-inspect-project-directory.md create mode 100644 packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts create mode 100644 packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts create mode 100644 packages/b2c-tooling-sdk/src/config/redaction.ts create mode 100644 packages/b2c-tooling-sdk/test/config/redaction.test.ts create mode 100644 plugins/b2c-dx-mcp/mcp.json create mode 100644 plugins/b2c-dx-mcp/plugin.json create mode 100644 skills/b2c-cli/plugin.json create mode 100644 skills/b2c/plugin.json create mode 100644 skills/figma-to-sfnext-pagedesigner/plugin.json create mode 100644 skills/storefront-next-figma/plugin.json create mode 100644 skills/storefront-next/plugin.json diff --git a/.changeset/agent-plugins-standard-manifests.md b/.changeset/agent-plugins-standard-manifests.md new file mode 100644 index 000000000..178e1f089 --- /dev/null +++ b/.changeset/agent-plugins-standard-manifests.md @@ -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. diff --git a/.changeset/cli-inspect-mask-jwt-passphrase.md b/.changeset/cli-inspect-mask-jwt-passphrase.md new file mode 100644 index 000000000..f2eb22201 --- /dev/null +++ b/.changeset/cli-inspect-mask-jwt-passphrase.md @@ -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. diff --git a/.changeset/mcp-config-inspect-project-directory.md b/.changeset/mcp-config-inspect-project-directory.md new file mode 100644 index 000000000..ac16db7e2 --- /dev/null +++ b/.changeset/mcp-config-inspect-project-directory.md @@ -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. diff --git a/docs/guide/agent-skills.md b/docs/guide/agent-skills.md index cbfd9c11f..462923589 100644 --- a/docs/guide/agent-skills.md +++ b/docs/guide/agent-skills.md @@ -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 diff --git a/docs/mcp/configuration.md b/docs/mcp/configuration.md index 42c214729..c642a7dd6 100644 --- a/docs/mcp/configuration.md +++ b/docs/mcp/configuration.md @@ -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: diff --git a/docs/mcp/toolsets.md b/docs/mcp/toolsets.md index 758a76cbd..7330a3489 100644 --- a/docs/mcp/toolsets.md +++ b/docs/mcp/toolsets.md @@ -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 diff --git a/packages/b2c-cli/src/commands/setup/inspect.ts b/packages/b2c-cli/src/commands/setup/inspect.ts index a9deaac7b..c61e79a25 100644 --- a/packages/b2c-cli/src/commands/setup/inspect.ts +++ b/packages/b2c-cli/src/commands/setup/inspect.ts @@ -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([ - 'certificatePassphrase', - 'clientSecret', - 'mrtApiKey', - 'password', - 'slasClientSecret', -]); - /** * JSON output structure for the inspect command. */ @@ -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. */ @@ -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; @@ -135,12 +111,7 @@ export default class SetupInspect extends BaseCommand { const unmask = this.flags.unmask; // Build output config with masking applied - const outputConfig: Record = {}; - 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, diff --git a/packages/b2c-dx-mcp/src/services.ts b/packages/b2c-dx-mcp/src/services.ts index 62a6e74d3..3c19e2a1e 100644 --- a/packages/b2c-dx-mcp/src/services.ts +++ b/packages/b2c-dx-mcp/src/services.ts @@ -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. @@ -398,17 +411,52 @@ 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. + * + * @param override - Optional explicit project directory from a tool argument + * @returns The absolute project directory and the source it was resolved from + */ + public resolveProjectDirectory(override?: string): {path: string; source: 'argument' | 'config' | 'cwd'} { + if (override) { + return {path: path.resolve(override), source: 'argument'}; + } + const configured = this.resolvedConfig.values.projectDirectory; + if (configured) { + return {path: path.resolve(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; } diff --git a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts index 1ea5dfd66..b9b8f8877 100644 --- a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts +++ b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts @@ -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; } @@ -169,6 +175,7 @@ function createCartridgeDeployTool( return { ...result, + resolvedDirectory: directory, postInstructions: CARTRIDGE_PATH_REMINDER, }; } catch (error) { diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts new file mode 100644 index 000000000..8a3b79550 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts @@ -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; + /** 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): McpTool { + return createToolAdapter( + { + 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, + ); +} diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/index.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/index.ts index 32720ebcb..8dd369739 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/index.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/index.ts @@ -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'; @@ -38,6 +39,7 @@ export function createDiagnosticsTools( injections?: DiagnosticsToolInjections, ): McpTool[] { return [ + createConfigInspectTool(loadServices), createDebugListSessionsTool(loadServices, serverContext), createDebugStartSessionTool(loadServices, serverContext), createDebugEndSessionTool(loadServices, serverContext), diff --git a/packages/b2c-dx-mcp/test/services.test.ts b/packages/b2c-dx-mcp/test/services.test.ts index e75a8cd4d..4f6684434 100644 --- a/packages/b2c-dx-mcp/test/services.test.ts +++ b/packages/b2c-dx-mcp/test/services.test.ts @@ -245,6 +245,48 @@ 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}); + + expect(services.resolveProjectDirectory('/override/root')).to.deep.equal({ + path: path.resolve('/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.resolve('/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', () => { diff --git a/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts b/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts new file mode 100644 index 000000000..fa82d2f76 --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts @@ -0,0 +1,100 @@ +/* + * 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 {expect} from 'chai'; +import path from 'node:path'; +import {createConfigInspectTool} from '../../../src/tools/diagnostics/config-inspect.js'; +import {Services} from '../../../src/services.js'; +import {createMockResolvedConfig, createMockLoadServices} from '../../test-helpers.js'; +import type {ToolResult} from '../../../src/utils/types.js'; +import type {NormalizedConfig, ConfigSourceInfo} from '@salesforce/b2c-tooling-sdk/config'; + +interface ConfigInspectOutput { + config: Record; + projectDirectory: {path: string; source: 'argument' | 'config' | 'cwd'}; + sources: ConfigSourceInfo[]; + warnings?: string[]; +} + +function getResultJson(result: ToolResult): T { + const content = result.content[0]; + if (content.type !== 'text') { + throw new Error(`Expected text content, got ${content.type}`); + } + return JSON.parse(content.text) as T; +} + +function createServices(values: Partial = {}, sources: ConfigSourceInfo[] = []): Services { + const config = createMockResolvedConfig(values); + // Mock config helper does not accept sources; attach them for this test. + (config as {sources: ConfigSourceInfo[]}).sources = sources; + return new Services({resolvedConfig: config}); +} + +describe('config_inspect tool', () => { + it('is registered in the DIAGNOSTICS toolset and does not require an instance', () => { + const tool = createConfigInspectTool(createMockLoadServices(createServices())); + expect(tool.name).to.equal('config_inspect'); + expect(tool.toolsets).to.include('DIAGNOSTICS'); + }); + + it('masks sensitive values by default', async () => { + const services = createServices({ + hostname: 'example.demandware.net', + clientId: 'aaaa-bbbb', + clientSecret: 'super-secret-value-1234', + password: 'my-web-dav-password', + }); + const tool = createConfigInspectTool(createMockLoadServices(services)); + + const result = getResultJson(await tool.handler({})); + + expect(result.config.hostname).to.equal('example.demandware.net'); + expect(result.config.clientId).to.equal('aaaa-bbbb'); + expect(result.config.clientSecret).to.equal('supe...REDACTED'); + expect(result.config.password).to.equal('my-w...REDACTED'); + }); + + it('shows secrets unmasked when unmask is true', async () => { + const services = createServices({clientSecret: 'super-secret-value-1234'}); + const tool = createConfigInspectTool(createMockLoadServices(services)); + + const result = getResultJson(await tool.handler({unmask: true})); + + expect(result.config.clientSecret).to.equal('super-secret-value-1234'); + }); + + it('reports the configured project directory and its source', async () => { + const services = createServices({projectDirectory: '/tmp/my-project'}); + const tool = createConfigInspectTool(createMockLoadServices(services)); + + const result = getResultJson(await tool.handler({})); + + expect(result.projectDirectory.source).to.equal('config'); + expect(result.projectDirectory.path).to.equal(path.resolve('/tmp/my-project')); + }); + + it('falls back to cwd when no project directory is configured', async () => { + const services = createServices({}); + const tool = createConfigInspectTool(createMockLoadServices(services)); + + const result = getResultJson(await tool.handler({})); + + expect(result.projectDirectory.source).to.equal('cwd'); + expect(result.projectDirectory.path).to.equal(process.cwd()); + }); + + it('includes contributing sources', async () => { + const sources: ConfigSourceInfo[] = [{name: 'dw.json', location: '/tmp/dw.json', fields: ['hostname']}]; + const services = createServices({hostname: 'example.demandware.net'}, sources); + const tool = createConfigInspectTool(createMockLoadServices(services)); + + const result = getResultJson(await tool.handler({})); + + expect(result.sources).to.have.lengthOf(1); + expect(result.sources[0].name).to.equal('dw.json'); + }); +}); diff --git a/packages/b2c-tooling-sdk/src/config/index.ts b/packages/b2c-tooling-sdk/src/config/index.ts index 7c1560363..4bef8e696 100644 --- a/packages/b2c-tooling-sdk/src/config/index.ts +++ b/packages/b2c-tooling-sdk/src/config/index.ts @@ -142,6 +142,9 @@ export type { // Instance management export {InstanceManager, createInstanceManager} from './instance-manager.js'; +// Redaction helpers for displaying resolved config (CLI inspect, MCP config_inspect) +export {SENSITIVE_CONFIG_FIELDS, isSensitiveConfigField, maskConfigValue, redactConfigValues} from './redaction.js'; + // Config source registry (global, auto-included in resolveConfig) export {ConfigSourceRegistry, globalConfigSourceRegistry} from './config-source-registry.js'; diff --git a/packages/b2c-tooling-sdk/src/config/redaction.ts b/packages/b2c-tooling-sdk/src/config/redaction.ts new file mode 100644 index 000000000..79a36e8de --- /dev/null +++ b/packages/b2c-tooling-sdk/src/config/redaction.ts @@ -0,0 +1,73 @@ +/* + * 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 + */ +/** + * Shared redaction utilities for resolved configuration. + * + * Used by any surface that displays resolved config to a user or agent — the + * CLI `setup inspect` command and the MCP `config_inspect` tool — so masking + * behaviour stays consistent. Redaction is on by default; callers may opt into + * unmasked output explicitly. + * + * @module config/redaction + */ +import type {NormalizedConfig} from './types.js'; + +/** + * Config fields whose values are secrets and are masked by default. + */ +export const SENSITIVE_CONFIG_FIELDS: ReadonlySet = new Set([ + 'certificatePassphrase', + 'clientSecret', + 'jwtPassphrase', + 'mrtApiKey', + 'password', + 'slasClientSecret', +]); + +/** + * Returns true when a field name holds a secret that should be masked. + */ +export function isSensitiveConfigField(field: string): boolean { + return SENSITIVE_CONFIG_FIELDS.has(field as keyof NormalizedConfig); +} + +/** + * Mask a sensitive value, showing the first 4 characters when long enough to + * aid identification without disclosing the secret. Matches the convention + * used by the SDK logger (`...REDACTED`). + * + * @param value - The raw value to mask + * @returns The masked representation + */ +export function maskConfigValue(value: string): string { + if (value.length > 10) { + return `${value.slice(0, 4)}...REDACTED`; + } + return 'REDACTED'; +} + +/** + * Produce a shallow copy of resolved config values with sensitive fields + * masked. Undefined values are omitted. + * + * @param values - Resolved configuration values + * @param options - `unmask: true` disables masking (secrets shown verbatim) + * @returns A new record safe to display, unless `unmask` was set + */ +export function redactConfigValues( + values: NormalizedConfig, + options: {unmask?: boolean} = {}, +): Record { + const {unmask = false} = options; + const output: Record = {}; + for (const [key, value] of Object.entries(values)) { + if (value === undefined) { + continue; + } + output[key] = !unmask && isSensitiveConfigField(key) ? maskConfigValue(String(value)) : value; + } + return output; +} diff --git a/packages/b2c-tooling-sdk/test/config/redaction.test.ts b/packages/b2c-tooling-sdk/test/config/redaction.test.ts new file mode 100644 index 000000000..b9043693d --- /dev/null +++ b/packages/b2c-tooling-sdk/test/config/redaction.test.ts @@ -0,0 +1,74 @@ +/* + * 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 {expect} from 'chai'; +import { + SENSITIVE_CONFIG_FIELDS, + isSensitiveConfigField, + maskConfigValue, + redactConfigValues, +} from '../../src/config/redaction.js'; + +describe('config redaction', () => { + describe('isSensitiveConfigField', () => { + it('flags known secret fields', () => { + expect(isSensitiveConfigField('password')).to.equal(true); + expect(isSensitiveConfigField('clientSecret')).to.equal(true); + expect(isSensitiveConfigField('mrtApiKey')).to.equal(true); + expect(isSensitiveConfigField('slasClientSecret')).to.equal(true); + expect(isSensitiveConfigField('certificatePassphrase')).to.equal(true); + expect(isSensitiveConfigField('jwtPassphrase')).to.equal(true); + }); + + it('does not flag non-secret fields', () => { + expect(isSensitiveConfigField('hostname')).to.equal(false); + expect(isSensitiveConfigField('clientId')).to.equal(false); + }); + }); + + describe('maskConfigValue', () => { + it('shows the first 4 characters of long values', () => { + expect(maskConfigValue('super-secret-value')).to.equal('supe...REDACTED'); + }); + + it('fully redacts short values', () => { + expect(maskConfigValue('short')).to.equal('REDACTED'); + // 10 chars is the boundary — still fully redacted. + expect(maskConfigValue('1234567890')).to.equal('REDACTED'); + }); + }); + + describe('redactConfigValues', () => { + it('masks secrets and passes through non-secrets by default', () => { + const result = redactConfigValues({ + hostname: 'example.demandware.net', + clientId: 'aaaa-bbbb', + clientSecret: 'super-secret-value-1234', + password: 'my-web-dav-password', + }); + + expect(result.hostname).to.equal('example.demandware.net'); + expect(result.clientId).to.equal('aaaa-bbbb'); + expect(result.clientSecret).to.equal('supe...REDACTED'); + expect(result.password).to.equal('my-w...REDACTED'); + }); + + it('leaves secrets untouched when unmask is true', () => { + const result = redactConfigValues({clientSecret: 'super-secret-value-1234'}, {unmask: true}); + expect(result.clientSecret).to.equal('super-secret-value-1234'); + }); + + it('omits undefined values', () => { + const result = redactConfigValues({hostname: 'example.demandware.net', codeVersion: undefined}); + expect(result).to.have.property('hostname'); + expect(result).to.not.have.property('codeVersion'); + }); + + it('exports a stable set of sensitive fields', () => { + expect(SENSITIVE_CONFIG_FIELDS.has('password')).to.equal(true); + }); + }); +}); diff --git a/plugins/b2c-dx-mcp/mcp.json b/plugins/b2c-dx-mcp/mcp.json new file mode 100644 index 000000000..a32f5969d --- /dev/null +++ b/plugins/b2c-dx-mcp/mcp.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "b2c-dx-mcp": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@salesforce/b2c-dx-mcp@1.9.2", + "--allow-non-ga-tools" + ] + } + } +} diff --git a/plugins/b2c-dx-mcp/plugin.json b/plugins/b2c-dx-mcp/plugin.json new file mode 100644 index 000000000..41981cace --- /dev/null +++ b/plugins/b2c-dx-mcp/plugin.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "b2c-dx-mcp", + "version": "1.9.2", + "description": "MCP server for AI-assisted Salesforce B2C Commerce development with project-aware tooling for common workflows.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "b2c-commerce", + "commerce-cloud", + "mcp", + "scapi", + "ocapi" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "B2C DX MCP", + "shortDescription": "Project-aware MCP server for Salesforce B2C Commerce development.", + "longDescription": "MCP server for AI-assisted Salesforce B2C Commerce development — deploy cartridges, inspect SCAPI schemas, stream logs, debug server-side scripts, and search docs, with automatic project detection.", + "developerName": "Salesforce", + "category": "Productivity", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/" + } + } + } +} diff --git a/scripts/sync-plugin-versions.mjs b/scripts/sync-plugin-versions.mjs index c93cd07b7..d551fdf9c 100644 --- a/scripts/sync-plugin-versions.mjs +++ b/scripts/sync-plugin-versions.mjs @@ -42,13 +42,23 @@ for (const plugin of marketplace.plugins) { } writeJson(marketplacePath, marketplace); -// Codex per-plugin manifests. -const codexTargets = [ +// Per-plugin manifests. Two layouts ship together during the transition to the +// Agent Plugins standard (agent-plugins.org): +// - `plugin.json` at the plugin root — the standard manifest read by Codex +// (CLI >= 0.146.0), Cursor, Copilot, VS Code, and Kiro. +// - `.codex-plugin/plugin.json` — the legacy Codex manifest, kept so users on +// Codex CLI < 0.146.0 (no root-`plugin.json` support) keep working. Codex +// >= 0.146.0 reads the root manifest and treats this as an overlay. +// Both must stay in version lockstep. (Claude Code uses the marketplace above.) +const pluginManifestTargets = [ + 'skills/b2c-cli/plugin.json', 'skills/b2c-cli/.codex-plugin/plugin.json', + 'skills/b2c/plugin.json', 'skills/b2c/.codex-plugin/plugin.json', + 'skills/storefront-next/plugin.json', 'skills/storefront-next/.codex-plugin/plugin.json', ]; -for (const rel of codexTargets) { +for (const rel of pluginManifestTargets) { const path = join(repoRoot, rel); const manifest = readJson(path); manifest.version = version; @@ -74,21 +84,32 @@ if (!mcpVersion) { process.exit(1); } -// (1) Rewrite the pinned version in the plugin's .mcp.json. -const mcpConfigPath = join(repoRoot, 'plugins/b2c-dx-mcp/.mcp.json'); -const mcpConfig = readJson(mcpConfigPath); -const mcpArgs = mcpConfig.mcpServers?.['b2c-dx-mcp']?.args; -if (!Array.isArray(mcpArgs)) { - console.error('plugins/b2c-dx-mcp/.mcp.json has no mcpServers["b2c-dx-mcp"].args array'); - process.exit(1); -} -const pkgArgIndex = mcpArgs.findIndex((arg) => typeof arg === 'string' && arg.startsWith('@salesforce/b2c-dx-mcp@')); -if (pkgArgIndex === -1) { - console.error('plugins/b2c-dx-mcp/.mcp.json args do not reference @salesforce/b2c-dx-mcp'); - process.exit(1); +// (1) Rewrite the pinned version in the plugin's MCP config. Two files carry +// the same server config: `.mcp.json` (Claude Code marketplace / legacy +// Codex native) and `mcp.json` (Agent Plugins standard, read by Codex, +// Cursor, Copilot, VS Code, Kiro). Keep both in sync. +for (const mcpRel of ['plugins/b2c-dx-mcp/.mcp.json', 'plugins/b2c-dx-mcp/mcp.json']) { + const mcpConfigPath = join(repoRoot, mcpRel); + const mcpConfig = readJson(mcpConfigPath); + const mcpArgs = mcpConfig.mcpServers?.['b2c-dx-mcp']?.args; + if (!Array.isArray(mcpArgs)) { + console.error(`${mcpRel} has no mcpServers["b2c-dx-mcp"].args array`); + process.exit(1); + } + const pkgArgIndex = mcpArgs.findIndex((arg) => typeof arg === 'string' && arg.startsWith('@salesforce/b2c-dx-mcp@')); + if (pkgArgIndex === -1) { + console.error(`${mcpRel} args do not reference @salesforce/b2c-dx-mcp`); + process.exit(1); + } + mcpArgs[pkgArgIndex] = `@salesforce/b2c-dx-mcp@${mcpVersion}`; + writeJson(mcpConfigPath, mcpConfig); } -mcpArgs[pkgArgIndex] = `@salesforce/b2c-dx-mcp@${mcpVersion}`; -writeJson(mcpConfigPath, mcpConfig); + +// Stamp the MCP version onto the plugin's Agent Plugins root manifest. +const mcpPluginManifestPath = join(repoRoot, 'plugins/b2c-dx-mcp/plugin.json'); +const mcpPluginManifest = readJson(mcpPluginManifestPath); +mcpPluginManifest.version = mcpVersion; +writeJson(mcpPluginManifestPath, mcpPluginManifest); // (2) Stamp the MCP version onto its marketplace entry so clients re-pull. const mcpEntry = marketplace.plugins.find((plugin) => plugin.name === 'b2c-dx-mcp'); diff --git a/skills/README.md b/skills/README.md index d97fea08b..b574a24fc 100644 --- a/skills/README.md +++ b/skills/README.md @@ -48,7 +48,7 @@ b2c setup skills storefront-next-figma --ide cursor ## Repository Layout -Each plugin is a directory containing a `.codex-plugin/plugin.json` manifest and a `skills/` subdirectory. Every skill is a folder with a `SKILL.md` file (instructions + frontmatter), optionally alongside `references/`, `assets/`, and `evals/`. +Each plugin is a directory containing a root `plugin.json` manifest ([Agent Plugins](https://agent-plugins.org/) standard) and a `skills/` subdirectory. Client-specific display metadata (the Codex/`com.openai` interface block) lives under the manifest's `extensions` key. A legacy `.codex-plugin/plugin.json` is also kept alongside it for now, so users on Codex CLI versions older than 0.146.0 (which predate root-`plugin.json` support) keep working; newer Codex reads the root manifest and treats the legacy file as an overlay. Both manifests are kept in version lockstep by `scripts/sync-plugin-versions.mjs`. Every skill is a folder with a `SKILL.md` file (instructions + frontmatter), optionally alongside `references/`, `assets/`, and `evals/`. ``` skills/ diff --git a/skills/b2c-cli/plugin.json b/skills/b2c-cli/plugin.json new file mode 100644 index 000000000..7e9550388 --- /dev/null +++ b/skills/b2c-cli/plugin.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "b2c-cli", + "version": "1.6.2", + "description": "B2C CLI Skills for Salesforce Commerce Cloud development.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "b2c-commerce", + "commerce-cloud", + "sfra", + "scapi", + "ocapi" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "B2C CLI", + "shortDescription": "Use the Salesforce B2C Commerce CLI from your AI assistant.", + "longDescription": "Skills for driving the Salesforce B2C Commerce CLI — deploy cartridges, run jobs, manage On-Demand Sandboxes, stream logs, and perform WebDAV operations without leaving your editor.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Deploy the cartridges in ./cartridges to my sandbox", + "Run the reindex job on my sandbox", + "Create a new On-Demand Sandbox with a 48-hour TTL", + "Tail the latest error logs from my instance" + ] + } + } + } +} diff --git a/skills/b2c/plugin.json b/skills/b2c/plugin.json new file mode 100644 index 000000000..1b47ff65b --- /dev/null +++ b/skills/b2c/plugin.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "b2c", + "version": "1.6.2", + "description": "B2C Commerce development skills including Custom API development guides.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "b2c-commerce", + "commerce-cloud", + "sfra", + "scapi", + "ocapi" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "B2C Commerce Dev", + "shortDescription": "B2C Commerce development patterns and API guidance.", + "longDescription": "Skills for Salesforce B2C Commerce development — controllers, ISML templates, hooks, forms, Custom APIs, services, Page Designer, and Business Manager extensions.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Build a SCAPI Custom API for loyalty information", + "Add logging to my checkout controller", + "Create an HTTP service for a payment gateway", + "Add a Page Designer component" + ] + } + } + } +} diff --git a/skills/figma-to-sfnext-pagedesigner/plugin.json b/skills/figma-to-sfnext-pagedesigner/plugin.json new file mode 100644 index 000000000..a77e3ca09 --- /dev/null +++ b/skills/figma-to-sfnext-pagedesigner/plugin.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "figma-to-sfnext-pagedesigner", + "version": "0.1.0", + "description": "Convert a Figma frame into live Storefront Next Page Designer blocks — React components with decorator metadata, brand-token reconciliation, and product data loaders. Requires the Figma MCP server.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "storefront-next", + "figma", + "page-designer", + "b2c-commerce", + "commerce-cloud" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "Figma to Storefront Next Page Designer", + "shortDescription": "Turn a Figma frame into merchant-authorable Page Designer blocks.", + "longDescription": "Point this skill at a Figma frame and a Storefront Next repo. It splits the frame into one Page Designer block per visual section, shows you the authorable fields for approval, reconciles brand tokens where the design diverges, generates React + Tailwind components with Page Designer decorator metadata, wires up SCAPI product loaders, and commits. Requires the Figma MCP server (and a browser MCP for visual validation) to be configured in your AI tool.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Turn this Figma frame into Page Designer blocks for my Storefront Next project", + "Convert this Figma design into merchant-authorable Page Designer components", + "Build a hero and product row from Figma as Storefront Next PD blocks", + "Generate Page Designer components from my Figma homepage design" + ] + } + } + } +} diff --git a/skills/storefront-next-figma/plugin.json b/skills/storefront-next-figma/plugin.json new file mode 100644 index 000000000..ca811d164 --- /dev/null +++ b/skills/storefront-next-figma/plugin.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "storefront-next-figma", + "version": "1.3.3", + "description": "Figma design-kit skills for Storefront Next verticals — duplicate the kit, sync brand variables, and publish Code Connect. Requires the Figma MCP server.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "storefront-next", + "figma", + "design-system", + "b2c-commerce", + "commerce-cloud" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "Storefront Next Figma", + "shortDescription": "Sync Storefront Next brand tokens with the Figma design kit.", + "longDescription": "Skills for keeping the Storefront Next design system in sync with Figma — duplicate the official kit for a new vertical, update Brand variable collections from brand.css, edit components at the correct layer, and publish Figma Code Connect mappings. Requires the Figma MCP server to be configured in your AI tool.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Duplicate the Storefront Next Figma kit for a new vertical", + "Sync my brand.css tokens into the Figma Brand variables", + "Update a component in the Figma kit and publish Code Connect", + "Set up the Figma kit for my new storefront brand" + ] + } + } + } +} diff --git a/skills/storefront-next/plugin.json b/skills/storefront-next/plugin.json new file mode 100644 index 000000000..da4f8e8d1 --- /dev/null +++ b/skills/storefront-next/plugin.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "storefront-next", + "version": "1.6.2", + "description": "Storefront Next development skills for building React 19 storefronts on Salesforce B2C Commerce.", + "author": { + "name": "Salesforce" + }, + "homepage": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "repository": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling", + "license": "Apache-2.0", + "keywords": [ + "salesforce", + "storefront-next", + "react", + "b2c-commerce", + "commerce-cloud" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "Storefront Next", + "shortDescription": "Build Salesforce B2C Storefront Next projects with AI assistance.", + "longDescription": "Skills for developing React 19 storefronts on Salesforce B2C Commerce — routing, data fetching, components, Page Designer, authentication, i18n, extensions, testing, and deployment to Managed Runtime.", + "developerName": "Salesforce", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "logo": "./assets/logo.svg", + "composerIcon": "./assets/logo.svg", + "brandColor": "#0D9DDA", + "websiteURL": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/", + "defaultPrompt": [ + "Set up a new Storefront Next project", + "Add a new route with data fetching to my storefront", + "Deploy my storefront to Managed Runtime", + "Add Page Designer support to my storefront component" + ] + } + } + } +} From 7f6a20c5b043e94fed88e3cf7fbe8abc02ce9cb7 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 12 Aug 2026 16:39:42 -0400 Subject: [PATCH 2/2] fix: correct docs dead link and Windows-safe project-directory resolution - toolsets.md linked ../configuration (one level too high) causing a VitePress dead-link build failure in build-docs and preview builds; correct it to ./configuration#project-directory. - Services.resolveProjectDirectory ran path.resolve() on the configured and override values, which drive-prefixes a POSIX-style absolute path on Windows (D:\path\to\project) and broke the services tests on test-windows. Return the supplied value as-is (cwd is already absolute) and update the tests to assert the pass-through contract. --- docs/mcp/toolsets.md | 2 +- packages/b2c-dx-mcp/src/services.ts | 10 +++++++--- packages/b2c-dx-mcp/test/services.test.ts | 6 ++++-- .../test/tools/diagnostics/config-inspect.test.ts | 3 +-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/mcp/toolsets.md b/docs/mcp/toolsets.md index 7330a3489..5263a1396 100644 --- a/docs/mcp/toolsets.md +++ b/docs/mcp/toolsets.md @@ -18,7 +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. +- `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 diff --git a/packages/b2c-dx-mcp/src/services.ts b/packages/b2c-dx-mcp/src/services.ts index 3c19e2a1e..ac8ec14eb 100644 --- a/packages/b2c-dx-mcp/src/services.ts +++ b/packages/b2c-dx-mcp/src/services.ts @@ -427,16 +427,20 @@ export class Services { * 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 absolute project directory and the source it was resolved from + * @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: path.resolve(override), source: 'argument'}; + return {path: override, source: 'argument'}; } const configured = this.resolvedConfig.values.projectDirectory; if (configured) { - return {path: path.resolve(configured), source: 'config'}; + return {path: configured, source: 'config'}; } return {path: process.cwd(), source: 'cwd'}; } diff --git a/packages/b2c-dx-mcp/test/services.test.ts b/packages/b2c-dx-mcp/test/services.test.ts index 4f6684434..979e590ce 100644 --- a/packages/b2c-dx-mcp/test/services.test.ts +++ b/packages/b2c-dx-mcp/test/services.test.ts @@ -262,8 +262,10 @@ describe('services', () => { 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: path.resolve('/override/root'), + path: '/override/root', source: 'argument', }); }); @@ -273,7 +275,7 @@ describe('services', () => { const services = new Services({resolvedConfig: config}); expect(services.resolveProjectDirectory()).to.deep.equal({ - path: path.resolve('/path/to/project'), + path: '/path/to/project', source: 'config', }); }); diff --git a/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts b/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts index fa82d2f76..0ed4b4918 100644 --- a/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts +++ b/packages/b2c-dx-mcp/test/tools/diagnostics/config-inspect.test.ts @@ -5,7 +5,6 @@ */ import {expect} from 'chai'; -import path from 'node:path'; import {createConfigInspectTool} from '../../../src/tools/diagnostics/config-inspect.js'; import {Services} from '../../../src/services.js'; import {createMockResolvedConfig, createMockLoadServices} from '../../test-helpers.js'; @@ -74,7 +73,7 @@ describe('config_inspect tool', () => { const result = getResultJson(await tool.handler({})); expect(result.projectDirectory.source).to.equal('config'); - expect(result.projectDirectory.path).to.equal(path.resolve('/tmp/my-project')); + expect(result.projectDirectory.path).to.equal('/tmp/my-project'); }); it('falls back to cwd when no project directory is configured', async () => {