Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
# Automatically activate the virtual environment when entering this directory
# Requires direnv: https://direnv.net/

# Check if .venv exists
if [ -d ".venv" ]; then
source .venv/bin/activate
echo "βœ“ Activated virtual environment (.venv)"
else
echo "⚠ Virtual environment not found. Run: python -m venv .venv"
fi
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ repos:
rev: v1.4.1
hooks:
- id: mypy
args: ["--ignore-missing-imports"]
args: ["--ignore-missing-imports", "--explicit-package-bases"]
exclude: ^examples/
additional_dependencies: [types-aiofiles>=25.0.0]
69 changes: 67 additions & 2 deletions agent_instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

- `run_tests_json(pytest_args?: string[]) -> PytestJson`: runs pytest and returns the JSON report body.
- `run_tests_focus(keyword: string) -> PytestJson`: focused run, equivalent to `pytest -k <keyword>`.
- `dap_launch(program: string, cwd?: string, breakpoints?: number[], console?: string, wait_for_breakpoint?: boolean, breakpoint_timeout?: number)` -> orchestrated launch via `debugpy.adapter` (returns initialize/configuration/launch responses and optional stopped event).
- `dap_launch(program: string, cwd?: string, breakpoints?: number[], breakpoints_by_source?: Dict[str, List[int]], stop_on_entry?: boolean, console?: string, wait_for_breakpoint?: boolean, breakpoint_timeout?: number)` -> orchestrated launch via `debugpy.adapter` (returns initialize/configuration/launch responses and optional stopped event). Use `breakpoints_by_source` to set breakpoints in imported modules or additional source files. **Use this for ALL debugging scenarios including web servers, long-running processes, and scripts.**
- `dap_set_breakpoints(source_path: string, lines: number[])` -> resilient setBreakpoints (waits for late `initialized` when needed).
- `dap_list_breakpoints()` -> returns the cached breakpoint map so you can confirm what has been registered.

Note: The server retries breakpoint registration after the debug adapter sends `initialized`. This retry applies both to main-program breakpoints and entries provided via `breakpoints_by_source`. A further retry pass occurs after the first `stopped` event to catch imported-module timing races.

- `dap_continue(thread_id?: number)` -> continues execution (auto-selects first thread if omitted).
- `dap_step_over(thread_id?: number)` / `dap_step_in(thread_id?: number)` / `dap_step_out(thread_id?: number)` -> step controls that reuse the last stopped thread by default.
- `dap_locals()` -> threads, stackTrace, scopes and variables for the current top frame.
Expand All @@ -23,7 +26,67 @@

**Key workflow reminder:** Start new debugging sessions with a single `dap_launch` call that includes your breakpoint list. The launch helper performs `initialize`, registers breakpoints, issues `configurationDone`, and starts the target. You do **not** need to call `dap_set_breakpoints` beforehand unless you are adjusting breakpoints mid-session.

### Example workflow (pseudo JSON-RPC calls)
**Breakpoints in imported modules:** Use the `breakpoints_by_source` parameter to set breakpoints in any source file, not just the main program. This is essential for debugging multi-file applications and imported modules. Relative paths are resolved from `PROJECT_ROOT` first, then from `cwd` if provided. The tool implements a three-phase retry strategy to ensure breakpoints are registered even if modules aren't loaded yet when the adapter starts.

## Debugging Web Applications (Flask, Django, FastAPI)

**Use `dap_launch` for web applications!** The debugger controls the application lifecycle and allows you to set breakpoints that trigger on HTTP requests.

### Steps

1. **Launch the web app under debugger control:**

```json
{
"name": "dap_launch",
"input": {
"program": "run_flask.py",
"cwd": ".",
"breakpoints_by_source": {
"examples/web_flask/inventory.py": [18]
},
"wait_for_breakpoint": false
}
}
```

2. **Trigger breakpoints via HTTP requests:**

```bash
curl http://127.0.0.1:5001/total
```

3. **Wait for stopped event:**

```json
{ "name": "dap_wait_for_event", "input": { "name": "stopped", "timeout": 10 } }
```

4. **Inspect variables and debug:**

```json
{ "name": "dap_locals" }
{ "name": "dap_step_over" }
{ "name": "dap_continue" }
```

5. **Clean up:**

```json
{ "name": "dap_shutdown" }
```

**Important:** Create a launcher script (like `run_flask.py`) that imports your web app as a module to avoid relative import issues. Example:

```python
from examples.web_flask import app
if __name__ == "__main__":
app.main()
```

**Why not dap_attach?** The `dap_attach` approach was investigated but does not work with debugpy. When you run `python -m debugpy --listen`, debugpy does not respond to DAP attach requests. Use `dap_launch` for all debugging scenarios.

## Example workflow for scripts

1. Configure your MCP client (VS Code, Claude Desktop, etc.) so it references `.venv/bin/python src/mcp_server.py`.
2. Launch the debugger session:
Expand Down Expand Up @@ -68,6 +131,8 @@

7. At any point, run tests with `{ "name": "run_tests_json" }` or `{ "name": "run_tests_focus", "input": { "keyword": "..." } }`.

