rails-active-mcp 0.1.5 → 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.
- checksums.yaml +4 -4
- data/app/controllers/rails_active_mcp/mcp_controller.rb +80 -0
- data/claude_desktop_config.json +12 -0
- data/docs/DEBUGGING.md +35 -3
- data/docs/GENERATOR_TESTING.md +121 -0
- data/exe/rails-active-mcp-server +56 -11
- data/lib/generators/rails_active_mcp/install/install_generator.rb +142 -2
- data/lib/generators/rails_active_mcp/install/templates/README.md +48 -8
- data/lib/rails_active_mcp/console_executor.rb +192 -78
- data/lib/rails_active_mcp/engine.rb +16 -0
- data/lib/rails_active_mcp/mcp_server.rb +36 -27
- data/lib/rails_active_mcp/railtie.rb +25 -3
- data/lib/rails_active_mcp/stdio_server.rb +111 -61
- data/lib/rails_active_mcp/version.rb +1 -1
- data/lib/rails_active_mcp.rb +7 -2
- data/rails_active_mcp.gemspec +5 -4
- metadata +27 -12
@@ -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
|
29
|
-
result =
|
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
|
-
|
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
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
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
|
-
|
190
|
-
|
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
|
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
|
200
|
-
|
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
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
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 <<
|
314
|
-
recommendations <<
|
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 <<
|
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 <<
|
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
|
-
|
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
|
-
|
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'], -
|
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'], -
|
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'], -
|
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: [
|
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
|
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
|
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
|
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?
|
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
|
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
|
-
|
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
|
|