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,122 @@
1
+ module RailsMcpServer
2
+ # Template module that provides complete guide loading implementation
3
+ # Eliminates the need to implement load_specific_guide in each resource
4
+ module GuideLoaderTemplate
5
+ def self.included(base)
6
+ # Ensure all required modules are included
7
+ base.include GuideFrameworkContract unless base.included_modules.include?(GuideFrameworkContract)
8
+ base.include GuideManifestOperations unless base.included_modules.include?(GuideManifestOperations)
9
+ base.include GuideFileFinder unless base.included_modules.include?(GuideFileFinder)
10
+ base.include GuideContentFormatter unless base.included_modules.include?(GuideContentFormatter)
11
+ base.include GuideErrorHandler unless base.included_modules.include?(GuideErrorHandler)
12
+ end
13
+
14
+ # Complete single guide content implementation
15
+ def content
16
+ guide_name = params[:guide_name]
17
+
18
+ begin
19
+ manifest = load_manifest
20
+ rescue => e
21
+ return handle_manifest_error(e)
22
+ end
23
+
24
+ if !guide_name.nil? && !guide_name.strip.empty?
25
+ log(:debug, "Loading #{framework_name} guide: #{guide_name}")
26
+ load_specific_guide(guide_name, manifest)
27
+ else
28
+ log(:debug, "Provide a name for a #{framework_name} guide")
29
+ "Provide a name for a #{framework_name} guide"
30
+ end
31
+ end
32
+
33
+ # Complete guides list implementation
34
+ def list_content
35
+ begin
36
+ manifest = load_manifest
37
+ rescue => e
38
+ return handle_manifest_error(e)
39
+ end
40
+
41
+ log(:debug, "Loading #{framework_name} guides...")
42
+ format_guides_index(manifest)
43
+ end
44
+
45
+ protected
46
+
47
+ # Template method for loading a specific guide
48
+ def load_specific_guide(guide_name, manifest)
49
+ normalized_guide_name = guide_name.gsub(/[^a-zA-Z0-9_\/.-]/, "")
50
+
51
+ begin
52
+ filename, guide_data = find_guide_file(normalized_guide_name, manifest)
53
+
54
+ if filename && guide_data
55
+ guides_path = File.dirname(File.join(config_dir, "resources", resource_directory, "manifest.yaml"))
56
+ guide_file_path = File.join(guides_path, filename)
57
+
58
+ if File.exist?(guide_file_path)
59
+ log(:debug, "Loading guide: #{filename}")
60
+ content = File.read(guide_file_path)
61
+
62
+ # Allow customization of display name
63
+ display_name = customize_display_name(guide_name, guide_data)
64
+ format_guide_content(content, display_name, guide_data, filename)
65
+ else
66
+ format_not_found_message(guide_name, manifest)
67
+ end
68
+ else
69
+ format_not_found_message(guide_name, manifest)
70
+ end
71
+ rescue => e
72
+ handle_guide_loading_error(guide_name, e)
73
+ end
74
+ end
75
+
76
+ # Template method for formatting guides index
77
+ def format_guides_index(manifest)
78
+ guides = []
79
+
80
+ guides << "# Available #{framework_name} Guides\n"
81
+ guides << "Use the `load_guide` tool with `guides: \"#{framework_name.downcase}\"` and `guide: \"guide_name\"` to load a specific guide.\n"
82
+
83
+ if supports_sections?
84
+ guides << "You can use either the full path (e.g., `handbook/01_introduction`) or just the filename (e.g., `01_introduction`).\n"
85
+ end
86
+
87
+ guide_files = get_guide_files(manifest)
88
+
89
+ if supports_sections?
90
+ guides.concat(format_sectioned_guides(guide_files))
91
+ else
92
+ guides.concat(format_flat_guides(guide_files))
93
+ end
94
+
95
+ # Add examples if this is a list resource
96
+ if respond_to?(:example_guides) && example_guides.any?
97
+ guides << format_usage_examples
98
+ end
99
+
100
+ guides.join("\n")
101
+ end
102
+
103
+ # Template method for not found messages
104
+ def format_not_found_message(guide_name, manifest)
105
+ guide_files = get_guide_files(manifest)
106
+ available_guides = guide_files.keys.map { |f| f.sub(".md", "") }
107
+
108
+ message = create_not_found_message(guide_name, available_guides)
109
+
110
+ # Allow framework-specific additions to not found message
111
+ message = customize_not_found_message(message, guide_name) if respond_to?(:customize_not_found_message, true)
112
+
113
+ log(:error, "Guide not found: #{guide_name}")
114
+ message
115
+ end
116
+
117
+ # Hook for customizing display name (override in resources if needed)
118
+ def customize_display_name(guide_name, guide_data)
119
+ guide_name
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,52 @@
1
+ module RailsMcpServer
2
+ # Module for handling guide manifest operations
3
+ module GuideManifestOperations
4
+ protected
5
+
6
+ # Load and validate manifest file
7
+ def load_manifest
8
+ manifest_file = File.join(config_dir, "resources", resource_directory, "manifest.yaml")
9
+
10
+ unless File.exist?(manifest_file)
11
+ error_message = "No #{framework_name} guides found. Run '#{download_command}' first."
12
+ log(:error, error_message)
13
+ raise StandardError, error_message
14
+ end
15
+
16
+ YAML.load_file(manifest_file)
17
+ end
18
+
19
+ # Extract guide metadata from manifest entry
20
+ def extract_guide_metadata(filename, file_data)
21
+ {
22
+ filename: filename,
23
+ guide_name: filename.sub(".md", ""),
24
+ title: file_data["title"] || generate_title_from_filename(filename),
25
+ description: file_data["description"] || "",
26
+ original_filename: file_data["original_filename"] # For custom guides
27
+ }
28
+ end
29
+
30
+ # Generate title from filename if not in manifest
31
+ def generate_title_from_filename(filename)
32
+ base_name = filename.sub(".md", "").split("/").last
33
+ base_name.gsub(/[_-]/, " ").split.map(&:capitalize).join(" ")
34
+ end
35
+
36
+ # Get all guide files from manifest
37
+ def get_guide_files(manifest)
38
+ manifest["files"].select { |filename, _| filename.end_with?(".md") }
39
+ end
40
+
41
+ # Get guide files organized by sections
42
+ def get_sectioned_guide_files(manifest)
43
+ guide_files = get_guide_files(manifest)
44
+
45
+ {
46
+ handbook: guide_files.select { |filename, _| filename.start_with?("handbook/") },
47
+ reference: guide_files.select { |filename, _| filename.start_with?("reference/") },
48
+ other: guide_files.reject { |filename, _| filename.start_with?("handbook/", "reference/") }
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ module RailsMcpServer
2
+ class KamalGuidesResource < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "kamal://guides/{guide_name}"
6
+ resource_name "Kamal Guides"
7
+ description "Access to specific Kamal deployment documentation"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Kamal"
14
+ end
15
+
16
+ def resource_directory
17
+ "kamal"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources kamal"
22
+ end
23
+
24
+ # Kamal guides have subdirectories but not handbook/reference sections
25
+ def supports_sections?
26
+ false
27
+ end
28
+
29
+ # Override for Kamal's directory structure
30
+ def framework_specific_filenames(normalized_guide_name)
31
+ possible_files = []
32
+
33
+ if normalized_guide_name.include?("/")
34
+ possible_files << normalized_guide_name
35
+ possible_files << "#{normalized_guide_name}.md"
36
+ else
37
+ %w[installation configuration commands hooks upgrading].each do |section|
38
+ possible_files << "#{section}/#{normalized_guide_name}.md"
39
+ possible_files << "#{section}/index.md" if normalized_guide_name == section
40
+ end
41
+ possible_files << "#{normalized_guide_name}/index.md"
42
+ end
43
+
44
+ possible_files
45
+ end
46
+
47
+ # Override for Kamal's section detection
48
+ def framework_specific_section(filename)
49
+ case filename
50
+ when /^installation\// then "Installation"
51
+ when /^configuration\// then "Configuration"
52
+ when /^commands\// then "Commands"
53
+ when /^hooks\// then "Hooks"
54
+ when /^upgrading\// then "Upgrading"
55
+ else; "Documentation"
56
+ end
57
+ end
58
+
59
+ # Enhanced fuzzy matching for hierarchical structure
60
+ def fuzzy_match_files(normalized_guide_name, manifest)
61
+ search_term = normalized_guide_name.downcase
62
+
63
+ manifest["files"].select do |file, _|
64
+ next false unless file.end_with?(".md")
65
+
66
+ file_path = file.downcase
67
+ file_name_base = file.sub(".md", "").split("/").last.downcase
68
+ file_full_path = file.sub(".md", "").downcase
69
+
70
+ file_path.include?(search_term) ||
71
+ file_name_base.include?(search_term) ||
72
+ search_term.include?(file_name_base) ||
73
+ file_full_path.include?(search_term) ||
74
+ search_term.include?(file_full_path) ||
75
+ file_name_base.gsub(/[_-]/, "").include?(search_term.gsub(/[_-]/, "")) ||
76
+ search_term.gsub(/[_-]/, "").include?(file_name_base.gsub(/[_-]/, ""))
77
+ end.to_a
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,110 @@
1
+ module RailsMcpServer
2
+ class KamalGuidesResources < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "kamal://guides"
6
+ resource_name "Kamal Guides"
7
+ description "Access to available Kamal deployment guides"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Kamal"
14
+ end
15
+
16
+ def resource_directory
17
+ "kamal"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources kamal"
22
+ end
23
+
24
+ def example_guides
25
+ [
26
+ {guide: "installation/index", comment: "Load installation guide"},
27
+ {guide: "configuration/environment-variables", comment: "Load environment variables configuration"},
28
+ {guide: "commands/deploy", comment: "Load deploy command guide"},
29
+ {guide: "hooks/overview", comment: "Load hooks overview"},
30
+ {guide: "upgrading/overview", comment: "Load upgrading overview"}
31
+ ]
32
+ end
33
+
34
+ # Kamal guides have subdirectories but not handbook/reference sections
35
+ def supports_sections?
36
+ false
37
+ end
38
+
39
+ # Override to format guides organized by Kamal's directory structure
40
+ def format_flat_guides(manifest)
41
+ guides = []
42
+
43
+ # Group guides by their directory structure
44
+ sections = {
45
+ "installation" => [],
46
+ "configuration" => [],
47
+ "commands" => [],
48
+ "hooks" => [],
49
+ "upgrading" => []
50
+ }
51
+
52
+ other_guides = []
53
+
54
+ manifest["files"].each do |filename, file_data|
55
+ next unless filename.end_with?(".md")
56
+
57
+ log(:debug, "Processing guide: #{filename}")
58
+
59
+ guide_name = filename.sub(".md", "")
60
+ title = file_data["title"] || guide_name.split("/").last.gsub(/[_-]/, " ").split.map(&:capitalize).join(" ")
61
+ description = file_data["description"] || ""
62
+
63
+ # Categorize by directory
64
+ case filename
65
+ when /^installation\//
66
+ sections["installation"] << {name: guide_name, title: title, description: description}
67
+ when /^configuration\//
68
+ sections["configuration"] << {name: guide_name, title: title, description: description}
69
+ when /^commands\//
70
+ sections["commands"] << {name: guide_name, title: title, description: description}
71
+ when /^hooks\//
72
+ sections["hooks"] << {name: guide_name, title: title, description: description}
73
+ when /^upgrading\//
74
+ sections["upgrading"] << {name: guide_name, title: title, description: description}
75
+ else
76
+ other_guides << {name: guide_name, title: title, description: description}
77
+ end
78
+ end
79
+
80
+ # Format each section
81
+ sections.each do |section_name, section_guides|
82
+ next if section_guides.empty?
83
+
84
+ guides << "\n## #{section_name.capitalize}\n"
85
+ section_guides.each do |guide|
86
+ guides << format_guide_entry(guide[:title], guide[:name], guide[:name], guide[:description])
87
+ end
88
+ end
89
+
90
+ # Add any other guides that don't fit the standard structure
91
+ if other_guides.any?
92
+ guides << "\n## Other\n"
93
+ other_guides.each do |guide|
94
+ guides << format_guide_entry(guide[:title], guide[:name], guide[:name], guide[:description])
95
+ end
96
+ end
97
+
98
+ guides
99
+ end
100
+
101
+ # Format individual guide entry
102
+ def format_guide_entry(title, short_name, full_name, description)
103
+ <<~GUIDE
104
+ ### #{title}
105
+ **Guide name:** `#{short_name}`
106
+ #{description.empty? ? "" : "**Description:** #{description}"}
107
+ GUIDE
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,29 @@
1
+ module RailsMcpServer
2
+ class RailsGuidesResource < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "rails://guides/{guide_name}"
6
+ resource_name "Rails Guides"
7
+ description "Access to specific Rails documentation"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Rails"
14
+ end
15
+
16
+ def resource_directory
17
+ "rails"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources rails"
22
+ end
23
+
24
+ # Rails guides don't use handbook/reference sections
25
+ def supports_sections?
26
+ false
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ module RailsMcpServer
2
+ class RailsGuidesResources < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "rails://guides"
6
+ resource_name "Rails Guides List"
7
+ description "Access to available Rails guides"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Rails"
14
+ end
15
+
16
+ def resource_directory
17
+ "rails"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources rails"
22
+ end
23
+
24
+ def example_guides
25
+ [
26
+ {guide: "active_record_validations", comment: "Load validations guide"},
27
+ {guide: "getting_started", comment: "Load getting started guide"},
28
+ {guide: "routing", comment: "Load routing guide"}
29
+ ]
30
+ end
31
+
32
+ # Rails guides don't use handbook/reference sections
33
+ def supports_sections?
34
+ false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ module RailsMcpServer
2
+ class StimulusGuidesResource < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "stimulus://guides/{guide_name}"
6
+ resource_name "Stimulus Guides"
7
+ description "Access to specific Stimulus documentation"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Stimulus"
14
+ end
15
+
16
+ def resource_directory
17
+ "stimulus"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources stimulus"
22
+ end
23
+
24
+ # Stimulus guides use handbook/reference sections
25
+ def supports_sections?
26
+ true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ module RailsMcpServer
2
+ class StimulusGuidesResources < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "stimulus://guides"
6
+ resource_name "Stimulus Guides"
7
+ description "Access to available Stimulus guides"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Stimulus"
14
+ end
15
+
16
+ def resource_directory
17
+ "stimulus"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources stimulus"
22
+ end
23
+
24
+ def example_guides
25
+ [
26
+ {guide: "actions", comment: "Load actions reference"},
27
+ {guide: "01_introduction", comment: "Load introduction"},
28
+ {guide: "reference/targets", comment: "Load targets with full path"}
29
+ ]
30
+ end
31
+
32
+ # Stimulus guides use handbook/reference sections
33
+ def supports_sections?
34
+ true
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ module RailsMcpServer
2
+ class TurboGuidesResource < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "turbo://guides/{guide_name}"
6
+ resource_name "Turbo Guides"
7
+ description "Access to specific Turbo documentation"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Turbo"
14
+ end
15
+
16
+ def resource_directory
17
+ "turbo"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources turbo"
22
+ end
23
+
24
+ # Turbo guides use handbook/reference sections
25
+ def supports_sections?
26
+ true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ module RailsMcpServer
2
+ class TurboGuidesResources < BaseResource
3
+ include GuideLoaderTemplate
4
+
5
+ uri "turbo://guides"
6
+ resource_name "Turbo Guides"
7
+ description "Access to available Turbo guides"
8
+ mime_type "text/markdown"
9
+
10
+ protected
11
+
12
+ def framework_name
13
+ "Turbo"
14
+ end
15
+
16
+ def resource_directory
17
+ "turbo"
18
+ end
19
+
20
+ def download_command
21
+ "rails-mcp-server-download-resources turbo"
22
+ end
23
+
24
+ def example_guides
25
+ [
26
+ {guide: "drive", comment: "Load drive reference"},
27
+ {guide: "02_drive", comment: "Load drive handbook"},
28
+ {guide: "reference/frames", comment: "Load frames with full path"}
29
+ ]
30
+ end
31
+
32
+ # Turbo guides use handbook/reference sections
33
+ def supports_sections?
34
+ true
35
+ end
36
+ end
37
+ end
@@ -1,6 +1,6 @@
1
1
  module RailsMcpServer
2
2
  class AnalyzeModels < BaseTool
3
- tool_name "analize_models"
3
+ tool_name "analyze_models"
4
4
 
5
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
6