rails-active-mcp 0.1.6 → 2.0.7

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/README.md +106 -279
  3. data/changelog.md +69 -0
  4. data/claude_desktop_config.json +12 -0
  5. data/docs/DEBUGGING.md +40 -8
  6. data/docs/GENERATOR_TESTING.md +121 -0
  7. data/docs/README.md +130 -142
  8. data/exe/rails-active-mcp-server +176 -65
  9. data/lib/generators/rails_active_mcp/install/install_generator.rb +123 -3
  10. data/lib/generators/rails_active_mcp/install/templates/README.md +34 -128
  11. data/lib/generators/rails_active_mcp/install/templates/initializer.rb +37 -38
  12. data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -3
  13. data/lib/rails_active_mcp/configuration.rb +37 -98
  14. data/lib/rails_active_mcp/console_executor.rb +202 -78
  15. data/lib/rails_active_mcp/engine.rb +36 -8
  16. data/lib/rails_active_mcp/sdk/server.rb +183 -0
  17. data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +103 -0
  18. data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +73 -0
  19. data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +106 -0
  20. data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +77 -0
  21. data/lib/rails_active_mcp/version.rb +1 -1
  22. data/lib/rails_active_mcp.rb +10 -11
  23. data/rails_active_mcp.gemspec +8 -4
  24. metadata +43 -17
  25. data/lib/rails_active_mcp/mcp_server.rb +0 -374
  26. data/lib/rails_active_mcp/railtie.rb +0 -48
  27. data/lib/rails_active_mcp/stdio_server.rb +0 -467
  28. data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
  29. data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
  30. data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
  31. data/lib/rails_active_mcp/tools/safe_query_tool.rb +0 -41
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'config/environment'
4
- require 'rails_active_mcp'
4
+ require 'rails_active_mcp/sdk/server'
5
5
 
6
- # Run the Rails Active MCP server
7
- run RailsActiveMcp::McpServer.new
6
+ # Run the Rails Active MCP server using the official MCP Ruby SDK
7
+ # Note: This file is primarily for reference. The recommended way to run
8
+ # the server is using: bin/rails-active-mcp-server
9
+
10
+ server = RailsActiveMcp::SDK::Server.new
11
+ server.run
@@ -1,119 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
 
3
5
  module RailsActiveMcp
4
6
  class Configuration
5
- attr_accessor :enabled, :safe_mode, :default_timeout, :max_results,
6
- :allowed_models, :blocked_models, :custom_safety_patterns,
7
- :log_executions, :audit_file, :enable_mutation_tools,
8
- :require_confirmation_for, :execution_environment, :server_mode,
9
- :server_host, :server_port
7
+ # Core configuration options
8
+ attr_accessor :allowed_commands, :command_timeout, :enable_logging, :log_level
9
+
10
+ # Safety and execution options
11
+ attr_accessor :safe_mode, :default_timeout, :max_results, :log_executions, :audit_file
12
+ attr_accessor :custom_safety_patterns, :allowed_models
10
13
 
11
14
  def initialize
12
- @enabled = true
15
+ @allowed_commands = %w[
16
+ ls pwd cat head tail grep find wc
17
+ rails console rails runner
18
+ bundle exec rspec bundle exec test
19
+ git status git log git diff
20
+ ]
21
+ @command_timeout = 30
22
+ @enable_logging = true
23
+ @log_level = :info
24
+
25
+ # Safety and execution defaults
13
26
  @safe_mode = true
14
27
  @default_timeout = 30
15
28
  @max_results = 100
16
- @allowed_models = [] # Empty means all models allowed
17
- @blocked_models = []
29
+ @log_executions = false
30
+ @audit_file = nil
18
31
  @custom_safety_patterns = []
19
- @log_executions = true
20
- # Safe Rails.root access
21
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
22
- @audit_file = rails_root_join('log',
23
- 'rails_active_mcp.log')
24
- end
25
- @enable_mutation_tools = false
26
- @require_confirmation_for = %i[delete destroy update_all delete_all]
27
- @execution_environment = :current # :current, :sandbox, :readonly_replica
28
- @server_mode = :stdio # :stdio, :http
29
- @server_host = 'localhost'
30
- @server_port = 3001
31
- end
32
-
33
- # Safety configuration
34
- def strict_mode!
35
- @safe_mode = true
36
- @enable_mutation_tools = false
37
- @default_timeout = 15
38
- @max_results = 50
39
- end
40
-
41
- def permissive_mode!
42
- @safe_mode = false
43
- @enable_mutation_tools = true
44
- @default_timeout = 60
45
- @max_results = 1000
46
- end
47
-
48
- def production_mode!
49
- strict_mode!
50
- @execution_environment = :readonly_replica
51
- @log_executions = true
52
- @require_confirmation_for = %i[delete destroy update create save]
53
- end
54
-
55
- # Model access configuration
56
- def allow_models(*models)
57
- @allowed_models.concat(models.map(&:to_s))
58
- end
59
-
60
- def block_models(*models)
61
- @blocked_models.concat(models.map(&:to_s))
32
+ @allowed_models = []
62
33
  end
