rails-active-mcp 0.1.6 → 0.1.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.
@@ -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,52 @@ 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 the return value
198
+ old_stdout = $stdout
199
+ captured_output = StringIO.new
200
+ $stdout = captured_output
201
+
202
+ begin
203
+ # Create thread-safe execution context
204
+ binding_context = create_thread_safe_console_binding
205
+
206
+ # Execute code
207
+ start_time = Time.now
208
+ return_value = binding_context.eval(code)
209
+ execution_time = Time.now - start_time
210
+
211
+ output = captured_output.string
212
+
213
+ {
214
+ success: true,
215
+ return_value: return_value,
216
+ output: output,
217
+ return_value_string: safe_inspect(return_value),
218
+ execution_time: execution_time,
219
+ code: code
220
+ }
221
+ rescue StandardError => e
222
+ execution_time = Time.now - start_time if defined?(start_time)
223
+
224
+ {
225
+ success: false,
226
+ error: e.message,
227
+ error_class: e.class.name,
228
+ backtrace: e.backtrace&.first(10),
229
+ execution_time: execution_time,
230
+ code: code
231
+ }
232
+ ensure
233
+ $stdout = old_stdout if old_stdout
234
+ end
235
+ end
151
236
  end
152
237
 
153
238
  def execute_direct(code)
154
- binding_context = create_console_binding
239
+ # Create thread-safe binding context
240
+ binding_context = create_thread_safe_console_binding
155
241
  start_time = Time.now
156
242
 
157
243
  result = binding_context.eval(code)
@@ -163,7 +249,7 @@ module RailsActiveMcp
163
249
  execution_time: execution_time,
164
250
  code: code
165
251
  }
166
- rescue => e
252
+ rescue StandardError => e
167
253
  execution_time = Time.now - start_time if defined?(start_time)
168
254
 
