Imported from AlDanial/tdb (
SKILL.md). Install upstream withnpx skills add AlDanial/tdb. Copyright stays with the author.
Skill: Interactive Debugging with tdb (Python, C/C++, Perl)
Use this skill when you need to understand runtime behavior of code -- variable values, control flow, why a condition is or isn't met, what a function actually returns, or why an exception occurs. This is faster and more reliable than inserting print/logging statements.
tdb debugs Python (via debugpy, full feature set), C/C++ or any native
binary built with -g (via gdb -i dap, or lldb-dap with
adapter="lldb-dap"), and Perl (via a bundled adapter driving perl5db,
perl ≥ 5.18). The language is auto-detected from the target: .py →
Python, ELF/Mach-O/PE executable → C/C++, .pl/.pm/.t → Perl.
When to use this
- A bug depends on runtime state you can't deduce from reading the code
- You need to inspect variables at a specific point in execution
- You want to trace control flow through conditional branches or loops
- An exception traceback doesn't give enough context
- You want to test what an expression evaluates to in a live scope
Two ways to drive tdb
- MCP tools (preferred when available). If the
tdbMCP server is registered in this session, use its tools directly (debug_launch,control,inspect, ...) — no server process or curl needed. See "MCP mode" below. - Headless HTTP JSON-RPC. Start
tdb --headlessyourself and POST to/rpcwith curl. Works everywhere, no MCP registration required. See "Quick start" below.
MCP mode
Register the server once (any of the three invocations work):
claude mcp add tdb -- tdb-mcp # or: tdb --mcp, or: python -m tdb.mcp
The server owns the debug session — do not also start tdb --headless.
17 tools:
| Cluster | Tools |
|---|---|
| Lifecycle | debug_launch(program, args?, cwd?, stop_on_entry?, just_my_code?, python?, breakpoints?, lang?, adapter?), debug_attach(host, port, breakpoints?, path_mappings?, program?, lang?, adapter?), quit() |
| Control | control(action, timeout_s=30) — action ∈ {continue, next, step_in, step_out, pause, wait_for_stop} |
| Inspection | inspect(expressions), read_source(file_path), stack_trace(), status(), get_output() |
| Breakpoints | set_breakpoint(spec, condition?, hit_condition?), remove_breakpoint(spec), list_breakpoints() |
| Concurrency | threads(thread_id?), tasks(task_name?), processes(name_or_pid?), wait_graph(), rust_concurrency() |
Multi-language notes:
debug_launchauto-detects the language fromprogram— pass a compiled binary directly (debug_launch(program="/abs/path/prog")debugs it via GDB's DAP mode);.pl/.pm/.tauto-detects Perl.lang="cpp"/lang="perl"forces it;adapter="lldb-dap"selects lldb-dap instead of GDB. Thepythonparam is only valid for Python debuggees (errors otherwise).debug_attachworks for Perl debuggees too, not just Python — the Perl program must be prepared withDevel::TdbRemote(use Devel::TdbRemote;first line,listen()+wait_for_client()) in place ofdebugpy.listen()/wait_for_client(). C/C++ has no attach mode.- Perl stops during compilation. A launched Perl debuggee's first stop is
the first compile-time statement of the program (usually
use strict;near the top), not the first runtime statement — this is what makesBEGINblocks steppable. Stepping from there enters them (stackreports the frame asmain::BEGIN). Two implications when scripting: aset_breakpointissued before compilation finishes is held and comes back unverified, but a breakpoint inside aBEGINblock still fires on the first run — it's checked against each compile-time statement as compilation proceeds, no manual stepping needed (a condition on it that errors behaves like a bad condition at runtime and does not fire;hitConditionisn't honored at compile time; and a breakpoint on a non-statement line such as theBEGIN {line itself never fires during the compile phase); and reaching your program's runtime entry point takes a few extranext/step_incalls. tasks,processes, andwait_graphremain Python-only; for other languages they return a structured "not supported" error.threadsworks everywhere.- Rust is never auto-detected — a native binary detects as C/C++.
Pass
lang="rust"(launch and attach) to enable the Rust profile; a Rust remote attach also needsprogram=pointing at a local unstripped copy of the remote executable.rust_concurrency()returns a JSON snapshot of threads, waits, and deadlock/stall findings for a stopped Rust session, and a structured "not supported" error elsewhere. - GDB (the default C/C++ adapter):
inspect/evaluateexpressions go through GDB's CLI — prefix withprint(inspect(expressions=["print x"])); barexcollides with GDB's examine-memory command. lldb-dap evaluates bare expressions directly. - If breakpoints in a C/C++ file never bind, the binary likely lacks debug
info — rebuild with
-g -O0.
Typical flow:
debug_launch(program="/abs/path/script.py", breakpoints=["/abs/path/script.py:42"])
control(action="continue", timeout_s=30)
inspect(expressions=["x", "len(items)", "type(data)"])
control(action="next")
quit()
Notes:
breakpointsspecs are"file.py:42"strings; paths must be absolute.- If
controlreturnsstill running — call pause or wait again, the program didn't stop withintimeout_s. Callcontrol(action="pause")to interrupt it, orcontrol(action="wait_for_stop")to keep waiting.pausebypasses the session lock, so it works even while anothercontrolcall is still blocked. wait_graph()is the fastest way to diagnose an asyncio hang: it shows blocked tasks, what each awaits, and any deadlock cycles.inspectexecutes arbitrary Python in the debuggee — same caveat as theinspect/evaluateRPC actions.
Quick start (HTTP JSON-RPC)
1. Start the debug server
.venv/bin/python -m tdb --headless --stop-on-entry /path/to/script.py &
The server starts on http://127.0.0.1:8150/rpc. Use --server-port PORT to change it.
If the script takes arguments:
.venv/bin/python -m tdb --headless --stop-on-entry /path/to/script.py arg1 arg2 &
If the script needs a specific virtualenv:
.venv/bin/python -m tdb --headless --stop-on-entry --python /path/to/venv/bin/python /path/to/script.py &
For a C/C++ binary (compiled with -g), the same headless mode works — the
language is auto-detected; add --adapter lldb-dap to use lldb-dap instead
of GDB:
.venv/bin/python -m tdb --headless /path/to/binary arg1 &
2. Send commands via JSON-RPC
Every command is a POST to /rpc with {"action": "...", "params": [...]}.
Responses are {"timestamp": "...", "success": true/false, "value": "..."}.
# Check status
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"status","params":[]}'
# Set a breakpoint
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"set_breakpoint","params":["/absolute/path/to/file.py:42"]}'
# Continue execution (runs until breakpoint or exit)
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"continue","params":[]}'
# Inspect variables
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"inspect","params":["x", "y", "len(items)"]}'
# Step to next line
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"next","params":[]}'
3. Clean up
curl -s -X POST http://127.0.0.1:8150/rpc \
-H 'Content-Type: application/json' \
-d '{"action":"quit","params":[]}'
All actions
| Action | Params | Description |
|---|---|---|
help |
[] |
List all actions with expected params |
status |
[] |
Current state: stopped/running/terminated with location |
set_breakpoint |
["file:line"] or ["file:line", "condition", "hit_condition"] |
Set a breakpoint, optionally conditional |
remove_breakpoint |
["file:line"] |
Remove a breakpoint |
list_breakpoints |
[] |
Show all breakpoints with conditions |
continue |
[] or [timeout_s] |
Resume execution until next breakpoint or exit |
next |
[] or [timeout_s] |
Step over (execute current line, stop at next) |
step_in |
[] or [timeout_s] |
Step into function call |
step_out |
[] or [timeout_s] |
Step out of current function |
pause |
[] |
Pause a running program (works even while a step/continue is blocked) |
wait_for_stop |
[] or [timeout_s] |
Wait for the next stop without issuing a step |
inspect |
["expr1", "expr2", ...] |
Evaluate multiple expressions, return all results |
evaluate |
["expression"] |
Evaluate a single expression in the current scope |
stack_up |
[] |
Move up the call stack (toward caller) |
stack_down |
[] |
Move down the call stack (toward callee) |
get_stack_trace |
[] |
Show full call stack with current frame marked |
get_output |
[] |
Drain buffered stdout/stderr from the program |
get_source |
["file_path"] |
Read a source file's contents |
restart |
[] |
Restart the debug session (preserves breakpoints) |
quit |
[] |
Stop the debuggee and shut down the server |
SSE event stream
For real-time event monitoring, connect to the SSE endpoint:
curl -N http://127.0.0.1:8150/events
Events: initialized, stopped, continued, terminated, exited, output.
Each event is JSON with event, data, and timestamp fields.
Debugging strategies
Strategy 1: Targeted breakpoint inspection
When you know roughly where the bug is:
# Start server
.venv/bin/python -m tdb --headless --stop-on-entry /path/to/script.py &
sleep 2
# Set breakpoint at the suspicious line
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"set_breakpoint","params":["/path/to/file.py:87"]}'
# Run to the breakpoint
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"continue","params":[]}'
# Inspect everything relevant
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"inspect","params":["request", "response.status_code", "len(results)", "type(data)"]}'
# Check the call stack for context
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"get_stack_trace","params":[]}'
Strategy 2: Conditional breakpoints
When a bug only occurs for specific input:
# Break only when the problematic condition is true
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"set_breakpoint","params":["/path/to/file.py:42", "user_id == 12345"]}'
# Or break on the Nth iteration
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"set_breakpoint","params":["/path/to/file.py:42", null, "100"]}'
Strategy 3: Step-through exploration
When you don't know where the bug is:
# Set breakpoint at function entry
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"set_breakpoint","params":["/path/to/file.py:20"]}'
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"continue","params":[]}'
# Step line by line, inspecting as you go
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"next","params":[]}'
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"inspect","params":["result"]}'
# Step into a function call to see what happens inside
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"step_in","params":[]}'
Strategy 4: Evaluate to test fixes
Use evaluate to test expressions in the live scope before changing code:
# What would this expression return?
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"evaluate","params":["sorted(items, key=lambda x: x.priority)"]}'
# Would this condition catch the edge case?
curl -s -X POST http://127.0.0.1:8150/rpc -H 'Content-Type: application/json' \
-d '{"action":"evaluate","params":["x is not None and len(x) > 0"]}'
Important notes
- Breakpoint paths must be absolute. Use the full path, not relative.
nextvsstep_in:nextstays in the current function;step_inenters called functions.inspectvsevaluate:inspecttakes multiple expressions and labels each result;evaluatereturns a single raw result.- Non-Python debuggees: expressions are evaluated by the language's adapter, not Python — with GDB, prefix expressions with
print(see MCP notes above). - Step/continue actions block until the program stops (breakpoint, exception, or exit). Default timeout is 600 seconds; pass a shorter per-call timeout as the first param (e.g.
{"action":"continue","params":[30]}). On timeout the response is a success with valuestill running — call pause or wait again— follow up withpauseto interrupt orwait_for_stopto keep waiting. --stop-on-entrypauses at the first line. Without it, the program runs until a breakpoint or exit.- Output capture: stdout/stderr from the debuggee is buffered. Use
get_outputto retrieve it, or it's included automatically in step/continue responses. - After termination: stepping and evaluation are unavailable. Use
restartto start over, orquitto shut down.