Skip to content

Commit 5336e32

Browse files
committed
Improve MCP debug tools based on real-world agent feedback
1. capture_at_breakpoint: add trigger_url parameter so the tool can fire the HTTP request itself after arming the breakpoint, avoiding the blocking coordination problem with LLM tool-calling. 2. set_breakpoints: surface warnings when a path cannot be round-trip mapped back to a local file. Set verified=false for unmapped paths instead of silently accepting them. 3. Tool descriptions: steer toward the non-blocking workflow (set_breakpoints → trigger → list_sessions → inspect) rather than leading with blocking tools. 4. list_sessions: include active breakpoints with resolved server paths so agents can inspect what's armed on the server. 5. start_session: return cartridge_mappings (name → local path) so agents can verify cartridge discovery and path mapping.
1 parent d10d760 commit 5336e32

5 files changed

Lines changed: 79 additions & 29 deletions

File tree

packages/b2c-dx-mcp/src/tools/diagnostics/debug-capture-at-breakpoint.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface CaptureInput {
2828
expressions?: string[];
2929
timeout_ms?: number;
3030
auto_continue?: boolean;
31+
trigger_url?: string;
3132
}
3233

3334
interface CaptureOutput {
@@ -58,6 +59,7 @@ interface CaptureOutput {
5859
result: string;
5960
}>;
6061
auto_continued: boolean;
62+
trigger_status?: number;
6163
}
6264

