debug-mcp 0.1.2

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +83 -0
  4. data/LICENSE +21 -0
  5. data/README.ja.md +383 -0
  6. data/README.md +384 -0
  7. data/examples/01_simple_bug.rb +43 -0
  8. data/examples/02_data_pipeline.rb +93 -0
  9. data/examples/03_recursion.rb +96 -0
  10. data/examples/RAILS_SCENARIOS.md +350 -0
  11. data/examples/SCENARIOS.md +142 -0
  12. data/examples/rails_test_app/setup.sh +428 -0
  13. data/examples/rails_test_app/testapp/.dockerignore +10 -0
  14. data/examples/rails_test_app/testapp/.ruby-version +1 -0
  15. data/examples/rails_test_app/testapp/Dockerfile +23 -0
  16. data/examples/rails_test_app/testapp/Gemfile +17 -0
  17. data/examples/rails_test_app/testapp/README.md +65 -0
  18. data/examples/rails_test_app/testapp/Rakefile +6 -0
  19. data/examples/rails_test_app/testapp/app/assets/images/.keep +0 -0
  20. data/examples/rails_test_app/testapp/app/assets/stylesheets/application.css +1 -0
  21. data/examples/rails_test_app/testapp/app/controllers/application_controller.rb +4 -0
  22. data/examples/rails_test_app/testapp/app/controllers/concerns/.keep +0 -0
  23. data/examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb +38 -0
  24. data/examples/rails_test_app/testapp/app/controllers/health_controller.rb +11 -0
  25. data/examples/rails_test_app/testapp/app/controllers/orders_controller.rb +100 -0
  26. data/examples/rails_test_app/testapp/app/controllers/posts_controller.rb +82 -0
  27. data/examples/rails_test_app/testapp/app/controllers/sessions_controller.rb +25 -0
  28. data/examples/rails_test_app/testapp/app/controllers/users_controller.rb +44 -0
  29. data/examples/rails_test_app/testapp/app/helpers/application_helper.rb +2 -0
  30. data/examples/rails_test_app/testapp/app/models/application_record.rb +3 -0
  31. data/examples/rails_test_app/testapp/app/models/comment.rb +8 -0
  32. data/examples/rails_test_app/testapp/app/models/concerns/.keep +0 -0
  33. data/examples/rails_test_app/testapp/app/models/order.rb +56 -0
  34. data/examples/rails_test_app/testapp/app/models/order_item.rb +16 -0
  35. data/examples/rails_test_app/testapp/app/models/post.rb +29 -0
  36. data/examples/rails_test_app/testapp/app/models/user.rb +34 -0
  37. data/examples/rails_test_app/testapp/app/services/order_report_service.rb +40 -0
  38. data/examples/rails_test_app/testapp/app/views/layouts/application.html.erb +28 -0
  39. data/examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb +22 -0
  40. data/examples/rails_test_app/testapp/app/views/pwa/service-worker.js +26 -0
  41. data/examples/rails_test_app/testapp/bin/ci +6 -0
  42. data/examples/rails_test_app/testapp/bin/dev +2 -0
  43. data/examples/rails_test_app/testapp/bin/rails +4 -0
  44. data/examples/rails_test_app/testapp/bin/rake +4 -0
  45. data/examples/rails_test_app/testapp/bin/setup +35 -0
  46. data/examples/rails_test_app/testapp/config/application.rb +42 -0
  47. data/examples/rails_test_app/testapp/config/boot.rb +3 -0
  48. data/examples/rails_test_app/testapp/config/ci.rb +14 -0
  49. data/examples/rails_test_app/testapp/config/database.yml +32 -0
  50. data/examples/rails_test_app/testapp/config/environment.rb +5 -0
  51. data/examples/rails_test_app/testapp/config/environments/development.rb +54 -0
  52. data/examples/rails_test_app/testapp/config/environments/production.rb +67 -0
  53. data/examples/rails_test_app/testapp/config/environments/test.rb +42 -0
  54. data/examples/rails_test_app/testapp/config/initializers/content_security_policy.rb +29 -0
  55. data/examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb +8 -0
  56. data/examples/rails_test_app/testapp/config/initializers/inflections.rb +16 -0
  57. data/examples/rails_test_app/testapp/config/locales/en.yml +31 -0
  58. data/examples/rails_test_app/testapp/config/puma.rb +39 -0
  59. data/examples/rails_test_app/testapp/config/routes.rb +34 -0
  60. data/examples/rails_test_app/testapp/config.ru +6 -0
  61. data/examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb +12 -0
  62. data/examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb +13 -0
  63. data/examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb +11 -0
  64. data/examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb +14 -0
  65. data/examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb +13 -0
  66. data/examples/rails_test_app/testapp/db/schema.rb +71 -0
  67. data/examples/rails_test_app/testapp/db/seeds.rb +85 -0
  68. data/examples/rails_test_app/testapp/docker-compose.yml +21 -0
  69. data/examples/rails_test_app/testapp/docker-entrypoint.sh +10 -0
  70. data/examples/rails_test_app/testapp/lib/tasks/.keep +0 -0
  71. data/examples/rails_test_app/testapp/log/.keep +0 -0
  72. data/examples/rails_test_app/testapp/public/400.html +135 -0
  73. data/examples/rails_test_app/testapp/public/404.html +135 -0
  74. data/examples/rails_test_app/testapp/public/406-unsupported-browser.html +135 -0
  75. data/examples/rails_test_app/testapp/public/422.html +135 -0
  76. data/examples/rails_test_app/testapp/public/500.html +135 -0
  77. data/examples/rails_test_app/testapp/public/icon.png +0 -0
  78. data/examples/rails_test_app/testapp/public/icon.svg +3 -0
  79. data/examples/rails_test_app/testapp/public/robots.txt +1 -0
  80. data/examples/rails_test_app/testapp/script/.keep +0 -0
  81. data/examples/rails_test_app/testapp/storage/.keep +0 -0
  82. data/examples/rails_test_app/testapp/tmp/.keep +0 -0
  83. data/examples/rails_test_app/testapp/tmp/pids/.keep +0 -0
  84. data/examples/rails_test_app/testapp/tmp/storage/.keep +0 -0
  85. data/examples/rails_test_app/testapp/vendor/.keep +0 -0
  86. data/exe/debug-mcp +39 -0
  87. data/exe/debug-rails +127 -0
  88. data/lib/debug_mcp/client_cleanup.rb +102 -0
  89. data/lib/debug_mcp/code_safety_analyzer.rb +124 -0
  90. data/lib/debug_mcp/debug_client.rb +1143 -0
  91. data/lib/debug_mcp/exit_message_builder.rb +112 -0
  92. data/lib/debug_mcp/pending_http_helper.rb +25 -0
  93. data/lib/debug_mcp/rails_helper.rb +155 -0
  94. data/lib/debug_mcp/server.rb +364 -0
  95. data/lib/debug_mcp/session_manager.rb +436 -0
  96. data/lib/debug_mcp/stop_event_annotator.rb +152 -0
  97. data/lib/debug_mcp/tcp_session_discovery.rb +226 -0
  98. data/lib/debug_mcp/tools/connect.rb +669 -0
  99. data/lib/debug_mcp/tools/continue_execution.rb +161 -0
  100. data/lib/debug_mcp/tools/disconnect.rb +169 -0
  101. data/lib/debug_mcp/tools/evaluate_code.rb +354 -0
  102. data/lib/debug_mcp/tools/finish.rb +84 -0
  103. data/lib/debug_mcp/tools/get_context.rb +217 -0
  104. data/lib/debug_mcp/tools/get_source.rb +193 -0
  105. data/lib/debug_mcp/tools/inspect_object.rb +107 -0
  106. data/lib/debug_mcp/tools/list_debug_sessions.rb +60 -0
  107. data/lib/debug_mcp/tools/list_files.rb +189 -0
  108. data/lib/debug_mcp/tools/list_paused_sessions.rb +108 -0
  109. data/lib/debug_mcp/tools/next.rb +70 -0
  110. data/lib/debug_mcp/tools/rails_info.rb +200 -0
  111. data/lib/debug_mcp/tools/rails_model.rb +362 -0
  112. data/lib/debug_mcp/tools/rails_routes.rb +186 -0
  113. data/lib/debug_mcp/tools/read_file.rb +214 -0
  114. data/lib/debug_mcp/tools/remove_breakpoint.rb +173 -0
  115. data/lib/debug_mcp/tools/run_debug_command.rb +55 -0
  116. data/lib/debug_mcp/tools/run_script.rb +293 -0
  117. data/lib/debug_mcp/tools/set_breakpoint.rb +206 -0
  118. data/lib/debug_mcp/tools/step.rb +67 -0
  119. data/lib/debug_mcp/tools/trigger_request.rb +515 -0
  120. data/lib/debug_mcp/version.rb +5 -0
  121. data/lib/debug_mcp.rb +40 -0
  122. metadata +251 -0
