rails-mcp-server 1.0.0 → 1.0.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 +4 -4
- data/lib/rails-mcp-server/version.rb +1 -1
- data/lib/rails_mcp_server.rb +664 -5
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77086c307fcceff18aac3bb1141c13a4b3de689342b65b276950156de22b09f0
|
4
|
+
data.tar.gz: 7afb0b7e81b51540befce4ef6dacb0aaf731aa3afcbb5135267c1e1311846f79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '03796522301b8d8d7c3ab258c67a146847d41ae909534148bc2015f7e11fe0156cbec424218de469f85c2f992958b3831b9b4a51da1799f9235b0c39469d5c4c'
|
7
|
+
data.tar.gz: 9511d7eaa3fdd2a3e05fa42cfd4ab7b102508641ce07831bacb8927311c1b275af4fed19e248754d28db28bcf7c5c82cdcb38e0b4ef9d7335bbee8100372214d
|
data/lib/rails_mcp_server.rb
CHANGED
@@ -93,12 +93,22 @@ version RailsMcpServer::VERSION
|
|
93
93
|
def get_directory_structure(path, max_depth: 3, current_depth: 0, prefix: "")
|
94
94
|
return "" if current_depth > max_depth || !File.directory?(path)
|
95
95
|
|
96
|
+
# Define ignored directories
|
97
|
+
ignored_dirs = [
|
98
|
+
".git", "node_modules", "tmp", "log",
|
99
|
+
"storage", "coverage", "public/assets",
|
100
|
+
"public/packs", ".bundle", "vendor/bundle",
|
101
|
+
"vendor/cache"
|
102
|
+
]
|
103
|
+
|
96
104
|
output = ""
|
97
105
|
directories = []
|
98
106
|
files = []
|
99
107
|
|
100
108
|
Dir.foreach(path) do |entry|
|
101
109
|
next if entry == "." || entry == ".."
|
110
|
+
next if ignored_dirs.include?(entry) # Skip ignored directories
|
111
|
+
|
102
112
|
full_path = File.join(path, entry)
|
103
113
|
|
104
114
|
if File.directory?(full_path)
|
@@ -155,6 +165,221 @@ def underscore(string)
|
|
155
165
|
.downcase
|
156
166
|
end
|
157
167
|
|
168
|
+
# Helper method to extract settings from environment files
|
169
|
+
def extract_env_settings(content)
|
170
|
+
settings = {}
|
171
|
+
|
172
|
+
# Match configuration settings
|
173
|
+
content.scan(/config\.([a-zA-Z0-9_.]+)\s*=\s*([^#\n]+)/) do |match|
|
174
|
+
key = match[0].strip
|
175
|
+
value = match[1].strip
|
176
|
+
|
177
|
+
# Clean up the value
|
178
|
+
value = value.chomp(";").strip
|
179
|
+
|
180
|
+
settings[key] = value
|
181
|
+
end
|
182
|
+
|
183
|
+
settings
|
184
|
+
end
|
185
|
+
|
186
|
+
# Helper method to find ENV variable usage in the codebase
|
187
|
+
def find_env_vars_in_codebase(project_path)
|
188
|
+
env_vars = {}
|
189
|
+
|
190
|
+
# Define directories to search
|
191
|
+
search_dirs = [
|
192
|
+
File.join(project_path, "app"),
|
193
|
+
File.join(project_path, "config"),
|
194
|
+
File.join(project_path, "lib")
|
195
|
+
]
|
196
|
+
|
197
|
+
# Define file patterns to search
|
198
|
+
file_patterns = ["*.rb", "*.yml", "*.erb", "*.js"]
|
199
|
+
|
200
|
+
search_dirs.each do |dir|
|
201
|
+
if File.directory?(dir)
|
202
|
+
file_patterns.each do |pattern|
|
203
|
+
Dir.glob(File.join(dir, "**", pattern)).each do |file|
|
204
|
+
content = File.read(file)
|
205
|
+
|
206
|
+
# Extract ENV variables
|
207
|
+
content.scan(/ENV\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
|
208
|
+
env_var = match[0]
|
209
|
+
env_vars[env_var] ||= []
|
210
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
211
|
+
end
|
212
|
+
|
213
|
+
# Also match ENV['VAR'] pattern
|
214
|
+
content.scan(/ENV\s*\.\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
|
215
|
+
env_var = match[0]
|
216
|
+
env_vars[env_var] ||= []
|
217
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
218
|
+
end
|
219
|
+
|
220
|
+
# Also match ENV.fetch('VAR') pattern
|
221
|
+
content.scan(/ENV\s*\.\s*fetch\s*\(\s*['"]([^'"]+)['"]\s*/).each do |match|
|
222
|
+
env_var = match[0]
|
223
|
+
env_vars[env_var] ||= []
|
224
|
+
env_vars[env_var] << file.sub("#{project_path}/", "")
|
225
|
+
end
|
226
|
+
rescue => e
|
227
|
+
log(:error, "Error reading file #{file}: #{e.message}")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
env_vars
|
234
|
+
end
|
235
|
+
|
236
|
+
# Helper method to parse .env files
|
237
|
+
def parse_dotenv_file(file_path)
|
238
|
+
vars = {}
|
239
|
+
|
240
|
+
begin
|
241
|
+
File.readlines(file_path).each do |line| # rubocop:disable Performance/IoReadlines
|
242
|
+
# Skip comments and empty lines
|
243
|
+
next if line.strip.empty? || line.strip.start_with?("#")
|
244
|
+
|
245
|
+
# Parse KEY=value pattern
|
246
|
+
if line =~ /\A([A-Za-z0-9_]+)=(.*)\z/
|
247
|
+
key = $1
|
248
|
+
# Store just the existence of the variable, not its value
|
249
|
+
vars[key] = true
|
250
|
+
end
|
251
|
+
end
|
252
|
+
rescue => e
|
253
|
+
log(:error, "Error parsing .env file #{file_path}: #{e.message}")
|
254
|
+
end
|
255
|
+
|
256
|
+
vars
|
257
|
+
end
|
258
|
+
|
259
|
+
# Helper method to parse database.yml
|
260
|
+
def parse_database_config(file_path)
|
261
|
+
config = {}
|
262
|
+
|
263
|
+
begin
|
264
|
+
# Simple YAML parsing - not handling ERB
|
265
|
+
yaml_content = File.read(file_path)
|
266
|
+
yaml_data = YAML.safe_load(yaml_content) || {}
|
267
|
+
|
268
|
+
# Extract environment configurations
|
269
|
+
%w[development test production staging].each do |env|
|
270
|
+
config[env] = yaml_data[env] if yaml_data[env]
|
271
|
+
end
|
272
|
+
rescue => e
|
273
|
+
log(:error, "Error parsing database.yml: #{e.message}")
|
274
|
+
end
|
275
|
+
|
276
|
+
config
|
277
|
+
end
|
278
|
+
|
279
|
+
# Helper method to compare environment settings
|
280
|
+
def compare_environment_settings(env_settings)
|
281
|
+
result = {
|
282
|
+
unique_settings: {},
|
283
|
+
different_values: {}
|
284
|
+
}
|
285
|
+
|
286
|
+
# Get all settings across all environments
|
287
|
+
all_settings = env_settings.values.map(&:keys).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
288
|
+
|
289
|
+
# Find settings unique to certain environments
|
290
|
+
env_settings.each do |env, settings|
|
291
|
+
unique = settings.keys - (all_settings - settings.keys)
|
292
|
+
result[:unique_settings][env] = unique if unique.any?
|
293
|
+
end
|
294
|
+
|
295
|
+
# Find settings with different values across environments
|
296
|
+
all_settings.each do |setting|
|
297
|
+
values = {}
|
298
|
+
|
299
|
+
env_settings.each do |env, settings|
|
300
|
+
values[env] = settings[setting] if settings[setting]
|
301
|
+
end
|
302
|
+
|
303
|
+
# Only include if there are different values
|
304
|
+
if values.values.uniq.size > 1
|
305
|
+
result[:different_values][setting] = values
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
result
|
310
|
+
end
|
311
|
+
|
312
|
+
# Helper method to find missing ENV variables
|
313
|
+
def find_missing_env_vars(env_vars_in_code, dotenv_vars)
|
314
|
+
missing_vars = {}
|
315
|
+
|
316
|
+
# Check each ENV variable used in code
|
317
|
+
env_vars_in_code.each do |var, files|
|
318
|
+
# Environments where the variable is missing
|
319
|
+
missing_in = []
|
320
|
+
|
321
|
+
# Check in each .env file
|
322
|
+
if dotenv_vars.empty?
|
323
|
+
missing_in << "all environments (no .env files found)"
|
324
|
+
else
|
325
|
+
dotenv_vars.each do |env_file, vars|
|
326
|
+
env_name = env_file.gsub(/^\.env\.?|\.local$/, "")
|
327
|
+
env_name = "development" if env_name.empty?
|
328
|
+
|
329
|
+
if !vars.key?(var)
|
330
|
+
missing_in << env_name
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
missing_vars[var] = missing_in if missing_in.any?
|
336
|
+
end
|
337
|
+
|
338
|
+
missing_vars
|
339
|
+
end
|
340
|
+
|
341
|
+
# Helper method to check for security issues
|
342
|
+
def check_security_configuration(env_settings, database_config)
|
343
|
+
findings = []
|
344
|
+
|
345
|
+
# Check for common security settings
|
346
|
+
env_settings.each do |env, settings|
|
347
|
+
# Check for secure cookies in production
|
348
|
+
if env == "production"
|
349
|
+
if settings["cookies.secure"] == "false"
|
350
|
+
findings << "Production has cookies.secure = false"
|
351
|
+
end
|
352
|
+
|
353
|
+
if settings["session_store.secure"] == "false"
|
354
|
+
findings << "Production has session_store.secure = false"
|
355
|
+
end
|
356
|
+
|
357
|
+
# Force SSL
|
358
|
+
if settings["force_ssl"] == "false"
|
359
|
+
findings << "Production has force_ssl = false"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Check for CSRF protection
|
364
|
+
if settings["action_controller.default_protect_from_forgery"] == "false"
|
365
|
+
findings << "#{env} has CSRF protection disabled"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Check for hardcoded credentials in database.yml
|
370
|
+
database_config.each do |env, config|
|
371
|
+
if config["username"] && !config["username"].include?("ENV")
|
372
|
+
findings << "Database username hardcoded in database.yml for #{env}"
|
373
|
+
end
|
374
|
+
|
375
|
+
if config["password"] && !config["password"].include?("ENV")
|
376
|
+
findings << "Database password hardcoded in database.yml for #{env}"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
findings
|
381
|
+
end
|
382
|
+
|
158
383
|
# Define tools using the mcp-rb DSL
|
159
384
|
tool "switch_project" do
|
160
385
|
description "Change the active Rails project to interact with a different codebase. Must be called before using other tools. Available projects are defined in the projects.yml configuration file."
|
@@ -234,12 +459,28 @@ tool "list_files" do
|
|
234
459
|
raise "Directory '#{directory}' not found in the project."
|
235
460
|
end
|
236
461
|
|
237
|
-
#
|
238
|
-
|
239
|
-
|
240
|
-
|
462
|
+
# Check if this is a git repository
|
463
|
+
is_git_repo = system("cd #{$active_project_path} && git rev-parse --is-inside-work-tree > /dev/null 2>&1")
|
464
|
+
|
465
|
+
if is_git_repo
|
466
|
+
log(:debug, "Project is a git repository, using git ls-files")
|
241
467
|
|
242
|
-
|
468
|
+
# Use git ls-files for tracked files
|
469
|
+
relative_dir = directory.empty? ? "" : "#{directory}/"
|
470
|
+
git_cmd = "cd #{$active_project_path} && git ls-files --cached --others --exclude-standard #{relative_dir}#{pattern}"
|
471
|
+
|
472
|
+
files = `#{git_cmd}`.split("\n").map(&:strip).sort # rubocop:disable Performance/ChainArrayAllocation
|
473
|
+
else
|
474
|
+
log(:debug, "Project is not a git repository or git not available, using Dir.glob")
|
475
|
+
|
476
|
+
# Use Dir.glob as fallback
|
477
|
+
files = Dir.glob(File.join(full_path, pattern))
|
478
|
+
.map { |f| f.sub("#{$active_project_path}/", "") }
|
479
|
+
.reject { |file| file.start_with?(".git/", "node_modules/") } # Explicitly filter .git and node_modules directories # rubocop:disable Performance/ChainArrayAllocation
|
480
|
+
.sort # rubocop:disable Performance/ChainArrayAllocation
|
481
|
+
end
|
482
|
+
|
483
|
+
log(:debug, "Found #{files.size} files matching pattern (respecting .gitignore and ignoring node_modules)")
|
243
484
|
|
244
485
|
"Files in #{directory.empty? ? "project root" : directory} matching '#{pattern}':\n\n#{files.join("\n")}"
|
245
486
|
end
|
@@ -506,4 +747,422 @@ tool "get_schema" do
|
|
506
747
|
end
|
507
748
|
end
|
508
749
|
end
|
750
|
+
|
751
|
+
tool "analyze_controller_views" do
|
752
|
+
description "Analyze the relationships between controllers, their actions, and corresponding views to understand the application's UI flow."
|
753
|
+
|
754
|
+
argument :controller_name, String, required: false,
|
755
|
+
description: "Name of a specific controller to analyze (e.g., 'UsersController' or 'users'). If omitted, all controllers will be analyzed."
|
756
|
+
|
757
|
+
call do |args|
|
758
|
+
unless $active_project
|
759
|
+
raise "No active project. Please switch to a project first."
|
760
|
+
end
|
761
|
+
|
762
|
+
controller_name = args[:controller_name]
|
763
|
+
|
764
|
+
# Find all controllers
|
765
|
+
controllers_dir = File.join($active_project_path, "app", "controllers")
|
766
|
+
unless File.directory?(controllers_dir)
|
767
|
+
raise "Controllers directory not found at app/controllers."
|
768
|
+
end
|
769
|
+
|
770
|
+
# Get all controller files
|
771
|
+
controller_files = Dir.glob(File.join(controllers_dir, "**", "*_controller.rb"))
|
772
|
+
|
773
|
+
if controller_files.empty?
|
774
|
+
raise "No controllers found in the project."
|
775
|
+
end
|
776
|
+
|
777
|
+
# If a specific controller was requested, filter the files
|
778
|
+
if controller_name
|
779
|
+
# Normalize controller name (allow both 'users' and 'UsersController')
|
780
|
+
controller_name = "#{controller_name.sub(/_?controller$/i, "").downcase}_controller.rb"
|
781
|
+
controller_files = controller_files.select { |f| File.basename(f).downcase == controller_name }
|
782
|
+
|
783
|
+
if controller_files.empty?
|
784
|
+
raise "Controller '#{args[:controller_name]}' not found."
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
# Parse controllers to extract actions
|
789
|
+
controllers_data = {}
|
790
|
+
|
791
|
+
controller_files.each do |file_path|
|
792
|
+
file_content = File.read(file_path)
|
793
|
+
controller_class = File.basename(file_path, ".rb").gsub(/_controller$/i, "").camelize + "Controller"
|
794
|
+
|
795
|
+
# Extract controller actions (methods that are not private/protected)
|
796
|
+
actions = []
|
797
|
+
action_matches = file_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
798
|
+
|
799
|
+
# Find where private/protected begins
|
800
|
+
private_index = file_content =~ /^\s*(private|protected)/
|
801
|
+
|
802
|
+
if private_index
|
803
|
+
# Get the actions defined before private/protected
|
804
|
+
private_content = file_content[private_index..-1]
|
805
|
+
private_methods = private_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
806
|
+
actions = action_matches - private_methods
|
807
|
+
else
|
808
|
+
actions = action_matches
|
809
|
+
end
|
810
|
+
|
811
|
+
# Remove Rails controller lifecycle methods
|
812
|
+
lifecycle_methods = %w[initialize action_name controller_name params response]
|
813
|
+
actions -= lifecycle_methods
|
814
|
+
|
815
|
+
# Get routes mapped to this controller
|
816
|
+
routes_cmd = "cd #{$active_project_path} && bin/rails routes -c #{controller_class}"
|
817
|
+
routes_output = `#{routes_cmd}`.strip
|
818
|
+
|
819
|
+
routes = {}
|
820
|
+
if routes_output && !routes_output.empty?
|
821
|
+
routes_output.split("\n").each do |line|
|
822
|
+
next if line.include?("(erb):") || line.include?("Prefix") || line.strip.empty?
|
823
|
+
parts = line.strip.split(/\s+/)
|
824
|
+
if parts.size >= 4
|
825
|
+
# Get action name from the rails routes output
|
826
|
+
action = parts[1].to_s.strip
|
827
|
+
if actions.include?(action)
|
828
|
+
verb = parts[0].to_s.strip
|
829
|
+
path = parts[2].to_s.strip
|
830
|
+
routes[action] = {verb: verb, path: path}
|
831
|
+
end
|
832
|
+
end
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
# Find views for each action
|
837
|
+
views_dir = File.join($active_project_path, "app", "views", File.basename(file_path, "_controller.rb"))
|
838
|
+
views = {}
|
839
|
+
|
840
|
+
if File.directory?(views_dir)
|
841
|
+
actions.each do |action|
|
842
|
+
# Look for view templates with various extensions
|
843
|
+
view_files = Dir.glob(File.join(views_dir, "#{action}.*"))
|
844
|
+
if view_files.any?
|
845
|
+
views[action] = {
|
846
|
+
templates: view_files.map { |f| f.sub("#{$active_project_path}/", "") },
|
847
|
+
partials: []
|
848
|
+
}
|
849
|
+
|
850
|
+
# Look for partials used in this template
|
851
|
+
view_files.each do |view_file|
|
852
|
+
if File.file?(view_file)
|
853
|
+
view_content = File.read(view_file)
|
854
|
+
# Find render calls with partials
|
855
|
+
partial_matches = view_content.scan(/render\s+(?:partial:|:partial\s+=>\s+|:partial\s*=>|partial:)\s*["']([^"']+)["']/).flatten
|
856
|
+
views[action][:partials] += partial_matches if partial_matches.any?
|
857
|
+
|
858
|
+
# Find instance variables used in the view
|
859
|
+
instance_vars = view_content.scan(/@([a-zA-Z0-9_]+)/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
860
|
+
views[action][:instance_variables] = instance_vars if instance_vars.any?
|
861
|
+
|
862
|
+
# Look for Stimulus controllers
|
863
|
+
stimulus_controllers = view_content.scan(/data-controller="([^"]+)"/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
864
|
+
views[action][:stimulus_controllers] = stimulus_controllers if stimulus_controllers.any?
|
865
|
+
end
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
# Extract instance variables set in the controller action
|
872
|
+
instance_vars_in_controller = {}
|
873
|
+
actions.each do |action|
|
874
|
+
# Find the action method in the controller
|
875
|
+
action_match = file_content.match(/def\s+#{action}\b(.*?)(?:(?:def|private|protected|public)\b|\z)/m)
|
876
|
+
if action_match && action_match[1]
|
877
|
+
action_body = action_match[1]
|
878
|
+
# Find instance variable assignments
|
879
|
+
vars = action_body.scan(/@([a-zA-Z0-9_]+)\s*=/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
880
|
+
instance_vars_in_controller[action] = vars if vars.any?
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
controllers_data[controller_class] = {
|
885
|
+
file: file_path.sub("#{$active_project_path}/", ""),
|
886
|
+
actions: actions,
|
887
|
+
routes: routes,
|
888
|
+
views: views,
|
889
|
+
instance_variables: instance_vars_in_controller
|
890
|
+
}
|
891
|
+
rescue => e
|
892
|
+
log(:error, "Error parsing controller #{file_path}: #{e.message}")
|
893
|
+
end
|
894
|
+
|
895
|
+
# Format the output
|
896
|
+
output = []
|
897
|
+
|
898
|
+
controllers_data.each do |controller, data|
|
899
|
+
output << "Controller: #{controller}"
|
900
|
+
output << " File: #{data[:file]}"
|
901
|
+
output << " Actions: #{data[:actions].size}"
|
902
|
+
|
903
|
+
data[:actions].each do |action|
|
904
|
+
output << " Action: #{action}"
|
905
|
+
|
906
|
+
# Show route if available
|
907
|
+
if data[:routes] && data[:routes][action]
|
908
|
+
route = data[:routes][action]
|
909
|
+
output << " Route: [#{route[:verb]}] #{route[:path]}"
|
910
|
+
else
|
911
|
+
output << " Route: Not mapped to a route"
|
912
|
+
end
|
913
|
+
|
914
|
+
# Show view templates if available
|
915
|
+
if data[:views] && data[:views][action]
|
916
|
+
view_data = data[:views][action]
|
917
|
+
|
918
|
+
output << " View Templates:"
|
919
|
+
view_data[:templates].each do |template|
|
920
|
+
output << " - #{template}"
|
921
|
+
end
|
922
|
+
|
923
|
+
# Show partials
|
924
|
+
if view_data[:partials]&.any?
|
925
|
+
output << " Partials Used:"
|
926
|
+
view_data[:partials].uniq.each do |partial|
|
927
|
+
output << " - #{partial}"
|
928
|
+
end
|
929
|
+
end
|
930
|
+
|
931
|
+
# Show Stimulus controllers
|
932
|
+
if view_data[:stimulus_controllers]&.any?
|
933
|
+
output << " Stimulus Controllers:"
|
934
|
+
view_data[:stimulus_controllers].each do |controller|
|
935
|
+
output << " - #{controller}"
|
936
|
+
end
|
937
|
+
end
|
938
|
+
|
939
|
+
# Show instance variables used in views
|
940
|
+
if view_data[:instance_variables]&.any?
|
941
|
+
output << " Instance Variables Used in View:"
|
942
|
+
view_data[:instance_variables].sort.each do |var|
|
943
|
+
output << " - @#{var}"
|
944
|
+
end
|
945
|
+
end
|
946
|
+
else
|
947
|
+
output << " View: No view template found"
|
948
|
+
end
|
949
|
+
|
950
|
+
# Show instance variables set in controller
|
951
|
+
if data[:instance_variables] && data[:instance_variables][action]
|
952
|
+
output << " Instance Variables Set in Controller:"
|
953
|
+
data[:instance_variables][action].sort.each do |var|
|
954
|
+
output << " - @#{var}"
|
955
|
+
end
|
956
|
+
end
|
957
|
+
|
958
|
+
output << ""
|
959
|
+
end
|
960
|
+
|
961
|
+
output << "-------------------------"
|
962
|
+
end
|
963
|
+
|
964
|
+
output.join("\n")
|
965
|
+
end
|
966
|
+
end
|
967
|
+
|
968
|
+
tool "analyze_environment_config" do
|
969
|
+
description "Analyze environment configurations to identify inconsistencies, security issues, and missing variables across environments."
|
970
|
+
|
971
|
+
call do |args|
|
972
|
+
unless $active_project
|
973
|
+
raise "No active project. Please switch to a project first."
|
974
|
+
end
|
975
|
+
|
976
|
+
# Check for required directories and files
|
977
|
+
env_dir = File.join($active_project_path, "config", "environments")
|
978
|
+
unless File.directory?(env_dir)
|
979
|
+
raise "Environment configuration directory not found at config/environments."
|
980
|
+
end
|
981
|
+
|
982
|
+
# Initialize data structures
|
983
|
+
env_files = {}
|
984
|
+
env_settings = {}
|
985
|
+
|
986
|
+
# 1. Parse environment files
|
987
|
+
Dir.glob(File.join(env_dir, "*.rb")).each do |file|
|
988
|
+
env_name = File.basename(file, ".rb")
|
989
|
+
env_files[env_name] = file
|
990
|
+
env_content = File.read(file)
|
991
|
+
|
992
|
+
# Extract settings from environment files
|
993
|
+
env_settings[env_name] = extract_env_settings(env_content)
|
994
|
+
end
|
995
|
+
|
996
|
+
# 2. Find ENV variable usage across the codebase
|
997
|
+
env_vars_in_code = find_env_vars_in_codebase($active_project_path)
|
998
|
+
|
999
|
+
# 3. Check for .env files and their variables
|
1000
|
+
dotenv_files = {}
|
1001
|
+
dotenv_vars = {}
|
1002
|
+
|
1003
|
+
# Common .env file patterns
|
1004
|
+
dotenv_patterns = [
|
1005
|
+
".env",
|
1006
|
+
".env.development",
|
1007
|
+
".env.test",
|
1008
|
+
".env.production",
|
1009
|
+
".env.local",
|
1010
|
+
".env.development.local",
|
1011
|
+
".env.test.local",
|
1012
|
+
".env.production.local"
|
1013
|
+
]
|
1014
|
+
|
1015
|
+
dotenv_patterns.each do |pattern|
|
1016
|
+
file_path = File.join($active_project_path, pattern)
|
1017
|
+
if File.exist?(file_path)
|
1018
|
+
dotenv_files[pattern] = file_path
|
1019
|
+
dotenv_vars[pattern] = parse_dotenv_file(file_path)
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
# 4. Check credentials files
|
1024
|
+
credentials_files = {}
|
1025
|
+
credentials_key_file = File.join($active_project_path, "config", "master.key")
|
1026
|
+
credentials_file = File.join($active_project_path, "config", "credentials.yml.enc")
|
1027
|
+
|
1028
|
+
if File.exist?(credentials_file)
|
1029
|
+
credentials_files["credentials.yml.enc"] = credentials_file
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
# Environment-specific credentials files
|
1033
|
+
Dir.glob(File.join($active_project_path, "config", "credentials", "*.yml.enc")).each do |file|
|
1034
|
+
env_name = File.basename(file, ".yml.enc")
|
1035
|
+
credentials_files["credentials/#{env_name}.yml.enc"] = file
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
# 5. Check database configuration
|
1039
|
+
database_config_file = File.join($active_project_path, "config", "database.yml")
|
1040
|
+
database_config = {}
|
1041
|
+
|
1042
|
+
if File.exist?(database_config_file)
|
1043
|
+
database_config = parse_database_config(database_config_file)
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
# 6. Generate findings
|
1047
|
+
|
1048
|
+
# 6.1. Compare environment settings
|
1049
|
+
env_diff = compare_environment_settings(env_settings)
|
1050
|
+
|
1051
|
+
# 6.2. Find missing ENV variables
|
1052
|
+
missing_env_vars = find_missing_env_vars(env_vars_in_code, dotenv_vars)
|
1053
|
+
|
1054
|
+
# 6.3. Check for potential security issues
|
1055
|
+
security_findings = check_security_configuration(env_settings, database_config)
|
1056
|
+
|
1057
|
+
# Format the output
|
1058
|
+
output = []
|
1059
|
+
|
1060
|
+
# Environment files summary
|
1061
|
+
output << "Environment Configuration Analysis"
|
1062
|
+
output << "=================================="
|
1063
|
+
output << ""
|
1064
|
+
output << "Environment Files:"
|
1065
|
+
env_files.each do |env, file|
|
1066
|
+
output << " - #{env}: #{file.sub("#{$active_project_path}/", "")}"
|
1067
|
+
end
|
1068
|
+
output << ""
|
1069
|
+
|
1070
|
+
# Environment variables summary
|
1071
|
+
output << "Environment Variables Usage:"
|
1072
|
+
output << " Total unique ENV variables found in codebase: #{env_vars_in_code.keys.size}"
|
1073
|
+
output << ""
|
1074
|
+
|
1075
|
+
# Missing ENV variables
|
1076
|
+
if missing_env_vars.any?
|
1077
|
+
output << "Missing ENV Variables:"
|
1078
|
+
missing_env_vars.each do |env_var, environments|
|
1079
|
+
output << " - #{env_var}: Used in codebase but missing in #{environments.join(", ")}"
|
1080
|
+
end
|
1081
|
+
else
|
1082
|
+
output << "All ENV variables appear to be defined in at least one .env file."
|
1083
|
+
end
|
1084
|
+
output << ""
|
1085
|
+
|
1086
|
+
# Environment differences
|
1087
|
+
if env_diff[:unique_settings].any?
|
1088
|
+
output << "Environment-Specific Settings:"
|
1089
|
+
env_diff[:unique_settings].each do |env, settings|
|
1090
|
+
output << " #{env}:"
|
1091
|
+
settings.each do |setting|
|
1092
|
+
output << " - #{setting}"
|
1093
|
+
end
|
1094
|
+
end
|
1095
|
+
output << ""
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
if env_diff[:different_values].any?
|
1099
|
+
output << "Settings with Different Values Across Environments:"
|
1100
|
+
env_diff[:different_values].each do |setting, values|
|
1101
|
+
output << " #{setting}:"
|
1102
|
+
values.each do |env, value|
|
1103
|
+
output << " - #{env}: #{value}"
|
1104
|
+
end
|
1105
|
+
end
|
1106
|
+
output << ""
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
# Credentials files
|
1110
|
+
output << "Credentials Management:"
|
1111
|
+
if credentials_files.any?
|
1112
|
+
output << " Encrypted credentials files found:"
|
1113
|
+
credentials_files.each do |name, file|
|
1114
|
+
output << " - #{name}"
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
output << if File.exist?(credentials_key_file)
|
1118
|
+
" Master key file exists (config/master.key)"
|
1119
|
+
else
|
1120
|
+
" Warning: No master.key file found. Credentials are likely managed through RAILS_MASTER_KEY environment variable."
|
1121
|
+
end
|
1122
|
+
else
|
1123
|
+
output << " No encrypted credentials files found. The application may be using ENV variables exclusively."
|
1124
|
+
end
|
1125
|
+
output << ""
|
1126
|
+
|
1127
|
+
# Database configuration
|
1128
|
+
output << "Database Configuration:"
|
1129
|
+
if database_config.any?
|
1130
|
+
database_config.each do |env, config|
|
1131
|
+
output << " #{env}:"
|
1132
|
+
# Show connection details without exposing passwords
|
1133
|
+
if config["adapter"]
|
1134
|
+
output << " - Adapter: #{config["adapter"]}"
|
1135
|
+
end
|
1136
|
+
if config["host"] && config["host"] != "localhost" && config["host"] != "127.0.0.1"
|
1137
|
+
output << " - Host: #{config["host"]}"
|
1138
|
+
end
|
1139
|
+
if config["database"]
|
1140
|
+
output << " - Database: #{config["database"]}"
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
# Check for credentials in database.yml
|
1144
|
+
if config["username"] && !config["username"].include?("ENV")
|
1145
|
+
output << " - Warning: Database username hardcoded in database.yml"
|
1146
|
+
end
|
1147
|
+
if config["password"] && !config["password"].include?("ENV")
|
1148
|
+
output << " - Warning: Database password hardcoded in database.yml"
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
else
|
1152
|
+
output << " Could not parse database configuration."
|
1153
|
+
end
|
1154
|
+
output << ""
|
1155
|
+
|
1156
|
+
# Security findings
|
1157
|
+
if security_findings.any?
|
1158
|
+
output << "Security Configuration Findings:"
|
1159
|
+
security_findings.each do |finding|
|
1160
|
+
output << " - #{finding}"
|
1161
|
+
end
|
1162
|
+
output << ""
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
output.join("\n")
|
1166
|
+
end
|
1167
|
+
end
|
509
1168
|
# rubocop:enable Style/GlobalVars
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-mcp-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mario Alberto Chávez Cárdenas
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-12 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: mcp-rb
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0'
|
89
89
|
requirements: []
|
90
|
-
rubygems_version: 3.6.
|
90
|
+
rubygems_version: 3.6.6
|
91
91
|
specification_version: 4
|
92
92
|
summary: MCP server for Rails projects
|
93
93
|
test_files: []
|