debug-mcp 0.1.2
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/README.ja.md +383 -0
- data/README.md +384 -0
- data/examples/01_simple_bug.rb +43 -0
- data/examples/02_data_pipeline.rb +93 -0
- data/examples/03_recursion.rb +96 -0
- data/examples/RAILS_SCENARIOS.md +350 -0
- data/examples/SCENARIOS.md +142 -0
- data/examples/rails_test_app/setup.sh +428 -0
- data/examples/rails_test_app/testapp/.dockerignore +10 -0
- data/examples/rails_test_app/testapp/.ruby-version +1 -0
- data/examples/rails_test_app/testapp/Dockerfile +23 -0
- data/examples/rails_test_app/testapp/Gemfile +17 -0
- data/examples/rails_test_app/testapp/README.md +65 -0
- data/examples/rails_test_app/testapp/Rakefile +6 -0
- data/examples/rails_test_app/testapp/app/assets/images/.keep +0 -0
- data/examples/rails_test_app/testapp/app/assets/stylesheets/application.css +1 -0
- data/examples/rails_test_app/testapp/app/controllers/application_controller.rb +4 -0
- data/examples/rails_test_app/testapp/app/controllers/concerns/.keep +0 -0
- data/examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb +38 -0
- data/examples/rails_test_app/testapp/app/controllers/health_controller.rb +11 -0
- data/examples/rails_test_app/testapp/app/controllers/orders_controller.rb +100 -0
- data/examples/rails_test_app/testapp/app/controllers/posts_controller.rb +82 -0
- data/examples/rails_test_app/testapp/app/controllers/sessions_controller.rb +25 -0
- data/examples/rails_test_app/testapp/app/controllers/users_controller.rb +44 -0
- data/examples/rails_test_app/testapp/app/helpers/application_helper.rb +2 -0
- data/examples/rails_test_app/testapp/app/models/application_record.rb +3 -0
- data/examples/rails_test_app/testapp/app/models/comment.rb +8 -0
- data/examples/rails_test_app/testapp/app/models/concerns/.keep +0 -0
- data/examples/rails_test_app/testapp/app/models/order.rb +56 -0
- data/examples/rails_test_app/testapp/app/models/order_item.rb +16 -0
- data/examples/rails_test_app/testapp/app/models/post.rb +29 -0
- data/examples/rails_test_app/testapp/app/models/user.rb +34 -0
- data/examples/rails_test_app/testapp/app/services/order_report_service.rb +40 -0
- data/examples/rails_test_app/testapp/app/views/layouts/application.html.erb +28 -0
- data/examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb +22 -0
- data/examples/rails_test_app/testapp/app/views/pwa/service-worker.js +26 -0
- data/examples/rails_test_app/testapp/bin/ci +6 -0
- data/examples/rails_test_app/testapp/bin/dev +2 -0
- data/examples/rails_test_app/testapp/bin/rails +4 -0
- data/examples/rails_test_app/testapp/bin/rake +4 -0
- data/examples/rails_test_app/testapp/bin/setup +35 -0
- data/examples/rails_test_app/testapp/config/application.rb +42 -0
- data/examples/rails_test_app/testapp/config/boot.rb +3 -0
- data/examples/rails_test_app/testapp/config/ci.rb +14 -0
- data/examples/rails_test_app/testapp/config/database.yml +32 -0
- data/examples/rails_test_app/testapp/config/environment.rb +5 -0
- data/examples/rails_test_app/testapp/config/environments/development.rb +54 -0
- data/examples/rails_test_app/testapp/config/environments/production.rb +67 -0
- data/examples/rails_test_app/testapp/config/environments/test.rb +42 -0
- data/examples/rails_test_app/testapp/config/initializers/content_security_policy.rb +29 -0
- data/examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/rails_test_app/testapp/config/initializers/inflections.rb +16 -0
- data/examples/rails_test_app/testapp/config/locales/en.yml +31 -0
- data/examples/rails_test_app/testapp/config/puma.rb +39 -0
- data/examples/rails_test_app/testapp/config/routes.rb +34 -0
- data/examples/rails_test_app/testapp/config.ru +6 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb +12 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb +13 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb +11 -0
- data/examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb +14 -0
- data/examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb +13 -0
- data/examples/rails_test_app/testapp/db/schema.rb +71 -0
- data/examples/rails_test_app/testapp/db/seeds.rb +85 -0
- data/examples/rails_test_app/testapp/docker-compose.yml +21 -0
- data/examples/rails_test_app/testapp/docker-entrypoint.sh +10 -0
- data/examples/rails_test_app/testapp/lib/tasks/.keep +0 -0
- data/examples/rails_test_app/testapp/log/.keep +0 -0
- data/examples/rails_test_app/testapp/public/400.html +135 -0
- data/examples/rails_test_app/testapp/public/404.html +135 -0
- data/examples/rails_test_app/testapp/public/406-unsupported-browser.html +135 -0
- data/examples/rails_test_app/testapp/public/422.html +135 -0
- data/examples/rails_test_app/testapp/public/500.html +135 -0
- data/examples/rails_test_app/testapp/public/icon.png +0 -0
- data/examples/rails_test_app/testapp/public/icon.svg +3 -0
- data/examples/rails_test_app/testapp/public/robots.txt +1 -0
- data/examples/rails_test_app/testapp/script/.keep +0 -0
- data/examples/rails_test_app/testapp/storage/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/pids/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/storage/.keep +0 -0
- data/examples/rails_test_app/testapp/vendor/.keep +0 -0
- data/exe/debug-mcp +39 -0
- data/exe/debug-rails +127 -0
- data/lib/debug_mcp/client_cleanup.rb +102 -0
- data/lib/debug_mcp/code_safety_analyzer.rb +124 -0
- data/lib/debug_mcp/debug_client.rb +1143 -0
- data/lib/debug_mcp/exit_message_builder.rb +112 -0
- data/lib/debug_mcp/pending_http_helper.rb +25 -0
- data/lib/debug_mcp/rails_helper.rb +155 -0
- data/lib/debug_mcp/server.rb +364 -0
- data/lib/debug_mcp/session_manager.rb +436 -0
- data/lib/debug_mcp/stop_event_annotator.rb +152 -0
- data/lib/debug_mcp/tcp_session_discovery.rb +226 -0
- data/lib/debug_mcp/tools/connect.rb +669 -0
- data/lib/debug_mcp/tools/continue_execution.rb +161 -0
- data/lib/debug_mcp/tools/disconnect.rb +169 -0
- data/lib/debug_mcp/tools/evaluate_code.rb +354 -0
- data/lib/debug_mcp/tools/finish.rb +84 -0
- data/lib/debug_mcp/tools/get_context.rb +217 -0
- data/lib/debug_mcp/tools/get_source.rb +193 -0
- data/lib/debug_mcp/tools/inspect_object.rb +107 -0
- data/lib/debug_mcp/tools/list_debug_sessions.rb +60 -0
- data/lib/debug_mcp/tools/list_files.rb +189 -0
- data/lib/debug_mcp/tools/list_paused_sessions.rb +108 -0
- data/lib/debug_mcp/tools/next.rb +70 -0
- data/lib/debug_mcp/tools/rails_info.rb +200 -0
- data/lib/debug_mcp/tools/rails_model.rb +362 -0
- data/lib/debug_mcp/tools/rails_routes.rb +186 -0
- data/lib/debug_mcp/tools/read_file.rb +214 -0
- data/lib/debug_mcp/tools/remove_breakpoint.rb +173 -0
- data/lib/debug_mcp/tools/run_debug_command.rb +55 -0
- data/lib/debug_mcp/tools/run_script.rb +293 -0
- data/lib/debug_mcp/tools/set_breakpoint.rb +206 -0
- data/lib/debug_mcp/tools/step.rb +67 -0
- data/lib/debug_mcp/tools/trigger_request.rb +515 -0
- data/lib/debug_mcp/version.rb +5 -0
- data/lib/debug_mcp.rb +40 -0
- metadata +251 -0
data/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# debug-mcp
|
|
2
|
+
|
|
3
|
+
[日本語版 (Japanese)](README.ja.md)
|
|
4
|
+
|
|
5
|
+
MCP (Model Context Protocol) server that connects LLM agents to Ruby's [debug gem](https://github.com/ruby/debug), giving them access to the runtime context of paused Ruby processes.
|
|
6
|
+
|
|
7
|
+
LLM agents can connect to a paused Ruby process, inspect variables, evaluate code, set breakpoints, and control execution — all through MCP tool calls.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Existing Ruby/Rails MCP servers only provide static analysis or application-level APIs. debug-mcp goes further: it connects to **running Ruby processes** via the debug gem and exposes their runtime state to LLM agents.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Agent → connect(host: "localhost", port: 12345)
|
|
15
|
+
Agent → get_context()
|
|
16
|
+
→ local variables, instance variables, call stack
|
|
17
|
+
Agent → evaluate_code(code: "user.valid?")
|
|
18
|
+
→ false
|
|
19
|
+
Agent → evaluate_code(code: "user.errors.full_messages")
|
|
20
|
+
→ ["Email can't be blank"]
|
|
21
|
+
Agent → continue_execution()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "debug-mcp"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install directly:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
gem install debug-mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Ruby >= 3.2.0.
|
|
37
|
+
|
|
38
|
+
> **Migrating from `girb-mcp`?** This gem was previously published as `girb-mcp`
|
|
39
|
+
> on RubyGems (last version: 0.1.1). It was renamed to `debug-mcp` starting from
|
|
40
|
+
> 0.1.2 to better reflect its purpose. Replace `gem "girb-mcp"` with
|
|
41
|
+
> `gem "debug-mcp"` and update your MCP client config (`girb-mcp` → `debug-mcp`,
|
|
42
|
+
> `girb-rails` → `debug-rails`). See [CHANGELOG.md](CHANGELOG.md) for details.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### 1. Start a Ruby process with the debugger
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Script
|
|
50
|
+
rdbg --open --port=12345 my_script.rb
|
|
51
|
+
|
|
52
|
+
# Or with environment variables
|
|
53
|
+
RUBY_DEBUG_OPEN=true RUBY_DEBUG_PORT=12345 ruby my_script.rb
|
|
54
|
+
|
|
55
|
+
# Or add `debugger` / `binding.break` in your code and run with rdbg
|
|
56
|
+
rdbg --open my_script.rb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Configure your MCP client
|
|
60
|
+
|
|
61
|
+
debug-mcp works with any MCP-compatible client. Add it to your client's MCP server configuration:
|
|
62
|
+
|
|
63
|
+
#### Claude Code
|
|
64
|
+
|
|
65
|
+
Add to `~/.claude/settings.json` (or project `.claude/settings.json`):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"debug-mcp": {
|
|
71
|
+
"command": "debug-mcp",
|
|
72
|
+
"args": []
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If using Bundler:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"debug-mcp": {
|
|
84
|
+
"command": "bundle",
|
|
85
|
+
"args": ["exec", "debug-mcp"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Gemini CLI
|
|
92
|
+
|
|
93
|
+
Add to `~/.gemini/settings.json`:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"debug-mcp": {
|
|
99
|
+
"command": "debug-mcp",
|
|
100
|
+
"args": []
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If using Bundler:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"debug-mcp": {
|
|
112
|
+
"command": "bundle",
|
|
113
|
+
"args": ["exec", "debug-mcp"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3. Start debugging
|
|
120
|
+
|
|
121
|
+
Ask your agent to connect and debug:
|
|
122
|
+
|
|
123
|
+
> "Connect to the debug session on port 12345 and show me the current state"
|
|
124
|
+
|
|
125
|
+
> "Set a breakpoint at app/models/user.rb line 42 and send a GET request to /users/1"
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Usage: debug-mcp [options]
|
|
131
|
+
-t, --transport TRANSPORT Transport type: stdio (default) or http
|
|
132
|
+
-p, --port PORT HTTP port (default: 6029, only for http transport)
|
|
133
|
+
--host HOST HTTP host (default: 127.0.0.1, only for http transport)
|
|
134
|
+
--session-timeout SECONDS Session timeout in seconds (default: 1800)
|
|
135
|
+
-v, --version Show version
|
|
136
|
+
-h, --help Show this help
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### STDIO transport (default)
|
|
140
|
+
|
|
141
|
+
Standard transport for MCP clients. No additional configuration needed.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
debug-mcp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### HTTP transport (Streamable HTTP)
|
|
148
|
+
|
|
149
|
+
For browser-based clients or other HTTP-compatible MCP clients.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
debug-mcp --transport http --port 8080
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The MCP endpoint will be available at `http://127.0.0.1:8080/mcp`.
|
|
156
|
+
|
|
157
|
+
### Session timeout
|
|
158
|
+
|
|
159
|
+
Debug sessions are automatically cleaned up after 30 minutes of inactivity. Adjust with:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
debug-mcp --session-timeout 3600 # 1 hour
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The session manager also detects and cleans up sessions whose target process has exited.
|
|
166
|
+
|
|
167
|
+
## Tools
|
|
168
|
+
|
|
169
|
+
### Discovery & Connection
|
|
170
|
+
|
|
171
|
+
| Tool | Description |
|
|
172
|
+
|------|-------------|
|
|
173
|
+
| `list_debug_sessions` | List available debug sessions (Unix sockets) |
|
|
174
|
+
| `connect` | Connect to a debug session via socket path or TCP |
|
|
175
|
+
| `list_paused_sessions` | List currently connected sessions |
|
|
176
|
+
|
|
177
|
+
### Investigation
|
|
178
|
+
|
|
179
|
+
| Tool | Description |
|
|
180
|
+
|------|-------------|
|
|
181
|
+
| `evaluate_code` | Execute Ruby code in the stopped binding |
|
|
182
|
+
| `inspect_object` | Get class, value, and instance variables of an object |
|
|
183
|
+
| `get_context` | Local variables, instance variables, call stack, breakpoints |
|
|
184
|
+
| `get_source` | Source code of a method or class |
|
|
185
|
+
| `read_file` | Read source files with optional line range |
|
|
186
|
+
| `list_files` | List files in a directory, with optional glob pattern |
|
|
187
|
+
|
|
188
|
+
### Execution Control
|
|
189
|
+
|
|
190
|
+
| Tool | Description |
|
|
191
|
+
|------|-------------|
|
|
192
|
+
| `set_breakpoint` | Set a breakpoint: line (file + line), method (`User#save`), or exception class |
|
|
193
|
+
| `remove_breakpoint` | Remove a breakpoint by file + line, method name, exception class, or number |
|
|
194
|
+
| `continue_execution` | Resume execution until next breakpoint or exit |
|
|
195
|
+
| `step` | Step into the next method call |
|
|
196
|
+
| `next` | Step over to the next line |
|
|
197
|
+
| `finish` | Run until the current method/block returns |
|
|
198
|
+
| `run_debug_command` | Execute any raw debugger command |
|
|
199
|
+
| `disconnect` | Disconnect from the session and terminate the process |
|
|
200
|
+
|
|
201
|
+
### Entry Points
|
|
202
|
+
|
|
203
|
+
| Tool | Description |
|
|
204
|
+
|------|-------------|
|
|
205
|
+
| `run_script` | Start a Ruby script under rdbg and connect to it |
|
|
206
|
+
| `trigger_request` | Send an HTTP request to a Rails app under debug |
|
|
207
|
+
|
|
208
|
+
### Rails Tools (auto-detected)
|
|
209
|
+
|
|
210
|
+
These tools are automatically registered when a Rails process is detected.
|
|
211
|
+
|
|
212
|
+
| Tool | Description |
|
|
213
|
+
|------|-------------|
|
|
214
|
+
| `rails_info` | Show app name, Rails/Ruby versions, environment, root path |
|
|
215
|
+
| `rails_routes` | Show routes (verb, path, controller#action), filterable by controller or path |
|
|
216
|
+
| `rails_model` | Show model structure: columns, associations, validations, enums, scopes |
|
|
217
|
+
|
|
218
|
+
## Workflows
|
|
219
|
+
|
|
220
|
+
### Debug a Ruby script
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Agent: run_script(file: "my_script.rb")
|
|
224
|
+
Agent: get_context()
|
|
225
|
+
Agent: evaluate_code(code: "result")
|
|
226
|
+
Agent: next()
|
|
227
|
+
Agent: evaluate_code(code: "result")
|
|
228
|
+
Agent: continue_execution()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Method breakpoints
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Agent: run_script(file: "my_script.rb", breakpoints: ["DataPipeline#validate"])
|
|
235
|
+
→ Script starts and pauses at DataPipeline#validate
|
|
236
|
+
Agent: evaluate_code(code: "records")
|
|
237
|
+
Agent: continue_execution()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Catch and debug exceptions
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
Agent: run_script(file: "my_script.rb")
|
|
244
|
+
Agent: set_breakpoint(exception_class: "NoMethodError")
|
|
245
|
+
Agent: continue_execution()
|
|
246
|
+
→ Execution pauses BEFORE the exception propagates
|
|
247
|
+
Agent: get_context()
|
|
248
|
+
Agent: evaluate_code(code: "$!.message")
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Restart after a crash
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
→ Program crashed with NoMethodError
|
|
255
|
+
Agent: run_script(file: "my_script.rb", restore_breakpoints: true)
|
|
256
|
+
→ Same breakpoints restored automatically
|
|
257
|
+
Agent: set_breakpoint(exception_class: "NoMethodError")
|
|
258
|
+
Agent: continue_execution()
|
|
259
|
+
→ Catches the exception before it crashes
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Debug a Rails request
|
|
263
|
+
|
|
264
|
+
Start your Rails server with debug enabled using `debug-rails`:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
debug-rails # equivalent to RUBY_DEBUG_OPEN=true bin/rails server
|
|
268
|
+
debug-rails s -p 4000 # specify port
|
|
269
|
+
debug-rails --debug-port 3333 # use specific TCP debug port (useful in Docker)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Then ask the agent to debug:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
Agent: connect()
|
|
276
|
+
Agent: set_breakpoint(file: "app/controllers/users_controller.rb", line: 15)
|
|
277
|
+
Agent: trigger_request(method: "GET", url: "http://localhost:3000/users/1")
|
|
278
|
+
Agent: get_context()
|
|
279
|
+
Agent: evaluate_code(code: "@user.attributes")
|
|
280
|
+
Agent: continue_execution()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Debug a Dockerized Rails app
|
|
284
|
+
|
|
285
|
+
> **Security note:** The debug gem has no authentication. Anyone who can reach the debug port can execute arbitrary code inside the container. Always restrict access as shown below.
|
|
286
|
+
|
|
287
|
+
#### Option A: Localhost-only TCP (simple)
|
|
288
|
+
|
|
289
|
+
Expose the debug port bound to `127.0.0.1` so only local processes can connect:
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
services:
|
|
293
|
+
web:
|
|
294
|
+
build: .
|
|
295
|
+
ports:
|
|
296
|
+
- "3000:3000"
|
|
297
|
+
- "127.0.0.1:12345:12345" # localhost only
|
|
298
|
+
environment:
|
|
299
|
+
- RUBY_DEBUG_OPEN=true
|
|
300
|
+
- RUBY_DEBUG_HOST=0.0.0.0
|
|
301
|
+
- RUBY_DEBUG_PORT=12345
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
Agent: connect(port: 12345)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The agent can read source files inside the container — no local copy of the source code is needed.
|
|
309
|
+
|
|
310
|
+
#### Option B: Unix socket volume mount (recommended)
|
|
311
|
+
|
|
312
|
+
Mount a shared directory for the debug socket. No port exposure needed:
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
services:
|
|
316
|
+
web:
|
|
317
|
+
build: .
|
|
318
|
+
ports:
|
|
319
|
+
- "3000:3000"
|
|
320
|
+
environment:
|
|
321
|
+
- RUBY_DEBUG_OPEN=true
|
|
322
|
+
- RUBY_DEBUG_SOCK_PATH=/debug/rdbg.sock
|
|
323
|
+
volumes:
|
|
324
|
+
- debug_sock:/debug
|
|
325
|
+
|
|
326
|
+
volumes:
|
|
327
|
+
debug_sock:
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
Agent: connect(path: "/path/to/debug_sock/rdbg.sock")
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Connect to an existing breakpoint
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# Terminal: your app hits a `debugger` statement
|
|
338
|
+
rdbg --open my_app.rb
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
Agent: list_debug_sessions()
|
|
343
|
+
Agent: connect(path: "/tmp/rdbg-1000/rdbg-12345")
|
|
344
|
+
Agent: get_context()
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## How it works
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
┌────────────┐ STDIO or Streamable HTTP ┌───────────┐ TCP/Unix Socket ┌──────────────┐
|
|
351
|
+
│ MCP Client │ ◄────────────────────────► │ debug-mcp │ ◄──────────────────► │ Ruby process │
|
|
352
|
+
│ │ (JSON-RPC) │(MCP Server)│ debug gem proto │ (rdbg) │
|
|
353
|
+
└────────────┘ └───────────┘ └──────────────┘
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
1. debug-mcp runs as an MCP server communicating via STDIO (default) or Streamable HTTP
|
|
357
|
+
2. The debug gem (`rdbg --open`) exposes a socket on the target Ruby process
|
|
358
|
+
3. debug-mcp connects to that socket using the debug gem's wire protocol
|
|
359
|
+
4. MCP tool calls are translated to debugger commands and results are returned
|
|
360
|
+
5. Idle sessions are automatically cleaned up after a configurable timeout
|
|
361
|
+
|
|
362
|
+
## Security
|
|
363
|
+
|
|
364
|
+
debug-mcp is a debugging tool that intentionally provides deep runtime access. Here's what you should know:
|
|
365
|
+
|
|
366
|
+
**Structured tools minimize arbitrary code execution.** Most debugging tasks — viewing variables, reading source code, inspecting model structure — are handled by dedicated tools that don't run arbitrary code. `evaluate_code` is available for runtime inspection, and a built-in safety checker warns about dangerous operations.
|
|
367
|
+
|
|
368
|
+
**The debug gem has no authentication.** Anyone who can reach the debug socket can execute arbitrary code in the target process. Always bind to localhost (`127.0.0.1`) or use Unix sockets. See the [Docker section](#debug-a-dockerized-rails-app) for configuration examples.
|
|
369
|
+
|
|
370
|
+
## Related projects
|
|
371
|
+
|
|
372
|
+
- [girb](https://github.com/rira100000000/girb) — AI-powered IRB assistant for humans, sharing the same philosophy.
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
git clone https://github.com/rira100000000/debug-mcp.git
|
|
378
|
+
cd debug-mcp
|
|
379
|
+
bundle install
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# シナリオ: 割引計算にバグがある
|
|
4
|
+
# 期待: 合計金額が1000円以上なら10%割引
|
|
5
|
+
# 実際: 割引が正しく適用されない
|
|
6
|
+
|
|
7
|
+
class Cart
|
|
8
|
+
attr_reader :items
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@items = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(name, price, quantity = 1)
|
|
15
|
+
@items << { name: name, price: price, quantity: quantity }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def subtotal
|
|
19
|
+
@items.sum { |item| item[:price] * item[:quantity] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def discount_rate
|
|
23
|
+
subtotal > 1000 ? 0.1 : 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def total
|
|
27
|
+
sub = subtotal
|
|
28
|
+
discount = sub * discount_rate
|
|
29
|
+
sub + discount # BUG: should be sub - discount
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
cart = Cart.new
|
|
34
|
+
cart.add("りんご", 200, 3)
|
|
35
|
+
cart.add("バナナ", 150, 2)
|
|
36
|
+
cart.add("みかん", 100, 4)
|
|
37
|
+
|
|
38
|
+
debugger
|
|
39
|
+
|
|
40
|
+
puts "小計: #{cart.subtotal}円"
|
|
41
|
+
puts "割引率: #{cart.discount_rate * 100}%"
|
|
42
|
+
puts "合計: #{cart.total}円"
|
|
43
|
+
puts "期待される合計: #{cart.subtotal * (1 - cart.discount_rate)}円"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# シナリオ: CSVデータ変換パイプラインで一部のレコードが消える
|
|
4
|
+
# ステップ実行で各段階のデータを追跡する
|
|
5
|
+
|
|
6
|
+
require "csv"
|
|
7
|
+
require "stringio"
|
|
8
|
+
|
|
9
|
+
csv_data = <<~CSV
|
|
10
|
+
id,name,age,score
|
|
11
|
+
1,田中太郎,28,85
|
|
12
|
+
2,鈴木花子,,92
|
|
13
|
+
3,佐藤次郎,35,
|
|
14
|
+
4,山田三郎,42,78
|
|
15
|
+
5,高橋四郎,-3,95
|
|
16
|
+
CSV
|
|
17
|
+
|
|
18
|
+
class DataPipeline
|
|
19
|
+
def initialize(csv_string)
|
|
20
|
+
@raw = CSV.parse(csv_string, headers: true)
|
|
21
|
+
@records = []
|
|
22
|
+
@errors = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
parse
|
|
27
|
+
validate
|
|
28
|
+
transform
|
|
29
|
+
{ records: @records, errors: @errors, stats: stats }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse
|
|
35
|
+
@records = @raw.map do |row|
|
|
36
|
+
{
|
|
37
|
+
id: row["id"]&.to_i,
|
|
38
|
+
name: row["name"],
|
|
39
|
+
age: row["age"]&.to_i, # nil.to_i => 0
|
|
40
|
+
score: row["score"]&.to_i, # nil.to_i => 0
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate
|
|
46
|
+
@records.reject! do |r|
|
|
47
|
+
if r[:age] <= 0
|
|
48
|
+
@errors << { id: r[:id], reason: "invalid age: #{r[:age]}" }
|
|
49
|
+
true
|
|
50
|
+
elsif r[:score] <= 0
|
|
51
|
+
@errors << { id: r[:id], reason: "invalid score: #{r[:score]}" }
|
|
52
|
+
true
|
|
53
|
+
else
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transform
|
|
60
|
+
avg = @records.sum { |r| r[:score] } / @records.size.to_f
|
|
61
|
+
@records.each do |r|
|
|
62
|
+
r[:grade] = case r[:score]
|
|
63
|
+
when 90.. then "A"
|
|
64
|
+
when 80.. then "B"
|
|
65
|
+
when 70.. then "C"
|
|
66
|
+
else "D"
|
|
67
|
+
end
|
|
68
|
+
r[:above_average] = r[:score] > avg
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stats
|
|
73
|
+
{
|
|
74
|
+
total_input: @raw.size,
|
|
75
|
+
valid_records: @records.size,
|
|
76
|
+
error_count: @errors.size,
|
|
77
|
+
average_score: @records.sum { |r| r[:score] } / @records.size.to_f,
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
pipeline = DataPipeline.new(csv_data)
|
|
83
|
+
|
|
84
|
+
debugger
|
|
85
|
+
|
|
86
|
+
result = pipeline.run
|
|
87
|
+
|
|
88
|
+
puts "=== 結果 ==="
|
|
89
|
+
puts "有効レコード: #{result[:records].size}"
|
|
90
|
+
result[:records].each { |r| puts " #{r}" }
|
|
91
|
+
puts "エラー: #{result[:errors].size}"
|
|
92
|
+
result[:errors].each { |e| puts " #{e}" }
|
|
93
|
+
puts "統計: #{result[:stats]}"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# シナリオ: 木構造の探索で特定ノードの状態を調査
|
|
4
|
+
# ブレークポイントを条件付きで設定して、特定の条件で止める
|
|
5
|
+
|
|
6
|
+
class TreeNode
|
|
7
|
+
attr_accessor :value, :children, :metadata
|
|
8
|
+
|
|
9
|
+
def initialize(value, metadata: {})
|
|
10
|
+
@value = value
|
|
11
|
+
@children = []
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(*values)
|
|
16
|
+
values.each do |v|
|
|
17
|
+
child = TreeNode.new(v)
|
|
18
|
+
@children << child
|
|
19
|
+
yield child if block_given?
|
|
20
|
+
end
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find(target)
|
|
25
|
+
return self if value == target
|
|
26
|
+
|
|
27
|
+
children.each do |child|
|
|
28
|
+
result = child.find(target)
|
|
29
|
+
return result if result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def depth_first(&block)
|
|
36
|
+
block.call(self)
|
|
37
|
+
children.each { |c| c.depth_first(&block) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def total_nodes
|
|
41
|
+
1 + children.sum(&:total_nodes)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def max_depth(current = 0)
|
|
45
|
+
if children.empty?
|
|
46
|
+
current
|
|
47
|
+
else
|
|
48
|
+
children.map { |c| c.max_depth(current + 1) }.max
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s(indent = 0)
|
|
53
|
+
result = "#{" " * indent}#{value}\n"
|
|
54
|
+
children.each { |c| result += c.to_s(indent + 2) }
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 組織図を構築
|
|
60
|
+
company = TreeNode.new("CEO")
|
|
61
|
+
|
|
62
|
+
company.add("CTO") do |cto|
|
|
63
|
+
cto.add("VP Engineering") do |vp|
|
|
64
|
+
vp.add("Team Lead A") do |tl|
|
|
65
|
+
tl.add("Engineer 1", "Engineer 2", "Engineer 3")
|
|
66
|
+
end
|
|
67
|
+
vp.add("Team Lead B") do |tl|
|
|
68
|
+
tl.add("Engineer 4", "Engineer 5")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
cto.add("VP Product") do |vp|
|
|
72
|
+
vp.add("PM 1", "PM 2")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
company.add("CFO") do |cfo|
|
|
77
|
+
cfo.add("Controller") do |ctrl|
|
|
78
|
+
ctrl.add("Accountant 1", "Accountant 2")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
company.add("COO") do |coo|
|
|
83
|
+
coo.add("VP Operations") do |vp|
|
|
84
|
+
vp.add("Manager 1", "Manager 2", "Manager 3")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
debugger
|
|
89
|
+
|
|
90
|
+
puts company.to_s
|
|
91
|
+
puts "Total nodes: #{company.total_nodes}"
|
|
92
|
+
puts "Max depth: #{company.max_depth}"
|
|
93
|
+
|
|
94
|
+
# 特定のノードを検索
|
|
95
|
+
target = company.find("Engineer 3")
|
|
96
|
+
puts "Found: #{target&.value || 'not found'}"
|