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/655.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**js `get_stack_trace` shows the user's frames, not the framework's** — on a js attach session paused inside an express/MCP server, `includeInternals: false` still returned ~60 frames: forty `node_modules/.pnpm/…` router/body-parser frames, ten phantom relative `../src/*.ts` paths (js-debug had applied every dependency's `.js.map`, whose sources are not shipped), and `<unknown_source>` `await`/`Promise.then` separators. `JsDebugAdapterPolicy.isInternalFrame` matched only `<node_internals>`. It now also hides any `node_modules` path segment (pnpm and Windows layouts included) and sourceless line-0 async separators, and drops its local first-frame fallback so the central issue-#346 guarantee (`allFramesInternal` + note) finally engages for JS. On attach, `resolveSourceMapLocations` defaults to launch's `["**", "!**/node_modules/**"]` (js-debug's own attach default collapses to "resolve everywhere") so dependency frames report their real `.js` path, and `cwd` defaults to the server's working directory because js-debug resolves no relative map source without a base path — the debuggee's own `dist/**` maps now resolve to the absolute `src/**/*.ts` beside them instead of an unopenable `../src/…` label. Frames the adapter still cannot place on disk carry `unresolvedSource: true` and the response `note` says their `file` is a label, not a path. `skipFiles` is deliberately untouched on attach — blackboxing `node_modules` would re-open the #513 pause step-chase (#655)
4 changes: 2 additions & 2 deletions docs/agent-debugging-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ evaluate_expression(sessionId=session_id, expression="a + b") # Returns: "3"
## Common Issues and Solutions

### Issue: JavaScript shows Node.js internals in stack trace
**Solution:** Use `continue_execution` to move past internal frames. Stack trace filtering hides internal frames by default for supported languages.
**Solution:** Use `continue_execution` to move past internal frames. Stack trace filtering hides Node internals, `node_modules` dependency frames, and async separators by default; `includeInternals: true` shows them. A frame marked `unresolvedSource: true` is a source-map label, not a file you can open.

### Issue: Python shows "special variables" instead of actual variables
**Solution:** This is normal hierarchical organization. Use the `variablesReference` to expand:
Expand Down Expand Up @@ -446,7 +446,7 @@ capability, not a view-only credential.
## Summary

The MCP Debugger is fully functional for Python, Ruby, JavaScript, Rust, Go, Java, .NET/C#, and C/C++. The key insights are:
- **JavaScript**: Stack trace filtering hides internal frames; may need `continue_execution` if initially stopped at internals
- **JavaScript**: Stack trace filtering hides internal, `node_modules`, and async-separator frames; may need `continue_execution` if initially stopped at internals
- **Python**: Use variablesReference to expand variable containers
- **Ruby**: Supports launch and attach flows through `rdbg`; use Bundler mode for Rails and RSpec-style entrypoints
- **Rust**: CodeLLDB adapter is vendored; the GNU toolchain is required for reliable debugging -- MSVC-built binaries may produce errors with CodeLLDB. Set `RUST_MSVC_BEHAVIOR` env var to control MSVC handling
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/adapter-policy-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ whenever it is among the contributors.
- `requiresCommandQueueing()` returns `true` -- commands are queued until initialize response, then reordered (configs -> configurationDone -> launch -> others)
- Provides a full `performHandshake()` implementation for the js-debug multi-session setup
- `isChildReadyEvent()` waits for `'thread'` or `'stopped'` (not `'initialized'`)
- `filterStackFrames()` removes `<node_internals>` frames
- `filterStackFrames()` removes `<node_internals>`/`node:` frames, any `node_modules` path segment, and sourceless line-0 async separators (issue #655); no local fallback — `FrameAnchorResolver` owns the all-internal case
- `extractLocalVariables()` treats the local-like group as a COLLECTING group: every non-empty block scope (`Block`, `Catch Block`, `With Block`, legacy `Block:<label>`) in adapter order — which V8 gives innermost-first — followed by the frame's `Local` scope (issue #558). A `Local` scope that exists but contributed nothing (only `this`, or emptied by a pushed-down `names` filter) still has its ref reported, so the session layer keeps naming it `Local` with no note. On an ESM top-level frame — blocks but no `Local` at all — the first `Script`/`Module` scope joins the merge as the frame's base, because that is what such a frame reported before block scopes were recognised; with a `Local` present, `Module` stays a fall-through-only scope. When nothing local-like yields anything, the later groups fall through first-match: `Closure`, then `Script`/`Module`, and `Global` only for frames with no local or block scope at all
- All three places that knew JS scope names — the extractor's predicates and `getLocalScopeName()` — now read one `JS_SCOPE_KINDS` table, because a disagreement between them is exactly how a false `note` shipped once. It is exported from the `@debugmcp/shared` package root (`adapter-policy-js.ts`). The LLDB policies use the same one-table idea for their own scope names, but `LLDB_LOCAL_SCOPE_NAMES` lives in `lldb-policy-shared.ts` and is imported only within the shared package, not re-exported from the root

Expand Down
16 changes: 16 additions & 0 deletions docs/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,22 @@ If neither `tsx` nor `ts-node` is installed, the factory emits a warning (not an
`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
- **Source-mapped frames you cannot open.** A package that ships `.js.map` files
whose `sources` point at `.ts` files it did not ship makes js-debug report those
frames with a relative label (`../src/shared/protocol.ts`) and a non-zero
`sourceReference`; mcp-debugger marks them `unresolvedSource: true` and says so
in the `note` (issue #655). On attach the common causes are already handled:
`resolveSourceMapLocations` defaults to `["**", "!**/node_modules/**"]` so
dependency maps are not applied (those frames show their real `.js` path), and
`cwd` defaults to the server's working directory because js-debug resolves no
relative map source without a base path — with it, the debuggee's own
`dist/**` maps resolve to the absolute `src/**/*.ts` next to them. Knobs, all via
`adapterConfig`: `sourceMaps: false` (generated `.js` paths everywhere),
`resolveSourceMapLocations` (globs, or `null` for everywhere), `cwd`,
`sourceMapPathOverrides`. `get_stack_trace` hides `node_modules` and async
separator frames by default; a debuggee that is itself an installed package
under `node_modules` shows its top frame plus an "all frames are internal"
note — pass `includeInternals: true`
- Debuggee exit codes are captured via an injected preload (js-debug itself
never emits a DAP `exited` event), so `exitCode` is unavailable in two
cases: attach mode (the target's environment is not under mcp-debugger's
Expand Down
19 changes: 11 additions & 8 deletions docs/stack-trace-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
`get_stack_trace` applies language-specific stack trace filtering, hiding internal/framework frames by default so the caller sees user code first.

## Features
- **JavaScript/Node.js**: Filters out `<node_internals>` frames by default
- **JavaScript/Node.js**: Filters out `<node_internals>`/`node:` frames, any frame under a `node_modules` path segment (pnpm's nested layout and Windows separators included), and js-debug's sourceless async separators (`await`, `Promise.then`, `bound-anonymous-fn` at line 0) by default (issue #655)
- **Go**: Filters out `/runtime/` and `/testing/` frames by default
- **Java**: Filters out JDK internal frames by default
- **.NET/C#**: Filters out `System.*` and `Microsoft.*` runtime frames and sourceless frames by default
Expand All @@ -28,10 +28,12 @@ When calling `get_stack_trace` without parameters or with `includeInternals: fal
}
```

For JavaScript, this will return only user code frames, filtering out Node.js internals like:
- `<node_internals>/internal/modules/...`
- `<node_internals>/internal/process/...`
- etc.
For JavaScript, this will return only user code frames, filtering out:
- Node.js internals: `<node_internals>/internal/modules/...`, `<node_internals>/internal/process/...`
- Dependencies: `/app/node_modules/express/lib/router/index.js`, `/app/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/layer.js`
- Async separators: `await` / `Promise.then` frames with no source and `line: 0`

The match is on a `node_modules` path *segment*, so `/app/src/node_modules_helper.js` is user code, and workspace packages (which Node realpaths to their `packages/...` location) stay visible. A debuggee that is itself an installed package (`/usr/lib/node_modules/<pkg>/...`) becomes all-internal — the top frame is kept and the `note` says so (see Edge Cases).

### Including Internal Frames
To see all frames including internals:
Expand Down Expand Up @@ -90,8 +92,8 @@ The filtering is implemented using the existing `AdapterPolicy` system:

2. **JsDebugAdapterPolicy** (`packages/shared/src/interfaces/adapter-policy-js.ts`)
- Implements filtering for JavaScript
- Identifies internal frames by checking for `<node_internals>` in the file path
- Keeps at least one frame if all are filtered
- Identifies internal frames by path: `<node_internals>`/`node:`, any `node_modules` segment, and sourceless line-0 async separators (issue #655); `frame.name` never participates
- No local fallback — the central `FrameAnchorResolver` guarantee below owns the all-internal case

3. **GoAdapterPolicy** (`packages/shared/src/interfaces/adapter-policy-go.ts`)
- Implements filtering for Go
Expand All @@ -117,8 +119,9 @@ The filtering is implemented using the existing `AdapterPolicy` system:
- Turns that metadata into the `hiddenFrames` field and the `note` sentence

### Edge Cases Handled
- **All frames internal**: The filtered stack is never empty when the adapter reported frames. `FrameAnchorResolver` keeps the top (unfiltered) frame and sets `allFramesInternal`, so `get_scopes` and `evaluate_expression` always have a valid `frameId`; the `note` says so and points at `includeInternals: true` (issue #346). This guarantee is central and applies to every language, Go and .NET included. Two policies additionally soften the result themselves — JS retains the first frame, Java returns the full unfiltered array (so a thread parked deep in JDK code still shows its stack)
- **All frames internal**: The filtered stack is never empty when the adapter reported frames. `FrameAnchorResolver` keeps the top (unfiltered) frame and sets `allFramesInternal`, so `get_scopes` and `evaluate_expression` always have a valid `frameId`; the `note` says so and points at `includeInternals: true` (issue #346). This guarantee is central and applies to every language, Go and .NET included. One policy additionally softens the result itself — Java returns the full unfiltered array (so a thread parked deep in JDK code still shows its stack)
- **No frames**: Returns empty array as before
- **Unresolvable source-mapped frames** (issue #655): when the adapter reports a frame's source as not-a-file-on-this-host (DAP `sourceReference != 0` with a real-looking path — js-debug does this for a source map's `../src/x.ts` that the package never shipped), the frame carries `unresolvedSource: true` and the `note` says its `file` is a label, not an openable path. These frames are the debuggee's own code and are never hidden. On js attach this is rare now: `resolveSourceMapLocations` excludes `node_modules` by default (so dependency maps are not applied and those frames report their real `.js` path) and `cwd` is defaulted so the debuggee's own relative map sources resolve
- **Python**: No filtering applied (Python's AdapterPolicy does not implement `filterStackFrames`)
- **Other languages**: Any language whose AdapterPolicy implements `filterStackFrames` has filtering applied

Expand Down
5 changes: 3 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,8 @@ Gets the current call stack.
- Stack frames are ordered from innermost (current) to outermost
- `threadId` identifies the thread represented by `stackFrames`, or the explicitly queried thread when the stack is empty. `lastStop.threadId`, when present, remains the thread reported by the original stop event.
- Frame IDs are used with `get_scopes`
- Internal/runtime frames (e.g. Node.js internals, Go `/runtime/`, `System.*`) are filtered out by default; pass `includeInternals: true` to see them. When any frames were hidden, the response additionally carries `hiddenFrames` (count) and a `note` explaining how to reveal them.
- Internal/runtime frames (e.g. Node.js internals and `node_modules` dependencies, Go `/runtime/`, `System.*`) are filtered out by default; pass `includeInternals: true` to see them. When any frames were hidden, the response additionally carries `hiddenFrames` (count) and a `note` explaining how to reveal them.
- A frame whose source the adapter could not find on this host (js-debug: a source-mapped `.ts` the package did not ship) carries `unresolvedSource: true`, and the `note` says its `file` is a label rather than an openable path — do not pass it to `get_source_context`.
- The filtered stack is never empty when the adapter reported frames: if *every* frame is internal (e.g. a goroutine paused inside the Go runtime), the top internal frame is kept so `get_scopes`/`evaluate_expression` still have a valid `frameId`, and the `note` says so.
- When an explicit thread reports no frames, the response remains anchored to that thread and its `note` suggests a frame-bearing alternative when one is available.
- When the implicit stopped thread is frameless, stack, locals, and default evaluation share one resolver. It scans siblings, prefers a thread whose frames the language policy recognizes as user code over runtime-only stacks, adopts it once, and discloses the switch in `note`/`anchorNote`.
Expand Down Expand Up @@ -1155,7 +1156,7 @@ When the requested pause has not landed by the time the tool answers (an idle No
- `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).
- js-debug attach honors `adapterConfig` too: `localRoot`/`remoteRoot`/`sourceMaps`/`skipFiles`/`continueOnAttach` and other js-debug attach options reach the debugger (issue #466). Two keys are defaulted when absent (issue #655): `resolveSourceMapLocations: ["**", "!**/node_modules/**"]` (js-debug's own attach default collapses to "resolve everywhere", which applied every dependency's `.js.map` and produced phantom `../src/*.ts` frames) and `cwd` (the server's working directory, or the workspace root in container mode — js-debug needs a base path before it resolves any source map's relative `sources`, even ones next to the generated file). Pass your own value, including `resolveSourceMapLocations: null`, to override. `skipFiles` is deliberately left at js-debug's default: blackboxing `node_modules` would turn a `pause_execution` that lands in framework code on an idle server into the endless step-chase of issue #513 — `get_stack_trace` hides those frames instead.
- Breakpoints set before the attach are re-sent once the debuggee-owning session is provably live, so their verified state in `list_breakpoints` is authoritative.
- Languages whose adapter has no attach implementation (`rust`, `go`, `mock`) fail fast with a clear error.
- A failed attach reports `success: false` with the reason in `message` (`Failed to attach: ...`) and tears the proxy down, so no live proxy is left behind; `state` is then `"error"` (or `"stopped"` if the session was already gone). When the failure happened during proxy initialization, `data` carries `proxyLogPath`, `proxyLogResource`, and `initProgress` diagnostics. Session-lifecycle failures (unknown or terminated session) return the standard application-level `error` payload instead — see [Error Handling](#error-handling).
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ You can also evaluate arbitrary expressions in the current debug context:
- Narrow the request to escape the cap: pass `names: ["a", "b"]` to `get_variables` or `get_local_variables` to fetch specific variables in full. Requested names that were not found are listed in the response's `notFound`

### Stack Trace Filtering
- `get_stack_trace` filters internal/runtime frames by default. When any are hidden the response carries `hiddenFrames` (the count) and a `note` saying so; pass `includeInternals: true` to get the full stack
- `get_stack_trace` filters internal/runtime frames by default (for JavaScript: Node internals, `node_modules` dependencies, and async separators). When any are hidden the response carries `hiddenFrames` (the count) and a `note` saying so; pass `includeInternals: true` to get the full stack. A frame with `unresolvedSource: true` has a `file` that is a label, not an openable path

### Breakpoint Behavior
- Breakpoints initially show `"verified": false` because verification happens asynchronously by the debug adapter once the module is loaded (e.g., debugpy verifies after the script starts)
Expand Down
Loading
Loading