rails-active-mcp 0.1.7 → 2.0.8

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -279
  3. data/changelog.md +69 -0
  4. data/docs/DEBUGGING.md +5 -5
  5. data/docs/README.md +130 -142
  6. data/examples/rails_app_integration.md +405 -0
  7. data/exe/rails-active-mcp-server +153 -76
  8. data/gemfiles/rails_7.1.gemfile +34 -0
  9. data/lib/generators/rails_active_mcp/install/install_generator.rb +19 -39
  10. data/lib/generators/rails_active_mcp/install/templates/README.md +134 -188
  11. data/lib/generators/rails_active_mcp/install/templates/initializer.rb +65 -28
  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 +13 -3
  15. data/lib/rails_active_mcp/engine.rb +36 -24
  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/tasks.rake +236 -80
  22. data/lib/rails_active_mcp/version.rb +1 -1
  23. data/lib/rails_active_mcp.rb +5 -11
  24. data/rails_active_mcp.gemspec +62 -11
  25. metadata +83 -24
  26. data/app/controllers/rails_active_mcp/mcp_controller.rb +0 -80
  27. data/lib/rails_active_mcp/mcp_server.rb +0 -383
  28. data/lib/rails_active_mcp/railtie.rb +0 -70
  29. data/lib/rails_active_mcp/stdio_server.rb +0 -517
  30. data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
  31. data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
  32. data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
  33. data/lib/rails_active_mcp/tools/safe_query_tool.rb +0 -41
@@ -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
@@ -194,10 +194,13 @@ module RailsActiveMcp
194
194
  def execute_with_captured_output(code)
195
195
  # Thread-safe output capture using mutex
196
196
  @execution_mutex.synchronize do
197
- # Capture both stdout and the return value
197
+ # Capture both stdout and stderr to prevent any Rails output leakage
198
198
  old_stdout = $stdout
199
+ old_stderr = $stderr
199
200
  captured_output = StringIO.new
201
+ captured_errors = StringIO.new
200
202
  $stdout = captured_output
203
+ $stderr = captured_errors
201
204
 
202
205
  begin
203
206
  # Create thread-safe execution context
@@ -209,17 +212,22 @@ module RailsActiveMcp
209
212
  execution_time = Time.now - start_time
210
213
 
211
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")
212
219
 
213
220
  {
214
221
  success: true,
215
222
  return_value: return_value,
216
- output: output,
223
+ output: combined_output,
217
224
  return_value_string: safe_inspect(return_value),
218
225
  execution_time: execution_time,
219
226
  code: code
220
227
  }
221
228
  rescue StandardError => e
222
229
  execution_time = Time.now - start_time if defined?(start_time)
230
+ errors = captured_errors.string
223
231
 
224
232
  {
225
233
  success: false,
@@ -227,10 +235,12 @@ module RailsActiveMcp
227
235
  error_class: e.class.name,
228
236
  backtrace: e.backtrace&.first(10),
229
237
  execution_time: execution_time,
230
- code: code
238
+ code: code,
239
+ stderr: errors.empty? ? nil : errors
231
240
  }
232
241
  ensure
233
242
  $stdout = old_stdout if old_stdout
243
+ $stderr = old_stderr if old_stderr
234
244
  end
235
245
  end
236
246
  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
@@ -13,22 +44,6 @@ module RailsActiveMcp
13
44
  g.helper false
14
45
  end
15
46
 
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
-
32
47
  initializer 'rails_active_mcp.configure' do |app|
33
48
  # Load configuration from Rails config if present
34
49
  if app.config.respond_to?(:rails_active_mcp)
@@ -39,18 +54,15 @@ module RailsActiveMcp
39
54
  end
40
55
  end
41
56
 
42
- # Set default audit file location
43
- RailsActiveMcp.config.audit_file ||= Rails.root.join('log', 'rails_active_mcp.log')
44
-
45
57
  # Validate configuration
46
- RailsActiveMcp.config.validate!
58
+ RailsActiveMcp.config.valid?
47
59
  end
48
60
 
49
- # Add our tools directory to the load path
50
- 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')
51
63
 
52
- # Ensure our tools are eager loaded in production
53
- 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')
54
66
 
55
67
  # Add rake tasks
