Mercury CLI
Documentation

Code

Debugger and tests

Mercury's Debug tool is a debugger speaking the Debug Adapter Protocol. It sets breakpoints, steps, and inspects the stack, scopes and variables of a live process, launched or attached, and evaluates expressions against it. The model drives it under the same permission checks as every other tool.

On hosted models that support deferred tools, Test and Debug load on demand through ToolSearch. A local model loads Test in full when the project declares a test runner or Python tests. Debug is available when a debug adapter is reachable.

Operations

  • launch and attach: Start a process under the debugger or connect to a running one. Both ask permission. When a program already listens for a debugger on a port, the tool attaches to that port and starts no debugger of its own; a port with nothing listening is refused at once, naming the port and the start command. When an adapter sends a DAP runInTerminal request (integrated or external), Mercury runs the requested command, working directory and environment through the session's Bash permission checks and guards, then spawns the approved command directly from its argument list as a session-managed process without opening a terminal window. Its process ID is returned to the adapter, its stdout and stderr stream into Debug's bounded output buffer tagged [debuggee], and disconnect terminates the process tree.
  • breakpoints, threads and stack: Set breakpoints or inspect the stopped program's threads and call stacks. breakpoints takes file with lines or breakpoints: [{ line, condition?, hitCondition?, logMessage? }], and passing lines: [] or breakpoints: [] clears a file's last breakpoint across every session in the debug tree (with later child sessions inheriting none).
  • scopes, variables and evaluate: Inspect the stopped program's state and evaluate expressions. evaluate accepts context (repl, watch, hover, clipboard or variables, defaulting to repl), defaults frameId to the top frame of the thread that stopped last, and returns variablesReference with named and indexed counts (namedVariables, indexedVariables) so structured results expand directly through variables.
  • continue, next, stepIn, stepOut and pause: Control execution. launch, attach, continue, next, stepIn, stepOut, pause and restart accept timeoutMs (default 10,000 milliseconds; longer waits are allowed) to bound how long they wait for a stop; reaching the deadline reports that the program is still running (still running after <timeout>) without terminating it, and aborting a call cancels in-flight DAP requests and stop waits immediately (use status to inspect a running program or disconnect to end the session).
  • output, status and disconnect: Read debugger output, report the debug session's state or end it.
  • loadedSources, modules, exceptionBreakpoints, source, completions and setVariable: Higher-frequency inspection operations, gated on the capabilities the adapter announced.
  • functionBreakpoints, dataBreakpoints, instructionBreakpoints, disassemble, readMemory, writeMemory and restart: Native-debugging operations, gated on the capabilities the adapter announced. functionBreakpoints accepts function names or { name, condition?, hitCondition? } rows across every session in a multi-session debug tree, replays them to child sessions started later, and clears them across the tree with functions: []. dataBreakpoints stops on the write or read of a variable, instructionBreakpoints stops at a machine instruction, and writeMemory patches the debugged process's memory.
  • customRequest: Sends any DAP request verbatim. It asks permission because an arbitrary request can change the debugged process.

An operation the adapter did not announce is refused with a precise refusal.

Adapters

Built-in adapter rows cover python (debugpy, vendored in the build with an installed-module fallback), js (js-debug for Node/TypeScript, vendored in the build; MERCURY_JS_DEBUG_DAP pins a custom copy), lldb, gdb, codelldb, go, dotnet, ruby, php, bash, dart (dart debug_adapter from the Dart SDK), flutter (flutter debug-adapter from the Flutter SDK), kotlin (kotlin-debug-adapter), elixir (the elixir-ls debug adapter) and godot; a row that needs a binary appears when that binary is on PATH, and /health names the install for the ones that are not. Custom adapters are one JSON row each (command, transport, file types, launch defaults) via MERCURY_DAP_ADAPTERS or the dap-adapters.json file in the config home; that is also the road for a custom adapter installation.

Python debugging uses the bundled CPython 3.14 runtime, so no separate Python installation is required. The script runs on the project's interpreter when it is Python 3.10 or newer: MERCURY_PYTHON, the active or project virtual environment, or python3 on PATH. Otherwise it uses the bundled interpreter.

Dart files use the Dart SDK's stdio adapter. A flutter dependency in the nearest pubspec.yaml selects Flutter's stdio adapter instead. Install the Dart SDK or Flutter SDK and put its bin directory on PATH; Mercury downloads neither SDK. Both launch requests take the program path, optional arguments and working directory. Custom adapter rows still override the built-in rows.

codelldb uses CodeLLDB over stdio for Rust, C and C++. Mercury prefers it for Cargo projects when it is on PATH or already installed in the config home; other native programs keep lldb-dap as their default. An explicit adapter: "codelldb" launch can ask to download the pinned platform release once. CodeLLDB is MIT-licensed; its VSIX includes LLDB. The archive's size and SHA-256 are checked before extraction into <configHome>/adapters/codelldb; the MIT licence from the pinned upstream tag is installed beside the adapter. Nothing is added to the Mercury build. A refusal or non-interactive launch downloads nothing.