63
34
 
64
- def add_safety_pattern(pattern, description = nil)
65
- @custom_safety_patterns << { pattern: pattern, description: description }
66
- end
67
-
68
- # Server configuration
69
- def stdio_mode!
70
- @server_mode = :stdio
71
- end
72
-
73
- def http_mode!(host: 'localhost', port: 3001)
74
- @server_mode = :http
75
- @server_host = host
76
- @server_port = port
77
- end
78
-
79
- def server_mode_valid?
80
- %i[stdio http].include?(@server_mode)
81
- end
82
-
83
- # Validation
84
35
  def model_allowed?(model_name)
85
- model_str = model_name.to_s
86
-
87
- # Check if specifically blocked
88
- return false if @blocked_models.include?(model_str)
89
-
90
- # If allow list is empty, allow all (except blocked)
91
- return true if @allowed_models.empty?
36
+ return true if @allowed_models.empty? # Allow all if none specified
92
37
 
93
- # Check allow list
94
- @allowed_models.include?(model_str)
38
+ @allowed_models.include?(model_name.to_s)
95
39
  end
96
40
 
97
- def validate!
98
- raise ArgumentError, 'timeout must be positive' if @default_timeout <= 0
99
- raise ArgumentError, 'max_results must be positive' if @max_results <= 0
100
- raise ArgumentError, "invalid server_mode: #{@server_mode}" unless server_mode_valid?
101
- raise ArgumentError, 'server_port must be positive' if @server_port <= 0
102
-
103
- return unless defined?(Rails) && @audit_file
104
-
105
- audit_dir = File.dirname(@audit_file)
106
- FileUtils.mkdir_p(audit_dir) unless File.directory?(audit_dir)
41
+ def valid?
42
+ allowed_commands.is_a?(Array) &&
43
+ command_timeout.is_a?(Numeric) && command_timeout > 0 &&
44
+ [true, false].include?(enable_logging) &&
45
+ %i[debug info warn error].include?(log_level) &&
46
+ [true, false].include?(safe_mode) &&
47
+ default_timeout.is_a?(Numeric) && default_timeout > 0 &&
48
+ max_results.is_a?(Numeric) && max_results > 0 &&
49
+ [true, false].include?(log_executions) &&
50
+ custom_safety_patterns.is_a?(Array) &&
51
+ allowed_models.is_a?(Array)
107
52
  end
108
53
 
109
- private
110
-
111
- def rails_root_join(*args)
112
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
113
- Rails.root.join(*args)
114
- else
115
- File.join(Dir.pwd, *args)
116
- end
54
+ def reset!
55
+ initialize
117
56
  end
118
57
  end
119
58
  end
@@ -5,9 +5,15 @@ require 'rails'
5
5
 
6
6
  module RailsActiveMcp
7
7
  class ConsoleExecutor
8
+ # Thread-safe execution errors
9
+ class ExecutionError < StandardError; end
10
+ class ThreadSafetyError < StandardError; end
11
+
8
12
  def initialize(config)
9
13
  @config = config
10
14
  @safety_checker = SafetyChecker.new(config)
15
+ # Thread-safe mutex for critical sections
16
+ @execution_mutex = Mutex.new
11
17
  end
12
18
 
13
19
  def execute(code, timeout: nil, safe_mode: nil, capture_output: true)
@@ -17,16 +23,14 @@ module RailsActiveMcp
17
23
  # Pre-execution safety check
18
24
  if safe_mode
19
25
  safety_analysis = @safety_checker.analyze(code)
20
- unless safety_analysis[:safe]
21
- raise SafetyError, "Code failed safety check: #{safety_analysis[:summary]}"
22
- end
26
+ raise SafetyError, "Code failed safety check: #{safety_analysis[:summary]}" unless safety_analysis[:safe]
23
27
  end
