Skip to content

Release 0.26.0-beta.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Oct 02:23
· 29 commits to main since this release
14ee316

🎉 Release 0.26.0-beta.1

[0.26.0-beta.1] - 2026-10-30

Highlights

  • Dart and Flutter debugging, the tenth language — the SDK's own debug adapters behind a TCP-to-stdio bridge, nothing vendored: Dart programs and dart test, Flutter apps and tests on Windows/macOS/Linux desktops, in Chrome or Edge, on Android emulators and devices and on the iOS simulator; attach by VM-service URI or, on Android, by deviceId alone; hot_reload and a hot restart_debugging; doctor dart lists the devices and emulators it sees. The Docker image carries the Dart SDK (Flutter is host-only) (#790, #882, #876, #877, #1011)
  • wait_for_stop, the 29th tool — block until the session next pauses or ends instead of polling list_debug_sessions; every pending: true answer now names it and says nothing was cancelled (#849)
  • Every stop answer carries a snapshot — exception, top frames and locals on start_debugging, restart_debugging, wait_for_stop, the steps, pause_execution and attach, read from the stop's own stack; a later stop in the same frame reports changedVariables instead of the whole scope (#982, #983, #951)
  • One-call failing-test debugging for pytest — start_debugging { scriptPath, testRunner: "pytest", testTarget } pauses at the failing assert with the autopsy in the answer, no breakpoint set; Python launches can run a module (#985)
  • hot_reload replaces redefine_classes — Flutter hot reload or Java class redefinition behind one tool; redefine_classes stays as a deprecated alias until 1.0 (#876)
  • One launch contract for every language — start_debugging holds about a second for the first stop and answers paused, stopped or running with pending: true, the same in all ten languages; wait_for_stop reports the breakpoints a program ran past (#823, #826, #1033). dryRunSpawn is a query that leaves the session untouched (#812)
  • Streamable HTTP conformance — an unknown session id answers 404 so a spec-following client reconnects by itself; the transport's spec clauses are pinned against the real server and the official conformance suite runs in CI (#1017, #1023)
  • Host and Docker no longer collide in the build caches — C/C++ builds carry a record of the environment that produced them, COBOL builds live under one root per environment (#1035, #1041)
  • Heads-up for attach callers: an attach_to_process that carries a key the adapter never used (a processId beside a port on Python/Ruby/Java, a port or remote host beside a processId on C/C++/COBOL/.NET) is now refused by name instead of silently dropped; drop the extra key (#903)

Added

  • Streamable HTTP spec contract and the official conformance suite in CI — the clauses of the Streamable HTTP transport (spec 2025-11-25) that this server answers with its own code — a missing Mcp-Session-Id is 400, an unknown, DELETEd or reaped one is 404, an invalid Origin is 403, an unsupported MCP-Protocol-Version is 400 — are pinned one case per clause against the real server over raw HTTP (tests/integration/cli/http-spec-contract.test.ts; the 404 cases are the ones #1017 fixed and nothing could have caught), and scripts/mcp-conformance.mjs runs the protocol org's @modelcontextprotocol/conformance suite (pinned) for its seven applicable scenarios against a local http server, as a CI lane on Linux (#1023)
  • Python launches can run a module — start_debugging { scriptPath, dapLaunchArgs: { "module": "pytest", "cwd": "/proj" }, args: [...] } now reaches debugpy as a python -m pytest launch. Every launch used to carry program, filled from scriptPath, and debugpy refuses the pair ("program", "module", and "code" are mutually exclusive), so a module launch could not be expressed at all. The Python policy now declares its launch-target keys (program, module, code), the connection manager fills program only when the launch config names no target of its own, and the Python launch transform drops program beside a module or code target and says so in the launch answer (dapLaunchArgs.module: launched as a module (python -m); program is not sent …). scriptPath still names an existing file — the package's __main__.py, or the test file a pytest run is about — and cwd is what -m puts on sys.path: for a module or code launch it defaults to the directory above the top-most package containing scriptPath (the script's directory, the program-launch default, is inside the package, where python -m modpkg fails), the launch answer names the directory chosen, and an explicit cwd wins — the project root for a pytest run. A dry run reports such a launch's data.script as module: <name> or code: …. This is the launch shape the pytest preset builds on (#985)
  • One-call failing-test debugging for Python: start_debugging { scriptPath: <test file>, testRunner: "pytest", testTarget: "test_name" } — no breakpoint needed. The preset runs python -m pytest <file>[::<target>] -x from pytest's rootdir (pytest's own rule: the nearest ancestor of the file holding pytest.ini, a pyproject.toml with a [tool.pytest.ini_options] table, a tox.ini with [pytest] or a setup.cfg with [tool:pytest], else setup.py; then the top-most conftest.py ancestor; dapLaunchArgs.cwd overrides it), with pytest-xdist and pytest-cov turned off — -p no:<plugin>, or the plugin's inert switch (-n0 / --no-cov) when the project's own addopts turns it on, so its options still parse — and arms debugpy's userUnhandled filter beside uncaught under the breakOnExceptions default — pytest catches every test failure, so uncaught alone never fired on one. The launch answer is the autopsy: paused at the failing assert, or at the raise site of an exception that escapes the test (the helper frame first, the live test frame below it), with snapshot.exception, snapshot.frames and snapshot.locals; a passing test runs to completion with exit code 0. testTarget is test_name, Class::test_name, test_name[param] or a node id of that file (one naming another file is refused); your args are appended as pytest arguments, and a plugin they ask for is left alone. Which runners a language takes is declared on its factory metadata (modes.testRunners, shown by list_supported_languages) and gated before the session changes state, so testRunner: "jest" on a Python session is refused by name with the runners Python does take. The preset rides in adapterLaunchConfig, so restart_debugging replays it and a dry run reports data.script: "module: pytest". pydevd's (note: full exception trace is shown but execution is paused at: …) suffix no longer leaks into snapshot.exception.type. Measured shapes — an exception pytest.raises expects never pauses on Python 3.12+ and pauses once on 3.11 and older (pydevd's two tracing implementations), pytest.skip() pauses once, a print before the assert reaches get_output with pytest's report after the run — are in docs/python/spike-notes-pytest.md. Any dapLaunchArgs: { module: "pytest" } launch gets the same exception filters (#985)
  • Step answers join the stop snapshot, and repeated stops report only what changed — step_over/step_into/step_out now carry the same snapshot as the arrival answers (#982), composed once per answer even on adapters that step once per token position (Dart's same-line repeats). A later stop in the same frame — the next step, or a continue_execution → wait_for_stop loop over one breakpoint — replaces locals with changedVariables: { name: { before, after } } (new variables appear with before: "<undefined>"; an empty {} means the executed line changed nothing visible), so stepping through a loop costs a diff per answer instead of the full scope every time. Frame identity is the locals anchor's name + file + visible depth: any mismatch re-emits full locals and rebases, never a wrong diff; the same stop answered twice never diffs against itself; the cache resets on relaunch/re-attach (#983)
  • Every PAUSED arrival answer now carries a bounded snapshot — exception, top frames, locals — so reading crash state costs zero extra tool calls — start_debugging, restart_debugging, wait_for_stop, pause_execution, attach_to_process and the create_debug_session attach shortcut answer a stop with snapshot: { exception?, frames, locals?, truncation?, note? }, composed from the landing read's own stack (never a second resolve): the exception from the stop record (secret-redacted; DAP exceptionInfo included only if already merged, never awaited), the top ≤3 policy-filtered frames, and the top frame's locals through the get_local_variables pipeline under tight caps (20 locals / 200 chars, DEBUG_MCP_SNAPSHOT_MAX_LOCALS / DEBUG_MCP_SNAPSHOT_MAX_VALUE_CHARS) and a 2 s bound whose note says what expired. DEBUG_MCP_NO_SNAPSHOT=1 turns it off; DEBUG_MCP_VARIABLE_ACCESS=explicit withholds the locals half; entry stops, dry runs and pending answers carry none. Attach answers gain landing parity (location/threadId/context beside the note), and the raw lastStop emissions (wait_for_stop, get_stack_trace, list_debug_sessions) are now secret-redacted copies (#982)
  • start_debugging answers a stop with where it is, and get_stack_trace carries the stale-build note — a launch (or restart_debugging) that is paused when it answers now carries location, the source context, and threadId/note when there is something to say, the way wait_for_stop does, read through the same landing read the step and pause answers use. The stale-build sentence (#938, #950) therefore reaches a fast program's first answer instead of waiting for a wait_for_stop, and get_stack_trace appends it when the file of its top frame is newer than the compiled file the debugger binds it through. One handler helper now hoists and annotates the landing for every stop answer (#951)
  • The stale-build warning now covers the compiled languages' prebuilt paths — Java and .NET never compile user code (JDI binds through the .class, netcoredbg through the DLL's PDB), and a Go exec, Rust binary / cargo, C/C++ or COBOL prebuilt launch runs the executable as given, so a source newer than its artefact used to put the echoed line on a different statement than the one that runs, with no signal. Once start_debugging has named the artefact — the .class under a classpath directory at the source's package path or beside the source, the launched DLL, the executable the launch runs (C/C++ headers and COBOL copybooks included) — a set_breakpoint in a newer source adds the hedged warning naming both files and their mtimes, and step_*, wait_for_stop and pause_execution carry it as a note. AdapterPolicy.generatedFileCandidates now receives the transformed launch (program, launchConfig) and answers candidates tagged linkedBy: 'sourceMap' | 'launch'; the core's js-debug-shaped exemption moved into the JS policy (#950)
  • set_breakpoint warns when the TypeScript source is newer than the compiled file js-debug binds it through — the answer echoes the line from the .ts you named while js-debug binds through the generated .js's source map, so a dist/ older than src/ used to put the breakpoint on a different statement than the one shown, with no signal. The server now locates the compiled file (the nearest tsconfig.json that names an outDir, or dist|build|out|lib under the nearest package.json when none does, then a sibling .js; only a candidate whose .js.map names the source counts) and, when it is older than the source, adds a hedged warning naming both files and their mtimes; step_*, wait_for_stop and pause_execution carry the same sentence as a note at such a location. Per-language knowledge lives on a new optional policy hook, AdapterPolicy.generatedFileCandidates (#938)
  • Attach addressing contract — each adapter now declares on its factory metadata (modes.attachBy) which attach_to_process addressing forms it accepts (processId, port, a debugger URI under adapterConfig), and the core refuses any other form by name before the session changes state: a processId on a port-only language (Python, Ruby, Java) or on Dart answers with the adapter's own recipe for opening a port or a VM-service URI, a port or a remote host on a PID-only language (C/C++, COBOL, .NET) says so, a processId beside a remote host is refused on every language (a PID is a process on this host), and a request naming neither on a language whose forms are those two is refused as "needs port" / "needs processId" instead of failing later with the debugger's defaults. The session stays usable after a refusal, and list_supported_languages shows the forms as modes.attach.by. Heads-up: requests that carried an extra key the adapter silently dropped — processId beside a port on Python/Ruby/Java, a port or remote host beside a processId on C/C++/COBOL/.NET — are refused now; the extra key never did anything, so drop it. Previously the top-level keys reached each adapter's transform, which dropped what it did not use silently — the class behind #450, #466, #871 and #895. The processId an adapter consumes (C/C++, COBOL, JavaScript) is no longer reported as an ignored adapterConfig key (#903)
  • A Flutter launch that misses its device says which devices exist, and doctor dart lists them — a flutter run whose deviceId matches no device prints the devices it found and exits about 3 s after the launch answered (measured), too late for the launch answer: the answers that report the program's end now carry what the adapter read into it — wait_for_stop answers availableDevices (id, name, kind, platform) with a note that says to pass one of them, the launch that ends within its hold says the same, and list_debug_sessions carries endNote and availableDevices; with no device connected the note says to boot an emulator, plug a device in, or use the web, and when devices exist that the project has no platform for they come back with supportedByProject: false and flutter's own remedy (flutter create .). mcp-debugger doctor dart gains a Targets line: the devices flutter devices sees and the AVDs flutter emulators lists, booted or not (0.9 s and 0.7 s, inside the doctor budget, all four probes at once). Behind both is one policy hook, explainProgramEnd, recorded once at the program's end for every answer that reports it (#877)
  • JavaScript attach by processId — attach_to_process { processId } on a JavaScript session now attaches to a running Node.js process that was not started with --inspect: the adapter does what VS Code's process picker does (js-debug's own DAP server has no PID step) — it signals the process (SIGUSR1, or process._debugProcess on Windows), which opens its inspector on 127.0.0.1:9229, confirms through the inspector's /json/list that the port belongs to that PID, and attaches there; port alongside processId names where a --inspect-port=<n> target opens. Previously the PID was silently dropped and js-debug dialled localhost:9229 on its own, so the tool answered a generic "adapter exited during attach verification (code=null)". Refusals name their cause: a non-loopback host, the server's own PID, a PID that is not a Node.js process or was started with --disable-sigusr1 (both checked before signalling on POSIX, where SIGUSR1 would terminate the process), a port held by another process's inspector or by a debugger that holds the target paused, a host PID from inside the container, and — new for every JavaScript attach — a request with neither port, processId nor websocketAddress is rejected up front instead of being sent to js-debug port-less (#871)
  • wait_for_stop: block until a session next pauses or ends, instead of polling — a 29th tool. continue_execution returns as soon as the adapter acknowledges the resume, and start_debugging, the step tools, pause_execution and attach_to_process answer pending: true when the stop they were waiting for has not arrived; until now the only way to learn about the later stop was to poll list_debug_sessions. wait_for_stop {sessionId, timeout?} blocks until the session is paused (answering with the same lastStop record the session listing shows, plus the top frame's location and source line) or over (the terminal state, with exitCode when the debuggee reported one). A session that is already paused or already finished answers at once; timeout (ms, default 30000, max 600000) bounds the call, not the program, and a wait that runs out answers state: "running" with pending: true so it can simply be called again. The wait is on the session's state rather than on the adapter's raw stopped event, so the entry stop a stopOnEntry: false launch resumes by itself is never reported, and a call the client cancels releases its wait. Every existing pending: true message and the affected tool descriptions now say that nothing was cancelled — breakpoints and exception filters stay armed, a step or pause is still in effect — and name the tool (#849)
  • Flutter on macOS and iOS, measured — flutter run -d macos and the iOS simulator (deviceId = the simulator's UDID, as flutter devices lists it while it is booted) are measured through the server on an Intel iMac Pro (Xcode 26.3, Flutter 3.47.8) and covered by host-only e2e cases (MCP_SKIP_FLUTTER_IOS=1 skips the simulator block; the e2e helper now generates the probe's ios/ folder on macOS); the Dart guide carries the numbers and the Xcode requirement (#790, #1011)
  • Dart and Flutter debugging, the tenth language — a new @debugmcp/adapter-dart (language id dart, bundled in @debugmcp/mcp-debugger) drives the SDK's own debug adapters, dart debug_adapter [--test] and flutter debug-adapter [--test], behind a TCP-to-stdio bridge; nothing is vendored — the Dart or Flutter SDK on the machine is the debugger (DART_SDK / FLUTTER_ROOT, the project's fvm pin, or PATH; mcp-debugger doctor dart shows what was found and the devices and emulators it sees). The runner (dart, dart-test, flutter, flutter-test) is detected from the nearest pubspec.yaml and the program path; an explicit runner wins. Measured through the server and covered by host e2e suites: Dart programs and dart test files; Flutter widget and integration tests and flutter run on Windows, macOS and Linux desktops (deviceId), in Chrome or Edge (headless too), on Android emulators and devices, and on the iOS simulator; conditional breakpoints and logpoints, Unhandled/All exception filters, noDebug, stopOnEntry as a temporary breakpoint on main; hot_reload (Flutter hot reload) and a hot restart_debugging once the app has started. Attach by VM-service URI or service-info file, or — for an app on an Android device — by deviceId alone (#882); a Flutter launch that misses its device answers with the devices that exist (#877); closing a flutter run session stops the app (#885); a simulator or desktop deviceId without a URI is refused up front (#1020); the tool's daily update banner no longer breaks the launch (#1026); step_over repeats the SDK's per-token steps until the line changes (#879). The Docker image carries the Dart SDK (Dart in-container; Flutter is host-only). CI runs Dart and Flutter host lanes beside the Linux/Windows matrix. Guide: docs/dart/README.md; the measured facts: docs/dart/spike-notes.md (#790)

Changed

  • Streamable HTTP: a debug session another MCP client created is answered as such, not as Session not found — each HTTP client session owns its own debug sessions (by design, #337/#658), but a second client naming one of them got the same Session not found an invalid id gets (or Failed to close debug session, or a protocol error from hot_reload) while the program kept running under the first client. Every tool now answers success: false with error: "Session <id> belongs to another MCP client session: each Streamable HTTP client sees only the debug sessions it created. It ends when that client closes its MCP session (DELETE /mcp) or when the idle reaper closes it: …", worded from the reaper windows actually in force (and saying so when both are 0). The check sits in the tools/call dispatch, in front of every handler, through a lookup the HTTP layer hands each server; stdio and SSE are unchanged, and an id nobody holds keeps each tool's existing wording. docs/tool-reference.md now says that debug sessions are per client in HTTP mode (#939)
  • Internal: the functional DAP core no longer carries a currentThreadId or adapterConfigured snapshot. Both were written on every stop and launch and read by nothing — the stop anchor has lived only in ProxyManager.currentThreadId since #496, and the two copies even disagreed on thread id 0. No behaviour change (#913)
  • One verdict on which thread a stop is anchored on — the attach controller's thread rescue and the stack resolver's sibling scan, previously two copies with different predicates and bounds, now share selectStopAnchor: the reported thread first, then a thread named main, then listed order; a thread reaches the program when a frame both names a source and is one the language policy calls the program's. For an attach that means a frame whose source has a name but no path (a Java class without debug info) now counts, while a thread whose sourced frames are all runtime by the policy's reckoning (glibc or winpthreads plumbing with a system path) no longer does, so the scan moves on to the thread in main. A Go pause_execution reported on a goroutine parked entirely under /runtime/ now moves to a goroutine in the program's code the same way. Caller-visible consequences: the resolver's scan now gives every unwind its own 3 s timeout; a threadId-less get_stack_trace answered from a thread other than the one the stop was reported on carries the note The stop was reported on thread X; inspection is anchored on thread Y.; get_stack_trace results carry noProgramFrame: true when no frame of the shown stack is program code (#904)
  • A Dart step_over now leaves the line. The SDK debug adapter stops once per token position, so a line like final answer = counter.value + 2; took three step_over calls, each answering "Stepped over" while still on the same line. mcp-debugger now repeats the DAP step while it lands on the line it left, in the same stack frame, up to eight times. The answer says how many steps it took, for example "Stepped over in 3 steps …". A breakpoint or exception on that line still ends the step at once. So does a landing in another frame, such as a recursive call returning to its caller on the same line (#879)
  • hot_reload replaces redefine_classes and adds Flutter hot reload — one tool applies changed code to the running program and keeps its state: on a Flutter session it is the SDK adapter's hot reload (the app must have started; the reassemble runs build(), so a breakpoint there is reported by the call as the stop with its location and snapshot — measured ~150 ms after the request; widget state, counters and timers survive; a reload sent to a paused program lands when it resumes), on Java it is the class redefinition the tool always did (the answer is unchanged). The behaviour is one AdapterPolicy.liveUpdate member — the events the worker forwards for it (flutter.appStart/flutter.appStarted), the request or the refusal, whether code changed, whether the update may stop the program — so a third adapter with a live update implements one object. Refused by name on the other languages; redefine_classes stays as a deprecated alias (#876)
  • restart_debugging uses the adapter's own restart when it offers one — a Flutter hot restart — for the adapters measured for it (the policy's restartViaDapRequest pin — Flutter today) whose live capabilities advertise supportsRestartRequest (the Flutter adapter does, in a DAP capabilities event once the app has started), the DAP restart request is sent instead of terminate + relaunch: main() re-runs in the same process and the first breakpoint is reached again about half a second later (measured on Windows desktop and Chrome), the answer carries data.hotRestart: true, the output buffer is kept (no outputReset), breakpoints stay armed in the adapter, a stopOnEntry: true launch stops at entry again, and a Flutter attach session can be restarted this way too. The proxy worker now forwards the adapter's capabilities events merged into the session's capabilities, treats restart as a resume, and re-arms the hidden entry breakpoint's relabel for the re-run. Every other language keeps the relaunch, described by the answer as before (outputReset: true) — js-debug advertises restart too, but its own relaunch goes through a child session the proxy does not follow, which is why the pin exists (#876)
  • A launch whose only armed instrument is a logpoint no longer says "no breakpoints" — start_debugging (and wait_for_stop) answered a still-running program with "nothing is armed to stop it (no breakpoints, no entry stop, no caught-exception filter)" even when a logpoint was set and firing; both halves were true, since a logpoint never stops the program, but "no breakpoints" reads as "your logpoint was not registered" — the first thing a caller checks when a logpoint seems silent. Logpoints the adapter runs on are now named apart from what is armed to stop the program: "1 logpoint(s) that log without stopping is armed (read get_output for the messages), and nothing is armed to stop it (no pausing breakpoints, …)"; with breakpoints as well, both are named. Logpoints downgraded to pausing breakpoints (adapters without supportsLogPoints) are still counted as armed, as before (#865)
  • pi install npm:@debugmcp/mcp-debugger registers the server with pi's built-in MCP support (pi 0.99+) — the package ships a pi extension that calls pi.registerMcpServer(), which is how pi lets a package add a server, so the debugger's tools reach pi without the community pi-mcp-adapter. Before this, on pi 0.99+ the install added the skill but not the server: the package declared it only through the adapter's pi.mcp manifest entry, which pi itself ignores. The extension runs the CLI installed next to it (no npx download at startup), with deferred exposure, so pi lists the debugger in its system prompt and the model loads the 29 tools with tool_search, and a 180 s request timeout for slow launches. The adapter-only pi.mcp entry and pi.mcp.json are removed; an install alongside pi-mcp-adapter 4.x still works through pi's built-in MCP, and pi older than 0.99 ignores the extension (#841)
  • start_debugging answers the same way in every language, after one short hold — once the program is launched, the call holds its answer briefly for the program's first stop and reports whichever comes first: paused with the reason, stopped with the exit code, or running with pending: true. That is now one rule for all ten languages. Before, JavaScript launches answered running as soon as the program was configured, even with breakpoints armed, so the launch response could not be read as "where did it stop" the way it could elsewhere; and every other language waited up to 30 s when something was armed, which blocked the caller for exactly the case where it has to act first — a breakpoint in a server's request handler that only a request can reach. The hold is about a second, sized from measured launch-to-first-stop latency (at most 172 ms across the ten language adapters on an idle machine, 678 ms with every core saturated), and is deliberately kept out of the tool contract: it is not a limit on the program, nothing is cancelled when it elapses, breakpoints stay armed, and wait_for_stop blocks until the stop arrives. A stopOnEntry launch is still answered at its entry stop however long that takes, and a noDebug launch follows the same rule (it is answered by its exit or by the hold, in debugger-off terms). What changes for callers: a JavaScript launch whose breakpoint is reached as the program starts keeps answering paused, and one that used to answer a bare running now carries pending: true; in the other languages a breakpoint first reached more than about a second after launch answers pending: true instead of holding the call open — call wait_for_stop. The pending message no longer states a duration, and it names what is armed when the answer is built rather than when the launch began (a breakpoint set while the launch was in flight used to be answered with "nothing is armed", #826). A launch answered while its program is still running no longer re-sends the breakpoints to the adapter afterwards — the send the launch itself made stands, and list_breakpoints reports them verified in all ten languages without it; the re-send is kept for a launch that is paused, because a running program may already be gone when it arrives (a logpoint that had fired then read verified: false, #856). So that nothing is lost by it, a function-breakpoint request the adapter rejects before launch is now echoed by the proxy with the debugger on as well as under noDebug, and the launch answer quotes it as a refusal rather than advising to check the symbol name. AdapterPolicy.isSessionReady is removed: no policy has a say in when a launch answers (#823, #826, #815)
  • start_debugging { dryRunSpawn: true } is now a query that leaves the session exactly as it was — it runs the launch preparation in the server (toolchain gate, adapter and environment checks, the launch transform — compiling a source-file program where the language does — executable resolution, the adapter command) and answers with the command the launch would run plus the configuration warnings. Nothing else changes: the answer's state is the session's current state, a running or paused program keeps running, and the previous run's output, exit code and last stop stay readable. Previously a dry run was a launch attempt that rode the proxy worker: it tore down a live program, reset every per-launch record and ended the session stopped. Attach-shaped configurations are refused, since preparing an attach acts on the target process (#812)
  • The Docker image carries every runtime file an adapter publishes — the image now copies each adapter package from its own package.json files field, the same list npm publishes, instead of a hand-maintained list of directories, so a runtime asset added to a package reaches the image as well as npm. The JavaScript adapter's exit-code shim (#796) had been missing from the image because that hand list left it out; the image also gains the JavaScript adapter's vendor-manifest.json and the shared package's README (#811, #796)
  • Go: Delve 1.27's own exited event is supported alongside the older console-line exit status — Delve 1.27 sends a DAP exited event with the program's exit code (go-delve/delve#4371). mcp-debugger already uses that event when it arrives, so exitCode in list_debug_sessions and the start_debugging run-to-completion summary is the same on every Delve. Older Delve, which is all a Go before 1.25 can run, is still covered by reading Delve's Process N has exited with status S console line. One visible difference: with Delve 1.27+ in debug mode, that console line no longer shows up in get_output, because Delve prints it only while the session is being torn down. CI now runs the Go integration tests on both Go 1.21 + dlv 1.24.2 and Go 1.27 + dlv 1.27.2 (#753)
  • Internal: one predicate each for "the session is over", "paused or over" and "the lifecycle is terminated" (isTerminalSessionState, isSettledSessionState, isSessionTerminated in @debugmcp/shared) replaces the inline spellings the step, pause, wait_for_stop, launch-hold, exit-handling, session-listing and tool-gate paths carried; no behaviour change (#735)

Deprecated

  • redefine_classes is a deprecated alias of hot_reload — still accepted with the same arguments and answered the same way, with a warning naming the new tool; no longer advertised by tools/list; removed at 1.0. The tool count stays at 29 (#876)

Removed

  • IDebugAdapter.translateErrorMessage is gone from @debugmcp/shared — all eleven adapters implemented it, but nothing ever called it, so the sentences it held never reached a caller, and wiring it in would have made answers worse: its "not found" mappings duplicated the messages that validate(), the launch gate and validateEnvironment() already report (and those are more specific — Python's lists the paths it tried), and several of its patterns were broad enough to mislabel other errors (JavaScript's "not found" match would have called a missing js-debug a missing Node.js). A third-party adapter that still implements the method keeps compiling; the extra method is simply never called. The one hint worth keeping, gem install debug, now follows the tried paths in the Ruby factory's "rdbg not found" error (#974)
  • IDebugAdapter.getDefaultLaunchConfig is removed from @debugmcp/shared (a required member, as AdapterPolicy.isSessionReady was in #823). Nothing called it: the only launch defaults ever applied are the session core's { stopOnEntry: false, justMyCode: true }, merged before the adapter's transformLaunchConfig (a policy's defaultStopOnEntry may still decide stopOnEntry when the caller gave none), and the values the adapters declared contradicted that — .NET and Java said stopOnEntry: true, most said cwd: process.cwd() (the server's directory; the launcher defaults cwd to the program's). Adapters that still define the method keep compiling; a caller would not, and none is known. Launch behaviour is unchanged (#929)
  • IDebugAdapter.getDefaultAttachConfig is removed from @debugmcp/shared. The host never called it, so the values the eight adapters returned (Ruby's and CodeLLDB's stopOnEntry: true, Dart's timeout: 30000) never reached an attach; the defaults that apply live in each adapter's transformAttachConfig. Adapters that still define the method keep compiling (extra members are fine); a caller of it would not, and none is known. Attach behaviour is unchanged (#919)
  • The proxy worker's dry-run path is removed: dryRunSpawn on the proxy config and init payload, the worker's handleDryRun, the dry_run_complete status and dry-run-complete event, getDryRunSnapshot/hasDryRunCompleted on the proxy manager, and the session manager's dryRunTimeoutMs. Since #961 a dry run is answered in the server from the launch preparation and never reaches the proxy (#812)
  • DEBUG_MCP_SKIP_AUTO_START is gone — the env var the bundled CLI set and restored around its import of the server entry no longer exists, and a DEBUG_MCP_SKIP_AUTO_START=1 inherited from the environment no longer stops dist/index.js from starting its server. Nothing else changes for users: main() now lives in a module with no side effects (src/main.ts) that the npm bundle imports directly instead of importing src/index.ts behind the flag, and the console silencing and argv cleanup both entries need moved to one prologue module (src/cli/entry-prologue.ts) that each evaluates first (#732)

Fixed

  • C/C++: a .tmp staging file or a Windows .exe copy is never picked as the cached binary on Linux/macOS — the managed-sibling check matched any <name>.debug-mcp-* file when the output had no extension, so a compiler's half-written staging file or a host's .exe seen over a bind mount could win the reuse as the newest artefact; the managed token is never dotted, and the check now says so (#1041)
  • Two agent-facing papercuts on the LLDB-based adapters and on expectedContent — CodeLLDB's own console housekeeping (the "Console is in 'commands' mode" banner, its Executing script: markers, the Launching: and Launched process lines, and Attached to process on an attach) landed in get_output ahead of any program output on C/C++, Rust and COBOL launches, consuming the cursor and reading as the program's own; the shared LLDB policy now drops those lines on the console category only (the engine's "Process exited with code N." stays, as does the same text on stdout or stderr). And set_breakpoint with an expectedContent that was the whole statement minus the line's trailing ; or . — the one character an agent quoting a statement drops — got the "matched as a substring, confirm this is the intended line" warning; that match is now exact-equivalent (matchQuality: "terminator-stripped-exact"), and a partial statement still warns (#1038)
  • A DAP request timeout under a second is reported in milliseconds — the attach thread poll uses sub-second timeouts, and its failure read "Request 'threads' timed out after 0s" inside the attach error, which looks like a misconfigured timeout; it now reads "after 400ms" (#1037)
  • A Go package directory as scriptPath launches, and a failed build says why — a directory target defaulted its working directory to the directory's parent, so go build ran under the parent's own go.mod and failed ("main module … does not contain package …"), and without an explicit mode the directory was treated as a binary (dlv exec: "not an executable file"). A directory is now its own cwd and a debug target. A launch that fails after the proxy started also quotes the last lines the adapter produced before the failure — the compiler's error behind Delve's "Build error: Check the debug console for details." — after the adapter's own message, so the agent does not need a second call to get_output to learn why (#1036)
  • Host and Docker no longer share one build cache entry over a bind mount. C/C++: the adapter's .debug-mcp/ cache decided reuse by file times alone, so a hello.cpp built on a Linux/macOS host was launched as-is inside the container (same path, same name), where its DWARF named the host's paths and every line breakpoint stayed unbound. Each compile now writes a build record beside the output (<binary>.build.json: compiler and version, flags, platform, architecture, whether it was built in the container, the source path as compiled), and a cached binary is reused only when the record matches the current environment and the source is not newer; a binary built before records existed is rebuilt once. COBOL: the builder kept one latest.json pointer and one prune pool per program for every environment, so the container's build repointed the host's pointer at a /workspace/... path the host cannot read (one rebuild into a <key>-2 directory per switch), each side's prune deleted the other's directories, and when the container had created a program's directory first the host launch failed outright with EACCES … mkdir. Artifacts now live under an environment segment, .debug-mcp/cobol/<platform>-<arch>[-container]/<name>/<buildKey>/ (linux-x64, win32-x64, linux-x64-container), each environment with its own pointer, prune pool and key directories, and the segment is part of the build key; a pointer that cannot be written is a warning, not a failed build. Caches from the previous layout are not reused or removed (one rebuild per program; the old .debug-mcp/cobol/<name>/ directories can be deleted). A root-owned .debug-mcp/ the container created before the host ever built still sends the host's artifacts to its temp directory with a warning: run the container as your own user (--user $(id -u):$(id -g), see docs/docker-support.md) and no root-owned file lands in the mount (#1035)
  • COBOL: the synthesised PERFORM frame names the performer from the manifest's call site — at a stop inside a performed paragraph the frame under it read 1000-INIT (PERFORM 1000-INIT) at the paragraph's own inline PERFORM (GnuCOBOL 3.1.2) or was missing altogether (the Linux 3.2 image), because the PERFORM line came from the entry's return address through the engine's line table, which attributes that address three ways across compilers. The shim now takes the PERFORM statement from the control flow the manifest already records — the call site whose THRU label is the entry's perform_through, with the line table only to tell apart several PERFORMs of one range, or as the fallback for a manifest without call sites — so the frame reads 0000-MAIN (PERFORM 1000-INIT) at the PERFORM statement on every compiler; step_out's PERFORM line comes from the same reading. Docs: the MCP_COBOL_ALLOW_PREBUILT variable was documented but never read — a prebuilt COBOL executable launches without cobc as it always did (#1034)
  • wait_for_stop reports the breakpoints a program ran past when it ends after the launch hold — the "verified but never hit" summary and unhitBreakpoints (#701) were only on the start_debugging answer, so a program that outlived the 1 s hold got a pending launch answer and then a bare "ran to completion" from the wait_for_stop that reported its exit: the same program answered differently depending on where it ended. The terminal wait_for_stop answer now carries the same summary when no stop was recorded in the launch — the sentence names the verified breakpoints and unhitBreakpoints lists every line breakpoint the program never reached; a program resumed past a stop keeps the plain exit message (#1033)
  • The unarmed launch answer no longer promises an uncaught-exception stop the adapter cannot deliver — the "still running" answer of start_debugging and wait_for_stop said the program "will stop only for an uncaught exception the debugger catches by default" whatever was armed: under breakOnExceptions: "none" (the filter the caller opted out of, #1009), on a Ruby launch with no breakOnExceptions (rdbg has no uncaught-only filter, so nothing is armed), and on a C/C++ launch on Windows (abort() ends the process with exit code 3; the SIGABRT stop is a POSIX fact) — then reported the exit. The verdict is now recorded beside the filter ids the mode actually armed, from the policy's own table — 'none' opted out, no filter available, or (an attach with no mode) none requested — and the launch and wait_for_stop pending answers say which, promising only the exit. An explicit breakOnExceptions the adapter resolves to no filter ("uncaught" on Ruby) now adds a warning naming the modes it does support instead of being a silent no-op; the policy default never warns. The cpp policy pins stopsOnUncaughtWithoutFilter per platform, and the cross-policy contract test requires that pin from any policy whose uncaught table is empty under an 'uncaught' default (#1032, #1009)
  • A Flutter launch no longer fails once a day on the tool's update banner — on any box one Flutter stable behind, the Flutter tool printed its "A new version of Flutter is available" box on stdout ahead of the debug adapter's first frame, once per day; the frame decoder cut its header block at the first blank line, which only the real header supplied, so the banner and the Content-Length line shared one block, the initialize response was thrown away with the banner, and the launch failed with "Debug proxy initialization did not complete within 30s". The adapter now runs the tool with --no-version-check --suppress-analytics ahead of debug-adapter, and the shared decoder resyncs on a Content-Length found inside a malformed block, so a stray line ahead of any adapter's frame costs nothing (reproduced on demand by setting the tool's version-check stamp back) (#1026)
  • A Flutter attach to an iOS simulator by deviceId alone is refused up front — the simulator has no logcat-style discovery of an app another tool started, so flutter attach -d <udid> waited for a connection that never came and the attach failed only at the thread-verification deadline; a simulator UDID without adapterConfig.vmServiceUri is now refused immediately with the URI hint, as desktop ids are, and the app.debugPort URI of the flutter run --machine that started the app attaches in ~4 s (measured on an iMac Pro); Android devices keep the no-URI recipe (#1020)
  • http mode answers an unknown Mcp-Session-Id with 404 — a request naming a session the server no longer knows (after a restart, or after the stale-session sweep reaped it) was answered 400, which a Streamable HTTP client treats as a dead end; the spec's 404 (-32001 Session not found, the SDK transport's own answer) lets a client that follows the spec, Claude Code included, start a new session by itself instead of needing a manual reconnect, and a request with no session id that is not an initialize still gets 400 (#1017; relates to #337)
  • A Python program that starts another Python process no longer hangs under the debugger — debugpy's sub-process debugging was on because the launch never named subProcess, so pydevd hooked every Python child the program started (subprocess, multiprocessing, pytest-xdist workers) and held it until a debug client attached to it; this server never did, so the child never ran, the parent waited on it, and the session reported running with no stop and no exit. The Python launch and attach transforms now default subProcess to false: children run undebugged, and an explicit subProcess: true is forwarded with a warning that names the hang. Debugging the children themselves is the feature half of #1005 (#1005)
  • Python debuggees on Windows no longer run under pythonw.exe, so the children they spawn keep working — the adapter had been swapped for a sibling pythonw.exe to keep console windows off the screen (#215), and debugpy's launch python defaults to the adapter's own interpreter, so the launcher and the debuggee ran windowless too; a debuggee child that inherited a standard handle (pytest-xdist's execnet workers under a venv) then died inside subprocess with OSError: [WinError 6] The handle is invalid before it existed. The adapter now runs under the session's interpreter on the hidden console the proxy's windowsHide spawn already gives it, which the launcher and the debuggee inherit: measured at zero new visible console windows over a full breakpoint session, pytest -n 2 spawns its workers, and sys.executable in the debuggee is python.exe (#993)
  • stopOnEntry: true now pauses a .NET launch at entry, on the opening brace of Main with reason entry. netcoredbg reads the flag as stopAtEntry, and the adapter used to send only stopOnEntry, so the program ran to completion. The launch now carries the same decision under both names. A stopAtEntry the caller passes in adapterLaunchConfig still wins (#973)
  • Toolchain checks are cheaper and more reliable — the checks behind list_supported_languages, the launch gate and mcp-debugger doctor now share one version probe that runs the tool directly (no shell), reads its output only after the pipes have drained, and stops it after 10 s; before this the Go, Python and C/C++ checks read output when the process exited, so a version printed late could be lost, and several had no time limit. Fewer duplicate runs: Java ran java -version twice (two JVM starts) and now runs it once, Python imported debugpy twice on the chosen interpreter and now does it once, Go runs dlv version and dlv dap --help side by side. Rust's cargo builds, test runs and metadata reads no longer go through a shell, which joined the arguments unquoted and broke on a project path with spaces (#969)
  • mcp-debugger doctor <language> no longer fails a language because other toolchains were being probed at the same time. Doctor now probes four languages at a time and the named ones first, on their own, so on a freshly booted machine or CI runner a gated verdict measures that toolchain only. The Rust probe is one cargo -vV instead of two shell-spawned cargo --version runs and a rustc -Vv, which also shortens the first list_supported_languages and the first Rust launch (#968)
  • A slow-starting debug adapter no longer fails the launch with ECONNREFUSED, and a failed launch is reported once — a launch whose debug adapter takes longer than ~12 s to start listening (rdbg on a loaded Windows runner) used to fail with Failed to connect DAP client: connect ECONNREFUSED while the adapter was still booting: the proxy's connect window was a fixed 60 attempts; it now ends only when the adapter process exits (at once, quoting the exit code and its last stderr lines) or when the launch's 30 s initialization deadline passes, whose stage-aware message the caller then gets. Direct-connect attaches (a remote rdbg/debugpy already listening) keep the previous 12.5 s window. A launch that fails after its adapter started was reported twice — the proxy worker sent a second copy after its teardown, with adapter PID=none exitCode=n/a in place of the real PID and exit code, and that degraded copy overwrote the first as the session's last proxy error. And the worker's stdout pipe was never read by the parent, so an embedder that does not silence console output (the server CLI does) had a worker that blocked solid, deaf to IPC, once its console transport had written 64 KB: the parent now drains it, and the worker's loggers skip the console transport altogether, so the per-session proxy-<sessionId>.log is its log and nothing fills a pipe no one reads (#963, #966, #967)
  • A .NET launch whose program is a .cs, .fs or .vb source file, which is what happens when dapLaunchArgs.program is left out, is now refused before netcoredbg starts. The answer says the program must be the compiled assembly, names dapLaunchArgs.program and the bin/Debug/<framework>/<Project>.dll convention, and names the built DLL when the project has exactly one. The same answer comes back from dryRunSpawn. Before this, the launch reached netcoredbg and failed at configurationDone with a bare 0x800700c1. When netcoredbg does answer with that code, for a native executable or an unmanaged or wrong-architecture DLL, the error now adds a sentence saying netcoredbg could not start the program as a .NET assembly (#959)
  • Windows: a wedged Dart/Flutter or .NET adapter no longer leaves its tool tree running when the session closes — the Dart bridge and the netcoredbg bridge ended their adapter with cp.kill() when the proxy worker's socket went away, which on Windows is TerminateProcess on the direct child alone; the Dart SDK launcher (dart.exe → dartvm.exe → flutter.bat → … → the app, DDS, the frontend server) and netcoredbg's launched debuggee were re-parented and kept running, and the worker's own tree kill then found an adapter that had already exited. Both bridges now end the child with taskkill /T /F on Windows first (the proxy worker's own idiom), fall back to the plain kill if the child is still there after 300 ms or taskkill cannot be started, and never touch a child that has already exited (its pid may be reused); POSIX keeps cp.kill(), where the worker's process-group kill covers the tree. The ordinary close is unaffected (the Dart adapter stops the app on terminate and exits by itself, #885); this is the path a wedged adapter takes. The netcoredbg bridge gains the exited-child guard it lacked (#937)
  • The adapter's own attach-transform notes reach the attach warning — the proxy launcher drained an adapter's transform diagnostics only for a launch, so the Dart adapter's note that a host/port attach became a no-auth ws://host:port/ws URI (and that deviceId is ignored on a Dart runner) was recorded and thrown away. They now join the attach answer's warning beside the dropped/forwarded key sentences, and COBOL's build options given without sources and its C-view-only note are diagnostics there too instead of log lines (#908)
  • pause_execution answers in the same shape as the step tools and wait_for_stop — pending, message, stopReason/rawStopReason, location (now with its source context), and the new threadId/note sit at the top level instead of only under data, which keeps the controller's own copy. A pause that landed on a thread other than the one the debugger reported names that thread and says why in note (#906, #904)
  • create_debug_session's attach shortcut takes everything attach_to_process does — processId, sourcePaths, justMyCode and breakOnExceptions were dropped silently, and a processId without a port did not trigger the attach at all. Both entry points now build one request: port, processId or an adapterConfig naming a debugger URI (vmServiceUri, websocketAddress, vmServiceInfoFile) attaches in the same call (host alone still does not), an invalid breakOnExceptions is refused before the session exists, and the shortcut no longer forces host: localhost / timeout: 30000 — the adapter's defaults apply, as for attach_to_process (a forced 30 s connect timeout on js-debug out-waited the 20 s verify window). The attach docs also no longer claim that stopOnEntry: false skips thread verification; it skips only the post-attach pause (#905)
  • A JavaScript pause that lands between event-loop dispatches says so — js-debug's pause on an idle Node server lands on an <anonymous> frame at <unknown_source>:1 with no scopes; it read as program code, so attach_to_process and pause_execution answered a bare location and get_stack_trace a one-frame stack with nothing said. A frame with no source at all is internal now whatever its line: the frame stays visible when it is the only one, get_stack_trace reports noProgramFrame: true, and the attach, pause and stack answers carry the note No program frame is active … continue_execution and trigger activity, or set a breakpoint (#900)
  • set_breakpoint on a local attach — statement and expectedContent addressing now work on an attach by processId or to a loopback host/port when the file exists on the mcp-debugger host, since that file is the debuggee's own source; the refusal stays for a remote attach and names the host, and that remote answer no longer carries content/context read from the host file it just said could not be trusted (a note says so instead) (#899)
  • A Windows pause_execution on a launched C/C++ program now shows the program's thread, not the injected break-in thread — CodeLLDB reports the pause on the helper thread Windows injects (an ntdll-only stack, every frame internal), while the program's own thread sits in main one list_threads away; get_stack_trace, get_local_variables and evaluate_expression now anchor on the first thread whose stack reaches the program and say which thread the stop was reported on in note. Breakpoint, step, exception and entry stops stay on the thread they were reported on, and an explicit threadId is always shown as asked (#898)
  • A failed attach or launch quotes the debug adapter's last stderr line — when the adapter exited or dropped its DAP connection while an attach was being verified, the answer said only debug adapter exited during attach verification (code=null); js-debug's own Error: Could not connect to debug target at http://localhost:5678: Could not find any debuggable target went to the proxy log and nowhere else. The proxy worker now sends the adapter's last sanitized stderr lines with its terminal status, and the message says how the adapter ended and quotes the line that matters: Failed to attach: The debug adapter ended the session during attach verification: it closed its DAP connection; its last stderr line: "Error: Could not connect to debug target at …". A proxy that dies during a launch carries the same line (Debug proxy exited unexpectedly during launch (code=1); adapter stderr: "…") (#897)
  • A JavaScript attach by inspector URL goes through the attach flow, and an attach js-debug refuses fails at once in its words — attach_to_process { adapterConfig: { websocketAddress } } reached js-debug as a launch whose body said request: 'attach' (the policy took its attach branch only on a numeric port): launch-branch keys on an attach, and a launch parent recorded for child adoption. The attach branch is now keyed on the request alone, honours websocketAddress without a port (continueOnAttach applies, so an --inspect-brk target runs briefly before the post-attach pause), accepts a numeric-string port, and refuses a port-less attach without a URL instead of launching. The handshake no longer swallows a rejected attach or launch request — it runs in the server process, where console output is silenced, so the rejection was invisible — and when js-debug answers the attach with its own error response the attach fails immediately: Failed to attach: The javascript debug adapter rejected the attach request: …. A stale proxy error from an earlier run is cleared before each attach attempt (#896)
  • Java attach_to_process { processId } is refused by name — 'java' does not attach by processId — JDWP has no PID step; start the JVM with -agentlib:jdwp=… and pass that port — instead of the PID being dropped silently and the JDI bridge failing later with "Port is required"; a Java attach that names neither port nor processId is refused up front the same way (#895)
  • Windows: debug adapters are no longer spawned detached, which made every Delve launch 8–13 s slow. Node's detached is DETACHED_PROCESS on win32 — the adapter gets no console — and Delve pays for that at every process it creates: measured on Windows 11, dlv version takes 0.1 s with an inherited or hidden console and 2–3 s detached, and a debug-mode launch of a trivial Go program spent 6 s in go build plus 4 s starting the debuggee instead of ~1 s in all — enough, under load, to hit the proxy's 30 s launch limit (issue #889's pre-push flake). The adapter keeps windowsHide (a hidden console) on Windows and stays detached on POSIX, where its own process group is what the shutdown signals; the Windows tree-kill is taskkill /T and never needed the group. The Go integration suite went from 48 s to 15 s on the same machine; Python, JavaScript, CodeLLDB, Java and Ruby adapter binaries start in the same time either way (#889)
  • The release dry-run fails on a package that has never been published — scripts/release-dry-run.sh asked npm only whether each package's version already existed, and a package that had never been published returned the same 404 as a not-yet-released version, so the check passed silently; the next tag would then have failed its first OIDC npm publish mid-run, after the earlier packages had gone out. The loop now asks for the package by name first: a 404 on the bare name fails the dry-run with the first-time-package steps from docs/release-checklist.md, a non-404 registry error stays a warning, and the already-published-version warning follows as before (#886)
  • Closing a flutter run session stops the desktop app — on Windows close_debug_session (and restart_debugging) left the app running after every close, four flutter_probe.exe after four sessions, with the tool's dart development-service beside it: the Flutter SDK's debug adapter treats DAP disconnect as the forced path and SIGKILLs only its own tool processes, and only a DAP terminate tells the tool to stop the app. mcp-debugger never sent one. The proxy worker now sends terminate ahead of the disconnect for adapters whose policy asks for it (the Dart and Flutter SDK adapters, terminateBeforeDisconnect), when the adapter advertised supportsTerminateRequest and the program has not already ended, waits up to 2 s for the answer and then disconnects as before; the Flutter desktop smoke and the Dart logpoint smoke now assert on the process table that the program is gone within 5 s of the close (#885)
  • Attach to a Flutter app running on an Android device or emulator — attach_to_process with adapterConfig: { runner: "flutter", deviceId: "emulator-5554" } and no URI now goes out as flutter attach -d <id>, which discovers the app's VM service through the device's log and forwards it itself: the attach answers in about 5 s and breakpoints hit right after. The URI another flutter run --machine reports in app.debugPort is that tool's own DDS proxy and never connects from a second tool, which is why the recipe the guide gave for desktop apps hung on a device; deviceId now counts as naming an attach target (so create_debug_session can attach in one call with it), the attach answer's warning says the app is being discovered, and a desktop deviceId without a URI is refused up front with the URI hint instead of waiting out the verification deadline. The probe app gains a periodic timer line (BP-TICK) so an attached app reaches a breakpoint on its own, and the Flutter e2e suites gain attach cases on the desktop (by URI) and the emulator (by device) (#882)
  • The .NET bridge no longer drops netcoredbg's last DAP frames — the TCP-to-stdio bridge ended the proxy's socket on the child's exit event, but Node fires exit when the process ends and close only once its stdio has drained, so an output/exited/terminated frame netcoredbg wrote just before exiting could still be in the pipe when the socket was closed (the same class as #860). The bridge now ends the socket on close, stops forwarding proxy bytes to a stdin that has no process behind it, logs an in-flight stdin error instead of dying on it, and says in its stderr when netcoredbg closed and when the proxy's socket errored, so the proxy log explains the next report of this class (#878)
  • Installs and Docker builds no longer fail on the GitHub API rate limit — vendoring the pinned js-debug release asked api.github.com for the release's asset list before downloading, and that lookup is subject to the unauthenticated quota of 60 requests per hour per IP: on a shared CI runner or inside a docker build (which has no token) it answered 403, failing the COBOL Host job's pnpm install and the daily published-artifact canary's image build. The pin in vendor-manifest.json already names the tag and the asset, so the vendoring now downloads it straight from releases/download/, which is outside the API quota — no token is needed for a pinned build. JS_DEBUG_VERSION=latest and version overrides still resolve through the API, and a direct download that fails falls back to it (#867, #813)
  • On Windows, a Rust, C/C++ or COBOL program's last line reaches get_output even when it has no newline — the debuggee writes into CodeLLDB's stdio pipes there, and the proxy splits that stream into lines, flushing a trailing fragment only when the pipe closes. CodeLLDB keeps its pipes open until the session is torn down, so a program whose final printf("result: 42") had no newline lost that text: the flush came after the exit had been reported and the session had stopped listening. For an adapter that outlives its debuggee the proxy now hands the fragment over when its exit-time drain settles on the pipes going quiet (#856) — before exited is forwarded, through the same path as a complete line, and as printed: no newline is added that the program never wrote. Adapters whose pipes close with the program (rdbg) are unchanged (#860)
  • A JavaScript function breakpoint on the first-declared function no longer misses its first call, and a late entry stop says where it landed — js-debug implements stopOnEntry with a breakpoint at line 1, column 1 that V8 resolves to the first breakable position in source order: for a file whose first declaration is a function the program calls later, that is the function's body, so the "entry" stop fires at the function's first call (after the top-level code before it ran) and again at every later call. The forced entry stop that binds a pre-launch function breakpoint was that very first call, and auto-continuing it lost the call for good with the breakpoint listed verified: true. The CDP bridge now recognises an entry stop whose paused frame is the function it just armed — matched by [[FunctionLocation]], never by name — and reports it as the function breakpoint. A stopOnEntry launch that lands inside a function carries the explanation in the stop's text (start_debugging, wait_for_stop, list_debug_sessions): where it stopped, why, that the code before it has run, that later calls stop again, and what to do instead. The JavaScript adapter also stops appending --inspect-brk=9229 to runtimeArgs for stopOnEntry and promoting a user's --inspect to --inspect-brk — js-debug strips --inspect-brk into stopOnEntry itself, so the first was inert and the second forced an entry stop nobody asked for. Reported upstream as microsoft/vscode-js-debug#2430 (#858)
  • On Windows, the end of a Rust, C/C++ or COBOL program is reported when it happens, not 2 s later — the proxy holds a program's exit until the output it printed on the way out has been forwarded, and took "the adapter's stdio pipes closed" as the sign of that. CodeLLDB outlives its debuggee and keeps those pipes open until the session is torn down, so on Windows (where the debuggee writes straight into them) the wait ran to its 2 s backstop at the end of every program: list_debug_sessions kept saying running, wait_for_stop kept blocking, and a start_debugging of a short program answered running with pending: true for a program that had already ended. For an adapter that outlives its debuggee the proxy now takes the pipes going quiet as the sign — 100 ms without output, counted from the adapter's exited and re-checked after a full turn of the event loop; the longest pause measured inside 9 MB of exit-time output was 14 ms idle and 39 ms with every core busy — so the end is reported about 100 ms after the fact and output that is still arriving keeps the wait open. The 2 s backstop is unchanged, as is rdbg, whose pipes do close with the program (#856)
  • The dev proxy no longer cuts backend tool calls off at 60 seconds — it forwarded every call with the MCP SDK client's default request options, so a call that is slow by design (evaluate_expression or hot_reload with a timeout up to 600 s, wait_for_stop) failed at 60 s with MCP error -32001: Request timed out and a hint that the backend "did not settle", which describes a restart in flight rather than a backend busy answering. Backend calls now get a ceiling above the longest a tool may take and carry the caller's cancellation signal, so a call the client gave up on stops in the backend too; a request timeout from a running backend is reported as a call that was not answered. Development-only: the proxy is not part of the shipped server (#854)
  • A JavaScript logpoint or condition that does not parse says so, where the breakpoint is — js-debug accepted a logMessage whose {expression} had a syntax error (or a malformed condition), never fired it, and reported it as a plain Unbound breakpoint; its only word on the cause was a Syntax error setting breakpoint with condition … line in the debuggee's stderr stream. Now set_breakpoint on a JavaScript session refuses a logMessage whose interpolations do not compile, with V8's own message (Invalid logpoint: logMessage expression {n +}: Unexpected token ')'), the way an expectedContent mismatch is refused; and for a plain condition js-debug rejects, the proxy correlates js-debug's stderr line with the request in flight — the setBreakpoints it sent, or the attach during which js-debug compiles the breakpoints it buffered — and stamps it onto the breakpoint record, so list_breakpoints and the launch's unbound-breakpoint warning carry the reason. The stderr line is still delivered as output (#853)
  • A breakpoint set while a launch is still starting reaches the debugger at once — set_breakpoint, remove_breakpoint and clear_breakpoints on a session whose start_debugging had not finished its handshake only updated the stored set: the adapter cannot take a breakpoint that early, and the proxy had already taken its own snapshot. Nothing sent the change until the launch's wait was over — up to 30 s, with the program running past the breakpoint all the while (measured: a breakpoint whose condition became true 11 s into the run stopped the program at 33 s) — and a removal was never sent at all, leaving a breakpoint the session no longer listed. The launcher now compares the stored set with what the proxy started with as soon as the program is launched, and sends the difference before it starts waiting for a stop, clearing what was removed (#851)
  • JavaScript logpoints are delivered by mcp-debugger, not by the program's console.log — js-debug compiles a logpoint into a console.log(...) call evaluated inside the debuggee, so a program that had replaced console.log (mcp-debugger's own stdio server does, to protect its protocol stream) lost every logpoint silently: verified: true, nothing in get_output, no hint why. The JavaScript policy now compiles the logMessage itself into a breakpoint condition that renders the message (strings raw, other values through util.inspect, a throwing {expr} shown inline as <ReferenceError: …>), hands it to a binding the proxy installs over js-debug's CDP proxy (Runtime.addBinding), and returns false so the program never pauses; the message arrives as a console entry in get_output and the output resource, whatever the program did to console. A logpoint that also carries a condition logs only when the condition holds instead of pausing (js-debug compiles the condition first and drops the message, #861). A pre-launch logpoint makes js-debug stop on entry so the binding is installed before the first hit; the SessionManager auto-continues that stop as it does for function breakpoints. The stored breakpoint keeps the user's logMessage and condition, so list_breakpoints and restart_debugging are unchanged (#850, #861)
  • No more focus-stealing console windows on Windows from spawned helper processes — on Windows, a console program started by a process without a console window of its own gets a new window that takes focus. mcp-debugger doctor's shell check and the orphan-process scan now pass windowsHide: true like the rest of the runtime. So do the build scripts, the dev proxy and the unit and integration tests, so a build or test run started from an agent, an IDE task or a git hook no longer flashes a window for every child process. A guard test fails when a new child_process call in the runtime, the scripts or the unit/integration tests omits the option (#843)
  • C/C++, Rust and COBOL stacks on Windows hide the CRT and NT runtime frames — the LLDB display filter knew glibc's start-up names and POSIX syscall wrappers but none of their Windows equivalents, and the source rule did not recognise CodeLLDB's path-less @symbol label (source: {name: '@NtWaitForSingleObject'}, which the anchor resolver carries into file), so a paused Windows program showed NtWaitForSingleObject, WaitForSingleObjectEx, three winpthreads frames, __tmainCRTStartup, mainCRTStartup, BaseThreadInitThunk and RtlUserThreadStart around its one line of main, with no hiddenFrames. The filter now treats an @symbol file as "no source", knows the Windows CRT start-up, the kernel32/ntdll thread and wait plumbing and pthread_*, and recognises MSYS2's mingw64/include and the MSVC vctools tree as system paths — under the same name + no-user-source rule, so a user function named Sleep with a workspace path is kept; the system-path roots are anchored, so a project's own lib/ folder no longer counts as one. Inspection anchors on the first visible frame (for a program asleep in std::this_thread::sleep_for, that header frame rather than main), and a stop that sits in a hidden frame of the anchored thread is disclosed as pausedFrame (the #672 rule). On Linux the pthread_* rule also hides glibc's nptl frames — pthread_*, __pthread_* and ___pthread_* — whose ./nptl/ source the filter already classified as a system path (#824)
  • Dev-proxy restarts keep status responsive — process termination, port discovery, and Docker
    cleanup now use bounded asynchronous commands instead of blocking the supervisor. Shutdown owns
    force-kill completion and clears its timers and listeners within the existing deadlines (#799)
  • .NET: the adapter finds its netcoredbg-bridge.js from a source checkout regardless of the working directory, and its unit tests no longer need a built dist/ or a repo-root cwd — pnpm --filter @debugmcp/adapter-dotnet test passes standalone (165/167 before). The bridge walk is now a pure candidate list shared in shape with the Dart bridge, pinned by a unit test; the candidate that reaches the project root from node_modules/@debugmcp/adapter-dotnet/dist was mislabelled a monorepo fallback (#743)
  • A worker whose exit is still being held for its IPC drain is reported as running, as the session is — while the proxy wrapper held a worker's raw exit back for the channel to drain (the #729 fix, up to 1 s), its public exit fields were already set, so ProxyManager.isRunning() answered false against a session still shown running or paused: a tool call landing in that window failed with "no active proxy", list_debug_sessions dropped the session's exposure, and stop() resolved without waiting for the exit. The wrapper now writes exitCode/signalCode when it delivers the exit event, keeps refusing writes and kills on the raw exit it has already seen, and the manager's one-exit-per-worker latch is reset when a worker is spawned rather than in cleanup, so a closure status followed by a held exit delivered after stop() no longer emits exit twice (#735)

Security

  • Debuggee output is no longer copied verbatim into the server and proxy logs at the default level — every DAP output event was written with its full body, at info, to the per-process server log (twice) and to the per-session proxy log, two files under the temp directory that outlive the session; a program that printed a credential left it there in plain text while get_output masked the same value, and the logs grew with everything a chatty program printed (one 480-line debuggee: 1.3 MB of log). Those records now carry the output's category and length only; the one verbatim copy is at debug level and masked like get_output is. Other event bodies (process, stopped, …) are logged after the same sanitization as request payloads — environment blocks counted, token-shaped values masked (#852)

📦 Installation

Docker:

docker pull debugmcp/mcp-debugger:0.26.0-beta.1

npm (global install):

npm install -g @debugmcp/mcp-debugger@0.26.0-beta.1

npx (no install):

npx @debugmcp/mcp-debugger@0.26.0-beta.1 stdio

PyPI:

pip install debug-mcp-server-launcher==0.26.0-beta.1

Optional adapters (for programmatic embedding; the CLI above bundles all of them):

npm install @debugmcp/adapter-python   # or: adapter-ruby, adapter-javascript,
                                       # adapter-go, adapter-java, adapter-dotnet,
                                       # adapter-dart, adapter-mock

🔏 Verify this release

gh attestation verify <asset>.tgz --repo debugmcp/mcp-debugger
gh attestation verify oci://index.docker.io/debugmcp/mcp-debugger:0.26.0-beta.1 --repo debugmcp/mcp-debugger
npm audit signatures   # in a project that installs @debugmcp packages

The PyPI launcher carries PEP 740 attestations — see the "Verified details" on its PyPI page.
SBOMs (SPDX + CycloneDX) are attached as release assets.

📚 Documentation

See the README for usage instructions.