Class: DebugMcp::Server

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

#startObject



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