24
28
 
25
29
  # Log execution if enabled
26
30
  log_execution(code) if @config.log_executions
27
31
 
28
- # Execute with timeout and output capture
29
- result = execute_with_timeout(code, timeout, capture_output)
32
+ # Execute with Rails 7.1 compatible thread safety
33
+ result = execute_with_rails_executor(code, timeout, capture_output)
30
34
 
31
35
  # Post-execution processing
32
36
  process_result(result)
@@ -36,16 +40,13 @@ module RailsActiveMcp
36
40
  limit ||= @config.max_results
37
41
 
38
42
  # Validate model access
39
- unless @config.model_allowed?(model)
40
- raise SafetyError, "Access to model '#{model}' is not allowed"
41
- end
43
+ raise SafetyError, "Access to model '#{model}' is not allowed" unless @config.model_allowed?(model)
42
44
 
43
45
  # Validate method safety
44
- unless safe_query_method?(method)
45
- raise SafetyError, "Method '#{method}' is not allowed for safe queries"
46
- end
46
+ raise SafetyError, "Method '#{method}' is not allowed for safe queries" unless safe_query_method?(method)
47
47
 
48
- begin
48
+ # Execute with proper Rails executor and connection management
49
+ execute_with_rails_executor_and_connection do
49
50
  model_class = model.to_s.constantize
50
51
 
51
52
  # Build and execute query
@@ -56,9 +57,7 @@ module RailsActiveMcp
56
57
  end
57
58
 
58
59
  # Apply limit for enumerable results
59
- if query.respond_to?(:limit) && !count_method?(method)
60
- query = query.limit(limit)
61
- end
60
+ query = query.limit(limit) if query.respond_to?(:limit) && !count_method?(method)
62
61
 
63
62
  result = execute_query_with_timeout(query)
64
63
 
@@ -71,7 +70,7 @@ module RailsActiveMcp
71
70
  count: calculate_count(result),
72
71
  executed_at: Time.now
73
72
  }
74
- rescue => e
73
+ rescue StandardError => e
75
74
  log_error(e, { model: model, method: method, args: args })