169
255
  {
@@ -186,35 +272,47 @@ module RailsActiveMcp
186
272
  end
187
273
  end
188
274
 
189
- def create_console_binding
190
- # Create a clean binding with Rails console helpers
275
+ # Thread-safe console binding creation
276
+ def create_thread_safe_console_binding
277
+ # Create a new binding context for each execution to avoid shared state
191
278
  console_context = Object.new
192
279
 
193
280
  console_context.instance_eval do
194
- # Add Rails helpers if available
281
+ # Add Rails helpers if available (thread-safe)
195
282
  if defined?(Rails) && Rails.application
196
- extend Rails.application.routes.url_helpers if Rails.application.routes
283
+ # Only extend if routes are available and it's safe to do so
284
+ extend Rails.application.routes.url_helpers if Rails.application.routes && !Rails.env.production?
197
285
 
198
286
  def reload!
199
- Rails.application.reloader.reload!
200
- "Reloaded!"
287
+ if defined?(Rails) && Rails.application && Rails.application.respond_to?(:reloader)
288
+ Rails.application.reloader.reload!
289
+ 'Reloaded!'
290
+ else
291
+ 'Reload not available'
292
+ end
201
293
  end
202
294
 
203
295
  def app
204
- Rails.application
296
+ Rails.application if defined?(Rails)
205
297
  end
206
298
 
207
299
  def helper
208
- ApplicationController.helpers if defined?(ApplicationController)
300
+ return unless defined?(ApplicationController) && ApplicationController.respond_to?(:helpers)
301
+
302
+ ApplicationController.helpers
209
303
  end
210
304
  end
211
305
 
212
- # Add common console helpers
306
+ # Add common console helpers (thread-safe)
213
307
  def sql(query)
308
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
309
+
214
310
  ActiveRecord::Base.connection.select_all(query).to_a
215
311
  end
216
312
 
217
313
  def schema(table_name)
314
+ raise NoMethodError, 'ActiveRecord not available' unless defined?(ActiveRecord::Base)
315
+
218
316
  ActiveRecord::Base.connection.columns(table_name)
219
317
  end
220
318
  end
@@ -222,6 +320,12 @@ module RailsActiveMcp
222
320
  console_context.instance_eval { binding }
223
321
  end
224
322
 
323
+ # Previous methods remain the same but are now called within thread-safe context
324
+ def create_console_binding
325
+ # Delegate to thread-safe version
326
+ create_thread_safe_console_binding
327
+ end
328
+
225
329
  def safe_query_method?(method)
226
330
  safe_methods = %w[
227
331
  find find_by find_each find_in_batches
@@ -274,18 +378,16 @@ module RailsActiveMcp
274
378
 
275
379
  def safe_inspect(object)
276
380
  object.inspect
277
- rescue => e
381
+ rescue StandardError => e
278
382
  "#<#{object.class}:0x#{object.object_id.to_s(16)} (inspect failed: #{e.message})>"
279
383
  end
280
384
 
281
385
  def process_result(result)
282
386
  # 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
387
+ if result[:success] && result[:return_value].is_a?(Array) && (result[:return_value].size > @config.max_results)
388
+ result[:return_value] = result[:return_value].first(@config.max_results)
389
+ result[:truncated] = true
390
+ result[:note] = "Result truncated to #{@config.max_results} items"
289
391
  end
290
392
 
291
393
  result
@@ -310,16 +412,16 @@ module RailsActiveMcp
310
412
  recommendations = []
311
413
 
312
414
  if safety_analysis[:violations].any?
313
- recommendations << "Consider using read-only alternatives"
314
- recommendations << "Review the code for unintended side effects"
415
+ recommendations << 'Consider using read-only alternatives'
416
+ recommendations << 'Review the code for unintended side effects'
315
417
 
316
418
  if safety_analysis[:violations].any? { |v| v[:severity] == :critical }
317
- recommendations << "This code contains critical safety violations and should not be executed"
419
+ recommendations << 'This code contains critical safety violations and should not be executed'
318
420
  end
319
421
  end
320
422
 
321
423
  unless safety_analysis[:read_only]
322
- recommendations << "Consider using the safe_query tool for read-only operations"
424
+ recommendations << 'Consider using the safe_query tool for read-only operations'
323
425
  end
324
426
 
325
427
  recommendations
@@ -338,9 +440,9 @@ module RailsActiveMcp
338
440
  File.open(@config.audit_file, 'a') do |f|
339
441
  f.puts(JSON.generate(log_entry))
340
442
  end
341
- rescue => e
443
+ rescue StandardError => e
342
444
  # Don't fail execution due to logging issues
343
- Rails.logger.warn "Failed to log Rails Active MCP execution: #{e.message}" if defined?(Rails)
445
+ RailsActiveMcp.logger.warn "Failed to log Rails Active MCP execution: #{e.message}"
344
446
  end
345
447
 
346
448
  def log_error(error, context = {})
@@ -358,7 +460,7 @@ module RailsActiveMcp
358
460
  File.open(@config.audit_file, 'a') do |f|
359
461
  f.puts(JSON.generate(log_entry))
360
462
  end
361
- rescue
463
+ rescue StandardError
362
464
  # Silently fail logging
363
465
  end
364
466
 
@@ -371,8 +473,20 @@ module RailsActiveMcp
371
473
  else
372
474
  { environment: Rails.env }
373
475
  end
374
- rescue
476
+ rescue StandardError
375
477
  { unknown: true }
376
478
  end
479
+
480
+ # Handle development mode reloading safely
481
+ def handle_development_reloading
482
+ return unless Rails.env.development?
483
+ return unless defined?(Rails.application.reloader)
484
+
485
+ # Check if reloading is needed and safe to do
486
+ Rails.application.reloader.reload! if Rails.application.reloader.check!
487
+ rescue StandardError => e
488
+ # Log but don't fail execution due to reloading issues
489
+ RailsActiveMcp.logger.warn "Failed to reload in development: #{e.message}" if defined?(RailsActiveMcp.logger)
490
+ end
377
491
  end
378
492
  end
@@ -13,6 +13,22 @@ module RailsActiveMcp
13
13
  g.helper false
14
14
  end
15
15
 
16
+ # Define routes for the engine
17
+ routes do
18
+ # Main MCP endpoint for HTTP clients
19
+ post '/', to: 'mcp#handle'
20
+ post '/messages', to: 'mcp#handle'
21
+
22
+ # SSE endpoint for better MCP client compatibility
23
+ get '/sse', to: 'mcp#sse'
24
+
25
+ # Health check endpoint
26
+ get '/health', to: 'mcp#health'
27
+
28
+ # Root redirect
29
+ root to: 'mcp#info'
30
+ end
31
+
16
32
  initializer 'rails_active_mcp.configure' do |app|
17
33
  # Load configuration from Rails config if present
18
34
  if app.config.respond_to?(:rails_active_mcp)
@@ -26,21 +26,15 @@ module RailsActiveMcp
26
26
  data = JSON.parse(body)
27
27
  response = handle_jsonrpc_request(data)
28
28
 
29
- [200, {'Content-Type' => 'application/json'}, [response.to_json]]
29
+ [200, { 'Content-Type' => 'application/json' }, [response.to_json]]
30
30
  rescue JSON::ParserError
31
31
  error_response(400, 'Invalid JSON')
32
- rescue => e
33
- Rails.logger.error "MCP Server Error: #{e.message}" if defined?(Rails)
32
+ rescue StandardError => e
33
+ RailsActiveMcp.logger.error "MCP Server Error: #{e.message}"
34
34
  error_response(500, 'Internal Server Error')
35
35
  end
36
36
  end
37
37
 
38
- private
39
-
40
- def json_request?(request)
41
- request.content_type&.include?('application/json')
42
- end
43
-
44
38
  def handle_jsonrpc_request(data)
45
39
  case data['method']
46
40
  when 'initialize'
@@ -53,11 +47,21 @@ module RailsActiveMcp
53
47
  handle_resources_list(data)
54
48
  when 'resources/read'
55
49
  handle_resources_read(data)
50
+ when 'ping'
51
+ handle_ping(data)
56
52
  else
57
- jsonrpc_error(data['id'], -32601, 'Method not found')
53
+ jsonrpc_error(data['id'], -32_601, 'Method not found')
58
54
  end
59
55
  end
60
56
 
57
+ def handle_ping(data)
58
+ {
59
+ jsonrpc: JSONRPC_VERSION,
60
+ id: data['id'],
61
+ result: {}
62
+ }
63
+ end
64
+
61
65
  def handle_initialize(data)
62
66
  {
63
67
  jsonrpc: JSONRPC_VERSION,
@@ -91,9 +95,7 @@ module RailsActiveMcp
91
95
  }
92
96
 
93
97
  # Add annotations if present
94
- if tool[:annotations] && !tool[:annotations].empty?
95
- tool_def[:annotations] = tool[:annotations]
96
- end
98
+ tool_def[:annotations] = tool[:annotations] if tool[:annotations] && !tool[:annotations].empty?
97
99
 
98
100
  tool_def
99
101
  end
@@ -110,7 +112,7 @@ module RailsActiveMcp
110
112
  arguments = data.dig('params', 'arguments') || {}
111
113
 
112
114
  tool = @tools[tool_name]
113
- return jsonrpc_error(data['id'], -32602, "Tool '#{tool_name}' not found") unless tool
115
+ return jsonrpc_error(data['id'], -32_602, "Tool '#{tool_name}' not found") unless tool
114
116
 
115
117
  begin
116
118
  result = tool[:handler].call(arguments)
@@ -119,8 +121,8 @@ module RailsActiveMcp
119
121
  id: data['id'],
120
122
  result: { content: [{ type: 'text', text: result.to_s }] }
121
123
  }
122
- rescue => e
123
- jsonrpc_error(data['id'], -32603, "Tool execution failed: #{e.message}")
124
+ rescue StandardError => e
125
+ jsonrpc_error(data['id'], -32_603, "Tool execution failed: #{e.message}")
124
126
  end
125
127
  end
126
128
 
@@ -150,6 +152,12 @@ module RailsActiveMcp
150
152
  }
