@salesforce/b2c-dx-mcp 1.0.14 → 1.1.1

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/mcp.d.ts +5 -0
  3. package/dist/commands/mcp.js +16 -6
  4. package/dist/registry.d.ts +3 -21
  5. package/dist/registry.js +13 -3
  6. package/dist/server-context.d.ts +27 -0
  7. package/dist/server-context.js +37 -0
  8. package/dist/server.js +7 -2
  9. package/dist/services.d.ts +9 -0
  10. package/dist/services.js +10 -0
  11. package/dist/tools/adapter.d.ts +7 -1
  12. package/dist/tools/adapter.js +2 -1
  13. package/dist/tools/diagnostics/debug-capture-at-breakpoint.d.ts +4 -0
  14. package/dist/tools/diagnostics/debug-capture-at-breakpoint.js +118 -0
  15. package/dist/tools/diagnostics/debug-continue.d.ts +4 -0
  16. package/dist/tools/diagnostics/debug-continue.js +27 -0
  17. package/dist/tools/diagnostics/debug-end-session.d.ts +4 -0
  18. package/dist/tools/diagnostics/debug-end-session.js +38 -0
  19. package/dist/tools/diagnostics/debug-evaluate.d.ts +4 -0
  20. package/dist/tools/diagnostics/debug-evaluate.js +30 -0
  21. package/dist/tools/diagnostics/debug-get-stack.d.ts +4 -0
  22. package/dist/tools/diagnostics/debug-get-stack.js +31 -0
  23. package/dist/tools/diagnostics/debug-get-variables.d.ts +4 -0
  24. package/dist/tools/diagnostics/debug-get-variables.js +45 -0
  25. package/dist/tools/diagnostics/debug-list-sessions.d.ts +4 -0
  26. package/dist/tools/diagnostics/debug-list-sessions.js +37 -0
  27. package/dist/tools/diagnostics/debug-set-breakpoints.d.ts +4 -0
  28. package/dist/tools/diagnostics/debug-set-breakpoints.js +56 -0
  29. package/dist/tools/diagnostics/debug-start-session.d.ts +4 -0
  30. package/dist/tools/diagnostics/debug-start-session.js +74 -0
  31. package/dist/tools/diagnostics/debug-step.d.ts +4 -0
  32. package/dist/tools/diagnostics/debug-step.js +38 -0
  33. package/dist/tools/diagnostics/debug-wait-for-stop.d.ts +4 -0
  34. package/dist/tools/diagnostics/debug-wait-for-stop.js +45 -0
  35. package/dist/tools/diagnostics/index.d.ts +4 -0
  36. package/dist/tools/diagnostics/index.js +32 -0
  37. package/dist/tools/diagnostics/session-registry.d.ts +56 -0
  38. package/dist/tools/diagnostics/session-registry.js +142 -0
  39. package/dist/tools/index.d.ts +1 -0
  40. package/dist/tools/index.js +1 -0
  41. package/oclif.manifest.json +29 -1
  42. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,7 @@ Full documentation: [https://salesforcecommercecloud.github.io/b2c-developer-too
8
8
 
9
9
  ## Installation
10
10
 
11
- **Prerequisites:** Node.js 22.0.0 or higher, MCP client (Cursor, Claude Code, GitHub Copilot, or compatible)
11
+ **Prerequisites:** Node.js 22.16.0 or higher, MCP client (Cursor, Claude Code, GitHub Copilot, or compatible)
12
12
 
13
13
  **Cursor** (project-level configuration - recommended):
14
14
 
@@ -39,6 +39,9 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
39
39
  'auth-methods': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
40
  'user-auth': import("@oclif/core/interfaces").BooleanFlag<boolean>;
41
41
  'account-manager-host': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
42
+ 'jwt-cert': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
+ 'jwt-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
+ 'jwt-passphrase': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
45
  'log-level': import("@oclif/core/interfaces").OptionFlag<"trace" | "debug" | "info" | "warn" | "error" | "silent" | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
46
  debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
47
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -56,6 +59,8 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
56
59
  'cloud-origin': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
57
60
  'credentials-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
58
61
  };
