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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +216 -0
  3. data/README.md +156 -46
  4. data/config/resources.yml +203 -0
  5. data/docs/RESOURCES.md +339 -0
  6. data/exe/rails-mcp-server +8 -5
  7. data/exe/rails-mcp-server-download-resources +120 -0
  8. data/lib/rails-mcp-server/config.rb +7 -1
  9. data/lib/rails-mcp-server/extensions/resource_templating.rb +182 -0
  10. data/lib/rails-mcp-server/extensions/server_templating.rb +333 -0
  11. data/lib/rails-mcp-server/helpers/resource_base.rb +143 -0
  12. data/lib/rails-mcp-server/helpers/resource_downloader.rb +104 -0
  13. data/lib/rails-mcp-server/helpers/resource_importer.rb +113 -0
  14. data/lib/rails-mcp-server/resources/base_resource.rb +7 -0
  15. data/lib/rails-mcp-server/resources/custom_guides_resource.rb +54 -0
  16. data/lib/rails-mcp-server/resources/custom_guides_resources.rb +37 -0
  17. data/lib/rails-mcp-server/resources/guide_content_formatter.rb +130 -0
  18. data/lib/rails-mcp-server/resources/guide_error_handler.rb +85 -0
  19. data/lib/rails-mcp-server/resources/guide_file_finder.rb +100 -0
  20. data/lib/rails-mcp-server/resources/guide_framework_contract.rb +65 -0
  21. data/lib/rails-mcp-server/resources/guide_loader_template.rb +122 -0
  22. data/lib/rails-mcp-server/resources/guide_manifest_operations.rb +52 -0
  23. data/lib/rails-mcp-server/resources/kamal_guides_resource.rb +80 -0
  24. data/lib/rails-mcp-server/resources/kamal_guides_resources.rb +110 -0
  25. data/lib/rails-mcp-server/resources/rails_guides_resource.rb +29 -0
  26. data/lib/rails-mcp-server/resources/rails_guides_resources.rb +37 -0
  27. data/lib/rails-mcp-server/resources/stimulus_guides_resource.rb +29 -0
  28. data/lib/rails-mcp-server/resources/stimulus_guides_resources.rb +37 -0
  29. data/lib/rails-mcp-server/resources/turbo_guides_resource.rb +29 -0
  30. data/lib/rails-mcp-server/resources/turbo_guides_resources.rb +37 -0
  31. data/lib/rails-mcp-server/tools/analyze_models.rb +1 -1
  32. data/lib/rails-mcp-server/tools/load_guide.rb +370 -0
  33. data/lib/rails-mcp-server/version.rb +1 -1
  34. data/lib/rails_mcp_server.rb +51 -283
  35. 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
@@ -1,3 +1,3 @@
1
1
  module RailsMcpServer
2
- VERSION = "1.1.4"
2
+ VERSION = "1.2.1"
3
3
  end