Skip to content
1 change: 1 addition & 0 deletions changelog.d/710.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**A `noDebug` launch says what it did to the debugger instead of blaming your breakpoints** — `dapLaunchArgs.noDebug: true` is DAP's "launch without enabling debugging", and where the adapter honours it no breakpoint binds and no stop ever arrives; the session then read as a mystery — a short script ended `stopped` with the #467 "check the file path and line" warning for breakpoints that were never going to bind, a server stayed `running` while the caller waited for a stop that could not come. Whether the flag turns the debugger off is now the adapter policy's word (`honoursNoDebug`), measured rather than assumed: js-debug honours it, and so do debugpy, Delve and CodeLLDB (their launches currently fail under it — #746); rdbg, netcoredbg and the Java bridge ignore it, and the rust launch transform never forwards it. Where the debugger is off, `start_debugging` warns when the flag is set alongside something the caller asked to stop on — line or function breakpoints, an *explicit* `breakOnExceptions` other than `none` (the policy default does not count, so a deliberate plain run stays silent), or `stopOnEntry` — naming each thing that will not fire, withholds the breakpoint-shaped launch warnings (#308, #467, #469) that presuppose a debugger, and no longer waits for an entry stop that cannot come. Where the adapter ignores the flag, the warning says it had no effect and the usual diagnostics stay. The flag is read the way the adapter will see it (`adapterLaunchConfig` over `dapLaunchArgs` over the server defaults), and the decision lives in the launcher, so `restart_debugging` and a `dryRunSpawn` configuration check carry the same warning; the `dapLaunchArgs` schema documents the flag. Found by the review of PR #706 (#710)
1 change: 1 addition & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ Starts debugging a script.
- `dapLaunchArgs` (object, optional): Standard DAP launch arguments:
- `stopOnEntry` (boolean): Stop at first line (default `false` — the opposite of attach, which pauses unless `stopOnEntry` is `false`)
- `justMyCode` (boolean): Debug only user code (default `true`). JavaScript launch: `true` blackboxes `node_modules` through js-debug's `skipFiles` and keeps js-debug's smart-stepper on, so a pause or step that lands in skipped code (Node internals, `node_modules`) is stepped through and may never land (`pending: true`, with an explanation); `false` drops `node_modules` from the skip list and turns the stepper off, so steps land inside dependencies and `pause_execution` lands as soon as any JavaScript runs (issue #678). A caller `skipFiles` replaces the default list. Source maps are on for every JavaScript launch, `.js` programs included — stops report `src/*.ts` when maps and sources are present; `adapterLaunchConfig: { sourceMaps: false }` opts out (issue #684)
- `noDebug` (boolean): DAP's "launch without enabling debugging" flag (default `false`). What it does depends on the adapter, and the response says which case you are in. **Honoured** — the debugger is off: no breakpoint binds, no exception filter arms, no entry stop lands, and no stop ever arrives, so a short script ends `stopped` and a server stays `running`. That is a legitimate plain run and says nothing by itself; when the session has breakpoints (line or function), an explicit `breakOnExceptions` other than `"none"`, or `stopOnEntry`, the `warning` says the debugger is off and names what will not fire, in place of the unbound-breakpoint diagnostics that would otherwise send you to check paths that are fine (issue #710). js-debug honours it today; debugpy, Delve and CodeLLDB honour it as well but their launches currently fail under it (issue #746). **Ignored** — rdbg, netcoredbg and the Java bridge run the debugger regardless, and the rust launch transform never forwards the flag: the `warning` says the flag had no effect and everything works as usual. The same warning comes back from `restart_debugging` and from a `dryRunSpawn` check.
- Additional DAP launch keys (`program`, `cwd`, `env`, language-specific options) pass through to the adapter. Top-level parameters do **not** belong here: a nested `breakOnExceptions` is honored as an alias (the top-level value wins if both are given) and reported via a `warning` in the response; other misplaced top-level keys (`dryRunSpawn`, `sessionId`, `scriptPath`, `adapterLaunchConfig`) are stripped with a warning instead of silently riding into the launch config.
- `adapterLaunchConfig` (object, optional): Adapter-specific launch configuration overrides. Use this for language-specific settings that go beyond standard DAP arguments (e.g., `mainClass` and `classpath` for Java, `buildCommand` for Rust). For Rust, `_adapterSettings` passes through to CodeLLDB (issue #441) — e.g. `{"_adapterSettings": {"scriptConfig": {"lang": {"rust": {"sysroot": "/path"}}}}}` points the Rust formatter lookup at an explicit sysroot; the `CODELLDB_RUST_SYSROOT` env var does the same without per-launch config (a user-supplied `_adapterSettings` value wins over the env var).
- `dryRunSpawn` (boolean, optional): Test spawn without actually starting
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/interfaces/adapter-policy-cpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {

export const CppAdapterPolicy = {
name: 'cpp',
// CodeLLDB honours noDebug by refusing debugger requests ("Not supported in
// noDebug mode"), which our launch sequence currently trips over (issue #746).
honoursNoDebug: true,
supportsLogPoints: true,
supportsFunctionBreakpoints: true,
// Unlike Rust (issue #303), a bare 'main' in C/C++ IS the user's entry
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/interfaces/adapter-policy-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import type { DapClientBehavior, DapClientContext, ReverseRequestResult } from '

export const GoAdapterPolicy = {
name: 'go',
// Delve runs the program undebugged under noDebug (verified live: the
// breakpoint never fired, the process exited 0); the launch currently fails
// init because no `initialized` follows (issue #746).
honoursNoDebug: true,
supportsLogPoints: true,
supportsFunctionBreakpoints: true,
/**
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/interfaces/adapter-policy-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export const JsDebugAdapterPolicy = {
// Names in late-loaded modules stay verified:false by design and bind at
// the next pause — unverified-at-launch is not a failure here (issue #308).
functionBreakpointsBindLate: true,
// Verified live: "Running with noDebug, so debug domains are disabled" —
// the program runs, nothing binds, no stop comes (issue #710).
honoursNoDebug: true,
supportsReverseStartDebugging: true,
childSessionStrategy: 'launchWithPendingTarget',
buildChildStartArgs: (pendingId: string, parentConfig: Record<string, unknown>) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/interfaces/adapter-policy-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import type { DapClientBehavior, DapClientContext, ReverseRequestResult } from '

export const PythonAdapterPolicy = {
name: 'python',
// debugpy runs the program undebugged under noDebug (verified live); the
// launch currently fails init because no `initialized` follows (issue #746).
honoursNoDebug: true,
supportsLogPoints: true,
supportsFunctionBreakpoints: true,
supportsReverseStartDebugging: false,
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/src/interfaces/adapter-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ export interface AdapterPolicy {
*/
functionBreakpointsBindLate?: boolean;

/**
* True when the adapter honours DAP's `noDebug` launch flag by not enabling
* the debugger, so breakpoints, exception filters and an entry stop are
* inert on such a launch and the start_debugging response says so
* (issue #710). Measured per adapter, not assumed: js-debug, debugpy, Delve
* and CodeLLDB honour it; rdbg, netcoredbg, the JDI bridge and the mock
* adapter ignore it, and the rust launch transform never forwards it. Left
* undefined, the debugger is taken to stay on and a caller who set the
* flag is told it had no effect.
*/
honoursNoDebug?: boolean;

/**
* Strategy for how to create/attach to the child session when reverse startDebugging occurs
*/
Expand Down
7 changes: 6 additions & 1 deletion src/proxy/dap-proxy-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,12 @@ export class DapProxyWorker {
(this.currentInitPayload?.initialFunctionBreakpoints?.length ?? 0) > 0
) {
const launchArgs = (payload.dapArgs ?? {}) as Record<string, unknown>;
if (launchArgs.stopOnEntry !== true) {
// Under an honoured noDebug the debugger is off (issue #710): no entry
// stop can come, and the function breakpoints will not bind either —
// the session layer has already told the caller so.
const noDebug = launchArgs.noDebug === true || launchArgs.noDebug === 'true';
const debuggerOff = noDebug && this.adapterPolicy.honoursNoDebug === true;
if (launchArgs.stopOnEntry !== true && !debuggerOff) {
payload.dapArgs = { ...launchArgs, stopOnEntry: true };
this.logger?.info('[Worker] Forcing stopOnEntry=true in the js-debug launch config (pending CDP function breakpoints, issue #295)');
}
Expand Down
3 changes: 2 additions & 1 deletion src/server/tool-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export function buildToolDefinitions(options: BuildToolDefinitionsOptions): Tool
type: 'object',
properties: {
stopOnEntry: { type: 'boolean', description: 'Pause at the first line before running. Default false — the opposite of attach, which pauses unless stopOnEntry is false' },
justMyCode: { type: 'boolean', description: 'Only debug user code (default true). JavaScript launch: true blackboxes node_modules via skipFiles and keeps js-debug\'s smart-stepper on, so a pause or step that lands in skipped code (Node internals, node_modules) is stepped through and may never land; false drops node_modules from skipFiles and turns the stepper off, so pauses and steps land inside dependencies' }
justMyCode: { type: 'boolean', description: 'Only debug user code (default true). JavaScript launch: true blackboxes node_modules via skipFiles and keeps js-debug\'s smart-stepper on, so a pause or step that lands in skipped code (Node internals, node_modules) is stepped through and may never land; false drops node_modules from skipFiles and turns the stepper off, so pauses and steps land inside dependencies' },
noDebug: { type: 'boolean', description: 'DAP\'s launch-without-debugging flag (default false). Where the adapter honours it (js-debug does; debugpy, Delve and CodeLLDB do too, though their launches currently fail under it) no breakpoint binds, no exception filter arms, no entry stop lands and no stop ever arrives — the response warns when breakpoints, an explicit breakOnExceptions or stopOnEntry are set alongside it. rdbg, netcoredbg and the Java bridge ignore the flag, and the rust launch never forwards it: their debugger stays on and the response says so' }
},
additionalProperties: true
},
Expand Down
91 changes: 90 additions & 1 deletion src/session/breakpoints/launch-warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* it); as free functions the purity is the signature rather than a convention.
*/
import path from 'path';
import type { AdapterPolicy } from '@debugmcp/shared';
import type { AdapterPolicy, ExceptionBreakMode } from '@debugmcp/shared';
import { normalizeBreakpointMessage } from '../../utils/breakpoint-message.js';
import type { ManagedSession } from '../session-store.js';

Expand Down Expand Up @@ -50,6 +50,95 @@ export function buildUnboundBreakpointExitWarning(
);
}

/**
* noDebug launch warning (issue #710). `noDebug: true` is DAP's "launch
* without enabling debugging". Where the adapter honours it (the policy's
* `honoursNoDebug`, measured per adapter), no breakpoint binds, no exception
* filter arms, no entry stop lands, and no `stopped` ever arrives. That is a
* legitimate way to just run the program, so the warning fires only when the
* caller also asked for a stop — line or function breakpoints in the store,
* an *explicit* breakOnExceptions other than 'none' (the caller's value, not
* the policy default the launcher fills in afterwards, which would make every
* bare noDebug run warn), or stopOnEntry — and names each thing that will not
* fire. The breakpoint-shaped launch warnings (#308, #467, #469) presuppose a
* debugger and are withheld by the launcher in that case.
*
* Where the adapter ignores the flag, the debugger stays on and the caller is
* told so instead — the flag they set changes nothing, which is worth a line.
*
* `noDebug` and `stopOnEntry` are the flags the adapter will actually see
* (adapterLaunchConfig over dapLaunchArgs, the launcher's merge order); the
* text names the flag alone since either source may have carried it.
*/
export function buildNoDebugLaunchWarning(
session: Pick<ManagedSession, 'breakpoints' | 'functionBreakpoints' | 'language'>,
launchArgs: { noDebug?: boolean; stopOnEntry?: boolean } | undefined,
explicitBreakOnExceptions: ExceptionBreakMode | undefined,
honoursNoDebug: boolean
): string | undefined {
if (launchArgs?.noDebug !== true) {
return undefined;
}
if (!honoursNoDebug) {
return (
`noDebug has no effect with the ${session.language} adapter: the debugger stays on, ` +
`and breakpoints, breakOnExceptions and stopOnEntry work as usual`
);
}
const expected: string[] = [];
let lineCount = 0;
let logpointCount = 0;
for (const bp of session.breakpoints.values()) {
if (bp.logMessage !== undefined) {
logpointCount++;
} else {
lineCount++;
}
}
const functionCount = session.functionBreakpoints?.size ?? 0;
if (lineCount > 0) {
expected.push(`${lineCount} breakpoint(s)`);
}
if (logpointCount > 0) {
expected.push(`${logpointCount} logpoint(s)`);
}
if (functionCount > 0) {
expected.push(`${functionCount} function breakpoint(s)`);
}
if (explicitBreakOnExceptions !== undefined && explicitBreakOnExceptions !== 'none') {
expected.push(`breakOnExceptions='${explicitBreakOnExceptions}'`);
}
if (launchArgs.stopOnEntry === true) {
expected.push('stopOnEntry');
}
if (expected.length === 0) {
return undefined;
}
const list =
expected.length === 1
? expected[0]
: `${expected.slice(0, -1).join(', ')} and ${expected[expected.length - 1]}`;
return (
`noDebug is true, so the debugger is disabled for this launch and no stop can arrive: ` +
`${list} will not fire. Drop noDebug to debug, or ignore this if you only meant to run the program`
);
}

/**
* The note a launch that failed under an honoured `noDebug` carries when the
* warning above had nothing to say (nothing was armed). It states the fact
* and the remedy without blaming the flag: the failure may be unrelated (a
* bad path), or it may be an adapter that cannot complete a launch under
* the flag (issue #746) — either way, the flag is what the caller needs to
* know before retrying paths and ports.
*/
export function buildNoDebugFailureNote(): string {
return (
'noDebug is true, so this launch ran with the debugger disabled. If the failure is unexpected, drop noDebug ' +
'and launch again — some adapters cannot complete a launch under the flag'
);
}

/** What a launch reports when it ended STOPPED: a sentence for the message, and the fields behind it. */
export interface RunToCompletionSummary {
/** Appended to the launch message after "Current state: stopped". */
Expand Down
Loading
Loading