56
68
  rake_tasks do
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+ require 'stringio'
5
+ require 'fileutils'
6
+
7
+ # Require all SDK tools
8
+ require_relative 'tools/console_execute_tool'
9
+ require_relative 'tools/model_info_tool'
10
+ require_relative 'tools/safe_query_tool'
11
+ require_relative 'tools/dry_run_tool'
12
+
13
+ module RailsActiveMcp
14
+ module Sdk
15
+ class Server
16
+ attr_reader :mcp_server
17
+
18
+ def initialize
19
+ # Store original streams for restoration
20
+ @original_stdout = $stdout
21
+ @original_stderr = $stderr
22
+
23
+ # Set up output redirection BEFORE any Rails interaction
24
+ setup_output_redirection
25
+
26
+ # Configure MCP first
27
+ configure_mcp
28
+
29
+ # Create the MCP server with our tools
30
+ @mcp_server = MCP::Server.new(
31
+ name: 'rails-active-mcp',
32
+ version: RailsActiveMcp::VERSION,
33
+ tools: discover_tools,
34
+ server_context: server_context
35
+ )
36
+
37
+ # Set up server handlers
38
+ setup_server_handlers
39
+ end
40
+
41
+ def run_stdio
42
+ # Ensure output redirection is active for stdio mode
43
+ ensure_output_redirection_for_stdio
44
+
45
+ require 'mcp/transports/stdio'
46
+ transport = MCP::Transports::StdioTransport.new(@mcp_server)
47
+ transport.open
48
+ rescue StandardError => e
49
+ # Log to stderr (which is redirected to file) and re-raise
50
+ warn "[#{Time.now}] [RAILS-MCP] FATAL: SDK Server crashed: #{e.message}"
51
+ warn "[#{Time.now}] [RAILS-MCP] FATAL: #{e.backtrace.join("\n")}"
52
+ raise
53
+ ensure
54
+ restore_output_streams
55
+ end
56
+
57
+ def run_http(port: 3001)
58
+ # HTTP transport might not be available in the SDK yet
59
+ # For now, fall back to a basic implementation or error
60
+ raise NotImplementedError, 'HTTP transport not yet implemented with official MCP SDK'
61
+ end
62
+
63
+ private
64
+
65
+ def setup_output_redirection
66
+ # Skip redirection if in debug mode
67
+ return if ENV['RAILS_MCP_DEBUG'] == '1'
68
+
69
+ # Create log directory
70
+ log_dir = File.join(Dir.pwd, 'log')
71
+ FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
72
+
73
+ # Redirect stderr to log file
74
+ stderr_log = File.join(log_dir, 'rails_mcp_stderr.log')
75
+ @stderr_file = File.open(stderr_log, 'a')
76
+ @stderr_file.sync = true
77
+ $stderr = @stderr_file
78
+
79
+ # Capture stdout during initialization to prevent interference
80
+ @stdout_buffer = StringIO.new
81
+ $stdout = @stdout_buffer
82
+
83
+ # Log redirection setup
84
+ warn "[#{Time.now}] [RAILS-MCP] INFO: Output redirection enabled. stderr -> #{stderr_log}"
85
+ end
86
+
87
+ def ensure_output_redirection_for_stdio
88
+ # Skip if in debug mode
89
+ return if ENV['RAILS_MCP_DEBUG'] == '1'
90
+
91
+ # Check if anything was captured during initialization
92
+ if @stdout_buffer && !@stdout_buffer.string.empty?
93
+ captured = @stdout_buffer.string
94
+ warn "[#{Time.now}] [RAILS-MCP] WARNING: Captured stdout during initialization: #{captured.inspect}"
95
+ end
96
+
97
+ # Restore original stdout for MCP communication, keep stderr redirected
98
+ $stdout = @original_stdout
99
+ warn "[#{Time.now}] [RAILS-MCP] INFO: stdout restored for MCP communication, stderr remains redirected"
100
+ end
101
+
102
+ def restore_output_streams
103
+ return if ENV['RAILS_MCP_DEBUG'] == '1'
104
+
105
+ begin
106
+ $stdout = @original_stdout if @original_stdout
107
+ $stderr = @original_stderr if @original_stderr
108
+ @stderr_file&.close
109
+ rescue StandardError => e
110
+ # Best effort cleanup
111
+ warn "Failed to restore output streams: #{e.message}" if @original_stderr
112
+ end
113
+ end
114
+
115
+ def discover_tools
116
+ [
117
+ RailsActiveMcp::Sdk::Tools::ConsoleExecuteTool,
118
+ RailsActiveMcp::Sdk::Tools::ModelInfoTool,
119
+ RailsActiveMcp::Sdk::Tools::SafeQueryTool,
120
+ RailsActiveMcp::Sdk::Tools::DryRunTool
121
+ ]
122
+ end
123
+
124
+ def configure_mcp
125
+ # Configure MCP SDK with Rails-specific handlers
126
+ MCP.configure do |config|
127
+ config.exception_reporter = method(:handle_rails_exception)
128
+ config.instrumentation_callback = method(:log_mcp_calls)
129
+ end
130
+ end
131
+
132
+ def setup_server_handlers
133
+ # Set up resource read handler (for future use)
134
+ @mcp_server.resources_read_handler do |params|
135
+ [
136
+ {
137
+ uri: params[:uri],
138
+ mimeType: 'text/plain',
139
+ text: "Rails Active MCP Resource: #{params[:uri]}"
140
+ }
141
+ ]
142
+ end
143
+ end
144
+
145
+ def handle_rails_exception(exception, context)
146
+ # Use stderr which is redirected to log file
147
+ warn "[#{Time.now}] [RAILS-MCP] ERROR: MCP Exception: #{exception.message}"
148
+ warn "[#{Time.now}] [RAILS-MCP] ERROR: #{exception.backtrace.join("\n")}" if ENV['RAILS_MCP_DEBUG'] == '1'
149
+
150
+ # Log context for debugging
151
+ return unless context && ENV['RAILS_MCP_DEBUG'] == '1'
152
+
153
+ warn "[#{Time.now}] [RAILS-MCP] DEBUG: MCP Context: #{context.inspect}"
154
+ end
155
+
156
+ def log_mcp_calls(data)
157
+ return unless ENV['RAILS_MCP_DEBUG'] == '1'
158
+
159
+ duration_ms = (data[:duration] * 1000).round(2)
160
+
161
+ log_message = "MCP #{data[:method]}"
162
+ log_message += " [#{data[:tool_name]}]" if data[:tool_name]
163
+ log_message += " (#{duration_ms}ms)"
164
+
165
+ warn "[#{Time.now}] [RAILS-MCP] DEBUG: #{log_message}"
166
+
167
+ # Log errors separately
168
+ return unless data[:error]
169
+
170
+ warn "[#{Time.now}] [RAILS-MCP] ERROR: MCP Call Error: #{data[:error]}"
171
+ end
172
+
173
+ def server_context
174
+ {
175
+ rails_env: defined?(Rails) ? Rails.env : 'unknown',
176
+ rails_root: defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.to_s : Dir.pwd,
177
+ config: RailsActiveMcp.config,
178
+ gem_version: RailsActiveMcp::VERSION
179
+ }
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+
5
+ module RailsActiveMcp
6
+ module Sdk
7
+ module Tools
8
+ class ConsoleExecuteTool < MCP::Tool
9
+ description 'Execute Ruby code in Rails console with safety checks'
10
+
11
+ input_schema(
12
+ properties: {
13
+ code: {
14
+ type: 'string',
15
+ description: 'Ruby code to execute in Rails console'
16
+ },
17
+ safe_mode: {
18
+ type: 'boolean',
19
+ description: 'Enable safety checks (default: true)'
20
+ },
21
+ timeout: {
22
+ type: 'integer',
23
+ description: 'Timeout in seconds (default: 30)'
24
+ },
25
+ capture_output: {
26
+ type: 'boolean',
27
+ description: 'Capture console output (default: true)'
28
+ }
29
+ },
30
+ required: ['code']
31
+ )
32
+
33
+ annotations(
34
+ title: 'Rails Console Executor',
35
+ destructive_hint: true,
36
+ read_only_hint: false,
37
+ idempotent_hint: false,
38
+ open_world_hint: false
39
+ )
40
+
41
+ def self.call(code:, server_context:, safe_mode: true, timeout: 30, capture_output: true)
42
+ config = RailsActiveMcp.config
43
+
44
+ # Create executor with config
45
+ executor = RailsActiveMcp::ConsoleExecutor.new(config)
46
+
47
+ begin
48
+ result = executor.execute(
49
+ code,
50
+ timeout: timeout,
51
+ safe_mode: safe_mode,
52
+ capture_output: capture_output
53
+ )
54
+
55
+ if result[:success]
56
+ MCP::Tool::Response.new([
57
+ { type: 'text', text: format_success_result(result) }
58
+ ])
59
+ else
60
+ MCP::Tool::Response.new([
61
+ { type: 'text', text: format_error_result(result) }
62
+ ])
63
+ end
64
+ rescue RailsActiveMcp::SafetyError => e
65
+ error_response("Safety check failed: #{e.message}")
66
+ rescue RailsActiveMcp::TimeoutError => e
67
+ error_response("Execution timed out: #{e.message}")
68
+ rescue StandardError => e
69
+ error_response("Execution failed: #{e.message}")
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def self.format_success_result(result)
76
+ output = []
77
+ output << "Code: #{result[:code]}"
78
+ output << "Result: #{result[:return_value_string] || result[:return_value]}"
79
+
80
+ output << "Output: #{result[:output]}" if result[:output].present?
81
+
82
+ output << "Execution time: #{result[:execution_time]}s" if result[:execution_time]
83
+
84
+ output << "Note: #{result[:note]}" if result[:note]
85
+
86
+ output.join("\n")
87
+ end
88
+
89
+ def self.format_error_result(result)
90
+ error_msg = "Error: #{result[:error]}"
91
+ error_msg += " (#{result[:error_class]})" if result[:error_class]
92
+ error_msg
93
+ end
94
+
95
+ def self.error_response(message)
96
+ MCP::Tool::Response.new([
97
+ { type: 'text', text: message }
98
+ ])
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+
5
+ module RailsActiveMcp
6
+ module Sdk
7
+ module Tools
8
+ class DryRunTool < MCP::Tool
9
+ description 'Analyze Ruby code for safety without executing it'
10
+
11
+ input_schema(
12
+ properties: {
13
+ code: {
14
+ type: 'string',
15
+ description: 'Ruby code to analyze for safety'
16
+ }
17
+ },
18
+ required: ['code']
19
+ )
20
+
21
+ annotations(
22
+ title: 'Code Safety Analyzer',
23
+ destructive_hint: false,
24
+ read_only_hint: true,
25
+ idempotent_hint: true,
26
+ open_world_hint: false
27
+ )
28
+
29
+ def self.call(code:, server_context:, check_safety: true, analyze_dependencies: true)
30
+ config = RailsActiveMcp.config
31
+
32
+ # Create safety checker
33
+
34
+ executor = RailsActiveMcp::ConsoleExecutor.new(config)
35
+ analysis = executor.dry_run(code)
36
+
37
+ output = []
38
+ output << "Code: #{analysis[:code]}"
39
+ output << "Safe: #{analysis[:safety_analysis][:safe] ? 'Yes' : 'No'}"
40
+ output << "Read-only: #{analysis[:safety_analysis][:read_only] ? 'Yes' : 'No'}"
41
+ output << "Risk level: #{analysis[:estimated_risk]}"
42
+ output << "Summary: #{analysis[:safety_analysis][:summary]}"
43
+
44
+ if analysis[:safety_analysis][:violations].any?
45
+ output << "\nViolations:"
46
+ analysis[:safety_analysis][:violations].each do |violation|
47
+ output << " - #{violation[:description]} (#{violation[:severity]})"
48
+ end
49
+ end
50
+
51
+ if analysis[:recommendations].any?
52
+ output << "\nRecommendations:"
53
+ analysis[:recommendations].each do |rec|
54
+ output << " - #{rec}"
55
+ end
56
+ end
57
+
58
+ MCP::Tool::Response.new([
59
+ { type: 'text', text: output.join("\n") }
60
+ ])
61
+ end
62
+
63
+ private
64
+
65
+ def self.error_response(message)
66
+ MCP::Tool::Response.new([
67
+ { type: 'text', text: message }
68
+ ])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end