6365
export function createDebugCaptureAtBreakpointTool(
@@ -68,10 +70,10 @@ export function createDebugCaptureAtBreakpointTool(
6870
{
6971
name: 'debug_capture_at_breakpoint',
7072
description:
71-
'Set a breakpoint, wait for it to be hit, and capture a diagnostic snapshot (stack, variables, expression results). ' +
72-
'IMPORTANT: This tool BLOCKS until the breakpoint is hit or the timeout expires — the user or an external process must trigger a request on the instance while this tool is waiting. ' +
73-
'Optionally resumes the thread after capture. ' +
74-
'Combines debug_set_breakpoints, debug_wait_for_stop, debug_get_stack, debug_get_variables, and debug_evaluate in a single call.',
73+
'Set a breakpoint, optionally trigger an HTTP request, wait for the breakpoint to be hit, and capture a diagnostic snapshot (stack, variables, expression results). ' +
74+
'Use trigger_url to have the tool fire the request itself (recommended) — this avoids needing to coordinate a separate request while the tool blocks. ' +
75+
'Without trigger_url, the tool BLOCKS until the breakpoint is hit or timeout expires and requires the user to trigger a request externally. ' +
76+
'For more control, use the non-blocking workflow: debug_set_breakpoints → trigger request → debug_list_sessions (check halted_threads) → debug_get_variables.',
7577
toolsets: ['CARTRIDGES', 'SCAPI', 'STOREFRONTNEXT'],
7678
inputSchema: {
7779
session_id: z.string().describe('Session ID returned by debug_start_session.'),
@@ -92,6 +94,13 @@ export function createDebugCaptureAtBreakpointTool(
9294
.boolean()
9395
.optional()
9496
.describe('If true, resume the thread after capturing the snapshot. Defaults to false.'),
97+
trigger_url: z
98+
.string()
99+
.optional()
100+
.describe(
101+
'URL to request after arming the breakpoint. The tool fires this HTTP GET in the background, then waits for the breakpoint to halt. ' +
102+
'This is the recommended approach — it avoids needing to coordinate a separate request while the tool blocks.',
103+
),
95104
},
96105
async execute(args, context) {
97106
const registry = context.serverContext?.debugSessions;
@@ -104,7 +113,6 @@ export function createDebugCaptureAtBreakpointTool(
104113

105114
const scriptPath = resolveBreakpointPath(args.file, entry.sourceMapper, entry.cartridges);
106115

107-
// Set breakpoint (adds to existing)
108116
const bpInput: BreakpointInput = {
109117
script_path: scriptPath,
110118
line_number: args.line,
@@ -126,6 +134,14 @@ export function createDebugCaptureAtBreakpointTool(
126134
script_path: scriptPath,
127135
};
128136

137+
// Fire trigger URL in the background (it will hang when the breakpoint halts the thread)
138+
let triggerPromise: Promise<number | undefined> | undefined;
139+
if (args.trigger_url) {
140+
triggerPromise = fetch(args.trigger_url, {redirect: 'follow'})
141+
.then((r) => r.status)
142+
.catch((): undefined => undefined);
143+
}
144+
129145
// Wait for halt
130146
const haltedThread = entry.manager.getKnownThreads().find((t) => t.status === 'halted');
131147
let thread: null | SdapiScriptThread = haltedThread ?? null;
@@ -150,7 +166,6 @@ export function createDebugCaptureAtBreakpointTool(
150166
};
151167
}
152168

153-
// Capture stack
154169
const threadDetail = await entry.manager.client.getThread(thread.id);
155170
const stack = threadDetail.call_stack.map((frame) => ({
156171
index: frame.index,
@@ -160,7 +175,6 @@ export function createDebugCaptureAtBreakpointTool(
160175
script_path: frame.location.script_path,
161176
}));
162177

163-
// Capture variables (top frame)
164178
const varsResult = await entry.manager.client.getVariables(thread.id, 0);
165179
const variables = varsResult.object_members.map((m) => ({
166180
name: m.name,
@@ -170,7 +184,6 @@ export function createDebugCaptureAtBreakpointTool(
170184
has_children: !PRIMITIVE_TYPES.has(m.type),
171185
}));
172186

173-
// Evaluate expressions (sequential — each must complete before the next on the debugger)
174187
const evaluations: Array<{expression: string; result: string}> = [];
175188
if (args.expressions) {
176189
for (const expr of args.expressions) {
@@ -187,13 +200,14 @@ export function createDebugCaptureAtBreakpointTool(
187200
}
188201
}
189202

190-
// Optionally continue
191203
let autoContinued = false;
192204
if (args.auto_continue) {
193205
await entry.manager.resume(thread.id);
194206
autoContinued = true;
195207
}
196208

209+
const triggerStatus = triggerPromise ? await triggerPromise : undefined;
210+
197211
return {
198212
breakpoint: breakpointInfo,
199213
halted: true,
@@ -202,6 +216,7 @@ export function createDebugCaptureAtBreakpointTool(
202216
variables,
203217
evaluations: evaluations.length > 0 ? evaluations : undefined,
204218
auto_continued: autoContinued,
219+
trigger_status: triggerStatus,
205220
};
206221
},
207222
formatOutput: (output) => jsonResult(output),

packages/b2c-dx-mcp/src/tools/diagnostics/debug-list-sessions.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ interface ListSessionsOutput {
1515
hostname: string;
1616
client_id: string;
1717
halted_threads: number[];
18+
breakpoints: Array<{
19+
id: number;
20+
file: null | string;
21+
line: number;
22+
script_path: string;
23+
}>;
1824
created_at: string;
1925
last_activity_at: string;
2026
}>;
@@ -28,9 +34,9 @@ export function createDebugListSessionsTool(
2834
{
2935
name: 'debug_list_sessions',
3036
description:
31-
'List all active script debugger sessions. ' +
32-
'Returns session IDs, connected hostnames, and any currently halted threads. ' +
33-
'Use this to discover existing sessions before creating a new one.',
37+
'List all active script debugger sessions with their breakpoints and halted threads. ' +
38+
'Use this to check session state: whether breakpoints are armed, which threads are halted, and whether you need to call debug_get_variables or debug_continue. ' +
39+
'This is the recommended way to poll for halted threads in the non-blocking debug workflow.',
3440
toolsets: ['CARTRIDGES', 'SCAPI', 'STOREFRONTNEXT'],
3541
inputSchema: {},
3642
async execute(_args, context) {
@@ -49,6 +55,12 @@ export function createDebugListSessionsTool(
4955
.getKnownThreads()
5056
.filter((t) => t.status === 'halted')
5157
.map((t) => t.id),
58+
breakpoints: entry.breakpoints.map((bp) => ({
59+
id: bp.id,
60+
file: entry.sourceMapper.toLocalPath(bp.script_path) ?? null,
61+
line: bp.line_number,
62+
script_path: bp.script_path,
63+
})),
5264
created_at: new Date(entry.createdAt).toISOString(),
5365
last_activity_at: new Date(entry.lastActivityAt).toISOString(),
5466
})),

packages/b2c-dx-mcp/src/tools/diagnostics/debug-set-breakpoints.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ interface SetBreakpointsInput {
1616
breakpoints: Array<{file: string; line: number; condition?: string}>;
1717
}
1818

19+
interface BreakpointResult {
20+
id: number;
21+
file: null | string;
22+
line: number;
23+
script_path: string;
24+
verified: boolean;
25+
condition?: string;
26+
}
27+
1928
interface SetBreakpointsOutput {
20-
breakpoints: Array<{
21-
id: number;
22-
file: null | string;
23-
line: number;
24-
script_path: string;
25-
verified: boolean;
26-
condition?: string;
27-
}>;
29+
breakpoints: BreakpointResult[];
30+
warnings?: string[];
2831
}
2932

3033
export function createDebugSetBreakpointsTool(
@@ -36,8 +39,8 @@ export function createDebugSetBreakpointsTool(
3639
name: 'debug_set_breakpoints',
3740
description:
3841
'Set breakpoints in a debug session. Replaces all previously set breakpoints. ' +
39-
'Accepts local file paths which are mapped to server script paths via cartridge mappings. ' +
40-
'You can also pass server paths directly (starting with /).',
42+
'Accepts local file paths (mapped to server paths via cartridge discovery), cartridge-prefixed paths (e.g. app_storefront/cartridge/controllers/Cart.js), or server paths starting with /. ' +
43+
'Check the "verified" field and "warnings" in the response — if a path could not be mapped to a known cartridge, it will be flagged.',
4144
toolsets: ['CARTRIDGES', 'SCAPI', 'STOREFRONTNEXT'],
4245
inputSchema: {
4346
session_id: z.string().describe('Session ID returned by debug_start_session.'),
@@ -65,12 +68,23 @@ export function createDebugSetBreakpointsTool(
6568
}
6669

6770
const entry = registry.getSessionOrThrow(args.session_id);
71+
const warnings: string[] = [];
6872

69-
const bpInputs: BreakpointInput[] = args.breakpoints.map((bp) => ({
70-
script_path: resolveBreakpointPath(bp.file, entry.sourceMapper, entry.cartridges),
71-
line_number: bp.line,
72-
condition: bp.condition,
73-
}));
73+
const bpInputs: BreakpointInput[] = args.breakpoints.map((bp) => {
74+
const scriptPath = resolveBreakpointPath(bp.file, entry.sourceMapper, entry.cartridges);
75+
const roundTrip = entry.sourceMapper.toLocalPath(scriptPath);
76+
if (!roundTrip) {
77+
warnings.push(
78+
`"${bp.file}" resolved to server path "${scriptPath}" but could not be mapped back to a local file. ` +
79+
`Verify this path exists on the instance.`,
80+
);
81+
}
82+
return {
83+
script_path: scriptPath,
84+
line_number: bp.line,
85+
condition: bp.condition,
86+
};
87+
});
7488

7589
const result = await entry.manager.setBreakpoints(bpInputs);
7690
entry.breakpoints = result;
@@ -81,9 +95,10 @@ export function createDebugSetBreakpointsTool(
8195
file: entry.sourceMapper.toLocalPath(bp.script_path) ?? null,
8296
line: bp.line_number,
8397
script_path: bp.script_path,
84-
verified: true,
98+
verified: entry.sourceMapper.toLocalPath(bp.script_path) !== undefined,
8599
condition: bp.condition,
86100
})),
101+
warnings: warnings.length > 0 ? warnings : undefined,
87102
};
88103
},
89104
formatOutput: (output) => jsonResult(output),

packages/b2c-dx-mcp/src/tools/diagnostics/debug-start-session.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface StartSessionOutput {
2626
session_id: string;
2727
hostname: string;
2828
cartridges: string[];
29+
cartridge_mappings: Record<string, string>;
2930
warnings: string[];
3031
}
3132

@@ -105,10 +106,16 @@ export function createDebugStartSessionTool(
105106

106107
const entry = registry.registerSession(hostname, clientId, manager, sourceMapper, cartridges);
107108

109+
const cartridgeMappings: Record<string, string> = {};
110+
for (const c of cartridges) {
111+
cartridgeMappings[c.name] = c.src;
112+
}
113+
108114
return {
109115
session_id: entry.sessionId,
110116
hostname,
111117
cartridges: cartridges.map((c) => c.name),
118+
cartridge_mappings: cartridgeMappings,
112119
warnings,
113120
};
114121
},

packages/b2c-dx-mcp/src/tools/diagnostics/debug-wait-for-stop.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export function createDebugWaitForStopTool(
4141
description:
4242
'Wait for a thread to halt at a breakpoint or step. ' +
4343
'Returns immediately if a thread is already halted. ' +
44-
'Otherwise BLOCKS until a halt occurs or the timeout expires — the user or an external process must trigger a request on the instance while this tool is waiting.',
44+
'Otherwise BLOCKS until a halt occurs or the timeout expires. ' +
45+
'Preferred non-blocking alternative: after debug_set_breakpoints, trigger the request yourself, then use debug_list_sessions to check if halted_threads is non-empty before calling debug_get_stack/debug_get_variables.',
4546
toolsets: ['CARTRIDGES', 'SCAPI', 'STOREFRONTNEXT'],
4647
inputSchema: {
4748
session_id: z.string().describe('Session ID returned by debug_start_session.'),

0 commit comments

Comments
 (0)