@salesforce/b2c-dx-mcp 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/mcp.d.ts +5 -0
- package/dist/commands/mcp.js +16 -6
- package/dist/registry.d.ts +3 -21
- package/dist/registry.js +13 -3
- package/dist/server-context.d.ts +27 -0
- package/dist/server-context.js +37 -0
- package/dist/server.js +7 -2
- package/dist/services.d.ts +9 -0
- package/dist/services.js +10 -0
- package/dist/tools/adapter.d.ts +7 -1
- package/dist/tools/adapter.js +2 -1
- package/dist/tools/diagnostics/debug-capture-at-breakpoint.d.ts +4 -0
- package/dist/tools/diagnostics/debug-capture-at-breakpoint.js +118 -0
- package/dist/tools/diagnostics/debug-continue.d.ts +4 -0
- package/dist/tools/diagnostics/debug-continue.js +27 -0
- package/dist/tools/diagnostics/debug-end-session.d.ts +4 -0
- package/dist/tools/diagnostics/debug-end-session.js +38 -0
- package/dist/tools/diagnostics/debug-evaluate.d.ts +4 -0
- package/dist/tools/diagnostics/debug-evaluate.js +30 -0
- package/dist/tools/diagnostics/debug-get-stack.d.ts +4 -0
- package/dist/tools/diagnostics/debug-get-stack.js +31 -0
- package/dist/tools/diagnostics/debug-get-variables.d.ts +4 -0
- package/dist/tools/diagnostics/debug-get-variables.js +45 -0
- package/dist/tools/diagnostics/debug-list-sessions.d.ts +4 -0
- package/dist/tools/diagnostics/debug-list-sessions.js +37 -0
- package/dist/tools/diagnostics/debug-set-breakpoints.d.ts +4 -0
- package/dist/tools/diagnostics/debug-set-breakpoints.js +56 -0
- package/dist/tools/diagnostics/debug-start-session.d.ts +4 -0
- package/dist/tools/diagnostics/debug-start-session.js +74 -0
- package/dist/tools/diagnostics/debug-step.d.ts +4 -0
- package/dist/tools/diagnostics/debug-step.js +38 -0
- package/dist/tools/diagnostics/debug-wait-for-stop.d.ts +4 -0
- package/dist/tools/diagnostics/debug-wait-for-stop.js +45 -0
- package/dist/tools/diagnostics/index.d.ts +4 -0
- package/dist/tools/diagnostics/index.js +32 -0
- package/dist/tools/diagnostics/session-registry.d.ts +56 -0
- package/dist/tools/diagnostics/session-registry.js +142 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/oclif.manifest.json +29 -1
- package/package.json +2 -2
|
@@ -0,0 +1,45 @@
|
|
|
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 { projectVariable } from '@salesforce/b2c-tooling-sdk/operations/debug';
|
|
9
|
+
import { getSessionEntry } from './session-registry.js';
|
|
10
|
+
export function createDebugGetVariablesTool(loadServices, serverContext) {
|
|
11
|
+
return createToolAdapter({
|
|
12
|
+
name: 'debug_get_variables',
|
|
13
|
+
description: 'Get variables for a stack frame in a halted thread. ' +
|
|
14
|
+
'Defaults to top-frame locals. Use scope to filter (local/closure/global) or object_path to drill into nested objects.',
|
|
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
|
+
frame_index: z.number().int().min(0).optional().describe('Stack frame index (0 = top frame). Defaults to 0.'),
|
|
20
|
+
scope: z
|
|
21
|
+
.enum(['local', 'closure', 'global'])
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Filter variables by scope. If omitted, returns all scopes.'),
|
|
24
|
+
object_path: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Dot-delimited path to drill into an object (e.g. "request.httpParameters"). Returns child members.'),
|
|
28
|
+
},
|
|
29
|
+
async execute(args, context) {
|
|
30
|
+
const entry = getSessionEntry(context, args.session_id);
|
|
31
|
+
const frameIndex = args.frame_index ?? 0;
|
|
32
|
+
if (args.object_path) {
|
|
33
|
+
const result = await entry.manager.client.getMembers(args.thread_id, frameIndex, args.object_path);
|
|
34
|
+
return { variables: result.object_members.map((m) => projectVariable(m, { includeScope: false })) };
|
|
35
|
+
}
|
|
36
|
+
const result = await entry.manager.client.getVariables(args.thread_id, frameIndex);
|
|
37
|
+
const members = args.scope
|
|
38
|
+
? result.object_members.filter((m) => m.scope === args.scope)
|
|
39
|
+
: result.object_members;
|
|
40
|
+
return { variables: members.map((m) => projectVariable(m)) };
|
|
41
|
+
},
|
|
42
|
+
formatOutput: (output) => jsonResult(output),
|
|
43
|
+
}, loadServices, serverContext);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=debug-get-variables.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 createDebugListSessionsTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
|
|
@@ -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 { createToolAdapter, jsonResult } from '../adapter.js';
|
|
7
|
+
import { projectBreakpoint } from '@salesforce/b2c-tooling-sdk/operations/debug';
|
|
8
|
+
import { getRegistry } from './session-registry.js';
|
|
9
|
+
export function createDebugListSessionsTool(loadServices, serverContext) {
|
|
10
|
+
return createToolAdapter({
|
|
11
|
+
name: 'debug_list_sessions',
|
|
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
|
+
toolsets: ['CARTRIDGES', 'SCAPI'],
|
|
15
|
+
inputSchema: {},
|
|
16
|
+
async execute(_args, context) {
|
|
17
|
+
const registry = getRegistry(context);
|
|
18
|
+
const entries = registry.listSessions();
|
|
19
|
+
return {
|
|
20
|
+
sessions: entries.map((entry) => ({
|
|
21
|
+
session_id: entry.sessionId,
|
|
22
|
+
hostname: entry.hostname,
|
|
23
|
+
client_id: entry.clientId,
|
|
24
|
+
halted_threads: entry.manager
|
|
25
|
+
.getKnownThreads()
|
|
26
|
+
.filter((t) => t.status === 'halted')
|
|
27
|
+
.map((t) => t.id),
|
|
28
|
+
breakpoints: entry.breakpoints.map((bp) => projectBreakpoint(bp, entry.sourceMapper)),
|
|
29
|
+
created_at: new Date(entry.createdAt).toISOString(),
|
|
30
|
+
last_activity_at: new Date(entry.lastActivityAt).toISOString(),
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
formatOutput: (output) => jsonResult(output),
|
|
35
|
+
}, loadServices, serverContext);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=debug-list-sessions.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 createDebugSetBreakpointsTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { projectBreakpoint, resolveBreakpointPath, } from '@salesforce/b2c-tooling-sdk/operations/debug';
|
|
9
|
+
import { getSessionEntry } from './session-registry.js';
|
|
10
|
+
export function createDebugSetBreakpointsTool(loadServices, serverContext) {
|
|
11
|
+
return createToolAdapter({
|
|
12
|
+
name: 'debug_set_breakpoints',
|
|
13
|
+
description: 'Set breakpoints in a debug session. Replaces all previously set breakpoints. ' +
|
|
14
|
+
'Accepts local file paths (mapped to server paths via cartridge discovery), cartridge-prefixed paths, or server paths starting with /. ' +
|
|
15
|
+
'Check the "verified" field and "warnings" — unmapped paths are flagged.',
|
|
16
|
+
toolsets: ['CARTRIDGES', 'SCAPI'],
|
|
17
|
+
inputSchema: {
|
|
18
|
+
session_id: z.string().describe('Session ID returned by debug_start_session.'),
|
|
19
|
+
breakpoints: z
|
|
20
|
+
.array(z.object({
|
|
21
|
+
file: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe('Local file path or server script path (e.g. /app_storefront/cartridge/controllers/Cart.js).'),
|
|
24
|
+
line: z.number().int().positive().describe('Line number for the breakpoint.'),
|
|
25
|
+
condition: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Optional conditional expression. Breakpoint only triggers when this evaluates to true.'),
|
|
29
|
+
}))
|
|
30
|
+
.describe('Array of breakpoints to set. Replaces all existing breakpoints.'),
|
|
31
|
+
},
|
|
32
|
+
async execute(args, context) {
|
|
33
|
+
const entry = getSessionEntry(context, args.session_id);
|
|
34
|
+
const warnings = [];
|
|
35
|
+
const bpInputs = args.breakpoints.map((bp) => {
|
|
36
|
+
const scriptPath = resolveBreakpointPath(bp.file, entry.sourceMapper, entry.cartridges);
|
|
37
|
+
if (!entry.sourceMapper.toLocalPath(scriptPath)) {
|
|
38
|
+
warnings.push(`"${bp.file}" resolved to server path "${scriptPath}" but could not be mapped back to a local file. ` +
|
|
39
|
+
`Verify this path exists on the instance.`);
|
|
40
|
+
}
|
|
41
|
+
return { script_path: scriptPath, line_number: bp.line, condition: bp.condition };
|
|
42
|
+
});
|
|
43
|
+
const result = await entry.manager.setBreakpoints(bpInputs);
|
|
44
|
+
entry.breakpoints = result;
|
|
45
|
+
return {
|
|
46
|
+
breakpoints: result.map((bp) => ({
|
|
47
|
+
...projectBreakpoint(bp, entry.sourceMapper),
|
|
48
|
+
verified: entry.sourceMapper.toLocalPath(bp.script_path) !== undefined,
|
|
49
|
+
})),
|
|
50
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
formatOutput: (output) => jsonResult(output),
|
|
54
|
+
}, loadServices, serverContext);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=debug-set-breakpoints.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 createDebugStartSessionTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { DebugSessionManager, createSourceMapper, } from '@salesforce/b2c-tooling-sdk/operations/debug';
|
|
9
|
+
import { findCartridges } from '@salesforce/b2c-tooling-sdk/operations/code';
|
|
10
|
+
import { getRegistry } from './session-registry.js';
|
|
11
|
+
export function createDebugStartSessionTool(loadServices, serverContext) {
|
|
12
|
+
return createToolAdapter({
|
|
13
|
+
name: 'debug_start_session',
|
|
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
|
+
'Returns a session_id for use with other debug tools, plus discovered cartridge mappings. ' +
|
|
16
|
+
'WARNING: Debug sessions halt remote request threads on the instance. Always call debug_end_session when finished. ' +
|
|
17
|
+
'Requires Basic auth credentials (username/password) and the script debugger enabled in Business Manager.',
|
|
18
|
+
toolsets: ['CARTRIDGES', 'SCAPI'],
|
|
19
|
+
inputSchema: {
|
|
20
|
+
cartridge_directory: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Path to directory containing cartridges. Defaults to project directory.'),
|
|
24
|
+
client_id: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Client ID for the debugger API. Defaults to "b2c-cli". Use a different ID to run concurrent sessions on the same host.'),
|
|
28
|
+
},
|
|
29
|
+
async execute(args, context) {
|
|
30
|
+
const registry = getRegistry(context);
|
|
31
|
+
const credentials = context.services.getBasicAuthCredentials();
|
|
32
|
+
if (!credentials) {
|
|
33
|
+
throw new Error('Basic auth credentials (hostname, username, password) are required for the script debugger. ' +
|
|
34
|
+
'Set via SFCC_SERVER/SFCC_USERNAME/SFCC_PASSWORD env vars, or configure in dw.json.');
|
|
35
|
+
}
|
|
36
|
+
const { hostname, username, password } = credentials;
|
|
37
|
+
const clientId = args.client_id ?? 'b2c-cli';
|
|
38
|
+
const cartridgeDir = context.services.resolveWithProjectDirectory(args.cartridge_directory);
|
|
39
|
+
const cartridges = findCartridges(cartridgeDir);
|
|
40
|
+
const warnings = [];
|
|
41
|
+
if (cartridges.length === 0) {
|
|
42
|
+
warnings.push(`No cartridges found in ${cartridgeDir}. Breakpoints will use server paths only.`);
|
|
43
|
+
}
|
|
44
|
+
const sourceMapper = createSourceMapper(cartridges);
|
|
45
|
+
const callbacks = {
|
|
46
|
+
onThreadStopped(thread) {
|
|
47
|
+
const entry = registry.findByHostAndClientId(hostname, clientId);
|
|
48
|
+
if (!entry)
|
|
49
|
+
return;
|
|
50
|
+
while (entry.haltWaiters.length > 0) {
|
|
51
|
+
const waiter = entry.haltWaiters.shift();
|
|
52
|
+
clearTimeout(waiter.timer);
|
|
53
|
+
waiter.resolve(thread);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const manager = new DebugSessionManager({ hostname, username, password, clientId, cartridgeRoots: cartridges }, callbacks);
|
|
58
|
+
await manager.connect();
|
|
59
|
+
const entry = registry.registerSession({ hostname, clientId, manager, sourceMapper, cartridges });
|
|
60
|
+
const cartridgeMappings = {};
|
|
61
|
+
for (const c of cartridges)
|
|
62
|
+
cartridgeMappings[c.name] = c.src;
|
|
63
|
+
return {
|
|
64
|
+
session_id: entry.sessionId,
|
|
65
|
+
hostname,
|
|
66
|
+
cartridges: cartridges.map((c) => c.name),
|
|
67
|
+
cartridge_mappings: cartridgeMappings,
|
|
68
|
+
warnings,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
formatOutput: (output) => jsonResult(output),
|
|
72
|
+
}, loadServices, serverContext);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=debug-start-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 createDebugStepTools(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 { getSessionEntry } from './session-registry.js';
|
|
9
|
+
const STEP_HANDLERS = {
|
|
10
|
+
step_into: (m, id) => m.stepInto(id),
|
|
11
|
+
step_out: (m, id) => m.stepOut(id),
|
|
12
|
+
step_over: (m, id) => m.stepOver(id),
|
|
13
|
+
};
|
|
14
|
+
function createStepTool(action, description, loadServices, serverContext) {
|
|
15
|
+
return createToolAdapter({
|
|
16
|
+
name: `debug_${action}`,
|
|
17
|
+
description: description + ' Follow with debug_wait_for_stop to see where execution landed.',
|
|
18
|
+
toolsets: ['CARTRIDGES', 'SCAPI'],
|
|
19
|
+
inputSchema: {
|
|
20
|
+
session_id: z.string().describe('Session ID returned by debug_start_session.'),
|
|
21
|
+
thread_id: z.number().int().describe('Thread ID of the halted thread to step.'),
|
|
22
|
+
},
|
|
23
|
+
async execute(args, context) {
|
|
24
|
+
const entry = getSessionEntry(context, args.session_id);
|
|
25
|
+
await STEP_HANDLERS[action](entry.manager, args.thread_id);
|
|
26
|
+
return { thread_id: args.thread_id, action };
|
|
27
|
+
},
|
|
28
|
+
formatOutput: (output) => jsonResult(output),
|
|
29
|
+
}, loadServices, serverContext);
|
|
30
|
+
}
|
|
31
|
+
export function createDebugStepTools(loadServices, serverContext) {
|
|
32
|
+
return [
|
|
33
|
+
createStepTool('step_over', 'Step to the next line in the current function.', loadServices, serverContext),
|
|
34
|
+
createStepTool('step_into', 'Step into the function call on the current line.', loadServices, serverContext),
|
|
35
|
+
createStepTool('step_out', 'Step out of the current function, returning to the caller.', loadServices, serverContext),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=debug-step.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 createDebugWaitForStopTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { projectThreadLocation } 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 createDebugWaitForStopTool(loadServices, serverContext) {
|
|
13
|
+
return createToolAdapter({
|
|
14
|
+
name: 'debug_wait_for_stop',
|
|
15
|
+
description: 'Wait for a thread to halt at a breakpoint or step. ' +
|
|
16
|
+
'Returns immediately if a thread is already halted; otherwise BLOCKS until a halt occurs or the timeout expires. ' +
|
|
17
|
+
'Preferred non-blocking alternative: after debug_set_breakpoints, trigger the request yourself, then use debug_list_sessions to check halted_threads before calling debug_get_stack/debug_get_variables.',
|
|
18
|
+
toolsets: ['CARTRIDGES', 'SCAPI'],
|
|
19
|
+
inputSchema: {
|
|
20
|
+
session_id: z.string().describe('Session ID returned by debug_start_session.'),
|
|
21
|
+
timeout_ms: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.positive()
|
|
25
|
+
.max(MAX_TIMEOUT_MS)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(`Timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS}).`),
|
|
28
|
+
},
|
|
29
|
+
async execute(args, context) {
|
|
30
|
+
const entry = getSessionEntry(context, args.session_id);
|
|
31
|
+
const timeout = Math.min(args.timeout_ms ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
32
|
+
const thread = await getRegistry(context).waitForHalt(entry, timeout);
|
|
33
|
+
if (!thread)
|
|
34
|
+
return { halted: false, timed_out: true };
|
|
35
|
+
const location = projectThreadLocation(thread, entry.sourceMapper);
|
|
36
|
+
return {
|
|
37
|
+
halted: true,
|
|
38
|
+
thread_id: thread.id,
|
|
39
|
+
location: location ?? undefined,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
formatOutput: (output) => jsonResult(output),
|
|
43
|
+
}, loadServices, serverContext);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=debug-wait-for-stop.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 createDiagnosticsTools(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool[];
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { createDebugListSessionsTool } from './debug-list-sessions.js';
|
|
7
|
+
import { createDebugStartSessionTool } from './debug-start-session.js';
|
|
8
|
+
import { createDebugEndSessionTool } from './debug-end-session.js';
|
|
9
|
+
import { createDebugSetBreakpointsTool } from './debug-set-breakpoints.js';
|
|
10
|
+
import { createDebugWaitForStopTool } from './debug-wait-for-stop.js';
|
|
11
|
+
import { createDebugGetStackTool } from './debug-get-stack.js';
|
|
12
|
+
import { createDebugGetVariablesTool } from './debug-get-variables.js';
|
|
13
|
+
import { createDebugEvaluateTool } from './debug-evaluate.js';
|
|
14
|
+
import { createDebugContinueTool } from './debug-continue.js';
|
|
15
|
+
import { createDebugStepTools } from './debug-step.js';
|
|
16
|
+
import { createDebugCaptureAtBreakpointTool } from './debug-capture-at-breakpoint.js';
|
|
17
|
+
export function createDiagnosticsTools(loadServices, serverContext) {
|
|
18
|
+
return [
|
|
19
|
+
createDebugListSessionsTool(loadServices, serverContext),
|
|
20
|
+
createDebugStartSessionTool(loadServices, serverContext),
|
|
21
|
+
createDebugEndSessionTool(loadServices, serverContext),
|
|
22
|
+
createDebugSetBreakpointsTool(loadServices, serverContext),
|
|
23
|
+
createDebugWaitForStopTool(loadServices, serverContext),
|
|
24
|
+
createDebugGetStackTool(loadServices, serverContext),
|
|
25
|
+
createDebugGetVariablesTool(loadServices, serverContext),
|
|
26
|
+
createDebugEvaluateTool(loadServices, serverContext),
|
|
27
|
+
createDebugContinueTool(loadServices, serverContext),
|
|
28
|
+
...createDebugStepTools(loadServices, serverContext),
|
|
29
|
+
createDebugCaptureAtBreakpointTool(loadServices, serverContext),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DebugSessionManager, SourceMapper, SdapiBreakpoint, SdapiScriptThread } from '@salesforce/b2c-tooling-sdk/operations/debug';
|
|
2
|
+
import type { CartridgeMapping } from '@salesforce/b2c-tooling-sdk/operations/code';
|
|
3
|
+
import type { ToolExecutionContext } from '../adapter.js';
|
|
4
|
+
export interface HaltWaiter {
|
|
5
|
+
resolve: (thread: SdapiScriptThread) => void;
|
|
6
|
+
reject: (error: Error) => void;
|
|
7
|
+
timer: ReturnType<typeof setTimeout>;
|
|
8
|
+
}
|
|
9
|
+
export interface DebugSessionEntry {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
hostname: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
manager: DebugSessionManager;
|
|
14
|
+
sourceMapper: SourceMapper;
|
|
15
|
+
cartridges: CartridgeMapping[];
|
|
16
|
+
breakpoints: SdapiBreakpoint[];
|
|
17
|
+
haltWaiters: HaltWaiter[];
|
|
18
|
+
createdAt: number;
|
|
19
|
+
lastActivityAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RegisterSessionOptions {
|
|
22
|
+
hostname: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
manager: DebugSessionManager;
|
|
25
|
+
sourceMapper: SourceMapper;
|
|
26
|
+
cartridges: CartridgeMapping[];
|
|
27
|
+
}
|
|
28
|
+
export declare class DebugSessionRegistry {
|
|
29
|
+
private cleanupTimer;
|
|
30
|
+
private readonly sessions;
|
|
31
|
+
constructor();
|
|
32
|
+
destroyAll(): Promise<void>;
|
|
33
|
+
destroySession(sessionId: string): Promise<void>;
|
|
34
|
+
findByHostAndClientId(hostname: string, clientId: string): DebugSessionEntry | undefined;
|
|
35
|
+
getSession(sessionId: string): DebugSessionEntry | undefined;
|
|
36
|
+
getSessionOrThrow(sessionId: string): DebugSessionEntry;
|
|
37
|
+
listSessions(): DebugSessionEntry[];
|
|
38
|
+
registerSession(opts: RegisterSessionOptions): DebugSessionEntry;
|
|
39
|
+
/**
|
|
40
|
+
* Wait for any thread in the session to halt.
|
|
41
|
+
*
|
|
42
|
+
* If a thread is already halted in the session's known threads, returns it
|
|
43
|
+
* immediately. Otherwise registers a halt waiter that resolves when the
|
|
44
|
+
* `onThreadStopped` callback fires, or returns null on timeout.
|
|
45
|
+
*/
|
|
46
|
+
waitForHalt(entry: DebugSessionEntry, timeoutMs: number): Promise<null | SdapiScriptThread>;
|
|
47
|
+
private cleanupIdleSessions;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the registry from the tool execution context, throwing a clear
|
|
51
|
+
* error if it's missing, then look up the session by ID. Used by every
|
|
52
|
+
* debug tool that takes a session_id.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getSessionEntry(context: ToolExecutionContext, sessionId: string): DebugSessionEntry;
|
|
55
|
+
/** Resolve the registry from the tool context (for tools that don't need a specific session). */
|
|
56
|
+
export declare function getRegistry(context: ToolExecutionContext): DebugSessionRegistry;
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
7
|
+
import { getLogger } from '@salesforce/b2c-tooling-sdk/logging';
|
|
8
|
+
const IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
9
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
export class DebugSessionRegistry {
|
|
11
|
+
cleanupTimer;
|
|
12
|
+
sessions = new Map();
|
|
13
|
+
constructor() {
|
|
14
|
+
this.cleanupTimer = setInterval(() => {
|
|
15
|
+
this.cleanupIdleSessions().catch(() => { });
|
|
16
|
+
}, CLEANUP_INTERVAL_MS);
|
|
17
|
+
this.cleanupTimer.unref();
|
|
18
|
+
}
|
|
19
|
+
async destroyAll() {
|
|
20
|
+
if (this.cleanupTimer) {
|
|
21
|
+
clearInterval(this.cleanupTimer);
|
|
22
|
+
this.cleanupTimer = undefined;
|
|
23
|
+
}
|
|
24
|
+
const destroyPromises = [...this.sessions.keys()].map((id) => this.destroySession(id));
|
|
25
|
+
await Promise.allSettled(destroyPromises);
|
|
26
|
+
}
|
|
27
|
+
async destroySession(sessionId) {
|
|
28
|
+
const entry = this.sessions.get(sessionId);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return;
|
|
31
|
+
for (const waiter of entry.haltWaiters) {
|
|
32
|
+
clearTimeout(waiter.timer);
|
|
33
|
+
waiter.reject(new Error('Debug session ended'));
|
|
34
|
+
}
|
|
35
|
+
entry.haltWaiters.length = 0;
|
|
36
|
+
try {
|
|
37
|
+
await entry.manager.disconnect();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Best-effort disconnect
|
|
41
|
+
}
|
|
42
|
+
this.sessions.delete(sessionId);
|
|
43
|
+
}
|
|
44
|
+
findByHostAndClientId(hostname, clientId) {
|
|
45
|
+
for (const entry of this.sessions.values()) {
|
|
46
|
+
if (entry.hostname === hostname && entry.clientId === clientId) {
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
getSession(sessionId) {
|
|
53
|
+
return this.sessions.get(sessionId);
|
|
54
|
+
}
|
|
55
|
+
getSessionOrThrow(sessionId) {
|
|
56
|
+
const entry = this.sessions.get(sessionId);
|
|
57
|
+
if (!entry) {
|
|
58
|
+
throw new Error(`No debug session found with id "${sessionId}". Use debug_list_sessions to see active sessions.`);
|
|
59
|
+
}
|
|
60
|
+
entry.lastActivityAt = Date.now();
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
listSessions() {
|
|
64
|
+
return [...this.sessions.values()];
|
|
65
|
+
}
|
|
66
|
+
registerSession(opts) {
|
|
67
|
+
const { hostname, clientId, manager, sourceMapper, cartridges } = opts;
|
|
68
|
+
const existing = this.findByHostAndClientId(hostname, clientId);
|
|
69
|
+
if (existing) {
|
|
70
|
+
throw new Error(`A debug session already exists for ${hostname} with client ID "${clientId}" ` +
|
|
71
|
+
`(session_id: "${existing.sessionId}"). ` +
|
|
72
|
+
`End it with debug_end_session first, or use a different client_id.`);
|
|
73
|
+
}
|
|
74
|
+
const sessionId = randomUUID();
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const entry = {
|
|
77
|
+
sessionId,
|
|
78
|
+
hostname,
|
|
79
|
+
clientId,
|
|
80
|
+
manager,
|
|
81
|
+
sourceMapper,
|
|
82
|
+
cartridges,
|
|
83
|
+
breakpoints: [],
|
|
84
|
+
haltWaiters: [],
|
|
85
|
+
createdAt: now,
|
|
86
|
+
lastActivityAt: now,
|
|
87
|
+
};
|
|
88
|
+
this.sessions.set(sessionId, entry);
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wait for any thread in the session to halt.
|
|
93
|
+
*
|
|
94
|
+
* If a thread is already halted in the session's known threads, returns it
|
|
95
|
+
* immediately. Otherwise registers a halt waiter that resolves when the
|
|
96
|
+
* `onThreadStopped` callback fires, or returns null on timeout.
|
|
97
|
+
*/
|
|
98
|
+
async waitForHalt(entry, timeoutMs) {
|
|
99
|
+
const halted = entry.manager.getKnownThreads().find((t) => t.status === 'halted');
|
|
100
|
+
if (halted)
|
|
101
|
+
return halted;
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
const idx = entry.haltWaiters.findIndex((w) => w.timer === timer);
|
|
105
|
+
if (idx !== -1)
|
|
106
|
+
entry.haltWaiters.splice(idx, 1);
|
|
107
|
+
resolve(null);
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
entry.haltWaiters.push({ resolve: (t) => resolve(t), reject, timer });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async cleanupIdleSessions() {
|
|
113
|
+
const logger = getLogger();
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const idleSessions = [...this.sessions.entries()].filter(([, entry]) => now - entry.lastActivityAt > IDLE_TTL_MS);
|
|
116
|
+
await Promise.allSettled(idleSessions.map(([sessionId, entry]) => {
|
|
117
|
+
logger.info({ sessionId, hostname: entry.hostname }, 'Cleaning up idle debug session');
|
|
118
|
+
return this.destroySession(sessionId);
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the registry from the tool execution context, throwing a clear
|
|
124
|
+
* error if it's missing, then look up the session by ID. Used by every
|
|
125
|
+
* debug tool that takes a session_id.
|
|
126
|
+
*/
|
|
127
|
+
export function getSessionEntry(context, sessionId) {
|
|
128
|
+
const registry = context.serverContext?.debugSessions;
|
|
129
|
+
if (!registry) {
|
|
130
|
+
throw new Error('Debug session registry not available');
|
|
131
|
+
}
|
|
132
|
+
return registry.getSessionOrThrow(sessionId);
|
|
133
|
+
}
|
|
134
|
+
/** Resolve the registry from the tool context (for tools that don't need a specific session). */
|
|
135
|
+
export function getRegistry(context) {
|
|
136
|
+
const registry = context.serverContext?.debugSessions;
|
|
137
|
+
if (!registry) {
|
|
138
|
+
throw new Error('Debug session registry not available');
|
|
139
|
+
}
|
|
140
|
+
return registry;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=session-registry.js.map
|
package/dist/tools/index.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
package/oclif.manifest.json
CHANGED
|
@@ -249,6 +249,7 @@
|
|
|
249
249
|
"multiple": true,
|
|
250
250
|
"options": [
|
|
251
251
|
"client-credentials",
|
|
252
|
+
"jwt",
|
|
252
253
|
"implicit",
|
|
253
254
|
"basic",
|
|
254
255
|
"api-key"
|
|
@@ -274,6 +275,33 @@
|
|
|
274
275
|
"multiple": false,
|
|
275
276
|
"type": "option"
|
|
276
277
|
},
|
|
278
|
+
"jwt-cert": {
|
|
279
|
+
"description": "Path to JWT certificate file (cert.pem) for JWT Bearer authentication",
|
|
280
|
+
"env": "SFCC_JWT_CERT",
|
|
281
|
+
"helpGroup": "AUTH",
|
|
282
|
+
"name": "jwt-cert",
|
|
283
|
+
"hasDynamicHelp": false,
|
|
284
|
+
"multiple": false,
|
|
285
|
+
"type": "option"
|
|
286
|
+
},
|
|
287
|
+
"jwt-key": {
|
|
288
|
+
"description": "Path to JWT private key file (key.pem) for JWT Bearer authentication",
|
|
289
|
+
"env": "SFCC_JWT_KEY",
|
|
290
|
+
"helpGroup": "AUTH",
|
|
291
|
+
"name": "jwt-key",
|
|
292
|
+
"hasDynamicHelp": false,
|
|
293
|
+
"multiple": false,
|
|
294
|
+
"type": "option"
|
|
295
|
+
},
|
|
296
|
+
"jwt-passphrase": {
|
|
297
|
+
"description": "Passphrase for encrypted JWT private key",
|
|
298
|
+
"env": "SFCC_JWT_PASSPHRASE",
|
|
299
|
+
"helpGroup": "AUTH",
|
|
300
|
+
"name": "jwt-passphrase",
|
|
301
|
+
"hasDynamicHelp": false,
|
|
302
|
+
"multiple": false,
|
|
303
|
+
"type": "option"
|
|
304
|
+
},
|
|
277
305
|
"server": {
|
|
278
306
|
"char": "s",
|
|
279
307
|
"description": "B2C instance hostname",
|
|
@@ -390,5 +418,5 @@
|
|
|
390
418
|
"enableJsonFlag": false
|
|
391
419
|
}
|
|
392
420
|
},
|
|
393
|
-
"version": "1.0
|
|
421
|
+
"version": "1.1.0"
|
|
394
422
|
}
|
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.0
|
|
4
|
+
"version": "1.1.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.8.1",
|
|
81
81
|
"postcss": "8.5.6",
|
|
82
82
|
"zod": "3.25.76",
|
|
83
|
-
"@salesforce/b2c-tooling-sdk": "1.
|
|
83
|
+
"@salesforce/b2c-tooling-sdk": "1.9.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@eslint/compat": "^1",
|