rails-active-mcp 2.0.7 → 2.0.12

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +141 -0
  3. data/README.md +3 -3
  4. data/examples/rails_app_integration.md +405 -0
  5. data/exe/rails-active-mcp-server +8 -8
  6. data/gemfiles/rails_6.0.gemfile +34 -0
  7. data/gemfiles/rails_6.1.gemfile +34 -0
  8. data/gemfiles/rails_7.0.gemfile +34 -0
  9. data/gemfiles/rails_7.1.gemfile +34 -0
  10. data/gemfiles/rails_7.2.gemfile +34 -0
  11. data/lib/generators/rails_active_mcp/install/install_generator.rb +44 -94
  12. data/lib/generators/rails_active_mcp/install/templates/README.md +134 -54
  13. data/lib/generators/rails_active_mcp/install/templates/initializer.rb +44 -6
  14. data/lib/generators/rails_active_mcp/install/templates/rails-active-mcp-server +104 -0
  15. data/lib/generators/rails_active_mcp/install/templates/rails-active-mcp-wrapper +94 -0
  16. data/lib/rails_active_mcp/configuration.rb +44 -4
  17. data/lib/rails_active_mcp/console_executor.rb +185 -83
  18. data/lib/rails_active_mcp/engine.rb +2 -2
  19. data/lib/rails_active_mcp/garbage_collection_utils.rb +13 -0
  20. data/lib/rails_active_mcp/safety_checker.rb +17 -7
  21. data/lib/rails_active_mcp/sdk/server.rb +1 -1
  22. data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +0 -2
  23. data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +0 -2
  24. data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +1 -3
  25. data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +0 -2
  26. data/lib/rails_active_mcp/tasks.rake +238 -82
  27. data/lib/rails_active_mcp/version.rb +1 -1
  28. data/lib/rails_active_mcp.rb +4 -3
  29. data/mcp.ru +2 -2
  30. data/rails_active_mcp.gemspec +62 -14
  31. metadata +72 -16
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Rails Active MCP Wrapper Script
4
+ # This script ensures Rails Active MCP works reliably across different Ruby version managers
5
+
6
+ # Function to detect Ruby version manager
7
+ detect_ruby_manager() {
8
+ if command -v asdf >/dev/null 2>&1 && [ -f .tool-versions ]; then
9
+ echo "asdf"
10
+ elif command -v rbenv >/dev/null 2>&1 && [ -f .ruby-version ]; then
11
+ echo "rbenv"
12
+ elif command -v rvm >/dev/null 2>&1 && [ -f .rvmrc ]; then
13
+ echo "rvm"
14
+ else
15
+ echo "system"
16
+ fi
17
+ }
18
+
19
+ # Function to setup environment for different Ruby managers
20
+ setup_ruby_environment() {
21
+ local manager=$1
22
+
23
+ case $manager in
24
+ "asdf")
25
+ if [ -n "$ASDF_DIR" ] && [ -f "$ASDF_DIR/asdf.sh" ]; then
26
+ source "$ASDF_DIR/asdf.sh"
27
+ elif [ -f "$HOME/.asdf/asdf.sh" ]; then
28
+ export ASDF_DIR="$HOME/.asdf"
29
+ source "$HOME/.asdf/asdf.sh"
30
+ fi
31
+ ;;
32
+ "rbenv")
33
+ if [ -n "$RBENV_ROOT" ] && [ -f "$RBENV_ROOT/bin/rbenv" ]; then
34
+ export PATH="$RBENV_ROOT/bin:$PATH"
35
+ eval "$(rbenv init -)"
36
+ elif [ -f "$HOME/.rbenv/bin/rbenv" ]; then
37
+ export PATH="$HOME/.rbenv/bin:$PATH"
38
+ eval "$(rbenv init -)"
39
+ fi
40
+ ;;
41
+ "rvm")
42
+ if [ -f "$HOME/.rvm/scripts/rvm" ]; then
43
+ source "$HOME/.rvm/scripts/rvm"
44
+ fi
45
+ ;;
46
+ esac
47
+ }
48
+
49
+ # Get the directory where this script is located
50
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
52
+
53
+ # Change to the Rails project directory
54
+ cd "$PROJECT_ROOT" || {
55
+ echo "Error: Cannot change to project root directory: $PROJECT_ROOT" >&2
56
+ exit 1
57
+ }
58
+
59
+ # Detect and setup Ruby environment
60
+ RUBY_MANAGER=$(detect_ruby_manager)
61
+ setup_ruby_environment "$RUBY_MANAGER"
62
+
63
+ # Verify we have the necessary files
64
+ if [ ! -f "Gemfile" ]; then
65
+ echo "Error: Gemfile not found in $PROJECT_ROOT" >&2
66
+ echo "Please ensure you're running this from a Rails application root" >&2
67
+ exit 1
68
+ fi
69
+
70
+ if [ ! -f "config/environment.rb" ]; then
71
+ echo "Warning: config/environment.rb not found. This may not be a Rails application." >&2
72
+ fi
73
+
74
+ # Set default environment if not specified
75
+ export RAILS_ENV="${RAILS_ENV:-development}"
76
+
77
+ # Ensure we have the rails-active-mcp gem available
78
+ if ! bundle list | grep -q "rails-active-mcp"; then
79
+ echo "Error: rails-active-mcp gem not found in bundle" >&2
80
+ echo "Please run: bundle install" >&2
81
+ exit 1
82
+ fi
83
+
84
+ # Execute the Rails Active MCP server
85
+ # Try bundle exec first, then fall back to direct execution
86
+ if bundle exec rails-active-mcp-server "$@" 2>/dev/null; then
87
+ exit 0
88
+ elif command -v rails-active-mcp-server >/dev/null 2>&1; then
89
+ exec rails-active-mcp-server "$@"
90
+ else
91
+ echo "Error: Cannot find rails-active-mcp-server executable" >&2
92
+ echo "Please ensure the rails-active-mcp gem is properly installed" >&2
93
+ exit 1
94
+ fi
@@ -8,7 +8,7 @@ module RailsActiveMcp
8
8
  attr_accessor :allowed_commands, :command_timeout, :enable_logging, :log_level
