rails-active-mcp 0.1.1

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.
@@ -0,0 +1,154 @@
1
+ namespace :rails_active_mcp do
2
+ desc "Check the safety of Ruby code"
3
+ task :check_safety, [:code] => :environment do |task, args|
4
+ code = args[:code]
5
+
6
+ if code.blank?
7
+ puts "Usage: rails rails_active_mcp:check_safety['User.count']"
8
+ exit 1
9
+ end
10
+
11
+ safety_checker = RailsActiveMcp::SafetyChecker.new(RailsActiveMcp.config)
12
+ analysis = safety_checker.analyze(code)
13
+
14
+ puts "Code: #{code}"
15
+ puts "Safe: #{analysis[:safe] ? 'Yes' : 'No'}"
16
+ puts "Read-only: #{analysis[:read_only] ? 'Yes' : 'No'}"
17
+ puts "Summary: #{analysis[:summary]}"
18
+
19
+ if analysis[:violations].any?
20
+ puts "\nViolations:"
21
+ analysis[:violations].each do |violation|
22
+ puts " - #{violation[:description]} (#{violation[:severity]})"
23
+ end
24
+ end
25
+ end
26
+
27
+ desc "Execute Ruby code with safety checks"
28
+ task :execute, [:code] => :environment do |task, args|
29
+ code = args[:code]
30
+
31
+ if code.blank?
32
+ puts "Usage: rails rails_active_mcp:execute['User.count']"
33
+ exit 1
34
+ end
35
+
36
+ begin
37
+ result = RailsActiveMcp.execute(code)
38
+
39
+ if result[:success]
40
+ puts "Result: #{result[:return_value_string] || result[:return_value]}"
41
+ puts "Output: #{result[:output]}" if result[:output].present?
42
+ puts "Execution time: #{result[:execution_time]}s" if result[:execution_time]
43
+ else
44
+ puts "Error: #{result[:error]}"
45
+ puts "Error class: #{result[:error_class]}" if result[:error_class]
46
+ end
47
+ rescue => e
48
+ puts "Failed to execute: #{e.message}"
49
+ exit 1
50
+ end
51
+ end
52
+
53
+ desc "Test MCP tools"
54
+ task :test_tools => :environment do
55
+ puts "Testing Rails Active MCP tools..."
56
+
57
+ # Test SafeQueryTool
58
+ puts "\n1. Testing SafeQueryTool..."
59
+ if defined?(User)
60
+ tool = RailsActiveMcp::Tools::SafeQueryTool.new
61
+ result = tool.call(model: "User", method: "count")
62
+ puts " User.count: #{result[:success] ? result[:result] : result[:error]}"
63
+ else
64
+ puts " Skipped (User model not found)"
65
+ end
66
+
67
+ # Test ConsoleExecuteTool
68
+ puts "\n2. Testing ConsoleExecuteTool..."
69
+ tool = RailsActiveMcp::Tools::ConsoleExecuteTool.new
70
+ result = tool.call(code: "1 + 1")
71
+ puts " 1 + 1: #{result[:success] ? result[:return_value] : result[:error]}"
72
+
73
+ # Test DryRunTool
74
+ puts "\n3. Testing DryRunTool..."
75
+ tool = RailsActiveMcp::Tools::DryRunTool.new
76
+ result = tool.call(code: "User.delete_all")
77
+ puts " User.delete_all analysis: #{result[:estimated_risk]} risk"
78
+
79
+ puts "\nAll tools tested!"
80
+ end
81
+
82
+ desc "Show configuration"
83
+ task :config => :environment do
84
+ config = RailsActiveMcp.config
85
+
86
+ puts "Rails Active MCP Configuration:"
87
+ puts " Enabled: #{config.enabled}"
88
+ puts " Safe mode: #{config.safe_mode}"
89
+ puts " Default timeout: #{config.default_timeout}s"
90
+ puts " Max results: #{config.max_results}"
91
+ puts " Log executions: #{config.log_executions}"
92
+ puts " Audit file: #{config.audit_file}"
93
+ puts " Enable mutation tools: #{config.enable_mutation_tools}"
94
+ puts " Execution environment: #{config.execution_environment}"
95
+
96
+ if config.allowed_models.any?
97
+ puts " Allowed models: #{config.allowed_models.join(', ')}"
98
+ end
99
+
100
+ if config.blocked_models.any?
101
+ puts " Blocked models: #{config.blocked_models.join(', ')}"
102
+ end
103
+
104
+ if config.custom_safety_patterns.any?
105
+ puts " Custom safety patterns: #{config.custom_safety_patterns.size}"
106
+ end
107
+ end
108
+
109
+ desc "View audit log"
110
+ task :audit_log, [:lines] => :environment do |task, args|
111
+ lines = args[:lines]&.to_i || 10
112
+ audit_file = RailsActiveMcp.config.audit_file
113
+
114
+ unless File.exist?(audit_file)
115
+ puts "Audit log not found at: #{audit_file}"
116
+ exit 1
117
+ end
118
+
119
+ puts "Last #{lines} entries from audit log:"
120
+ puts "=" * 50
121
+
122
+ File.readlines(audit_file).last(lines).each do |line|
123
+ begin
124
+ entry = JSON.parse(line)
125
+ timestamp = entry['timestamp']
126
+ code = entry['code']
127
+ user = entry.dig('user', 'email') || entry.dig('user', 'environment') || 'unknown'
128
+
129
+ puts "#{timestamp} [#{user}]: #{code}"
130
+
131
+ if entry['type'] == 'error'
132
+ puts " ERROR: #{entry['error']}"
133
+ elsif entry['safety_check'] && !entry['safety_check']['safe']
134
+ puts " SAFETY: #{entry['safety_check']['summary']}"
135
+ end
136
+ puts
137
+ rescue JSON::ParserError
138
+ puts "Invalid JSON entry: #{line}"
139
+ end
140
+ end
141
+ end
142
+
143
+ desc "Clear audit log"
144
+ task :clear_audit_log => :environment do
145
+ audit_file = RailsActiveMcp.config.audit_file
146
+
147
+ if File.exist?(audit_file)
148
+ File.truncate(audit_file, 0)
149
+ puts "Audit log cleared: #{audit_file}"
150
+ else
151
+ puts "Audit log not found: #{audit_file}"
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,61 @@
1
+ module RailsActiveMcp
2
+ module Tools
3
+ class ConsoleExecuteTool < ApplicationMCPTool
4
+ tool_name "console_execute"
5
+ description "Execute Ruby code in Rails console with safety checks"
6
+
7
+ property :code, type: "string", description: 'Ruby code to execute in Rails console', required: true
8
+ property :safe_mode, type: "boolean", description: 'Enable safety checks (default: true)', required: false
9
+ property :timeout, type: "integer", description: 'Timeout in seconds (default: 30)', required: false
10
+ property :capture_output, type: "boolean", description: 'Capture console output (default: true)', required: false
11
+
12
+ def perform
13
+ code = properties[:code]
14
+ safe_mode = properties[:safe_mode]
15
+ timeout = properties[:timeout]
16
+ capture_output = properties.fetch(:capture_output, true)
17
+
18
+ return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
19
+
20
+ executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
21
+
22
+ begin
23
+ result = executor.execute(
24
+ code,
25
+ timeout: timeout,
26
+ safe_mode: safe_mode,
27
+ capture_output: capture_output
28
+ )
29
+
30
+ if result[:success]
31
+ render(text: format_success_result(result))
32
+ else
33
+ render(error: [format_error_result(result)])
34
+ end
35
+ rescue RailsActiveMcp::SafetyError => e
36
+ render(error: ["Safety check failed: #{e.message}"])
37
+ rescue RailsActiveMcp::TimeoutError => e
38
+ render(error: ["Execution timed out: #{e.message}"])
39
+ rescue => e
40
+ render(error: ["Execution failed: #{e.message}"])
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def format_success_result(result)
47
+ output = []
48
+ output << "Code: #{result[:code]}"
49
+ output << "Result: #{result[:return_value_string] || result[:return_value]}"
50
+ output << "Output: #{result[:output]}" if result[:output].present?
51
+ output << "Execution time: #{result[:execution_time]}s" if result[:execution_time]
52
+ output << "Note: #{result[:note]}" if result[:note]
53
+ output.join("\n")
54
+ end
55
+
56
+ def format_error_result(result)
57
+ "Error: #{result[:error]} (#{result[:error_class]})"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ module RailsActiveMcp
2
+ module Tools
3
+ class DryRunTool < ApplicationMCPTool
4
+ tool_name "dry_run"
5
+ description "Analyze Ruby code for safety without executing it"
6
+
7
+ property :code, type: "string", description: 'Ruby code to analyze for safety', required: true
8
+
9
+ def perform
10
+ return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
11
+
12
+ code = properties[:code]
13
+ executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
14
+ analysis = executor.dry_run(code)
15
+
16
+ output = []
17
+ output << "Code: #{analysis[:code]}"
18
+ output << "Safe: #{analysis[:safety_analysis][:safe] ? 'Yes' : 'No'}"
19
+ output << "Read-only: #{analysis[:safety_analysis][:read_only] ? 'Yes' : 'No'}"
20
+ output << "Risk level: #{analysis[:estimated_risk]}"
21
+ output << "Summary: #{analysis[:safety_analysis][:summary]}"
22
+
23
+ if analysis[:safety_analysis][:violations].any?
24
+ output << "\nViolations:"
25
+ analysis[:safety_analysis][:violations].each do |violation|
26
+ output << " - #{violation[:description]} (#{violation[:severity]})"
27
+ end
28
+ end
29
+
30
+ if analysis[:recommendations].any?
31
+ output << "\nRecommendations:"
32
+ analysis[:recommendations].each do |rec|
33
+ output << " - #{rec}"
34
+ end
35
+ end
36
+
37
+ render(text: output.join("\n"))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ module RailsActiveMcp
2
+ module Tools
3
+ class ModelInfoTool < ApplicationMCPTool
4
+ tool_name "model_info"
5
+ description "Get information about Rails models including schema and associations"
6
+
7
+ property :model, type: "string", description: 'Model class name', required: true
8
+ property :include_schema, type: "boolean", description: 'Include database schema information', required: false
9
+ property :include_associations, type: "boolean", description: 'Include model associations', required: false
10
+ property :include_validations, type: "boolean", description: 'Include model validations', required: false
11
+
12
+ def perform
13
+ return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
14
+
15
+ model = properties[:model]
16
+ include_schema = properties.fetch(:include_schema, true)
17
+ include_associations = properties.fetch(:include_associations, true)
18
+ include_validations = properties.fetch(:include_validations, true)
19
+
20
+ begin
21
+ model_class = model.constantize
22
+
23
+ output = []
24
+ output << "Model: #{model}"
25
+ output << "Table: #{model_class.table_name}"
26
+ output << "Primary Key: #{model_class.primary_key}"
27
+
28
+ if include_schema
29
+ output << "\nSchema:"
30
+ model_class.columns.each do |column|
31
+ output << " #{column.name}: #{column.type} (#{column.sql_type})"
32
+ output << " - Null: #{column.null}"
33
+ output << " - Default: #{column.default}" if column.default
34
+ end
35
+ end
36
+
37
+ if include_associations
38
+ output << "\nAssociations:"
39
+ model_class.reflections.each do |name, reflection|
40
+ output << " #{name}: #{reflection.class.name.split('::').last} -> #{reflection.class_name}"
41
+ end
42
+ end
43
+
44
+ if include_validations
45
+ validations = {}
46
+ model_class.validators.each do |validator|
47
+ validator.attributes.each do |attribute|
48
+ validations[attribute] ||= []
49
+ validations[attribute] << validator.class.name.split('::').last
50
+ end
51
+ end
52
+
53
+ if validations.any?
54
+ output << "\nValidations:"
55
+ validations.each do |attr, validators|
56
+ output << " #{attr}: #{validators.join(', ')}"
57
+ end
58
+ end
59
+ end
60
+
61
+ render(text: output.join("\n"))
62
+ rescue NameError
63
+ render(error: ["Model '#{model}' not found"])
64
+ rescue => e
65
+ render(error: ["Error analyzing model: #{e.message}"])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,41 @@
1
+ module RailsActiveMcp
2
+ module Tools
3
+ class SafeQueryTool < ApplicationMCPTool
4
+ tool_name "safe_query"
5
+ description "Execute safe read-only database queries on Rails models"
6
+
7
+ property :model, type: "string", description: 'Model class name (e.g., "User", "Product")', required: true
8
+ property :method, type: "string", description: 'Query method (find, where, count, etc.)', required: true
9
+ property :args, type: "array", description: 'Arguments for the query method', required: false
10
+ property :limit, type: "integer", description: 'Limit results (default: 100)', required: false
11
+
12
+ def perform
13
+ return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
14
+
15
+ model = properties[:model]
16
+ method = properties[:method]
17
+ args = properties[:args] || []
18
+ limit = properties[:limit]
19
+
20
+ executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
21
+
22
+ result = executor.execute_safe_query(
23
+ model: model,
24
+ method: method,
25
+ args: args,
26
+ limit: limit
27
+ )
28
+
29
+ if result[:success]
30
+ output = []
31
+ output << "Query: #{model}.#{method}(#{args.join(', ')})"
32
+ output << "Count: #{result[:count]}"
33
+ output << "Result: #{result[:result].inspect}"
34
+ render(text: output.join("\n"))
35
+ else
36
+ render(error: [result[:error]])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsActiveMcp
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require_relative 'rails_active_mcp/version'
5
+ require_relative 'rails_active_mcp/configuration'
6
+ require_relative 'rails_active_mcp/safety_checker'
7
+ require_relative 'rails_active_mcp/console_executor'
8
+ require_relative 'rails_active_mcp/mcp_server'
9
+ require_relative 'rails_active_mcp/engine' if defined?(Rails)
10
+
11
+
12
+ module RailsActiveMcp
13
+ class Error < StandardError; end
14
+
15
+ class SafetyError < Error; end
16
+
17
+ class ExecutionError < Error; end
18
+
19
+ class TimeoutError < Error; end
20
+
21
+ class << self
22
+ attr_accessor :configuration
23
+
24
+ def configure
25
+ self.configuration ||= Configuration.new
26
+ yield(configuration) if block_given?
27
+ configuration
28
+ end
29
+
30
+ def config
31
+ configuration || configure
32
+ end
33
+
34
+ # Quick access to safety checker
35
+ def safe?(code)
36
+ SafetyChecker.new(config).safe?(code)
37
+ end
38
+
39
+ # Quick execution method
40
+ def execute(code, **options)
41
+ ConsoleExecutor.new(config).execute(code, **options)
42
+ end
43
+
44
+ # Access to MCP server instance
45
+ def server
46
+ @server ||= McpServer.new
47
+ end
48
+
49
+ # Add logger accessor
50
+ attr_accessor :logger
51
+
52
+ def logger
53
+ @logger ||= defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger ? Rails.logger : Logger.new(STDOUT)
54
+ end
55
+ end
56
+ end
57
+
58
+ # Auto-configure for Rails
59
+ require_relative 'rails_active_mcp/railtie' if defined?(Rails)
data/mcp.ru ADDED
@@ -0,0 +1,5 @@
1
+ require_relative "config/environment"
2
+ require_relative "lib/rails_active_mcp"
3
+
4
+ # Run the Rails Active MCP server
5
+ run RailsActiveMcp::McpServer.new
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/rails_active_mcp/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rails-active-mcp'
7
+ spec.version = RailsActiveMcp::VERSION
8
+ spec.authors = ['Brandyn Britton']
9
+ spec.email = ['brandynbb96@gmail.com']
10
+
11
+ spec.summary = 'Rails Console access via Model Context Protocol (MCP)'
12
+ spec.description = 'Secure Rails console access for AI agents through Model Context Protocol with safety features and read-only modes'
13
+ spec.homepage = 'https://github.com/goodpie/rails-active-mcp'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.0.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
26
+ end
27
+ end
28
+
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ # Dependencies
34
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.3.5'
35
+ spec.add_runtime_dependency 'rails', '~> 7.0'
36
+
37
+ spec.add_dependency 'json', '~> 2.0'
38
+ spec.add_dependency 'rack', '~> 3.0'
39
+ spec.add_dependency 'timeout', '~> 0.4'
40
+
41
+ # Development dependencies - keep versions consistent with Gemfile
42
+ spec.add_development_dependency 'factory_bot_rails', '~> 6.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.1'
44
+ spec.add_development_dependency 'rspec-rails'
45
+ spec.add_development_dependency 'rubocop', '~> 1.77'
46
+ spec.add_development_dependency 'rubocop-rails', '~> 2.32'
47
+ spec.add_development_dependency 'rubocop-rspec'
48
+ spec.add_development_dependency 'sqlite3', '~> 2.7'
49
+ end