62
+ /** Server-scoped persistent state (debug sessions, log watches, etc.) */
63
+ private serverContext?;
59
64
  /** Signal that triggered shutdown (if any) - used to exit process after finally() */
60
65
  private shutdownSignal?;
61
66
  /** Promise that resolves when stdin closes (MCP client disconnects) */
@@ -137,6 +137,7 @@ import { BaseCommand, MrtCommand, InstanceCommand, loadConfig, extractInstanceFl
137
137
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
138
138
  import { B2CDxMcpServer } from '../server.js';
139
139
  import { Services } from '../services.js';
140
+ import { ServerContext } from '../server-context.js';
140
141
  import { registerToolsets } from '../registry.js';
141
142
  import { TOOLSETS } from '../utils/index.js';
142
143
  /**
@@ -199,6 +200,8 @@ export default class McpServerCommand extends BaseCommand {
199
200
  default: false,
200
201
  }),
201
202
  };
203
+ /** Server-scoped persistent state (debug sessions, log watches, etc.) */
204
+ serverContext;
202
205
  /** Signal that triggered shutdown (if any) - used to exit process after finally() */
203
206
  shutdownSignal;
204
207
  /** Promise that resolves when stdin closes (MCP client disconnects) */
@@ -315,9 +318,11 @@ export default class McpServerCommand extends BaseCommand {
315
318
  },
316
319
  telemetry: this.telemetry,
317
320
  });
321
+ // Create server context for persistent state (debug sessions, log watches)
322
+ this.serverContext = new ServerContext();
318
323
  // Register toolsets with loader function that loads config and creates Services on each tool call
319
324
  // This allows tools to pick up changes to config files (dw.json, ~/.mobify) between invocations
320
- await registerToolsets(startupFlags, server, this.loadServices.bind(this));
325
+ await registerToolsets(startupFlags, server, this.loadServices.bind(this), this.serverContext);
321
326
  // Connect to stdio transport
322
327
  const transport = new StdioServerTransport();
323
328
  await server.connect(transport);
