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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d6b13d9872252c1545e274913eb23db3d29d9e9df89e8943d6bc81b36bc530c
4
- data.tar.gz: 888854cbf2bd61251c0908462deee4ed2579bade5ccb4fb28cf6e050fcd47bdd
3
+ metadata.gz: 77086c307fcceff18aac3bb1141c13a4b3de689342b65b276950156de22b09f0
4
+ data.tar.gz: 7afb0b7e81b51540befce4ef6dacb0aaf731aa3afcbb5135267c1e1311846f79
5
5
  SHA512:
6
- metadata.gz: 7181f3eb02a5cc1d85bb0503df37d5c6985a95a26f3634d3c677e4be51204ed761a5e50041c66817fa341f460d083e036510699b380b119d0506bcadc6ee95fd
7
- data.tar.gz: 7ed062c493c6b9071086a96161dc7f1c63f54f8227a48ad3aa5492c6fd81904abab548e1e4bc9438599ab9a69a068f2ebebe1b9c21a19a8195d3f20d08020748
6
+ metadata.gz: '03796522301b8d8d7c3ab258c67a146847d41ae909534148bc2015f7e11fe0156cbec424218de469f85c2f992958b3831b9b4a51da1799f9235b0c39469d5c4c'
7
+ data.tar.gz: 9511d7eaa3fdd2a3e05fa42cfd4ab7b102508641ce07831bacb8927311c1b275af4fed19e248754d28db28bcf7c5c82cdcb38e0b4ef9d7335bbee8100372214d
@@ -1,3 +1,3 @@
1
1
  module RailsMcpServer
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
@@ -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
- # Use Dir.glob to get matching files
238
- files = Dir.glob(File.join(full_path, pattern))
239
- .map { |f| f.sub("#{$active_project_path}/", "") }
240
- .sort # rubocop:disable Performance/ChainArrayAllocation
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
- log(:debug, "Found #{files.size} files matching pattern")
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.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-03-20 00:00:00.000000000 Z
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.2
90
+ rubygems_version: 3.6.6
91
91
  specification_version: 4
92
92
  summary: MCP server for Rails projects
93
93
  test_files: []