debug-mcp 0.2.1 → 0.3.0

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.ja.md +15 -3
  4. data/README.md +15 -3
  5. data/lib/debug_mcp/notifications_subscriber.rb +162 -38
  6. data/lib/debug_mcp/rails_helper.rb +68 -0
  7. data/lib/debug_mcp/server.rb +15 -5
  8. data/lib/debug_mcp/tools/rails_info.rb +20 -0
  9. data/lib/debug_mcp/tools/rails_mail_deliveries.rb +222 -0
  10. data/lib/debug_mcp/tools/rails_recent_events.rb +187 -0
  11. data/lib/debug_mcp/version.rb +1 -1
  12. metadata +23 -80
  13. data/examples/01_simple_bug.rb +0 -43
  14. data/examples/02_data_pipeline.rb +0 -93
  15. data/examples/03_recursion.rb +0 -96
  16. data/examples/RAILS_SCENARIOS.md +0 -350
  17. data/examples/SCENARIOS.md +0 -142
  18. data/examples/rails_test_app/setup.sh +0 -428
  19. data/examples/rails_test_app/testapp/.dockerignore +0 -10
  20. data/examples/rails_test_app/testapp/.ruby-version +0 -1
  21. data/examples/rails_test_app/testapp/Dockerfile +0 -23
  22. data/examples/rails_test_app/testapp/Gemfile +0 -17
  23. data/examples/rails_test_app/testapp/README.md +0 -65
  24. data/examples/rails_test_app/testapp/Rakefile +0 -6
  25. data/examples/rails_test_app/testapp/app/assets/images/.keep +0 -0
  26. data/examples/rails_test_app/testapp/app/assets/stylesheets/application.css +0 -1
  27. data/examples/rails_test_app/testapp/app/controllers/application_controller.rb +0 -4
  28. data/examples/rails_test_app/testapp/app/controllers/concerns/.keep +0 -0
  29. data/examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb +0 -38
  30. data/examples/rails_test_app/testapp/app/controllers/health_controller.rb +0 -11
  31. data/examples/rails_test_app/testapp/app/controllers/orders_controller.rb +0 -100
  32. data/examples/rails_test_app/testapp/app/controllers/posts_controller.rb +0 -82
  33. data/examples/rails_test_app/testapp/app/controllers/sessions_controller.rb +0 -25
  34. data/examples/rails_test_app/testapp/app/controllers/users_controller.rb +0 -44
  35. data/examples/rails_test_app/testapp/app/helpers/application_helper.rb +0 -2
  36. data/examples/rails_test_app/testapp/app/models/application_record.rb +0 -3
  37. data/examples/rails_test_app/testapp/app/models/comment.rb +0 -8
  38. data/examples/rails_test_app/testapp/app/models/concerns/.keep +0 -0
  39. data/examples/rails_test_app/testapp/app/models/order.rb +0 -56
  40. data/examples/rails_test_app/testapp/app/models/order_item.rb +0 -16
  41. data/examples/rails_test_app/testapp/app/models/post.rb +0 -29
  42. data/examples/rails_test_app/testapp/app/models/user.rb +0 -34
  43. data/examples/rails_test_app/testapp/app/services/order_report_service.rb +0 -40
  44. data/examples/rails_test_app/testapp/app/views/layouts/application.html.erb +0 -28
  45. data/examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb +0 -22
  46. data/examples/rails_test_app/testapp/app/views/pwa/service-worker.js +0 -26
  47. data/examples/rails_test_app/testapp/bin/ci +0 -6
  48. data/examples/rails_test_app/testapp/bin/dev +0 -2
  49. data/examples/rails_test_app/testapp/bin/rails +0 -4
  50. data/examples/rails_test_app/testapp/bin/rake +0 -4
  51. data/examples/rails_test_app/testapp/bin/setup +0 -35
  52. data/examples/rails_test_app/testapp/config/application.rb +0 -42
  53. data/examples/rails_test_app/testapp/config/boot.rb +0 -3
  54. data/examples/rails_test_app/testapp/config/ci.rb +0 -14
  55. data/examples/rails_test_app/testapp/config/database.yml +0 -32
  56. data/examples/rails_test_app/testapp/config/environment.rb +0 -5
  57. data/examples/rails_test_app/testapp/config/environments/development.rb +0 -54
  58. data/examples/rails_test_app/testapp/config/environments/production.rb +0 -67
  59. data/examples/rails_test_app/testapp/config/environments/test.rb +0 -42
  60. data/examples/rails_test_app/testapp/config/initializers/content_security_policy.rb +0 -29
  61. data/examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb +0 -8
  62. data/examples/rails_test_app/testapp/config/initializers/inflections.rb +0 -16
  63. data/examples/rails_test_app/testapp/config/locales/en.yml +0 -31
  64. data/examples/rails_test_app/testapp/config/puma.rb +0 -39
  65. data/examples/rails_test_app/testapp/config/routes.rb +0 -34
  66. data/examples/rails_test_app/testapp/config.ru +0 -6
  67. data/examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb +0 -12
  68. data/examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb +0 -13
  69. data/examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb +0 -11
  70. data/examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb +0 -14
  71. data/examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb +0 -13
  72. data/examples/rails_test_app/testapp/db/schema.rb +0 -71
  73. data/examples/rails_test_app/testapp/db/seeds.rb +0 -85
  74. data/examples/rails_test_app/testapp/docker-compose.yml +0 -21
  75. data/examples/rails_test_app/testapp/docker-entrypoint.sh +0 -10
  76. data/examples/rails_test_app/testapp/lib/tasks/.keep +0 -0
  77. data/examples/rails_test_app/testapp/log/.keep +0 -0
  78. data/examples/rails_test_app/testapp/public/400.html +0 -135
  79. data/examples/rails_test_app/testapp/public/404.html +0 -135
  80. data/examples/rails_test_app/testapp/public/406-unsupported-browser.html +0 -135
  81. data/examples/rails_test_app/testapp/public/422.html +0 -135
  82. data/examples/rails_test_app/testapp/public/500.html +0 -135
  83. data/examples/rails_test_app/testapp/public/icon.png +0 -0
  84. data/examples/rails_test_app/testapp/public/icon.svg +0 -3
  85. data/examples/rails_test_app/testapp/public/robots.txt +0 -1
  86. data/examples/rails_test_app/testapp/script/.keep +0 -0
  87. data/examples/rails_test_app/testapp/storage/.keep +0 -0
  88. data/examples/rails_test_app/testapp/tmp/.keep +0 -0
  89. data/examples/rails_test_app/testapp/tmp/pids/.keep +0 -0
  90. data/examples/rails_test_app/testapp/tmp/storage/.keep +0 -0
  91. data/examples/rails_test_app/testapp/vendor/.keep +0 -0
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require "base64"
5
+ require_relative "../rails_helper"
6
+
7
+ module DebugMcp
8
+ module Tools
9
+ # Read ActionMailer::Base.deliveries from the running process.
10
+ #
11
+ # Observability caveat (surfaced in every response): deliveries is only
12
+ # populated when delivery_method is :test. With :smtp / :letter_opener / etc.
13
+ # the array is usually empty, so an empty result is NOT proof that no mail
14
+ # was sent — it may simply not be observable this way.
15
+ #
16
+ # PII: recipients, subject and body can contain personal data or secrets, and
17
+ # Rails' filter_parameters does NOT apply to this path. Bodies are truncated
18
+ # to a preview by default; attachment CONTENT is never returned (only name
19
+ # and content type).
20
+ class RailsMailDeliveries < MCP::Tool
21
+ description "[Investigation] Show emails captured in ActionMailer::Base.deliveries (from, to, " \
22
+ "cc, bcc, subject, body preview, attachment names). Only populated when " \
23
+ "delivery_method is :test — with :smtp/:letter_opener the list is usually empty, so " \
24
+ "an empty result does NOT prove no mail was sent (the response says whether it is " \
25
+ "observable). Bodies are truncated by default and may contain PII; attachment content " \
26
+ "is never returned. Requires the process to be paused."
27
+
28
+ annotations(
29
+ title: "Rails Mail Deliveries",
30
+ read_only_hint: true,
31
+ destructive_hint: false,
32
+ open_world_hint: false,
33
+ )
34
+
35
+ DEFAULT_LIMIT = 20
36
+ MAX_LIMIT = 200
37
+ DEFAULT_PREVIEW_CHARS = 500
38
+ MAX_PREVIEW_CHARS = 5000
39
+ # Hard ceiling applied even when include_body is true, so a multi-megabyte
40
+ # body can never cross the line-oriented debug socket as one JSON line.
41
+ MAX_BODY_CHARS = 50_000
42
+
43
+ input_schema(
44
+ properties: {
45
+ limit: {
46
+ type: "integer",
47
+ description: "How many of the most recent deliveries to show (default: #{DEFAULT_LIMIT}).",
48
+ },
49
+ include_body: {
50
+ type: "boolean",
51
+ description: "Return more of the body (still capped at #{MAX_BODY_CHARS} chars) instead of " \
52
+ "a short preview. Off by default because bodies may contain PII/secrets.",
53
+ },
54
+ body_preview_chars: {
55
+ type: "integer",
56
+ description: "Preview length when include_body is false " \
57
+ "(default: #{DEFAULT_PREVIEW_CHARS}, max: #{MAX_PREVIEW_CHARS}).",
58
+ },
59
+ session_id: {
60
+ type: "string",
61
+ description: "Debug session ID (uses default session if omitted)",
62
+ },
63
+ },
64
+ )
65
+
66
+ class << self
67
+ def call(limit: nil, include_body: false, body_preview_chars: nil, session_id: nil, server_context:)
68
+ client = server_context[:session_manager].client(session_id)
69
+ client.auto_repause!
70
+ RailsHelper.require_rails!(client)
71
+
72
+ # The probe iterates Mail objects and puts a JSON line; both need a
73
+ # normal (non-trap) paused context.
74
+ if RailsHelper.trap_context?(client)
75
+ return text_response("Rails mail deliveries: unavailable in trap context.\n\n" \
76
+ "#{RailsHelper::TRAP_CONTEXT_HINT}")
77
+ end
78
+
79
+ data = fetch_deliveries(
80
+ client,
81
+ limit: resolve_limit(limit),
82
+ include_body: include_body ? true : false,
83
+ preview_chars: resolve_preview(body_preview_chars),
84
+ )
85
+
86
+ if data.nil? || data.empty? || data[:error]
87
+ msg = data && data[:error] ? "Error: #{data[:error]}" : "Rails mail deliveries: unavailable."
88
+ msg += "\n\n#{RailsHelper::TRAP_CONTEXT_HINT}" if RailsHelper.trap_context?(client)
89
+ return text_response(msg)
90
+ end
91
+
92
+ text_response(format_output(data))
93
+ rescue DebugMcp::Error => e
94
+ text_response("Error: #{e.message}")
95
+ end
96
+
97
+ private
98
+
99
+ def resolve_limit(limit)
100
+ n = limit.to_i
101
+ n = DEFAULT_LIMIT unless n.positive?
102
+ [n, MAX_LIMIT].min
103
+ end
104
+
105
+ def resolve_preview(chars)
106
+ n = chars.to_i
107
+ return DEFAULT_PREVIEW_CHARS unless n.positive?
108
+
109
+ [n, MAX_PREVIEW_CHARS].min
110
+ end
111
+
112
+ def fetch_deliveries(client, limit:, include_body:, preview_chars:)
113
+ script = build_script(limit: limit, include_body: include_body, preview_chars: preview_chars)
114
+ encoded = Base64.strict_encode64(script)
115
+ cmd = "require 'base64'; eval(::Base64.decode64('#{encoded}').force_encoding('UTF-8'))"
116
+ result = client.send_command(cmd, timeout: 15)
117
+ # The script's value is a base64 JSON blob (see build_script) — the
118
+ # debug socket does not forward the debuggee's stdout, so we return the
119
+ # data as the evaluated expression's value, not via puts.
120
+ RailsHelper.decode_json_result(result, nil)
121
+ rescue DebugMcp::Error
122
+ nil
123
+ end
124
+
125
+ # Runs inside the target process. Truncates the body and strips newlines
126
+ # there (not in the MCP layer) so the result stays small, and returns the
127
+ # JSON base64-encoded as the script's value (send_command only sees the
128
+ # evaluated value, not puts output).
129
+ def build_script(limit:, include_body:, preview_chars:)
130
+ <<~RUBY
131
+ __result = begin
132
+ if defined?(ActionMailer::Base)
133
+ dm = ActionMailer::Base.delivery_method
134
+ deliveries = ActionMailer::Base.deliveries
135
+ shown = deliveries.last(#{limit})
136
+ offset = deliveries.size - shown.size
137
+ items = shown.map.with_index do |m, i|
138
+ raw = begin
139
+ if m.multipart?
140
+ part = m.text_part || m.html_part
141
+ part ? part.body.decoded.to_s : ""
142
+ else
143
+ m.body.decoded.to_s
144
+ end
145
+ rescue StandardError
146
+ ""
147
+ end
148
+ full_len = raw.length
149
+ cap = #{include_body} ? #{MAX_BODY_CHARS} : #{preview_chars}
150
+ shown = raw[0, cap].to_s.gsub(/\\s+/, " ").strip
151
+ atts = (m.attachments || []).map { |a|
152
+ { filename: a.filename.to_s[0, 200], content_type: a.content_type.to_s[0, 100] }
153
+ } rescue []
154
+ {
155
+ index: offset + i,
156
+ from: Array(m.from).join(", ")[0, 500],
157
+ to: Array(m.to).join(", ")[0, 1000],
158
+ cc: Array(m.cc).join(", ")[0, 1000],
159
+ bcc: Array(m.bcc).join(", ")[0, 1000],
160
+ subject: m.subject.to_s.gsub(/\\s+/, " ").strip[0, 500],
161
+ multipart: (m.multipart? rescue false),
162
+ body_preview: shown,
163
+ body_truncated: (full_len > cap),
164
+ attachments: atts,
165
+ }
166
+ end
167
+ { observable: (dm == :test), delivery_method: dm.to_s,
168
+ total: deliveries.size, deliveries: items }.to_json
169
+ else
170
+ { observable: false, delivery_method: "(ActionMailer not loaded)",
171
+ total: 0, deliveries: [] }.to_json
172
+ end
173
+ rescue => e
174
+ { error: e.class.to_s + ": " + e.message }.to_json
175
+ end
176
+ [__result].pack("m0")
177
+ RUBY
178
+ end
179
+
180
+ def format_output(data)
181
+ lines = ["=== Rails Mail Deliveries ==="]
182
+ lines << "delivery_method: #{data[:delivery_method]}"
183
+ lines << "observable: #{data[:observable] ? true : false}"
184
+ unless data[:observable]
185
+ lines << "Note: deliveries is only populated when delivery_method is :test. " \
186
+ "An empty list does NOT prove no mail was sent."
187
+ end
188
+ lines << "total captured: #{data[:total]}"
189
+
190
+ deliveries = data[:deliveries] || []
191
+ if deliveries.empty?
192
+ lines << "\n(no deliveries captured)"
193
+ return lines.join("\n")
194
+ end
195
+
196
+ deliveries.each do |m|
197
+ lines << ""
198
+ lines << "## [#{m[:index]}] #{m[:subject]}"
199
+ lines << "from: #{m[:from]}"
200
+ lines << "to: #{m[:to]}"
201
+ lines << "cc: #{m[:cc]}" unless m[:cc].to_s.empty?
202
+ lines << "bcc: #{m[:bcc]}" unless m[:bcc].to_s.empty?
203
+ lines << "multipart: #{m[:multipart]}"
204
+ if (atts = m[:attachments]) && !atts.empty?
205
+ names = atts.map { |a| "#{a[:filename]} (#{a[:content_type]})" }.join(", ")
206
+ lines << "attachments: #{names}"
207
+ end
208
+ preview = m[:body_preview].to_s
209
+ trunc = m[:body_truncated] ? " [truncated]" : ""
210
+ lines << "body: #{preview}#{trunc}"
211
+ end
212
+
213
+ lines.join("\n")
214
+ end
215
+
216
+ def text_response(text)
217
+ MCP::Tool::Response.new([{ type: "text", text: text }])
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require_relative "../rails_helper"
5
+ require_relative "../notifications_subscriber"
6
+ require_relative "../event_formatter"
7
+
8
+ module DebugMcp
9
+ module Tools
10
+ # Read recent Rails internal events (SQL, render, cache, job enqueue,
11
+ # request lifecycle) from the in-process Notifications buffer, independent
12
+ # of trigger_request.
13
+ #
14
+ # IMPORTANT semantics, surfaced in every response so the model does not
15
+ # mistake "empty" for "nothing happened":
16
+ # - forward-only: only events fired AFTER the subscriber was installed are
17
+ # visible. The first call installs the subscriber, so it usually returns
18
+ # little or nothing — that is not evidence that no SQL/jobs ran.
19
+ # - paused-only: the target must be paused at a debugger prompt (we send
20
+ # commands over the debug socket).
21
+ # - NOT read-only: installing the subscriber subscribes to
22
+ # ActiveSupport::Notifications inside the target — an in-process
23
+ # instrumentation side effect (no application data is written).
24
+ class RailsRecentEvents < MCP::Tool
25
+ description "[Investigation] Show recent Rails internal events (SQL, renders, cache, job " \
26
+ "enqueues, request lifecycle) captured from the running process, without needing " \
27
+ "trigger_request. FORWARD-ONLY: the first call installs an ActiveSupport::Notifications " \
28
+ "subscriber and only events fired AFTER that are visible — an empty result is NOT proof " \
29
+ "that nothing happened. Requires the process to be paused (set a breakpoint / use " \
30
+ "trigger_request first on threaded servers). Installing the subscriber is an in-process " \
31
+ "instrumentation side effect, so this tool is not strictly read-only."
32
+
33
+ annotations(
34
+ title: "Rails Recent Events",
35
+ # Not read-only: installs an in-process Notifications subscriber.
36
+ read_only_hint: false,
37
+ destructive_hint: false,
38
+ open_world_hint: false,
39
+ )
40
+
41
+ DEFAULT_LIMIT = 50
42
+ MAX_LIMIT = 500
43
+
44
+ input_schema(
45
+ properties: {
46
+ kinds: {
47
+ type: "array",
48
+ items: { type: "string", enum: %w[sql render cache job request] },
49
+ description: "Filter to these event kinds (default: all). " \
50
+ "One or more of: sql, render, cache, job, request.",
51
+ },
52
+ limit: {
53
+ type: "integer",
54
+ description: "How many of the most recent buffered events to scan (default: #{DEFAULT_LIMIT}).",
55
+ },
56
+ after_seq: {
57
+ type: "integer",
58
+ description: "Cursor: return only events with seq greater than this. Use the previous " \
59
+ "response's newest_seq to page forward. Clock-independent (preferred over time).",
60
+ },
61
+ include_debug_eval: {
62
+ type: "boolean",
63
+ description: "Include events caused by debug-mcp's own evaluate_code/inspect_object calls " \
64
+ "(default: false).",
65
+ },
66
+ session_id: {
67
+ type: "string",
68
+ description: "Debug session ID (uses default session if omitted)",
69
+ },
70
+ },
71
+ )
72
+
73
+ KIND_MATCHERS = {
74
+ "sql" => ->(name) { name == "sql.active_record" },
75
+ "render" => ->(name) { name.start_with?("render_") },
76
+ "cache" => ->(name) { name.start_with?("cache_") },
77
+ "job" => ->(name) { name == "enqueue.active_job" },
78
+ "request" => ->(name) { name.end_with?(".action_controller") },
79
+ }.freeze
80
+
81
+ class << self
82
+ def call(kinds: nil, limit: nil, after_seq: nil, include_debug_eval: false, session_id: nil,
83
+ server_context:)
84
+ client = server_context[:session_manager].client(session_id)
85
+ client.auto_repause!
86
+ RailsHelper.require_rails!(client)
87
+
88
+ # Reads and install both send commands over the debug socket and both
89
+ # take the buffer/Notifications mutex, which fails in trap context.
90
+ if RailsHelper.trap_context?(client)
91
+ return text_response("Rails recent events: unavailable in trap context.\n\n" \
92
+ "#{RailsHelper::TRAP_CONTEXT_HINT}")
93
+ end
94
+
95
+ installed = NotificationsSubscriber.install(client)
96
+ unless installed
97
+ return text_response("Rails recent events: could not install the Notifications subscriber " \
98
+ "in the target process.\n\n#{RailsHelper::TRAP_CONTEXT_HINT}")
99
+ end
100
+
101
+ limit = resolve_limit(limit)
102
+ events = fetch_events(client, after_seq: after_seq, limit: limit)
103
+ events = filter_by_kinds(events, kinds)
104
+ metadata = NotificationsSubscriber.metadata(client)
105
+
106
+ # `installed` is the result of the install we just performed — the only
107
+ # trustworthy signal here. metadata[:installed] can be missing if the
108
+ # metadata round-trip itself returned no parseable object.
109
+ text_response(build_output(events, metadata, kinds, include_debug_eval, limit, installed))
110
+ rescue DebugMcp::Error => e
111
+ text_response("Error: #{e.message}")
112
+ end
113
+
114
+ private
115
+
116
+ def resolve_limit(limit)
117
+ n = limit.to_i
118
+ n = DEFAULT_LIMIT unless n.positive?
119
+ [n, MAX_LIMIT].min
120
+ end
121
+
122
+ def fetch_events(client, after_seq:, limit:)
123
+ if after_seq
124
+ # Forward cursor paging: oldest-after-cursor first, capped by limit
125
+ # (target-side) so a busy buffer never dumps all 1000 events.
126
+ NotificationsSubscriber.fetch_after_seq(client, after_seq.to_i, limit)
127
+ else
128
+ NotificationsSubscriber.fetch_last(client, limit)
129
+ end
130
+ end
131
+
132
+ def filter_by_kinds(events, kinds)
133
+ return events if kinds.nil? || kinds.empty?
134
+
135
+ matchers = kinds.filter_map { |k| KIND_MATCHERS[k.to_s] }
136
+ # All requested kinds were invalid: return nothing rather than silently
137
+ # falling back to every event (which would expose more, not less).
138
+ return [] if matchers.empty?
139
+
140
+ events.select { |e| matchers.any? { |m| m.call(e[:name].to_s) } }
141
+ end
142
+
143
+ def build_output(events, metadata, kinds, include_debug_eval, limit, installed)
144
+ header = build_metadata_header(metadata, kinds, include_debug_eval, installed)
145
+ # Align EventFormatter's per-section display caps with the user's limit
146
+ # so the "... and N more (limit=...)" note matches what was requested
147
+ # instead of EventFormatter's internal defaults.
148
+ limits = EventFormatter::DEFAULT_LIMITS.merge(
149
+ sql: limit, render: limit, cache: limit, job: limit,
150
+ )
151
+ formatted = EventFormatter.format(events, limits: limits, include_debug_eval: include_debug_eval)
152
+
153
+ body = if formatted.nil? || formatted.empty?
154
+ "(no matching events captured since the subscriber was installed)"
155
+ else
156
+ formatted
157
+ end
158
+
159
+ "#{header}\n\n#{body}"
160
+ end
161
+
162
+ def build_metadata_header(metadata, kinds, include_debug_eval, installed)
163
+ lines = ["=== Rails Recent Events ==="]
164
+ lines << "installed: #{installed ? true : false}"
165
+ lines << "forward_only: true (only events fired after install are visible)"
166
+ lines << "paused_only: true"
167
+ lines << "events_before_install_are_unavailable: true"
168
+ lines << "installed_at: #{metadata[:installed_at] || "(unknown)"}"
169
+ if metadata[:buffer_size]
170
+ lines << "buffer: #{metadata[:buffer_size]}/#{metadata[:buffer_max]} " \
171
+ "(dropped: #{metadata[:dropped_count] || 0})"
172
+ end
173
+ if metadata[:oldest_seq] || metadata[:newest_seq]
174
+ lines << "seq range: #{metadata[:oldest_seq] || "-"}..#{metadata[:newest_seq] || "-"}"
175
+ end
176
+ lines << "filtered_kinds: #{kinds && !kinds.empty? ? kinds.join(", ") : "all"}"
177
+ lines << "include_debug_eval: #{include_debug_eval ? true : false}"
178
+ lines.join("\n")
179
+ end
180
+
181
+ def text_response(text)
182
+ MCP::Tool::Response.new([{ type: "text", text: text }])
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugMcp
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debug-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rira100000000
@@ -65,6 +65,26 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: activesupport
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.0'
75
+ - - "<"
76
+ - !ruby/object:Gem::Version
77
+ version: '9.0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '7.0'
85
+ - - "<"
86
+ - !ruby/object:Gem::Version
87
+ version: '9.0'
68
88
  - !ruby/object:Gem::Dependency
69
89
  name: rake
70
90
  requirement: !ruby/object:Gem::Requirement
@@ -109,85 +129,6 @@ files:
109
129
  - LICENSE
110
130
  - README.ja.md
111
131
  - README.md
112
- - examples/01_simple_bug.rb
113
- - examples/02_data_pipeline.rb
114
- - examples/03_recursion.rb
115
- - examples/RAILS_SCENARIOS.md
116
- - examples/SCENARIOS.md
117
- - examples/rails_test_app/setup.sh
118
- - examples/rails_test_app/testapp/.dockerignore
119
- - examples/rails_test_app/testapp/.ruby-version
120
- - examples/rails_test_app/testapp/Dockerfile
121
- - examples/rails_test_app/testapp/Gemfile
122
- - examples/rails_test_app/testapp/README.md
123
- - examples/rails_test_app/testapp/Rakefile
124
- - examples/rails_test_app/testapp/app/assets/images/.keep
125
- - examples/rails_test_app/testapp/app/assets/stylesheets/application.css
126
- - examples/rails_test_app/testapp/app/controllers/application_controller.rb
127
- - examples/rails_test_app/testapp/app/controllers/concerns/.keep
128
- - examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb
129
- - examples/rails_test_app/testapp/app/controllers/health_controller.rb
130
- - examples/rails_test_app/testapp/app/controllers/orders_controller.rb
131
- - examples/rails_test_app/testapp/app/controllers/posts_controller.rb
132
- - examples/rails_test_app/testapp/app/controllers/sessions_controller.rb
133
- - examples/rails_test_app/testapp/app/controllers/users_controller.rb
134
- - examples/rails_test_app/testapp/app/helpers/application_helper.rb
135
- - examples/rails_test_app/testapp/app/models/application_record.rb
136
- - examples/rails_test_app/testapp/app/models/comment.rb
137
- - examples/rails_test_app/testapp/app/models/concerns/.keep
138
- - examples/rails_test_app/testapp/app/models/order.rb
139
- - examples/rails_test_app/testapp/app/models/order_item.rb
140
- - examples/rails_test_app/testapp/app/models/post.rb
141
- - examples/rails_test_app/testapp/app/models/user.rb
142
- - examples/rails_test_app/testapp/app/services/order_report_service.rb
143
- - examples/rails_test_app/testapp/app/views/layouts/application.html.erb
144
- - examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb
145
- - examples/rails_test_app/testapp/app/views/pwa/service-worker.js
146
- - examples/rails_test_app/testapp/bin/ci
147
- - examples/rails_test_app/testapp/bin/dev
148
- - examples/rails_test_app/testapp/bin/rails
149
- - examples/rails_test_app/testapp/bin/rake
150
- - examples/rails_test_app/testapp/bin/setup
151
- - examples/rails_test_app/testapp/config.ru
152
- - examples/rails_test_app/testapp/config/application.rb
153
- - examples/rails_test_app/testapp/config/boot.rb
154
- - examples/rails_test_app/testapp/config/ci.rb
155
- - examples/rails_test_app/testapp/config/database.yml
156
- - examples/rails_test_app/testapp/config/environment.rb
157
- - examples/rails_test_app/testapp/config/environments/development.rb
158
- - examples/rails_test_app/testapp/config/environments/production.rb
159
- - examples/rails_test_app/testapp/config/environments/test.rb
160
- - examples/rails_test_app/testapp/config/initializers/content_security_policy.rb
161
- - examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb
162
- - examples/rails_test_app/testapp/config/initializers/inflections.rb
163
- - examples/rails_test_app/testapp/config/locales/en.yml
164
- - examples/rails_test_app/testapp/config/puma.rb
165
- - examples/rails_test_app/testapp/config/routes.rb
166
- - examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb
167
- - examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb
168
- - examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb
169
- - examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb
170
- - examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb
171
- - examples/rails_test_app/testapp/db/schema.rb
172
- - examples/rails_test_app/testapp/db/seeds.rb
173
- - examples/rails_test_app/testapp/docker-compose.yml
174
- - examples/rails_test_app/testapp/docker-entrypoint.sh
175
- - examples/rails_test_app/testapp/lib/tasks/.keep
176
- - examples/rails_test_app/testapp/log/.keep
177
- - examples/rails_test_app/testapp/public/400.html
178
- - examples/rails_test_app/testapp/public/404.html
179
- - examples/rails_test_app/testapp/public/406-unsupported-browser.html
180
- - examples/rails_test_app/testapp/public/422.html
181
- - examples/rails_test_app/testapp/public/500.html
182
- - examples/rails_test_app/testapp/public/icon.png
183
- - examples/rails_test_app/testapp/public/icon.svg
184
- - examples/rails_test_app/testapp/public/robots.txt
185
- - examples/rails_test_app/testapp/script/.keep
186
- - examples/rails_test_app/testapp/storage/.keep
187
- - examples/rails_test_app/testapp/tmp/.keep
188
- - examples/rails_test_app/testapp/tmp/pids/.keep
189
- - examples/rails_test_app/testapp/tmp/storage/.keep
190
- - examples/rails_test_app/testapp/vendor/.keep
191
132
  - exe/debug-mcp
192
133
  - exe/debug-rails
193
134
  - lib/debug_mcp.rb
@@ -217,7 +158,9 @@ files:
217
158
  - lib/debug_mcp/tools/list_paused_sessions.rb
218
159
  - lib/debug_mcp/tools/next.rb
219
160
  - lib/debug_mcp/tools/rails_info.rb
161
+ - lib/debug_mcp/tools/rails_mail_deliveries.rb
220
162
  - lib/debug_mcp/tools/rails_model.rb
163
+ - lib/debug_mcp/tools/rails_recent_events.rb
221
164
  - lib/debug_mcp/tools/rails_routes.rb
222
165
  - lib/debug_mcp/tools/read_file.rb
223
166
  - lib/debug_mcp/tools/remove_breakpoint.rb
@@ -1,43 +0,0 @@
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)}円"
@@ -1,93 +0,0 @@
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]}"