rails-mcp-server 1.4.0 → 1.5.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/CHANGELOG.md +116 -0
  3. data/README.md +82 -4
  4. data/docs/{AGENT.md → AGENT.md} +84 -5
  5. data/docs/COPILOT_AGENT.md +261 -0
  6. data/docs/RESOURCES.md +2 -2
  7. data/exe/rails-mcp-config +89 -75
  8. data/exe/rails-mcp-server +5 -0
  9. data/lib/rails-mcp-server/analyzers/analyze_controller_views.rb +14 -7
  10. data/lib/rails-mcp-server/analyzers/analyze_environment_config.rb +1 -1
  11. data/lib/rails-mcp-server/analyzers/analyze_models.rb +18 -9
  12. data/lib/rails-mcp-server/analyzers/base_analyzer.rb +16 -6
  13. data/lib/rails-mcp-server/analyzers/get_file.rb +16 -4
  14. data/lib/rails-mcp-server/analyzers/get_routes.rb +5 -4
  15. data/lib/rails-mcp-server/analyzers/get_schema.rb +102 -44
  16. data/lib/rails-mcp-server/analyzers/list_files.rb +56 -14
  17. data/lib/rails-mcp-server/analyzers/load_guide.rb +25 -7
  18. data/lib/rails-mcp-server/analyzers/project_info.rb +1 -1
  19. data/lib/rails-mcp-server/config.rb +87 -2
  20. data/lib/rails-mcp-server/helpers/resource_base.rb +48 -9
  21. data/lib/rails-mcp-server/helpers/resource_downloader.rb +2 -2
  22. data/lib/rails-mcp-server/resources/guide_content_formatter.rb +3 -3
  23. data/lib/rails-mcp-server/resources/guide_error_handler.rb +2 -2
  24. data/lib/rails-mcp-server/resources/guide_loader_template.rb +1 -1
  25. data/lib/rails-mcp-server/resources/guide_manifest_operations.rb +1 -1
  26. data/lib/rails-mcp-server/resources/kamal_guides_resources.rb +1 -1
  27. data/lib/rails-mcp-server/tools/execute_tool.rb +9 -3
  28. data/lib/rails-mcp-server/tools/get_model.rb +18 -13
  29. data/lib/rails-mcp-server/tools/search_tools.rb +4 -4
  30. data/lib/rails-mcp-server/tools/switch_project.rb +10 -1
  31. data/lib/rails-mcp-server/utilities/path_validator.rb +100 -0
  32. data/lib/rails-mcp-server/utilities/run_process.rb +25 -15
  33. data/lib/rails-mcp-server/version.rb +1 -1
  34. data/lib/rails_mcp_server.rb +1 -0
  35. metadata +34 -4
@@ -103,7 +103,7 @@ module RailsMcpServer
103
103
  <<~GUIDE
104
104
  ### #{title}
105
105
  **Guide name:** `#{short_name}`
106
- #{description.empty? ? "" : "**Description:** #{description}"}
106
+ #{"**Description:** #{description}" unless description.empty?}
107
107
  GUIDE
108
108
  end
109
109
  end
@@ -13,7 +13,7 @@ module RailsMcpServer
13
13
  required(:tool_name).filled(:string).description(
14
14
  "Name of the analyzer to execute (e.g., 'get_routes', 'analyze_models', 'get_schema')"
15
15
  )
16
- optional(:params).description(
16
+ optional(:params).hash.description(
17
17
  "Hash of parameters to pass to the analyzer (e.g., { model_name: 'User', analysis_type: 'full' })"
18
18
  )
19
19
  end
@@ -54,7 +54,7 @@ module RailsMcpServer
54
54
  },
55
55
  "load_guide" => {
56
56
  class_name: "Analyzers::LoadGuide",
57
- params: [:guides, :guide]
57
+ params: [:library, :guide]
58
58
  }
59
59
  }.freeze
60
60
 
@@ -63,7 +63,13 @@ module RailsMcpServer
63
63
 
64
64
  unless tool_config
65
65
  available = INTERNAL_TOOLS.keys.sort.join(", ")
66
- return "Unknown tool '#{tool_name}'. Available: #{available}\n\nUse search_tools to discover tools and their parameters."
66
+ return <<~ERROR
67
+ Unknown tool '#{tool_name}'.
68
+
69
+ Available tools: #{available}
70
+
71
+ Tip: Use search_tools(query: "keyword") to discover tools and their parameters.
72
+ ERROR
67
73
  end