PHP files select php. Mercury can download the pinned PHP Debug adapter (MIT) once with approval, unpack its VSIX under <configHome>/adapters/php, and run extension/out/phpDebug.js under Node. Its licence is kept in the unpacked extension. The PHP runtime and Xdebug extension are separate prerequisites and are never installed by Mercury. Check php -m | grep -i xdebug; the Xdebug installation guide recommends pie install xdebug/xdebug and zend_extension=xdebug in PHP's configuration. The guide also documents pecl install xdebug as a legacy method. The adapter launches CLI scripts with Xdebug enabled and a local, automatically allocated connection port.

Shell files (.sh, .bash) select bash when vscode-bash-debug and bashdb are installed. Put bash-debug-adapter on PATH or unpack the extension so its entry is <configHome>/adapters/bash/extension/out/bashDebug.js. Bash and bashdb must also be on PATH, with bashdb's library beside its script or under the installation prefix's share/bashdb or lib/bashdb. A custom adapter row can set pathBashdbLib for another layout. The adapter requires Bash 4 or newer; macOS's /bin/bash 3.2 is not supported. bashdb is maintained and its current 5.2 branch supports Bash 5.2 and newer. The adapter's latest release is from 2021, so choose a compatible pair; Mercury downloads neither.

On macOS, a native adapter (lldb, gdb, codelldb) that starts and then never answers is usually waiting on the operating system: without Developer Mode, task_for_pid waits for an interactive authorisation a debug adapter cannot give, and the grant lasts one boot. The debugger's timeout message and the health check's IDE plane row read the setting live (DevToolsSecurity -status) and name the durable fix: sudo DevToolsSecurity -enable.

Debug a failing test

Call the Test tool's debug operation for the failing test to launch it directly under the debugger, with no launch configuration written and no runner arguments reconstructed by hand, and stop on the first line of the one test you named with its locals readable.

Python tests (pytest and unittest) run under debugpy; node-test, vitest and jest tests run under the bundled js-debug (vitest in one worker with its timeout off, jest in-band); Go tests run under Delve's DAP server (dlv), built in test mode for their package; cargo tests run under lldb-dap or gdb, from the test binary cargo builds, with --exact. The test is spelled as its runner names it: file::name or a bare name from the latest run for the Node runners, TestName, TestName/subtest or dir::TestName for Go, and module::path::test_name for cargo. When the debugger for that language is not installed on the machine, debug states the missing debugger and its install command in one line and still runs the test without a debugger stop.

run on a vitest, jest or Go project reads the runner's own machine report (vitest's and jest's JSON report, go test -json) and answers passed, failed and skipped counts with each failing case's name, file, line and message, as node-test and cargo do from their summaries; rerunFailed selects the recorded failures by the names the runner itself matches. When a run or rerunFailed test selection matches nothing in pytest, unittest, node-test, vitest, jest, Go or cargo, it is reported as no test matched <selection>: <the runner's own words> with zero counts and a no-match verdict (no-change outcome, neither failed nor passed).

When Test (discover, run, rerunFailed, debug or report) is given a path inside another project from any session folder, it reads that project's own test runner or Python tests, runs there, finds its records there and names the project it read. At a folder with no manifest of its own, Test checks manifests one level down: when one sub-package declares a test runner it runs there and names the subfolder, and when several sub-packages exist it lists them so framework or path can pick one. In a Python project whose tests are plain test_*.py files with no pytest configuration marker, Test runs them under pytest when pytest is importable for the project's interpreter (while files with a TestCase class or unittest.main( keep unittest, and a unittest.mock import alone does not select unittest); if pytest is not importable or unittest skips a test file it could not collect, the run reports failed and names the files and remedy.

The .vscode/launch.json import carries env and envFile (launch.json's env over the file over the shell, with workspaceFolder and env macros expanded and other macros refused by name), a plain preLaunchTask from tasks.json that runs before the launch and stops it with the reason when the task fails or its shape cannot be run, Python module launches, and attach configurations as attach profiles that name the Debug tool's attach call. Launch op: "run" reports the program's own exit code when it finishes.

Check debugger availability

Run mercury health or enter /health in a session and read the Tools withheld row, which names a withheld Debug tool and the adapters to install.

The tool is withheld from the catalogue on a machine where no debug adapter is reachable. A session already running picks the tool up at its next /clear or compaction once an adapter is reachable. MERCURY_DAP=0 removes the tool from the catalogue.

The census that decides availability reads the filesystem only: the vendored packs, PATH and the adapter tables, plus a memo for two toolchain probes, xcrun -f lldb-dap on macOS and gdb --version, which run once per process in the background. A catalogue built before the probes answer withholds the tool with words saying the probe has not answered yet, and the next build reads the answer. The health check waits for the probes before it writes its row.

Child debug sessions

An adapter can start child sessions: js-debug starts one session per Node process, and a test runner may start one per file. A child joins the same debug tree, and every operation addresses root and children uniformly. The tree is bounded at 16 children and 4 levels deep by default, so a runaway adapter cannot fork forever.