rails-mcp-server 1.2.3 → 1.4.0

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +168 -166
  3. data/docs/AGENT.md +345 -0
  4. data/exe/rails-mcp-config +1411 -0
  5. data/exe/rails-mcp-server +23 -10
  6. data/exe/rails-mcp-setup-claude +1 -1
  7. data/lib/rails-mcp-server/analyzers/analyze_controller_views.rb +253 -0
  8. data/lib/rails-mcp-server/analyzers/analyze_environment_config.rb +79 -0
  9. data/lib/rails-mcp-server/analyzers/analyze_models.rb +251 -0
  10. data/lib/rails-mcp-server/analyzers/base_analyzer.rb +42 -0
  11. data/lib/rails-mcp-server/analyzers/get_file.rb +40 -0
  12. data/lib/rails-mcp-server/analyzers/get_routes.rb +212 -0
  13. data/lib/rails-mcp-server/analyzers/get_schema.rb +216 -0
  14. data/lib/rails-mcp-server/analyzers/list_files.rb +43 -0
  15. data/lib/rails-mcp-server/analyzers/load_guide.rb +84 -0
  16. data/lib/rails-mcp-server/analyzers/project_info.rb +136 -0
  17. data/lib/rails-mcp-server/tools/base_tool.rb +2 -0
  18. data/lib/rails-mcp-server/tools/execute_ruby.rb +409 -0
  19. data/lib/rails-mcp-server/tools/execute_tool.rb +115 -0
  20. data/lib/rails-mcp-server/tools/search_tools.rb +186 -0
  21. data/lib/rails-mcp-server/tools/switch_project.rb +16 -1
  22. data/lib/rails-mcp-server/version.rb +1 -1
  23. data/lib/rails_mcp_server.rb +19 -53
  24. metadata +65 -18
  25. data/lib/rails-mcp-server/extensions/resource_templating.rb +0 -182
  26. data/lib/rails-mcp-server/extensions/server_templating.rb +0 -333
  27. data/lib/rails-mcp-server/tools/analyze_controller_views.rb +0 -239
  28. data/lib/rails-mcp-server/tools/analyze_environment_config.rb +0 -427
  29. data/lib/rails-mcp-server/tools/analyze_models.rb +0 -116
  30. data/lib/rails-mcp-server/tools/get_file.rb +0 -55
  31. data/lib/rails-mcp-server/tools/get_routes.rb +0 -24
  32. data/lib/rails-mcp-server/tools/get_schema.rb +0 -141
  33. data/lib/rails-mcp-server/tools/list_files.rb +0 -54
  34. data/lib/rails-mcp-server/tools/load_guide.rb +0 -370
  35. data/lib/rails-mcp-server/tools/project_info.rb +0 -86