76
75
  {
77
76
  success: false,
@@ -99,6 +98,87 @@ module RailsActiveMcp
99
98
 
100
99
  private
101
100
 
101
+ def execute_with_rails_executor(code, timeout, capture_output)
102
+ if defined?(Rails) && Rails.application
103
+ # Handle development mode reloading if needed
104
+ handle_development_reloading if Rails.env.development?
105
+
106
+ # Rails 7.1 compatible execution with proper dependency loading
107
+ if defined?(ActiveSupport::Dependencies) && ActiveSupport::Dependencies.respond_to?(:interlock)
108
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
109
+ Rails.application.executor.wrap do
110
+ execute_with_connection_pool(code, timeout, capture_output)
111
+ end
112
+ end
113
+ else
114
+ # Fallback for older Rails versions
115
+ Rails.application.executor.wrap do
116
+ execute_with_connection_pool(code, timeout, capture_output)
117
+ end
118
+ end
119
+ else
120
+ # Non-Rails execution
121
+ execute_with_timeout(code, timeout, capture_output)
122
+ end
123
+ rescue TimeoutError => e
124
+ # Re-raise timeout errors as-is
125
+ raise e
126
+ rescue StandardError => e
127
+ raise ThreadSafetyError, "Thread-safe execution failed: #{e.message}"
128
+ end
129
+
130
+ # Manage ActiveRecord connection pool properly
131
+ def execute_with_connection_pool(code, timeout, capture_output)
132
+ if defined?(ActiveRecord::Base)
133
+ ActiveRecord::Base.connection_pool.with_connection do
134
+ execute_with_timeout(code, timeout, capture_output)
135
+ end
136
+ else
137
+ execute_with_timeout(code, timeout, capture_output)
138
+ end
139
+ 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
146
+ end
147
+
148
+ # Helper method for safe queries with proper Rails executor and connection management
149
+ def execute_with_rails_executor_and_connection(&block)
150
+ if defined?(Rails) && Rails.application
151
+ if defined?(ActiveSupport::Dependencies) && ActiveSupport::Dependencies.respond_to?(:interlock)
152
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
153
+ Rails.application.executor.wrap do
154
+ if defined?(ActiveRecord::Base)
155
+ ActiveRecord::Base.connection_pool.with_connection(&block)
156
+ else
157
+ yield
158
+ end
159
+ end
160
+ end
161
+ else
162
+ Rails.application.executor.wrap do
163
+ if defined?(ActiveRecord::Base)
164
+ ActiveRecord::Base.connection_pool.with_connection(&block)
165
+ else
166
+ yield
167
+ end
168
+ end
169
+ end
170
+ else
171
+ yield
172
+ end
173
+ 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
180
+ end
181
+
102
182
  def execute_with_timeout(code, timeout, capture_output)
103
183
  Timeout.timeout(timeout) do
104
184
  if capture_output
@@ -112,46 +192,62 @@ module RailsActiveMcp
112
192
  end
113
193
 
114
194
  def execute_with_captured_output(code)
115
- # Capture both stdout and the return value
116
- old_stdout = $stdout
117
- captured_output = StringIO.new
118
- $stdout = captured_output
119
-
120
- # Create execution context
121
- binding_context = create_console_binding
122
-
123
- # Execute code
124
- start_time = Time.now
125
- return_value = binding_context.eval(code)
126
- execution_time = Time.now - start_time
127
-
128
- output = captured_output.string
129
- $stdout = old_stdout
130
-
131
- {
132
- success: true,
133
- return_value: return_value,
134
- output: output,
135
- return_value_string: safe_inspect(return_value),
136
- execution_time: execution_time,
137
- code: code
138
- }
139
- rescue => e
140
- $stdout = old_stdout if old_stdout
141
- execution_time = Time.now - start_time if defined?(start_time)
142
-
143
- {
144
- success: false,
145
- error: e.message,
146
- error_class: e.class.name,
147
- backtrace: e.backtrace&.first(10),
148
- execution_time: execution_time,
149
- code: code
150
- }
195
+ # Thread-safe output capture using mutex
196
+ @execution_mutex.synchronize do
197
+ # Capture both stdout and stderr to prevent any Rails output leakage
198
+ old_stdout = $stdout
199
+ old_stderr = $stderr
200
+ captured_output = StringIO.new
201
+ captured_errors = StringIO.new
202
+ $stdout = captured_output
203
+ $stderr = captured_errors
204
+
205
+ begin
206
+ # Create thread-safe execution context
207
+ binding_context = create_thread_safe_console_binding
208
+
209
+ # Execute code
210
+ start_time = Time.now
211
+ return_value = binding_context.eval(code)
212
+ execution_time = Time.now - start_time
213
+
214
+ output = captured_output.string
215
+ errors = captured_errors.string
216
+
217
+ # Combine output and errors for comprehensive result
218
+ combined_output = [output, errors].reject(&:empty?).join("\n")
219
+
220
+ {
221
+ success: true,
222
+ return_value: return_value,
223
+ output: combined_output,
224
+ return_value_string: safe_inspect(return_value),
225
+ execution_time: execution_time,
226
+ code: code
227
+ }
228
+ rescue StandardError => e
229
+ execution_time = Time.now - start_time if defined?(start_time)
230
+ errors = captured_errors.string
231
+
232
+ {
233
+ success: false,
234
+ error: e.message,
235
+ error_class: e.class.name,
236
+ backtrace: e.backtrace&.first(10),
237
+ execution_time: execution_time,
238
+ code: code,
239
+ stderr: errors.empty? ? nil : errors
240
+ }
241
+ ensure
242
+ $stdout = old_stdout if old_stdout
243
+ $stderr = old_stderr if old_stderr
244
+ end
245
+ end
151
246
  end
152
247
 
153
248
  def execute_direct(code)
154
- binding_context = create_console_binding
249
+ # Create thread-safe binding context
250
+ binding_context = create_thread_safe_console_binding
155
251
  start_time = Time.now
156
252
 
157
253
  result = binding_context.eval(code)
@@ -163,7 +259,7 @@ module RailsActiveMcp
163
259
  execution_time: execution_time,
164
260
  code: code
165
261
  }
166
- rescue => e
262
+ rescue StandardError => e
167
263
  execution_time = Time.now - start_time if defined?(start_time)
168
264
 