9
9
 
10
10
  # Safety and execution options
11
- attr_accessor :safe_mode, :default_timeout, :max_results, :log_executions, :audit_file
11
+ attr_accessor :safe_mode, :max_results, :log_executions, :audit_file, :enabled
12
12
  attr_accessor :custom_safety_patterns, :allowed_models
13
13
 
14
14
  def initialize
@@ -24,12 +24,12 @@ module RailsActiveMcp
24
24
 
25
25
  # Safety and execution defaults
26
26
  @safe_mode = true
27
- @default_timeout = 30
28
27
  @max_results = 100
29
28
  @log_executions = false
30
29
  @audit_file = nil
31
30
  @custom_safety_patterns = []
32
31
  @allowed_models = []
32
+ @enabled = true
33
33
  end
34
34
 
35
35
  def model_allowed?(model_name)
@@ -44,15 +44,55 @@ module RailsActiveMcp
44
44
  [true, false].include?(enable_logging) &&
45
45
  %i[debug info warn error].include?(log_level) &&
46
46
  [true, false].include?(safe_mode) &&
47
- default_timeout.is_a?(Numeric) && default_timeout > 0 &&
48
47
  max_results.is_a?(Numeric) && max_results > 0 &&
49
48
  [true, false].include?(log_executions) &&
50
49
  custom_safety_patterns.is_a?(Array) &&
51
- allowed_models.is_a?(Array)
50
+ allowed_models.is_a?(Array) &&
51
+ [true, false].include?(enabled)
52
+ end
53
+
54
+ def validate?
55
+ raise ArgumentError, 'allowed_commands must be an array' unless allowed_commands.is_a?(Array)
56
+
57
+ raise ArgumentError, 'command_timeout must be positive' unless command_timeout.is_a?(Numeric) && command_timeout > 0
58
+
59
+ raise ArgumentError, 'log_level must be one of: debug, info, warn, error' unless %i[debug info warn error].include?(log_level)
60
+
61
+ raise ArgumentError, 'safe_mode must be a boolean' unless [true, false].include?(safe_mode)
62
+
63
+ raise ArgumentError, 'max_results must be positive' unless max_results.is_a?(Numeric) && max_results > 0
64
+
65
+ raise ArgumentError, 'enabled must be a boolean' unless [true, false].include?(enabled)
66
+
67
+ true
52
68
  end
