@salesforce/b2c-dx-mcp 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/guidance/b2c-cli/b2c-config/SKILL.md +23 -4
- package/content/guidance/b2c-cli/b2c-ecdn/SKILL.md +2 -0
- package/content/guidance/b2c-cli/b2c-job/SKILL.md +9 -3
- package/content/guidance/b2c-cli/b2c-job/references/RUN-AND-MONITOR.md +35 -6
- package/content/guidance/b2c-cli/b2c-scapi-schemas/SKILL.md +5 -1
- package/content/guidance/b2c-ops/b2c-edge-traffic-triage/SKILL.md +69 -0
- package/content/guidance/b2c-ops/b2c-job-health/SKILL.md +11 -6
- package/content/guidance/b2c-ops/b2c-production-triage/SKILL.md +1 -0
- package/content/guidance/index.json +67 -22
- package/content/guidance/mcp/b2c-mcp-scapi/SKILL.md +50 -226
- package/content/guidance/mcp/b2c-mcp-scapi/references/authentication.md +34 -0
- package/content/guidance/mcp/b2c-mcp-scapi/references/compose-verify.md +25 -0
- package/content/guidance/mcp/b2c-mcp-scapi/references/confirmation.md +30 -0
- package/content/guidance/mcp/b2c-mcp-scapi/references/discover.md +94 -0
- package/content/guidance/mcp/b2c-mcp-scapi/references/jobs.md +42 -0
- package/content/guidance/mcp/b2c-mcp-scapi/references/workflows.md +28 -0
- package/content/guidance/mcp/b2c-mcp-server/SKILL.md +4 -1
- package/dist/commands/mcp.js +7 -7
- package/dist/tools/docs/docs-read.js +4 -2
- package/dist/tools/docs/docs-search.js +10 -8
- package/dist/tools/docs/topics.d.ts +5 -0
- package/dist/tools/docs/topics.js +5 -0
- package/dist/tools/scapi/index.d.ts +1 -1
- package/dist/tools/scapi/index.js +2 -2
- package/dist/tools/scapi/scapi-code.js +96 -27
- package/dist/tools/scapi/scapi-schemas-list.js +93 -56
- package/node_modules/@salesforce/b2c-agent-plugins/CHANGELOG.md +12 -0
- package/node_modules/@salesforce/b2c-agent-plugins/b2c/.codex-plugin/plugin.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/b2c/plugin.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/.codex-plugin/plugin.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/plugin.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-config/SKILL.md +23 -4
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-ecdn/SKILL.md +2 -0
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/SKILL.md +9 -3
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/references/RUN-AND-MONITOR.md +35 -6
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-scapi-schemas/SKILL.md +5 -1
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/.codex-plugin/plugin.json +4 -3
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/README.md +1 -0
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/plugin.json +4 -3
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-edge-traffic-triage/SKILL.md +69 -0
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-job-health/SKILL.md +11 -6
- package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-production-triage/SKILL.md +1 -0
- package/node_modules/@salesforce/b2c-agent-plugins/package.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/.codex-plugin/plugin.json +1 -1
- package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/plugin.json +1 -1
- package/oclif.manifest.json +2 -2
- package/package.json +4 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -135,7 +135,7 @@ import { Flags } from '@oclif/core';
|
|
|
135
135
|
import { BaseCommand, MrtCommand, InstanceCommand, loadConfig, extractInstanceFlags, extractMrtFlags, } from '@salesforce/b2c-tooling-sdk/cli';
|
|
136
136
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
137
137
|
import { EnvSource, readProjectEnvironment } from '@salesforce/b2c-tooling-sdk/config';
|
|
138
|
-
import {
|
|
138
|
+
import { isRemoteScapiSchema, loadScapiSchemaOverrides, } from '@salesforce/b2c-tooling-sdk/scapi';
|
|
139
139
|
import { B2CDxMcpServer } from '../server.js';
|
|
140
140
|
import { Services } from '../services.js';
|
|
141
141
|
import { ServerContext } from '../server-context.js';
|
|
@@ -206,9 +206,9 @@ export default class McpServerCommand extends BaseCommand {
|
|
|
206
206
|
env: 'SFCC_DOCS_TOPICS',
|
|
207
207
|
}),
|
|
208
208
|
'scapi-schemas': Flags.string({
|
|
209
|
-
description: 'Local SCAPI OpenAPI contracts for scapi_search and scapi_execute (comma-separated JSON files or
|
|
209
|
+
description: 'Local SCAPI OpenAPI contracts for scapi_search and scapi_execute (comma-separated JSON files, directories or http(s) URLs). ' +
|
|
210
210
|
'Each replaces the bundled or live contract with the same family/name/version from servers[0].url, or adds an API. ' +
|
|
211
|
-
'Developer option for beta and pre-release APIs.',
|
|
211
|
+
'Developer option for beta and pre-release APIs. Overrides the project config scapi-schemas (dw.json or project .env).',
|
|
212
212
|
env: 'SFCC_SCAPI_SCHEMAS',
|
|
213
213
|
}),
|
|
214
214
|
};
|
|
@@ -378,7 +378,7 @@ export default class McpServerCommand extends BaseCommand {
|
|
|
378
378
|
// (--docs-topics / SFCC_DOCS_TOPICS), else config `docsCategories`
|
|
379
379
|
// (dw.json `docs-categories`, SFCC_DOCS_CATEGORIES, package.json).
|
|
380
380
|
docsTopics: this.flags['docs-topics'] ?? this.resolvedConfig?.values.docsCategories?.join(','),
|
|
381
|
-
scapiSchemas: this.loadLocalScapiSchemas(),
|
|
381
|
+
scapiSchemas: await this.loadLocalScapiSchemas(),
|
|
382
382
|
};
|
|
383
383
|
// Add toolsets to telemetry attributes
|
|
384
384
|
if (this.telemetry && startupFlags.toolsets) {
|
|
@@ -447,7 +447,7 @@ export default class McpServerCommand extends BaseCommand {
|
|
|
447
447
|
this.logger.info({ version: this.config.version }, 'MCP Server running on stdio');
|
|
448
448
|
}
|
|
449
449
|
/** Load --scapi-schemas once at startup; relative paths resolve from the project directory. */
|
|
450
|
-
loadLocalScapiSchemas() {
|
|
450
|
+
async loadLocalScapiSchemas() {
|
|
451
451
|
const value = this.flags['scapi-schemas'];
|
|
452
452
|
if (!value)
|
|
453
453
|
return undefined;
|
|
@@ -456,10 +456,10 @@ export default class McpServerCommand extends BaseCommand {
|
|
|
456
456
|
.split(',')
|
|
457
457
|
.map((entry) => entry.trim())
|
|
458
458
|
.filter(Boolean)
|
|
459
|
-
.map((entry) => path.resolve(base, entry));
|
|
459
|
+
.map((entry) => (isRemoteScapiSchema(entry) ? entry : path.resolve(base, entry)));
|
|
460
460
|
let documents;
|
|
461
461
|
try {
|
|
462
|
-
documents =
|
|
462
|
+
documents = await loadScapiSchemaOverrides(paths);
|
|
463
463
|
}
|
|
464
464
|
catch (error) {
|
|
465
465
|
this.error(error instanceof Error ? error.message : String(error));
|
|
@@ -7,7 +7,7 @@ import { TOOLSETS } from '../../utils/constants.js';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { readDocByQuery } from '@salesforce/b2c-tooling-sdk/docs';
|
|
9
9
|
import { createToolAdapter, errorResult, jsonResult } from '../adapter.js';
|
|
10
|
-
import { enabledCategoriesNote } from './topics.js';
|
|
10
|
+
import { DOCS_CITATION_NOTE, enabledCategoriesNote } from './topics.js';
|
|
11
11
|
import { workspaceInputSchema, resolveProjectWorkspace } from './storefront.js';
|
|
12
12
|
/** Default maximum characters of content returned per read call, to bound the inline payload. */
|
|
13
13
|
const DEFAULT_MAX_LENGTH = 12_000;
|
|
@@ -18,7 +18,8 @@ export function createDocsReadTool(loadServices, enabledCategories, detectedWork
|
|
|
18
18
|
idempotent: true,
|
|
19
19
|
openWorld: true,
|
|
20
20
|
description: 'Read a B2C Commerce (SFCC/Demandware) Script API reference, job step, developer guide, admin/merchant help article, or tooling doc by ID or fuzzy query. ' +
|
|
21
|
-
'Use docs_search to find IDs.' +
|
|
21
|
+
'Use docs_search to find IDs. ' +
|
|
22
|
+
'Cite by entry.url (sourceUrl for Markdown); never show doc IDs.' +
|
|
22
23
|
enabledCategoriesNote(enabledCategories),
|
|
23
24
|
toolsets: [...TOOLSETS],
|
|
24
25
|
inputSchema: {
|
|
@@ -56,6 +57,7 @@ export function createDocsReadTool(loadServices, enabledCategories, detectedWork
|
|
|
56
57
|
const end = offset + slice.length;
|
|
57
58
|
const truncated = end < totalLength;
|
|
58
59
|
return {
|
|
60
|
+
citation: DOCS_CITATION_NOTE,
|
|
59
61
|
entry: found.entry,
|
|
60
62
|
content: slice,
|
|
61
63
|
totalLength,
|
|
@@ -7,15 +7,15 @@ import { TOOLSETS } from '../../utils/constants.js';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { searchDocs } from '@salesforce/b2c-tooling-sdk/docs';
|
|
9
9
|
import { createToolAdapter, jsonResult } from '../adapter.js';
|
|
10
|
-
import { categoryEnumValues, enabledCategoriesNote } from './topics.js';
|
|
10
|
+
import { DOCS_CITATION_NOTE, categoryEnumValues, enabledCategoriesNote } from './topics.js';
|
|
11
11
|
import { workspaceInputSchema, detectedWorkspaceNote, resolveProjectWorkspace, } from './storefront.js';
|
|
12
12
|
/** Default number of results returned when `limit` is not supplied. Kept small to bound payload size for agents. */
|
|
13
13
|
const DEFAULT_LIMIT = 5;
|
|
14
14
|
/**
|
|
15
15
|
* Projects a search hit to the payload returned to an agent. By default we keep
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* the triage-critical fields (id, title, category, summary, score) plus `url`,
|
|
17
|
+
* so an agent can cite the page without exposing the id. `keywords`
|
|
18
|
+
* (index-tuning metadata) and `sourceUrl` (Markdown twin) are verbose-only.
|
|
19
19
|
*/
|
|
20
20
|
function leanResult(entry, score, verbose) {
|
|
21
21
|
const base = {
|
|
@@ -26,11 +26,11 @@ function leanResult(entry, score, verbose) {
|
|
|
26
26
|
};
|
|
27
27
|
if (entry.summary)
|
|
28
28
|
base.summary = entry.summary;
|
|
29
|
+
if (entry.url)
|
|
30
|
+
base.url = entry.url;
|
|
29
31
|
if (verbose) {
|
|
30
32
|
if (entry.keywords && entry.keywords.length > 0)
|
|
31
33
|
base.keywords = entry.keywords;
|
|
32
|
-
if (entry.url)
|
|
33
|
-
base.url = entry.url;
|
|
34
34
|
if (entry.sourceUrl)
|
|
35
35
|
base.sourceUrl = entry.sourceUrl;
|
|
36
36
|
}
|
|
@@ -43,7 +43,8 @@ export function createDocsSearchTool(loadServices, detectedWorkspaces = [], enab
|
|
|
43
43
|
idempotent: true,
|
|
44
44
|
openWorld: false,
|
|
45
45
|
description: 'Search B2C Commerce (SFCC/Demandware) Script API, job steps, developer guides, admin/merchant help, and tooling docs. ' +
|
|
46
|
-
'Use for natural-language queries or unknown IDs; call docs_read with a result ID.' +
|
|
46
|
+
'Use for natural-language queries or unknown IDs; call docs_read with a result ID. ' +
|
|
47
|
+
'Cite by url; never show doc IDs.' +
|
|
47
48
|
enabledCategoriesNote(enabledCategories) +
|
|
48
49
|
detectedWorkspaceNote(detectedWorkspaces),
|
|
49
50
|
toolsets: [...TOOLSETS],
|
|
@@ -66,7 +67,7 @@ export function createDocsSearchTool(loadServices, detectedWorkspaces = [], enab
|
|
|
66
67
|
verbose: z
|
|
67
68
|
.boolean()
|
|
68
69
|
.optional()
|
|
69
|
-
.describe('Include keywords and
|
|
70
|
+
.describe('Include keywords and the Markdown sourceUrl on each result (larger payload). Defaults to false.'),
|
|
70
71
|
},
|
|
71
72
|
async execute(args) {
|
|
72
73
|
const workspace = await resolveProjectWorkspace(args, detectedWorkspaces);
|
|
@@ -89,6 +90,7 @@ export function createDocsSearchTool(loadServices, detectedWorkspaces = [], enab
|
|
|
89
90
|
...(workspace && { workspace }),
|
|
90
91
|
total: ranked.length,
|
|
91
92
|
offset,
|
|
93
|
+
citation: DOCS_CITATION_NOTE,
|
|
92
94
|
results: results.map((r) => leanResult(r.entry, r.score, args.verbose ?? false)),
|
|
93
95
|
...(truncated && { truncated: true, nextOffset: end }),
|
|
94
96
|
};
|
|
@@ -18,4 +18,9 @@ export declare function categoryEnumValues(enabledCategories?: readonly DocCateg
|
|
|
18
18
|
* A sentence for a tool description noting the corpus is restricted to the
|
|
19
19
|
* configured topics, or an empty string when there is no restriction.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* Returned with every docs_search/docs_read result. Doc IDs are lookup keys for docs_read, not user-facing references;
|
|
23
|
+
* the rule sits next to the data because agents cite while reading results, long after reading tool descriptions.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DOCS_CITATION_NOTE = "Cite docs to users by url (sourceUrl for Markdown). Never show doc ids; they are docs_read keys. Show ids only if asked.";
|
|
21
26
|
export declare function enabledCategoriesNote(enabledCategories?: readonly DocCategory[]): string;
|
|
@@ -30,6 +30,11 @@ export function categoryEnumValues(enabledCategories) {
|
|
|
30
30
|
* A sentence for a tool description noting the corpus is restricted to the
|
|
31
31
|
* configured topics, or an empty string when there is no restriction.
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* Returned with every docs_search/docs_read result. Doc IDs are lookup keys for docs_read, not user-facing references;
|
|
35
|
+
* the rule sits next to the data because agents cite while reading results, long after reading tool descriptions.
|
|
36
|
+
*/
|
|
37
|
+
export const DOCS_CITATION_NOTE = 'Cite docs to users by url (sourceUrl for Markdown). Never show doc ids; they are docs_read keys. Show ids only if asked.';
|
|
33
38
|
export function enabledCategoriesNote(enabledCategories) {
|
|
34
39
|
if (!enabledCategories || enabledCategories.length === 0)
|
|
35
40
|
return '';
|
|
@@ -15,7 +15,7 @@ import type { ScapiExecutionRegistry } from './execution-registry.js';
|
|
|
15
15
|
*
|
|
16
16
|
* @param loadServices - Function that loads configuration and returns Services instance
|
|
17
17
|
* @param executions - Server-scoped registry for retained code-mode executions
|
|
18
|
-
* @param localSchemas -
|
|
18
|
+
* @param localSchemas - Startup --scapi-schemas contracts; when omitted, each call reads the project's `scapiSchemas` config
|
|
19
19
|
* @returns Array of MCP tools
|
|
20
20
|
*/
|
|
21
21
|
export declare function createScapiTools(loadServices: () => Promise<Services> | Services, executions?: ScapiExecutionRegistry, localSchemas?: readonly ScapiSchemaDocument[]): McpTool[];
|
|
@@ -13,10 +13,10 @@ import { ScapiLiveSchemaCache, ScapiShopperSessions } from '@salesforce/b2c-tool
|
|
|
13
13
|
*
|
|
14
14
|
* @param loadServices - Function that loads configuration and returns Services instance
|
|
15
15
|
* @param executions - Server-scoped registry for retained code-mode executions
|
|
16
|
-
* @param localSchemas -
|
|
16
|
+
* @param localSchemas - Startup --scapi-schemas contracts; when omitted, each call reads the project's `scapiSchemas` config
|
|
17
17
|
* @returns Array of MCP tools
|
|
18
18
|
*/
|
|
19
|
-
export function createScapiTools(loadServices, executions, localSchemas
|
|
19
|
+
export function createScapiTools(loadServices, executions, localSchemas) {
|
|
20
20
|
// Live contracts discovered by search or schema fetches are reused by execution for the same tenant.
|
|
21
21
|
const schemaCache = new ScapiLiveSchemaCache();
|
|
22
22
|
const shopperSessions = new ScapiShopperSessions();
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2
|
|
4
4
|
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
5
|
*/
|
|
6
|
+
import path from 'node:path';
|
|
6
7
|
import { z } from 'zod';
|
|
7
|
-
import { loadScapiSchemas, mergeScapiSchemas, scapiTenantKey, ScapiLiveSchemaCache, ScapiShopperSessions, runScapiCode, createScapiRequest, createScapiAuth, loadScapiSnippets, saveScapiSnippet, } from '@salesforce/b2c-tooling-sdk/scapi';
|
|
8
|
+
import { loadScapiSchemas, matchesScapiApi, mergeScapiSchemas, suggestScapiApis, scapiTenantKey, ScapiLiveSchemaCache, ScapiShopperSessions, runScapiCode, createScapiRequest, createScapiAuth, loadScapiSnippets, saveScapiSnippet, isRemoteScapiSchema, loadScapiSchemaOverrides, } from '@salesforce/b2c-tooling-sdk/scapi';
|
|
8
9
|
import { toOrganizationId } from '@salesforce/b2c-tooling-sdk/clients';
|
|
9
10
|
import { getB2CConfigDirectory } from '@salesforce/b2c-tooling-sdk/config';
|
|
10
11
|
import { resolveEffectiveSafetyConfig, loadGlobalSafetyConfig } from '@salesforce/b2c-tooling-sdk/safety';
|
|
@@ -25,7 +26,7 @@ function requireScapiSkill(read) {
|
|
|
25
26
|
if (read !== true)
|
|
26
27
|
throw new Error(SCAPI_SKILL_REQUIRED);
|
|
27
28
|
}
|
|
28
|
-
const searchDescription = `Requires reading skill://mcp/b2c-mcp-scapi/SKILL.md first. Search SCAPI Admin/Shopper OpenAPI contracts by running JavaScript over \`spec
|
|
29
|
+
const searchDescription = `Requires reading skill://mcp/b2c-mcp-scapi/SKILL.md first. Search SCAPI Admin/Shopper OpenAPI contracts by running JavaScript over \`spec\` (only spec.apis, spec.paths[fullPath][method] and spec.resolve exist), to find the operationId, path, inputs and scopes for a Commerce API task when no dedicated tool fits. Use cip_* for analytics and webdav_* for files. Searches the bundled contracts by default (offline, no credentials); schemas:"live" fetches the current contracts from the configured instance instead, including tenant c_* fields, custom APIs and APIs newer than the bundle (falls back to the bundled contracts with a warning when live access fails). Operations carry summary, description and tags: match on those, not only operationId. Results are capped at 24 KB: operations are an outline by default (no nested descriptions or examples; detail:"full" restores them), and you should still return fields such as operationId, method, path and summary, not whole operations.`;
|
|
29
30
|
const executeDescription = `Requires reading skill://mcp/b2c-mcp-scapi/SKILL.md first. Run any SCAPI Admin or Shopper API operation (reads, writes, searches and actions) through scapi.request() in JavaScript. Covers most developer, merchant and administrator tasks: catalogs, products, pricing, promotions, orders, customers, inventory, sites, jobs, code versions, observability, and storefront flows such as baskets. Use it whenever no more specific tool fits. Find operations with scapi_search first. Auth is automatic; Shopper calls run as a per-site guest whose basket persists. Operations are governed by Safety Mode and may ask the user for confirmation.`;
|
|
30
31
|
function codeResult(data, resolution) {
|
|
31
32
|
const result = resolution ? attachResolution(jsonResult(data, 0), resolution, 0) : jsonResult(data, 0);
|
|
@@ -45,21 +46,41 @@ function failure(error, resolution) {
|
|
|
45
46
|
isError: true,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
|
-
export function createScapiCodeTools(loadServices, snippetDirectory, registry = new ScapiExecutionRegistry(), schemaCache = new ScapiLiveSchemaCache(), shopperSessions = new ScapiShopperSessions(), localSchemas
|
|
49
|
-
// Developer-supplied contracts
|
|
50
|
-
|
|
49
|
+
export function createScapiCodeTools(loadServices, snippetDirectory, registry = new ScapiExecutionRegistry(), schemaCache = new ScapiLiveSchemaCache(), shopperSessions = new ScapiShopperSessions(), localSchemas) {
|
|
50
|
+
// Developer-supplied contracts replace bundled and live ones with the same id. The startup
|
|
51
|
+
// --scapi-schemas flag wins; otherwise each call reads the project's `scapiSchemas` config
|
|
52
|
+
// (dw.json `scapi-schemas` or SFCC_SCAPI_SCHEMAS, including the project .env). Entries may be
|
|
53
|
+
// files, directories or http(s) URLs; fetched contracts are kept until this server ends.
|
|
54
|
+
const remoteSchemas = new Map();
|
|
55
|
+
const projectSchemas = async (services, signal) => {
|
|
56
|
+
if (localSchemas)
|
|
57
|
+
return localSchemas;
|
|
58
|
+
const entries = services?.getResolvedConfig().values.scapiSchemas;
|
|
59
|
+
if (!services || !entries?.length)
|
|
60
|
+
return [];
|
|
61
|
+
const base = services.getResolution().projectDirectory?.path ?? process.cwd();
|
|
62
|
+
return loadScapiSchemaOverrides(entries.map((entry) => (isRemoteScapiSchema(entry) ? entry : path.resolve(base, entry))), { cache: remoteSchemas, signal });
|
|
63
|
+
};
|
|
51
64
|
// Retain only source for the last 50 completed executions, until this server ends.
|
|
52
65
|
const executions = new Map();
|
|
53
66
|
const searchInput = {
|
|
54
67
|
...createProjectContextInputSchema('configuration'),
|
|
55
68
|
code,
|
|
56
69
|
skillRead,
|
|
57
|
-
api: z
|
|
70
|
+
api: z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(/^[a-z0-9-]+(?:\/[a-z0-9-]+){0,2}\/?$/i, 'api must be family, family/name or family/name/version.')
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Limit schemas to an API family, family/name or family/name/version (cdn, cdn/zones, cdn/zones/v1). Omit for all APIs.'),
|
|
58
75
|
schemas: z
|
|
59
76
|
.enum(['bundled', 'live'])
|
|
60
77
|
.optional()
|
|
61
78
|
.describe('bundled (default, offline) or live tenant contracts from the Schemas API.'),
|
|
62
79
|
refresh: z.boolean().optional().describe('With schemas:"live", refetch instead of using cached contracts.'),
|
|
80
|
+
detail: z
|
|
81
|
+
.enum(['outline', 'full'])
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('outline (default) drops nested descriptions and examples to fit 24 KB; full keeps all prose.'),
|
|
63
84
|
authType: z
|
|
64
85
|
.enum(['admin', 'shopper'])
|
|
65
86
|
.optional()
|
|
@@ -106,36 +127,83 @@ export function createScapiCodeTools(loadServices, snippetDirectory, registry =
|
|
|
106
127
|
throw new Error('SCAPI_SEARCH_ARGUMENT_INVALID: refresh requires schemas:"live".');
|
|
107
128
|
let documents;
|
|
108
129
|
let failures = [];
|
|
130
|
+
let schemaSource = input.schemas === 'live' ? 'live' : 'bundled';
|
|
131
|
+
const warnings = [];
|
|
132
|
+
let services;
|
|
109
133
|
if (input.schemas === 'live') {
|
|
110
|
-
|
|
134
|
+
services = await loadServices(input);
|
|
111
135
|
resolution = services.getResolution();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
}
|
|
137
|
+
else if (!localSchemas) {
|
|
138
|
+
// Bundled search works without configuration; config is read only for local contracts,
|
|
139
|
+
// and configuration errors surface on tools that need it.
|
|
140
|
+
try {
|
|
141
|
+
services = await loadServices(input);
|
|
142
|
+
resolution = services.getResolution();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
services = undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const localDocuments = await projectSchemas(services, context?.signal);
|
|
149
|
+
const local = (api) => localDocuments.filter((document) => matchesScapiApi(document.entry.id, api));
|
|
150
|
+
// Name close ids so a near-miss filter (ecdn, zones) can be corrected in one step.
|
|
151
|
+
const unknownApi = (message, known) => {
|
|
152
|
+
const close = suggestScapiApis(known.map((document) => document.entry.id), input.api);
|
|
153
|
+
return new Error(`${message}${close.length > 0 ? ` Close matches: ${close.join(', ')}.` : ''}`);
|
|
154
|
+
};
|
|
155
|
+
const searchBundled = () => {
|
|
156
|
+
const all = mergeScapiSchemas(loadScapiSchemas(), [], local());
|
|
157
|
+
const bundled = all.filter((document) => matchesScapiApi(document.entry.id, input.api));
|
|
158
|
+
if (bundled.length === 0)
|
|
159
|
+
throw unknownApi('Unknown schema ID. Omit api to discover available APIs, or use schemas:"live" for tenant and custom APIs.', all);
|
|
160
|
+
return bundled;
|
|
161
|
+
};
|
|
162
|
+
if (services && input.schemas === 'live') {
|
|
163
|
+
try {
|
|
164
|
+
// Throws with configuration guidance when shortCode, tenantId, or OAuth credentials are missing.
|
|
165
|
+
const client = services.getScapiSchemasClient();
|
|
166
|
+
const organizationId = services.getOrganizationId();
|
|
167
|
+
const tenant = scapiTenantKey(services.getShortCode(), organizationId);
|
|
168
|
+
({ documents, failures } = await schemaCache.load(tenant, client, organizationId, {
|
|
169
|
+
api: input.api,
|
|
170
|
+
refresh: input.refresh,
|
|
171
|
+
signal: context?.signal,
|
|
172
|
+
}));
|
|
173
|
+
documents = mergeScapiSchemas([], documents, local(input.api));
|
|
174
|
+
if (documents.length === 0 && failures.length === 0)
|
|
175
|
+
throw unknownApi('Unknown schema ID for this tenant. Omit api to discover available APIs.', mergeScapiSchemas(loadScapiSchemas(), schemaCache.get(tenant), local()));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Custom APIs exist only on the tenant; an unknown ID is a caller error, not an access problem.
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
if (context?.signal?.aborted ||
|
|
181
|
+
input.api?.split('/')[0].toLowerCase() === 'custom' ||
|
|
182
|
+
message.startsWith('Unknown schema ID'))
|
|
183
|
+
throw error;
|
|
184
|
+
documents = searchBundled();
|
|
185
|
+
schemaSource = 'bundled';
|
|
186
|
+
warnings.push(`Live schemas unavailable (${message.replaceAll(/\s+/g, ' ').trim().slice(0, 300)}). Searching the bundled standard contracts instead: no tenant c_* properties or custom APIs, and they may be older than your tenant.`);
|
|
187
|
+
}
|
|
124
188
|
}
|
|
125
189
|
else {
|
|
126
|
-
documents =
|
|
127
|
-
if (documents.length === 0)
|
|
128
|
-
throw new Error('Unknown schema ID. Omit api to discover available APIs, or use schemas:"live" for tenant and custom APIs.');
|
|
190
|
+
documents = searchBundled();
|
|
129
191
|
}
|
|
130
192
|
const result = await runScapiCode({
|
|
131
193
|
code: input.code,
|
|
132
194
|
documents,
|
|
133
195
|
authType: input.authType,
|
|
196
|
+
detail: input.detail ?? 'outline',
|
|
134
197
|
signal: context?.signal,
|
|
135
198
|
timeoutMs: 10_000,
|
|
136
199
|
snippets: loadScapiSnippets(snippetDirectory),
|
|
137
200
|
});
|
|
138
|
-
return codeResult({
|
|
201
|
+
return codeResult({
|
|
202
|
+
result,
|
|
203
|
+
...(input.schemas === 'live' ? { schemaSource } : {}),
|
|
204
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
205
|
+
...(failures.length > 0 ? { schemaFailures: failures } : {}),
|
|
206
|
+
}, resolution);
|
|
139
207
|
}
|
|
140
208
|
catch (error) {
|
|
141
209
|
return failure(error, resolution);
|
|
@@ -173,6 +241,7 @@ export function createScapiCodeTools(loadServices, snippetDirectory, registry =
|
|
|
173
241
|
const services = await loadServices(input);
|
|
174
242
|
resolution = services.getResolution();
|
|
175
243
|
const config = services.getResolvedConfig();
|
|
244
|
+
const localDocuments = await projectSchemas(services, context?.signal);
|
|
176
245
|
const { shortCode, tenantId, siteId } = config.values;
|
|
177
246
|
const tenant = shortCode && tenantId ? scapiTenantKey(shortCode, toOrganizationId(tenantId)) : undefined;
|
|
178
247
|
const safetyEnvironment = Object.fromEntries(['SFCC_SAFETY_LEVEL', 'SFCC_SAFETY_CONFIRM', 'SFCC_SAFETY_CONFIG'].map((name) => [
|
|
@@ -198,10 +267,10 @@ export function createScapiCodeTools(loadServices, snippetDirectory, registry =
|
|
|
198
267
|
}),
|
|
199
268
|
// Live contracts discovered for this tenant replace bundled ones, and local ones replace both;
|
|
200
269
|
// the user owns their accuracy.
|
|
201
|
-
documents: mergeScapiSchemas(loadScapiSchemas(), tenant ? schemaCache.get(tenant) : [],
|
|
202
|
-
onSchema(document,
|
|
203
|
-
// Keep
|
|
204
|
-
if (tenant && (
|
|
270
|
+
documents: mergeScapiSchemas(loadScapiSchemas(), tenant ? schemaCache.get(tenant) : [], localDocuments),
|
|
271
|
+
onSchema(document, full) {
|
|
272
|
+
// Keep full tenant contracts (custom properties and prose) from being replaced by lighter fetches.
|
|
273
|
+
if (tenant && (full || document.entry.apiFamily === 'custom'))
|
|
205
274
|
schemaCache.put(tenant, document);
|
|
206
275
|
},
|
|
207
276
|
confirm: (request) => execution.confirm(request),
|
|
@@ -16,7 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
import { createToolAdapter, jsonResult } from '../adapter.js';
|
|
17
17
|
import { getApiErrorMessage } from '@salesforce/b2c-tooling-sdk/clients';
|
|
18
18
|
import { collapseOpenApiSchema } from '@salesforce/b2c-tooling-sdk/schemas';
|
|
19
|
-
import { createLiveScapiDocument, scapiTenantKey } from '@salesforce/b2c-tooling-sdk/scapi';
|
|
19
|
+
import { SCAPI_SCHEMA_EXPANSIONS, SCAPI_STANDARD_SCHEMA_EXPAND, createLiveScapiDocument, fetchScapiSchemaWithFallback, isFullScapiExpand, listScapiSchemasWithFallback, normalizeScapiSchemaExpand, scapiSchemaExpandFor, scapiTenantKey, } from '@salesforce/b2c-tooling-sdk/scapi';
|
|
20
20
|
/**
|
|
21
21
|
* Builds the base URL for a SCAPI API endpoint.
|
|
22
22
|
*
|
|
@@ -45,37 +45,67 @@ function getSchemasApiError(error, response) {
|
|
|
45
45
|
}
|
|
46
46
|
return message;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Smallest Schemas API `expand` for the request: collapsed output drops operation bodies, so it never needs
|
|
50
|
+
* prose; expandAll returns the whole contract. An explicit `include` wins.
|
|
51
|
+
*/
|
|
52
|
+
function resolveExpand(args) {
|
|
53
|
+
if (args.include?.length)
|
|
54
|
+
return normalizeScapiSchemaExpand(args.include);
|
|
55
|
+
const customProperties = args.expandCustomProperties !== false;
|
|
56
|
+
if (args.expandAll)
|
|
57
|
+
return customProperties ? scapiSchemaExpandFor({ full: true }) : SCAPI_STANDARD_SCHEMA_EXPAND;
|
|
58
|
+
return scapiSchemaExpandFor({ customProperties });
|
|
59
|
+
}
|
|
48
60
|
/**
|
|
49
61
|
* Fetches a specific schema from the SCAPI Schemas API.
|
|
50
62
|
*
|
|
51
63
|
* @param params - Fetch parameters
|
|
52
|
-
* @param params.
|
|
53
|
-
* @param params.organizationId - Organization ID
|
|
64
|
+
* @param params.svc - Services (client, organization and short code are resolved inside the live fetch)
|
|
54
65
|
* @param params.args - Input arguments with API identifiers
|
|
55
|
-
* @param params.shortCode - Optional short code for building base URL
|
|
56
66
|
* @param params.schemaCache - Optional live contract cache shared with SCAPI code mode
|
|
57
67
|
* @returns Schema fetch output
|
|
58
68
|
*/
|
|
59
69
|
async function fetchSpecificSchema(params) {
|
|
60
|
-
const {
|
|
70
|
+
const { svc, args, schemaCache } = params;
|
|
61
71
|
const { apiFamily, apiName, apiVersion, expandAll, status } = args;
|
|
72
|
+
const expand = resolveExpand(args);
|
|
62
73
|
// Warn if status filter was provided (it's ignored in fetch mode)
|
|
63
|
-
const
|
|
74
|
+
const statusWarning = status
|
|
64
75
|
? `Note: 'status' filter is ignored when fetching a specific schema. The API endpoint for retrieving a specific schema (${apiFamily}/${apiName}/${apiVersion}) does not support status filtering - you're already specifying the exact version. Use discovery mode (omit one or more of apiFamily/apiName/apiVersion) to filter by status.`
|
|
65
76
|
: undefined;
|
|
66
|
-
const {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
const identity = { apiFamily: apiFamily, apiName: apiName, apiVersion: apiVersion };
|
|
78
|
+
// Shared by the live fetch and the cache below; a missing shortCode or credentials throws inside the fetch.
|
|
79
|
+
let live;
|
|
80
|
+
const result = await fetchScapiSchemaWithFallback(identity, async () => {
|
|
81
|
+
const client = svc.getScapiSchemasClient();
|
|
82
|
+
const organizationId = svc.getOrganizationId();
|
|
83
|
+
let shortCode;
|
|
84
|
+
try {
|
|
85
|
+
shortCode = svc.getShortCode();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Continue without shortCode if not available
|
|
89
|
+
}
|
|
90
|
+
const { data, error, response } = await client.GET('/organizations/{organizationId}/schemas/{apiFamily}/{apiName}/{apiVersion}', {
|
|
91
|
+
params: {
|
|
92
|
+
path: { organizationId, ...identity },
|
|
93
|
+
// One comma-joined value: the API rejects a repeated expand parameter.
|
|
94
|
+
query: expand ? { expand } : undefined,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
if (error)
|
|
98
|
+
throw new Error(`Failed to fetch schema for ${identity.apiFamily}/${identity.apiName}/${identity.apiVersion}: ${getSchemasApiError(error, response)}`);
|
|
99
|
+
live = { shortCode, organizationId };
|
|
100
|
+
return data;
|
|
71
101
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
// Contracts carrying custom properties and prose become searchable and executable in SCAPI code mode.
|
|
103
|
+
if (result.source === 'live' &&
|
|
104
|
+
schemaCache &&
|
|
105
|
+
live?.shortCode &&
|
|
106
|
+
(isFullScapiExpand(expand) || apiFamily === 'custom')) {
|
|
77
107
|
try {
|
|
78
|
-
schemaCache.put(scapiTenantKey(shortCode, organizationId), createLiveScapiDocument(
|
|
108
|
+
schemaCache.put(scapiTenantKey(live.shortCode, live.organizationId), createLiveScapiDocument(identity, result.schema));
|
|
79
109
|
}
|
|
80
110
|
catch {
|
|
81
111
|
// Not a usable OpenAPI contract; still return it to the caller.
|
|
@@ -84,20 +114,21 @@ async function fetchSpecificSchema(params) {
|
|
|
84
114
|
// Apply collapsing unless expandAll is requested
|
|
85
115
|
const collapsed = !expandAll;
|
|
86
116
|
const processedSchema = collapsed
|
|
87
|
-
? collapseOpenApiSchema(
|
|
88
|
-
:
|
|
117
|
+
? collapseOpenApiSchema(result.schema)
|
|
118
|
+
: result.schema;
|
|
89
119
|
// Build base URL for the SCAPI API (where to call the API)
|
|
90
|
-
const baseUrl = shortCode
|
|
91
|
-
? buildScapiApiUrl(shortCode, apiFamily, apiName, apiVersion)
|
|
120
|
+
const baseUrl = live?.shortCode
|
|
121
|
+
? buildScapiApiUrl(live.shortCode, identity.apiFamily, identity.apiName, identity.apiVersion)
|
|
92
122
|
: undefined;
|
|
123
|
+
const warning = [statusWarning, result.warning].filter(Boolean).join(' ') || undefined;
|
|
93
124
|
return {
|
|
94
|
-
|
|
95
|
-
apiName: apiName,
|
|
96
|
-
apiVersion: apiVersion,
|
|
125
|
+
...identity,
|
|
97
126
|
schema: processedSchema,
|
|
98
127
|
timestamp: new Date().toISOString(),
|
|
99
128
|
collapsed,
|
|
100
129
|
warning,
|
|
130
|
+
source: result.source,
|
|
131
|
+
...(result.source === 'live' && expand ? { expand } : {}),
|
|
101
132
|
baseUrl,
|
|
102
133
|
};
|
|
103
134
|
}
|
|
@@ -105,39 +136,51 @@ async function fetchSpecificSchema(params) {
|
|
|
105
136
|
* Fetches and filters schemas list from the SCAPI Schemas API.
|
|
106
137
|
*
|
|
107
138
|
* @param params - Fetch parameters
|
|
108
|
-
* @param params.
|
|
109
|
-
* @param params.organizationId - Organization ID
|
|
139
|
+
* @param params.svc - Services (client, organization and short code are resolved inside the live fetch)
|
|
110
140
|
* @param params.args - Input parameters
|
|
111
|
-
* @param params.shortCode - Optional short code for building base URLs
|
|
112
141
|
* @returns Discovery mode output
|
|
113
142
|
*/
|
|
114
143
|
async function fetchSchemasList(params) {
|
|
115
|
-
const {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
const { svc, args } = params;
|
|
145
|
+
let shortCode;
|
|
146
|
+
const result = await listScapiSchemasWithFallback({ apiFamily: args.apiFamily, apiName: args.apiName, apiVersion: args.apiVersion, status: args.status }, async () => {
|
|
147
|
+
const client = svc.getScapiSchemasClient();
|
|
148
|
+
const organizationId = svc.getOrganizationId();
|
|
149
|
+
try {
|
|
150
|
+
shortCode = svc.getShortCode();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Continue without shortCode if not available
|
|
154
|
+
}
|
|
155
|
+
const { data, error, response } = await client.GET('/organizations/{organizationId}/schemas', {
|
|
156
|
+
params: {
|
|
157
|
+
path: { organizationId },
|
|
158
|
+
query: {
|
|
159
|
+
apiFamily: args.apiFamily,
|
|
160
|
+
apiName: args.apiName,
|
|
161
|
+
apiVersion: args.apiVersion,
|
|
162
|
+
status: args.status,
|
|
163
|
+
},
|
|
124
164
|
},
|
|
125
|
-
}
|
|
165
|
+
});
|
|
166
|
+
if (error)
|
|
167
|
+
throw new Error(`Failed to fetch SCAPI schemas: ${getSchemasApiError(error, response)}`);
|
|
168
|
+
const schemas = data?.data ?? [];
|
|
169
|
+
return { schemas, total: data?.total ?? schemas.length };
|
|
126
170
|
});
|
|
127
|
-
|
|
128
|
-
throw new Error(`Failed to fetch SCAPI schemas: ${getSchemasApiError(error, response)}`);
|
|
129
|
-
}
|
|
130
|
-
const schemas = data?.data ?? [];
|
|
171
|
+
const { schemas } = result;
|
|
131
172
|
const filteredSchemas = prepareSchemaListForConsumer(schemas, shortCode);
|
|
132
173
|
const discoveryMetadata = getAvailableFilters(schemas);
|
|
133
174
|
// Generate helpful message for empty results
|
|
134
175
|
const message = schemas.length === 0 ? generateEmptyResultMessage(args) : undefined;
|
|
135
176
|
return {
|
|
136
177
|
schemas: filteredSchemas,
|
|
137
|
-
total:
|
|
178
|
+
total: result.total,
|
|
138
179
|
timestamp: new Date().toISOString(),
|
|
139
180
|
...discoveryMetadata,
|
|
140
181
|
message,
|
|
182
|
+
source: result.source,
|
|
183
|
+
warning: result.warning,
|
|
141
184
|
};
|
|
142
185
|
}
|
|
143
186
|
/**
|
|
@@ -234,19 +277,12 @@ export function createScapiSchemasListTool(loadServices, schemaCache) {
|
|
|
234
277
|
.boolean()
|
|
235
278
|
.default(true)
|
|
236
279
|
.describe('Include tenant custom property definitions when fetching a schema.'),
|
|
280
|
+
include: z
|
|
281
|
+
.array(z.enum(SCAPI_SCHEMA_EXPANSIONS))
|
|
282
|
+
.optional()
|
|
283
|
+
.describe('Schemas API expand sections (e.g. summaries, examples, all). Overrides expandAll/expandCustomProperties.'),
|
|
237
284
|
},
|
|
238
285
|
async execute(args, { services: svc }) {
|
|
239
|
-
// Get client and organization ID
|
|
240
|
-
const client = svc.getScapiSchemasClient();
|
|
241
|
-
const organizationId = svc.getOrganizationId();
|
|
242
|
-
// Get shortCode for building base URLs (optional)
|
|
243
|
-
let shortCode;
|
|
244
|
-
try {
|
|
245
|
-
shortCode = svc.getShortCode();
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// Continue without shortCode if not available
|
|
249
|
-
}
|
|
250
286
|
// Determine operation mode
|
|
251
287
|
const hasAllIdentifiers = Boolean(args.apiFamily && args.apiName && args.apiVersion);
|
|
252
288
|
const isFetchMode = hasAllIdentifiers && args.includeSchemas;
|
|
@@ -255,11 +291,12 @@ export function createScapiSchemasListTool(loadServices, schemaCache) {
|
|
|
255
291
|
throw new Error('includeSchemas=true requires all three identifiers: apiFamily, apiName, and apiVersion. ' +
|
|
256
292
|
'Please provide all three to fetch a specific schema, or omit includeSchemas to discover available schemas.');
|
|
257
293
|
}
|
|
258
|
-
// Execute appropriate mode
|
|
294
|
+
// Execute appropriate mode. Live access is attempted inside, so missing configuration, credentials or
|
|
295
|
+
// network falls back to the bundled corpus with a warning.
|
|
259
296
|
if (isFetchMode) {
|
|
260
|
-
return fetchSpecificSchema({
|
|
297
|
+
return fetchSpecificSchema({ svc, args, schemaCache });
|
|
261
298
|
}
|
|
262
|
-
return fetchSchemasList({
|
|
299
|
+
return fetchSchemasList({ svc, args });
|
|
263
300
|
},
|
|
264
301
|
formatOutput: (output) => jsonResult(output),
|
|
265
302
|
}, loadServices);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @salesforce/b2c-agent-plugins
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#747](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/747) [`f3b9974`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/f3b997487f2094a1c9f1c7a846147e303c2db422) - New `b2c-edge-traffic-triage` operations runbook for reviewing bot and abusive eCDN traffic against your firewall, WAF and rate-limiting rules, and SCAPI code mode now points agents to the operations runbooks. (Thanks [@clavery](https://github.com/clavery)!)
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#744](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/744) [`3964752`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/3964752a63665b1b6005be14a47f70ccd80349da) - Fixed the search-index job examples (`{"site_scope":["Site"]}`, not an object), documented system-job request bodies for CLI and MCP code mode, and stopped `--api-backend auto` from retrying SCAPI 400 (invalid request) errors over OCAPI, which hid the real error. When a fallback does happen and OCAPI also fails, both errors are reported. (Thanks [@clavery](https://github.com/clavery)!)
|
|
12
|
+
|
|
13
|
+
- [#741](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/741) [`c45c5fa`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/c45c5fac903c5bf77eecfc4a4befd259712aa84e) - The `b2c-scapi-schemas` skill explains how to fetch operation descriptions and examples and how the bundled fallback behaves. (Thanks [@clavery](https://github.com/clavery)!)
|
|
14
|
+
|
|
3
15
|
## 1.11.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|