rails-mcp-server 1.1.4 → 1.2.1
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 +216 -0
- data/README.md +156 -46
- data/config/resources.yml +203 -0
- data/docs/RESOURCES.md +339 -0
- data/exe/rails-mcp-server +8 -5
- data/exe/rails-mcp-server-download-resources +120 -0
- data/lib/rails-mcp-server/config.rb +7 -1
- data/lib/rails-mcp-server/extensions/resource_templating.rb +182 -0
- data/lib/rails-mcp-server/extensions/server_templating.rb +333 -0
- data/lib/rails-mcp-server/helpers/resource_base.rb +143 -0
- data/lib/rails-mcp-server/helpers/resource_downloader.rb +104 -0
- data/lib/rails-mcp-server/helpers/resource_importer.rb +113 -0
- data/lib/rails-mcp-server/resources/base_resource.rb +7 -0
- data/lib/rails-mcp-server/resources/custom_guides_resource.rb +54 -0
- data/lib/rails-mcp-server/resources/custom_guides_resources.rb +37 -0
- data/lib/rails-mcp-server/resources/guide_content_formatter.rb +130 -0
- data/lib/rails-mcp-server/resources/guide_error_handler.rb +85 -0
- data/lib/rails-mcp-server/resources/guide_file_finder.rb +100 -0
- data/lib/rails-mcp-server/resources/guide_framework_contract.rb +65 -0
- data/lib/rails-mcp-server/resources/guide_loader_template.rb +122 -0
- data/lib/rails-mcp-server/resources/guide_manifest_operations.rb +52 -0
- data/lib/rails-mcp-server/resources/kamal_guides_resource.rb +80 -0
- data/lib/rails-mcp-server/resources/kamal_guides_resources.rb +110 -0
- data/lib/rails-mcp-server/resources/rails_guides_resource.rb +29 -0
- data/lib/rails-mcp-server/resources/rails_guides_resources.rb +37 -0
- data/lib/rails-mcp-server/resources/stimulus_guides_resource.rb +29 -0
- data/lib/rails-mcp-server/resources/stimulus_guides_resources.rb +37 -0
- data/lib/rails-mcp-server/resources/turbo_guides_resource.rb +29 -0
- data/lib/rails-mcp-server/resources/turbo_guides_resources.rb +37 -0
- data/lib/rails-mcp-server/tools/analyze_models.rb +1 -1
- data/lib/rails-mcp-server/tools/load_guide.rb +370 -0
- data/lib/rails-mcp-server/version.rb +1 -1
- data/lib/rails_mcp_server.rb +51 -283
- metadata +49 -6
@@ -0,0 +1,370 @@
|
|
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
|