@@ -0,0 +1,135 @@
1
+ <!doctype html>
2
+
3
+ <html lang="en">
4
+
5
+ <head>
6
+
7
+ <title>We're sorry, but something went wrong (500 Internal Server Error)</title>
8
+
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="initial-scale=1, width=device-width">
11
+ <meta name="robots" content="noindex, nofollow">
12
+
13
+ <style>
14
+
15
+ *, *::before, *::after {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ }
22
+
23
+ html {
24
+ font-size: 16px;
25
+ }
26
+
27
+ body {
28
+ background: #FFF;
29
+ color: #261B23;
30
+ display: grid;
31
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
32
+ font-size: clamp(1rem, 2.5vw, 2rem);
33
+ -webkit-font-smoothing: antialiased;
34
+ font-style: normal;
35
+ font-weight: 400;
36
+ letter-spacing: -0.0025em;
37
+ line-height: 1.4;
38
+ min-height: 100dvh;
39
+ place-items: center;
40
+ text-rendering: optimizeLegibility;
41
+ -webkit-text-size-adjust: 100%;
42
+ }
43
+
44
+ #error-description {
45
+ fill: #d30001;
46
+ }
47
+
48
+ #error-id {
49
+ fill: #f0eff0;
50
+ }
51
+
52
+ @media (prefers-color-scheme: dark) {
53
+ body {
54
+ background: #101010;
55
+ color: #e0e0e0;
56
+ }
57
+
58
+ #error-description {
59
+ fill: #FF6161;
60
+ }
61
+
62
+ #error-id {
63
+ fill: #2c2c2c;
64
+ }
65
+ }
66
+
67
+ a {
68
+ color: inherit;
69
+ font-weight: 700;
70
+ text-decoration: underline;
71
+ text-underline-offset: 0.0925em;
72
+ }
73
+
74
+ b, strong {
75
+ font-weight: 700;
76
+ }
77
+
78
+ i, em {
79
+ font-style: italic;
80
+ }
81
+
82
+ main {
83
+ display: grid;
84
+ gap: 1em;
85
+ padding: 2em;
86
+ place-items: center;
87
+ text-align: center;
88
+ }
89
+
90
+ main header {
91
+ width: min(100%, 12em);
92
+ }
93
+
94
+ main header svg {
95
+ height: auto;
96
+ max-width: 100%;
97
+ width: 100%;
98
+ }
99
+
100
+ main article {
101
+ width: min(100%, 30em);
102
+ }
103
+
104
+ main article p {
105
+ font-size: 75%;
106
+ }
107
+
108
+ main article br {
109
+ display: none;
110
+
111
+ @media(min-width: 48em) {
112
+ display: inline;
113
+ }
114
+ }
115
+
116
+ </style>
117
+
118
+ </head>
119
+
120
+ <body>
121
+
122
+ <!-- This file lives in public/500.html -->
123
+
124
+ <main>
125
+ <header>
126
+ <svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m101.23 93.8427c-8.1103 0-15.4098 3.7849-19.7354 8.3813h-36.2269v-99.21891h103.8143v37.03791h-68.3984v24.8722c5.1366-2.7035 15.1396-5.9477 24.6014-5.9477 35.146 0 56.233 22.7094 56.233 55.4215 0 34.605-23.791 57.315-60.558 57.315-37.8492 0-61.64-22.169-63.8028-55.963h42.9857c1.0814 10.814 9.1919 19.195 21.6281 19.195 11.355 0 19.465-8.381 19.465-20.547 0-11.625-7.299-20.5463-20.006-20.5463zm138.833 77.8613c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" id="error-id"/><path d="m23.1377 68.9967v34.0033h-8.9162v-34.0033zm4.3157 34.0033v-24.921h8.6947v2.1598c1.3845-1.5506 3.8212-2.7136 6.701-2.7136 5.538 0 8.8054 3.5997 8.8054 9.1377v16.3371h-8.6393v-14.2327c0-2.049-1.0522-3.5443-3.2674-3.5443-1.7168 0-3.1567.9969-3.5997 2.7136v15.0634zm29.9913-8.5839v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.5839v6.8671h5.2058v6.7564h-5.2058v8.307c0 1.9383.9415 2.769 2.6583 2.769.9414 0 1.9937-.2216 2.769-.5538v7.3654c-.9969.443-2.8798.775-4.8181.775-5.8703 0-9.1931-2.769-9.1931-9.0819zm32.3666-.1108h8.0301c-.8861 5.7597-5.2057 9.2487-11.6852 9.2487-7.6424 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.3165-13.0143 12.5159-13.0143 7.6424 0 11.9621 5.095 11.9621 12.5159v2.1598h-16.1156c.2769 2.9905 1.8275 4.5965 4.3196 4.5965 1.7722 0 3.1567-.7753 3.6551-2.4921zm-3.8212-10.0237c-2.0491 0-3.4336 1.2737-3.9874 3.5997h7.5317c-.1107-2.0491-1.3845-3.5997-3.5443-3.5997zm31.4299-6.3134v8.3624c-1.052-.5538-2.215-.7753-3.599-.7753-2.382 0-3.988 1.0522-4.431 2.8244v14.6203h-8.694v-24.921h8.694v2.2152c1.219-1.6614 3.157-2.769 5.649-2.769 1.108 0 1.994.2215 2.381.443zm2.949 25.0318v-24.921h8.694v2.1598c1.385-1.5506 3.821-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.64v-14.2327c0-2.049-1.052-3.5443-3.267-3.5443-1.717 0-3.157.9969-3.6 2.7136v15.0634zm50.371 0h-8.363v-1.274c-.83.831-3.323 1.717-5.981 1.717-4.929 0-9.082-2.769-9.082-8.0301 0-4.818 4.153-7.9193 9.581-7.9193 2.049 0 4.485.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.046-2.9905-1.606 0-2.547.7199-2.935 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.433.7199-3.433 2.3813 0 1.7168 1.716 2.4367 3.433 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm20.742-29.0191v35.997h-8.694v-35.997zm13.036 25.9178h9.248c.72 2.326 2.714 3.489 5.483 3.489 2.713 0 4.596-1.163 4.596-3.2674 0-1.6061-1.052-2.326-3.212-2.8244l-6.534-1.3845c-4.985-1.1076-8.751-3.7105-8.751-9.47 0-6.6456 5.538-11.0206 13.07-11.0206 8.307 0 13.014 4.5411 13.956 10.4114h-8.695c-.72-1.8829-2.27-3.3228-5.205-3.3228-2.548 0-4.265 1.1076-4.265 2.9905 0 1.4953 1.052 2.326 2.825 2.7137l6.645 1.5506c5.815 1.3845 9.027 4.5412 9.027 9.8023 0 6.9778-5.87 10.9654-13.291 10.9654-8.141 0-13.679-3.9322-14.897-10.6332zm46.509 1.3845h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm31.431-6.3134v8.3624c-1.053-.5538-2.216-.7753-3.6-.7753-2.381 0-3.988 1.0522-4.431 2.8244v14.6203h-8.694v-24.921h8.694v2.2152c1.219-1.6614 3.157-2.769 5.649-2.769 1.108 0 1.994.2215 2.382.443zm18.288 25.0318h-7.809l-9.47-24.921h8.861l4.763 14.288 4.652-14.288h8.528zm25.614-8.6947h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997zm31.43-6.3134v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.157-2.769 5.649-2.769 1.107 0 1.993.2215 2.381.443zm13.703-8.9715h24.312v7.6424h-15.562v5.3165h14.232v7.4763h-14.232v5.8703h15.562v7.6978h-24.312zm44.667 8.9715v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.156-2.769 5.648-2.769 1.108 0 1.994.2215 2.382.443zm19.673 0v8.3624c-1.053-.5538-2.216-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.156-2.769 5.648-2.769 1.108 0 1.994.2215 2.382.443zm26.769 12.5713c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm28.082-12.5713v8.3624c-1.052-.5538-2.215-.7753-3.6-.7753-2.381 0-3.987 1.0522-4.43 2.8244v14.6203h-8.695v-24.921h8.695v2.2152c1.218-1.6614 3.157-2.769 5.649-2.769 1.107 0 1.993.2215 2.381.443z" id="error-description"/></svg>
127
+ </header>
128
+ <article>
129
+ <p><strong>We're sorry, but something went wrong.</strong><br> If you're the application owner check the logs for more information.</p>
130
+ </article>
131
+ </main>
132
+
133
+ </body>
134
+
135
+ </html>
@@ -0,0 +1,3 @@
1
+ <svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
2
+ <circle cx="256" cy="256" r="256" fill="red"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/exe/debug-mcp ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "debug_mcp"
5
+ require "optparse"
6
+
7
+ options = {}
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: debug-mcp [options]"
11
+
12
+ opts.on("-t", "--transport TRANSPORT", %w[stdio http], "Transport type: stdio (default) or http") do |t|
13
+ options[:transport] = t
14
+ end
15
+
16
+ opts.on("-p", "--port PORT", Integer, "HTTP port (default: 6029, only for http transport)") do |p|
17
+ options[:port] = p
18
+ end
19
+
20
+ opts.on("--host HOST", "HTTP host (default: 127.0.0.1, only for http transport)") do |h|
21
+ options[:host] = h
22
+ end
23
+
24
+ opts.on("--session-timeout SECONDS", Integer, "Session timeout in seconds (default: 1800)") do |t|
25
+ options[:session_timeout] = t
26
+ end
27
+
28
+ opts.on("-v", "--version", "Show version") do
29
+ puts "debug-mcp #{DebugMcp::VERSION}"
30
+ exit
31
+ end
32
+
33
+ opts.on("-h", "--help", "Show this help") do
34
+ puts opts
35
+ exit
36
+ end
37
+ end.parse!
38
+
39
+ DebugMcp::Server.new(**options).start
data/exe/debug-rails ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ class DebugRails
5
+ DEFAULT_DEBUG_PORT = 12321
6
+
7
+ def self.parse_args(argv)
8
+ debug_port = nil
9
+ show_help = false
10
+
11
+ i = 0
12
+ while i < argv.length
13
+ case argv[i]
14
+ when "--debug-port"
15
+ i += 1
16
+ raise ArgumentError, "--debug-port requires a port number" if i >= argv.length
17
+ debug_port = parse_port(argv[i])
18
+ i += 1
19
+ when /\A--debug-port=(.+)\z/
20
+ debug_port = parse_port($1)
21
+ i += 1
22
+ when "-h", "--help"
23
+ show_help = true
24
+ i += 1
25
+ else
26
+ break
27
+ end
28
+ end
29
+
30
+ rails_args = argv[i..] || []
31
+
32
+ if rails_args.empty? || rails_args.first.start_with?("-")
33
+ rails_args.unshift("server")
34
+ end
35
+
36
+ { debug_port: debug_port, rails_args: rails_args, show_help: show_help }
37
+ end
38
+
39
+ def self.parse_port(value)
40
+ port = Integer(value)
41
+ raise ArgumentError unless port.positive?
42
+ port
43
+ rescue ArgumentError, TypeError
44
+ raise ArgumentError, "--debug-port requires a valid port number, got: #{value.inspect}"
45
+ end
46
+
47
+ def self.build_env(debug_port:, docker:)
48
+ if docker
49
+ {
50
+ "RUBY_DEBUG_HOST" => "0.0.0.0",
51
+ "RUBY_DEBUG_PORT" => (debug_port || DEFAULT_DEBUG_PORT).to_s,
52
+ }
53
+ else
54
+ env = { "RUBY_DEBUG_OPEN" => "true" }
55
+ env["RUBY_DEBUG_PORT"] = debug_port.to_s if debug_port
56
+ env
57
+ end
58
+ end
59
+
60
+ def self.docker?
61
+ File.exist?("/.dockerenv")
62
+ end
63
+
64
+ def self.print_help(io = $stdout)
65
+ io.puts <<~HELP
66
+ Usage: debug-rails [options] [rails-command] [rails-options]
67
+
68
+ Launch a Rails server with Ruby debug gem enabled for debug-mcp debugging.
69
+
70
+ Options:
71
+ --debug-port PORT Debug port for TCP connection (default: #{DEFAULT_DEBUG_PORT} in Docker)
72
+ -h, --help Show this help
73
+
74
+ Examples:
75
+ debug-rails # rails server with debug enabled
76
+ debug-rails s -p 4000 # rails server on port 4000
77
+ debug-rails --debug-port 3333 # use TCP debug port 3333
78
+ debug-rails console # rails console with debug enabled
79
+
80
+ Environment:
81
+ Outside Docker: Sets RUBY_DEBUG_OPEN=true
82
+ Inside Docker: Sets RUBY_DEBUG_HOST=0.0.0.0, RUBY_DEBUG_PORT=PORT
83
+ HELP
84
+ end
85
+
86
+ def self.run(argv = ARGV)
87
+ parsed = parse_args(argv)
88
+
89
+ if parsed[:show_help]
90
+ print_help
91
+ exit 0
92
+ end
93
+
94
+ unless File.exist?("bin/rails")
95
+ $stderr.puts "Error: bin/rails not found in current directory."
96
+ $stderr.puts "Run this command from your Rails application root."
97
+ exit 1
98
+ end
99
+
100
+ env = build_env(debug_port: parsed[:debug_port], docker: docker?)
101
+ pid = spawn(env, "bin/rails", *parsed[:rails_args])
102
+
103
+ force_quit_deadline = nil
104
+
105
+ Signal.trap("INT") do
106
+ if force_quit_deadline && Time.now < force_quit_deadline
107
+ Process.kill("KILL", pid)
108
+ else
109
+ Process.kill("TERM", pid)
110
+ $stderr.write "\nStopping... Press Ctrl+C again within 3s to force quit\n"
111
+ force_quit_deadline = Time.now + 3
112
+ end
113
+ end
114
+
115
+ Signal.trap("TERM") do
116
+ Process.kill("TERM", pid)
117
+ end
118
+
119
+ _, status = Process.waitpid2(pid)
120
+ exit(status.exitstatus || 1)
121
+ rescue ArgumentError => e
122
+ $stderr.puts "Error: #{e.message}"
123
+ exit 1
124
+ end
125
+ end
126
+
127
+ DebugRails.run unless defined?(RSpec)
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebugMcp
4
+ # Shared cleanup logic for graceful disconnect.
5
+ # Used by Disconnect tool and SessionManager reaper to avoid ~100 lines of duplication.
6
+ # Performs: stdout restore, breakpoint deletion, SIGINT handler restore,
7
+ # process resume, and stale pause defense.
8
+ module ClientCleanup
9
+ # Perform best-effort cleanup and resume a paused debug client.
10
+ # The client MUST be paused before calling this method.
11
+ # @param client [DebugClient] the client to clean up
12
+ # @param deadline [Time] hard deadline for all cleanup operations
13
+ # @param max_stale_retries [Integer] max retries for stale pause defense
14
+ def self.cleanup_and_resume(client, deadline:, max_stale_retries: 2)
15
+ # Restore $stdout if evaluate_code left it redirected (its ensure block
16
+ # fails when send_command timeout sets @paused=false).
17
+ remaining = deadline - Time.now
18
+ if remaining > 0
19
+ begin
20
+ client.send_command(
21
+ '$stdout = STDOUT if $stdout != STDOUT',
22
+ timeout: [remaining, 1].min,
23
+ )
24
+ rescue DebugMcp::Error
25
+ # Best-effort
26
+ end
27
+ end
28
+
29
+ # Delete all breakpoints FIRST — this is the most critical step.
30
+ # If breakpoints remain and the process resumes without a client,
31
+ # it will hit a breakpoint and become stuck with no way to continue.
32
+ delete_all_breakpoints(client, deadline)
33
+
34
+ # Restore original SIGINT handler
35
+ remaining = deadline - Time.now
36
+ if remaining > 0
37
+ begin
38
+ client.send_command(
39
+ "p $_debug_mcp_orig_int ? (trap('INT',$_debug_mcp_orig_int);$_debug_mcp_orig_int=nil;:ok) : nil",
40
+ timeout: [remaining, 2].min,
41
+ )
42
+ rescue DebugMcp::Error
43
+ # Best-effort
44
+ end
45
+ end
46
+
47
+ # Resume the process. If a cleanup command timed out (setting
48
+ # @paused=false even though the process is actually still paused),
49
+ # use force: true to bypass the @paused check.
50
+ client.send_command_no_wait("c", force: true)
51
+
52
+ # Wait for the debug gem to settle after 'c'. After SIGINT recovery,
53
+ # the main thread needs to finish the interrupted eval and re-enter
54
+ # the command loop (sending `input PID`). If we close the socket
55
+ # before this completes, the debug gem's cleanup_reader closes @q_msg
56
+ # while the main thread is still pushing results, leaving it stuck
57
+ # on a futex. Draining here gives the debug gem time to settle.
58
+ client.ensure_paused(timeout: 2)
59
+
60
+ # Stale pause defense: after 'c' → ensure_paused, the process might
61
+ # have been re-paused by a stale `pause` message left in the debug
62
+ # gem's socket buffer. If still paused, delete remaining BPs and
63
+ # send 'c' again (bounded retries to prevent infinite loop).
64
+ stale_retries = 0
65
+ while client.paused && stale_retries < max_stale_retries
66
+ stale_retries += 1
67
+ remaining = deadline - Time.now
68
+ break if remaining <= 0
69
+
70
+ delete_all_breakpoints(client, deadline)
71
+
72
+ remaining = deadline - Time.now
73
+ break if remaining <= 0
74
+
75
+ client.send_command_no_wait("c", force: true)
76
+ client.ensure_paused(timeout: [remaining, 1].min)
77
+ end
78
+ end
79
+
80
+ # Delete all breakpoints from the debug session.
81
+ # @param client [DebugClient] the client
82
+ # @param deadline [Time] hard deadline
83
+ def self.delete_all_breakpoints(client, deadline)
84
+ remaining = deadline - Time.now
85
+ return if remaining <= 0
86
+
87
+ bp_output = client.send_command("info breakpoints", timeout: [remaining, 2].min)
88
+ return if bp_output.strip.empty?
89
+
90
+ bp_output.each_line do |line|
91
+ remaining = deadline - Time.now
92
+ break if remaining <= 0
93
+
94
+ if (match = line.match(/#(\d+)/))
95
+ client.send_command("delete #{match[1]}", timeout: [remaining, 2].min) rescue nil
96
+ end
97
+ end
98
+ rescue DebugMcp::Error
99
+ # Best-effort
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebugMcp
4
+ module CodeSafetyAnalyzer
5
+ DANGEROUS_PATTERNS = {
6
+ file_operations: [
7
+ [/\bFile\s*\.\s*(write|delete|unlink|rename|chmod|chown)\b/, "File.write/delete/unlink/rename"],
8
+ [/\bFileUtils\b/, "FileUtils"],
9
+ [/\bIO\s*\.\s*(write|binwrite)\b/, "IO.write"],
10
+ [/\bDir\s*\.\s*(mkdir|rmdir|delete|unlink)\b/, "Dir.mkdir/rmdir"],
11
+ ],
12
+ system_commands: [
13
+ [/\bsystem\s*\(/, "system()"],
14
+ [/\bexec\s*\(/, "exec()"],
15
+ [/\bspawn\s*\(/, "spawn()"],
16
+ [/`[^`]+`/, "backtick command"],
17
+ [/%x\{/, "%x{}"],
18
+ [/%x\[/, "%x[]"],
19
+ [/%x\(/, "%x()"],
20
+ [/\bOpen3\b/, "Open3"],
21
+ [/\bIO\s*\.\s*popen\b/, "IO.popen"],
22
+ ],
23
+ process_manipulation: [
24
+ [/\bProcess\s*\.\s*(kill|fork|exit)\b/, "Process.kill/fork/exit"],
25
+ [/\bfork\s*[\s({]/, "fork"],
26
+ [/\bexit!/, "exit!"],
27
+ [/\babort\b/, "abort"],
28
+ ],
29
+ network_operations: [
30
+ [/\bNet::HTTP\b/, "Net::HTTP"],
31
+ [/\bTCPSocket\b/, "TCPSocket"],
32
+ [/\bUDPSocket\b/, "UDPSocket"],
33
+ [/\bFaraday\b/, "Faraday"],
34
+ [/\bHTTParty\b/, "HTTParty"],
35
+ [/\bopen-uri\b/, "open-uri"],
36
+ [/\bURI\s*\.\s*open\b/, "URI.open"],
37
+ [/\bRestClient\b/, "RestClient"],
38
+ ],
39
+ destructive_data: [
40
+ [/\.destroy_all\b/, ".destroy_all"],
41
+ [/\.delete_all\b/, ".delete_all"],
42
+ [/\.update_all\b/, ".update_all"],
43
+ [/\b(DROP|TRUNCATE)\s+(TABLE|DATABASE)\b/i, "DROP/TRUNCATE SQL"],
44
+ ],
45
+ mutation_operations: [
46
+ [/\.save!/, ".save!"],
47
+ [/\.save\b(?![!?])/, ".save"],
48
+ [/\.update![\s(]/, ".update!"],
49
+ [/\.update[\s(]/, ".update"],
50
+ [/\.create![\s(]/, ".create!"],
51
+ [/\.create[\s(]/, ".create"],
52
+ [/\.destroy!/, ".destroy!"],
53
+ [/\.destroy\b(?![_!])/, ".destroy"],
54
+ [/\.touch\b/, ".touch"],
55
+ [/\.increment!/, ".increment!"],
56
+ [/\.decrement!/, ".decrement!"],
57
+ [/\.toggle!/, ".toggle!"],
58
+ ],
59
+ }.freeze
60
+
61
+ # Filter out warnings whose categories have been acknowledged.
62
+ def self.filter_acknowledged(warnings, acknowledged_categories)
63
+ return warnings if acknowledged_categories.nil? || acknowledged_categories.empty?
64
+
65
+ warnings.reject { |w| acknowledged_categories.include?(w[:category]) }
66
+ end
67
+
68
+ # Analyze code for dangerous patterns.
69
+ # Returns an array of warnings: [{ category:, label:, matches: }]
70
+ def self.analyze(code)
71
+ warnings = []
72
+
73
+ DANGEROUS_PATTERNS.each do |category, patterns|
74
+ matches = []
75
+ patterns.each do |regexp, label|
76
+ matches << label if code.match?(regexp)
77
+ end
78
+ next if matches.empty?
79
+
80
+ warnings << { category: category, matches: matches }
81
+ end
82
+
83
+ warnings
84
+ end
85
+
86
+ CATEGORY_LABELS = {
87
+ file_operations: "File system operations",
88
+ system_commands: "System command execution",
89
+ process_manipulation: "Process manipulation",
90
+ network_operations: "Network operations",
91
+ destructive_data: "Destructive data operations",
92
+ mutation_operations: "Data mutation (modifies database records)",
93
+ }.freeze
94
+
95
+ # Format warnings into human-readable text.
96
+ # Returns nil if no warnings.
97
+ # Uses a compact "Note:" format when only mutation_operations are present,
98
+ # and a verbose "WARNING:" format when other categories are involved.
99
+ def self.format_warnings(warnings)
100
+ return nil if warnings.empty?
101
+
102
+ # Compact format for mutation-only warnings
103
+ if warnings.all? { |w| w[:category] == :mutation_operations }
104
+ matches = warnings.flat_map { |w| w[:matches] }
105
+ return "Note: Data mutation detected (#{matches.join(", ")}). " \
106
+ "Use acknowledge_mutations to suppress this notice."
107
+ end
108
+
109
+ # Verbose format for dangerous operations
110
+ lines = []
111
+ lines << "WARNING: Potentially dangerous operations detected in code."
112
+ lines << "evaluate_code should only be used for investigating runtime state."
113
+ lines << "Use the agent's own tools for file/system/network operations."
114
+ lines << ""
115
+
116
+ warnings.each do |w|
117
+ label = CATEGORY_LABELS[w[:category]] || w[:category].to_s
118
+ lines << " #{label}: #{w[:matches].join(", ")}"
119
+ end
120
+
121
+ lines.join("\n")
122
+ end
123
+ end
124
+ end