@@ -1,427 +0,0 @@
1
- module RailsMcpServer
2
- class AnalyzeEnvironmentConfig < BaseTool
3
- tool_name "analyze_environment_config"
4
-
5
- description "Analyze environment configurations to identify inconsistencies, security issues, and missing variables across environments."
6
-
7
- def call
8
- unless current_project
9
- message = "No active project. Please switch to a project first."
10
- log(:warn, message)
11
-
12
- return message
13
- end
14
-
15
- # Check for required directories and files
16
- env_dir = File.join(active_project_path, "config", "environments")
17
- unless File.directory?(env_dir)
18
- message = "Environment configuration directory not found at config/environments."
19
- log(:warn, message)
20
-
21
- return message
22
- end
23
-
24
- # Initialize data structures
25
- env_files = {}
26
- env_settings = {}
27
-
28
- # 1. Parse environment files
29
- Dir.glob(File.join(env_dir, "*.rb")).each do |file|
30
- env_name = File.basename(file, ".rb")
31
- env_files[env_name] = file
32
- env_content = File.read(file)
33
-
34
- # Extract settings from environment files
35
- env_settings[env_name] = extract_env_settings(env_content)
36
- end
37
-
38
- # 2. Find ENV variable usage across the codebase
39
- env_vars_in_code = find_env_vars_in_codebase(active_project_path)
40
-
41
- # 3. Check for .env files and their variables
42
- dotenv_files = {}
43
- dotenv_vars = {}
44
-
45
- # Common .env file patterns
46
- dotenv_patterns = [
47
- ".env",
48
- ".env.development",
49
- ".env.test",
50
- ".env.production",
51
- ".env.local",
52
- ".env.development.local",
53
- ".env.test.local",
54
- ".env.production.local"
55
- ]
56
-
57
- dotenv_patterns.each do |pattern|
58
- file_path = File.join(active_project_path, pattern)
59
- if File.exist?(file_path)
60
- dotenv_files[pattern] = file_path
61
- dotenv_vars[pattern] = parse_dotenv_file(file_path)
62
- end
63
- end
64
-
65
- # 4. Check credentials files
66
- credentials_files = {}
67
- credentials_key_file = File.join(active_project_path, "config", "master.key")
68
- credentials_file = File.join(active_project_path, "config", "credentials.yml.enc")
69
-
70
- if File.exist?(credentials_file)
71
- credentials_files["credentials.yml.enc"] = credentials_file
72
- end
73
-
74
- # Environment-specific credentials files
75
- Dir.glob(File.join(active_project_path, "config", "credentials", "*.yml.enc")).each do |file|
76
- env_name = File.basename(file, ".yml.enc")
77
- credentials_files["credentials/#{env_name}.yml.enc"] = file
78
- end
79
-
80
- # 5. Check database configuration
81
- database_config_file = File.join(active_project_path, "config", "database.yml")
82
- database_config = {}
83
-
84
- if File.exist?(database_config_file)
85
- database_config = parse_database_config(database_config_file)
86
- end
87
-
88
- # 6. Generate findings
89
-
90
- # 6.1. Compare environment settings
91
- env_diff = compare_environment_settings(env_settings)
92
-
93
- # 6.2. Find missing ENV variables
94
- missing_env_vars = find_missing_env_vars(env_vars_in_code, dotenv_vars)
95
-
96
- # 6.3. Check for potential security issues
97
- security_findings = check_security_configuration(env_settings, database_config)
98
-
99
- # Format the output
100
- output = []
101
-
102
- # Environment files summary
103
- output << "Environment Configuration Analysis"
104
- output << "=================================="
105
- output << ""
106
- output << "Environment Files:"
107
- env_files.each do |env, file|
108
- output << " - #{env}: #{file.sub("#{active_project_path}/", "")}"
109
- end
110
- output << ""
111
-
112
- # Environment variables summary
113
- output << "Environment Variables Usage:"
114
- output << " Total unique ENV variables found in codebase: #{env_vars_in_code.keys.size}"
115
- output << ""
116
-
117
- # Missing ENV variables
118
- if missing_env_vars.any?
119
- output << "Missing ENV Variables:"
120
- missing_env_vars.each do |env_var, environments|
121
- output << " - #{env_var}: Used in codebase but missing in #{environments.join(", ")}"
122
- end
123
- else
124
- output << "All ENV variables appear to be defined in at least one .env file."
125
- end
126
- output << ""
127
-
128
- # Environment differences
129
- if env_diff[:unique_settings].any?
130
- output << "Environment-Specific Settings:"
131
- env_diff[:unique_settings].each do |env, settings|
132
- output << " #{env}:"
133
- settings.each do |setting|
134
- output << " - #{setting}"
135
- end
136
- end
137
- output << ""
138
- end
139
-
140
- if env_diff[:different_values].any?
141
- output << "Settings with Different Values Across Environments:"
142
- env_diff[:different_values].each do |setting, values|
143
- output << " #{setting}:"
144
- values.each do |env, value|
145
- output << " - #{env}: #{value}"
146
- end
147
- end
148
- output << ""
149
- end
150
-
151
- # Credentials files
152
- output << "Credentials Management:"
153
- if credentials_files.any?
154
- output << " Encrypted credentials files found:"
155
- credentials_files.each do |name, file|
156
- output << " - #{name}"
157
- end
158
-
159
- output << if File.exist?(credentials_key_file)
160
- " Master key file exists (config/master.key)"
161
- else
162
- " Warning: No master.key file found. Credentials are likely managed through RAILS_MASTER_KEY environment variable."
163
- end
164
- else
165
- output << " No encrypted credentials files found. The application may be using ENV variables exclusively."
166
- end
167
- output << ""
168
-
169
- # Database configuration
170
- output << "Database Configuration:"
171
- if database_config.any?
172
- database_config.each do |env, config|
173
- output << " #{env}:"
174
- # Show connection details without exposing passwords
175
- if config["adapter"]
176
- output << " - Adapter: #{config["adapter"]}"
177
- end
178
- if config["host"] && config["host"] != "localhost" && config["host"] != "127.0.0.1"
179
- output << " - Host: #{config["host"]}"
180
- end
181
- if config["database"]
182
- output << " - Database: #{config["database"]}"
183
- end
184
-
185
- # Check for credentials in database.yml
186
- if config["username"] && !config["username"].include?("ENV")
187
- output << " - Warning: Database username hardcoded in database.yml"
188
- end
189
- if config["password"] && !config["password"].include?("ENV")
190
- output << " - Warning: Database password hardcoded in database.yml"
191
- end
192
- end
193
- else
194
- output << " Could not parse database configuration."
195
- end
196
- output << ""
197
-
198
- # Security findings
199
- if security_findings.any?
200
- output << "Security Configuration Findings:"
201
- security_findings.each do |finding|
202
- output << " - #{finding}"
203
- end
204
- output << ""
205
- end
206
-
207
- output.join("\n")
208
- end
209
-
210
- private
211
-
212
- # Helper method to extract settings from environment files
213
- def extract_env_settings(content)
214
- settings = {}
215
-
216
- # Match configuration settings
217
- content.scan(/config\.([a-zA-Z0-9_.]+)\s*=\s*([^#\n]+)/) do |match|
218
- key = match[0].strip
219
- value = match[1].strip
220
-
221
- # Clean up the value
222
- value = value.chomp(";").strip
223
-
224
- settings[key] = value
225
- end
226
-
227
- settings
228
- end
229
-
230
- # Helper method to find ENV variable usage in the codebase
231
- def find_env_vars_in_codebase(project_path)
232
- env_vars = {}
233
-
234
- # Define directories to search
235
- search_dirs = [
236
- File.join(project_path, "app"),
237
- File.join(project_path, "config"),
238
- File.join(project_path, "lib")
239
- ]
240
-
241
- # Define file patterns to search
242
- file_patterns = ["*.rb", "*.yml", "*.erb", "*.js"]
243
-
244
- search_dirs.each do |dir|
245
- if File.directory?(dir)
246
- file_patterns.each do |pattern|
247
- Dir.glob(File.join(dir, "**", pattern)).each do |file|
248
- content = File.read(file)
249
-
250
- # Extract ENV variables
251
- content.scan(/ENV\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
252
- env_var = match[0]
253
- env_vars[env_var] ||= []
254
- env_vars[env_var] << file.sub("#{project_path}/", "")
255
- end
256
-
257
- # Also match ENV['VAR'] pattern
258
- content.scan(/ENV\s*\.\s*\[\s*['"]([^'"]+)['"]\s*\]/).each do |match|
259
- env_var = match[0]
260
- env_vars[env_var] ||= []
261
- env_vars[env_var] << file.sub("#{project_path}/", "")
262
- end
263
-
264
- # Also match ENV.fetch('VAR') pattern
265
- content.scan(/ENV\s*\.\s*fetch\s*\(\s*['"]([^'"]+)['"]\s*/).each do |match|
266
- env_var = match[0]
267
- env_vars[env_var] ||= []
268
- env_vars[env_var] << file.sub("#{project_path}/", "")
269
- end
270
- rescue => e
271
- log(:error, "Error reading file #{file}: #{e.message}")
272
- end
273
- end
274
- end
275
- end
276
-
277
- env_vars
278
- end
279
-
280
- # Helper method to parse .env files
281
- def parse_dotenv_file(file_path)
282
- vars = {}
283
-
284
- begin
285
- File.readlines(file_path).each do |line| # rubocop:disable Performance/IoReadlines
286
- # Skip comments and empty lines
287
- next if line.strip.empty? || line.strip.start_with?("#")
288
-
289
- # Parse KEY=value pattern
290
- if line =~ /\A([A-Za-z0-9_]+)=(.*)\z/
291
- key = $1
292
- # Store just the existence of the variable, not its value
293
- vars[key] = true
294
- end
295
- end
296
- rescue => e
297
- log(:error, "Error parsing .env file #{file_path}: #{e.message}")
298
- end
299
-
300
- vars
301
- end
302
-
303
- # Helper method to parse database.yml
304
- def parse_database_config(file_path)
305
- config = {}
306
-
307
- begin
308
- # Simple YAML parsing - not handling ERB
309
- yaml_content = File.read(file_path)
310
- yaml_data = YAML.safe_load(yaml_content) || {}
311
-
312
- # Extract environment configurations
313
- %w[development test production staging].each do |env|
314
- config[env] = yaml_data[env] if yaml_data[env]
315
- end
316
- rescue => e
317
- log(:error, "Error parsing database.yml: #{e.message}")
318
- end
319
-
320
- config
321
- end
322
-
323
- # Helper method to compare environment settings
324
- def compare_environment_settings(env_settings)
325
- result = {
326
- unique_settings: {},
327
- different_values: {}
328
- }
329
-
330
- # Get all settings across all environments
331
- all_settings = env_settings.values.map(&:keys).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
332
-
333
- # Find settings unique to certain environments
334
- env_settings.each do |env, settings|
335
- unique = settings.keys - (all_settings - settings.keys)
336
- result[:unique_settings][env] = unique if unique.any?
337
- end
338
-
339
- # Find settings with different values across environments
340
- all_settings.each do |setting|
341
- values = {}
342
-
343
- env_settings.each do |env, settings|
344
- values[env] = settings[setting] if settings[setting]
345
- end
346
-
347
- # Only include if there are different values
348
- if values.values.uniq.size > 1
349
- result[:different_values][setting] = values
350
- end
351
- end
352
-
353
- result
354
- end
355
-
356
- # Helper method to find missing ENV variables
357
- def find_missing_env_vars(env_vars_in_code, dotenv_vars)
358
- missing_vars = {}
359
-
360
- # Check each ENV variable used in code
361
- env_vars_in_code.each do |var, files|
362
- # Environments where the variable is missing
363
- missing_in = []
364
-
365
- # Check in each .env file
366
- if dotenv_vars.empty?
367
- missing_in << "all environments (no .env files found)"
368
- else
369
- dotenv_vars.each do |env_file, vars|
370
- env_name = env_file.gsub(/^\.env\.?|\.local$/, "")
371
- env_name = "development" if env_name.empty?
372
-
373
- if !vars.key?(var)
374
- missing_in << env_name
375
- end
376
- end
377
- end
378
-
379
- missing_vars[var] = missing_in if missing_in.any?
380
- end
381
-
382
- missing_vars
383
- end
384
-
385
- # Helper method to check for security issues
386
- def check_security_configuration(env_settings, database_config)
387
- findings = []
388
-
389
- # Check for common security settings
390
- env_settings.each do |env, settings|
391
- # Check for secure cookies in production
392
- if env == "production"
393
- if settings["cookies.secure"] == "false"
394
- findings << "Production has cookies.secure = false"
395
- end
396
-
397
- if settings["session_store.secure"] == "false"
398
- findings << "Production has session_store.secure = false"
399
- end
400
-
401
- # Force SSL
402
- if settings["force_ssl"] == "false"
403
- findings << "Production has force_ssl = false"
404
- end
405
- end
406
-
407
- # Check for CSRF protection
408
- if settings["action_controller.default_protect_from_forgery"] == "false"
409
- findings << "#{env} has CSRF protection disabled"
410
- end
411
- end
412
-
413
- # Check for hardcoded credentials in database.yml
414
- database_config.each do |env, config|
415
- if config["username"] && !config["username"].include?("ENV")
416
- findings << "Database username hardcoded in database.yml for #{env}"
417
- end
418
-
419
- if config["password"] && !config["password"].include?("ENV")
420
- findings << "Database password hardcoded in database.yml for #{env}"
421
- end
422
- end
423
-
424
- findings
425
- end
426
- end
427
- end
@@ -1,116 +0,0 @@
1
- module RailsMcpServer
2
- class AnalyzeModels < BaseTool
3
- tool_name "analyze_models"
4
-
5
- description "Retrieve detailed information about Active Record models in the project. When called without parameters, lists all model files. When a specific model is specified, returns its schema, associations (has_many, belongs_to, has_one), and complete source code."
6
-
7
- arguments do
8
- optional(:model_name).filled(:string).description("Class name of a specific model to get detailed information for (e.g., 'User', 'Product'). Use CamelCase, not snake_case. If omitted, returns a list of all models.")
9
- end
10
-
11
- def call(model_name: nil)
12
- unless current_project
13
- message = "No active project. Please switch to a project first."
14
- log(:warn, message)
15
-
16
- return message
17
- end
18
-
19
- if model_name
20
- log(:info, "Getting info for specific model: #{model_name}")
21
-
22
- # Check if the model file exists
23
- model_file = File.join(active_project_path, "app", "models", "#{underscore(model_name)}.rb")
24
- unless File.exist?(model_file)
25
- log(:warn, "Model file not found: #{model_name}")
26
- message = "Model '#{model_name}' not found."
27
- log(:warn, message)
28
-
29
- return message
30
- end
31
-
32
- log(:debug, "Reading model file: #{model_file}")
33
-
34
- # Get the model file content
35
- model_content = File.read(model_file)
36
-
37
- # Try to get schema information
38
- log(:debug, "Executing Rails runner to get schema information")
39
- schema_info = execute_rails_command(
40
- active_project_path,
41
- "runner \"puts #{model_name}.column_names\""
42
- )
43
-
44
- # Try to get associations
45
- associations = []
46
- if model_content.include?("has_many")
47
- has_many = model_content.scan(/has_many\s+:(\w+)/).flatten
48
- associations << "Has many: #{has_many.join(", ")}" unless has_many.empty?
49
- end
50
-
51
- if model_content.include?("belongs_to")
52
- belongs_to = model_content.scan(/belongs_to\s+:(\w+)/).flatten
53
- associations << "Belongs to: #{belongs_to.join(", ")}" unless belongs_to.empty?
54
- end
55
-
56
- if model_content.include?("has_one")
57
- has_one = model_content.scan(/has_one\s+:(\w+)/).flatten
58
- associations << "Has one: #{has_one.join(", ")}" unless has_one.empty?
59
- end
60
-
61
- log(:debug, "Found #{associations.size} associations for model: #{model_name}")
62
-
63
- # Format the output
64
- <<~INFO
65
- Model: #{model_name}
66
-
67
- Schema:
68
- #{schema_info}
69
-
70
- Associations:
71
- #{associations.empty? ? "None found" : associations.join("\n")}
72
-
73
- Model Definition:
74
- ```ruby
75
- #{model_content}
76
- ```
77
- INFO
78
- else
79
- log(:info, "Listing all models")
80
-
81
- # List all models
82
- models_dir = File.join(active_project_path, "app", "models")
83
- unless File.directory?(models_dir)
84
- message = "Models directory not found."
85
- log(:warn, message)
86
-
87
- return message
88
- end
89
-
90
- # Get all .rb files in the models directory and its subdirectories
91
- model_files = Dir.glob(File.join(models_dir, "**", "*.rb"))
92
- .map { |f| f.sub("#{models_dir}/", "").sub(/\.rb$/, "") }
93
- .sort # rubocop:disable Performance/ChainArrayAllocation
94
-
95
- log(:debug, "Found #{model_files.size} model files")
96
-
97
- "Models in the project:\n\n#{model_files.join("\n")}"
98
- end
99
- end
100
-
101
- private
102
-
103
- def execute_rails_command(project_path, command)
104
- full_command = "cd #{project_path} && bin/rails #{command}"
105
- `#{full_command}`
106
- end
107
-
108
- def underscore(string)
109
- string.gsub("::", "/")
110
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
111
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
112
- .tr("-", "_")
113
- .downcase
114
- end
115
- end
116
- end
@@ -1,55 +0,0 @@
1
- module RailsMcpServer
2
- class GetFile < BaseTool
3
- tool_name "get_file"
4
-
5
- description "Retrieve the complete content of a specific file with syntax highlighting. Use this to examine implementation details, configurations, or any text file in the project."
6
-
7
- arguments do
8
- required(:path).filled(:string).description("File path relative to the project root (e.g., 'app/models/user.rb', 'config/routes.rb'). Use list_files first if you're not sure about the exact path.")
9
- end
10
-
11
- def call(path:)
12
- unless current_project
13
- message = "No active project. Please switch to a project first."
14
- log(:warn, message)
15
-
16
- return message
17
- end
18
-
19
- full_path = File.join(active_project_path, path)
20
-
21
- unless File.exist?(full_path)
22
- message = "File '#{path}' not found in the project."
23
- log(:warn, message)
24
-
25
- return message
26
- end
27
-
28
- content = File.read(full_path)
29
- log(:debug, "Read file: #{path} (#{content.size} bytes)")
30
-
31
- "File: #{path}\n\n```#{get_file_extension(path)}\n#{content}\n```"
32
- end
33
-
34
- private
35
-
36
- def get_file_extension(path)
37
- case File.extname(path).downcase
38
- when ".rb"
39
- "ruby"
40
- when ".js"
41
- "javascript"
42
- when ".html", ".erb"
43
- "html"
44
- when ".css"
45
- "css"
46
- when ".json"
47
- "json"
48
- when ".yml", ".yaml"
49
- "yaml"
50
- else
51
- ""
52
- end
53
- end
54
- end
55
- end
@@ -1,24 +0,0 @@
1
- module RailsMcpServer
2
- class GetRoutes < BaseTool
3
- tool_name "get_routes"
4
-
5
- description "Retrieve all HTTP routes defined in the Rails application with their associated controllers and actions. Equivalent to running 'rails routes' command. This helps understand the API endpoints or page URLs available in the application."
6
-
7
- def call
8
- unless current_project
9
- message = "No active project. Please switch to a project first."
10
- log(:warn, message)
11
-
12
- return message
13
- end
14
-
15
- # Execute the Rails routes command
16
- routes_output = RailsMcpServer::RunProcess.execute_rails_command(
17
- active_project_path, "bin/rails routes"
18
- )
19
- log(:debug, "Routes command completed, output size: #{routes_output.size} bytes")
20
-
21
- "Rails Routes:\n\n```\n#{routes_output}\n```"
22
- end
23
- end
24
- end