68
74
 
69
75
  # Get the analyzer class from the Analyzers module
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
1
3
  module RailsMcpServer
2
4
  class GetModels < BaseTool
3
5
  tool_name "get_models"
@@ -17,10 +19,16 @@ module RailsMcpServer
17
19
  end
18
20
 
19
21
  if model_name
22
+ unless PathValidator.valid_identifier?(model_name)
23
+ message = "Invalid model name: #{model_name}. Use CamelCase (e.g., 'User', 'Admin::User')."
24
+ log(:warn, message)
25
+ return message
26
+ end
27
+
20
28
  log(:info, "Getting info for specific model: #{model_name}")
21
29
 
22
30
  # Check if the model file exists
23
- model_file = File.join(active_project_path, "app", "models", "#{underscore(model_name)}.rb")
31
+ model_file = File.join(active_project_path, "app", "models", "#{model_name.underscore}.rb")
24
32
  unless File.exist?(model_file)
25
33
  log(:warn, "Model file not found: #{model_name}")
26
34
  message = "Model '#{model_name}' not found."
@@ -38,7 +46,7 @@ module RailsMcpServer
38
46
  log(:debug, "Executing Rails runner to get schema information")
39
47
  schema_info = execute_rails_command(
40
48
  active_project_path,
41
- "runner \"puts #{model_name}.column_names\""
49
+ "puts #{model_name}.column_names"
42
50
  )
43
51
 
44
52
  # Try to get associations
@@ -100,17 +108,14 @@ module RailsMcpServer
100
108
 
101
109
  private
102
110
 
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
111
+ def execute_rails_command(project_path, runner_script)
112
+ Dir.chdir(project_path) do
113
+ IO.popen(
114
+ ["bin/rails", "runner", runner_script],
115
+ err: [:child, :out],
116
+ &:read
117
+ )
118
+ end
114
119
  end
115
120
  end
116
121
  end
@@ -104,11 +104,11 @@ module RailsMcpServer
104
104
  },
105
105
  "load_guide" => {
106
106
  category: "guides",
107
- keywords: %w[guide documentation rails turbo stimulus kamal docs help],
108
- summary: "Load Rails, Turbo, Stimulus, or Kamal documentation guides",
107
+ keywords: %w[guide documentation rails turbo stimulus kamal docs help custom],
108
+ summary: "Load Rails, Turbo, Stimulus, Kamal, or custom documentation guides",
109
109
  parameters: [
110
- {name: "guides", type: "string", required: true, description: "Library: 'rails', 'turbo', 'stimulus', 'kamal', 'custom'"},
111
- {name: "guide", type: "string", required: false, description: "Specific guide name to load"}
110
+ {name: "library", type: "string", required: true, description: "Guide source: 'rails', 'turbo', 'stimulus', 'kamal', or 'custom'"},
111
+ {name: "guide", type: "string", required: false, description: "Specific guide name to load (omit to list available guides)"}
112
112
  ]
113
113
  }
114
114
  }.freeze
@@ -2,7 +2,16 @@ module RailsMcpServer
2
2
  class SwitchProject < BaseTool
3
3
  tool_name "switch_project"
4
4
 
5
- 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."
5
+ description <<~DESC.strip
6
+ Change the active Rails project to interact with a different codebase.
7
+ Must be called before using other tools when multiple projects are configured.
8
+
9
+ Note: If RAILS_MCP_PROJECT_PATH is set, --single-project flag is used,
10
+ or the server is started from a Rails directory, the project is auto-detected
11
+ and switch_project is optional.
12
+
13
+ Available projects are defined in the projects.yml configuration file.
14
+ DESC
6
15
 
7
16
  arguments do
8
17
  required(:project_name).filled(:string).description("Name of the project as defined in the projects.yml file (case-sensitive)")
