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.
- checksums.yaml +4 -4
- data/README.md +106 -279
- data/changelog.md +69 -0
- data/claude_desktop_config.json +12 -0
- data/docs/DEBUGGING.md +40 -8
- data/docs/GENERATOR_TESTING.md +121 -0
- data/docs/README.md +130 -142
- data/exe/rails-active-mcp-server +176 -65
- data/lib/generators/rails_active_mcp/install/install_generator.rb +123 -3
- data/lib/generators/rails_active_mcp/install/templates/README.md +34 -128
- data/lib/generators/rails_active_mcp/install/templates/initializer.rb +37 -38
- data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -3
- data/lib/rails_active_mcp/configuration.rb +37 -98
- data/lib/rails_active_mcp/console_executor.rb +202 -78
- data/lib/rails_active_mcp/engine.rb +36 -8
- data/lib/rails_active_mcp/sdk/server.rb +183 -0
- data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +103 -0
- data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +73 -0
- data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +106 -0
- data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +77 -0
- data/lib/rails_active_mcp/version.rb +1 -1
- data/lib/rails_active_mcp.rb +10 -11
- data/rails_active_mcp.gemspec +8 -4
- metadata +43 -17
- data/lib/rails_active_mcp/mcp_server.rb +0 -374
- data/lib/rails_active_mcp/railtie.rb +0 -48
- data/lib/rails_active_mcp/stdio_server.rb +0 -467
- data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
- data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
- data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
- 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
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
@
|
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
|
-
@
|
17
|
-
@
|
29
|
+
@log_executions = false
|
30
|
+
@audit_file = nil
|
18
31
|
@custom_safety_patterns = []
|
19
|
-
@
|
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
|
-
|
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
|
-
|
94
|
-
@allowed_models.include?(model_str)
|
38
|
+
@allowed_models.include?(model_name.to_s)
|
95
39
|
end
|
96
40
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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
|
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,62 @@ 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 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
|
-
|
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
|
-
|
190
|
-
|
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
|
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
|
200
|
-
|
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
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
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 <<
|
314
|
-
recommendations <<
|
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 <<
|
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 <<
|
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
|
-
|
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.
|
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
|