169
265
  {
@@ -186,35 +282,47 @@ module RailsActiveMcp
186
282
  end
187
283
  end
188
284
 
189
- def create_console_binding
190
- # Create a clean binding with Rails console helpers
285
+ # Thread-safe console binding creation
286
+ def create_thread_safe_console_binding
287
+ # Create a new binding context for each execution to avoid shared state
191
288
  console_context = Object.new
192
289
 
193
290
  console_context.instance_eval do
194
- # Add Rails helpers if available
291
+ # Add Rails helpers if available (thread-safe)
195
292
  if defined?(Rails) && Rails.application
196
- extend Rails.application.routes.url_helpers if Rails.application.routes
293
+ # Only extend if routes are available and it's safe to do so
294
+ extend Rails.application.routes.url_helpers if Rails.application.routes && !Rails.env.production?
197
295
 
198
296
  def reload!
199
- Rails.application.reloader.reload!
200
- "Reloaded!"
297
+ if defined?(Rails) && Rails.application && Rails.application.respond_to?(:reloader)
298
+ Rails.application.reloader.reload!
299
+ 'Reloaded!'
300
+ else
301
+ 'Reload not available'
302
+ end
201
303
  end
202
304
 
203
305
  def app
204
- Rails.application
306
+ Rails.application if defined?(Rails)
205
307
  end
206
308
 
207
309
  def helper
208
- ApplicationController.helpers if defined?(ApplicationController)
310
+ return unless defined?(ApplicationController) && ApplicationController.respond_to?(:helpers)
311
+
312
+ ApplicationController.helpers
209
313
  end
210
314
  end
211
315
 
212
- # Add common console helpers
316
+ # Add common console helpers (thread-safe)
213
317
  def sql(query)
318
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
319
+
214
320
  ActiveRecord::Base.connection.select_all(query).to_a
215
321
  end
216
322
 
217
323
  def schema(table_name)
324
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
325
+
218
326
  ActiveRecord::Base.connection.columns(table_name)
219
327
  end
220
328
  end
@@ -222,6 +330,12 @@ module RailsActiveMcp
222
330
  console_context.instance_eval { binding }
223
331
  end
224
332
 
333
+ # Previous methods remain the same but are now called within thread-safe context
334
+ def create_console_binding
335
+ # Delegate to thread-safe version
336
+ create_thread_safe_console_binding
337
+ end
338
+
225
339
  def safe_query_method?(method)
226
340
  safe_methods = %w[
227
341
  find find_by find_each find_in_batches
@@ -274,18 +388,16 @@ module RailsActiveMcp
274
388
 
275
389
  def safe_inspect(object)
276
390
  object.inspect
277
- rescue => e
391
+ rescue StandardError => e
278
392
  "#<#{object.class}:0x#{object.object_id.to_s(16)} (inspect failed: #{e.message})>"
279
393
  end
280
394
 
281
395
  def process_result(result)
282
396
  # Apply max results limit to output
283
- if result[:success] && result[:return_value].is_a?(Array)
284
- if result[:return_value].size > @config.max_results
285
- result[:return_value] = result[:return_value].first(@config.max_results)
286
- result[:truncated] = true
287
- result[:note] = "Result truncated to #{@config.max_results} items"
288
- end
397
+ if result[:success] && result[:return_value].is_a?(Array) && (result[:return_value].size > @config.max_results)
398
+ result[:return_value] = result[:return_value].first(@config.max_results)
399
+ result[:truncated] = true
400
+ result[:note] = "Result truncated to #{@config.max_results} items"
289
401
  end
290
402
 
291
403
  result
@@ -310,16 +422,16 @@ module RailsActiveMcp
310
422
  recommendations = []
311
423
 
312
424
  if safety_analysis[:violations].any?
313
- recommendations << "Consider using read-only alternatives"
314
- recommendations << "Review the code for unintended side effects"
425
+ recommendations << 'Consider using read-only alternatives'
426
+ recommendations << 'Review the code for unintended side effects'
315
427
 
316
428
  if safety_analysis[:violations].any? { |v| v[:severity] == :critical }
317
- recommendations << "This code contains critical safety violations and should not be executed"
429
+ recommendations << 'This code contains critical safety violations and should not be executed'
318
430
  end
319
431
  end
320
432
 
321
433
  unless safety_analysis[:read_only]
322
- recommendations << "Consider using the safe_query tool for read-only operations"
434
+ recommendations << 'Consider using the safe_query tool for read-only operations'
323
435
  end
324
436
 
325
437
  recommendations
@@ -338,9 +450,9 @@ module RailsActiveMcp
338
450
  File.open(@config.audit_file, 'a') do |f|
339
451
  f.puts(JSON.generate(log_entry))
340
452
  end
341
- rescue => e
453
+ rescue StandardError => e
342
454
  # Don't fail execution due to logging issues
343
- Rails.logger.warn "Failed to log Rails Active MCP execution: #{e.message}" if defined?(Rails)
455
+ RailsActiveMcp.logger.warn "Failed to log Rails Active MCP execution: #{e.message}"
344
456
  end
345
457
 
346
458
  def log_error(error, context = {})
@@ -358,7 +470,7 @@ module RailsActiveMcp
358
470
  File.open(@config.audit_file, 'a') do |f|
359
471
  f.puts(JSON.generate(log_entry))
360
472
  end
361
- rescue
473
+ rescue StandardError
362
474
  # Silently fail logging
363
475
  end
364
476
 
@@ -371,8 +483,20 @@ module RailsActiveMcp
371
483
  else
372
484
  { environment: Rails.env }
373
485
  end
374
- rescue
486
+ rescue StandardError
375
487
  { unknown: true }
376
488
  end
489
+
490
+ # Handle development mode reloading safely
491
+ def handle_development_reloading
492
+ return unless Rails.env.development?
493
+ return unless defined?(Rails.application.reloader)
494
+
495
+ # Check if reloading is needed and safe to do
496
+ Rails.application.reloader.reload! if Rails.application.reloader.check!
497
+ rescue StandardError => e
498
+ # Log but don't fail execution due to reloading issues
499
+ RailsActiveMcp.logger.warn "Failed to reload in development: #{e.message}" if defined?(RailsActiveMcp.logger)
500
+ end
377
501
  end
378
502
  end
@@ -6,6 +6,37 @@ module RailsActiveMcp
6
6
 
7
7
  config.rails_active_mcp = ActiveSupport::OrderedOptions.new
8
8
 
9
+ # Ensure configuration is available very early
10
+ initializer 'rails_active_mcp.early_configuration', before: :load_config_initializers do
11
+ RailsActiveMcp.configure unless RailsActiveMcp.configuration
12
+ end
13
+
14
+ # Configure logging with Rails 7.1+ compatibility
15
+ initializer 'rails_active_mcp.logger', after: :initialize_logger, before: :set_clear_dependencies_hook do
16
+ # Only set logger if Rails logger is available and responds to logging methods
17
+ RailsActiveMcp.logger = if defined?(Rails.logger) && Rails.logger.respond_to?(:info)
18
+ # Check if Rails logger is using semantic logger or other custom loggers
19
+ if Rails.logger.class.name.include?('SemanticLogger')
20
+ # For semantic logger, we need to create a tagged logger
21
+ Rails.logger.tagged('RailsActiveMcp')
22
+ else
23
+ # For standard Rails logger, use it directly
24
+ Rails.logger
25
+ end
26
+ else
27
+ # Fallback to our own logger if Rails logger is not available
28
+ Logger.new(STDERR).tap do |logger|
29
+ logger.level = Rails.env.production? ? Logger::WARN : Logger::INFO
30
+ logger.formatter = proc do |severity, datetime, progname, msg|
31
+ "[#{datetime}] #{severity} -- RailsActiveMcp: #{msg}\n"
32
+ end
33
+ end
34
+ end
35
+
36
+ # Log that the logger has been initialized
37
+ RailsActiveMcp.logger.info "Rails Active MCP logger initialized (#{RailsActiveMcp.logger.class.name})"
38
+ end
39
+
9
40
  # Add generators configuration
10
41
  config.generators do |g|
11
42
  g.test_framework :rspec, fixture: false
@@ -23,18 +54,15 @@ module RailsActiveMcp
23
54
  end
24
55
  end
25
56
 
26
- # Set default audit file location
27
- RailsActiveMcp.config.audit_file ||= Rails.root.join('log', 'rails_active_mcp.log')
28
-
29
57
  # Validate configuration
30
- RailsActiveMcp.config.validate!
58
+ RailsActiveMcp.config.valid?
31
59
  end
32
60
 
33
- # Add our tools directory to the load path
34
- config.autoload_paths << root.join('lib', 'rails_active_mcp', 'tools')
61
+ # Add our SDK tools directory to the load path
62
+ config.autoload_paths << root.join('lib', 'rails_active_mcp', 'sdk', 'tools')
35
63
 
36
- # Ensure our tools are eager loaded in production
37
- config.eager_load_paths << root.join('lib', 'rails_active_mcp', 'tools')
64
+ # Ensure our SDK tools are eager loaded in production
65
+ config.eager_load_paths << root.join('lib', 'rails_active_mcp', 'sdk', 'tools')
38
66
 
39
67
  # Add rake tasks
40
68
  rake_tasks do