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.
- checksums.yaml +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/discord.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/rails-active-mcp-gem.iml +111 -0
- data/.idea/vcs.xml +6 -0
- data/README.md +369 -0
- data/changelog.md +59 -0
- data/docs/README.md +185 -0
- data/exe/rails-active-mcp-server +24 -0
- data/lib/generators/rails_active_mcp/install/install_generator.rb +37 -0
- data/lib/generators/rails_active_mcp/install/templates/README.md +60 -0
- data/lib/generators/rails_active_mcp/install/templates/initializer.rb +39 -0
- data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -0
- data/lib/rails_active_mcp/configuration.rb +95 -0
- data/lib/rails_active_mcp/console_executor.rb +378 -0
- data/lib/rails_active_mcp/engine.rb +32 -0
- data/lib/rails_active_mcp/mcp_server.rb +374 -0
- data/lib/rails_active_mcp/railtie.rb +48 -0
- data/lib/rails_active_mcp/safety_checker.rb +149 -0
- data/lib/rails_active_mcp/tasks.rake +154 -0
- data/lib/rails_active_mcp/tools/console_execute_tool.rb +61 -0
- data/lib/rails_active_mcp/tools/dry_run_tool.rb +41 -0
- data/lib/rails_active_mcp/tools/model_info_tool.rb +70 -0
- data/lib/rails_active_mcp/tools/safe_query_tool.rb +41 -0
- data/lib/rails_active_mcp/version.rb +5 -0
- data/lib/rails_active_mcp.rb +59 -0
- data/mcp.ru +5 -0
- data/rails_active_mcp.gemspec +49 -0
- metadata +241 -0
@@ -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,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,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
|