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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +116 -0
- data/README.md +82 -4
- data/docs/{AGENT.md → AGENT.md} +84 -5
- data/docs/COPILOT_AGENT.md +261 -0
- data/docs/RESOURCES.md +2 -2
- data/exe/rails-mcp-config +89 -75
- data/exe/rails-mcp-server +5 -0
- data/lib/rails-mcp-server/analyzers/analyze_controller_views.rb +14 -7
- data/lib/rails-mcp-server/analyzers/analyze_environment_config.rb +1 -1
- data/lib/rails-mcp-server/analyzers/analyze_models.rb +18 -9
- data/lib/rails-mcp-server/analyzers/base_analyzer.rb +16 -6
- data/lib/rails-mcp-server/analyzers/get_file.rb +16 -4
- data/lib/rails-mcp-server/analyzers/get_routes.rb +5 -4
- data/lib/rails-mcp-server/analyzers/get_schema.rb +102 -44
- data/lib/rails-mcp-server/analyzers/list_files.rb +56 -14
- data/lib/rails-mcp-server/analyzers/load_guide.rb +25 -7
- data/lib/rails-mcp-server/analyzers/project_info.rb +1 -1
- data/lib/rails-mcp-server/config.rb +87 -2
- data/lib/rails-mcp-server/helpers/resource_base.rb +48 -9
- data/lib/rails-mcp-server/helpers/resource_downloader.rb +2 -2
- data/lib/rails-mcp-server/resources/guide_content_formatter.rb +3 -3
- data/lib/rails-mcp-server/resources/guide_error_handler.rb +2 -2
- data/lib/rails-mcp-server/resources/guide_loader_template.rb +1 -1
- data/lib/rails-mcp-server/resources/guide_manifest_operations.rb +1 -1
- data/lib/rails-mcp-server/resources/kamal_guides_resources.rb +1 -1
- data/lib/rails-mcp-server/tools/execute_tool.rb +9 -3
- data/lib/rails-mcp-server/tools/get_model.rb +18 -13
- data/lib/rails-mcp-server/tools/search_tools.rb +4 -4
- data/lib/rails-mcp-server/tools/switch_project.rb +10 -1
- data/lib/rails-mcp-server/utilities/path_validator.rb +100 -0
- data/lib/rails-mcp-server/utilities/run_process.rb +25 -15
- data/lib/rails-mcp-server/version.rb +1 -1
- data/lib/rails_mcp_server.rb +1 -0
- metadata +34 -4
|
@@ -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: [:
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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}")
|
data/lib/rails_mcp_server.rb
CHANGED
|
@@ -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
|
+
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
|
-
-
|
|
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.
|
|
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.
|
|
251
|
+
rubygems_version: 4.0.1
|
|
222
252
|
specification_version: 4
|
|
223
253
|
summary: MCP server for Rails projects
|
|
224
254
|
test_files: []
|