@@ -326,11 +331,16 @@ export default class McpServerCommand extends BaseCommand {
326
331
  this.stdinClosePromise = new Promise((resolve) => {
327
332
  const sendStopAndResolve = (signal) => {
328
333
  this.shutdownSignal = signal;
329
- this.telemetry?.sendEvent('SERVER_STOPPED', { signal });
330
- // Flush telemetry before resolving to ensure SERVER_STOPPED is sent
331
- // before finally() proceeds to stop telemetry
332
- const flushPromise = this.telemetry?.flush() ?? Promise.resolve();
333
- flushPromise.then(() => resolve()).catch(() => resolve());
334
+ const cleanup = this.serverContext?.destroyAll() ?? Promise.resolve();
335
+ cleanup
336
+ .catch(() => { })
337
+ .then(() => {
338
+ this.telemetry?.sendEvent('SERVER_STOPPED', { signal });
339
+ const flushPromise = this.telemetry?.flush() ?? Promise.resolve();
340
+ return flushPromise;
341
+ })
342
+ .then(() => resolve())
343
+ .catch(() => resolve());
334
344
  };
335
345
  // Handle stdin close (MCP client disconnects normally)
336
346
  process.stdin.on('close', () => sendStopAndResolve('stdin_close'));
@@ -1,6 +1,7 @@
1
1
  import type { McpTool, Toolset, StartupFlags } from './utils/index.js';
2
2
  import type { B2CDxMcpServer } from './server.js';
3
3
  import type { Services } from './services.js';
4
+ import type { ServerContext } from './server-context.js';
4
5
  /**
5
6
  * Registry of tools organized by toolset.
6
7
  * Tools can belong to multiple toolsets via their `toolsets` array.
@@ -14,24 +15,5 @@ export type ToolRegistry = Record<Toolset, McpTool[]>;
14
15
  * @param loadServices - Function that loads configuration and returns Services instance
15
16
  * @returns Complete tool registry
16
17
  */
17
- export declare function createToolRegistry(loadServices: () => Promise<Services> | Services): ToolRegistry;
18
- /**
19
- * Register tools with the MCP server based on startup flags.
20
- *
21
- * Tool selection logic:
22
- * 1. If no valid tools result from --toolsets and --tools, perform auto-discovery
23
- * 2. Start with all tools from --toolsets (or auto-discovered toolsets)
24
- * 3. Add individual tools from --tools (can be from any toolset)
25
- *
26
- * Auto-discovery always enables at least the BASE_TOOLSET (SCAPI), even if no
27
- * project types are detected in the workspace.
28
- *
29
- * Example:
30
- * --toolsets STOREFRONTNEXT,MRT --tools cartridge_deploy
31
- * This enables STOREFRONTNEXT and MRT toolsets, plus adds cartridge_deploy from CARTRIDGES.
32
- *
33
- * @param flags - Startup flags from CLI
34
- * @param server - B2CDxMcpServer instance
35
- * @param loadServices - Function that loads configuration and returns Services instance
36
- */
37
- export declare function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, loadServices: () => Promise<Services> | Services): Promise<void>;
18
+ export declare function createToolRegistry(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): ToolRegistry;
19
+ export declare function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): Promise<void>;
package/dist/registry.js CHANGED
@@ -7,6 +7,7 @@ import { getLogger } from '@salesforce/b2c-tooling-sdk/logging';
7
7
  import { detectWorkspaceType } from '@salesforce/b2c-tooling-sdk/discovery';
8
8
  import { ALL_TOOLSETS, TOOLSETS, VALID_TOOLSET_NAMES } from './utils/index.js';
9
9
  import { createCartridgesTools } from './tools/cartridges/index.js';
10
+ import { createDiagnosticsTools } from './tools/diagnostics/index.js';
10
11
  import { createMrtTools } from './tools/mrt/index.js';
11
12
  import { createPwav3Tools } from './tools/pwav3/index.js';
12
13
  import { createScapiTools } from './tools/scapi/index.js';
@@ -61,7 +62,7 @@ function getToolsetsForProjectTypes(projectTypes) {
61
62
  * @param loadServices - Function that loads configuration and returns Services instance
62
63
  * @returns Complete tool registry
63
64
  */
64
- export function createToolRegistry(loadServices) {
65
+ export function createToolRegistry(loadServices, serverContext) {
65
66
  const registry = {
66
67
  CARTRIDGES: [],
67
68
  MRT: [],
@@ -72,6 +73,7 @@ export function createToolRegistry(loadServices) {
72
73
  // Collect all tools from all factories
73
74
  const allTools = [
74
75
  ...createCartridgesTools(loadServices),
76
+ ...createDiagnosticsTools(loadServices, serverContext),
75
77
  ...createMrtTools(loadServices),
76
78
  ...createPwav3Tools(loadServices),
77
79
  ...createScapiTools(loadServices),
@@ -134,13 +136,21 @@ async function performAutoDiscovery(flags, reason) {
134
136
  * @param server - B2CDxMcpServer instance
135
137
  * @param loadServices - Function that loads configuration and returns Services instance
136
138
  */
137
- export async function registerToolsets(flags, server, loadServices) {
139
+ // Guards against accidental double-registration. The MCP SDK throws on duplicate
140
+ // `addTool` names, but tracking servers we've already populated lets callers fail
141
+ // fast with an explicit message instead of a cryptic SDK error.
142
+ const REGISTERED_SERVERS = new WeakSet();
143
+ export async function registerToolsets(flags, server, loadServices, serverContext) {
144
+ if (REGISTERED_SERVERS.has(server)) {
145
+ throw new Error('registerToolsets() was called more than once for the same server instance');
146
+ }
147
+ REGISTERED_SERVERS.add(server);
138
148
  const toolsets = flags.toolsets ?? [];
139
149
  const individualTools = flags.tools ?? [];
140
150
  const allowNonGaTools = flags.allowNonGaTools ?? false;
141
151
  const logger = getLogger();
142
152
  // Create the tool registry (all available tools)
143
- const toolRegistry = createToolRegistry(loadServices);
153
+ const toolRegistry = createToolRegistry(loadServices, serverContext);
144
154
  // Build flat list of all tools for lookup
145
155
  const allTools = Object.values(toolRegistry).flat();
146
156
  const allToolsByName = new Map(allTools.map((tool) => [tool.name, tool]));
@@ -0,0 +1,27 @@
1
+ import { DebugSessionRegistry } from './tools/diagnostics/session-registry.js';
2
+ /**
3
+ * Server-scoped persistent state that lives for the lifetime of the MCP
4
+ * server process. Holds registries for stateful resources (debug sessions,
5
+ * future log watches) that need to span multiple tool invocations.
6
+ *
7
+ * ## Multi-agent / shared-state caveats
8
+ *
9
+ * One `ServerContext` is created per MCP server process. With the default
10
+ * stdio transport, each MCP client connection spawns its own server
11
+ * subprocess, so this state is naturally isolated per client/agent.
12
+ *
13
+ * If this server is ever wired up to a shared transport (e.g. HTTP with
14
+ * multiple connected clients), `ServerContext` state would be shared
15
+ * across all connected clients. Sub-agents that run within the same MCP
16
+ * client would also share the same context. Tools that mutate registries
17
+ * (like the debug tools) should not assume single-tenant access.
18
+ *
19
+ * The debug registry already handles concurrent sessions via session IDs
20
+ * and host:client_id pairs, so multi-agent use is functional but agents
21
+ * may see each other's sessions via `debug_list_sessions`.
22
+ */
23
+ export declare class ServerContext {
24
+ readonly debugSessions: DebugSessionRegistry;
25
+ constructor();
26
+ destroyAll(): Promise<void>;
27
+ }
@@ -0,0 +1,37 @@
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 { DebugSessionRegistry } from './tools/diagnostics/session-registry.js';
7
+ /**
8
+ * Server-scoped persistent state that lives for the lifetime of the MCP
9
+ * server process. Holds registries for stateful resources (debug sessions,
10
+ * future log watches) that need to span multiple tool invocations.
11
+ *
12
+ * ## Multi-agent / shared-state caveats
13
+ *
14
+ * One `ServerContext` is created per MCP server process. With the default
15
+ * stdio transport, each MCP client connection spawns its own server
16
+ * subprocess, so this state is naturally isolated per client/agent.
17
+ *
18
+ * If this server is ever wired up to a shared transport (e.g. HTTP with
19
+ * multiple connected clients), `ServerContext` state would be shared
20
+ * across all connected clients. Sub-agents that run within the same MCP
21
+ * client would also share the same context. Tools that mutate registries
22
+ * (like the debug tools) should not assume single-tenant access.
23
+ *
24
+ * The debug registry already handles concurrent sessions via session IDs
25
+ * and host:client_id pairs, so multi-agent use is functional but agents
26
+ * may see each other's sessions via `debug_list_sessions`.
27
+ */
28
+ export class ServerContext {
29
+ debugSessions;
30
+ constructor() {
31
+ this.debugSessions = new DebugSessionRegistry();
32
+ }
33
+ async destroyAll() {
34
+ await this.debugSessions.destroyAll();
35
+ }
36
+ }
37
+ //# sourceMappingURL=server-context.js.map
package/dist/server.js CHANGED
@@ -4,6 +4,7 @@
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
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { getLogger } from '@salesforce/b2c-tooling-sdk/logging';
7
8
  /**
8
9
  * A server implementation that extends the base MCP server.
9
10
  *
@@ -53,7 +54,9 @@ export class B2CDxMcpServer extends McpServer {
53
54
  runTimeMs,
54
55
  isError: result.isError ?? false,
55
56
  })
56
- .catch(() => { });
57
+ .catch((error_) => {
58
+ getLogger().debug({ err: error_, toolName: name }, '[mcp] telemetry sendEventAndFlush failed');
59
+ });
57
60
  return result;
58
61
  }
59
62
  catch (error) {
@@ -64,7 +67,9 @@ export class B2CDxMcpServer extends McpServer {
64
67
  runTimeMs,
65
68
  isError: true,
66
69
  })
67
- .catch(() => { });
70
+ .catch((error_) => {
71
+ getLogger().debug({ err: error_, toolName: name }, '[mcp] telemetry sendEventAndFlush failed');
72
+ });
68
73
  throw error;
69
74
  }
70
75
  };
@@ -122,6 +122,15 @@ export declare class Services {
122
122
  * @returns True if exists, false otherwise
123
123
  */
124
124
  exists(targetPath: string): boolean;
125
+ /**
126
+ * Get Basic auth credentials for SDAPI operations (script debugger).
127
+ * Returns undefined if credentials are not configured.
128
+ */
129
+ getBasicAuthCredentials(): undefined | {
130
+ hostname: string;
131
+ username: string;
132
+ password: string;
133
+ };
125
134
  /**
126
135
  * Get Custom APIs client for managing custom SCAPI endpoints.
127
136
  * Requires shortCode, tenantId, and OAuth credentials to be configured.
package/dist/services.js CHANGED
@@ -126,6 +126,16 @@ export class Services {
126
126
  exists(targetPath) {
127
127
  return fs.existsSync(targetPath);
128
128
  }
129
+ /**
130
+ * Get Basic auth credentials for SDAPI operations (script debugger).
131
+ * Returns undefined if credentials are not configured.
132
+ */
133
+ getBasicAuthCredentials() {
134
+ const { hostname, username, password } = this.resolvedConfig.values;
135
+ if (!hostname || !username || !password)
136
+ return undefined;
137
+ return { hostname, username, password };
138
+ }
129
139
  /**
130
140
  * Get Custom APIs client for managing custom SCAPI endpoints.
131
141
  * Requires shortCode, tenantId, and OAuth credentials to be configured.
@@ -68,6 +68,7 @@ import { type ZodRawShape } from 'zod';
68
68
  import type { B2CInstance } from '@salesforce/b2c-tooling-sdk';
69
69
  import type { McpTool, ToolResult, Toolset } from '../utils/index.js';
70
70
  import type { Services, MrtConfig } from '../services.js';
71
+ import type { ServerContext } from '../server-context.js';
71
72
  /**
72
73
  * Context provided to tool execute functions.
73
74
  * Contains the B2CInstance and/or MRT config based on tool requirements.
@@ -89,6 +90,11 @@ export interface ToolExecutionContext {
89
90
  * Services instance for file system access and other utilities.
90
91
  */
91
92
  services: Services;
93
+ /**
94
+ * Server-scoped persistent state (debug sessions, log watches, etc.).
95
+ * Created once at server startup and shared across all tool invocations.
96
+ */
97
+ serverContext?: ServerContext;
92
98
  }
93
99
  /**
94
100
  * Options for creating a tool adapter.
@@ -206,4 +212,4 @@ export declare function jsonResult(data: unknown, indent?: number): ToolResult;
206
212
  * }, loadServices);
207
213
  * ```
208
214
  */
209
- export declare function createToolAdapter<TInput, TOutput>(options: ToolAdapterOptions<TInput, TOutput>, loadServices: () => Promise<Services> | Services): McpTool;
215
+ export declare function createToolAdapter<TInput, TOutput>(options: ToolAdapterOptions<TInput, TOutput>, loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -168,7 +168,7 @@ function formatZodErrors(error) {
168
168
  * }, loadServices);
169
169
  * ```
170
170
  */
171
- export function createToolAdapter(options, loadServices) {
171
+ export function createToolAdapter(options, loadServices, serverContext) {
172
172
  const { name, description, inputSchema, toolsets, isGA = true, requiresInstance = false, requiresMrtAuth = false, execute, formatOutput, } = options;
173
173
  // Create Zod schema from inputSchema definition
174
174
  const zodSchema = z.object(inputSchema);
@@ -214,6 +214,7 @@ export function createToolAdapter(options, loadServices) {
214
214
  b2cInstance,
215
215
  mrtConfig,
216
216
  services,
217
+ serverContext,
217
218
  };
218
219
  const output = await execute(args, context);
219
220
  // 6. Format output
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugCaptureAtBreakpointTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,118 @@
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 { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { projectFrame, projectVariable, resolveBreakpointPath, } from '@salesforce/b2c-tooling-sdk/operations/debug';
9
+ import { getRegistry, getSessionEntry } from './session-registry.js';
10
+ const DEFAULT_TIMEOUT_MS = 30_000;
11
+ const MAX_TIMEOUT_MS = 120_000;
12
+ export function createDebugCaptureAtBreakpointTool(loadServices, serverContext) {
13
+ return createToolAdapter({
14
+ name: 'debug_capture_at_breakpoint',
15
+ description: 'Set a breakpoint, optionally trigger an HTTP request, wait for the breakpoint to be hit, and capture a diagnostic snapshot (stack, variables, expression results). ' +
16
+ 'Use trigger_url to have the tool fire the request itself (recommended) — this avoids needing to coordinate a separate request while the tool blocks. ' +
17
+ 'Without trigger_url, the tool BLOCKS until the breakpoint is hit or timeout expires and requires the user to trigger a request externally. ' +
18
+ 'For more control, use the non-blocking workflow: debug_set_breakpoints → trigger request → debug_list_sessions (check halted_threads) → debug_get_variables.',
19
+ toolsets: ['CARTRIDGES', 'SCAPI'],
20
+ inputSchema: {
21
+ session_id: z.string().describe('Session ID returned by debug_start_session.'),
22
+ file: z.string().describe('Local file path or server script path for the breakpoint.'),
23
+ line: z.number().int().positive().describe('Line number for the breakpoint.'),
24
+ condition: z.string().optional().describe('Optional conditional expression for the breakpoint.'),
25
+ expressions: z.array(z.string()).optional().describe('Expressions to evaluate when the breakpoint is hit.'),
26
+ timeout_ms: z
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .max(MAX_TIMEOUT_MS)
31
+ .optional()
32
+ .describe(`Timeout in milliseconds waiting for the breakpoint to be hit (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS}).`),
33
+ auto_continue: z
34
+ .boolean()
35
+ .optional()
36
+ .describe('If true, resume the thread after capturing the snapshot. Defaults to false.'),
37
+ trigger_url: z
38
+ .string()
39
+ .optional()
40
+ .describe('URL to request after arming the breakpoint. The tool fires this HTTP GET in the background, then waits for the breakpoint to halt. ' +
41
+ 'This is the recommended approach — it avoids needing to coordinate a separate request while the tool blocks.'),
42
+ },
43
+ async execute(args, context) {
44
+ const entry = getSessionEntry(context, args.session_id);
45
+ const registry = getRegistry(context);
46
+ const timeout = Math.min(args.timeout_ms ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
47
+ const scriptPath = resolveBreakpointPath(args.file, entry.sourceMapper, entry.cartridges);
48
+ // Add breakpoint to existing set
49
+ const allBps = [
50
+ ...entry.breakpoints.map((bp) => ({
51
+ script_path: bp.script_path,
52
+ line_number: bp.line_number,
53
+ condition: bp.condition,
54
+ })),
55
+ { script_path: scriptPath, line_number: args.line, condition: args.condition },
56
+ ];
57
+ entry.breakpoints = await entry.manager.setBreakpoints(allBps);
58
+ const breakpointInfo = {
59
+ file: entry.sourceMapper.toLocalPath(scriptPath) ?? null,
60
+ line: args.line,
61
+ script_path: scriptPath,
62
+ };
63
+ // Fire trigger URL in the background (it will hang when the breakpoint halts the thread)
64
+ const triggerPromise = args.trigger_url
65
+ ? fetch(args.trigger_url, { redirect: 'follow' })
66
+ .then((r) => r.status)
67
+ .catch(() => undefined)
68
+ : undefined;
69
+ const thread = await registry.waitForHalt(entry, timeout);
70
+ if (!thread) {
71
+ return {
72
+ breakpoint: breakpointInfo,
73
+ halted: false,
74
+ timed_out: true,
75
+ auto_continued: false,
76
+ };
77
+ }
78
+ const threadDetail = await entry.manager.client.getThread(thread.id);
79
+ const stack = threadDetail.call_stack.map((frame) => projectFrame(frame, entry.sourceMapper));
80
+ const varsResult = await entry.manager.client.getVariables(thread.id, 0);
81
+ const variables = varsResult.object_members.map((m) => projectVariable(m));
82
+ const evaluations = [];
83
+ if (args.expressions) {
84
+ for (const expr of args.expressions) {
85
+ try {
86
+ // eslint-disable-next-line no-await-in-loop -- sequential: each eval must complete before the next
87
+ const evalResult = await entry.manager.client.evaluate(thread.id, 0, expr);
88
+ evaluations.push({ expression: evalResult.expression, result: evalResult.result });
89
+ }
90
+ catch (error) {
91
+ evaluations.push({
92
+ expression: expr,
93
+ result: `Error: ${error instanceof Error ? error.message : String(error)}`,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ let autoContinued = false;
99
+ if (args.auto_continue) {
100
+ await entry.manager.resume(thread.id);
101
+ autoContinued = true;
102
+ }
103
+ const triggerStatus = triggerPromise ? await triggerPromise : undefined;
104
+ return {
105
+ breakpoint: breakpointInfo,
106
+ halted: true,
107
+ thread_id: thread.id,
108
+ stack,
109
+ variables,
110
+ evaluations: evaluations.length > 0 ? evaluations : undefined,
111
+ auto_continued: autoContinued,
112
+ trigger_status: triggerStatus,
113
+ };
114
+ },
115
+ formatOutput: (output) => jsonResult(output),
116
+ }, loadServices, serverContext);
117
+ }
118
+ //# sourceMappingURL=debug-capture-at-breakpoint.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugContinueTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,27 @@
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 { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { getSessionEntry } from './session-registry.js';
9
+ export function createDebugContinueTool(loadServices, serverContext) {
10
+ return createToolAdapter({
11
+ name: 'debug_continue',
12
+ description: 'Resume execution of a halted thread. ' +
13
+ 'The thread runs until the next breakpoint or completes the current request.',
14
+ toolsets: ['CARTRIDGES', 'SCAPI'],
15
+ inputSchema: {
16
+ session_id: z.string().describe('Session ID returned by debug_start_session.'),
17
+ thread_id: z.number().int().describe('Thread ID of the halted thread to resume.'),
18
+ },
19
+ async execute(args, context) {
20
+ const entry = getSessionEntry(context, args.session_id);
21
+ await entry.manager.resume(args.thread_id);
22
+ return { thread_id: args.thread_id, status: 'resumed' };
23
+ },
24
+ formatOutput: (output) => jsonResult(output),
25
+ }, loadServices, serverContext);
26
+ }
27
+ //# sourceMappingURL=debug-continue.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugEndSessionTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -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 { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { getRegistry, getSessionEntry } from './session-registry.js';
9
+ export function createDebugEndSessionTool(loadServices, serverContext) {
10
+ return createToolAdapter({
11
+ name: 'debug_end_session',
12
+ description: 'End a script debugger session and free its slot on the instance. ' +
13
+ 'IMPORTANT: Always call this when finished debugging — leaving sessions open can interfere with other debuggers and consumes a debugger client slot.',
14
+ toolsets: ['CARTRIDGES', 'SCAPI'],
15
+ inputSchema: {
16
+ session_id: z.string().describe('Session ID returned by debug_start_session.'),
17
+ clear_breakpoints: z
18
+ .boolean()
19
+ .optional()
20
+ .describe('If true, delete all breakpoints before disconnecting. Defaults to false.'),
21
+ },
22
+ async execute(args, context) {
23
+ const entry = getSessionEntry(context, args.session_id);
24
+ if (args.clear_breakpoints) {
25
+ try {
26
+ await entry.manager.client.deleteBreakpoints();
27
+ }
28
+ catch {
29
+ // Best-effort
30
+ }
31
+ }
32
+ await getRegistry(context).destroySession(args.session_id);
33
+ return { session_id: args.session_id, status: 'disconnected' };
34
+ },
35
+ formatOutput: (output) => jsonResult(output),
36
+ }, loadServices, serverContext);
37
+ }
38
+ //# sourceMappingURL=debug-end-session.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugEvaluateTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,30 @@
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 { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { getSessionEntry } from './session-registry.js';
9
+ export function createDebugEvaluateTool(loadServices, serverContext) {
10
+ return createToolAdapter({
11
+ name: 'debug_evaluate',
12
+ description: 'Evaluate a JavaScript expression in the context of a halted thread and stack frame. ' +
13
+ 'WARNING: Expressions may have side effects (modify variables, call functions). Use with care.',
14
+ toolsets: ['CARTRIDGES', 'SCAPI'],
15
+ inputSchema: {
16
+ session_id: z.string().describe('Session ID returned by debug_start_session.'),
17
+ thread_id: z.number().int().describe('Thread ID from debug_wait_for_stop or debug_list_sessions.'),
18
+ frame_index: z.number().int().min(0).optional().describe('Stack frame index (0 = top frame). Defaults to 0.'),
19
+ expression: z.string().describe('JavaScript expression to evaluate in the frame context.'),
20
+ },
21
+ async execute(args, context) {
22
+ const entry = getSessionEntry(context, args.session_id);
23
+ const frameIndex = args.frame_index ?? 0;
24
+ const result = await entry.manager.client.evaluate(args.thread_id, frameIndex, args.expression);
25
+ return { expression: result.expression, result: result.result };
26
+ },
27
+ formatOutput: (output) => jsonResult(output),
28
+ }, loadServices, serverContext);
29
+ }
30
+ //# sourceMappingURL=debug-evaluate.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugGetStackTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,31 @@
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 { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { projectFrame } from '@salesforce/b2c-tooling-sdk/operations/debug';
9
+ import { getSessionEntry } from './session-registry.js';
10
+ export function createDebugGetStackTool(loadServices, serverContext) {
11
+ return createToolAdapter({
12
+ name: 'debug_get_stack',
13
+ description: 'Get the call stack for a halted thread. ' +
14
+ 'Returns frames with mapped local file paths and server script paths.',
15
+ toolsets: ['CARTRIDGES', 'SCAPI'],
16
+ inputSchema: {
17
+ session_id: z.string().describe('Session ID returned by debug_start_session.'),
18
+ thread_id: z.number().int().describe('Thread ID from debug_wait_for_stop or debug_list_sessions.'),
19
+ },
20
+ async execute(args, context) {
21
+ const entry = getSessionEntry(context, args.session_id);
22
+ const thread = await entry.manager.client.getThread(args.thread_id);
23
+ return {
24
+ thread_id: thread.id,
25
+ frames: thread.call_stack.map((frame) => projectFrame(frame, entry.sourceMapper)),
26
+ };
27
+ },
28
+ formatOutput: (output) => jsonResult(output),
29
+ }, loadServices, serverContext);
30
+ }
31
+ //# sourceMappingURL=debug-get-stack.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createDebugGetVariablesTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;