Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/654.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Attaching says up front that it will pause the target** — `create_debug_session` now advertises `stopOnEntry` for attach mode, and both it and `attach_to_process` state the default in the in-band schema (omitted = pause after attach; `false` = attach without stopping, required for a live service you still need to use), while `start_debugging.dapLaunchArgs.stopOnEntry` documents its opposite `false` default. When the post-attach pause has not landed by the time the tool answers, the `message` now names it (`…; post-attach pause pending — the target stops when it next executes code (pass stopOnEntry: false to attach without pausing)`) alongside the existing `pending: true`, instead of a bare `state: "running"` that reads as "nothing happened" seconds before the target freezes. `set_breakpoint` on an attach session no longer tells a caller who already passed `line` to "use line addressing instead" — it says to drop `expectedContent` and keep `line`. The attach default itself is unchanged (#654)
2 changes: 1 addition & 1 deletion docs/agent-debugging-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This guide explains how to correctly use the MCP Debugger tools when testing deb
**How it works:**
- The multi-session architecture properly routes evaluate commands to the active debugging context
- You can immediately evaluate expressions when stopped at breakpoints
- When `stopOnEntry` is false (the default), the debugger auto-continues past entry breakpoints so execution advances to user code automatically
- When `stopOnEntry` is false (the **launch** default), the debugger auto-continues past entry breakpoints so execution advances to user code automatically. Attach is the opposite: omitting `stopOnEntry` on `attach_to_process`/`create_debug_session` pauses the target (possibly a little after the response, reported as `pending: true`) — pass `stopOnEntry: false` to attach to a live service without freezing it

### Python Variable Inspection

Expand Down
2 changes: 1 addition & 1 deletion docs/cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Compile with **`-gdwarf-4 -O0`**: full debug info, no optimization (optimized co
attach_to_process sessionId=... processId=<pid>
```

- The target is held paused after attach (`stopOnEntry` defaults to `true` for attach; pass `false` to resume immediately).
- The target is held paused after attach (`stopOnEntry` defaults to `true` for attach in every language, not just C/C++; pass `false` to resume immediately).
- `detach_from_process` leaves the target running.
- **Linux**: `kernel.yama.ptrace_scope=1` (the default on many distros) only allows attaching to child processes. For arbitrary processes: `sudo sysctl kernel.yama.ptrace_scope=0` (temporary) or run the server with `CAP_SYS_PTRACE`.
- **Windows**: attach requires same-or-higher privilege than the target.
Expand Down
4 changes: 4 additions & 0 deletions docs/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ If neither `tsx` nor `ts-node` is installed, the factory emits a warning (not an
target, including pods via `kubectl port-forward` (see
[attach presets](../../examples/kubernetes/attach-presets.md)); the target must be
started with the inspector enabled, which mcp-debugger cannot do for you
- Attach pauses the target unless you pass `stopOnEntry: false`. js-debug's pause
lands on the next event-loop dispatch, so an idle server answers
`state: "running", pending: true` (the `message` names the pending pause) and
freezes on its next request — attach to a live server with `stopOnEntry: false`
- Some advanced DAP features may not be exposed through MCP tools
- Debuggee exit codes are captured via an injected preload (js-debug itself
never emits a DAP `exited` event), so `exitCode` is unavailable in two
Expand Down
23 changes: 19 additions & 4 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Creates a new debugging session.
- `executablePath` (string, optional): Path to the language interpreter/executable (e.g., Python interpreter path).
- `host` (string, optional): Host to attach to for remote debugging. Defaults to `localhost`.
- `port` (number, optional): Debug port to attach to. **Passing `port` switches the call into attach mode** — the session is created and immediately attached (see [attach_to_process](#attach_to_process) for the full attach contract). `host` alone does not trigger it.
- `stopOnEntry` (boolean, optional): Attach mode only — same semantics as [attach_to_process](#attach_to_process)'s `stopOnEntry`: **omitting it pauses the target after attach** (the pause may land after the response, reported as `pending: true` and named in `message`); pass `false` to attach to a live service without stopping it.
- `timeout` (number, optional): Attach mode only — connection timeout in milliseconds (default: `30000`).
- `verifyTimeout` (number, optional): Attach mode only — how long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: `20000`, max: `600000`).
- `adapterConfig` (object, optional): Attach mode only — adapter-specific attach configuration merged into the attach config, with the same semantics as [attach_to_process](#attach_to_process)'s `adapterConfig`.
Expand All @@ -80,7 +81,7 @@ Creates a new debugging session.
**Notes:**
- Session IDs are UUIDs in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
- Sessions start in `"created"` state
- When a `port` parameter is provided in `create_debug_session`, the server performs an inline attach (creating the session and immediately attaching to a running process on that port). The response then mirrors `attach_to_process`: alongside `sessionId` it carries `state`, the attach `data` payload, an optional `warning`, and — when a requested post-attach pause has not landed yet — `pending: true`
- When a `port` parameter is provided in `create_debug_session`, the server performs an inline attach (creating the session and immediately attaching to a running process on that port). The response then mirrors `attach_to_process`: alongside `sessionId` it carries `state`, the attach `data` payload, an optional `warning`, and — when a requested post-attach pause has not landed yet — `pending: true`, with `message` saying so (`…; post-attach pause pending — the target stops when it next executes code (pass stopOnEntry: false to attach without pausing)`)

---

Expand Down Expand Up @@ -350,7 +351,7 @@ Starts debugging a script.
- `scriptPath` (string, required): Path to the script to debug. Must be **absolute** in host mode (a relative path is rejected with `Path must be absolute`); in container mode it is re-rooted under `MCP_WORKSPACE_ROOT`.
- `args` (array of strings, optional): Command line arguments for the script.
- `dapLaunchArgs` (object, optional): Standard DAP launch arguments:
- `stopOnEntry` (boolean): Stop at first line
- `stopOnEntry` (boolean): Stop at first line (default `false` — the opposite of attach, which pauses unless `stopOnEntry` is `false`)
- `justMyCode` (boolean): Debug only user code
- 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).
Expand Down Expand Up @@ -1119,7 +1120,7 @@ Attaches the debugger to a running process. Unless you pass `stopOnEntry: false`
- `timeout` (number, optional): Connection timeout in milliseconds (default: `30000`).
- `verifyTimeout` (number, optional): How long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: `20000`, max: `600000`). Decrease for fast failure-by-design probes; increase for targets that are exceptionally slow to become debuggable. Not used when `stopOnEntry: false` — that path performs no thread verification.
- `sourcePaths` (string[], optional): Source paths for code mapping.
- `stopOnEntry` (boolean, optional): Request a pause immediately after attaching. Anything but `false` — including omitting it — takes the verified path described above; `false` skips both the thread verification and the post-attach pause, and the attach returns `state: "running"`.
- `stopOnEntry` (boolean, optional): Request a pause immediately after attaching. Anything but `false` — including omitting it — takes the verified path described above; `false` skips both the thread verification and the post-attach pause, and the attach returns `state: "running"`. **Pass `false` when attaching to a live service you still need to use** — this is the opposite of `start_debugging`, whose `stopOnEntry` defaults to `false`. A pause that lands after the response is reported as `pending: true` and named in `message`.
- `justMyCode` (boolean, optional): Only debug user code (skip library code).
- `breakOnExceptions` (string, optional): `"uncaught"`, `"all"`, or `"none"` — same mode semantics as on `start_debugging`, but attach never applies a language default: it stays `"none"` unless requested.
- `adapterConfig` (object, optional): Adapter-specific attach extras, merged into the attach config before the adapter transforms it, mirroring `start_debugging`'s `adapterLaunchConfig` (C/C++/LLDB example: `{"program": "/proc/1/root/pricer"}` for symbol resolution from a kubectl-debug ephemeral container, or `initCommands`; Python example: `{"pathMappings": [{"localRoot": "/home/user/checkout/src", "remoteRoot": "/app"}]}` so breakpoints at local-checkout paths bind against a remote debugpy, issue #450). Reserved keys `request`/`__attachMode` are ignored with a warning; set `stopOnEntry` via the top-level parameter.
Expand All @@ -1136,8 +1137,22 @@ Attaches the debugger to a running process. Unless you pass `stopOnEntry: false`
}
```

When the requested pause has not landed by the time the tool answers (an idle Node server, say — js-debug's pause lands on the next event-loop dispatch), the response says so:
```json
{
"success": true,
"state": "running",
"pending": true,
"message": "Attached to process at 127.0.0.1:9229; post-attach pause pending — the target stops when it next executes code (pass stopOnEntry: false to attach without pausing)",
"data": {
"message": "Attached to process at 127.0.0.1:9229; post-attach pause pending — the target stops when it next executes code (pass stopOnEntry: false to attach without pausing)",
"pending": true
}
}
```

**Notes:**
- `state` is `"paused"` only once a stopped event has actually been observed; otherwise the attach reports `"running"`. When a requested post-attach pause is accepted but its stopped event has not arrived within the bounded wait, the response is successful with `state: "running"` and `pending: true` (at the top level and in `data`); the late stopped event is the only transition to `paused`, and every paused session has a `lastStop`.
- `state` is `"paused"` only once a stopped event has actually been observed; otherwise the attach reports `"running"`. When a requested post-attach pause is accepted but its stopped event has not arrived within the bounded wait, the response is successful with `state: "running"` and `pending: true` (at the top level and in `data`) and the `message` names the pending pause; the late stopped event is the only transition to `paused`, and every paused session has a `lastStop`.
- When `processId` was used, the message reads `Attached to process PID <pid>` instead.
- The response `warning` reports two distinct `adapterConfig` key outcomes (issues #450/#466): keys the adapter's attach transform genuinely drops (e.g. Python's ptvsd-era `localRoot`/`remoteRoot` — use `pathMappings`) are named as **ignored**, while keys mcp-debugger doesn't recognize are **forwarded to the adapter as-is** and named with an edit-distance suggestion for near-misses (`pathMapping (did you mean pathMappings?)`) — "ignored" means dropped, "forwarded as-is" means the adapter still sees them. The same field also carries the launch-style warning for function breakpoints still unverified at attach (issue #308).
- js-debug attach honors `adapterConfig` too: `localRoot`/`remoteRoot`/`sourceMaps`/`skipFiles`/`continueOnAttach` and other js-debug attach options reach the debugger (issue #466).
Expand Down
4 changes: 2 additions & 2 deletions examples/kubernetes/attach-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ through `/proc/<pid>/root/` — required because mount namespaces are not shared
and `/proc/1/maps` paths aren't openable from the sidecar. The target is
**PID 1** of the shared namespace when injected with `--target=app`.
`--profile=general` is what injects `SYS_PTRACE` (nodes run
`kernel.yama.ptrace_scope=1`). `stopOnEntry` defaults to `true` for C/C++
attach. Expect `<optimized out>` locals on `-O2` builds and symbol-only
`kernel.yama.ptrace_scope=1`). `stopOnEntry` defaults to `true` for attach in
every language (pass `false` to leave the target running). Expect `<optimized out>` locals on `-O2` builds and symbol-only
breakpoints on stripped binaries.

## dotnet — no Kubernetes recipe today
Expand Down
1 change: 1 addition & 0 deletions skills/debugging/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ For an already-running process (including remote machines, containers, and Kuber
attach_to_process {sessionId, host: "localhost", port: 5678, sourcePaths: ["<local src>"], adapterConfig: {...}}
```

- **Attach pauses the target by default** (omitting `stopOnEntry` means `true` — the opposite of `start_debugging`). Pass `stopOnEntry: false` for a live service you must not freeze. A response with `pending: true` means the pause lands when the target next runs code; `continue_execution` releases it.
- **Python**: target ran `python -m debugpy --listen <host>:<port> ...`; to address breakpoints by local-checkout path, map it onto the debuggee tree with `adapterConfig: {pathMappings: [{localRoot: "<abs local>", remoteRoot: "/app"}]}`
- **Ruby**: target ran `rdbg --open --port <port> ...` (works through `kubectl port-forward`); `localfsMap: "/app:<abs local dir>"` maps paths
- **Java**: target JVM has `-agentlib:jdwp=transport=dt_socket,server=y,address=*:<port>`; breakpoints in not-yet-loaded classes are deferred automatically, and a fully-qualified class name as `file` needs no source files at all
Expand Down
2 changes: 1 addition & 1 deletion skills/debugging/references/cpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ close_debug_session {"sessionId": "<id>"}
attach_to_process {"sessionId": "<id>", "processId": 4242}
```

- Target is held **paused** after attach (pass `stopOnEntry: false` to resume immediately). `detach_from_process` leaves it running.
- Target is held **paused** after attach — the attach default for every language, not C/C++-specific (pass `stopOnEntry: false` to resume immediately). `detach_from_process` leaves it running.
- Linux: `kernel.yama.ptrace_scope=1` limits attach to child processes — `sudo sysctl kernel.yama.ptrace_scope=0` for arbitrary PIDs. Windows: same-privilege processes.
- Adapter extras go in `adapterConfig`: `{"adapterConfig": {"program": "/path/to/binary"}}` helps symbol resolution when LLDB cannot open the module paths from `/proc/<pid>/maps` (different mount namespace — kubectl-debug sidecar: use `"/proc/<pid>/root/<binary>"`); `initCommands` runs LLDB commands before attach.

Expand Down
15 changes: 12 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,15 +370,24 @@ export class DebugMcpServer implements ToolContext {
const resolved = await this.resolveBreakpointFile(req.sessionId, req.file, { requireExists: true });
const mode = getBpAddressingMode(this.environment);

const readLinesForContentAddressing = async (feature: string): Promise<string[]> => {
const readLinesForContentAddressing = async (
feature: 'statement addressing' | 'expectedContent'
): Promise<string[]> => {
if (!resolved.contentAddressable) {
// Two distinct causes, two honest reasons (issue #497): an attach
// session's file may be perfectly readable here — the rule is that
// the debuggee's loaded source is the authority, not the host's copy.
// The remedy is feature-specific (issue #654): an expectedContent
// caller already passed line, so "use line addressing instead" read
// as a contradiction — tell them what to drop, not what to add.
const remedy =
feature === 'expectedContent'
? 'drop expectedContent and keep line — the breakpoint is set by plain line addressing.'
: 'use line addressing instead.';
const reason =
resolved.nonAddressableReason === 'attach'
? `${feature} is not supported for attach sessions the debuggee's loaded source may not match the file on the mcp-debugger host. Use line addressing instead.`
: `${feature} requires a source file readable by the mcp-debugger server; "${req.file}" is a class name or remote path. Use line addressing instead.`;
? `${feature} is not supported for attach sessions (the debuggee's loaded source may differ from the file on the mcp-debugger host); ${remedy}`
: `${feature} requires a source file readable by the mcp-debugger server; "${req.file}" is a class name or remote path — ${remedy}`;
throw new McpError(McpErrorCode.InvalidParams, reason);
}
const lines = await this.lineReader.getFileLines(resolved.path);
Expand Down
5 changes: 4 additions & 1 deletion src/server/handlers/session-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ export const createDebugSessionTool: ToolHandler = async (ctx, args) => {
sessionId: sessionInfo.id,
state: attachResult.state,
message: attachResult.success
? `Created and attached ${sessionInfo.language} debug session: ${sessionInfo.name}`
? `Created and attached ${sessionInfo.language} debug session: ${sessionInfo.name}` +
// The top-level message is what an agent reads; a pause that has
// not landed yet must be named here too, not only in data (#654).
(attachData?.pending ? `; ${ErrorMessages.attachPausePending}` : '')
: `Created session but attach failed: ${attachResult.error || 'Unknown error'}`,
...(attachData?.pending ? { pending: true } : {}),
...(attachData ? { data: attachData } : {}),
Expand Down
Loading
Loading