@@ -0,0 +1,100 @@
1
+ require "shellwords"
2
+
3
+ module RailsMcpServer
4
+ module PathValidator
5
+ # Sensitive file patterns - files that should never be read or listed
6
+ SENSITIVE_PATTERNS = [
7
+ /\.env(\..*)?$/i, # .env, .env.local, .env.production
8
+ /\.key$/i, # *.key files
9
+ /\.pem$/i, # SSL certificates
10
+ /\.crt$/i, # SSL certificates
11
+ /\.p12$/i, # PKCS12 certificates
12
+ /credentials\.yml(\.enc)?$/i, # Rails credentials (encrypted or not)
13
+ /secrets\.yml(\.enc)?$/i, # Rails secrets
14
+ /master\.key$/i, # Rails master key
15
+ /config\/credentials/i, # credentials directory
16
+ /config\/secrets/i, # secrets directory
17
+ /\.secret$/i, # generic secret files
18
+ /password/i, # password files
19
+ /\.ssh\//i, # SSH directory
20
+ /id_rsa/i, # SSH keys
21
+ /id_ed25519/i, # SSH keys
22
+ /id_ecdsa/i, # SSH keys
23
+ /\.gnupg\//i, # GPG directory
24
+ /\.netrc$/i, # netrc credentials
25
+ /\.pgpass$/i, # PostgreSQL passwords
26
+ /database\.yml$/i, # Database configuration
27
+ /storage\.yml$/i # Active Storage config (may contain keys)
28
+ ].freeze
29
+
30
+ # Directories that should be excluded from listing
31
+ EXCLUDED_DIRECTORIES = %w[
32
+ .git/
33
+ .bundle/
34
+ node_modules/
35
+ vendor/bundle/
36
+ vendor/cache/
37
+ tmp/
38
+ log/
39
+ storage/
40
+ .ruby-lsp/
41
+ ].freeze
42
+
43
+ module_function
44
+
45
+ # Check if a path matches sensitive patterns
46
+ def sensitive_path?(path)
47
+ normalized = path.to_s.downcase
48
+ SENSITIVE_PATTERNS.any? { |pattern| normalized.match?(pattern) }
49
+ end
50
+
51
+ # Reject paths that resolve to project_root itself (e.g., ".", "..")
52
+ def safe_path?(path, project_root)
53
+ return false if path.nil? || path.empty?
54
+
55
+ expanded = File.expand_path(path, project_root)
56
+ expanded.start_with?(project_root + "/")
57
+ end
58
+
59
+ # Validate and return safe absolute path, or nil if unsafe
60
+ def validate_path(path, project_root)
61
+ return nil unless safe_path?(path, project_root)
62
+
63
+ expanded = File.expand_path(path, project_root)
64
+ return nil if sensitive_path?(expanded.sub(project_root + "/", ""))
65
+
66
+ expanded
67
+ end
68
+
69
+ # Escape a string for safe shell usage
70
+ def shell_escape(str)
71
+ Shellwords.escape(str.to_s)
72
+ end
73
+
74
+ def valid_identifier?(name)
75
+ return false if name.nil? || name.empty?
76
+
77
+ name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*(::[a-zA-Z_][a-zA-Z0-9_]*)*\z/)
78
+ end
79
+
80
+ # Validate table names (alphanumeric and underscores only)
81
+ def valid_table_name?(name)
82
+ return false if name.nil? || name.empty?
83
+
84
+ name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
85
+ end
86
+
87
+ # Filter a list of files, removing sensitive ones
88
+ def filter_sensitive_files(files, project_root)
89
+ files.reject do |file|
90
+ relative_path = file.sub("#{project_root}/", "")
91
+ sensitive_path?(relative_path)
92
+ end
93
+ end
94
+
95
+ # Check if path is in excluded directory
96
+ def excluded_directory?(path)
97
+ EXCLUDED_DIRECTORIES.any? { |dir| path.start_with?(dir) }
98
+ end
99
+ end
100
+ end
@@ -1,27 +1,37 @@
1
1
  require "bundler"
2
+ require "shellwords"
2
3
 
3
4
  module RailsMcpServer
4
5
  class RunProcess
5
6
  def self.execute_rails_command(project_path, command)
6
- subprocess_env = ENV.to_h.merge(Bundler.original_env).merge(
7
- "BUNDLE_GEMFILE" => File.join(project_path, "Gemfile")
8
- )
9
-
10
7
  RailsMcpServer.log(:debug, "Executing: #{command}")
11
8
 
