Repository navigation
Release 0.26.0-beta.1
Pre-release
Pre-release
·
29 commits
to main
since this release
🎉 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, bydeviceIdalone;hot_reloadand a hotrestart_debugging;doctor dartlists 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 pollinglist_debug_sessions; everypending: trueanswer 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_executionand attach, read from the stop's own stack; a later stop in the same frame reportschangedVariablesinstead 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_reloadreplacesredefine_classes— Flutter hot reload or Java class redefinition behind one tool;redefine_classesstays as a deprecated alias until 1.0 (#876)- One launch contract for every language —
start_debuggingholds about a second for the first stop and answerspaused,stoppedorrunningwithpending: true, the same in all ten languages;wait_for_stopreports the breakpoints a program ran past (#823, #826, #1033).dryRunSpawnis 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_processthat carries a key the adapter never used (aprocessIdbeside aporton Python/Ruby/Java, aportor remotehostbeside aprocessIdon 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-Idis 400, an unknown, DELETEd or reaped one is 404, an invalidOriginis 403, an unsupportedMCP-Protocol-Versionis 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), andscripts/mcp-conformance.mjsruns the protocol org's@modelcontextprotocol/conformancesuite (pinned) for its seven applicable scenarios against a localhttpserver, 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 apython -m pytestlaunch. Every launch used to carryprogram, filled fromscriptPath, 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 fillsprogramonly when the launch config names no target of its own, and the Python launch transform dropsprogrambeside amoduleorcodetarget and says so in the launch answer (dapLaunchArgs.module: launched as a module (python -m); program is not sent …).scriptPathstill names an existing file — the package's__main__.py, or the test file apytestrun is about — andcwdis what-mputs onsys.path: for a module or code launch it defaults to the directory above the top-most package containingscriptPath(the script's directory, the program-launch default, is inside the package, wherepython -m modpkgfails), the launch answer names the directory chosen, and an explicitcwdwins — the project root for a pytest run. A dry run reports such a launch'sdata.scriptasmodule: <name>orcode: …. 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 runspython -m pytest <file>[::<target>] -xfrom pytest's rootdir (pytest's own rule: the nearest ancestor of the file holdingpytest.ini, apyproject.tomlwith a[tool.pytest.ini_options]table, atox.iniwith[pytest]or asetup.cfgwith[tool:pytest], elsesetup.py; then the top-mostconftest.pyancestor;dapLaunchArgs.cwdoverrides 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 ownaddoptsturns it on, so its options still parse — and arms debugpy'suserUnhandledfilter besideuncaughtunder thebreakOnExceptionsdefault — pytest catches every test failure, souncaughtalone 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), withsnapshot.exception,snapshot.framesandsnapshot.locals; a passing test runs to completion with exit code 0.testTargetistest_name,Class::test_name,test_name[param]or a node id of that file (one naming another file is refused); yourargsare 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 bylist_supported_languages) and gated before the session changes state, sotestRunner: "jest"on a Python session is refused by name with the runners Python does take. The preset rides inadapterLaunchConfig, sorestart_debuggingreplays it and a dry run reportsdata.script: "module: pytest". pydevd's(note: full exception trace is shown but execution is paused at: …)suffix no longer leaks intosnapshot.exception.type. Measured shapes — an exceptionpytest.raisesexpects never pauses on Python 3.12+ and pauses once on 3.11 and older (pydevd's two tracing implementations),pytest.skip()pauses once, aprintbefore the assert reachesget_outputwith pytest's report after the run — are indocs/python/spike-notes-pytest.md. AnydapLaunchArgs: { 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_outnow carry the samesnapshotas 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 acontinue_execution→wait_for_stoploop over one breakpoint — replaceslocalswithchangedVariables: { name: { before, after } }(new variables appear withbefore: "<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 fulllocalsand 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_processand thecreate_debug_sessionattach shortcut answer a stop withsnapshot: { 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; DAPexceptionInfoincluded only if already merged, never awaited), the top ≤3 policy-filtered frames, and the top frame's locals through theget_local_variablespipeline 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=1turns it off;DEBUG_MCP_VARIABLE_ACCESS=explicitwithholds the locals half; entry stops, dry runs andpendinganswers carry none. Attach answers gain landing parity (location/threadId/contextbeside the note), and the rawlastStopemissions (wait_for_stop,get_stack_trace,list_debug_sessions) are now secret-redacted copies (#982) start_debugginganswers a stop with where it is, andget_stack_tracecarries the stale-build note — a launch (orrestart_debugging) that is paused when it answers now carrieslocation, the sourcecontext, andthreadId/notewhen there is something to say, the waywait_for_stopdoes, 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 await_for_stop, andget_stack_traceappends 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 Goexec, 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. Oncestart_debugginghas named the artefact — the.classunder aclasspathdirectory 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) — aset_breakpointin a newer source adds the hedgedwarningnaming both files and their mtimes, andstep_*,wait_for_stopandpause_executioncarry it as anote.AdapterPolicy.generatedFileCandidatesnow receives the transformed launch (program,launchConfig) and answers candidates taggedlinkedBy: 'sourceMap' | 'launch'; the core's js-debug-shaped exemption moved into the JS policy (#950) set_breakpointwarns when the TypeScript source is newer than the compiled file js-debug binds it through — the answer echoes the line from the.tsyou named while js-debug binds through the generated.js's source map, so adist/older thansrc/used to put the breakpoint on a different statement than the one shown, with no signal. The server now locates the compiled file (the nearesttsconfig.jsonthat names anoutDir, ordist|build|out|libunder the nearestpackage.jsonwhen none does, then a sibling.js; only a candidate whose.js.mapnames the source counts) and, when it is older than the source, adds a hedgedwarningnaming both files and their mtimes;step_*,wait_for_stopandpause_executioncarry the same sentence as anoteat 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) whichattach_to_processaddressing forms it accepts (processId,port, a debugger URI underadapterConfig), and the core refuses any other form by name before the session changes state: aprocessIdon 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, aportor a remotehoston a PID-only language (C/C++, COBOL, .NET) says so, aprocessIdbeside a remotehostis 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, andlist_supported_languagesshows the forms asmodes.attach.by. Heads-up: requests that carried an extra key the adapter silently dropped —processIdbeside aporton Python/Ruby/Java, aportor remotehostbeside aprocessIdon 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. TheprocessIdan adapter consumes (C/C++, COBOL, JavaScript) is no longer reported as an ignoredadapterConfigkey (#903) - A Flutter launch that misses its device says which devices exist, and
doctor dartlists them — aflutter runwhosedeviceIdmatches 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_stopanswersavailableDevices(id, name, kind, platform) with a note that says to pass one of them, the launch that ends within its hold says the same, andlist_debug_sessionscarriesendNoteandavailableDevices; 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 withsupportedByProject: falseand flutter's own remedy (flutter create .).mcp-debugger doctor dartgains a Targets line: the devicesflutter devicessees and the AVDsflutter emulatorslists, 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, orprocess._debugProcesson Windows), which opens its inspector on127.0.0.1:9229, confirms through the inspector's/json/listthat the port belongs to that PID, and attaches there;portalongsideprocessIdnames where a--inspect-port=<n>target opens. Previously the PID was silently dropped and js-debug dialledlocalhost:9229on 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, whereSIGUSR1would 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 neitherport,processIdnorwebsocketAddressis 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_executionreturns as soon as the adapter acknowledges the resume, andstart_debugging, the step tools,pause_executionandattach_to_processanswerpending: truewhen the stop they were waiting for has not arrived; until now the only way to learn about the later stop was to polllist_debug_sessions.wait_for_stop {sessionId, timeout?}blocks until the session is paused (answering with the samelastStoprecord the session listing shows, plus the top frame'slocationand source line) or over (the terminal state, withexitCodewhen 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 answersstate: "running"withpending: trueso it can simply be called again. The wait is on the session's state rather than on the adapter's rawstoppedevent, so the entry stop astopOnEntry: falselaunch resumes by itself is never reported, and a call the client cancels releases its wait. Every existingpending: truemessage 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 macosand the iOS simulator (deviceId= the simulator's UDID, asflutter deviceslists 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=1skips the simulator block; the e2e helper now generates the probe'sios/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 iddart, bundled in@debugmcp/mcp-debugger) drives the SDK's own debug adapters,dart debug_adapter [--test]andflutter 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 dartshows what was found and the devices and emulators it sees). Therunner(dart,dart-test,flutter,flutter-test) is detected from the nearestpubspec.yamland the program path; an explicitrunnerwins. Measured through the server and covered by host e2e suites: Dart programs anddart testfiles; Flutter widget and integration tests andflutter runon 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/Allexception filters,noDebug,stopOnEntryas a temporary breakpoint onmain;hot_reload(Flutter hot reload) and a hotrestart_debuggingonce the app has started. Attach by VM-service URI or service-info file, or — for an app on an Android device — bydeviceIdalone (#882); a Flutter launch that misses its device answers with the devices that exist (#877); closing aflutter runsession stops the app (#885); a simulator or desktopdeviceIdwithout a URI is refused up front (#1020); the tool's daily update banner no longer breaks the launch (#1026);step_overrepeats 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 sameSession not foundan invalid id gets (orFailed to close debug session, or a protocol error fromhot_reload) while the program kept running under the first client. Every tool now answerssuccess: falsewitherror: "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 are0). 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.mdnow says that debug sessions are per client in HTTP mode (#939) - Internal: the functional DAP core no longer carries a
currentThreadIdoradapterConfiguredsnapshot. Both were written on every stop and launch and read by nothing — the stop anchor has lived only inProxyManager.currentThreadIdsince #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 namedmain, 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 inmain. A Gopause_executionreported 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; athreadId-lessget_stack_traceanswered from a thread other than the one the stop was reported on carries the noteThe stop was reported on thread X; inspection is anchored on thread Y.;get_stack_traceresults carrynoProgramFrame: truewhen no frame of the shown stack is program code (#904) - A Dart
step_overnow leaves the line. The SDK debug adapter stops once per token position, so a line likefinal answer = counter.value + 2;took threestep_overcalls, 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_reloadreplacesredefine_classesand 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 runsbuild(), 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 oneAdapterPolicy.liveUpdatemember — 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_classesstays as a deprecated alias (#876)restart_debugginguses the adapter's own restart when it offers one — a Flutter hot restart — for the adapters measured for it (the policy'srestartViaDapRequestpin — Flutter today) whose live capabilities advertisesupportsRestartRequest(the Flutter adapter does, in a DAPcapabilitiesevent once the app has started), the DAPrestartrequest 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 carriesdata.hotRestart: true, the output buffer is kept (nooutputReset), breakpoints stay armed in the adapter, astopOnEntry: truelaunch stops at entry again, and a Flutter attach session can be restarted this way too. The proxy worker now forwards the adapter'scapabilitiesevents merged into the session's capabilities, treatsrestartas 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 advertisesrestarttoo, 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(andwait_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 withoutsupportsLogPoints) are still counted as armed, as before (#865) pi install npm:@debugmcp/mcp-debuggerregisters the server with pi's built-in MCP support (pi 0.99+) — the package ships a pi extension that callspi.registerMcpServer(), which is how pi lets a package add a server, so the debugger's tools reach pi without the communitypi-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'spi.mcpmanifest entry, which pi itself ignores. The extension runs the CLI installed next to it (nonpxdownload at startup), withdeferredexposure, so pi lists the debugger in its system prompt and the model loads the 29 tools withtool_search, and a 180 s request timeout for slow launches. The adapter-onlypi.mcpentry andpi.mcp.jsonare 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_debugginganswers 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:pausedwith the reason,stoppedwith the exit code, orrunningwithpending: true. That is now one rule for all ten languages. Before, JavaScript launches answeredrunningas 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, andwait_for_stopblocks until the stop arrives. AstopOnEntrylaunch is still answered at its entry stop however long that takes, and anoDebuglaunch 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 answeringpaused, and one that used to answer a barerunningnow carriespending: true; in the other languages a breakpoint first reached more than about a second after launch answerspending: trueinstead of holding the call open — callwait_for_stop. Thependingmessage 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, andlist_breakpointsreports 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 readverified: 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 undernoDebug, and the launch answer quotes it as a refusal rather than advising to check the symbol name.AdapterPolicy.isSessionReadyis 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'sstateis 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 sessionstopped. 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.jsonfilesfield, 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'svendor-manifest.jsonand the shared package's README (#811, #796) - Go: Delve 1.27's own
exitedevent is supported alongside the older console-line exit status — Delve 1.27 sends a DAPexitedevent with the program's exit code (go-delve/delve#4371). mcp-debugger already uses that event when it arrives, soexitCodeinlist_debug_sessionsand thestart_debuggingrun-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'sProcess N has exited with status Sconsole line. One visible difference: with Delve 1.27+ in debug mode, that console line no longer shows up inget_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,isSessionTerminatedin@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_classesis a deprecated alias ofhot_reload— still accepted with the same arguments and answered the same way, with awarningnaming the new tool; no longer advertised bytools/list; removed at 1.0. The tool count stays at 29 (#876)
Removed
IDebugAdapter.translateErrorMessageis 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 thatvalidate(), the launch gate andvalidateEnvironment()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.getDefaultLaunchConfigis removed from@debugmcp/shared(a required member, asAdapterPolicy.isSessionReadywas in #823). Nothing called it: the only launch defaults ever applied are the session core's{ stopOnEntry: false, justMyCode: true }, merged before the adapter'stransformLaunchConfig(a policy'sdefaultStopOnEntrymay still decidestopOnEntrywhen the caller gave none), and the values the adapters declared contradicted that — .NET and Java saidstopOnEntry: true, most saidcwd: process.cwd()(the server's directory; the launcher defaultscwdto 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.getDefaultAttachConfigis removed from@debugmcp/shared. The host never called it, so the values the eight adapters returned (Ruby's and CodeLLDB'sstopOnEntry: true, Dart'stimeout: 30000) never reached an attach; the defaults that apply live in each adapter'stransformAttachConfig. 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:
dryRunSpawnon the proxy config and init payload, the worker'shandleDryRun, thedry_run_completestatus anddry-run-completeevent,getDryRunSnapshot/hasDryRunCompletedon the proxy manager, and the session manager'sdryRunTimeoutMs. Since #961 a dry run is answered in the server from the launch preparation and never reaches the proxy (#812) DEBUG_MCP_SKIP_AUTO_STARTis gone — the env var the bundled CLI set and restored around its import of the server entry no longer exists, and aDEBUG_MCP_SKIP_AUTO_START=1inherited from the environment no longer stopsdist/index.jsfrom 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 importingsrc/index.tsbehind 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
.tmpstaging file or a Windows.execopy 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.exeseen 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, itsExecuting script:markers, theLaunching:andLaunched processlines, andAttached to processon an attach) landed inget_outputahead 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 theconsolecategory only (the engine's "Process exited with code N." stays, as does the same text on stdout or stderr). Andset_breakpointwith anexpectedContentthat 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
scriptPathlaunches, and a failed build says why — a directory target defaulted its working directory to the directory's parent, sogo buildran under the parent's owngo.modand failed ("main module … does not contain package …"), and without an explicitmodethe directory was treated as a binary (dlv exec: "not an executable file"). A directory is now its own cwd and adebugtarget. 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 toget_outputto 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 ahello.cppbuilt 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 onelatest.jsonpointer 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>-2directory 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 withEACCES … 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), seedocs/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'sperform_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 reads0000-MAIN (PERFORM 1000-INIT)at the PERFORM statement on every compiler;step_out's PERFORM line comes from the same reading. Docs: theMCP_COBOL_ALLOW_PREBUILTvariable was documented but never read — a prebuilt COBOL executable launches without cobc as it always did (#1034) wait_for_stopreports the breakpoints a program ran past when it ends after the launch hold — the "verified but never hit" summary andunhitBreakpoints(#701) were only on thestart_debugginganswer, so a program that outlived the 1 s hold got apendinglaunch answer and then a bare "ran to completion" from thewait_for_stopthat reported its exit: the same program answered differently depending on where it ended. The terminalwait_for_stopanswer now carries the same summary when no stop was recorded in the launch — the sentence names the verified breakpoints andunhitBreakpointslists 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_debuggingandwait_for_stopsaid the program "will stop only for an uncaught exception the debugger catches by default" whatever was armed: underbreakOnExceptions: "none"(the filter the caller opted out of, #1009), on a Ruby launch with nobreakOnExceptions(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 andwait_for_stoppending answers say which, promising only the exit. An explicitbreakOnExceptionsthe adapter resolves to no filter ("uncaught"on Ruby) now adds awarningnaming the modes it does support instead of being a silent no-op; the policy default never warns. The cpp policy pinsstopsOnUncaughtWithoutFilterper platform, and the cross-policy contract test requires that pin from any policy whoseuncaughttable 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-Lengthline 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-analyticsahead ofdebug-adapter, and the shared decoder resyncs on aContent-Lengthfound 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
deviceIdalone is refused up front — the simulator has no logcat-style discovery of an app another tool started, soflutter attach -d <udid>waited for a connection that never came and the attach failed only at the thread-verification deadline; a simulator UDID withoutadapterConfig.vmServiceUriis now refused immediately with the URI hint, as desktop ids are, and theapp.debugPortURI of theflutter run --machinethat started the app attaches in ~4 s (measured on an iMac Pro); Android devices keep the no-URI recipe (#1020) httpmode answers an unknownMcp-Session-Idwith 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 aninitializestill 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 reportedrunningwith no stop and no exit. The Python launch and attach transforms now defaultsubProcesstofalse: children run undebugged, and an explicitsubProcess: trueis 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 siblingpythonw.exeto keep console windows off the screen (#215), and debugpy's launchpythondefaults 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 insidesubprocesswithOSError: [WinError 6] The handle is invalidbefore it existed. The adapter now runs under the session's interpreter on the hidden console the proxy'swindowsHidespawn already gives it, which the launcher and the debuggee inherit: measured at zero new visible console windows over a full breakpoint session,pytest -n 2spawns its workers, andsys.executablein the debuggee ispython.exe(#993) stopOnEntry: truenow pauses a .NET launch at entry, on the opening brace ofMainwith reasonentry. netcoredbg reads the flag asstopAtEntry, and the adapter used to send onlystopOnEntry, so the program ran to completion. The launch now carries the same decision under both names. AstopAtEntrythe caller passes inadapterLaunchConfigstill wins (#973)- Toolchain checks are cheaper and more reliable — the checks behind
list_supported_languages, the launch gate andmcp-debugger doctornow 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 ranjava -versiontwice (two JVM starts) and now runs it once, Python imported debugpy twice on the chosen interpreter and now does it once, Go runsdlv versionanddlv dap --helpside by side. Rust'scargobuilds, 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 onecargo -vVinstead of two shell-spawnedcargo --versionruns and arustc -Vv, which also shortens the firstlist_supported_languagesand 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 withFailed to connect DAP client: connect ECONNREFUSEDwhile 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, withadapter PID=none exitCode=n/ain 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-sessionproxy-<sessionId>.logis its log and nothing fills a pipe no one reads (#963, #966, #967) - A .NET launch whose program is a
.cs,.fsor.vbsource file, which is what happens whendapLaunchArgs.programis left out, is now refused before netcoredbg starts. The answer says the program must be the compiled assembly, namesdapLaunchArgs.programand thebin/Debug/<framework>/<Project>.dllconvention, and names the built DLL when the project has exactly one. The same answer comes back fromdryRunSpawn. Before this, the launch reached netcoredbg and failed atconfigurationDonewith a bare0x800700c1. 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 isTerminateProcesson 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 withtaskkill /T /Fon 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 keepscp.kill(), where the worker's process-group kill covers the tree. The ordinary close is unaffected (the Dart adapter stops the app onterminateand 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 ahost/portattach became a no-authws://host:port/wsURI (and thatdeviceIdis ignored on a Dart runner) was recorded and thrown away. They now join the attach answer'swarningbeside the dropped/forwarded key sentences, and COBOL's build options given withoutsourcesand its C-view-only note are diagnostics there too instead of log lines (#908) pause_executionanswers in the same shape as the step tools andwait_for_stop—pending,message,stopReason/rawStopReason,location(now with its sourcecontext), and the newthreadId/notesit at the top level instead of only underdata, 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 innote(#906, #904)create_debug_session's attach shortcut takes everythingattach_to_processdoes —processId,sourcePaths,justMyCodeandbreakOnExceptionswere dropped silently, and aprocessIdwithout aportdid not trigger the attach at all. Both entry points now build one request:port,processIdor anadapterConfignaming a debugger URI (vmServiceUri,websocketAddress,vmServiceInfoFile) attaches in the same call (hostalone still does not), an invalidbreakOnExceptionsis refused before the session exists, and the shortcut no longer forceshost: localhost/timeout: 30000— the adapter's defaults apply, as forattach_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 thatstopOnEntry: falseskips 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>:1with no scopes; it read as program code, soattach_to_processandpause_executionanswered a bare location andget_stack_tracea 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_tracereportsnoProgramFrame: true, and the attach, pause and stack answers carry the noteNo program frame is active … continue_execution and trigger activity, or set a breakpoint(#900) set_breakpointon a local attach —statementandexpectedContentaddressing now work on an attach byprocessIdor to a loopbackhost/portwhen 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 carriescontent/contextread from the host file it just said could not be trusted (anotesays so instead) (#899)- A Windows
pause_executionon 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 inmainonelist_threadsaway;get_stack_trace,get_local_variablesandevaluate_expressionnow anchor on the first thread whose stack reaches the program and say which thread the stop was reported on innote. Breakpoint, step, exception and entry stops stay on the thread they were reported on, and an explicitthreadIdis 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 ownError: Could not connect to debug target at http://localhost:5678: Could not find any debuggable targetwent 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 alaunchwhose body saidrequest: '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, honourswebsocketAddresswithout a port (continueOnAttachapplies, so an--inspect-brktarget 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 rejectedattachorlaunchrequest — 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 neitherportnorprocessIdis 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
detachedisDETACHED_PROCESSon win32 — the adapter gets no console — and Delve pays for that at every process it creates: measured on Windows 11,dlv versiontakes 0.1 s with an inherited or hidden console and 2–3 s detached, and a debug-modelaunchof a trivial Go program spent 6 s ingo buildplus 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 keepswindowsHide(a hidden console) on Windows and stays detached on POSIX, where its own process group is what the shutdown signals; the Windows tree-kill istaskkill /Tand 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.shasked 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 OIDCnpm publishmid-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 fromdocs/release-checklist.md, a non-404 registry error stays a warning, and the already-published-version warning follows as before (#886) - Closing a
flutter runsession stops the desktop app — on Windowsclose_debug_session(andrestart_debugging) left the app running after every close, fourflutter_probe.exeafter four sessions, with the tool'sdart development-servicebeside it: the Flutter SDK's debug adapter treats DAPdisconnectas the forced path and SIGKILLs only its own tool processes, and only a DAPterminatetells the tool to stop the app. mcp-debugger never sent one. The proxy worker now sendsterminateahead of thedisconnectfor adapters whose policy asks for it (the Dart and Flutter SDK adapters,terminateBeforeDisconnect), when the adapter advertisedsupportsTerminateRequestand 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_processwithadapterConfig: { runner: "flutter", deviceId: "emulator-5554" }and no URI now goes out asflutter 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 anotherflutter run --machinereports inapp.debugPortis 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;deviceIdnow counts as naming an attach target (socreate_debug_sessioncan attach in one call with it), the attach answer'swarningsays the app is being discovered, and a desktopdeviceIdwithout 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
exitevent, but Node firesexitwhen the process ends andcloseonly once its stdio has drained, so anoutput/exited/terminatedframe 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 onclose, 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.comfor 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 adocker build(which has no token) it answered 403, failing the COBOL Host job'spnpm installand the daily published-artifact canary's image build. The pin invendor-manifest.jsonalready names the tag and the asset, so the vendoring now downloads it straight fromreleases/download/, which is outside the API quota — no token is needed for a pinned build.JS_DEBUG_VERSION=latestand 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_outputeven 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 finalprintf("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) — beforeexitedis 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
stopOnEntrywith 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 listedverified: 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. AstopOnEntrylaunch that lands inside a function carries the explanation in the stop'stext(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=9229toruntimeArgsforstopOnEntryand promoting a user's--inspectto--inspect-brk— js-debug strips--inspect-brkintostopOnEntryitself, 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_sessionskept sayingrunning,wait_for_stopkept blocking, and astart_debuggingof a short program answeredrunningwithpending: truefor 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'sexitedand 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_expressionorhot_reloadwith atimeoutup to 600 s,wait_for_stop) failed at 60 s withMCP error -32001: Request timed outand 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
logMessagewhose{expression}had a syntax error (or a malformedcondition), never fired it, and reported it as a plainUnbound breakpoint; its only word on the cause was aSyntax error setting breakpoint with condition …line in the debuggee's stderr stream. Nowset_breakpointon a JavaScript session refuses alogMessagewhose interpolations do not compile, with V8's own message (Invalid logpoint: logMessage expression {n +}: Unexpected token ')'), the way anexpectedContentmismatch is refused; and for a plainconditionjs-debug rejects, the proxy correlates js-debug's stderr line with the request in flight — thesetBreakpointsit sent, or theattachduring which js-debug compiles the breakpoints it buffered — and stamps it onto the breakpoint record, solist_breakpointsand 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_breakpointandclear_breakpointson a session whosestart_debugginghad 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 aconsole.log(...)call evaluated inside the debuggee, so a program that had replacedconsole.log(mcp-debugger's own stdio server does, to protect its protocol stream) lost every logpoint silently:verified: true, nothing inget_output, no hint why. The JavaScript policy now compiles thelogMessageitself into a breakpoint condition that renders the message (strings raw, other values throughutil.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 aconsoleentry inget_outputand the output resource, whatever the program did toconsole. A logpoint that also carries aconditionlogs 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'slogMessageandcondition, solist_breakpointsandrestart_debuggingare 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 passwindowsHide: truelike 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 newchild_processcall 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
@symbollabel (source: {name: '@NtWaitForSingleObject'}, which the anchor resolver carries intofile), so a paused Windows program showedNtWaitForSingleObject,WaitForSingleObjectEx, three winpthreads frames,__tmainCRTStartup,mainCRTStartup,BaseThreadInitThunkandRtlUserThreadStartaround its one line ofmain, with nohiddenFrames. The filter now treats an@symbolfile as "no source", knows the Windows CRT start-up, the kernel32/ntdll thread and wait plumbing andpthread_*, and recognises MSYS2'smingw64/includeand the MSVCvctoolstree as system paths — under the same name + no-user-source rule, so a user function namedSleepwith a workspace path is kept; the system-path roots are anchored, so a project's ownlib/folder no longer counts as one. Inspection anchors on the first visible frame (for a program asleep instd::this_thread::sleep_for, that header frame rather thanmain), and a stop that sits in a hidden frame of the anchored thread is disclosed aspausedFrame(the #672 rule). On Linux thepthread_*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.jsfrom a source checkout regardless of the working directory, and its unit tests no longer need a builtdist/or a repo-root cwd —pnpm --filter @debugmcp/adapter-dotnet testpasses 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 fromnode_modules/@debugmcp/adapter-dotnet/distwas 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_sessionsdropped the session'sexposure, andstop()resolved without waiting for the exit. The wrapper now writesexitCode/signalCodewhen it delivers theexitevent, 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 afterstop()no longer emitsexittwice (#735)
Security
- Debuggee output is no longer copied verbatim into the server and proxy logs at the default level — every DAP
outputevent was written with its full body, atinfo, 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 whileget_outputmasked 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 atdebuglevel and masked likeget_outputis. 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.1npm (global install):
npm install -g @debugmcp/mcp-debugger@0.26.0-beta.1npx (no install):
npx @debugmcp/mcp-debugger@0.26.0-beta.1 stdioPyPI:
pip install debug-mcp-server-launcher==0.26.0-beta.1Optional 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 packagesThe 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.