53
69
 
54
70
  def reset!
55
71
  initialize
56
72
  end
73
+
74
+ # Environment-specific configuration presets
75
+ def production_mode!
76
+ @safe_mode = true
77
+ @log_level = :warn
78
+ @command_timeout = 15
79
+ @max_results = 50
80
+ @log_executions = true
81
+ end
82
+
83
+ def development_mode!
84
+ @safe_mode = false
85
+ @log_level = :debug
86
+ @command_timeout = 60
87
+ @max_results = 200
88
+ @log_executions = false
89
+ end
90
+
91
+ def test_mode!
92
+ @safe_mode = true
93
+ @log_level = :error
94
+ @command_timeout = 30
95
+ @log_executions = false
96
+ end
57
97
  end
58
98
  end
@@ -4,9 +4,11 @@ require 'concurrent-ruby'
4
4
  require 'rails'
5
5
 
6
6
  module RailsActiveMcp
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class ConsoleExecutor
8
9
  # Thread-safe execution errors
9
10
  class ExecutionError < StandardError; end
11
+
10
12
  class ThreadSafetyError < StandardError; end
11
13
 
12
14
  def initialize(config)
@@ -17,13 +19,13 @@ module RailsActiveMcp
17
19
  end
18
20
 
19
21
  def execute(code, timeout: nil, safe_mode: nil, capture_output: true)
20
- timeout ||= @config.default_timeout
22
+ timeout ||= @config.command_timeout
21
23
  safe_mode = @config.safe_mode if safe_mode.nil?
22
24
 
23
25
  # Pre-execution safety check
24
26
  if safe_mode
25
27
  safety_analysis = @safety_checker.analyze(code)
26
- raise SafetyError, "Code failed safety check: #{safety_analysis[:summary]}" unless safety_analysis[:safe]
28
+ raise RailsActiveMcp::SafetyError, "Code failed safety check: #{safety_analysis[:summary]}" unless safety_analysis[:safe]
27
29
  end
28
30
 
29
31
  # Log execution if enabled
@@ -34,41 +36,59 @@ module RailsActiveMcp
34
36
 
35
37
  # Post-execution processing
36
38
  process_result(result)
39
+ rescue RailsActiveMcp::SafetyError => e
40
+ {
41
+ success: false,
42
+ error: e.message,
43
+ error_class: 'SafetyError',
44
+ code: code
45
+ }
37
46
  end
38
47
 
39
48
  def execute_safe_query(model:, method:, args: [], limit: nil)
40
49
  limit ||= @config.max_results
41
50
 
42
- # Validate model access
43
- raise SafetyError, "Access to model '#{model}' is not allowed" unless @config.model_allowed?(model)
51
+ begin
52
+ # Validate model access
53
+ raise RailsActiveMcp::SafetyError, "Access to model '#{model}' is not allowed" unless @config.model_allowed?(model)
44
54
 
45
- # Validate method safety
46
- raise SafetyError, "Method '#{method}' is not allowed for safe queries" unless safe_query_method?(method)
55
+ # Validate method safety
56
+ raise RailsActiveMcp::SafetyError, "Method '#{method}' is not allowed for safe queries" unless safe_query_method?(method)
47
57
 
48
- # Execute with proper Rails executor and connection management
49
- execute_with_rails_executor_and_connection do
50
- model_class = model.to_s.constantize
58
+ # Execute with proper Rails executor and connection management
59
+ execute_with_rails_executor_and_connection do
60
+ model_class = model.to_s.constantize
51
61
 
52
- # Build and execute query
53
- query = if args.empty?
54
- model_class.public_send(method)
55
- else
56
- model_class.public_send(method, *args)
57
- end
62
+ # Build and execute query
63
+ query = if args.empty?
64
+ model_class.public_send(method)
65
+ else
66
+ model_class.public_send(method, *args)
67
+ end
58
68
 
59
- # Apply limit for enumerable results
60
- query = query.limit(limit) if query.respond_to?(:limit) && !count_method?(method)
69
+ # Apply limit for enumerable results
70
+ query = query.limit(limit) if query.respond_to?(:limit) && !count_method?(method)
61
71
 