151
153
  end
152
154
 
155
+ private
156
+
157
+ def json_request?(request)
158
+ request.content_type&.include?('application/json')
159
+ end
160
+
153
161
  def register_default_tools
154
162
  register_tool(
155
163
  'rails_console_execute',
@@ -207,7 +215,7 @@ module RailsActiveMcp
207
215
  query: { type: 'string', description: 'Safe query to execute' },
208
216
  model: { type: 'string', description: 'Model class name' }
209
217
  },
210
- required: ['query', 'model']
218
+ required: %w[query model]
211
219
  },
212
220
  # Safe read-only query tool
213
221
  {
@@ -244,7 +252,7 @@ module RailsActiveMcp
244
252
  end
245
253
 
246
254
  def execute_console_code(args)
247
- return "Rails Active MCP is disabled" unless RailsActiveMcp.config.enabled
255
+ return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
248
256
 
249
257
  executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
250
258
 
@@ -265,13 +273,13 @@ module RailsActiveMcp
265
273
  "Safety check failed: #{e.message}"
266
274
  rescue RailsActiveMcp::TimeoutError => e
267
275
  "Execution timed out: #{e.message}"
268
- rescue => e
276
+ rescue StandardError => e
269
277
  "Execution failed: #{e.message}"
270
278
  end
271
279
  end
272
280
 
273
281
  def get_model_info(model_name)
274
- return "Rails Active MCP is disabled" unless RailsActiveMcp.config.enabled
282
+ return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
275
283
 
276
284
  begin
277
285
  model_class = model_name.constantize
@@ -285,20 +293,21 @@ module RailsActiveMcp
285
293
  info.join("\n")
286
294
  rescue NameError
287
295
  "Model '#{model_name}' not found"
288
- rescue => e
296
+ rescue StandardError => e
289
297
  "Error getting model info: #{e.message}"
290
298
  end
291
299
  end
292
300
 
293
301
  def execute_safe_query(args)
294
- return "Rails Active MCP is disabled" unless RailsActiveMcp.config.enabled
302
+ return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
295
303
 
296
304
  begin
297
305
  model_class = args['model'].constantize
298
306
  return "#{args['model']} is not an ActiveRecord model" unless model_class < ActiveRecord::Base
299
307
 
300
308
  # Only allow safe read-only methods
301
- safe_methods = %w[find find_by where select count sum average maximum minimum first last pluck ids exists? empty? any? many? include?]
309
+ safe_methods = %w[find find_by where select count sum average maximum minimum first last pluck ids exists?
310
+ empty? any? many? include?]
302
311
  query_method = args['query'].split('.').first
303
312
 
304
313
  return "Unsafe query method: #{query_method}" unless safe_methods.include?(query_method)
@@ -307,13 +316,13 @@ module RailsActiveMcp
307
316
  result.to_s
308
317
  rescue NameError
309
318
  "Model '#{args['model']}' not found"
310
- rescue => e
319
+ rescue StandardError => e
311
320
  "Error executing query: #{e.message}"
312
321
  end
313
322
  end
314
323
 
315
324
  def dry_run_analysis(code)
316
- return "Rails Active MCP is disabled" unless RailsActiveMcp.config.enabled
325
+ return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
317
326
 
318
327
  executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
319
328
 
@@ -343,7 +352,7 @@ module RailsActiveMcp
343
352
  end
344
353
 
345
354
  output.join("\n")
346
- rescue => e
355
+ rescue StandardError => e
347
356
  "Analysis failed: #{e.message}"
348
357
  end
349
358
  end
@@ -367,7 +376,7 @@ module RailsActiveMcp
367
376
  end
368
377
 
369
378
  def error_response(status, message)
370
- [status, {'Content-Type' => 'application/json'},
379
+ [status, { 'Content-Type' => 'application/json' },
371
380
  [{ error: message }.to_json]]
372
381
  end
373
382
  end
@@ -25,9 +25,31 @@ module RailsActiveMcp
25
25
  Rails::ConsoleMethods.include(RailsActiveMcp::ConsoleMethods) if defined?(Rails::ConsoleMethods)
26
26
  end
27
27
 
28
- # Configure logging
29
- initializer 'rails_active_mcp.logger' do
30
- RailsActiveMcp.logger = Rails.logger if defined?(Rails.logger)
28
+ # Configure logging - Fixed for Rails 7.1 compatibility
29
+ initializer 'rails_active_mcp.logger', after: :initialize_logger, before: :set_clear_dependencies_hook do
30
+ # Only set logger if Rails logger is available and responds to logging methods
31
+ RailsActiveMcp.logger = if defined?(Rails.logger) && Rails.logger.respond_to?(:info)
32
+ # Check if Rails logger is using semantic logger or other custom loggers
33
+ if Rails.logger.class.name.include?('SemanticLogger')
34
+ # For semantic logger, we need to create a tagged logger
35
+ Rails.logger.tagged('RailsActiveMcp')
36
+ else
37
+ # For standard Rails logger, use it directly
38
+ Rails.logger
39
+ end
40
+ else
41
+ # Fallback to our own logger if Rails logger is not available
42
+ # This should not happen in normal Rails apps but provides safety
43
+ Logger.new(STDERR).tap do |logger|
44
+ logger.level = Rails.env.production? ? Logger::WARN : Logger::INFO
45
+ logger.formatter = proc do |severity, datetime, progname, msg|
46
+ "[#{datetime}] #{severity} -- RailsActiveMcp: #{msg}\n"
47
+ end
48
+ end
49
+ end
50
+
51
+ # Log that the logger has been initialized
52
+ RailsActiveMcp.logger.info "Rails Active MCP logger initialized (#{RailsActiveMcp.logger.class.name})"
31
53
  end
32
54
  end
33
55