@salesforce/b2c-dx-mcp 1.4.1 → 1.6.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.
@@ -21,6 +21,7 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
21
21
  static flags: {
22
22
  toolsets: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
23
  tools: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ 'docs-topics': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
25
  'allow-non-ga-tools': import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
26
  server: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
27
  'webdav-server': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -34,7 +35,7 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
34
35
  'client-id': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
35
36
  'client-secret': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
37
  'auth-scope': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
- 'short-code': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
38
+ 'short-code': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
38
39
  'tenant-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
40
  'auth-methods': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
41
  'user-auth': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -193,6 +193,12 @@ export default class McpServerCommand extends BaseCommand {
193
193
  env: 'SFCC_TOOLS',
194
194
  parse: async (input) => input.toLowerCase(),
195
195
  }),
196
+ 'docs-topics': Flags.string({
197
+ description: 'Limit the documentation exposed by the docs tools to these categories (comma-separated allowlist). ' +
198
+ 'Options: script-api, job-step, commerce-api, pwa-kit-managed-runtime, sfnext, sfra, b2c-commerce, tooling. ' +
199
+ 'Bounds the whole docs corpus; per-call category/storefront narrow within it. Unknown names are ignored.',
200
+ env: 'SFCC_DOCS_TOPICS',
201
+ }),
196
202
  // Feature flags
