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
-
.clean_script_output(output) ⇒ Object
Clean debug gem output from a script that returns a string value.
-
.eval_expr(client, expr) ⇒ Object
Evaluate a simple ‘p` expression and return the cleaned string result.
-
.lightweight_routes(client, controller: nil, path: nil, limit: 200) ⇒ Object
Fetch routes using a single ‘p` expression (trap-safe).
-
.log_file_path(client) ⇒ Object
Get the path to the Rails log file (trap-safe).
-
.model_files(client) ⇒ Object
List model files from app/models/ using Dir.glob (trap-safe).
-
.rails?(client) ⇒ Boolean
Check if Rails is available without raising.
-
.require_rails!(client) ⇒ Object
Verify that the connected process is a Rails application.
-
.route_summary(client, limit: 5) ⇒ Object
Fetch a compact route summary for connect output (trap-safe).
-
.run_base64_script(client, code, timeout: 15) ⇒ Object
Execute a multi-line Ruby script via Base64 encoding in the target process.
-
.trap_context?(client) ⇒ Boolean
Check if the client is in signal trap context.
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.
50 51 52 53 54 55 56 57 58 |
# File 'lib/debug_mcp/rails_helper.rb', line 50 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.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/debug_mcp/rails_helper.rb', line 66 def eval_expr(client, expr) # Note: internal probes use simple expressions (Rails.root, Rails.env, # Dir.pwd, etc.) that don't fire ActiveSupport::Notifications events, # so SourceTagging.wrap isn't applied here. Tools that take arbitrary # user expressions (evaluate_code, inspect_object) handle tagging # themselves at the call site. 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.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/debug_mcp/rails_helper.rb', line 87 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.
148 149 150 151 152 153 154 155 156 |
# File 'lib/debug_mcp/rails_helper.rb', line 148 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.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/debug_mcp/rails_helper.rb', line 135 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.
21 22 23 24 25 26 |
# File 'lib/debug_mcp/rails_helper.rb', line 21 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.
12 13 14 15 16 17 |
# File 'lib/debug_mcp/rails_helper.rb', line 12 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.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/debug_mcp/rails_helper.rb', line 114 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.
41 42 43 44 45 46 |
# File 'lib/debug_mcp/rails_helper.rb', line 41 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.
30 31 32 33 34 |
# File 'lib/debug_mcp/rails_helper.rb', line 30 def trap_context?(client) client.respond_to?(:in_trap_context?) && client.in_trap_context? rescue DebugMcp::Error false end |