---

If you need a fresh demo script, call `{ "name": "ensure_demo_program", "input": { "directory": "/tmp/debug_demo" } }` (or omit `directory` to use the default location) before launching the debugger. The response includes `launchInput` you can pass straight to `dap_launch`. Use `{ "name": "read_text_file", "input": { "path": "/tmp/debug_demo/demo_program.py" } }` if you want to review the script first.

*Note:* The stdio client prints incoming events as `[dap:event] ...` for debugging. Feel free to keep or remove these prints in `src/dap_stdio_client.py` depending on your logging needs.
180 changes: 180 additions & 0 deletions docs/DEBUGGING_WEB_APPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Debugging Web Applications with dap_launch

This document explains the **correct and only** way to debug web applications (Flask, Django, FastAPI, etc.) using this MCP debugging server.

## TL;DR

**Use `dap_launch` for ALL debugging, including web apps.** The `dap_attach` approach does not work with debugpy.

## How to Debug Web Apps

### 1. Create a Launcher Script

Create a script like `run_flask.py` that imports your web app as a module:

```python
from examples.web_flask import app

if __name__ == "__main__":
app.main()
```

This avoids relative import issues when the debugger launches your app.

### 2. Launch with dap_launch

Use the MCP `dap_launch` tool to start your web app under debugger control:

```json
{
"name": "dap_launch",
"input": {
"program": "run_flask.py",
"cwd": ".",
"breakpoints_by_source": {
"examples/web_flask/inventory.py": [18]
},
"wait_for_breakpoint": false
}
}
```

**Key points:**

- Use `breakpoints_by_source` to set breakpoints in your business logic files
- Set `wait_for_breakpoint=false` since you'll trigger breakpoints via HTTP requests
- The debugger starts and controls the Flask/Django/FastAPI process

### 3. Trigger Breakpoints via HTTP

Make HTTP requests to your app to trigger breakpoints:

```bash
curl http://127.0.0.1:5001/total
```

### 4. Wait for Stopped Event

```json
{
"name": "dap_wait_for_event",
"input": {
"name": "stopped",
"timeout": 10
}
}
```

### 5. Inspect and Debug

```json
{"name": "dap_locals"}
{"name": "dap_step_over"}
{"name": "dap_continue"}
```

### 6. Clean Up

```json
{"name": "dap_shutdown"}
```

## Why Not dap_attach?

The `dap_attach` approach was thoroughly investigated but **does not work with debugpy**.

### What We Found

When you run:

```bash
python -m debugpy --listen 5678 -m your.app
```

And then try to connect to it directly:

1. βœ… `initialize` request works and gets a response
2. ❌ `attach` request sent but **debugpy never responds**
3. ❌ Connection times out

### Why It Fails

- **debugpy is not a full DAP server** when started with `--listen`
- It's designed for IDE integration (VS Code), not pure DAP attach scenarios
- The adapter (`debugpy.adapter`) doesn't support `--connect-to` flag
- Direct TCP connection to debugpy doesn't follow standard DAP protocol for attach

### What We Tried

1. **DirectDAPClient**: Created a TCP client to connect directly to debugpy
- Result: debugpy doesn't respond to attach requests

2. **StdioDAPClient with --connect-to**: Modified to launch adapter with connection flag
- Result: `debugpy.adapter` doesn't support `--connect-to` flag

3. **Multiple debugpy command variations**: Tested different flags and modes
- Result: All failed with the same issue - no response to attach requests

### Conclusion

**debugpy fundamentally does not support the DAP attach workflow for already-running processes.** This is not a bug in our implementation - it's how debugpy works.

## The Solution: dap_launch for Everything

`dap_launch` works perfectly for all scenarios:

- βœ… **Regular scripts**: Debugger starts and stops with the script
- βœ… **Web servers**: Debugger starts Flask/Django/FastAPI and keeps it alive
- βœ… **Long-running processes**: Debugger controls the entire lifecycle
- βœ… **Breakpoints on HTTP requests**: Set breakpoints, trigger via curl/browser
- βœ… **Module imports**: Use `breakpoints_by_source` for any file

## Example: Complete Flask Debugging Session

```json
// 1. Launch Flask under debugger
{
"name": "dap_launch",
"input": {
"program": "run_flask.py",
"cwd": ".",
"breakpoints_by_source": {
"examples/web_flask/inventory.py": [18]
},
"wait_for_breakpoint": false
}
}

// 2. Trigger breakpoint (in bash)
// curl http://127.0.0.1:5001/total

// 3. Wait for stopped event
{
"name": "dap_wait_for_event",
"input": {
"name": "stopped",
"timeout": 10
}
}

// 4. Inspect variables
{
"name": "dap_locals"
}

// 5. Continue execution
{
"name": "dap_continue"
}

// 6. Shutdown
{
"name": "dap_shutdown"
}
```

## References

- [agent_instructions.md](agent_instructions.md) - Complete MCP tool documentation
- [docs/mcp_usage.md](docs/mcp_usage.md) - Detailed usage guide
- [examples/web_flask/README.md](examples/web_flask/README.md) - Flask example walkthrough
Loading
Loading