Module: DebugMcp::RailsHelper

Defined in:
lib/debug_mcp/rails_helper.rb

Constant Summary collapse

TRAP_CONTEXT_HINT =
"Note: The process may be in signal trap context (common with Puma). " \
"Set a breakpoint and use trigger_request to escape trap context first."

Class Method Summary collapse

Class Method Details

.clean_script_output(output) ⇒ Object

Clean debug gem output from a script that returns a string value. Strips “=> ” prefix, removes surrounding quotes, and unescapes \n.



49
50
51
52
53
54
55
56
57
# File 'lib/debug_mcp/rails_helper.rb', line 49

def clean_script_output(output)
  cleaned = output.strip.sub(/\A=> /, "")
  return nil if cleaned == "nil" || cleaned.empty?

  if cleaned.start_with?('"') && cleaned.end_with?('"')
    cleaned = cleaned[1..-2].gsub('\\n', "\n").gsub('\\"', '"')
  end
  cleaned.empty? ? nil : cleaned
end

.eval_expr(client, expr) ⇒ Object

Evaluate a simple ‘p` expression and return the cleaned string result. Uses `p` (not `puts`) because `p` output is captured as the expression result by the debug gem, which works even in signal trap context. Returns nil if the result is nil or evaluation fails.



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/debug_mcp/rails_helper.rb', line 65

def eval_expr(client, expr)
  result = client.send_command("p #{expr}")
  cleaned = result.strip.sub(/\A=> /, "")
  return nil if cleaned == "nil" || cleaned.empty?

  if cleaned.start_with?('"') && cleaned.end_with?('"')
    cleaned = cleaned[1..-2]
    cleaned = cleaned.gsub('\\n', "\n").gsub('\\"', '"').gsub("\\\\", "\\")
  end
  cleaned.empty? ? nil : cleaned
rescue DebugMcp::Error
  nil
end

.lightweight_routes(client, controller: nil, path: nil, limit: 200) ⇒ Object

Fetch routes using a single ‘p` expression (trap-safe). Returns { count: Integer, lines: String } or nil on failure.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/debug_mcp/rails_helper.rb', line 81

def lightweight_routes(client, controller: nil, path: nil, limit: 200)
  filter_parts = ["r.defaults[:controller].to_s!=''"]
  filter_parts << "r.defaults[:controller].to_s.include?(#{controller.inspect})" if controller
  filter_parts << "r.path.spec.to_s.include?(#{path.inspect})" if path
  filter = filter_parts.join(" && ")

  count_output = eval_expr(client,
    "Rails.application.routes.routes.count{|r|r.defaults[:controller].to_s!=''}")
  return nil if count_output.nil? # eval failed — can't access routes

  count = count_output.to_i

  expr = "Rails.application.routes.routes.select{|r|#{filter}}." \
         "first(#{limit}).map{|r|" \
         "r.verb.to_s.ljust(7)+' '+" \
         "r.path.spec.to_s.sub('(.:format)','')+' '+" \
         "r.defaults[:controller].to_s+'#'+r.defaults[:action].to_s+" \
         "(r.name.to_s.empty? ? '' : '  ('+r.name.to_s+')')}.join(\"\\n\")"
  lines = eval_expr(client, expr)

  { count: count, lines: lines || "" }
rescue DebugMcp::Error
  nil
end

.log_file_path(client) ⇒ Object

Get the path to the Rails log file (trap-safe). Returns the absolute path string or nil if not determinable.



142
143
144
145
146
147
148
149
150
# File 'lib/debug_mcp/rails_helper.rb', line 142

def log_file_path(client)
  root = eval_expr(client, "Rails.root.to_s")
  env = eval_expr(client, "Rails.env")
  return nil unless root && env

  "#{root}/log/#{env}.log"
rescue DebugMcp::Error
  nil
end

.model_files(client) ⇒ Object

List model files from app/models/ using Dir.glob (trap-safe). Returns array of model file names (e.g., [“user”, “post”, “admin/account”]) or nil.



129
130
131
132
133
134
135
136
137
138
# File 'lib/debug_mcp/rails_helper.rb', line 129

def model_files(client)
  output = eval_expr(client,
    "Dir.glob(Rails.root.join('app','models','**','*.rb').to_s)." \
    "sort.map{|f|f.split('/models/').last.sub('.rb','')}.reject{|f|f=='application_record'}.join(', ')")
  return nil if output.nil? || output.empty?

  output.split(", ")
rescue DebugMcp::Error
  nil
end

.rails?(client) ⇒ Boolean

Check if Rails is available without raising. Returns true if the connected process has Rails loaded.

Returns:

  • (Boolean)


20
21
22
23
24
25
# File 'lib/debug_mcp/rails_helper.rb', line 20

def rails?(client)
  result = client.send_command("p defined?(Rails)")
  result.strip.sub(/\A=> /, "").include?("constant")
rescue DebugMcp::Error
  false
end

.require_rails!(client) ⇒ Object

Verify that the connected process is a Rails application. Raises SessionError if Rails is not defined.



11
12
13
14
15
16
# File 'lib/debug_mcp/rails_helper.rb', line 11

def require_rails!(client)
  result = client.send_command("p defined?(Rails)")
  unless result.strip.sub(/\A=> /, "").include?("constant")
    raise DebugMcp::SessionError, "Not a Rails application. This tool requires a connected Rails process."
  end
end

.route_summary(client, limit: 5) ⇒ Object

Fetch a compact route summary for connect output (trap-safe). Returns { count: Integer, samples: [String] } or nil.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/debug_mcp/rails_helper.rb', line 108

def route_summary(client, limit: 5)
  count_output = eval_expr(client,
    "Rails.application.routes.routes.count{|r|r.defaults[:controller].to_s!=''}")
  return nil if count_output.nil?

  count = count_output.to_i

  sample_expr = "Rails.application.routes.routes.select{|r|r.defaults[:controller].to_s!=''}." \
                "first(#{limit}).map{|r|" \
                "r.verb.to_s.ljust(7)+' '+" \
                "r.path.spec.to_s.sub('(.:format)','')+' '+" \
                "r.defaults[:controller].to_s+'#'+r.defaults[:action].to_s}.join(\"\\n\")"
  samples = eval_expr(client, sample_expr)

  { count: count, samples: samples&.split("\n") || [] }
rescue DebugMcp::Error
  nil
end

.run_base64_script(client, code, timeout: 15) ⇒ Object

Execute a multi-line Ruby script via Base64 encoding in the target process. Returns the cleaned string result, or nil if the script returned nil/empty. Raises DebugMcp::Error on communication failure.



40
41
42
43
44
45
# File 'lib/debug_mcp/rails_helper.rb', line 40

def run_base64_script(client, code, timeout: 15)
  encoded = Base64.strict_encode64(code.encode(Encoding::UTF_8))
  command = "require 'base64'; eval(::Base64.decode64('#{encoded}').force_encoding('UTF-8'))"
  output = client.send_command(command, timeout: timeout)
  clean_script_output(output)
end

.trap_context?(client) ⇒ Boolean

Check if the client is in signal trap context. Returns true if thread operations are restricted.

Returns:

  • (Boolean)


29
30
31
32
33
# File 'lib/debug_mcp/rails_helper.rb', line 29

def trap_context?(client)
  client.respond_to?(:in_trap_context?) && client.in_trap_context?
rescue DebugMcp::Error
  false
end