12
- # Execute the command and capture stdout, stderr, and status
13
- stdout_str, stderr_str, status = Open3.capture3(subprocess_env, command, chdir: project_path)
9
+ Bundler.with_unbundled_env do
10
+ subprocess_env = ENV.to_h
11
+ subprocess_env.delete("BUNDLE_GEMFILE")
12
+
13
+ # Set RBENV_VERSION from project's .ruby-version if it exists
14
+ ruby_version_file = File.join(project_path, ".ruby-version")
15
+ if File.exist?(ruby_version_file)
16
+ subprocess_env["RBENV_VERSION"] = File.read(ruby_version_file).strip
17
+ else
18
+ subprocess_env.delete("RBENV_VERSION")
19
+ end
20
+
21
+ shell = ENV.fetch("SHELL", "/bin/bash")
22
+ shell_command = "cd #{Shellwords.escape(project_path)} && #{command}"
23
+ stdout_str, stderr_str, status = Open3.capture3(subprocess_env, shell, "-l", "-c", shell_command)
14
24
 
15
- if status.success?
16
- RailsMcpServer.log(:debug, "Command succeeded")
17
- stdout_str
18
- else
19
- # Log error details
20
- RailsMcpServer.log(:error, "Command failed with status: #{status.exitstatus}")
21
- RailsMcpServer.log(:error, "stderr: #{stderr_str}")
25
+ if status.success?
26
+ RailsMcpServer.log(:debug, "Command succeeded")
27
+ stdout_str
28
+ else
29
+ RailsMcpServer.log(:error, "Command failed with status: #{status.exitstatus}")
30
+ RailsMcpServer.log(:error, "stderr: #{stderr_str}")
22
31
 
23
- # Return error message
24
- "Error executing Rails command: #{command}\n\n#{stderr_str}"
32
+ error_output = stderr_str.empty? ? stdout_str : stderr_str
33
+ "Error executing Rails command: #{command}\n\n#{error_output}"
34
+ end
25
35
  end
26
36
  rescue => e
27
37
  RailsMcpServer.log(:error, "Exception executing Rails command: #{e.message}")
@@ -1,3 +1,3 @@
1
1
  module RailsMcpServer
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -5,6 +5,7 @@ require "open3"
5
5
  require_relative "rails-mcp-server/version"
6
6
  require_relative "rails-mcp-server/config"
7
7
  require_relative "rails-mcp-server/utilities/run_process"
8
+ require_relative "rails-mcp-server/utilities/path_validator"
8
9
 
9
10
  # MCP Tools (registered with FastMCP)
10
11
  require_relative "rails-mcp-server/tools/base_tool"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-mcp-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chávez Cárdenas
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: addressable
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +93,20 @@ dependencies:
79
93
  - - "~>"
80
94
  - !ruby/object:Gem::Version
81
95
  version: 1.7.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
82
110
  - !ruby/object:Gem::Dependency
83
111
  name: standard
84
112
  requirement: !ruby/object:Gem::Requirement
@@ -150,7 +178,8 @@ files:
150
178
  - LICENSE.txt
151
179
  - README.md
152
180
  - config/resources.yml
153
- - 'docs/AGENT.md '
181
+ - docs/AGENT.md
182
+ - docs/COPILOT_AGENT.md
154
183
  - docs/RESOURCES.md
155
184
  - exe/rails-mcp-config
156
185
  - exe/rails-mcp-server
@@ -193,6 +222,7 @@ files:
193
222
  - lib/rails-mcp-server/tools/get_model.rb
194
223
  - lib/rails-mcp-server/tools/search_tools.rb
195
224
  - lib/rails-mcp-server/tools/switch_project.rb
225
+ - lib/rails-mcp-server/utilities/path_validator.rb
196
226
  - lib/rails-mcp-server/utilities/run_process.rb
197
227
  - lib/rails-mcp-server/version.rb
198
228
  - lib/rails_mcp_server.rb
@@ -211,14 +241,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
211
241
  requirements:
212
242
  - - ">="
213
243
  - !ruby/object:Gem::Version
214
- version: 2.5.0
244
+ version: 3.2.0
215
245
  required_rubygems_version: !ruby/object:Gem::Requirement
216
246
  requirements:
217
247
  - - ">="
218
248
  - !ruby/object:Gem::Version
219
249
  version: '0'
220
250
  requirements: []
221
- rubygems_version: 4.0.0
251
+ rubygems_version: 4.0.1
222
252
  specification_version: 4
223
253
  summary: MCP server for Rails projects
224
254
  test_files: []