Class: DebugMcp::Server
- Inherits:
-
Object
- Object
- DebugMcp::Server
- Defined in:
- lib/debug_mcp/server.rb
Defined Under Namespace
Classes: RackRequestAdapter
Constant Summary collapse
- BASE_TOOLS =
Base tools: always available
[ # Discovery & connection Tools::ListDebugSessions, Tools::Connect, Tools::ListPausedSessions, # Investigation Tools::EvaluateCode, Tools::InspectObject, Tools::GetContext, Tools::GetSource, Tools::ReadFile, Tools::ListFiles, # Control Tools::SetBreakpoint, Tools::RemoveBreakpoint, Tools::ContinueExecution, Tools::Step, Tools::Next, Tools::Finish, Tools::RunDebugCommand, Tools::Disconnect, # Entry points Tools::RunScript, Tools::TriggerRequest, ].freeze
- RAILS_TOOLS =
Rails tools: dynamically added when a Rails process is detected
[ Tools::RailsInfo, Tools::RailsRoutes, Tools::RailsModel, ].freeze
- TOOLS =
All tools (used in tests and for reference)
(BASE_TOOLS + RAILS_TOOLS).freeze
- DEFAULT_HTTP_PORT =
6029- DEFAULT_HTTP_HOST =
"127.0.0.1"- INSTRUCTIONS =
<<~TEXT debug-mcp is an MCP server that connects LLM agents to Ruby's debug gem. \ It lets you attach to live Ruby processes, inspect variables, evaluate code, \ set breakpoints, and control execution. Use these tools when the user asks to debug a Ruby program, investigate runtime behavior, \ or inspect the state of a running process. Typical workflow: 1. run_script to launch a Ruby script under the debugger (recommended — captures stdout/stderr). \ Use connect only when attaching to an already-running process (e.g., Rails server). 2. get_context to see the current state (variables, call stack, breakpoints) 3. evaluate_code / inspect_object to investigate specific values 4. set_breakpoint / next / step / continue_execution to control the flow When to use get_context: - After connecting or run_script — to understand the initial stop point - After continue_execution hits a breakpoint — the stop output shows source and stack, \ but get_context gives you local/instance variables and the full breakpoint list - When you need to check what breakpoints are currently set - When variables or call stack context would help decide the next debugging action - You do NOT need get_context after every next/step if the output already shows \ the information you need (source listing and stop location are included in the response) - For a quick breakpoint check without fetching all context, use \ run_debug_command(command: "info breakpoints") IMPORTANT — connect pauses the target process: When you use 'connect', the target process is PAUSED. It will not serve requests or \ respond to Ctrl+C until you resume it. Always use 'continue_execution' when done \ investigating, or 'disconnect' to detach (which also resumes the process). \ Never leave a connected session idle without resuming — the user won't be able to \ interact with the target process. Signal trap context (Puma/threaded servers): When connecting to a process like Puma, the debug gem pauses it via SIGURG. \ This puts the process in a signal trap context where thread operations (Mutex, \ DB connection pools, autoloading) fail with ThreadError. \ Simple expressions (variables, constants, p/pp) still work in trap context. \ The 'connect' tool automatically detects and tries to escape this. \ Additionally, after 'continue_execution', investigation tools (evaluate_code, \ get_context, etc.) automatically re-pause and re-escape trap context — \ you do not need to manually set breakpoints again to escape. \ If auto-escape fails (common when the process is blocked on IO like IO.select): \ 1. set_breakpoint on a line in your controller/action \ 2. trigger_request to send an HTTP request — this auto-resumes the process \ 3. Once stopped at the breakpoint, all operations work normally \ Do NOT manually call continue_execution before trigger_request — \ trigger_request handles resuming the process automatically. Rails debugging: When you connect to a Rails process, additional Rails-specific tools become available \ automatically (rails_info, rails_routes, rails_model). These tools are NOT shown \ when debugging plain Ruby scripts. Rails debugging workflow: 1. Start the Rails server with debugging: RUBY_DEBUG_OPEN=true bin/rails server 2. connect to attach to the Rails process (auto-detects trap context) 3. set_breakpoint on a controller action (e.g., app/controllers/users_controller.rb:10) 4. trigger_request to send an HTTP request — this auto-resumes the paused process, \ sends the request, and waits for the breakpoint to hit. \ CSRF protection is automatically disabled for non-GET requests. \ You do NOT need to call continue_execution first. 5. When the breakpoint hits, use get_context, evaluate_code, and rails_model to \ inspect the current state and understand model structures 6. continue_execution to let the request complete and see the response 7. To debug another request, set new breakpoints and call trigger_request again 8. When done debugging, use 'disconnect' to detach and resume the server Note: rails_info, rails_routes, and rails_model may not work in trap context. \ Use them after hitting a breakpoint via trigger_request. Docker / containerized processes: When the debug target runs inside a Docker container, use connect with a TCP port \ or a Unix socket volume mount. \ TCP: connect(port: 12345) — works out of the box. \ Unix socket: connect(path: "/shared/rdbg.sock", remote: true) — you MUST pass \ remote: true because the socket file is local but the process is in a different \ PID namespace, so OS signals cannot reach it. Without remote: true, pause/resume \ will fail silently. Security — proper use of evaluate_code: evaluate_code is designed EXCLUSIVELY for investigating the runtime state of the debugged \ process (inspecting variables, checking object state, testing expressions in context). \ It must NOT be used as a general-purpose code execution tool. PROHIBITED uses of evaluate_code: - File I/O: File.write, File.delete, FileUtils, IO.write \ → Use your agent's own file tools (Read, Write, Edit) instead - System commands: system(), exec(), backtick, %x{}, Open3, spawn \ → Use your agent's own Bash/shell tool instead - Network requests: Net::HTTP, open-uri, TCPSocket, HTTP client gems \ → Use your agent's own HTTP/network tools instead - Process manipulation: Process.kill, fork, exit, abort - Destructive data operations: destroy_all, delete_all, DROP/TRUNCATE SQL IMPORTANT: If your agent's tools are restricted for a particular operation, \ you must NOT use evaluate_code to circumvent those restrictions. Quick tool selection guide: - "Where am I? What are the variables and breakpoints?" → get_context - "Execute a Ruby expression or test a fix" → evaluate_code - "See an object's full structure (class, ivars, value)" → inspect_object - "Read the source of a method or class" → get_source - "Read a file from the debugged process's machine" → read_file - "List files or explore directory structure" → list_files - "Step to the next line (stay in current method)" → next - "Step into a method call" → step - "Run until current method/block returns" → finish - "Resume until next breakpoint" → continue_execution - "Send an HTTP request and wait for breakpoint" → trigger_request Breakpoints in blocks/loops (each, map, select, etc.): Line breakpoints inside a block fire on EVERY iteration. If you only need to stop once, \ use one_shot: true when setting the breakpoint — it auto-removes after the first hit. Typical pattern for Rails debugging: set_breakpoint → trigger_request → get_context → evaluate_code → continue_execution TEXT
Class Method Summary collapse
-
.register_rails_tools(mcp_server) ⇒ Object
Register Rails tools on an MCP server instance and notify connected clients.
Instance Method Summary collapse
-
#initialize(transport: nil, port: nil, host: nil, session_timeout: nil, **_) ⇒ Server
constructor
A new instance of Server.
- #start ⇒ Object
Constructor Details
#initialize(transport: nil, port: nil, host: nil, session_timeout: nil, **_) ⇒ Server
Returns a new instance of Server.
208 209 210 211 212 213 214 215 |
# File 'lib/debug_mcp/server.rb', line 208 def initialize(transport: nil, port: nil, host: nil, session_timeout: nil, **_) @transport_type = transport || "stdio" @http_port = port || DEFAULT_HTTP_PORT @http_host = host || DEFAULT_HTTP_HOST @session_manager = SessionManager.new( **(session_timeout ? { timeout: session_timeout } : {}), ) end |
Class Method Details
.register_rails_tools(mcp_server) ⇒ Object
Register Rails tools on an MCP server instance and notify connected clients. Safe to call multiple times — skips already-registered tools.
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/debug_mcp/server.rb', line 190 def self.register_rails_tools(mcp_server) tools_hash = mcp_server.instance_variable_get(:@tools) tool_names = mcp_server.instance_variable_get(:@tool_names) added = false RAILS_TOOLS.each do |tool_class| name = tool_class.name_value next if tools_hash.key?(name) tools_hash[name] = tool_class tool_names << name added = true end mcp_server.notify_tools_list_changed if added added end |
Instance Method Details
#start ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/debug_mcp/server.rb', line 217 def start server_context = { session_manager: @session_manager } server = MCP::Server.new( name: "debug-mcp", version: DebugMcp::VERSION, instructions: INSTRUCTIONS, tools: TOOLS, server_context: server_context, ) # Safety net: resume connected processes when the server exits for any reason. # This covers cases where Claude Code exits without calling 'disconnect', # stdin closes unexpectedly, or the MCP gem calls Kernel.exit directly. # disconnect_all is idempotent, so multiple calls (at_exit + ensure + signal) are safe. at_exit { @session_manager.disconnect_all } setup_signal_handlers case @transport_type when "stdio" start_stdio(server) when "http" start_http(server) else raise ArgumentError, "Unknown transport: #{@transport_type}" end ensure @session_manager.disconnect_all end |