@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.
Files changed (47) hide show
  1. package/content/guidance/b2c-cli/b2c-config/SKILL.md +23 -4
  2. package/content/guidance/b2c-cli/b2c-ecdn/SKILL.md +2 -0
  3. package/content/guidance/b2c-cli/b2c-job/SKILL.md +9 -3
  4. package/content/guidance/b2c-cli/b2c-job/references/RUN-AND-MONITOR.md +35 -6
  5. package/content/guidance/b2c-cli/b2c-scapi-schemas/SKILL.md +5 -1
  6. package/content/guidance/b2c-ops/b2c-edge-traffic-triage/SKILL.md +69 -0
  7. package/content/guidance/b2c-ops/b2c-job-health/SKILL.md +11 -6
  8. package/content/guidance/b2c-ops/b2c-production-triage/SKILL.md +1 -0
  9. package/content/guidance/index.json +67 -22
  10. package/content/guidance/mcp/b2c-mcp-scapi/SKILL.md +50 -226
  11. package/content/guidance/mcp/b2c-mcp-scapi/references/authentication.md +34 -0
  12. package/content/guidance/mcp/b2c-mcp-scapi/references/compose-verify.md +25 -0
  13. package/content/guidance/mcp/b2c-mcp-scapi/references/confirmation.md +30 -0
  14. package/content/guidance/mcp/b2c-mcp-scapi/references/discover.md +94 -0
  15. package/content/guidance/mcp/b2c-mcp-scapi/references/jobs.md +42 -0
  16. package/content/guidance/mcp/b2c-mcp-scapi/references/workflows.md +28 -0
  17. package/content/guidance/mcp/b2c-mcp-server/SKILL.md +4 -1
  18. package/dist/commands/mcp.js +7 -7
  19. package/dist/tools/docs/docs-read.js +4 -2
  20. package/dist/tools/docs/docs-search.js +10 -8
  21. package/dist/tools/docs/topics.d.ts +5 -0
  22. package/dist/tools/docs/topics.js +5 -0
  23. package/dist/tools/scapi/index.d.ts +1 -1
  24. package/dist/tools/scapi/index.js +2 -2
  25. package/dist/tools/scapi/scapi-code.js +96 -27
  26. package/dist/tools/scapi/scapi-schemas-list.js +93 -56
  27. package/node_modules/@salesforce/b2c-agent-plugins/CHANGELOG.md +12 -0
  28. package/node_modules/@salesforce/b2c-agent-plugins/b2c/.codex-plugin/plugin.json +1 -1
  29. package/node_modules/@salesforce/b2c-agent-plugins/b2c/plugin.json +1 -1
  30. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/.codex-plugin/plugin.json +1 -1
  31. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/plugin.json +1 -1
  32. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-config/SKILL.md +23 -4
  33. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-ecdn/SKILL.md +2 -0
  34. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/SKILL.md +9 -3
  35. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/references/RUN-AND-MONITOR.md +35 -6
  36. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-scapi-schemas/SKILL.md +5 -1
  37. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/.codex-plugin/plugin.json +4 -3
  38. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/README.md +1 -0
  39. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/plugin.json +4 -3
  40. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-edge-traffic-triage/SKILL.md +69 -0
  41. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-job-health/SKILL.md +11 -6
  42. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/skills/b2c-production-triage/SKILL.md +1 -0
  43. package/node_modules/@salesforce/b2c-agent-plugins/package.json +1 -1
  44. package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/.codex-plugin/plugin.json +1 -1
  45. package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/plugin.json +1 -1
  46. package/oclif.manifest.json +2 -2
  47. package/package.json +4 -4
@@ -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 { loadLocalScapiSchemas } from '@salesforce/b2c-tooling-sdk/scapi';
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 directories). ' +
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 = loadLocalScapiSchemas(paths);
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
- * only the triage-critical fields (id, title, category, summary, score) and drop
17
- * `keywords` (index-tuning metadata) and `url` (derivable / returned on read),
18
- * which together roughly double the payload. `verbose` restores them.
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 canonical url on each result (larger payload). Defaults to false.'),
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 - Developer-supplied contracts that override bundled and live ones in code mode
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 - Developer-supplied contracts that override bundled and live ones in code mode
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\`, 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.`;
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 (--scapi-schemas) replace bundled and live ones with the same id.
50
- const local = (api) => localSchemas.filter((document) => !api || document.entry.id === api);
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.string().optional().describe('Limit schemas to family/name/version. Omit for all APIs.'),
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
- const services = await loadServices(input);
134
+ services = await loadServices(input);
111
135
  resolution = services.getResolution();
112
- // Throws with configuration guidance when shortCode, tenantId, or OAuth credentials are missing.
113
- const client = services.getScapiSchemasClient();
114
- const organizationId = services.getOrganizationId();
115
- const tenant = scapiTenantKey(services.getShortCode(), organizationId);
116
- ({ documents, failures } = await schemaCache.load(tenant, client, organizationId, {
117
- api: input.api,
118
- refresh: input.refresh,
119
- signal: context?.signal,
120
- }));
121
- documents = mergeScapiSchemas([], documents, local(input.api));
122
- if (documents.length === 0 && failures.length === 0)
123
- throw new Error('Unknown schema ID for this tenant. Omit api to discover available APIs.');
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 = mergeScapiSchemas(loadScapiSchemas(), [], local()).filter((document) => !input.api || document.entry.id === input.api);
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({ result, ...(failures.length > 0 ? { schemaFailures: failures } : {}) }, resolution);
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) : [], localSchemas),
202
- onSchema(document, customProperties) {
203
- // Keep expanded tenant contracts from being replaced by unexpanded fetches.
204
- if (tenant && (customProperties || document.entry.apiFamily === 'custom'))
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.client - SCAPI Schemas client
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 { client, organizationId, args, shortCode, schemaCache } = params;
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 warning = status
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 { data, error, response } = await client.GET('/organizations/{organizationId}/schemas/{apiFamily}/{apiName}/{apiVersion}', {
67
- params: {
68
- path: { organizationId, apiFamily: apiFamily, apiName: apiName, apiVersion: apiVersion },
69
- query: args.expandCustomProperties === false ? undefined : { expand: 'custom_properties' },
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
- if (error) {
73
- throw new Error(`Failed to fetch schema for ${apiFamily}/${apiName}/${apiVersion}: ${getSchemasApiError(error, response)}`);
74
- }
75
- // Expanded tenant contracts become searchable and executable in SCAPI code mode.
76
- if (schemaCache && shortCode && (args.expandCustomProperties !== false || apiFamily === 'custom')) {
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({ apiFamily: apiFamily, apiName: apiName, apiVersion: apiVersion }, data));
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(data)
88
- : data;
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 && apiFamily && apiName && apiVersion
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
- apiFamily: apiFamily,
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.client - SCAPI Schemas client
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 { client, organizationId, args, shortCode } = params;
116
- const { data, error, response } = await client.GET('/organizations/{organizationId}/schemas', {
117
- params: {
118
- path: { organizationId },
119
- query: {
120
- apiFamily: args.apiFamily,
121
- apiName: args.apiName,
122
- apiVersion: args.apiVersion,
123
- status: args.status,
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
- if (error) {
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: data?.total ?? schemas.length,
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({ client, organizationId, args, shortCode, schemaCache });
297
+ return fetchSpecificSchema({ svc, args, schemaCache });
261
298
  }
262
- return fetchSchemasList({ client, organizationId, args, shortCode });
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