62
- result = execute_query_with_timeout(query)
72
+ result = execute_query_with_timeout(query)
63
73
 
74
+ {
75
+ success: true,
76
+ model: model,
77
+ method: method,
78
+ args: args,
79
+ result: serialize_result(result),
80
+ count: calculate_count(result),
81
+ executed_at: Time.now
82
+ }
83
+ end
84
+ rescue RailsActiveMcp::SafetyError => e
64
85
  {
65
- success: true,
86
+ success: false,
87
+ error: e.message,
88
+ error_class: 'SafetyError',
66
89
  model: model,
67
90
  method: method,
68
- args: args,
69
- result: serialize_result(result),
70
- count: calculate_count(result),
71
- executed_at: Time.now
91
+ args: args
72
92
  }
73
93
  rescue StandardError => e
74
94
  log_error(e, { model: model, method: method, args: args })
@@ -83,6 +103,86 @@ module RailsActiveMcp
83
103
  end
84
104
  end
85
105
 
106
+ def get_model_info(model_name)
107
+ # Validate model access
108
+ raise RailsActiveMcp::SafetyError, "Access to model '#{model_name}' is not allowed" unless @config.model_allowed?(model_name)
109
+
110
+ begin
111
+ model_class = model_name.to_s.constantize
112
+
113
+ # Ensure it's an ActiveRecord model
114
+ unless model_class.respond_to?(:columns) && model_class.respond_to?(:reflect_on_all_associations)
115
+ raise RailsActiveMcp::SafetyError, "#{model_name} is not a valid ActiveRecord model"
116
+ end
117
+
118
+ # Extract model information
119
+ columns_info = build_model_columns(model_class)
120
+
121
+ associations_info = build_model_reflections(model_class)
122
+
123
+ validators_info = build_model_validator_info(model_class)
124
+
125
+ {
126
+ success: true,
127
+ model_name: model_name,
128
+ table_name: model_class.table_name,
129
+ primary_key: model_class.primary_key,
130
+ columns: columns_info,
131
+ associations: associations_info,
132
+ validators: validators_info,
133
+ extracted_at: Time.now
134
+ }
135
+ rescue NameError => e
136
+ {
137
+ success: false,
138
+ error: "Model '#{model_name}' not found: #{e.message}",
139
+ error_class: 'NameError',
140
+ model_name: model_name
141
+ }
142
+ rescue StandardError => e
143
+ {
144
+ success: false,
145
+ error: e.message,
146
+ error_class: e.class.name,
147
+ model_name: model_name
148
+ }
149
+ end
150
+ end
151
+
152
+ def build_model_reflections(model_class)
153
+ model_class.reflect_on_all_associations.map do |association|
154
+ {
155
+ name: association.name,
156
+ type: association.macro,
157
+ class_name: association.class_name
158
+ }
159
+ end
160
+ end
161
+
162
+ def build_model_columns(model_class)
163
+ model_class.columns.map do |column|
164
+ {
165
+ name: column.name,
166
+ type: column.type,
167
+ primary: column.name == model_class.primary_key
168
+ }
169
+ end
170
+ end
171
+
172
+ def build_model_validator_info(model_class)
173
+ if model_class.respond_to?(:validators)
174
+ model_class.validators.map do |validator|
175
+ {
176
+ type: validator.class.name,
177
+ attributes: validator.attributes,
178
+ options: validator.options
179
+ }
180
+ end
181
+ else
182
+ []
183
+ end
184
+ end
185
+
86
186
  def dry_run(code)
87
187
  # Analyze without executing
88
188
  safety_analysis = @safety_checker.analyze(code)
@@ -98,6 +198,21 @@ module RailsActiveMcp
98
198
 
99
199
  private
100
200
 
201
+ def safe_query_method?(method)
202
+ # Define safe read-only methods for database queries
203
+ safe_methods = %w[
204
+ find find_by find_by! first last
205
+ where select limit offset order
206
+ count size length exists? empty?
207
+ pluck ids maximum minimum average sum
208
+ group having joins includes
209
+ readonly distinct unscope
210
+ all none
211
+ ]
212
+
213
+ safe_methods.include?(method.to_s)
214
+ end
215
+
101
216
  def execute_with_rails_executor(code, timeout, capture_output)
