textual-debugger 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/PKG-INFO +91 -6
- {textual_debugger-0.1.3/src/tdb → textual_debugger-0.1.4}/README.md +89 -5
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/pyproject.toml +3 -1
- {textual_debugger-0.1.3 → textual_debugger-0.1.4/src/tdb}/README.md +89 -5
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/__init__.py +1 -1
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_handlers/inspection.py +39 -123
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/cli.py +20 -8
- textual_debugger-0.1.4/src/tdb/mcp/__init__.py +10 -0
- textual_debugger-0.1.4/src/tdb/mcp/__main__.py +6 -0
- textual_debugger-0.1.4/src/tdb/mcp/server.py +306 -0
- textual_debugger-0.1.4/src/tdb/mcp/session.py +205 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/app.py +14 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/handlers.py +156 -105
- textual_debugger-0.1.4/src/tdb/session/inspect_service.py +265 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/PKG-INFO +91 -6
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/SOURCES.txt +5 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/entry_points.txt +1 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/requires.txt +1 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/LICENSE +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/setup.cfg +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/__main__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/_timeouts.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_handlers/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_handlers/dap_events.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_handlers/routing.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_handlers/ui_panels.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/app_helpers.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/breakpoint_hook.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/dap/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/dap/client.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/dap/messages.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/dap/protocol.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/dap/types.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/inspection.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/inspection_full.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/keybindings/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/persist.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/post_mortem.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/processes_cache.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/event_handler.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/rpc_types.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/server/runner.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/child_processes.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/controller.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/event_bus.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/messages.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/post_mortem_loader.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/state.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/statement_stepper.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/terminal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/session/textual_handler.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/source_analysis.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/__init__.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/_inspection_modal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/async_tasks_modal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/breakpoint_view.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/code_view.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/console_view.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/evaluate_console.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/full_contents_modal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/menu_bar.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/modals.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/processes_modal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/stack_view.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/status_bar.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/threads_modal.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/tdb/widgets/variable_view.py +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/dependency_links.txt +0 -0
- {textual_debugger-0.1.3 → textual_debugger-0.1.4}/src/textual_debugger.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: textual-debugger
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A TUI Python debugger based on textual and debugpy
|
|
5
5
|
Author-email: Al Danial <al.danial@gmail.com>
|
|
6
6
|
License: textual-debugger is licensed to you under the MIT/X Consortium license:
|
|
@@ -51,6 +51,7 @@ Requires-Dist: debugpy>=1.8.0
|
|
|
51
51
|
Requires-Dist: pygments>=2.17.0
|
|
52
52
|
Requires-Dist: fastapi>=0.115.0
|
|
53
53
|
Requires-Dist: uvicorn>=0.30.0
|
|
54
|
+
Requires-Dist: mcp>=1.28.0
|
|
54
55
|
Provides-Extra: dev
|
|
55
56
|
Requires-Dist: pytest; extra == "dev"
|
|
56
57
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -738,11 +739,12 @@ curl -s -X POST http://127.0.0.1:8150/rpc \
|
|
|
738
739
|
| `set_breakpoint` | `["file:line"]` or `["file:line", "condition", "hit_condition"]` | Set a breakpoint |
|
|
739
740
|
| `remove_breakpoint` | `["file:line"]` | Remove a breakpoint |
|
|
740
741
|
| `list_breakpoints` | `[]` | Show all breakpoints |
|
|
741
|
-
| `continue` | `[]` | Resume execution |
|
|
742
|
-
| `next` | `[]` | Step over |
|
|
743
|
-
| `step_in` | `[]` | Step into |
|
|
744
|
-
| `step_out` | `[]` | Step out |
|
|
745
|
-
| `pause` | `[]` | Pause execution |
|
|
742
|
+
| `continue` | `[]` or `[timeout_s]` | Resume execution; on timeout returns `"still running — call pause or wait again"` (success) |
|
|
743
|
+
| `next` | `[]` or `[timeout_s]` | Step over |
|
|
744
|
+
| `step_in` | `[]` or `[timeout_s]` | Step into |
|
|
745
|
+
| `step_out` | `[]` or `[timeout_s]` | Step out |
|
|
746
|
+
| `pause` | `[]` | Pause execution; bypasses the dispatch lock so it can interrupt an in-flight blocking action |
|
|
747
|
+
| `wait_for_stop` | `[]` or `[timeout_s]` | Wait for the next stop without issuing a step (use after `continue` returns `"still running"` to keep waiting) |
|
|
746
748
|
| `inspect` | `["expr1", "expr2", ...]` | Evaluate multiple expressions |
|
|
747
749
|
| `evaluate` | `["expression"]` | Evaluate a single expression |
|
|
748
750
|
| `stack_up` | `[]` | Move up the call stack |
|
|
@@ -771,6 +773,89 @@ curl -N http://127.0.0.1:8150/events
|
|
|
771
773
|
Events: `initialized`, `stopped`, `continued`, `terminated`, `exited`, `output`.
|
|
772
774
|
Each is JSON with `event`, `data`, and `timestamp` fields.
|
|
773
775
|
|
|
776
|
+
## MCP Integration
|
|
777
|
+
|
|
778
|
+
tdb ships a Model Context Protocol (MCP) server (`tdb-mcp`) that exposes
|
|
779
|
+
the debugger as a curated set of tools an AI agent can call. The MCP
|
|
780
|
+
server is a third in-process consumer of the same dispatch handlers the
|
|
781
|
+
TUI and the HTTP server use — so an agent gets the same lock semantics,
|
|
782
|
+
including the pause-during-continue bypass, and the same DAP-backed
|
|
783
|
+
inspection surface.
|
|
784
|
+
|
|
785
|
+
For a worked end-to-end example (prompting an agent to find two
|
|
786
|
+
runtime bugs in a sample program) see
|
|
787
|
+
[docs/tutorial-mcp-debugging.md](docs/tutorial-mcp-debugging.md) and
|
|
788
|
+
its companion `examples/sales_report_buggy.py`.
|
|
789
|
+
|
|
790
|
+
### Running the MCP server
|
|
791
|
+
|
|
792
|
+
Configure your MCP client (Claude Desktop, an IDE extension, etc.) to
|
|
793
|
+
launch `tdb-mcp` over stdio. Example `claude_desktop_config.json`:
|
|
794
|
+
|
|
795
|
+
```json
|
|
796
|
+
{
|
|
797
|
+
"mcpServers": {
|
|
798
|
+
"tdb": {
|
|
799
|
+
"command": "tdb-mcp"
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Three equivalent invocation forms: `tdb-mcp` (the dedicated entry
|
|
806
|
+
point), `tdb --mcp` (the main CLI with the `--mcp` switch), and
|
|
807
|
+
`python -m tdb.mcp` (module form). Pick whichever matches how your MCP
|
|
808
|
+
client expects to launch servers.
|
|
809
|
+
|
|
810
|
+
### Tool surface (16 tools, curated)
|
|
811
|
+
|
|
812
|
+
| Cluster | Tools |
|
|
813
|
+
|---------|-------|
|
|
814
|
+
| Lifecycle | `debug_launch`, `debug_attach`, `quit` |
|
|
815
|
+
| Control | `control(action, timeout_s=30)` — `action ∈ {continue, next, step_in, step_out, pause, wait_for_stop}` |
|
|
816
|
+
| Inspection | `inspect(expressions)`, `read_source(file_path)`, `stack_trace()`, `status()`, `get_output()` |
|
|
817
|
+
| Breakpoints | `set_breakpoint(spec, condition?, hit_condition?)`, `remove_breakpoint(spec)`, `list_breakpoints()` |
|
|
818
|
+
| Differentiators | `threads(thread_id?)`, `tasks(task_name?)`, `processes(name_or_pid?)`, `wait_graph()` |
|
|
819
|
+
|
|
820
|
+
`control` is intentionally one tool that takes an action enum — the six
|
|
821
|
+
underlying RPC actions share a return shape, and agents perform
|
|
822
|
+
measurably better with a small surface than with one tool per action.
|
|
823
|
+
`threads` / `tasks` / `processes` overload list-vs-inspect via a
|
|
824
|
+
single optional argument for the same reason.
|
|
825
|
+
|
|
826
|
+
### Agent flow for a long-running step
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
agent → control(action="continue", timeout_s=30)
|
|
830
|
+
mcp → "still running — call pause or wait again"
|
|
831
|
+
agent → control(action="pause") # OR: control(action="wait_for_stop", timeout_s=30)
|
|
832
|
+
mcp → "<file>:<line>"
|
|
833
|
+
agent → inspect(["x", "len(buf)"])
|
|
834
|
+
mcp → "x = 7\nlen(buf) = 1024"
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
`pause` bypasses the dispatch lock so it can interrupt a `continue`
|
|
838
|
+
that's still mid-flight (HTTP and MCP share the same `NO_LOCK_ACTIONS`
|
|
839
|
+
policy — see `tdb/server/app.py`).
|
|
840
|
+
|
|
841
|
+
### Security caveat
|
|
842
|
+
|
|
843
|
+
`inspect` calls debugpy's `evaluate`, which is **arbitrary Python
|
|
844
|
+
execution in the debuggee process**. This is inherent to a debugger and
|
|
845
|
+
not a tdb-specific concern, but MCP clients (and the humans running
|
|
846
|
+
them) should apply appropriate permission models: don't auto-approve
|
|
847
|
+
`inspect` against untrusted expressions, and don't expose `tdb-mcp` on
|
|
848
|
+
a network — stdio transport only by design.
|
|
849
|
+
|
|
850
|
+
### Deferred / out of scope (v1)
|
|
851
|
+
|
|
852
|
+
- SSE-style event push — `control` and `wait_for_stop` make polling
|
|
853
|
+
efficient enough; events would also need uneven MCP-client support.
|
|
854
|
+
- HTTP / streamable-HTTP transports — would require auth (which the
|
|
855
|
+
HTTP RPC server also currently lacks); stdio inherits the trust of
|
|
856
|
+
the process that spawned it.
|
|
857
|
+
- Multi-session — one debug session per MCP process.
|
|
858
|
+
|
|
774
859
|
## CLI Reference
|
|
775
860
|
|
|
776
861
|
```
|
|
@@ -673,11 +673,12 @@ curl -s -X POST http://127.0.0.1:8150/rpc \
|
|
|
673
673
|
| `set_breakpoint` | `["file:line"]` or `["file:line", "condition", "hit_condition"]` | Set a breakpoint |
|
|
674
674
|
| `remove_breakpoint` | `["file:line"]` | Remove a breakpoint |
|
|
675
675
|
| `list_breakpoints` | `[]` | Show all breakpoints |
|
|
676
|
-
| `continue` | `[]` | Resume execution |
|
|
677
|
-
| `next` | `[]` | Step over |
|
|
678
|
-
| `step_in` | `[]` | Step into |
|
|
679
|
-
| `step_out` | `[]` | Step out |
|
|
680
|
-
| `pause` | `[]` | Pause execution |
|
|
676
|
+
| `continue` | `[]` or `[timeout_s]` | Resume execution; on timeout returns `"still running — call pause or wait again"` (success) |
|
|
677
|
+
| `next` | `[]` or `[timeout_s]` | Step over |
|
|
678
|
+
| `step_in` | `[]` or `[timeout_s]` | Step into |
|
|
679
|
+
| `step_out` | `[]` or `[timeout_s]` | Step out |
|
|
680
|
+
| `pause` | `[]` | Pause execution; bypasses the dispatch lock so it can interrupt an in-flight blocking action |
|
|
681
|
+
| `wait_for_stop` | `[]` or `[timeout_s]` | Wait for the next stop without issuing a step (use after `continue` returns `"still running"` to keep waiting) |
|
|
681
682
|
| `inspect` | `["expr1", "expr2", ...]` | Evaluate multiple expressions |
|
|
682
683
|
| `evaluate` | `["expression"]` | Evaluate a single expression |
|
|
683
684
|
| `stack_up` | `[]` | Move up the call stack |
|
|
@@ -706,6 +707,89 @@ curl -N http://127.0.0.1:8150/events
|
|
|
706
707
|
Events: `initialized`, `stopped`, `continued`, `terminated`, `exited`, `output`.
|
|
707
708
|
Each is JSON with `event`, `data`, and `timestamp` fields.
|
|
708
709
|
|
|
710
|
+
## MCP Integration
|
|
711
|
+
|
|
712
|
+
tdb ships a Model Context Protocol (MCP) server (`tdb-mcp`) that exposes
|
|
713
|
+
the debugger as a curated set of tools an AI agent can call. The MCP
|
|
714
|
+
server is a third in-process consumer of the same dispatch handlers the
|
|
715
|
+
TUI and the HTTP server use — so an agent gets the same lock semantics,
|
|
716
|
+
including the pause-during-continue bypass, and the same DAP-backed
|
|
717
|
+
inspection surface.
|
|
718
|
+
|
|
719
|
+
For a worked end-to-end example (prompting an agent to find two
|
|
720
|
+
runtime bugs in a sample program) see
|
|
721
|
+
[docs/tutorial-mcp-debugging.md](docs/tutorial-mcp-debugging.md) and
|
|
722
|
+
its companion `examples/sales_report_buggy.py`.
|
|
723
|
+
|
|
724
|
+
### Running the MCP server
|
|
725
|
+
|
|
726
|
+
Configure your MCP client (Claude Desktop, an IDE extension, etc.) to
|
|
727
|
+
launch `tdb-mcp` over stdio. Example `claude_desktop_config.json`:
|
|
728
|
+
|
|
729
|
+
```json
|
|
730
|
+
{
|
|
731
|
+
"mcpServers": {
|
|
732
|
+
"tdb": {
|
|
733
|
+
"command": "tdb-mcp"
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Three equivalent invocation forms: `tdb-mcp` (the dedicated entry
|
|
740
|
+
point), `tdb --mcp` (the main CLI with the `--mcp` switch), and
|
|
741
|
+
`python -m tdb.mcp` (module form). Pick whichever matches how your MCP
|
|
742
|
+
client expects to launch servers.
|
|
743
|
+
|
|
744
|
+
### Tool surface (16 tools, curated)
|
|
745
|
+
|
|
746
|
+
| Cluster | Tools |
|
|
747
|
+
|---------|-------|
|
|
748
|
+
| Lifecycle | `debug_launch`, `debug_attach`, `quit` |
|
|
749
|
+
| Control | `control(action, timeout_s=30)` — `action ∈ {continue, next, step_in, step_out, pause, wait_for_stop}` |
|
|
750
|
+
| Inspection | `inspect(expressions)`, `read_source(file_path)`, `stack_trace()`, `status()`, `get_output()` |
|
|
751
|
+
| Breakpoints | `set_breakpoint(spec, condition?, hit_condition?)`, `remove_breakpoint(spec)`, `list_breakpoints()` |
|
|
752
|
+
| Differentiators | `threads(thread_id?)`, `tasks(task_name?)`, `processes(name_or_pid?)`, `wait_graph()` |
|
|
753
|
+
|
|
754
|
+
`control` is intentionally one tool that takes an action enum — the six
|
|
755
|
+
underlying RPC actions share a return shape, and agents perform
|
|
756
|
+
measurably better with a small surface than with one tool per action.
|
|
757
|
+
`threads` / `tasks` / `processes` overload list-vs-inspect via a
|
|
758
|
+
single optional argument for the same reason.
|
|
759
|
+
|
|
760
|
+
### Agent flow for a long-running step
|
|
761
|
+
|
|
762
|
+
```
|
|
763
|
+
agent → control(action="continue", timeout_s=30)
|
|
764
|
+
mcp → "still running — call pause or wait again"
|
|
765
|
+
agent → control(action="pause") # OR: control(action="wait_for_stop", timeout_s=30)
|
|
766
|
+
mcp → "<file>:<line>"
|
|
767
|
+
agent → inspect(["x", "len(buf)"])
|
|
768
|
+
mcp → "x = 7\nlen(buf) = 1024"
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
`pause` bypasses the dispatch lock so it can interrupt a `continue`
|
|
772
|
+
that's still mid-flight (HTTP and MCP share the same `NO_LOCK_ACTIONS`
|
|
773
|
+
policy — see `tdb/server/app.py`).
|
|
774
|
+
|
|
775
|
+
### Security caveat
|
|
776
|
+
|
|
777
|
+
`inspect` calls debugpy's `evaluate`, which is **arbitrary Python
|
|
778
|
+
execution in the debuggee process**. This is inherent to a debugger and
|
|
779
|
+
not a tdb-specific concern, but MCP clients (and the humans running
|
|
780
|
+
them) should apply appropriate permission models: don't auto-approve
|
|
781
|
+
`inspect` against untrusted expressions, and don't expose `tdb-mcp` on
|
|
782
|
+
a network — stdio transport only by design.
|
|
783
|
+
|
|
784
|
+
### Deferred / out of scope (v1)
|
|
785
|
+
|
|
786
|
+
- SSE-style event push — `control` and `wait_for_stop` make polling
|
|
787
|
+
efficient enough; events would also need uneven MCP-client support.
|
|
788
|
+
- HTTP / streamable-HTTP transports — would require auth (which the
|
|
789
|
+
HTTP RPC server also currently lacks); stdio inherits the trust of
|
|
790
|
+
the process that spawned it.
|
|
791
|
+
- Multi-session — one debug session per MCP process.
|
|
792
|
+
|
|
709
793
|
## CLI Reference
|
|
710
794
|
|
|
711
795
|
```
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"pygments>=2.17.0",
|
|
26
26
|
"fastapi>=0.115.0",
|
|
27
27
|
"uvicorn>=0.30.0",
|
|
28
|
-
|
|
28
|
+
"mcp>=1.28.0",
|
|
29
|
+
]
|
|
29
30
|
|
|
30
31
|
# extras needed to run the examples and tests
|
|
31
32
|
[project.optional-dependencies]
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
|
|
44
45
|
[project.scripts]
|
|
45
46
|
tdb = "tdb.cli:main"
|
|
47
|
+
tdb-mcp = "tdb.mcp.server:main"
|
|
46
48
|
|
|
47
49
|
[build-system]
|
|
48
50
|
requires = ["setuptools>=68.0"]
|
|
@@ -673,11 +673,12 @@ curl -s -X POST http://127.0.0.1:8150/rpc \
|
|
|
673
673
|
| `set_breakpoint` | `["file:line"]` or `["file:line", "condition", "hit_condition"]` | Set a breakpoint |
|
|
674
674
|
| `remove_breakpoint` | `["file:line"]` | Remove a breakpoint |
|
|
675
675
|
| `list_breakpoints` | `[]` | Show all breakpoints |
|
|
676
|
-
| `continue` | `[]` | Resume execution |
|
|
677
|
-
| `next` | `[]` | Step over |
|
|
678
|
-
| `step_in` | `[]` | Step into |
|
|
679
|
-
| `step_out` | `[]` | Step out |
|
|
680
|
-
| `pause` | `[]` | Pause execution |
|
|
676
|
+
| `continue` | `[]` or `[timeout_s]` | Resume execution; on timeout returns `"still running — call pause or wait again"` (success) |
|
|
677
|
+
| `next` | `[]` or `[timeout_s]` | Step over |
|
|
678
|
+
| `step_in` | `[]` or `[timeout_s]` | Step into |
|
|
679
|
+
| `step_out` | `[]` or `[timeout_s]` | Step out |
|
|
680
|
+
| `pause` | `[]` | Pause execution; bypasses the dispatch lock so it can interrupt an in-flight blocking action |
|
|
681
|
+
| `wait_for_stop` | `[]` or `[timeout_s]` | Wait for the next stop without issuing a step (use after `continue` returns `"still running"` to keep waiting) |
|
|
681
682
|
| `inspect` | `["expr1", "expr2", ...]` | Evaluate multiple expressions |
|
|
682
683
|
| `evaluate` | `["expression"]` | Evaluate a single expression |
|
|
683
684
|
| `stack_up` | `[]` | Move up the call stack |
|
|
@@ -706,6 +707,89 @@ curl -N http://127.0.0.1:8150/events
|
|
|
706
707
|
Events: `initialized`, `stopped`, `continued`, `terminated`, `exited`, `output`.
|
|
707
708
|
Each is JSON with `event`, `data`, and `timestamp` fields.
|
|
708
709
|
|
|
710
|
+
## MCP Integration
|
|
711
|
+
|
|
712
|
+
tdb ships a Model Context Protocol (MCP) server (`tdb-mcp`) that exposes
|
|
713
|
+
the debugger as a curated set of tools an AI agent can call. The MCP
|
|
714
|
+
server is a third in-process consumer of the same dispatch handlers the
|
|
715
|
+
TUI and the HTTP server use — so an agent gets the same lock semantics,
|
|
716
|
+
including the pause-during-continue bypass, and the same DAP-backed
|
|
717
|
+
inspection surface.
|
|
718
|
+
|
|
719
|
+
For a worked end-to-end example (prompting an agent to find two
|
|
720
|
+
runtime bugs in a sample program) see
|
|
721
|
+
[docs/tutorial-mcp-debugging.md](docs/tutorial-mcp-debugging.md) and
|
|
722
|
+
its companion `examples/sales_report_buggy.py`.
|
|
723
|
+
|
|
724
|
+
### Running the MCP server
|
|
725
|
+
|
|
726
|
+
Configure your MCP client (Claude Desktop, an IDE extension, etc.) to
|
|
727
|
+
launch `tdb-mcp` over stdio. Example `claude_desktop_config.json`:
|
|
728
|
+
|
|
729
|
+
```json
|
|
730
|
+
{
|
|
731
|
+
"mcpServers": {
|
|
732
|
+
"tdb": {
|
|
733
|
+
"command": "tdb-mcp"
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Three equivalent invocation forms: `tdb-mcp` (the dedicated entry
|
|
740
|
+
point), `tdb --mcp` (the main CLI with the `--mcp` switch), and
|
|
741
|
+
`python -m tdb.mcp` (module form). Pick whichever matches how your MCP
|
|
742
|
+
client expects to launch servers.
|
|
743
|
+
|
|
744
|
+
### Tool surface (16 tools, curated)
|
|
745
|
+
|
|
746
|
+
| Cluster | Tools |
|
|
747
|
+
|---------|-------|
|
|
748
|
+
| Lifecycle | `debug_launch`, `debug_attach`, `quit` |
|
|
749
|
+
| Control | `control(action, timeout_s=30)` — `action ∈ {continue, next, step_in, step_out, pause, wait_for_stop}` |
|
|
750
|
+
| Inspection | `inspect(expressions)`, `read_source(file_path)`, `stack_trace()`, `status()`, `get_output()` |
|
|
751
|
+
| Breakpoints | `set_breakpoint(spec, condition?, hit_condition?)`, `remove_breakpoint(spec)`, `list_breakpoints()` |
|
|
752
|
+
| Differentiators | `threads(thread_id?)`, `tasks(task_name?)`, `processes(name_or_pid?)`, `wait_graph()` |
|
|
753
|
+
|
|
754
|
+
`control` is intentionally one tool that takes an action enum — the six
|
|
755
|
+
underlying RPC actions share a return shape, and agents perform
|
|
756
|
+
measurably better with a small surface than with one tool per action.
|
|
757
|
+
`threads` / `tasks` / `processes` overload list-vs-inspect via a
|
|
758
|
+
single optional argument for the same reason.
|
|
759
|
+
|
|
760
|
+
### Agent flow for a long-running step
|
|
761
|
+
|
|
762
|
+
```
|
|
763
|
+
agent → control(action="continue", timeout_s=30)
|
|
764
|
+
mcp → "still running — call pause or wait again"
|
|
765
|
+
agent → control(action="pause") # OR: control(action="wait_for_stop", timeout_s=30)
|
|
766
|
+
mcp → "<file>:<line>"
|
|
767
|
+
agent → inspect(["x", "len(buf)"])
|
|
768
|
+
mcp → "x = 7\nlen(buf) = 1024"
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
`pause` bypasses the dispatch lock so it can interrupt a `continue`
|
|
772
|
+
that's still mid-flight (HTTP and MCP share the same `NO_LOCK_ACTIONS`
|
|
773
|
+
policy — see `tdb/server/app.py`).
|
|
774
|
+
|
|
775
|
+
### Security caveat
|
|
776
|
+
|
|
777
|
+
`inspect` calls debugpy's `evaluate`, which is **arbitrary Python
|
|
778
|
+
execution in the debuggee process**. This is inherent to a debugger and
|
|
779
|
+
not a tdb-specific concern, but MCP clients (and the humans running
|
|
780
|
+
them) should apply appropriate permission models: don't auto-approve
|
|
781
|
+
`inspect` against untrusted expressions, and don't expose `tdb-mcp` on
|
|
782
|
+
a network — stdio transport only by design.
|
|
783
|
+
|
|
784
|
+
### Deferred / out of scope (v1)
|
|
785
|
+
|
|
786
|
+
- SSE-style event push — `control` and `wait_for_stop` make polling
|
|
787
|
+
efficient enough; events would also need uneven MCP-client support.
|
|
788
|
+
- HTTP / streamable-HTTP transports — would require auth (which the
|
|
789
|
+
HTTP RPC server also currently lacks); stdio inherits the trust of
|
|
790
|
+
the process that spawned it.
|
|
791
|
+
- Multi-session — one debug session per MCP process.
|
|
792
|
+
|
|
709
793
|
## CLI Reference
|
|
710
794
|
|
|
711
795
|
```
|
|
@@ -20,18 +20,11 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
|
-
from pathlib import Path
|
|
24
23
|
from typing import TYPE_CHECKING
|
|
25
24
|
|
|
26
25
|
from tdb.dap.types import Source, StackFrame
|
|
27
|
-
from tdb.inspection import
|
|
28
|
-
|
|
29
|
-
ProcessInfo,
|
|
30
|
-
TASK_COLLECT_EXPR,
|
|
31
|
-
TASK_LOCALS_EXPR,
|
|
32
|
-
parse_process_json,
|
|
33
|
-
parse_task_json,
|
|
34
|
-
)
|
|
26
|
+
from tdb.inspection import ProcessInfo
|
|
27
|
+
from tdb.session.inspect_service import InspectService, SessionGateError
|
|
35
28
|
from tdb.widgets.async_tasks_modal import AsyncTasksModal
|
|
36
29
|
from tdb.widgets.menu_bar import MenuBar
|
|
37
30
|
from tdb.widgets.processes_modal import ProcessesModal
|
|
@@ -48,10 +41,17 @@ log = logging.getLogger(__name__)
|
|
|
48
41
|
|
|
49
42
|
|
|
50
43
|
class InspectionWorkflows:
|
|
51
|
-
"""Drives the threads/processes/async-tasks UI flows.
|
|
44
|
+
"""Drives the threads/processes/async-tasks UI flows.
|
|
45
|
+
|
|
46
|
+
Fetch-and-parse mechanics live in `tdb.session.inspect_service.
|
|
47
|
+
InspectService` (shared with the JSON-RPC server); this collaborator
|
|
48
|
+
holds only the TUI presentation around them — gates rendered as
|
|
49
|
+
toasts, modal lifecycle, menu-bar labels.
|
|
50
|
+
"""
|
|
52
51
|
|
|
53
52
|
def __init__(self, app: TdbApp) -> None:
|
|
54
53
|
self.app = app
|
|
54
|
+
self._svc = InspectService(lambda: self.app.controller)
|
|
55
55
|
|
|
56
56
|
# --- Async tasks ----------------------------------------------------
|
|
57
57
|
|
|
@@ -82,19 +82,15 @@ class InspectionWorkflows:
|
|
|
82
82
|
if ctrl.state.is_running:
|
|
83
83
|
self.app.notify("Program is running — pause first", title="Async Tasks")
|
|
84
84
|
return
|
|
85
|
-
raw = ""
|
|
86
85
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
tasks = await self._svc.collect_tasks()
|
|
87
|
+
except SessionGateError:
|
|
88
|
+
return # phase changed between our gate and the fetch
|
|
89
89
|
except Exception:
|
|
90
90
|
log.exception("Error fetching async tasks")
|
|
91
91
|
tasks = []
|
|
92
92
|
|
|
93
93
|
if not tasks:
|
|
94
|
-
log.warning(
|
|
95
|
-
"Async task collection returned no tasks. Raw: %s",
|
|
96
|
-
raw[:300] if raw else "(empty)",
|
|
97
|
-
)
|
|
98
94
|
self.app.notify(
|
|
99
95
|
"No asyncio tasks found (program may not use asyncio)",
|
|
100
96
|
title="Async Tasks",
|
|
@@ -110,12 +106,10 @@ class InspectionWorkflows:
|
|
|
110
106
|
self.app.panels.async_tasks = None
|
|
111
107
|
|
|
112
108
|
async def refresh_async_tasks(self) -> None:
|
|
113
|
-
ctrl = self.app.controller
|
|
114
|
-
if ctrl.state.is_terminated or ctrl.state.is_running:
|
|
115
|
-
return
|
|
116
109
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
tasks = await self._svc.collect_tasks()
|
|
111
|
+
except SessionGateError:
|
|
112
|
+
return
|
|
119
113
|
except Exception:
|
|
120
114
|
log.exception("Error refreshing async tasks")
|
|
121
115
|
return
|
|
@@ -124,29 +118,15 @@ class InspectionWorkflows:
|
|
|
124
118
|
|
|
125
119
|
async def load_task_variables(self, task_name: str) -> None:
|
|
126
120
|
"""Fetch a task's local variables via DAP and populate the tree."""
|
|
127
|
-
ctrl = self.app.controller
|
|
128
|
-
if ctrl.state.is_terminated or ctrl.state.is_running:
|
|
129
|
-
return
|
|
130
121
|
modal = self.app.panels.async_tasks
|
|
131
122
|
if modal is None:
|
|
132
123
|
return
|
|
133
|
-
variables: list = []
|
|
134
124
|
try:
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
frame_id = await ctrl.resolve_evaluate_frame_id(ctrl.active_client)
|
|
141
|
-
_result, var_ref = await ctrl.active_client.evaluate(
|
|
142
|
-
expr,
|
|
143
|
-
frame_id=frame_id,
|
|
144
|
-
context="repl",
|
|
145
|
-
)
|
|
146
|
-
if var_ref > 0:
|
|
147
|
-
variables = await ctrl.active_client.variables(var_ref)
|
|
148
|
-
except Exception:
|
|
149
|
-
log.debug("Failed to load variables for task %s", task_name)
|
|
125
|
+
# Routes around synthetic frame ids and evaluates against the
|
|
126
|
+
# active client — see InspectService.task_locals.
|
|
127
|
+
variables = await self._svc.task_locals(task_name)
|
|
128
|
+
except SessionGateError:
|
|
129
|
+
return
|
|
150
130
|
modal.show_task_variables(variables)
|
|
151
131
|
|
|
152
132
|
def navigate_to_task(self, task_name: str) -> bool:
|
|
@@ -213,7 +193,9 @@ class InspectionWorkflows:
|
|
|
213
193
|
self.app.notify("Program is running — pause first", title="Threads")
|
|
214
194
|
return
|
|
215
195
|
try:
|
|
216
|
-
threads = await
|
|
196
|
+
threads = await self._svc.list_threads()
|
|
197
|
+
except SessionGateError:
|
|
198
|
+
return
|
|
217
199
|
except Exception:
|
|
218
200
|
log.exception("Error fetching threads")
|
|
219
201
|
self.app.notify("Failed to fetch threads", title="Threads")
|
|
@@ -230,29 +212,16 @@ class InspectionWorkflows:
|
|
|
230
212
|
|
|
231
213
|
async def load_thread_detail(self, thread_id: int) -> None:
|
|
232
214
|
"""Fetch stack trace and variables for a thread."""
|
|
233
|
-
ctrl = self.app.controller
|
|
234
|
-
if ctrl.state.is_terminated or ctrl.state.is_running:
|
|
235
|
-
return
|
|
236
215
|
modal = self.app.panels.threads
|
|
237
216
|
if modal is None:
|
|
238
217
|
return
|
|
239
218
|
try:
|
|
240
|
-
frames = await
|
|
219
|
+
frames, scopes, variables = await self._svc.thread_stack(thread_id)
|
|
220
|
+
except SessionGateError:
|
|
221
|
+
return
|
|
241
222
|
except Exception:
|
|
242
223
|
log.debug("Failed to fetch stack trace for thread %d", thread_id)
|
|
243
|
-
frames = []
|
|
244
|
-
scopes: list = []
|
|
245
|
-
variables: dict = {}
|
|
246
|
-
if frames:
|
|
247
|
-
top_frame = frames[0]
|
|
248
|
-
try:
|
|
249
|
-
scopes = await ctrl.client.scopes(top_frame.id)
|
|
250
|
-
for scope in scopes:
|
|
251
|
-
variables[scope.variables_reference] = await ctrl.client.variables(
|
|
252
|
-
scope.variables_reference
|
|
253
|
-
)
|
|
254
|
-
except Exception:
|
|
255
|
-
log.debug("Failed to fetch variables for thread %d", thread_id)
|
|
224
|
+
frames, scopes, variables = [], [], {}
|
|
256
225
|
modal.show_thread_detail(thread_id, frames, scopes, variables)
|
|
257
226
|
|
|
258
227
|
# --- Full-Contents (variable subtree pre-fetch) --------------------
|
|
@@ -315,55 +284,17 @@ class InspectionWorkflows:
|
|
|
315
284
|
|
|
316
285
|
# --- Processes ------------------------------------------------------
|
|
317
286
|
|
|
318
|
-
def
|
|
319
|
-
"""
|
|
287
|
+
async def get_processes(self) -> list[ProcessInfo]:
|
|
288
|
+
"""Get child process info, trying eval on parent first, then /proc.
|
|
320
289
|
|
|
321
|
-
|
|
322
|
-
|
|
290
|
+
Maps a gate failure (phase changed mid-flight) to an empty list:
|
|
291
|
+
callers treat "nothing to show" and "can't look right now" the
|
|
292
|
+
same way.
|
|
323
293
|
"""
|
|
324
|
-
result = []
|
|
325
|
-
for pid in self.app.controller.get_child_pids():
|
|
326
|
-
try:
|
|
327
|
-
cmdline_path = Path(f"/proc/{pid}/cmdline")
|
|
328
|
-
if not cmdline_path.exists():
|
|
329
|
-
continue
|
|
330
|
-
cmdline = (
|
|
331
|
-
cmdline_path.read_bytes().replace(b"\x00", b" ").decode().strip()
|
|
332
|
-
)
|
|
333
|
-
status_path = Path(f"/proc/{pid}/status")
|
|
334
|
-
name = f"Process-{pid}"
|
|
335
|
-
if status_path.exists():
|
|
336
|
-
for line in status_path.read_text().splitlines():
|
|
337
|
-
if line.startswith("Name:"):
|
|
338
|
-
name = line.split(":", 1)[1].strip()
|
|
339
|
-
break
|
|
340
|
-
result.append(
|
|
341
|
-
ProcessInfo(
|
|
342
|
-
name=name,
|
|
343
|
-
pid=pid,
|
|
344
|
-
alive=True,
|
|
345
|
-
exitcode=None,
|
|
346
|
-
daemon=False,
|
|
347
|
-
target=cmdline[:200] if cmdline else "unknown",
|
|
348
|
-
args="()",
|
|
349
|
-
kwargs="{}",
|
|
350
|
-
start_method="",
|
|
351
|
-
)
|
|
352
|
-
)
|
|
353
|
-
except Exception:
|
|
354
|
-
pass
|
|
355
|
-
return result
|
|
356
|
-
|
|
357
|
-
async def get_processes(self) -> list[ProcessInfo]:
|
|
358
|
-
"""Get child process info, trying eval on parent first, then /proc."""
|
|
359
294
|
try:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return processes
|
|
364
|
-
except Exception:
|
|
365
|
-
pass
|
|
366
|
-
return self.get_processes_from_pids()
|
|
295
|
+
return await self._svc.collect_processes()
|
|
296
|
+
except SessionGateError:
|
|
297
|
+
return []
|
|
367
298
|
|
|
368
299
|
async def fetch_process_count(self) -> None:
|
|
369
300
|
"""Update the Processes label with child process count."""
|
|
@@ -470,25 +401,10 @@ class InspectionWorkflows:
|
|
|
470
401
|
modal = self.app.panels.processes
|
|
471
402
|
if modal is None:
|
|
472
403
|
return
|
|
473
|
-
|
|
474
|
-
if
|
|
404
|
+
detail = await self._svc.process_stack(pid)
|
|
405
|
+
if detail is None:
|
|
475
406
|
# PIDs from /proc might not exactly match the controller's keys
|
|
476
407
|
modal.show_process_detail(pid, [], [], {})
|
|
477
408
|
return
|
|
478
|
-
frames
|
|
479
|
-
scopes: list = []
|
|
480
|
-
variables: dict = {}
|
|
481
|
-
try:
|
|
482
|
-
threads = await child.threads()
|
|
483
|
-
if threads:
|
|
484
|
-
frames = await child.stack_trace(threads[0].id)
|
|
485
|
-
if frames:
|
|
486
|
-
top_frame = frames[0]
|
|
487
|
-
scopes = await child.scopes(top_frame.id)
|
|
488
|
-
for scope in scopes:
|
|
489
|
-
variables[scope.variables_reference] = await child.variables(
|
|
490
|
-
scope.variables_reference
|
|
491
|
-
)
|
|
492
|
-
except Exception:
|
|
493
|
-
log.debug("Failed to fetch detail for child process %d", pid)
|
|
409
|
+
frames, scopes, variables = detail
|
|
494
410
|
modal.show_process_detail(pid, frames, scopes, variables)
|
|
@@ -9,14 +9,9 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _get_version() -> str:
|
|
12
|
-
|
|
13
|
-
from importlib.metadata import version
|
|
14
|
-
|
|
15
|
-
return version("textual-debugger")
|
|
16
|
-
except Exception:
|
|
17
|
-
from tdb import __version__
|
|
12
|
+
from tdb import __version__
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
return __version__
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -141,6 +136,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
141
136
|
action="store_true",
|
|
142
137
|
help="Run as a headless JSON-RPC debug server (no TUI)",
|
|
143
138
|
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--mcp",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Run as a Model Context Protocol (MCP) server over stdio. "
|
|
143
|
+
"Equivalent to `python -m tdb.mcp`. The session is owned by the "
|
|
144
|
+
"MCP client (via debug_launch / debug_attach tools); no program "
|
|
145
|
+
"argument is needed on the CLI.",
|
|
146
|
+
)
|
|
144
147
|
parser.add_argument(
|
|
145
148
|
"-k",
|
|
146
149
|
"--breakpoint",
|
|
@@ -417,7 +420,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
417
420
|
args = parser.parse_args(argv)
|
|
418
421
|
_apply_flag_implications(args)
|
|
419
422
|
|
|
420
|
-
if args.doc or args.doc_text or args.post_mortem:
|
|
423
|
+
if args.doc or args.doc_text or args.post_mortem or args.mcp:
|
|
421
424
|
return args
|
|
422
425
|
|
|
423
426
|
_validate_terminal_choice(args, parser)
|
|
@@ -461,12 +464,21 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
461
464
|
_run_doc_text()
|
|
462
465
|
elif args.post_mortem:
|
|
463
466
|
_run_post_mortem(args)
|
|
467
|
+
elif args.mcp:
|
|
468
|
+
_run_mcp()
|
|
464
469
|
elif args.headless:
|
|
465
470
|
_run_headless(args)
|
|
466
471
|
else:
|
|
467
472
|
_run_tui(args)
|
|
468
473
|
|
|
469
474
|
|
|
475
|
+
def _run_mcp() -> None:
|
|
476
|
+
"""Launch the MCP server over stdio. Mirrors `python -m tdb.mcp`."""
|
|
477
|
+
from tdb.mcp.server import main as mcp_main
|
|
478
|
+
|
|
479
|
+
mcp_main()
|
|
480
|
+
|
|
481
|
+
|
|
470
482
|
def _run_doc() -> None:
|
|
471
483
|
"""Display the bundled README.md in a Textual MarkdownViewer."""
|
|
472
484
|
from tdb.app_helpers import find_readme
|