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,141 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
class GetSchema < BaseTool
|
|
3
|
-
tool_name "get_schema"
|
|
4
|
-
|
|
5
|
-
description "Retrieve database schema information for the Rails application. Without parameters, returns all tables and the complete schema.rb. With a table name, returns detailed column information including data types, constraints, and foreign keys for that specific table."
|
|
6
|
-
|
|
7
|
-
arguments do
|
|
8
|
-
optional(:table_name).filled(:string).description("Database table name to get detailed schema information for (e.g., 'users', 'products'). Use snake_case, plural form. If omitted, returns complete database schema.")
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def call(table_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 table_name
|
|
20
|
-
log(:info, "Getting schema for table: #{table_name}")
|
|
21
|
-
|
|
22
|
-
# Execute the Rails schema command for a specific table
|
|
23
|
-
schema_output = RailsMcpServer::RunProcess.execute_rails_command(
|
|
24
|
-
active_project_path,
|
|
25
|
-
"bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.columns('#{table_name}').map{|c| [c.name, c.type, c.null, c.default].inspect}.join('\\n')\""
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
if schema_output.strip.empty?
|
|
29
|
-
message = "Table '#{table_name}' not found or has no columns."
|
|
30
|
-
log(:warn, message)
|
|
31
|
-
|
|
32
|
-
return message
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Parse the column information
|
|
36
|
-
columns = schema_output.strip.split("\\n").map do |column_info|
|
|
37
|
-
eval(column_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Format the output
|
|
41
|
-
formatted_columns = columns.map do |name, type, nullable, default|
|
|
42
|
-
"#{name} (#{type})#{nullable ? ", nullable" : ""}#{default ? ", default: #{default}" : ""}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
output = <<~SCHEMA
|
|
46
|
-
Table: #{table_name}
|
|
47
|
-
|
|
48
|
-
Columns:
|
|
49
|
-
#{formatted_columns.join("\n")}
|
|
50
|
-
SCHEMA
|
|
51
|
-
|
|
52
|
-
# Try to get foreign keys
|
|
53
|
-
begin
|
|
54
|
-
fk_output = RailsMcpServer::RunProcess.execute_rails_command(
|
|
55
|
-
active_project_path,
|
|
56
|
-
"bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.foreign_keys('#{table_name}').map{|fk| [fk.from_table, fk.to_table, fk.column, fk.primary_key].inspect}.join('\n')\""
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
unless fk_output.strip.empty?
|
|
60
|
-
foreign_keys = fk_output.strip.split("\n").map do |fk_info|
|
|
61
|
-
eval(fk_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
formatted_fks = foreign_keys.map do |from_table, to_table, column, primary_key|
|
|
65
|
-
"#{column} -> #{to_table}.#{primary_key}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
output += <<~FK
|
|
69
|
-
|
|
70
|
-
Foreign Keys:
|
|
71
|
-
#{formatted_fks.join("\n")}
|
|
72
|
-
FK
|
|
73
|
-
end
|
|
74
|
-
rescue => e
|
|
75
|
-
log(:warn, "Error fetching foreign keys: #{e.message}")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
output
|
|
79
|
-
else
|
|
80
|
-
log(:info, "Getting full schema")
|
|
81
|
-
|
|
82
|
-
# Execute the Rails schema:dump command
|
|
83
|
-
# First, check if we need to create the schema file
|
|
84
|
-
schema_file = File.join(active_project_path, "db", "schema.rb")
|
|
85
|
-
unless File.exist?(schema_file)
|
|
86
|
-
log(:info, "Schema file not found, attempting to generate it")
|
|
87
|
-
RailsMcpServer::RunProcess.execute_rails_command(active_project_path, "db:schema:dump")
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
if File.exist?(schema_file)
|
|
91
|
-
# Read the schema file
|
|
92
|
-
schema_content = File.read(schema_file)
|
|
93
|
-
|
|
94
|
-
# Try to get table list
|
|
95
|
-
tables_output = RailsMcpServer::RunProcess.execute_rails_command(
|
|
96
|
-
active_project_path,
|
|
97
|
-
"bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
tables = tables_output.strip.split("\n")
|
|
101
|
-
|
|
102
|
-
<<~SCHEMA
|
|
103
|
-
Database Schema
|
|
104
|
-
|
|
105
|
-
Tables:
|
|
106
|
-
#{tables.join("\n")}
|
|
107
|
-
|
|
108
|
-
Schema Definition:
|
|
109
|
-
```ruby
|
|
110
|
-
#{schema_content}
|
|
111
|
-
```
|
|
112
|
-
SCHEMA
|
|
113
|
-
else
|
|
114
|
-
# If we can't get the schema file, try to get the table list
|
|
115
|
-
tables_output = RailsMcpServer::RunProcess.execute_rails_command(
|
|
116
|
-
active_project_path,
|
|
117
|
-
"bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
if tables_output.strip.empty?
|
|
121
|
-
message = "Could not retrieve schema information. Try running 'rails db:schema:dump' in your project first."
|
|
122
|
-
log(:warn, message)
|
|
123
|
-
|
|
124
|
-
return message
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
tables = tables_output.strip.split("\n")
|
|
128
|
-
|
|
129
|
-
<<~SCHEMA
|
|
130
|
-
Database Schema
|
|
131
|
-
|
|
132
|
-
Tables:
|
|
133
|
-
#{tables.join("\n")}
|
|
134
|
-
|
|
135
|
-
Note: Full schema definition is not available. Run 'rails db:schema:dump' to generate the schema.rb file.
|
|
136
|
-
SCHEMA
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
class ListFiles < BaseTool
|
|
3
|
-
tool_name "list_files"
|
|
4
|
-
|
|
5
|
-
description "List files in the Rails project matching specific criteria. Use this to explore project directories or locate specific file types. If no parameters are provided, lists files in the project root."
|
|
6
|
-
|
|
7
|
-
arguments do
|
|
8
|
-
optional(:directory).filled(:string).description("Directory path relative to the project root (e.g., 'app/models', 'config'). Leave empty to list files at the root.")
|
|
9
|
-
optional(:pattern).filled(:string).description("File pattern using glob syntax (e.g., '*.rb' for Ruby files, '*.erb' for ERB templates, '*_controller.rb' for controllers)")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(directory: "", pattern: "*.rb")
|
|
13
|
-
unless current_project
|
|
14
|
-
message = "No active project. Please switch to a project first."
|
|
15
|
-
log(:warn, message)
|
|
16
|
-
|
|
17
|
-
return message
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
full_path = File.join(active_project_path, directory)
|
|
21
|
-
unless File.directory?(full_path)
|
|
22
|
-
message = "Directory '#{directory}' not found in the project."
|
|
23
|
-
log(:warn, message)
|
|
24
|
-
|
|
25
|
-
return message
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Check if this is a git repository
|
|
29
|
-
is_git_repo = system("cd #{active_project_path} && git rev-parse --is-inside-work-tree > /dev/null 2>&1")
|
|
30
|
-
|
|
31
|
-
if is_git_repo
|
|
32
|
-
log(:debug, "Project is a git repository, using git ls-files")
|
|
33
|
-
|
|
34
|
-
# Use git ls-files for tracked files
|
|
35
|
-
relative_dir = directory.empty? ? "" : "#{directory}/"
|
|
36
|
-
git_cmd = "cd #{active_project_path} && git ls-files --cached --others --exclude-standard #{relative_dir}#{pattern}"
|
|
37
|
-
|
|
38
|
-
files = `#{git_cmd}`.split("\n").map(&:strip).sort # rubocop:disable Performance/ChainArrayAllocation
|
|
39
|
-
else
|
|
40
|
-
log(:debug, "Project is not a git repository or git not available, using Dir.glob")
|
|
41
|
-
|
|
42
|
-
# Use Dir.glob as fallback
|
|
43
|
-
files = Dir.glob(File.join(full_path, pattern))
|
|
44
|
-
.map { |f| f.sub("#{active_project_path}/", "") }
|
|
45
|
-
.reject { |file| file.start_with?(".git/", ".ruby-lsp/", "node_modules/", "storage/", "public/assets/", "public/packs/", ".bundle/", "vendor/bundle/", "vendor/cache/", "tmp/", "log/") } # rubocop:disable Performance/ChainArrayAllocation
|
|
46
|
-
.sort # rubocop:disable Performance/ChainArrayAllocation
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
log(:debug, "Found #{files.size} files matching pattern (respecting .gitignore and ignoring node_modules)")
|
|
50
|
-
|
|
51
|
-
"Files in #{directory.empty? ? "project root" : directory} matching '#{pattern}':\n\n#{files.join("\n")}"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
class LoadGuide < BaseTool
|
|
3
|
-
tool_name "load_guide"
|
|
4
|
-
|
|
5
|
-
description "Load documentation guides from Rails, Turbo, Stimulus, Kamal, or Custom. Use this to get guide content for context in conversations."
|
|
6
|
-
|
|
7
|
-
arguments do
|
|
8
|
-
required(:guides).filled(:string).description("The guides library to search: 'rails', 'turbo', 'stimulus', 'kamal', or 'custom'")
|
|
9
|
-
optional(:guide).maybe(:string).description("Specific guide name to load. If not provided, returns available guides list.")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(guides:, guide: nil)
|
|
13
|
-
# Normalize guides parameter
|
|
14
|
-
guides_type = guides.downcase.strip
|
|
15
|
-
|
|
16
|
-
# Validate supported guide types
|
|
17
|
-
unless %w[rails turbo stimulus kamal custom].include?(guides_type)
|
|
18
|
-
message = "Unsupported guide type '#{guides_type}'. Supported types: rails, turbo, stimulus, kamal, custom."
|
|
19
|
-
log(:error, message)
|
|
20
|
-
return message
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
if guide.nil? || guide.strip.empty?
|
|
24
|
-
log(:debug, "Loading available #{guides_type} guides...")
|
|
25
|
-
load_guides_list(guides_type)
|
|
26
|
-
else
|
|
27
|
-
log(:debug, "Loading specific #{guides_type} guide: #{guide}")
|
|
28
|
-
load_specific_guide(guide, guides_type)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def load_guides_list(guides_type)
|
|
35
|
-
case guides_type
|
|
36
|
-
when "rails"
|
|
37
|
-
uri = "rails://guides"
|
|
38
|
-
read_resource(uri, RailsGuidesResources)
|
|
39
|
-
when "stimulus"
|
|
40
|
-
uri = "stimulus://guides"
|
|
41
|
-
read_resource(uri, StimulusGuidesResources)
|
|
42
|
-
when "turbo"
|
|
43
|
-
uri = "turbo://guides"
|
|
44
|
-
read_resource(uri, TurboGuidesResources)
|
|
45
|
-
when "kamal"
|
|
46
|
-
uri = "kamal://guides"
|
|
47
|
-
read_resource(uri, KamalGuidesResources)
|
|
48
|
-
when "custom"
|
|
49
|
-
uri = "custom://guides"
|
|
50
|
-
read_resource(uri, CustomGuidesResources)
|
|
51
|
-
else
|
|
52
|
-
"Guide type '#{guides_type}' not supported."
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def load_specific_guide(guide_name, guides_type)
|
|
57
|
-
# First try exact match
|
|
58
|
-
exact_match_content = try_exact_match(guide_name, guides_type)
|
|
59
|
-
return exact_match_content if exact_match_content && !exact_match_content.include?("Guide not found")
|
|
60
|
-
|
|
61
|
-
# If exact match fails, try fuzzy matching
|
|
62
|
-
try_fuzzy_matching(guide_name, guides_type)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def try_exact_match(guide_name, guides_type)
|
|
66
|
-
case guides_type
|
|
67
|
-
when "rails"
|
|
68
|
-
uri = "rails://guides/#{guide_name}"
|
|
69
|
-
read_resource(uri, RailsGuidesResource, {guide_name: guide_name})
|
|
70
|
-
when "stimulus"
|
|
71
|
-
uri = "stimulus://guides/#{guide_name}"
|
|
72
|
-
read_resource(uri, StimulusGuidesResource, {guide_name: guide_name})
|
|
73
|
-
when "turbo"
|
|
74
|
-
uri = "turbo://guides/#{guide_name}"
|
|
75
|
-
read_resource(uri, TurboGuidesResource, {guide_name: guide_name})
|
|
76
|
-
when "kamal"
|
|
77
|
-
uri = "kamal://guides/#{guide_name}"
|
|
78
|
-
read_resource(uri, KamalGuidesResource, {guide_name: guide_name})
|
|
79
|
-
when "custom"
|
|
80
|
-
uri = "custom://guides/#{guide_name}"
|
|
81
|
-
read_resource(uri, CustomGuidesResource, {guide_name: guide_name})
|
|
82
|
-
else
|
|
83
|
-
"Guide type '#{guides_type}' not supported."
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def try_fuzzy_matching(guide_name, guides_type)
|
|
88
|
-
# Get all matching guides using the base guide resource directly
|
|
89
|
-
matching_guides = find_matching_guides(guide_name, guides_type)
|
|
90
|
-
|
|
91
|
-
case matching_guides.size
|
|
92
|
-
when 0
|
|
93
|
-
format_guide_not_found_message(guide_name, guides_type)
|
|
94
|
-
when 1
|
|
95
|
-
# Load the single match
|
|
96
|
-
match = matching_guides.first
|
|
97
|
-
log(:debug, "Found single fuzzy match: #{match}")
|
|
98
|
-
try_exact_match(match, guides_type)
|
|
99
|
-
when 2..3
|
|
100
|
-
# Load multiple matches (up to 3)
|
|
101
|
-
log(:debug, "Found #{matching_guides.size} fuzzy matches, loading all")
|
|
102
|
-
load_multiple_guides(matching_guides, guides_type, guide_name)
|
|
103
|
-
else
|
|
104
|
-
# Too many matches, show options
|
|
105
|
-
format_multiple_matches_message(guide_name, matching_guides, guides_type)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def find_matching_guides(guide_name, guides_type)
|
|
110
|
-
# Get the manifest to find matching files
|
|
111
|
-
manifest = load_manifest_for_guides_type(guides_type)
|
|
112
|
-
return [] unless manifest
|
|
113
|
-
|
|
114
|
-
available_guides = manifest["files"].keys.select { |f| f.end_with?(".md") }.map { |f| f.sub(".md", "") } # rubocop:disable Performance/ChainArrayAllocation
|
|
115
|
-
|
|
116
|
-
# Generate variations and find matches
|
|
117
|
-
variations = generate_guide_name_variations(guide_name, guides_type)
|
|
118
|
-
matching_guides = []
|
|
119
|
-
|
|
120
|
-
variations.each do |variation|
|
|
121
|
-
matches = available_guides.select do |guide|
|
|
122
|
-
guide.downcase.include?(variation.downcase) ||
|
|
123
|
-
variation.downcase.include?(guide.downcase) ||
|
|
124
|
-
guide.gsub(/[_\-\s]/, "").downcase.include?(variation.gsub(/[_\-\s]/, "").downcase)
|
|
125
|
-
end
|
|
126
|
-
matching_guides.concat(matches)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
matching_guides.uniq.sort # rubocop:disable Performance/ChainArrayAllocation
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def load_manifest_for_guides_type(guides_type)
|
|
133
|
-
config = RailsMcpServer.config
|
|
134
|
-
manifest_file = File.join(config.config_dir, "resources", guides_type, "manifest.yaml")
|
|
135
|
-
|
|
136
|
-
return nil unless File.exist?(manifest_file)
|
|
137
|
-
|
|
138
|
-
YAML.load_file(manifest_file)
|
|
139
|
-
rescue => e
|
|
140
|
-
log(:error, "Failed to load manifest for #{guides_type}: #{e.message}")
|
|
141
|
-
nil
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def load_multiple_guides(guide_names, guides_type, original_query)
|
|
145
|
-
results = []
|
|
146
|
-
|
|
147
|
-
results << "# Multiple Guides Found for '#{original_query}'"
|
|
148
|
-
results << ""
|
|
149
|
-
results << "Found #{guide_names.size} matching guides. Loading all:\n"
|
|
150
|
-
|
|
151
|
-
guide_names.each_with_index do |guide_name, index|
|
|
152
|
-
results << "---"
|
|
153
|
-
results << ""
|
|
154
|
-
results << "## #{index + 1}. #{guide_name}"
|
|
155
|
-
results << ""
|
|
156
|
-
|
|
157
|
-
content = try_exact_match(guide_name, guides_type)
|
|
158
|
-
if content && !content.include?("Guide not found") && !content.include?("Error")
|
|
159
|
-
# Remove the header from individual guide content to avoid duplication
|
|
160
|
-
clean_content = content.sub(/^#[^\n]*\n/, "").sub(/^\*\*Source:.*?\n---\n/m, "")
|
|
161
|
-
results << clean_content.strip
|
|
162
|
-
else
|
|
163
|
-
results << "*Failed to load this guide*"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
results << "" if index < guide_names.size - 1
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
results.join("\n")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def format_multiple_matches_message(guide_name, matches, guides_type)
|
|
173
|
-
message = <<~MSG
|
|
174
|
-
# Multiple Guides Found
|
|
175
|
-
|
|
176
|
-
Found #{matches.size} guides matching '#{guide_name}' in #{guides_type} guides:
|
|
177
|
-
|
|
178
|
-
MSG
|
|
179
|
-
|
|
180
|
-
matches.first(10).each_with_index do |match, index|
|
|
181
|
-
message += "#{index + 1}. #{match}\n"
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
if matches.size > 10
|
|
185
|
-
message += "... and #{matches.size - 10} more\n"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
message += <<~MSG
|
|
189
|
-
|
|
190
|
-
## To load a specific guide, use the exact name:
|
|
191
|
-
```
|
|
192
|
-
MSG
|
|
193
|
-
|
|
194
|
-
matches.first(3).each do |match|
|
|
195
|
-
message += "load_guide guides: \"#{guides_type}\", guide: \"#{match}\"\n"
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
message += "```\n"
|
|
199
|
-
message
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def read_resource(uri, resource_class, params = {})
|
|
203
|
-
# Check if the resource supports the instance method (from templating extension)
|
|
204
|
-
if resource_class.respond_to?(:instance)
|
|
205
|
-
instance = resource_class.instance(uri)
|
|
206
|
-
return instance.content
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Fallback: manually create instance with proper initialization
|
|
210
|
-
create_resource_instance(resource_class, params)
|
|
211
|
-
rescue => e
|
|
212
|
-
log(:error, "Error reading resource #{uri}: #{e.message}")
|
|
213
|
-
format_error_message("Error loading guide: #{e.message}")
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def create_resource_instance(resource_class, params)
|
|
217
|
-
# Create instance using the proper pattern for FastMcp resources
|
|
218
|
-
instance = resource_class.allocate
|
|
219
|
-
|
|
220
|
-
# Set up the instance with parameters
|
|
221
|
-
instance.instance_variable_set(:@params, params)
|
|
222
|
-
|
|
223
|
-
# Initialize the instance (this calls the BaseResource initialize)
|
|
224
|
-
instance.send(:initialize)
|
|
225
|
-
|
|
226
|
-
# Call content to get the actual guide content
|
|
227
|
-
instance.content
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def generate_guide_name_variations(guide_name, guides_type)
|
|
231
|
-
variations = []
|
|
232
|
-
|
|
233
|
-
# Original name
|
|
234
|
-
variations << guide_name
|
|
235
|
-
|
|
236
|
-
# Underscore variations
|
|
237
|
-
variations << guide_name.gsub(/[_-]/, "_")
|
|
238
|
-
variations << guide_name.gsub(/\s+/, "_")
|
|
239
|
-
|
|
240
|
-
# Hyphen variations
|
|
241
|
-
variations << guide_name.gsub(/[_-]/, "-")
|
|
242
|
-
variations << guide_name.gsub(/\s+/, "-")
|
|
243
|
-
|
|
244
|
-
# Case variations
|
|
245
|
-
variations << guide_name.downcase
|
|
246
|
-
variations << guide_name.upcase
|
|
247
|
-
|
|
248
|
-
# Remove special characters
|
|
249
|
-
variations << guide_name.gsub(/[^a-zA-Z0-9_\/.-]/, "")
|
|
250
|
-
|
|
251
|
-
# Common guide patterns (snake_case, kebab-case)
|
|
252
|
-
if !guide_name.include?("_")
|
|
253
|
-
variations << guide_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# For Stimulus/Turbo, try with handbook/ and reference/ prefixes
|
|
257
|
-
# Custom and Rails and Kamal guides use flat structure, so no prefixes needed
|
|
258
|
-
unless guide_name.include?("/") || %w[custom rails kamal].include?(guides_type)
|
|
259
|
-
variations << "handbook/#{guide_name}"
|
|
260
|
-
variations << "reference/#{guide_name}"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Remove path prefixes for alternatives (for Stimulus/Turbo)
|
|
264
|
-
if guide_name.include?("/") && !%w[custom rails kamal].include?(guides_type)
|
|
265
|
-
base_name = guide_name.split("/").last
|
|
266
|
-
variations << base_name
|
|
267
|
-
variations.concat(generate_guide_name_variations(base_name, guides_type))
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
variations.uniq.compact # rubocop:disable Performance/ChainArrayAllocation
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def format_guide_not_found_message(guide_name, guides_type)
|
|
274
|
-
message = <<~MSG
|
|
275
|
-
# Guide Not Found
|
|
276
|
-
|
|
277
|
-
Guide '#{guide_name}' not found in #{guides_type} guides.
|
|
278
|
-
|
|
279
|
-
## Suggestions:
|
|
280
|
-
- Use `load_guide guides: "#{guides_type}"` to see all available guides
|
|
281
|
-
- Check the guide name spelling
|
|
282
|
-
- Try common variations like:
|
|
283
|
-
- `#{guide_name.gsub(/[_-]/, "_")}`
|
|
284
|
-
- `#{guide_name.gsub(/\s+/, "_")}`
|
|
285
|
-
- `#{guide_name.downcase}`
|
|
286
|
-
MSG
|
|
287
|
-
|
|
288
|
-
# Add framework-specific suggestions
|
|
289
|
-
case guides_type
|
|
290
|
-
when "stimulus", "turbo"
|
|
291
|
-
message += <<~MSG
|
|
292
|
-
- Try with section prefix: `handbook/#{guide_name}` or `reference/#{guide_name}`
|
|
293
|
-
- Try without section prefix if you used one
|
|
294
|
-
MSG
|
|
295
|
-
when "custom"
|
|
296
|
-
message += <<~MSG
|
|
297
|
-
- Import custom guides with: `rails-mcp-server-download-resources --file /path/to/guides`
|
|
298
|
-
- Make sure your custom guides have been imported
|
|
299
|
-
MSG
|
|
300
|
-
when "kamal"
|
|
301
|
-
message += <<~MSG
|
|
302
|
-
- Try with section prefix: `commands/#{guide_name}` or `configuration/#{guide_name}`
|
|
303
|
-
- Check available sections: installation, configuration, commands, hooks, upgrading
|
|
304
|
-
MSG
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
message += <<~MSG
|
|
308
|
-
|
|
309
|
-
## Available Commands:
|
|
310
|
-
- List guides: `load_guide guides: "#{guides_type}"`
|
|
311
|
-
- Load guide: `load_guide guides: "#{guides_type}", guide: "guide_name"`
|
|
312
|
-
|
|
313
|
-
## Example Usage:
|
|
314
|
-
```
|
|
315
|
-
MSG
|
|
316
|
-
|
|
317
|
-
case guides_type
|
|
318
|
-
when "rails"
|
|
319
|
-
message += <<~MSG
|
|
320
|
-
load_guide guides: "rails", guide: "active_record_validations"
|
|
321
|
-
load_guide guides: "rails", guide: "getting_started"
|
|
322
|
-
MSG
|
|
323
|
-
when "stimulus"
|
|
324
|
-
message += <<~MSG
|
|
325
|
-
load_guide guides: "stimulus", guide: "actions"
|
|
326
|
-
load_guide guides: "stimulus", guide: "01_introduction"
|
|
327
|
-
load_guide guides: "stimulus", guide: "handbook/02_hello_stimulus"
|
|
328
|
-
MSG
|
|
329
|
-
when "turbo"
|
|
330
|
-
message += <<~MSG
|
|
331
|
-
load_guide guides: "turbo", guide: "drive"
|
|
332
|
-
load_guide guides: "turbo", guide: "02_drive"
|
|
333
|
-
load_guide guides: "turbo", guide: "reference/attributes"
|
|
334
|
-
MSG
|
|
335
|
-
when "kamal"
|
|
336
|
-
message += <<~MSG
|
|
337
|
-
load_guide guides: "kamal", guide: "installation"
|
|
338
|
-
load_guide guides: "kamal", guide: "configuration"
|
|
339
|
-
load_guide guides: "kamal", guide: "commands/deploy"
|
|
340
|
-
MSG
|
|
341
|
-
when "custom"
|
|
342
|
-
message += <<~MSG
|
|
343
|
-
load_guide guides: "custom", guide: "api_documentation"
|
|
344
|
-
load_guide guides: "custom", guide: "setup_guide"
|
|
345
|
-
load_guide guides: "custom", guide: "user_manual"
|
|
346
|
-
MSG
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
message += "```\n"
|
|
350
|
-
|
|
351
|
-
log(:warn, "Guide not found: #{guide_name}")
|
|
352
|
-
message
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def format_error_message(message)
|
|
356
|
-
<<~MSG
|
|
357
|
-
# Error Loading Guide
|
|
358
|
-
|
|
359
|
-
#{message}
|
|
360
|
-
|
|
361
|
-
## Troubleshooting:
|
|
362
|
-
- Ensure guides are downloaded: `rails-mcp-server-download-resources [rails|stimulus|turbo|kamal]`
|
|
363
|
-
- For custom guides: `rails-mcp-server-download-resources --file /path/to/guides`
|
|
364
|
-
- Check that the MCP server is properly configured
|
|
365
|
-
- Verify guide name is correct
|
|
366
|
-
- Use `load_guide guides: "[rails|stimulus|turbo|kamal|custom]"` to see available guides
|
|
367
|
-
MSG
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
class ProjectInfo < BaseTool
|
|
3
|
-
tool_name "project_info"
|
|
4
|
-
|
|
5
|
-
description "Retrieve comprehensive information about the current Rails project, including Rails version, directory structure, API-only status, and overall project organization. Useful for initial project exploration and understanding the codebase structure."
|
|
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
|
-
# Get additional project information
|
|
16
|
-
gemfile_path = File.join(active_project_path, "Gemfile")
|
|
17
|
-
gemfile_content = File.exist?(gemfile_path) ? File.read(gemfile_path) : "Gemfile not found"
|
|
18
|
-
|
|
19
|
-
# Get Rails version
|
|
20
|
-
rails_version = gemfile_content.match(/gem ['"]rails['"],\s*['"](.+?)['"]/)&.captures&.first || "Unknown"
|
|
21
|
-
|
|
22
|
-
# Check if it's an API-only app
|
|
23
|
-
config_application_path = File.join(active_project_path, "config", "application.rb")
|
|
24
|
-
is_api_only = File.exist?(config_application_path) &&
|
|
25
|
-
File.read(config_application_path).include?("config.api_only = true")
|
|
26
|
-
|
|
27
|
-
log(:info, "Project info: Rails v#{rails_version}, API-only: #{is_api_only}")
|
|
28
|
-
|
|
29
|
-
<<~INFO
|
|
30
|
-
Current project: #{current_project}
|
|
31
|
-
Path: #{active_project_path}
|
|
32
|
-
Rails version: #{rails_version}
|
|
33
|
-
API only: #{is_api_only ? "Yes" : "No"}
|
|
34
|
-
|
|
35
|
-
Project structure:
|
|
36
|
-
#{get_directory_structure(active_project_path, max_depth: 2)}
|
|
37
|
-
INFO
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
# Utility functions for Rails operations
|
|
43
|
-
def get_directory_structure(path, max_depth: 3, current_depth: 0, prefix: "")
|
|
44
|
-
return "" if current_depth > max_depth || !File.directory?(path)
|
|
45
|
-
|
|
46
|
-
# Define ignored directories
|
|
47
|
-
ignored_dirs = [
|
|
48
|
-
".git", "node_modules", "tmp", "log",
|
|
49
|
-
"storage", "coverage", "public/assets",
|
|
50
|
-
"public/packs", ".bundle", "vendor/bundle",
|
|
51
|
-
"vendor/cache", ".ruby-lsp"
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
output = ""
|
|
55
|
-
directories = []
|
|
56
|
-
files = []
|
|
57
|
-
|
|
58
|
-
Dir.foreach(path) do |entry|
|
|
59
|
-
next if entry == "." || entry == ".."
|
|
60
|
-
next if ignored_dirs.include?(entry) # Skip ignored directories
|
|
61
|
-
|
|
62
|
-
full_path = File.join(path, entry)
|
|
63
|
-
|
|
64
|
-
if File.directory?(full_path)
|
|
65
|
-
directories << entry
|
|
66
|
-
else
|
|
67
|
-
files << entry
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
directories.sort.each do |dir|
|
|
72
|
-
output << "#{prefix}└── #{dir}/\n"
|
|
73
|
-
full_path = File.join(path, dir)
|
|
74
|
-
output << get_directory_structure(full_path, max_depth: max_depth,
|
|
75
|
-
current_depth: current_depth + 1,
|
|
76
|
-
prefix: "#{prefix} ")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
files.sort.each do |file|
|
|
80
|
-
output << "#{prefix}└── #{file}\n"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
output
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|