102
217
  if defined?(Rails) && Rails.application
103
218
  # Handle development mode reloading if needed
@@ -129,20 +244,15 @@ module RailsActiveMcp
129
244
 
130
245
  # Manage ActiveRecord connection pool properly
131
246
  def execute_with_connection_pool(code, timeout, capture_output)
132
- if defined?(ActiveRecord::Base)
133
- ActiveRecord::Base.connection_pool.with_connection do
247
+ if defined?(::ActiveRecord::Base)
248
+ ::ActiveRecord::Base.connection_pool.with_connection do
134
249
  execute_with_timeout(code, timeout, capture_output)
135
250
  end
136
251
  else
137
252
  execute_with_timeout(code, timeout, capture_output)
138
253
  end
139
254
  ensure
140
- # Clean up connections to prevent pool exhaustion
141
- if defined?(ActiveRecord::Base)
142
- ActiveRecord::Base.clear_active_connections!
143
- # Probabilistic garbage collection for long-running processes
144
- GC.start if rand(100) < 5
145
- end
255
+ RailsActiveMcp::GarbageCollectionUtils.probalistic_clean!
146
256
  end
147
257
 
148
258
  # Helper method for safe queries with proper Rails executor and connection management
@@ -151,8 +261,8 @@ module RailsActiveMcp
151
261
  if defined?(ActiveSupport::Dependencies) && ActiveSupport::Dependencies.respond_to?(:interlock)
152
262
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
153
263
  Rails.application.executor.wrap do
154
- if defined?(ActiveRecord::Base)
155
- ActiveRecord::Base.connection_pool.with_connection(&block)
264
+ if defined?(::ActiveRecord::Base)
265
+ ::ActiveRecord::Base.connection_pool.with_connection(&block)
156
266
  else
157
267
  yield
158
268
  end
@@ -160,8 +270,8 @@ module RailsActiveMcp
160
270
  end
161
271
  else
162
272
  Rails.application.executor.wrap do
163
- if defined?(ActiveRecord::Base)
164
- ActiveRecord::Base.connection_pool.with_connection(&block)
273
+ if defined?(::ActiveRecord::Base)
274
+ ::ActiveRecord::Base.connection_pool.with_connection(&block)
165
275
  else
166
276
  yield
167
277
  end
@@ -171,12 +281,7 @@ module RailsActiveMcp
171
281
  yield
172
282
  end
173
283
  ensure
174
- # Clean up connections
175
- if defined?(ActiveRecord::Base)
176
- ActiveRecord::Base.clear_active_connections!
177
- # Probabilistic garbage collection for long-running processes
178
- GC.start if rand(100) < 5
179
- end
284
+ RailsActiveMcp::GarbageCollectionUtils.probalistic_clean!
180
285
  end
181
286
 
182
287
  def execute_with_timeout(code, timeout, capture_output)
@@ -208,32 +313,39 @@ module RailsActiveMcp
208
313
 
209
314
  # Execute code
210
315
  start_time = Time.now
211
- return_value = binding_context.eval(code)
212
- execution_time = Time.now - start_time
213
-
316
+ begin
317
+ return_value = binding_context.eval(code)
318
+ rescue SyntaxError => e
319
+ return {
320
+ success: false,
321
+ error: "Syntax Error: #{e.message}",
322
+ error_class: 'SyntaxError',
323
+ backtrace: e.backtrace&.first(10),
324
+ code: code,
325
+ output: nil
326
+ }
327
+ end
214
328
  output = captured_output.string
215
329
  errors = captured_errors.string
216
330
 
217
331
  # Combine output and errors for comprehensive result
218
332
  combined_output = [output, errors].reject(&:empty?).join("\n")
219
-
220
333
  {
221
334
  success: true,
222
335
  return_value: return_value,
223
336
  output: combined_output,
224
337
  return_value_string: safe_inspect(return_value),
225
- execution_time: execution_time,
338
+ execution_time: Time.now - start_time,
226
339
  code: code
227
340
  }
228
341
  rescue StandardError => e
229
- execution_time = Time.now - start_time if defined?(start_time)
342
+ execution_time = Time.now - start_time if start_time.present?
230
343
  errors = captured_errors.string
231
-
232
344
  {
233
345
  success: false,
234
- error: e.message,
346
+ error: e&.message || 'Unknown error',
235
347
  error_class: e.class.name,
236
- backtrace: e.backtrace&.first(10),
348
+ backtrace: e&.backtrace&.first(10) || [],
237
349
  execution_time: execution_time,
238
350
  code: code,
239
351
  stderr: errors.empty? ? nil : errors
@@ -260,7 +372,7 @@ module RailsActiveMcp
260
372
  code: code
261
373
  }
262
374
  rescue StandardError => e
263
- execution_time = Time.now - start_time if defined?(start_time)
375
+ execution_time = Time.now - start_time if start_time.present?
264
376
 
265
377
  {
266
378
  success: false,
@@ -273,8 +385,8 @@ module RailsActiveMcp
273
385
  end
274
386
 
275
387
  def execute_query_with_timeout(query)
276
- Timeout.timeout(@config.default_timeout) do
277
- if query.is_a?(ActiveRecord::Relation)
388
+ Timeout.timeout(@config.command_timeout) do
389
+ if defined?(::ActiveRecord::Relation) && query.is_a?(::ActiveRecord::Relation)
278
390
  query.to_a
279
391
  else
280
392
  query
@@ -315,15 +427,15 @@ module RailsActiveMcp
315
427
 
316
428
  # Add common console helpers (thread-safe)
317
429
  def sql(query)
318
- raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
430
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(::ActiveRecord::Base)
319
431
 
320
- ActiveRecord::Base.connection.select_all(query).to_a
432
+ ::ActiveRecord::Base.connection.select_all(query).to_a
321
433
  end
322
434
 
323
435
  def schema(table_name)
324
- raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
436
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(::ActiveRecord::Base)
325
437
 
326
- ActiveRecord::Base.connection.columns(table_name)
438
+ ::ActiveRecord::Base.connection.columns(table_name)
327
439
  end
328
440
  end
329
441
 
@@ -336,40 +448,28 @@ module RailsActiveMcp
336
448
  create_thread_safe_console_binding
337
449
  end
338
450
 
339
- def safe_query_method?(method)
340
- safe_methods = %w[
341
- find find_by find_each find_in_batches
342
- where all first last take
343
- count sum average maximum minimum size length
344
- pluck ids exists? empty? any? many?
345
- select distinct group order limit offset
346
- includes joins left_joins preload eager_load
347
- to_a to_sql explain inspect as_json to_json
348
- attributes attribute_names column_names
349
- model_name table_name primary_key
350
- ]
351
- safe_methods.include?(method.to_s)
352
- end
353
-
354
451
  def count_method?(method)
355
452
  %w[count sum average maximum minimum size length].include?(method.to_s)
356
453
  end
357
454
 
358
455
  def serialize_result(result)
359
456
  case result
360
- when ActiveRecord::Base
361
- result.attributes.merge(_model_class: result.class.name)
362
457
  when Array
363
458
  limited_result = result.first(@config.max_results)
364
459
  limited_result.map { |item| serialize_result(item) }
365
- when ActiveRecord::Relation
366
- serialize_result(result.to_a)
367
460
  when Hash
368
461
  result
369
462
  when Numeric, String, TrueClass, FalseClass, NilClass
370
463
  result
371
464
  else
372
- safe_inspect(result)
465
+ # Handle ActiveRecord objects if available
466
+ if defined?(::ActiveRecord::Base) && result.is_a?(::ActiveRecord::Base)
467
+ result.attributes.merge(_model_class: result.class.name)
468
+ elsif defined?(::ActiveRecord::Relation) && result.is_a?(::ActiveRecord::Relation)
469
+ serialize_result(result.to_a)
470
+ else
471
+ safe_inspect(result)
472
+ end
373
473
  end
374
474
  end
375
475
 
@@ -377,12 +477,15 @@ module RailsActiveMcp
377
477
  case result
378
478
  when Array
379
479
  result.size
380
- when ActiveRecord::Relation
381
- result.count
382
480
  when Numeric
383
481
  1
384
482
  else
385
- 1
483
+ # Handle ActiveRecord::Relation if available
484
+ if defined?(::ActiveRecord::Relation) && result.is_a?(::ActiveRecord::Relation)
485
+ result.count
486
+ else
487
+ 1
488
+ end
386
489
  end
387
490
  end
388
491
 
@@ -430,9 +533,7 @@ module RailsActiveMcp
430
533
  end
431
534
  end
432
535
 
433
- unless safety_analysis[:read_only]
434
- recommendations << 'Consider using the safe_query tool for read-only operations'
435
- end
536
+ recommendations << 'Consider using the safe_query tool for read-only operations' unless safety_analysis[:read_only]
436
537
 
437
538
  recommendations
438
539
  end
@@ -499,4 +600,5 @@ module RailsActiveMcp
499
600
  RailsActiveMcp.logger.warn "Failed to reload in development: #{e.message}" if defined?(RailsActiveMcp.logger)
500
601
  end
501
602
  end
603
+ # rubocop:enable Metrics/ClassLength
502
604
  end
@@ -25,9 +25,9 @@ module RailsActiveMcp
25
25
  end
26
26
  else
27
27
  # Fallback to our own logger if Rails logger is not available
28
- Logger.new(STDERR).tap do |logger|
28
+ Logger.new($stderr).tap do |logger|
29
29
  logger.level = Rails.env.production? ? Logger::WARN : Logger::INFO
30
- logger.formatter = proc do |severity, datetime, progname, msg|
30
+ logger.formatter = proc do |severity, datetime, _progname, msg|
31
31
  "[#{datetime}] #{severity} -- RailsActiveMcp: #{msg}\n"
32
32
  end
33
33
  end
@@ -0,0 +1,13 @@
1
+ module RailsActiveMcp
2
+ class GarbageCollectionUtils
3
+ # Probabilistic garbage collection for long-running processes
4
+ # Not sure if this is the best approach but it's a quick common pattern that works
5
+ def self.probalistic_clean!
6
+ return unless defined?(::ActiveRecord::Base)
7
+
8
+ # Clean up connections to prevent pool exhaustion
9
+ ::ActiveRecord::Base.clear_active_connections!
10
+ GC.start if rand(100) < 5
11
+ end
12
+ end
13
+ end
@@ -54,7 +54,6 @@ module RailsActiveMcp
54
54
  end
55
55
 
56
56
  def safe?(code)
57
-
58
57
  analysis = analyze(code)
59
58
  analysis[:safe]
60
59
  end
@@ -85,8 +84,21 @@ module RailsActiveMcp
85
84
  critical_violations = violations.select { |v| v[:severity] == :critical }
86
85
  high_violations = violations.select { |v| v[:severity] == :high }
87
86
 
88
- safe = (@config.safe_mode && read_only && critical_violations.empty? && high_violations.empty?) ||
89
- (!@config.safe_mode && critical_violations.empty?)
87
+ # In safe mode: allow code with no critical/high violations
88
+ # For database operations, also require read_only patterns
89
+ # For simple Ruby code (no database patterns), allow if no dangerous violations
90
+ if @config.safe_mode
91
+ has_database_operations = code.match?(/\b(User|Model|ActiveRecord|\.where|\.find|\.create|\.update|\.delete)\b/)
92
+ safe = if has_database_operations
93
+ read_only && critical_violations.empty? && high_violations.empty?
94
+ else
95
+ # Simple Ruby code - just check for dangerous patterns
96
+ critical_violations.empty? && high_violations.empty?
97
+ end
98
+ else
99
+ # Not in safe mode - only block critical violations
100
+ safe = critical_violations.empty?
101
+ end
90
102
 
91
103
  {
92
104
  safe: safe,
@@ -136,10 +148,8 @@ module RailsActiveMcp
136
148
  read_only ? 'Code appears safe and read-only' : 'Code appears safe'
137
149
  else
138
150
  severity_counts = violations.group_by { |v| v[:severity] }.transform_values(&:count)
139
- parts = []
140
-
141
- severity_counts.each do |severity, count|
142
- parts << "#{count} #{severity} violation#{'s' if count > 1}"
151
+ parts = severity_counts.map do |severity, count|
152
+ "#{count} #{severity} violation#{'s' if count > 1}"
143
153
  end
144
154
 
145
155
  "Found #{parts.join(', ')}"