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.
- checksums.yaml +4 -4
- data/README.md +168 -166
- data/docs/AGENT.md +345 -0
- data/exe/rails-mcp-config +1411 -0
- data/exe/rails-mcp-server +23 -10
- data/exe/rails-mcp-setup-claude +1 -1
- data/lib/rails-mcp-server/analyzers/analyze_controller_views.rb +253 -0
- data/lib/rails-mcp-server/analyzers/analyze_environment_config.rb +79 -0
- data/lib/rails-mcp-server/analyzers/analyze_models.rb +251 -0
- data/lib/rails-mcp-server/analyzers/base_analyzer.rb +42 -0
- data/lib/rails-mcp-server/analyzers/get_file.rb +40 -0
- data/lib/rails-mcp-server/analyzers/get_routes.rb +212 -0
- data/lib/rails-mcp-server/analyzers/get_schema.rb +216 -0
- data/lib/rails-mcp-server/analyzers/list_files.rb +43 -0
- data/lib/rails-mcp-server/analyzers/load_guide.rb +84 -0
- data/lib/rails-mcp-server/analyzers/project_info.rb +136 -0
- data/lib/rails-mcp-server/tools/base_tool.rb +2 -0
- data/lib/rails-mcp-server/tools/execute_ruby.rb +409 -0
- data/lib/rails-mcp-server/tools/execute_tool.rb +115 -0
- data/lib/rails-mcp-server/tools/search_tools.rb +186 -0
- data/lib/rails-mcp-server/tools/switch_project.rb +16 -1
- data/lib/rails-mcp-server/version.rb +1 -1
- data/lib/rails_mcp_server.rb +19 -53
- metadata +65 -18
- data/lib/rails-mcp-server/extensions/resource_templating.rb +0 -182
- data/lib/rails-mcp-server/extensions/server_templating.rb +0 -333
- data/lib/rails-mcp-server/tools/analyze_controller_views.rb +0 -239
- data/lib/rails-mcp-server/tools/analyze_environment_config.rb +0 -427
- data/lib/rails-mcp-server/tools/analyze_models.rb +0 -116
- data/lib/rails-mcp-server/tools/get_file.rb +0 -55
- data/lib/rails-mcp-server/tools/get_routes.rb +0 -24
- data/lib/rails-mcp-server/tools/get_schema.rb +0 -141
- data/lib/rails-mcp-server/tools/list_files.rb +0 -54
- data/lib/rails-mcp-server/tools/load_guide.rb +0 -370
- 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
|