Skip to content

Breakpoint-binding failures are under-surfaced: start_debugging hides unbound breakpoints, and known-never-bind function breakpoints are still accepted #467

Description

@debugmcpdev

Two places where a breakpoint that will never fire is reported in a way that reads like success. In both, the server already holds the information needed to say otherwise — it just isn't surfaced at the moment the agent would act on it. Same theme as #439 ("the run that worked looking like the one that failed"), one step earlier in the loop.

Case 1 — start_debugging returns state: "stopped" with no hint that a breakpoint didn't bind

set_breakpoint  { file: "examples/rust/hello_world/src/main.rs", line: 27 }
→ { "success": true, "verified": false }          // normal pre-launch

start_debugging { scriptPath: "…/target-linux/debug/hello_world" }
→ { "success": true, "state": "stopped", "data": { "stopOnEntrySuccessful": false } }

The program ran to completion because the breakpoint never bound. Nothing in the response says so — the only signal is state: "stopped" where the agent expected "paused", which requires the agent to already suspect the problem.

The explanation exists and is genuinely excellent — it's just one tool call away, and nothing points there:

list_breakpoints { }
→ { "verified": false,
    "message": "Breakpoint at /workspace/examples/rust/hello_world/src/main.rs:27 could not be
                resolved, but a valid location was found at /workspace/rust/hello_world/src/main.rs:27" }

That message names the exact fix (a sourceMap entry). An agent that doesn't think to call list_breakpoints after an unexpectedly-stopped launch never sees it.

Suggested fix: when a launch reaches stopped and the session has unverified breakpoints, attach a warning to the start_debugging response summarising them (count + the per-breakpoint message already stored). Cheap, uses existing data, and closes the loop exactly where the agent is looking.

Case 2 — a breakpoint the server knows can never bind is still accepted

set_breakpoint { function: "main" }        // go session
→ { "success": true, "verified": false,
    "warning": "Go function breakpoints use package-qualified names — for func main in package
                main use 'main.main'. A bare identifier like 'main' will never bind" }

The warning is genuinely good — clear, specific, names the correct form. But the call still returns success: true for a breakpoint the server has just stated will never bind, and it then sits in list_breakpoints looking like a live breakpoint. Confirmed: launching with it never stops.

Suggested fix, in rough order of preference:

  1. Auto-qualify. The server knows the language, the package, and the correct form. mainmain.main is mechanical. Other adapters (.NET Add, Rust calculate_sum) bind bare names fine, so today the agent must carry per-adapter naming trivia that the server already has.
  2. Return the corrected name in a structured field (e.g. suggestedName: "main.main") so it's machine-actionable rather than prose an agent has to parse.
  3. Reject it. If the server is certain it can never bind, success: false with the same message is more honest than a permanently-dead breakpoint in the list.

(1) and (2) compose well: qualify it, and say that you did.

Why these matter for agent ergonomics

Both are cases where an agent proceeds on a false belief that a breakpoint is live, and no response contradicts it until the debugging session has already gone wrong. The diagnostics are already written and already good — the gap is purely where they surface.

Found during a full /testdebugger sweep (9 servers × 3 backends).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions