mcp-debugger supports Ruby debugging through rdbg, the
CLI of Ruby's official debug gem, speaking the Debug Adapter Protocol over TCP.
MCP client ──> mcp-debugger ──> DAP proxy ──TCP/DAP──> rdbg ──> your Ruby program
Two modes are supported:
- Launch — mcp-debugger starts your script under
rdbgand debugs it from the first line. - Attach — your program is already running under
rdbg --open(locally, in a container, or in a Kubernetes pod) and mcp-debugger connects to its DAP socket directly. No adapter process is spawned; the proxy connects straight to the listening debugger.
- Ruby 2.7+ (Ruby 3.1+ recommended — it bundles the
debuggem) - debug gem 1.7+ providing
rdbg:gem install debug
- Verify:
ruby --version rdbg --version
mcp-debugger auto-detects ruby and rdbg from PATH plus common install locations
(RubyInstaller C:\RubyXX-x64\bin on Windows, Homebrew, system paths). Override with the
RUBY_PATH (or its alias RUBY_EXECUTABLE) and RDBG_PATH environment variables when needed.
Windows note: gem executables are
.batshims, which Node.js refuses to spawn directly. mcp-debugger automatically runs the siblingrdbgRuby script via your Ruby interpreter instead — no configuration required for RubyInstaller layouts.
create_debug_session { "language": "ruby", "name": "My Session" }
set_breakpoint { "sessionId": "...", "file": "/abs/path/app.rb", "line": 15 }
start_debugging { "sessionId": "...", "scriptPath": "/abs/path/app.rb" }
Under the hood mcp-debugger runs:
rdbg --open --host 127.0.0.1 --port <free-port> -c -- ruby /abs/path/app.rbrdbg suspends the script at load and waits for the debugger to connect, so breakpoints
are configured before any code runs — even for scripts that finish in milliseconds. With
stopOnEntry: false (the default) the entry pause is released automatically and execution
runs to your first breakpoint; with stopOnEntry: true you get control at the first line.
Conditional breakpoints are supported:
set_breakpoint { "sessionId": "...", "file": "/abs/path/app.rb", "line": 15, "condition": "i == 6" }
While paused, the usual inspection tools work: get_stack_trace, get_scopes (rdbg
reports a Local variables scope), get_local_variables, evaluate_expression
(evaluated in rdbg's repl context — expressions can read and modify program state),
step_over / step_into / step_out, and continue_execution.
rdbg -c runs the script as a child of the adapter process with inherited stdio, so the
program's puts/warn output lands on the adapter's pipes rather than in DAP output
events. The proxy forwards those lines as synthesized stdout/stderr entries, so
get_output (and the debug://sessions/{id}/output resource) returns the script's
output as usual; rdbg's own DEBUGGER: stderr banners are excluded and only appear in
the session log.
Because Ruby block-buffers $stdout when it is a pipe, launch mode also injects a small
prelude (via ruby -r) that sets $stdout.sync = true and $stderr.sync = true before
the script runs. Without it, puts output would only appear in get_output after the
process exits; with it, output streams in near-real-time — including while the session is
paused at a breakpoint (issue #317). A script that genuinely needs buffered stdout can
set $stdout.sync = false itself. Attach mode connects to a process the server did not
start, so no prelude is injected there — set $stdout.sync = true in your program if you
need mid-run output while attached.
Launch mode applies dapLaunchArgs.env and dapLaunchArgs.cwd to the debuggee's
process at spawn time (because rdbg -c starts the script immediately, the later DAP
launch request cannot carry them). An explicit env value there wins over the server's
inherited environment; without cwd the debuggee inherits the server's working
directory. Attach mode cannot set either — the target process is already running;
configure it before starting.
Pass useBundler through the launch configuration to run the target via bundle exec:
start_debugging {
"sessionId": "...",
"scriptPath": "/abs/path/bin/rspec",
"adapterLaunchConfig": { "useBundler": true }
}
Start your program with an rdbg DAP listener:
# Suspended at load, waiting for a debugger (good for debugging startup):
rdbg --open --host 127.0.0.1 --port 12345 app.rb
# Running immediately, debugger can attach at any time (good for services):
rdbg --open --host 127.0.0.1 --port 12345 --nonstop app.rbThen attach:
create_debug_session { "language": "ruby", "name": "Attach Session" }
attach_to_process { "sessionId": "...", "host": "127.0.0.1", "port": 12345 }
Attach pauses the target (mcp-debugger issues an explicit pause if the program was already running), so you can set breakpoints and inspect immediately. Detach with:
detach_from_process { "sessionId": "...", "terminateProcess": false }
The target keeps running after detach, and rdbg keeps listening — you can re-attach
later. Pass terminateProcess: true to kill the target instead.
Note: in attach mode there is no adapter process between mcp-debugger and the target, so
get_output captures nothing — the program's stdio stays on whatever terminal (or pod
log) the process was started in.
Because attach connects directly to rdbg's TCP socket, anything that forwards a TCP port
gives you remote debugging. A working demo lives in
examples/ruby/remote-attach/.
⚠️ Security: the rdbg DAP socket is unauthenticated and allows arbitrary code execution in the target process. Never expose it on a public interface. Reach it only through localhost port mappings,kubectl port-forward, or an SSH tunnel (ssh -L 12345:127.0.0.1:12345 user@host).
docker build -t ruby-remote-attach:demo examples/ruby/remote-attach
docker run --rm -d --name ruby-demo -p 12345:12345 ruby-remote-attach:democreate_debug_session { "language": "ruby" }
attach_to_process { "sessionId": "...", "host": "127.0.0.1", "port": 12345 }
set_breakpoint { "sessionId": "...", "file": "/app/app.rb", "line": 18 }
continue_execution { "sessionId": "..." }
Use the container's source paths for breakpoints (/app/app.rb, as reported by
get_stack_trace) — the debugger resolves paths against its own filesystem. For attach
sessions mcp-debugger skips host-side file existence checks for exactly this reason.
The same flow works against a pod through kubectl port-forward (verified with a
kind cluster and the manifest in the demo directory):
docker build -t ruby-remote-attach:demo examples/ruby/remote-attach
kind create cluster --name ruby-debug-demo
kind load docker-image ruby-remote-attach:demo --name ruby-debug-demo
kubectl apply -f examples/ruby/remote-attach/pod.yaml
kubectl port-forward pod/ruby-remote-attach 12399:12345attach_to_process { "sessionId": "...", "host": "127.0.0.1", "port": 12399 }
Breakpoints, conditional breakpoints, locals, and expression evaluation all work against
the pod exactly as they do locally. For a pod on a real cluster, the only difference is
where kubectl port-forward points.
Attach sessions send breakpoint paths to the remote rdbg verbatim — rdbg checks them
against the debug target's filesystem, not yours. When the debugger and the debuggee
see the project at different paths (host mcp-debugger + containerized app, or a
containerized mcp-debugger + host app), a path that is valid on your side can fail on the
target with <path> is not available. That warning is topology, not breakage:
-
Prefer target-side paths. Set breakpoints using the path the debuggee sees (
/app/app.rbinside the container, as reported byget_stack_trace), not the path on your machine. -
Or map paths with
localfsMap. rdbg accepts alocalfsMapattach option ("remote_prefix:local_prefix", comma-separated for multiple pairs) that translates paths between the two filesystems. Pass it throughattach_to_process:attach_to_process { "sessionId": "...", "host": "127.0.0.1", "port": 12345, "localfsMap": "/app:/home/user/project" } -
Reaching a host-side rdbg from a containerized mcp-debugger: use
host.docker.internalas the attach host (add--add-host=host.docker.internal:host-gatewayon Linux Docker), and remember the target-side rule still applies — the host rdbg needs host paths.
| Symptom | Likely cause / fix |
|---|---|
rdbg not found |
gem install debug, or set RDBG_PATH to the rdbg executable |
| Connect timeout on launch | Ruby startup can take a few seconds; check the session log under the temp directory for the spawn command and rdbg's stderr |
| Connect refused on attach | Verify the target was started with --open --host --port and the port is reachable (rdbg prints Debugger can attach via TCP/IP) |
| Breakpoint not verified on attach | Use the path as the debuggee sees it (e.g. /app/app.rb in a container), not the host path |
| Locals empty | Make sure the session is paused (breakpoint hit or explicit pause); rdbg reports locals only while stopped |
- ruby/debug — the debug gem and rdbg CLI
- Debug Adapter Protocol
- Tool reference — full MCP tool documentation