197
203
  'allow-non-ga-tools': Flags.boolean({
198
204
  description: 'Enable non-GA (experimental) tools',
@@ -302,6 +308,8 @@ export default class McpServerCommand extends BaseCommand {
302
308
  configPath: this.flags.config,
303
309
  // Project directory for auto-discovery. oclif handles flag with env fallback.
304
310
  projectDirectory: this.flags['project-directory'],
311
+ // Docs topic allowlist (bounds the docs corpus at startup).
312
+ docsTopics: this.flags['docs-topics'],
305
313
  };
306
314
  // Add toolsets to telemetry attributes
307
315
  if (this.telemetry && startupFlags.toolsets) {
@@ -1,3 +1,5 @@
1
+ import { type ProjectType } from '@salesforce/b2c-tooling-sdk/discovery';
2
+ import { type DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
1
3
  import type { McpTool, Toolset, StartupFlags } from './utils/index.js';
2
4
  import type { B2CDxMcpServer } from './server.js';
3
5
  import type { Services } from './services.js';
@@ -15,5 +17,5 @@ export type ToolRegistry = Record<Toolset, McpTool[]>;
15
17
  * @param loadServices - Function that loads configuration and returns Services instance
16
18
  * @returns Complete tool registry
17
19
  */
18
- export declare function createToolRegistry(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): ToolRegistry;
20
+ export declare function createToolRegistry(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext, detectedWorkspaces?: readonly ProjectType[], enabledDocCategories?: readonly DocCategory[]): ToolRegistry;
19
21
  export declare function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): Promise<void>;
package/dist/registry.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { getLogger } from '@salesforce/b2c-tooling-sdk/logging';
7
7
  import { detectWorkspaceType } from '@salesforce/b2c-tooling-sdk/discovery';
8
+ import { DOC_CATEGORIES, resolveEnabledCategories } from '@salesforce/b2c-tooling-sdk/docs';
8
9
  import { ALL_TOOLSETS, DEPRECATED_TOOLSETS, TOOLSETS, VALID_TOOLSET_NAMES } from './utils/index.js';
9
10
  import { createCartridgesTools } from './tools/cartridges/index.js';
10
11
  import { createDiagnosticsTools } from './tools/diagnostics/index.js';
@@ -26,6 +27,9 @@ const BASE_TOOLSETS = ['SCAPI', 'DIAGNOSTICS'];
26
27
  */
27
28
  const PROJECT_TYPE_TOOLSETS = {
28
29
  cartridges: ['CARTRIDGES'],
30
+ // SFRA is a cartridge project, so it enables the same toolset as `cartridges`.
31
+ // (A SFRA workspace also matches the `cartridges` marker; the union dedupes.)
32
+ sfra: ['CARTRIDGES'],
29
33
  'pwa-kit-v3': ['PWAV3', 'MRT'],
30
34
  // Note: STOREFRONTNEXT_DEPRECATED is intentionally NOT auto-activated. The
31
35
  // legacy sfnext_* tools are superseded by the storefront-next agent-skills
@@ -70,7 +74,7 @@ function getToolsetsForProjectTypes(projectTypes) {
70
74
  * @param loadServices - Function that loads configuration and returns Services instance
71
75
  * @returns Complete tool registry
72
76
  */
73
- export function createToolRegistry(loadServices, serverContext) {
77
+ export function createToolRegistry(loadServices, serverContext, detectedWorkspaces = [], enabledDocCategories) {
74
78
  const registry = {
75
79
  CARTRIDGES: [],
76
80
  DIAGNOSTICS: [],
@@ -84,7 +88,7 @@ export function createToolRegistry(loadServices, serverContext) {
84
88
  const allTools = [
85
89
  ...createCartridgesTools(loadServices),
86
90
  ...createDiagnosticsTools(loadServices, serverContext),
87
- ...createDocsTools(loadServices),
91
+ ...createDocsTools(loadServices, { detectedWorkspaces, enabledCategories: enabledDocCategories }),
88
92
  ...createMrtTools(loadServices),
89
93
  ...createPwav3Tools(loadServices),
90
94
  ...createScapiTools(loadServices),
@@ -106,7 +110,7 @@ export function createToolRegistry(loadServices, serverContext) {
106
110
  * @param reason - Reason for triggering auto-discovery (for logging)
107
111
  * @returns Array of toolsets to enable
108
112
  */
109
- async function performAutoDiscovery(flags, reason) {
113
+ async function detectProjectTypes(flags) {
110
114
  const logger = getLogger();
111
115
  // Project directory from --project-directory flag or SFCC_PROJECT_DIRECTORY env var
112
116
  const projectDirectory = flags.projectDirectory ?? process.cwd();
@@ -117,16 +121,8 @@ async function performAutoDiscovery(flags, reason) {
117
121
  'Set --project-directory or SFCC_PROJECT_DIRECTORY for reliable auto-discovery.');
118
122
  }
119
123
  const detectionResult = await detectWorkspaceType(projectDirectory);
120
- // Map all detected project types to MCP toolsets (union)
121
- // Note: getToolsetsForProjectTypes always includes BASE_TOOLSET
122
- const mappedToolsets = getToolsetsForProjectTypes(detectionResult.projectTypes);
123
- logger.info({
124
- reason,
125
- projectTypes: detectionResult.projectTypes,
126
- matchedPatterns: detectionResult.matchedPatterns,
127
- enabledToolsets: mappedToolsets,
128
- }, `Auto-discovery (${reason}): project types: ${detectionResult.projectTypes.join(', ') || 'none'}`);
129
- return mappedToolsets;
124
+ logger.info({ projectTypes: detectionResult.projectTypes, matchedPatterns: detectionResult.matchedPatterns }, `Workspace detection: project types: ${detectionResult.projectTypes.join(', ') || 'none'}`);
125
+ return detectionResult.projectTypes;
130
126
  }
131
127
  /**
132
128
  * Register tools with the MCP server based on startup flags.
@@ -160,8 +156,18 @@ export async function registerToolsets(flags, server, loadServices, serverContex
160
156
  const individualTools = flags.tools ?? [];
161
157
  const allowNonGaTools = flags.allowNonGaTools ?? false;
162
158
  const logger = getLogger();
159
+ // Detect the workspace type once up front. It informs both the
160
+ // docs tools (favoring the current workspace's docs and surfacing it in the
161
+ // tool descriptions) and toolset auto-discovery below, so we only hit the
162
+ // filesystem a single time.
163
+ const detectedWorkspaces = await detectProjectTypes(flags);
164
+ // Resolve the launch-time docs topic allowlist (bounds the whole docs corpus).
165
+ const enabledDocCategories = resolveEnabledCategories(flags.docsTopics, (invalid) => logger.warn({ invalidTopics: invalid, validTopics: DOC_CATEGORIES }, `Ignoring unknown documentation topic(s) in --docs-topics: "${invalid.join('", "')}"`));
166
+ if (enabledDocCategories) {
167
+ logger.info({ docsTopics: enabledDocCategories }, `Documentation restricted to topics: ${enabledDocCategories.join(', ')}`);
168
+ }
163
169
  // Create the tool registry (all available tools)
164
- const toolRegistry = createToolRegistry(loadServices, serverContext);
170
+ const toolRegistry = createToolRegistry(loadServices, serverContext, detectedWorkspaces, enabledDocCategories);
165
171
  // Build flat list of all tools for lookup
166
172
  const allTools = Object.values(toolRegistry).flat();
167
173
  const allToolsByName = new Map(allTools.map((tool) => [tool.name, tool]));
@@ -184,12 +190,13 @@ export async function registerToolsets(flags, server, loadServices, serverContex
184
190
  const validToolsets = toolsets.filter((t) => TOOLSETS.includes(t));
185
191
  const allNonDeprecatedToolsets = TOOLSETS.filter((t) => !DEPRECATED_TOOLSETS.includes(t));
186
192
  const toolsetsToEnable = new Set(toolsets.includes(ALL_TOOLSETS) ? allNonDeprecatedToolsets : validToolsets);
187
- // Auto-discovery: If no valid toolsets AND no valid individual tools, detect workspace type.
188
- // This handles both: (1) no flags provided, and (2) all provided flags are invalid.
189
- // Auto-discovery enables appropriate toolsets based on workspace type,
190
- // or at minimum the BASE_TOOLSETS if no project types are detected.
193
+ // Auto-discovery: If no valid toolsets AND no valid individual tools, map the
194
+ // already-detected workspace type(s) to toolsets. This handles both: (1) no
195
+ // flags provided, and (2) all provided flags are invalid. Always enables at
196
+ // least the BASE_TOOLSETS even if no project types are detected.
191
197
  if (toolsetsToEnable.size === 0 && validIndividualTools.length === 0) {
192
- const discoveredToolsets = await performAutoDiscovery(flags, 'no valid toolsets or tools');
198
+ const discoveredToolsets = getToolsetsForProjectTypes(detectedWorkspaces);
199
+ logger.info({ projectTypes: detectedWorkspaces, enabledToolsets: discoveredToolsets }, `Auto-discovery: enabling toolsets ${discoveredToolsets.join(', ')}`);
193
200
  for (const toolset of discoveredToolsets) {
194
201
  toolsetsToEnable.add(toolset);
195
202
  }
package/dist/server.js CHANGED
@@ -48,11 +48,14 @@ export class B2CDxMcpServer extends McpServer {
48
48
  try {
49
49
  const result = await handler(args);
50
50
  const runTimeMs = Date.now() - startTime;
51
+ // Extract error message from CallToolResult content when isError is true
52
+ const errorMessage = result.isError ? result.content?.find((c) => c.type === 'text')?.text : undefined;
51
53
  await this.telemetry
52
54
  ?.sendEventAndFlush('TOOL_CALLED', {
53
55
  toolName: name,
54
56
  runTimeMs,
55
57
  isError: result.isError ?? false,
58
+ ...(errorMessage ? { errorMessage } : {}),
56
59
  })
57
60
  .catch((error_) => {
58
61
  getLogger().debug({ err: error_, toolName: name }, '[mcp] telemetry sendEventAndFlush failed');
@@ -66,6 +69,8 @@ export class B2CDxMcpServer extends McpServer {
66
69
  toolName: name,
67
70
  runTimeMs,
68
71
  isError: true,
72
+ errorMessage: error instanceof Error ? error.message : String(error),
73
+ ...(error instanceof Error && error.cause ? { errorCause: String(error.cause) } : {}),
69
74
  })
70
75
  .catch((error_) => {
71
76
  getLogger().debug({ err: error_, toolName: name }, '[mcp] telemetry sendEventAndFlush failed');
@@ -40,7 +40,7 @@ import fs from 'node:fs';
40
40
  import type { B2CInstance } from '@salesforce/b2c-tooling-sdk';
41
41
  import type { AuthStrategy } from '@salesforce/b2c-tooling-sdk/auth';
42
42
  import type { ResolvedB2CConfig } from '@salesforce/b2c-tooling-sdk/config';
43
- import { WebDavClient, type CustomApisClient, type ScapiSchemasClient } from '@salesforce/b2c-tooling-sdk/clients';
43
+ import { WebDavClient, type CustomApisClient, type MetricsClient, type ScapiSchemasClient } from '@salesforce/b2c-tooling-sdk/clients';
44
44
  /**
45
45
  * MRT (Managed Runtime) configuration.
46
46
  * Groups auth, project, environment, and origin settings.
@@ -147,6 +147,14 @@ export declare class Services {
147
147
  * Get the user's home directory.
148
148
  */
149
149
  getHomeDir(): string;
150
+ /**
151
+ * Get Metrics client for accessing SCAPI observability metrics.
152
+ * Requires shortCode, tenantId, and OAuth credentials to be configured.
153
+ *
154
+ * @throws Error if shortCode, tenantId, or OAuth credentials are missing
155
+ * @returns Typed Metrics client
156
+ */
157
+ getMetricsClient(): MetricsClient;
150
158
  /**
151
159
  * Get organization ID for SCAPI API calls.
152
160
  * Ensures the tenant ID has the required f_ecom_ prefix.
package/dist/services.js CHANGED
@@ -44,7 +44,7 @@
44
44
  import fs from 'node:fs';
45
45
  import path from 'node:path';
46
46
  import os from 'node:os';
47
- import { createCustomApisClient, createScapiSchemasClient, toOrganizationId, WebDavClient, } from '@salesforce/b2c-tooling-sdk/clients';
47
+ import { createCustomApisClient, createMetricsClient, createScapiSchemasClient, toOrganizationId, WebDavClient, } from '@salesforce/b2c-tooling-sdk/clients';
48
48
  /**
49
49
  * Services class that provides utilities for MCP tools.
50
50
  *
@@ -167,6 +167,25 @@ export class Services {
167
167
  getHomeDir() {
168
168
  return os.homedir();
169
169
  }
170
+ /**
171
+ * Get Metrics client for accessing SCAPI observability metrics.
172
+ * Requires shortCode, tenantId, and OAuth credentials to be configured.
173
+ *
174
+ * @throws Error if shortCode, tenantId, or OAuth credentials are missing
175
+ * @returns Typed Metrics client
176
+ */
177
+ getMetricsClient() {
178
+ const { shortCode, tenantId } = this.resolvedConfig.values;
179
+ if (!shortCode) {
180
+ throw new Error('SCAPI short code required. Provide --short-code, set SFCC_SHORTCODE, or configure short-code in dw.json.');
181
+ }
182
+ if (!tenantId) {
183
+ throw new Error('Tenant ID required. Provide --tenant-id, set SFCC_TENANT_ID, or configure tenant-id in dw.json.');
184
+ }
185
+ // This will throw if OAuth credentials are missing
186
+ const oauthStrategy = this.getOAuthStrategy();
187
+ return createMetricsClient({ shortCode, tenantId }, oauthStrategy);
188
+ }
170
189
  /**
171
190
  * Get organization ID for SCAPI API calls.
172
191
  * Ensures the tenant ID has the required f_ecom_ prefix.
@@ -9,6 +9,9 @@ import { projectFrame, projectVariable, resolveBreakpointPath, } from '@salesfor
9
9
  import { getRegistry, getSessionEntry } from './session-registry.js';
10
10
  const DEFAULT_TIMEOUT_MS = 30_000;
11
11
  const MAX_TIMEOUT_MS = 120_000;
12
+ const TIMEOUT_HINT = 'Breakpoint not hit. First confirm the triggered request actually exercised this code path and the breakpoint line is reachable. ' +
13
+ 'In some production instance group configurations (which can run multiple app servers — this never applies to sandboxes), a breakpoint may be missed because the request landed on a different app server than the one holding the debug session. ' +
14
+ 'Only in that specific case, retrieve session_cookie from debug_list_sessions and resend the triggering request with that cookie (Cookie: <name>=<value>) to pin it to the app server holding the session.';
12
15
  export function createDebugCaptureAtBreakpointTool(loadServices, serverContext) {
13
16
  return createToolAdapter({
14
17
  name: 'debug_capture_at_breakpoint',
@@ -73,6 +76,7 @@ export function createDebugCaptureAtBreakpointTool(loadServices, serverContext)
73
76
  halted: false,
74
77
  timed_out: true,
75
78
  auto_continued: false,
79
+ hint: TIMEOUT_HINT,
76
80
  };
77
81
  }
78
82
  const threadDetail = await entry.manager.client.getThread(thread.id);
@@ -9,8 +9,8 @@ import { getRegistry } from './session-registry.js';
9
9
  export function createDebugListSessionsTool(loadServices, serverContext) {
10
10
  return createToolAdapter({
11
11
  name: 'debug_list_sessions',
12
- description: 'List active script debugger sessions with their breakpoints, any halted threads, and the session_cookie (e.g. dwsid). ' +
13
- 'Use this to discover orphaned sessions, check whether breakpoints are armed, poll for halted threads in the non-blocking debug workflow, and retrieve the session cookie to pin a triggering request (Cookie: <name>=<value>) to the app server holding the session.',
12
+ description: 'List active script debugger sessions with their breakpoints and any halted threads. ' +
13
+ 'Use this to discover orphaned sessions, check whether breakpoints are armed, and poll for halted threads in the non-blocking debug workflow.',
14
14
  toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
15
15
  inputSchema: {},
16
16
  async execute(_args, context) {
@@ -13,9 +13,7 @@ export function createDebugStartSessionTool(loadServices, serverContext) {
13
13
  name: 'debug_start_session',
14
14
  description: 'Start a script debugger session on a B2C Commerce instance to debug SFRA controllers, custom API scripts, hooks, jobs, or any server-side script. ' +
15
15
  'Returns a session_id for use with other debug tools, plus discovered cartridge mappings. ' +
16
- 'Also returns session_cookie (e.g. dwsid): to reliably hit a breakpoint on a multi-app-server instance, send the request that triggers your code (storefront page load, SCAPI/OCAPI call) with this cookie (Cookie: <name>=<value>) so it lands on the same app server holding the debug session. ' +
17
- 'WARNING: Debug sessions halt remote request threads on the instance. Always call debug_end_session when finished. ' +
18
- 'Requires Basic auth credentials (username/password) and the script debugger enabled in Business Manager.',
16
+ 'WARNING: Debug sessions halt remote request threads on the instance. Always call debug_end_session when finished.',
19
17
  toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
20
18
  inputSchema: {
21
19
  cartridge_directory: z
@@ -31,8 +29,8 @@ export function createDebugStartSessionTool(loadServices, serverContext) {
31
29
  const registry = getRegistry(context);
32
30
  const credentials = context.services.getBasicAuthCredentials();
33
31
  if (!credentials) {
34
- throw new Error('Basic auth credentials (hostname, username, password) are required for the script debugger. ' +
35
- 'Set via SFCC_SERVER/SFCC_USERNAME/SFCC_PASSWORD env vars, or configure in dw.json.');
32
+ throw new Error('Basic auth credentials (username/password) are required for the script debugger. ' +
33
+ 'Set via SFCC_SERVER/SFCC_USERNAME/SFCC_PASSWORD env vars, or dw.json.');
36
34
  }
37
35
  const { hostname, username, password } = credentials;
38
36
  const clientId = args.client_id ?? 'b2c-cli';
@@ -9,6 +9,9 @@ import { projectThreadLocation } from '@salesforce/b2c-tooling-sdk/operations/de
9
9
  import { getRegistry, getSessionEntry } from './session-registry.js';
10
10
  const DEFAULT_TIMEOUT_MS = 30_000;
11
11
  const MAX_TIMEOUT_MS = 120_000;
12
+ const TIMEOUT_HINT = 'Breakpoint not hit. First confirm the request actually exercised this code path and the breakpoint line is reachable. ' +
13
+ 'In some production instance group configurations (which can run multiple app servers — this never applies to sandboxes), a breakpoint may be missed because the request landed on a different app server than the one holding the debug session. ' +
14
+ 'Only in that specific case, retrieve session_cookie from debug_list_sessions and resend the triggering request with that cookie (Cookie: <name>=<value>) to pin it to the app server holding the session.';
12
15
  export function createDebugWaitForStopTool(loadServices, serverContext) {
13
16
  return createToolAdapter({
14
17
  name: 'debug_wait_for_stop',
@@ -31,7 +34,7 @@ export function createDebugWaitForStopTool(loadServices, serverContext) {
31
34
  const timeout = Math.min(args.timeout_ms ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
32
35
  const thread = await getRegistry(context).waitForHalt(entry, timeout);
33
36
  if (!thread)
34
- return { halted: false, timed_out: true };
37
+ return { halted: false, timed_out: true, hint: TIMEOUT_HINT };
35
38
  const location = projectThreadLocation(thread, entry.sourceMapper);
36
39
  return {
37
40
  halted: true,
@@ -1,3 +1,5 @@
1
+ import { type DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
2
+ import type { ProjectType } from '@salesforce/b2c-tooling-sdk/discovery';
1
3
  import type { McpTool } from '../../utils/index.js';
2
4
  import type { Services } from '../../services.js';
3
- export declare function createDocsListTool(loadServices: () => Promise<Services> | Services): McpTool;
5
+ export declare function createDocsListTool(loadServices: () => Promise<Services> | Services, detectedWorkspaces?: readonly ProjectType[], enabledCategories?: readonly DocCategory[]): McpTool;
@@ -3,18 +3,78 @@
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 { listDocs } from '@salesforce/b2c-tooling-sdk/docs';
6
+ import { z } from 'zod';
7
+ import { listDocs, categoriesForWorkspace } from '@salesforce/b2c-tooling-sdk/docs';
7
8
  import { createToolAdapter, jsonResult } from '../adapter.js';
8
- export function createDocsListTool(loadServices) {
9
+ import { categoryEnumValues, enabledCategoriesNote } from './topics.js';
10
+ import { WORKSPACE_VALUES, detectedWorkspaceNote, resolveWorkspace } from './storefront.js';
11
+ /** Default page size for a filtered listing. Bounds payload size (large corpora hold 500+ entries). */
12
+ const DEFAULT_LIMIT = 100;
13
+ /** Projects a full entry to the table-of-contents shape. */
14
+ function toListEntry(entry) {
15
+ return { id: entry.id, title: entry.title, category: entry.category };
16
+ }
17
+ export function createDocsListTool(loadServices, detectedWorkspaces = [], enabledCategories) {
9
18
  return createToolAdapter({
10
19
  name: 'docs_list',
11
- description: 'List every available Script API documentation entry (id + title). ' +
12
- 'Output is large; prefer docs_search for targeted lookups.',
20
+ description: 'Enumerate B2C Commerce documentation entries (id + title + category only) for a category or workspace. ' +
21
+ 'Prefer docs_search for questions this tool is for browsing a known category. Without a category or ' +
22
+ 'workspace it returns just a category directory (counts), not the full corpus. Results are a table of ' +
23
+ 'contents; paginated via limit/offset. Use docs_read for content.' +
24
+ enabledCategoriesNote(enabledCategories) +
25
+ detectedWorkspaceNote(detectedWorkspaces),
13
26
  toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'MRT', 'PWAV3', 'SCAPI', 'STOREFRONTNEXT'],
14
- inputSchema: {},
15
- async execute() {
16
- const entries = listDocs();
17
- return { count: entries.length, entries };
27
+ inputSchema: {
28
+ category: z
29
+ .enum(categoryEnumValues(enabledCategories))
30
+ .optional()
31
+ .describe('Restrict the listing to one documentation category.'),
32
+ workspace: z
33
+ .enum(WORKSPACE_VALUES)
34
+ .optional()
35
+ .describe('Limit to a workspace\'s relevant categories. "auto" uses the auto-detected workspace; ' +
36
+ 'or name a type. Omit for the category directory.'),
37
+ limit: z.number().int().positive().optional().describe(`Max entries per page. Defaults to ${DEFAULT_LIMIT}.`),
38
+ offset: z.number().int().nonnegative().optional().describe('Number of entries to skip (for pagination).'),
39
+ },
40
+ async execute(args) {
41
+ // Explicit category wins; otherwise a workspace narrows to its relevant categories.
42
+ const workspace = resolveWorkspace(args.workspace, detectedWorkspaces);
43
+ const filter = args.category ??
44
+ (args.workspace && args.workspace !== 'all' && workspace ? categoriesForWorkspace(workspace) : undefined);
45
+ // No filter at all → return a compact directory of categories + counts,
46
+ // never the whole corpus (which would blow the inline payload budget).
47
+ // The directory itself is bounded by the launch-time allowlist.
48
+ if (!filter) {
49
+ const counts = new Map();
50
+ for (const e of listDocs(undefined, enabledCategories)) {
51
+ if (e.category)
52
+ counts.set(e.category, (counts.get(e.category) ?? 0) + 1);
53
+ }
54
+ const categories = [...counts.entries()]
55
+ .map(([category, count]) => ({ category, count }))
56
+ .sort((a, b) => b.count - a.count);
57
+ const total = categories.reduce((sum, c) => sum + c.count, 0);
58
+ return {
59
+ note: 'Pass a category (or workspace) to list its entries, or use docs_search for a query.',
60
+ total,
61
+ categories,
62
+ };
63
+ }
64
+ const all = listDocs(filter, enabledCategories);
65
+ const offset = args.offset ?? 0;
66
+ const limit = args.limit ?? DEFAULT_LIMIT;
67
+ const page = all.slice(offset, offset + limit).map((e) => toListEntry(e));
68
+ const end = offset + page.length;
69
+ const truncated = end < all.length;
70
+ return {
71
+ category: filter,
72
+ total: all.length,
73
+ offset,
74
+ count: page.length,
75
+ entries: page,
76
+ ...(truncated && { truncated: true, nextOffset: end }),
77
+ };
18
78
  },
19
79
  formatOutput: (output) => jsonResult(output),
20
80
  }, loadServices);
@@ -1,3 +1,4 @@
1
+ import { type DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
1
2
  import type { McpTool } from '../../utils/index.js';
2
3
  import type { Services } from '../../services.js';
3
- export declare function createDocsReadTool(loadServices: () => Promise<Services> | Services): McpTool;
4
+ export declare function createDocsReadTool(loadServices: () => Promise<Services> | Services, enabledCategories?: readonly DocCategory[]): McpTool;
@@ -6,18 +6,56 @@
6
6
  import { z } from 'zod';
7
7
  import { readDocByQuery } from '@salesforce/b2c-tooling-sdk/docs';
8
8
  import { createToolAdapter, errorResult, jsonResult } from '../adapter.js';
9
- export function createDocsReadTool(loadServices) {
9
+ import { enabledCategoriesNote } from './topics.js';
10
+ /** Default maximum characters of content returned per read call, to bound the inline payload. */
11
+ const DEFAULT_MAX_LENGTH = 12_000;
12
+ export function createDocsReadTool(loadServices, enabledCategories) {
10
13
  return createToolAdapter({
11
14
  name: 'docs_read',
12
- description: 'Read full Script API documentation (markdown) for a B2C Commerce class or module. ' +
13
- 'Accepts an exact id (e.g., "dw.catalog.ProductMgr") or fuzzy query — best match wins. ' +
14
- 'Content can be large; if you do not know the id, call docs_search first to narrow down.',
15
+ description: 'Read B2C Commerce documentation (markdown) for a class, module, job step, or guide. ' +
16
+ 'Accepts an exact id (e.g. "dw.catalog.ProductMgr", "sfnext/sfnext-get-started") or a fuzzy ' +
17
+ 'query best match wins. Script API / job-step content is bundled; Developer Center guide ' +
18
+ 'content is fetched from its published URL on demand (with a summary/headings fallback if the ' +
19
+ 'network is unavailable). If you do not know the id, call docs_search first. Long docs are ' +
20
+ 'truncated to maxLength chars; page with offset when truncated=true. The returned entry ' +
21
+ 'includes the canonical url for citation.' +
22
+ enabledCategoriesNote(enabledCategories),
15
23
  toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'MRT', 'PWAV3', 'SCAPI', 'STOREFRONTNEXT'],
16
24
  inputSchema: {
17
- query: z.string().min(1).describe('Exact id ("dw.catalog.ProductMgr") or fuzzy query ("ProductMgr").'),
25
+ query: z
26
+ .string()
27
+ .min(1)
28
+ .describe('Exact id ("dw.catalog.ProductMgr", "sfnext/sfnext-get-started") or fuzzy query.'),
29
+ offset: z
30
+ .number()
31
+ .int()
32
+ .nonnegative()
33
+ .optional()
34
+ .describe('Character offset to start reading from (for paging long docs). Defaults to 0.'),
35
+ maxLength: z
36
+ .number()
37
+ .int()
38
+ .positive()
39
+ .optional()
40
+ .describe(`Maximum characters of content to return. Defaults to ${DEFAULT_MAX_LENGTH}.`),
18
41
  },
19
42
  async execute(args) {
20
- return readDocByQuery(args.query);
43
+ const found = await readDocByQuery(args.query, { enabledCategories });
44
+ if (!found)
45
+ return null;
46
+ const totalLength = found.content.length;
47
+ const offset = Math.min(args.offset ?? 0, totalLength);
48
+ const maxLength = args.maxLength ?? DEFAULT_MAX_LENGTH;
49
+ const slice = found.content.slice(offset, offset + maxLength);
50
+ const end = offset + slice.length;
51
+ const truncated = end < totalLength;
52
+ return {
53
+ entry: found.entry,
54
+ content: slice,
55
+ totalLength,
56
+ offset,
57
+ ...(truncated && { truncated: true, nextOffset: end }),
58
+ };
21
59
  },
22
60
  formatOutput: (output) => output ? jsonResult(output) : errorResult('No documentation found. Try docs_search to find candidates.'),
23
61
  }, loadServices);
@@ -1,3 +1,5 @@
1
+ import { type DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
2
+ import type { ProjectType } from '@salesforce/b2c-tooling-sdk/discovery';
1
3
  import type { McpTool } from '../../utils/index.js';
2
4
  import type { Services } from '../../services.js';
3
- export declare function createDocsSearchTool(loadServices: () => Promise<Services> | Services): McpTool;
5
+ export declare function createDocsSearchTool(loadServices: () => Promise<Services> | Services, detectedWorkspaces?: readonly ProjectType[], enabledCategories?: readonly DocCategory[]): McpTool;
@@ -6,20 +6,81 @@
6
6
  import { z } from 'zod';
7
7
  import { searchDocs } from '@salesforce/b2c-tooling-sdk/docs';
8
8
  import { createToolAdapter, jsonResult } from '../adapter.js';
9
- export function createDocsSearchTool(loadServices) {
9
+ import { categoryEnumValues, enabledCategoriesNote } from './topics.js';
10
+ import { WORKSPACE_VALUES, detectedWorkspaceNote, resolveWorkspace } from './storefront.js';
11
+ /** Default number of results returned when `limit` is not supplied. Kept small to bound payload size for agents. */
12
+ const DEFAULT_LIMIT = 5;
13
+ /**
14
+ * Projects a search hit to the payload returned to an agent. By default we keep
15
+ * only the triage-critical fields (id, title, category, summary, score) and drop
16
+ * `keywords` (index-tuning metadata) and `url` (derivable / returned on read),
17
+ * which together roughly double the payload. `verbose` restores them.
18
+ */
19
+ function leanResult(entry, score, verbose) {
20
+ const base = {
21
+ id: entry.id,
22
+ title: entry.title,
23
+ category: entry.category,
24
+ score,
25
+ };
26
+ if (entry.summary)
27
+ base.summary = entry.summary;
28
+ if (verbose) {
29
+ if (entry.keywords && entry.keywords.length > 0)
30
+ base.keywords = entry.keywords;
31
+ if (entry.url)
32
+ base.url = entry.url;
33
+ if (entry.sourceUrl)
34
+ base.sourceUrl = entry.sourceUrl;
35
+ }
36
+ return base;
37
+ }
38
+ export function createDocsSearchTool(loadServices, detectedWorkspaces = [], enabledCategories) {
10
39
  return createToolAdapter({
11
40
  name: 'docs_search',
12
- description: 'Fuzzy-search the bundled B2C Commerce Script API documentation by class, module, or partial name ' +
13
- '(e.g., "ProductMgr", "dw.catalog", "Status"). Returns matching ids + relevance score. ' +
14
- 'Use this BEFORE docs_read when you do not know the exact id.',
41
+ description: 'PRIMARY entry point for B2C Commerce docs: Script API reference (e.g. "ProductMgr"), standard job steps, ' +
42
+ 'Developer Center guides (commerce-api, pwa-kit-managed-runtime, sfnext, sfra, b2c-commerce), and this ' +
43
+ "tooling's own guides. Use for ANY B2C Commerce developer or admin question not already grounded in a " +
44
+ 'loaded skill or the current project. Content-aware ranking — pass a natural-language query (prefer this ' +
45
+ 'over docs_list, which only enumerates). Optionally restrict by category or workspace. Returns id, title, ' +
46
+ 'category, summary, and score for triage; pass verbose=true for keywords+url. Call this BEFORE docs_read ' +
47
+ 'when you do not know the exact id.' +
48
+ enabledCategoriesNote(enabledCategories) +
49
+ detectedWorkspaceNote(detectedWorkspaces),
15
50
  toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'MRT', 'PWAV3', 'SCAPI', 'STOREFRONTNEXT'],
16
51
  inputSchema: {
17
- query: z.string().min(1).describe('Search query (class name, module path, or partial match).'),
18
- limit: z.number().int().positive().optional().describe('Maximum number of results to return. Defaults to 20.'),
52
+ query: z.string().min(1).describe('Search query (class name, topic, or natural-language phrase).'),
53
+ category: z.enum(categoryEnumValues(enabledCategories)).optional().describe('Restrict results to one corpus.'),
54
+ workspace: z
55
+ .enum(WORKSPACE_VALUES)
56
+ .optional()
57
+ .describe('Workspace context. "auto" (default) favors the auto-detected workspace\'s docs; ' +
58
+ '"all" disables the preference; or name a type (cartridges, sfra, pwa-kit-v3, storefront-next).'),
59
+ limit: z
60
+ .number()
61
+ .int()
62
+ .positive()
63
+ .optional()
64
+ .describe(`Maximum number of results to return. Defaults to ${DEFAULT_LIMIT}.`),
65
+ verbose: z
66
+ .boolean()
67
+ .optional()
68
+ .describe('Include keywords and canonical url on each result (larger payload). Defaults to false.'),
19
69
  },
20
70
  async execute(args) {
21
- const results = searchDocs(args.query, args.limit ?? 20);
22
- return { query: args.query, results };
71
+ const workspace = resolveWorkspace(args.workspace, detectedWorkspaces);
72
+ const results = searchDocs(args.query, {
73
+ limit: args.limit ?? DEFAULT_LIMIT,
74
+ category: args.category,
75
+ workspace,
76
+ enabledCategories,
77
+ });
78
+ return {
79
+ query: args.query,
80
+ ...(args.category && { category: args.category }),
81
+ ...(workspace && { workspace }),
82
+ results: results.map((r) => leanResult(r.entry, r.score, args.verbose ?? false)),
83
+ };
23
84
  },
24
85
  formatOutput: (output) => jsonResult(output),
25
86
  }, loadServices);
@@ -1,3 +1,25 @@
1
+ import type { DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
2
+ import type { ProjectType } from '@salesforce/b2c-tooling-sdk/discovery';
1
3
  import type { McpTool } from '../../utils/index.js';
2
4
  import type { Services } from '../../services.js';
3
- export declare function createDocsTools(loadServices: () => Promise<Services> | Services): McpTool[];
5
+ /**
6
+ * Server-startup context baked into the documentation tools.
7
+ */
8
+ export interface DocsToolContext {
9
+ /**
10
+ * Workspace type(s) detected at startup. Baked into the search/list
11
+ * tool descriptions and used as the default workspace context so results
12
+ * favor the current workspace.
13
+ */
14
+ detectedWorkspaces?: readonly ProjectType[];
15
+ /**
16
+ * A launch-time allowlist of documentation categories (from `--docs-topics`).
17
+ * When set, the docs tools expose ONLY these categories — a hard boundary the
18
+ * per-call `category`/`workspace` narrowing operates within.
19
+ */
20
+ enabledCategories?: readonly DocCategory[];
21
+ }
22
+ /**
23
+ * Builds the documentation tools with the given server-startup context.
24
+ */
25
+ export declare function createDocsTools(loadServices: () => Promise<Services> | Services, context?: DocsToolContext): McpTool[];
@@ -9,11 +9,15 @@ import { createDocsSchemaListTool } from './docs-schema-list.js';
9
9
  import { createDocsSchemaReadTool } from './docs-schema-read.js';
10
10
  import { createDocsSchemaSearchTool } from './docs-schema-search.js';
11
11
  import { createDocsSearchTool } from './docs-search.js';
12
- export function createDocsTools(loadServices) {
12
+ /**
13
+ * Builds the documentation tools with the given server-startup context.
14
+ */
15
+ export function createDocsTools(loadServices, context = {}) {
16
+ const { detectedWorkspaces = [], enabledCategories } = context;
13
17
  return [
14
- createDocsSearchTool(loadServices),
15
- createDocsReadTool(loadServices),
16
- createDocsListTool(loadServices),
18
+ createDocsSearchTool(loadServices, detectedWorkspaces, enabledCategories),
19
+ createDocsReadTool(loadServices, enabledCategories),
20
+ createDocsListTool(loadServices, detectedWorkspaces, enabledCategories),
17
21
  createDocsSchemaSearchTool(loadServices),
18
22
  createDocsSchemaReadTool(loadServices),
19
23
  createDocsSchemaListTool(loadServices),
@@ -0,0 +1,27 @@
1
+ import type { ProjectType } from '@salesforce/b2c-tooling-sdk/discovery';
2
+ /**
3
+ * Human-readable label for each detected workspace/project type, used in the
4
+ * docs tool descriptions so an agent can see what was auto-detected.
5
+ */
6
+ export declare const PROJECT_TYPE_LABELS: Record<ProjectType, string>;
7
+ /**
8
+ * Accepted values for the docs tools' `workspace` parameter. Mirrors the CLI
9
+ * `--workspace` vocabulary: `auto` uses the auto-detected workspace, `all`
10
+ * disables the preference, or name a concrete type.
11
+ */
12
+ export declare const WORKSPACE_VALUES: readonly ["auto", "all", "cartridges", "sfra", "pwa-kit-v3", "storefront-next"];
13
+ export type WorkspaceParam = (typeof WORKSPACE_VALUES)[number];
14
+ /**
15
+ * Resolves the `workspace` tool parameter into the concrete project type(s) to
16
+ * pass to the SDK search, given what was auto-detected at server startup.
17
+ *
18
+ * - `all` → undefined (no workspace preference)
19
+ * - `auto` (or unset) → the detected workspace(s), if any
20
+ * - an explicit type → that type
21
+ */
22
+ export declare function resolveWorkspace(param: undefined | WorkspaceParam, detected: readonly ProjectType[]): ProjectType[] | undefined;
23
+ /**
24
+ * Builds a short sentence describing the detected workspace for a tool
25
+ * description, or an empty string when nothing was detected.
26
+ */
27
+ export declare function detectedWorkspaceNote(detected: readonly ProjectType[]): string;
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ /**
7
+ * Human-readable label for each detected workspace/project type, used in the
8
+ * docs tool descriptions so an agent can see what was auto-detected.
9
+ */
10
+ export const PROJECT_TYPE_LABELS = {
11
+ cartridges: 'Cartridges',
12
+ sfra: 'SFRA (cartridges)',
13
+ 'pwa-kit-v3': 'PWA Kit (Composable Storefront)',
14
+ 'storefront-next': 'Storefront Next',
15
+ };
16
+ /**
17
+ * Accepted values for the docs tools' `workspace` parameter. Mirrors the CLI
18
+ * `--workspace` vocabulary: `auto` uses the auto-detected workspace, `all`
19
+ * disables the preference, or name a concrete type.
20
+ */
21
+ export const WORKSPACE_VALUES = ['auto', 'all', 'cartridges', 'sfra', 'pwa-kit-v3', 'storefront-next'];
22
+ /**
23
+ * Resolves the `workspace` tool parameter into the concrete project type(s) to
24
+ * pass to the SDK search, given what was auto-detected at server startup.
25
+ *
26
+ * - `all` → undefined (no workspace preference)
27
+ * - `auto` (or unset) → the detected workspace(s), if any
28
+ * - an explicit type → that type
29
+ */
30
+ export function resolveWorkspace(param, detected) {
31
+ if (param === 'all')
32
+ return undefined;
33
+ if (param && param !== 'auto')
34
+ return [param];
35
+ // 'auto' or unset: default to the detected workspace(s)
36
+ return detected.length > 0 ? [...detected] : undefined;
37
+ }
38
+ /**
39
+ * Builds a short sentence describing the detected workspace for a tool
40
+ * description, or an empty string when nothing was detected.
41
+ */
42
+ export function detectedWorkspaceNote(detected) {
43
+ if (detected.length === 0)
44
+ return '';
45
+ const labels = detected.map((t) => PROJECT_TYPE_LABELS[t] ?? t).join(' + ');
46
+ return ` Detected workspace: ${labels} — by default results favor this workspace's docs (pass workspace="all" to disable).`;
47
+ }
48
+ //# sourceMappingURL=storefront.js.map
@@ -0,0 +1,21 @@
1
+ import { type DocCategory } from '@salesforce/b2c-tooling-sdk/docs';
2
+ /**
3
+ * Helpers for the launch-time documentation "topics" allowlist (`--docs-topics`),
4
+ * shared across the docs search/list/read tools so the enabled-category boundary
5
+ * is described and enforced consistently.
6
+ */
7
+ /**
8
+ * The category values a docs tool's `category` parameter should accept, given
9
+ * the optional launch-time allowlist. When an allowlist is configured the enum
10
+ * is narrowed to it (so the schema itself reflects what is reachable); otherwise
11
+ * every category is accepted.
12
+ *
13
+ * Always returns a non-empty tuple so `z.enum(...)` is happy — an allowlist that
14
+ * somehow filtered to nothing falls back to the full set.
15
+ */
16
+ export declare function categoryEnumValues(enabledCategories?: readonly DocCategory[]): [DocCategory, ...DocCategory[]];
17
+ /**
18
+ * A sentence for a tool description noting the corpus is restricted to the
19
+ * configured topics, or an empty string when there is no restriction.
20
+ */
21
+ export declare function enabledCategoriesNote(enabledCategories?: readonly DocCategory[]): string;
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { DOC_CATEGORIES } from '@salesforce/b2c-tooling-sdk/docs';
7
+ /**
8
+ * Helpers for the launch-time documentation "topics" allowlist (`--docs-topics`),
9
+ * shared across the docs search/list/read tools so the enabled-category boundary
10
+ * is described and enforced consistently.
11
+ */
12
+ /**
13
+ * The category values a docs tool's `category` parameter should accept, given
14
+ * the optional launch-time allowlist. When an allowlist is configured the enum
15
+ * is narrowed to it (so the schema itself reflects what is reachable); otherwise
16
+ * every category is accepted.
17
+ *
18
+ * Always returns a non-empty tuple so `z.enum(...)` is happy — an allowlist that
19
+ * somehow filtered to nothing falls back to the full set.
20
+ */
21
+ export function categoryEnumValues(enabledCategories) {
22
+ const all = [...DOC_CATEGORIES];
23
+ if (!enabledCategories || enabledCategories.length === 0) {
24
+ return all;
25
+ }
26
+ const allowed = all.filter((c) => enabledCategories.includes(c));
27
+ return (allowed.length > 0 ? allowed : all);
28
+ }
29
+ /**
30
+ * A sentence for a tool description noting the corpus is restricted to the
31
+ * configured topics, or an empty string when there is no restriction.
32
+ */
33
+ export function enabledCategoriesNote(enabledCategories) {
34
+ if (!enabledCategories || enabledCategories.length === 0)
35
+ return '';
36
+ return ` Documentation is restricted at startup to: ${enabledCategories.join(', ')}.`;
37
+ }
38
+ //# sourceMappingURL=topics.js.map
@@ -3,9 +3,10 @@
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 { createScapiSchemasListTool } from './scapi-schemas-list.js';
7
- import { createScapiCustomApisStatusTool } from './scapi-custom-apis-get-status.js';
6
+ import { createMetricsGetTool } from './metrics-get.js';
8
7
  import { createScaffoldCustomApiTool } from './scapi-custom-api-generate-scaffold.js';
8
+ import { createScapiCustomApisStatusTool } from './scapi-custom-apis-get-status.js';
9
+ import { createScapiSchemasListTool } from './scapi-schemas-list.js';
9
10
  /**
10
11
  * Creates all tools for the SCAPI toolset.
11
12
  *
@@ -14,9 +15,10 @@ import { createScaffoldCustomApiTool } from './scapi-custom-api-generate-scaffol
14
15
  */
15
16
  export function createScapiTools(loadServices) {
16
17
  return [
17
- createScapiSchemasListTool(loadServices),
18
- createScapiCustomApisStatusTool(loadServices),
18
+ createMetricsGetTool(loadServices),
19
19
  createScaffoldCustomApiTool(loadServices),
20
+ createScapiCustomApisStatusTool(loadServices),
21
+ createScapiSchemasListTool(loadServices),
20
22
  ];
21
23
  }
22
24
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ import type { Services } from '../../services.js';
2
+ import type { McpTool } from '../../utils/index.js';
3
+ /**
4
+ * Creates the metrics_get tool.
5
+ *
6
+ * Retrieves observability metrics time-series for a B2C Commerce tenant by category:
7
+ * - **overall**: Aggregate site metrics (requests, response times, errors)
8
+ * - **sales**: Sales and order metrics
9
+ * - **ecdn**: Edge CDN performance metrics
10
+ * - **third-party**: External service integration metrics (filter by thirdPartyServiceId)
11
+ * - **scapi**: SCAPI endpoint metrics (filter by apiFamily, apiName)
12
+ * - **scapi-hooks**: SCAPI hooks execution metrics
13
+ * - **mrt**: Managed Runtime (PWA Kit) metrics
14
+ * - **controller**: Controller execution metrics
15
+ * - **ocapi**: OCAPI endpoint metrics (filter by ocapiCategory, ocapiApi)
16
+ *
17
+ * Returns time-series data with metric metadata (title, description, unit) and data points
18
+ * (timestamp, value) grouped by series (e.g., 2xx, 4xx, 5xx response codes).
19
+ *
20
+ * **Requirements:** OAuth with `sfcc.metrics` scope.
21
+ *
22
+ * @param loadServices - Function that loads configuration and returns Services instance
23
+ * @returns MCP tool for retrieving metrics
24
+ */
25
+ export declare function createMetricsGetTool(loadServices: () => Promise<Services> | Services): McpTool;
@@ -0,0 +1,144 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ /**
7
+ * Metrics Get tool.
8
+ *
9
+ * Retrieves observability metrics time-series data for B2C Commerce tenants via SCAPI
10
+ * observability/metrics/v1. Returns metrics data grouped by category (overall, sales, ecdn,
11
+ * third-party, scapi, scapi-hooks, mrt, controller, ocapi).
12
+ *
13
+ * @module tools/scapi/metrics-get
14
+ */
15
+ import { z } from 'zod';
16
+ import { createToolAdapter, jsonResult } from '../adapter.js';
17
+ import { getMetricsByCategory, resolveMetricsWindow, enrichMetricsTags, } from '@salesforce/b2c-tooling-sdk';
18
+ /**
19
+ * Creates the metrics_get tool.
20
+ *
21
+ * Retrieves observability metrics time-series for a B2C Commerce tenant by category:
22
+ * - **overall**: Aggregate site metrics (requests, response times, errors)
23
+ * - **sales**: Sales and order metrics
24
+ * - **ecdn**: Edge CDN performance metrics
25
+ * - **third-party**: External service integration metrics (filter by thirdPartyServiceId)
26
+ * - **scapi**: SCAPI endpoint metrics (filter by apiFamily, apiName)
27
+ * - **scapi-hooks**: SCAPI hooks execution metrics
28
+ * - **mrt**: Managed Runtime (PWA Kit) metrics
29
+ * - **controller**: Controller execution metrics
30
+ * - **ocapi**: OCAPI endpoint metrics (filter by ocapiCategory, ocapiApi)
31
+ *
32
+ * Returns time-series data with metric metadata (title, description, unit) and data points
33
+ * (timestamp, value) grouped by series (e.g., 2xx, 4xx, 5xx response codes).
34
+ *
35
+ * **Requirements:** OAuth with `sfcc.metrics` scope.
36
+ *
37
+ * @param loadServices - Function that loads configuration and returns Services instance
38
+ * @returns MCP tool for retrieving metrics
39
+ */
40
+ export function createMetricsGetTool(loadServices) {
41
+ return createToolAdapter({
42
+ name: 'metrics_get',
43
+ description: `CLOSED BETA: the Metrics API must be enabled for your organization, and its behavior, output, and OAuth scopes may change without notice.
44
+
45
+ Retrieve observability metrics time-series for a B2C Commerce tenant. Returns metrics data grouped by category with time-series data points.
46
+
47
+ **Categories:**
48
+ - overall: Aggregate site metrics (requests, response times, errors)
49
+ - sales: Sales and order metrics
50
+ - ecdn: Edge CDN performance metrics
51
+ - third-party: External service metrics (use thirdPartyServiceId filter)
52
+ - scapi: SCAPI endpoint metrics (use apiFamily/apiName filters)
53
+ - scapi-hooks: SCAPI hooks execution metrics
54
+ - mrt: Managed Runtime (PWA Kit) metrics
55
+ - controller: Controller execution metrics
56
+ - ocapi: OCAPI endpoint metrics (use ocapiCategory/ocapiApi filters)
57
+
58
+ **Time window:** Provide "from" and/or "to" as a relative duration ("1h", "7d" — interpreted as ago) or an ISO 8601 timestamp, and/or "window" as a duration ("1h", "30m"). The tool always sends an explicit from+to range, defaulting to a 24-hour window: from + window → to = from + window; to + window → from = to - window; window alone → the last <window>; from alone → 24h forward from it (capped at now); to alone → 24h back from it; nothing → the last 24h. Do not supply from, to, and window together. The API caps a window at 24h and retains 30 days; an explicit range wider than 24h is sent as-is and the API returns a clear error.
59
+
60
+ **Response:** { query, data } — "query" echoes the resolved from/to (ISO + epoch seconds), filters, and defaultedWindow/clampedFrom flags; "data[]" contains metricId, title, description, unit, and dataSeries[] with time-series points (timestamp in epoch milliseconds, value). Each series also carries a structured "tags" object (realm, environment, any applied filters, and per-series dimensions like apiFamily/host/cacheStatus) parsed client-side from the packed series id — use these to group/filter rather than parsing the series id string.
61
+
62
+ **Requirements:** OAuth with sfcc.metrics scope.`,
63
+ toolsets: ['SCAPI'],
64
+ isGA: false,
65
+ requiresInstance: false, // SCAPI uses OAuth directly
66
+ inputSchema: {
67
+ category: z
68
+ .enum(['overall', 'sales', 'ecdn', 'third-party', 'scapi', 'scapi-hooks', 'mrt', 'controller', 'ocapi'])
69
+ .describe('Metrics category: overall (aggregate), sales, ecdn (CDN), third-party (external), scapi (SCAPI APIs), scapi-hooks, mrt (PWA Kit), controller, ocapi'),
70
+ from: z
71
+ .string()
72
+ .optional()
73
+ .describe('Start bound: relative ("1h", "7d" ago) or ISO 8601. Alone → a 24h window forward (capped at now).'),
74
+ to: z
75
+ .string()
76
+ .optional()
77
+ .describe('End bound: relative ("6h" ago) or ISO 8601. Alone → a 24h window back from it.'),
78
+ window: z
79
+ .string()
80
+ .optional()
81
+ .describe('Window duration ("1h", "30m", "2d"). With from → to=from+window; with to → from=to-window; alone → the last <window>. Defaults to 24h.'),
82
+ thirdPartyServiceId: z
83
+ .string()
84
+ .optional()
85
+ .describe('Filter by third-party service ID (third-party category only)'),
86
+ apiFamily: z.string().optional().describe('Filter by SCAPI API family (scapi category only)'),
87
+ apiName: z.string().optional().describe('Filter by SCAPI API name (scapi category only)'),
88
+ ocapiCategory: z.string().optional().describe('Filter by OCAPI category (ocapi category only)'),
89
+ ocapiApi: z.string().optional().describe('Filter by OCAPI API (ocapi category only)'),
90
+ },
91
+ async execute(args, { services: svc }) {
92
+ // Get client and tenant ID
93
+ const client = svc.getMetricsClient();
94
+ const tenantId = svc.getTenantId();
95
+ if (!tenantId) {
96
+ throw new Error('Tenant ID required. Provide --tenant-id, set SFCC_TENANT_ID, or configure tenant-id in dw.json.');
97
+ }
98
+ // Resolve the requested bounds into an explicit from+to range, filling any
99
+ // open bound from the 24-hour default window (the API caps a window at 24h
100
+ // and pairs a missing `to` with its own `now`). Throws a clear error on
101
+ // unparseable/over-specified input before the request.
102
+ const window = resolveMetricsWindow({ from: args.from, to: args.to, window: args.window });
103
+ const raw = await getMetricsByCategory(client, tenantId, args.category, {
104
+ from: window.from,
105
+ to: window.to,
106
+ thirdPartyServiceId: args.thirdPartyServiceId,
107
+ apiFamily: args.apiFamily,
108
+ apiName: args.apiName,
109
+ ocapiCategory: args.ocapiCategory,
110
+ ocapiApi: args.ocapiApi,
111
+ });
112
+ // Enrich each series with a structured `tags` object (realm/environment
113
+ // from the request, applied filters, and per-series dimensions parsed
114
+ // from the packed id). Additive and always-on for this machine consumer.
115
+ const response = enrichMetricsTags(raw, args.category, {
116
+ tenantId,
117
+ apiFamily: args.apiFamily,
118
+ apiName: args.apiName,
119
+ ocapiCategory: args.ocapiCategory,
120
+ ocapiApi: args.ocapiApi,
121
+ thirdPartyServiceId: args.thirdPartyServiceId,
122
+ });
123
+ return {
124
+ ...response,
125
+ query: {
126
+ category: args.category,
127
+ from: window.fromIso,
128
+ to: window.toIso,
129
+ fromEpochSeconds: window.fromEpochSeconds,
130
+ toEpochSeconds: window.toEpochSeconds,
131
+ clampedFrom: window.clampedFrom || undefined,
132
+ defaultedWindow: window.defaultedWindow || undefined,
133
+ thirdPartyServiceId: args.thirdPartyServiceId,
134
+ apiFamily: args.apiFamily,
135
+ apiName: args.apiName,
136
+ ocapiCategory: args.ocapiCategory,
137
+ ocapiApi: args.ocapiApi,
138
+ },
139
+ };
140
+ },
141
+ formatOutput: (output) => jsonResult(output),
142
+ }, loadServices);
143
+ }
144
+ //# sourceMappingURL=metrics-get.js.map
@@ -42,4 +42,10 @@ export interface StartupFlags {
42
42
  configPath?: string;
43
43
  /** Project project directory for tools (auto-discovery, scaffolding, etc.) */
44
44
  projectDirectory?: string;
45
+ /**
46
+ * Comma-separated allowlist of documentation categories the docs tools may
47
+ * expose (from `--docs-topics` / `SFCC_DOCS_TOPICS`). Bounds the whole docs
48
+ * corpus at startup; unset means all categories.
49
+ */
50
+ docsTopics?: string;
45
51
  }
@@ -402,6 +402,14 @@
402
402
  "multiple": false,
403
403
  "type": "option"
404
404
  },
405
+ "docs-topics": {
406
+ "description": "Limit the documentation exposed by the docs tools to these categories (comma-separated allowlist). Options: script-api, job-step, commerce-api, pwa-kit-managed-runtime, sfnext, sfra, b2c-commerce, tooling. Bounds the whole docs corpus; per-call category/storefront narrow within it. Unknown names are ignored.",
407
+ "env": "SFCC_DOCS_TOPICS",
408
+ "name": "docs-topics",
409
+ "hasDynamicHelp": false,
410
+ "multiple": false,
411
+ "type": "option"
412
+ },
405
413
  "allow-non-ga-tools": {
406
414
  "description": "Enable non-GA (experimental) tools",
407
415
  "env": "SFCC_ALLOW_NON_GA_TOOLS",
@@ -420,5 +428,5 @@
420
428
  "enableJsonFlag": false
421
429
  }
422
430
  },
423
- "version": "1.4.1"
431
+ "version": "1.6.0"
424
432
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@salesforce/b2c-dx-mcp",
3
3
  "description": "MCP server for B2C Commerce developer experience tools",
4
- "version": "1.4.1",
4
+ "version": "1.6.0",
5
5
  "author": "Salesforce",
6
6
  "license": "Apache-2.0",
7
7
  "repository": "SalesforceCommerceCloud/b2c-developer-tooling",
@@ -80,7 +80,7 @@
80
80
  "yaml": "2.9.0",
81
81
  "postcss": "8.5.15",
82
82
  "zod": "3.25.76",
83
- "@salesforce/b2c-tooling-sdk": "1.17.0"
83
+ "@salesforce/b2c-tooling-sdk": "1.19.0"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@eslint/compat": "^1",
@@ -123,10 +123,10 @@
123
123
  "inspect": "mcp-inspector node bin/run.js --toolsets all --allow-non-ga-tools",
124
124
  "inspect:dev": "mcp-inspector node --conditions development bin/dev.js --toolsets all --allow-non-ga-tools",
125
125
  "pretest": "tsc --noEmit -p test",
126
- "test": "c8 mocha --forbid-only --ignore 'test/e2e/**' \"test/**/*.test.ts\"",
127
- "test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore 'test/e2e/**' \"test/**/*.test.ts\"",
128
- "test:ci:win": "c8 --check-coverage=false mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore 'test/e2e/**' \"test/**/*.test.ts\"",
129
- "test:agent": "mocha --forbid-only --reporter min --ignore 'test/e2e/**' \"test/**/*.test.ts\"",
126
+ "test": "c8 mocha --forbid-only --ignore \"test/e2e/**\" \"test/**/*.test.ts\"",
127
+ "test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore \"test/e2e/**\" \"test/**/*.test.ts\"",
128
+ "test:ci:win": "c8 --check-coverage=false mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore \"test/e2e/**\" \"test/**/*.test.ts\"",
129
+ "test:agent": "mocha --forbid-only --reporter min --ignore \"test/e2e/**\" \"test/**/*.test.ts\"",
130
130
  "test:e2e": "mocha --forbid-only \"test/e2e/**/*.test.ts\"",
131
131
  "test:e2e:ci": "mocha --forbid-only --reporter json --reporter-option output=test-results-e2e.json \"test/e2e/**/*.test.ts\"",
132
132
  "coverage": "c8 report",