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,333 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
module Extensions
|
|
3
|
-
# Extension module to add any missing templated resource support to FastMcp::Server
|
|
4
|
-
# This version of the server already has most templated resource functionality
|
|
5
|
-
module ServerTemplating
|
|
6
|
-
# Instance methods to be prepended
|
|
7
|
-
module InstanceMethods
|
|
8
|
-
# The target server already has most functionality, but we can add defensive checks
|
|
9
|
-
def read_resource(uri)
|
|
10
|
-
# Handle both hash-based and array-based resource storage
|
|
11
|
-
if @resources.is_a?(Hash)
|
|
12
|
-
# First try exact match (hash lookup)
|
|
13
|
-
exact_match = @resources[uri]
|
|
14
|
-
return exact_match if exact_match
|
|
15
|
-
|
|
16
|
-
# Then try templated resource matching
|
|
17
|
-
@resources.values.find { |r| r.respond_to?(:match) && r.match(uri) }
|
|
18
|
-
else
|
|
19
|
-
# Array-based storage (original target server behavior)
|
|
20
|
-
resource = @resources.find { |r| r.respond_to?(:match) && r.match(uri) }
|
|
21
|
-
|
|
22
|
-
# Fallback: if no templated match, try exact URI match for backward compatibility
|
|
23
|
-
resource ||= @resources.find { |r| r.respond_to?(:uri) && r.uri == uri }
|
|
24
|
-
|
|
25
|
-
resource
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Add some defensive programming to handle_resources_read
|
|
30
|
-
def handle_resources_read(params, id)
|
|
31
|
-
uri = params["uri"]
|
|
32
|
-
|
|
33
|
-
return send_error(-32_602, "Invalid params: missing resource URI", id) unless uri
|
|
34
|
-
|
|
35
|
-
@logger.debug("Looking for resource with URI: #{uri}")
|
|
36
|
-
|
|
37
|
-
begin
|
|
38
|
-
resource = read_resource(uri)
|
|
39
|
-
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
|
40
|
-
|
|
41
|
-
# Defensive check for templated method
|
|
42
|
-
is_templated = resource.respond_to?(:templated?) ? resource.templated? : false
|
|
43
|
-
@logger.debug("Found resource: #{resource.respond_to?(:resource_name) ? resource.resource_name : resource.name}, templated: #{is_templated}")
|
|
44
|
-
|
|
45
|
-
base_content = {uri: uri}
|
|
46
|
-
base_content[:mimeType] = resource.mime_type if resource.mime_type
|
|
47
|
-
|
|
48
|
-
# Handle both templated and non-templated resources
|
|
49
|
-
resource_instance = if is_templated && resource.respond_to?(:instance)
|
|
50
|
-
resource.instance(uri)
|
|
51
|
-
else
|
|
52
|
-
# Fallback for non-templated resources or resources without instance method
|
|
53
|
-
resource.respond_to?(:instance) ? resource.instance : resource
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Defensive check for params method
|
|
57
|
-
if resource_instance.respond_to?(:params)
|
|
58
|
-
@logger.debug("Resource instance params: #{resource_instance.params.inspect}")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
result = if resource_instance.respond_to?(:binary?) && resource_instance.binary?
|
|
62
|
-
{
|
|
63
|
-
contents: [base_content.merge(blob: Base64.strict_encode64(resource_instance.content))]
|
|
64
|
-
}
|
|
65
|
-
else
|
|
66
|
-
{
|
|
67
|
-
contents: [base_content.merge(text: resource_instance.content)]
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
send_result(result, id)
|
|
72
|
-
rescue => e
|
|
73
|
-
@logger.error("Error reading resource: #{e.message}")
|
|
74
|
-
@logger.error(e.backtrace.join("\n"))
|
|
75
|
-
send_error(-32_600, "Internal error reading resource: #{e.message}", id)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# The target server already has these methods, but we can add defensive checks
|
|
80
|
-
def handle_resources_list(id)
|
|
81
|
-
# Handle both hash-based and array-based resource storage
|
|
82
|
-
resources_collection = @resources.is_a?(Hash) ? @resources.values : @resources
|
|
83
|
-
|
|
84
|
-
resources_list = resources_collection.select do |resource|
|
|
85
|
-
!resource.respond_to?(:templated?) || resource.non_templated?
|
|
86
|
-
end.map(&:metadata) # rubocop:disable Performance/ChainArrayAllocation
|
|
87
|
-
|
|
88
|
-
send_result({resources: resources_list}, id)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def handle_resources_templates_list(id)
|
|
92
|
-
@logger.debug("Handling resources/templates/list request")
|
|
93
|
-
|
|
94
|
-
# Handle both hash-based and array-based resource storage
|
|
95
|
-
resources_collection = @resources.is_a?(Hash) ? @resources.values : @resources
|
|
96
|
-
|
|
97
|
-
templated_resources_list = resources_collection.select do |resource|
|
|
98
|
-
resource.respond_to?(:templated?) && resource.templated?
|
|
99
|
-
end.map do |resource| # rubocop:disable Performance/ChainArrayAllocation
|
|
100
|
-
metadata = resource.metadata
|
|
101
|
-
@logger.debug("Template resource metadata: #{metadata}")
|
|
102
|
-
metadata
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
@logger.info("Returning #{templated_resources_list.length} templated resources")
|
|
106
|
-
send_result({resourceTemplates: templated_resources_list}, id)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Override handle_request to ensure resources/templates/list endpoint is available
|
|
110
|
-
def handle_request(*args)
|
|
111
|
-
# Extract arguments - handle different signatures
|
|
112
|
-
if args.length == 2
|
|
113
|
-
json_str, headers = args
|
|
114
|
-
headers ||= {}
|
|
115
|
-
else
|
|
116
|
-
json_str = args[0]
|
|
117
|
-
headers = {}
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
begin
|
|
121
|
-
request = JSON.parse(json_str)
|
|
122
|
-
rescue JSON::ParserError, TypeError
|
|
123
|
-
return send_error(-32_600, "Invalid Request", nil)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
@logger.debug("Received request: #{request.inspect}")
|
|
127
|
-
|
|
128
|
-
# Check if it's a valid JSON-RPC 2.0 request
|
|
129
|
-
unless request["jsonrpc"] == "2.0" && request["method"]
|
|
130
|
-
return send_error(-32_600, "Invalid Request", request["id"])
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
method = request["method"]
|
|
134
|
-
params = request["params"] || {}
|
|
135
|
-
id = request["id"]
|
|
136
|
-
|
|
137
|
-
# Handle the resources/templates/list endpoint specifically since it might not exist in original
|
|
138
|
-
if method == "resources/templates/list"
|
|
139
|
-
@logger.debug("Handling resources/templates/list via extension")
|
|
140
|
-
return handle_resources_templates_list(id)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# For all other methods, call the original implementation
|
|
144
|
-
begin
|
|
145
|
-
super
|
|
146
|
-
rescue NoMethodError => e
|
|
147
|
-
# If super doesn't work, provide our own fallback
|
|
148
|
-
@logger.debug("Original handle_request not available, using fallback: #{e.message}")
|
|
149
|
-
handle_request_fallback(method, params, id, headers)
|
|
150
|
-
end
|
|
151
|
-
rescue => e
|
|
152
|
-
@logger.error("Error handling request: #{e.message}, #{e.backtrace.join("\n")}")
|
|
153
|
-
send_error(-32_600, "Internal error: #{e.message}", id)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
private
|
|
157
|
-
|
|
158
|
-
def handle_request_fallback(method, params, id, headers)
|
|
159
|
-
@logger.debug("Using fallback handler for method: #{method}")
|
|
160
|
-
|
|
161
|
-
case method
|
|
162
|
-
when "ping"
|
|
163
|
-
send_result({}, id)
|
|
164
|
-
when "initialize"
|
|
165
|
-
handle_initialize(params, id)
|
|
166
|
-
when "notifications/initialized"
|
|
167
|
-
handle_initialized_notification
|
|
168
|
-
when "tools/list"
|
|
169
|
-
handle_tools_list(id)
|
|
170
|
-
when "tools/call"
|
|
171
|
-
# Handle different method signatures for tools/call
|
|
172
|
-
if method(:handle_tools_call).arity == 3
|
|
173
|
-
handle_tools_call(params, headers, id)
|
|
174
|
-
else
|
|
175
|
-
handle_tools_call(params, id)
|
|
176
|
-
end
|
|
177
|
-
when "resources/list"
|
|
178
|
-
handle_resources_list(id)
|
|
179
|
-
when "resources/templates/list"
|
|
180
|
-
handle_resources_templates_list(id)
|
|
181
|
-
when "resources/read"
|
|
182
|
-
handle_resources_read(params, id)
|
|
183
|
-
when "resources/subscribe"
|
|
184
|
-
handle_resources_subscribe(params, id)
|
|
185
|
-
when "resources/unsubscribe"
|
|
186
|
-
handle_resources_unsubscribe(params, id)
|
|
187
|
-
else
|
|
188
|
-
send_error(-32_601, "Method not found: #{method}", id)
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Add defensive programming to resource subscription methods
|
|
193
|
-
def handle_resources_subscribe(params, id)
|
|
194
|
-
return unless @client_initialized
|
|
195
|
-
|
|
196
|
-
uri = params["uri"]
|
|
197
|
-
|
|
198
|
-
unless uri
|
|
199
|
-
send_error(-32_602, "Invalid params: missing resource URI", id)
|
|
200
|
-
return
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Use the read_resource method which supports templated resources
|
|
204
|
-
resource = read_resource(uri)
|
|
205
|
-
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
|
206
|
-
|
|
207
|
-
# Add to subscriptions
|
|
208
|
-
@resource_subscriptions[uri] ||= []
|
|
209
|
-
@resource_subscriptions[uri] << id
|
|
210
|
-
|
|
211
|
-
send_result({subscribed: true}, id)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Enhanced logging for resource registration
|
|
215
|
-
def register_resource(resource)
|
|
216
|
-
# Handle both hash-based and array-based resource storage
|
|
217
|
-
if @resources.is_a?(Hash)
|
|
218
|
-
@resources[resource.uri] = resource
|
|
219
|
-
else
|
|
220
|
-
@resources << resource
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
resource_name = if resource.respond_to?(:resource_name)
|
|
224
|
-
resource.resource_name
|
|
225
|
-
else
|
|
226
|
-
(resource.respond_to?(:name) ? resource.name : "Unknown")
|
|
227
|
-
end
|
|
228
|
-
is_templated = resource.respond_to?(:templated?) ? resource.templated? : false
|
|
229
|
-
|
|
230
|
-
@logger.debug("Registered resource: #{resource_name} (#{resource.uri}) - Templated: #{is_templated}")
|
|
231
|
-
resource.server = self if resource.respond_to?(:server=)
|
|
232
|
-
|
|
233
|
-
# Notify subscribers about the list change
|
|
234
|
-
notify_resource_list_changed if @transport
|
|
235
|
-
|
|
236
|
-
resource
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Called when this module is prepended to a class
|
|
241
|
-
def self.prepended(base)
|
|
242
|
-
base.prepend(InstanceMethods)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Setup class for server extensions
|
|
247
|
-
class ServerExtensionSetup
|
|
248
|
-
class << self
|
|
249
|
-
def setup!
|
|
250
|
-
return if @setup_complete
|
|
251
|
-
|
|
252
|
-
ensure_dependencies_loaded!
|
|
253
|
-
check_server_compatibility!
|
|
254
|
-
apply_extensions_if_needed!
|
|
255
|
-
|
|
256
|
-
@setup_complete = true
|
|
257
|
-
RailsMcpServer.log(:info, "FastMcp::Server extensions checked and applied if needed")
|
|
258
|
-
rescue => e
|
|
259
|
-
RailsMcpServer.log(:error, "Failed to setup server extensions: #{e.message}")
|
|
260
|
-
raise
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def reset!
|
|
264
|
-
@setup_complete = false
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def setup_complete?
|
|
268
|
-
@setup_complete || false
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
private
|
|
272
|
-
|
|
273
|
-
def ensure_dependencies_loaded!
|
|
274
|
-
# Check that FastMcp::Server exists
|
|
275
|
-
unless defined?(FastMcp::Server)
|
|
276
|
-
begin
|
|
277
|
-
require "fast-mcp"
|
|
278
|
-
rescue LoadError => e
|
|
279
|
-
raise LoadError, "fast-mcp gem is required but not available: #{e.message}"
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Verify the expected interface exists
|
|
284
|
-
unless FastMcp::Server.instance_methods.include?(:read_resource)
|
|
285
|
-
raise "FastMcp::Server doesn't have expected read_resource method. Check fast-mcp gem version."
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Check handle_request method signature
|
|
289
|
-
handle_request_method = FastMcp::Server.instance_method(:handle_request)
|
|
290
|
-
arity = handle_request_method.arity
|
|
291
|
-
RailsMcpServer.log(:debug, "FastMcp::Server#handle_request arity: #{arity}")
|
|
292
|
-
|
|
293
|
-
# Check if resources/templates/list is already supported
|
|
294
|
-
test_server = FastMcp::Server.new(name: "test", version: "1.0.0")
|
|
295
|
-
has_templates_method = test_server.respond_to?(:handle_resources_templates_list)
|
|
296
|
-
RailsMcpServer.log(:debug, "Original server has handle_resources_templates_list: #{has_templates_method}")
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def check_server_compatibility!
|
|
300
|
-
# Check if the server already has templated resource support
|
|
301
|
-
server_instance = FastMcp::Server.new(name: "test", version: "1.0.0")
|
|
302
|
-
|
|
303
|
-
@server_has_templates = server_instance.respond_to?(:handle_resources_templates_list)
|
|
304
|
-
@server_has_advanced_read = begin
|
|
305
|
-
# Check if read_resource method body includes 'match'
|
|
306
|
-
method_source = FastMcp::Server.instance_method(:read_resource).source_location
|
|
307
|
-
method_source ? true : false
|
|
308
|
-
rescue
|
|
309
|
-
false
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
RailsMcpServer.log(:debug, "Server template support detected: #{@server_has_templates}")
|
|
313
|
-
RailsMcpServer.log(:debug, "Server advanced read support detected: #{@server_has_advanced_read}")
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def apply_extensions_if_needed!
|
|
317
|
-
# Always apply extensions to ensure resources/templates/list endpoint is available
|
|
318
|
-
# The MCP inspector error shows this endpoint is missing
|
|
319
|
-
RailsMcpServer.log(:info, "Applying server extensions to ensure full MCP compliance")
|
|
320
|
-
FastMcp::Server.prepend(ServerTemplating)
|
|
321
|
-
|
|
322
|
-
# Verify the extension was applied by checking if our methods are available
|
|
323
|
-
test_server = FastMcp::Server.new(name: "test", version: "1.0.0")
|
|
324
|
-
has_templates_list = test_server.respond_to?(:handle_resources_templates_list)
|
|
325
|
-
RailsMcpServer.log(:info, "Server extension verification - handle_resources_templates_list available: #{has_templates_list}")
|
|
326
|
-
rescue => e
|
|
327
|
-
RailsMcpServer.log(:error, "Error applying server extensions: #{e.message}")
|
|
328
|
-
raise
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
end
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
module RailsMcpServer
|
|
2
|
-
class AnalyzeControllerViews < BaseTool
|
|
3
|
-
tool_name "analyze_controller_views"
|
|
4
|
-
|
|
5
|
-
description "Analyze the relationships between controllers, their actions, and corresponding views to understand the application's UI flow."
|
|
6
|
-
|
|
7
|
-
arguments do
|
|
8
|
-
optional(:controller_name).filled(:string).description("Name of a specific controller to analyze (e.g., 'UsersController' or 'users'). If omitted, all controllers will be analyzed.")
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def call(controller_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
|
-
# Find all controllers
|
|
20
|
-
controllers_dir = File.join(active_project_path, "app", "controllers")
|
|
21
|
-
unless File.directory?(controllers_dir)
|
|
22
|
-
message = "Controllers directory not found at app/controllers."
|
|
23
|
-
log(:warn, message)
|
|
24
|
-
|
|
25
|
-
return message
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Get all controller files
|
|
29
|
-
controller_files = Dir.glob(File.join(controllers_dir, "**", "*_controller.rb"))
|
|
30
|
-
|
|
31
|
-
if controller_files.empty?
|
|
32
|
-
message = "No controllers found in the project."
|
|
33
|
-
log(:warn, message)
|
|
34
|
-
|
|
35
|
-
return message
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# If a specific controller was requested, filter the files
|
|
39
|
-
if controller_name
|
|
40
|
-
# Normalize controller name (allow both 'users' and 'UsersController')
|
|
41
|
-
controller_name = "#{controller_name.sub(/_?controller$/i, "").downcase}_controller.rb"
|
|
42
|
-
controller_files = controller_files.select { |f| File.basename(f).downcase == controller_name }
|
|
43
|
-
|
|
44
|
-
if controller_files.empty?
|
|
45
|
-
message = "Controller '#{controller_name}' not found."
|
|
46
|
-
log(:warn, message)
|
|
47
|
-
|
|
48
|
-
return message
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Parse controllers to extract actions
|
|
53
|
-
controllers_data = {}
|
|
54
|
-
|
|
55
|
-
controller_files.each do |file_path|
|
|
56
|
-
file_content = File.read(file_path)
|
|
57
|
-
controller_class = File.basename(file_path, ".rb").gsub(/_controller$/i, "").then { |s| camelize(s) } + "Controller"
|
|
58
|
-
|
|
59
|
-
# Extract controller actions (methods that are not private/protected)
|
|
60
|
-
actions = []
|
|
61
|
-
action_matches = file_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
|
62
|
-
|
|
63
|
-
# Find where private/protected begins
|
|
64
|
-
private_index = file_content =~ /^\s*(private|protected)/
|
|
65
|
-
|
|
66
|
-
if private_index
|
|
67
|
-
# Get the actions defined before private/protected
|
|
68
|
-
private_content = file_content[private_index..-1]
|
|
69
|
-
private_methods = private_content.scan(/def\s+([a-zA-Z0-9_]+)/).flatten
|
|
70
|
-
actions = action_matches - private_methods
|
|
71
|
-
else
|
|
72
|
-
actions = action_matches
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Remove Rails controller lifecycle methods
|
|
76
|
-
lifecycle_methods = %w[initialize action_name controller_name params response]
|
|
77
|
-
actions -= lifecycle_methods
|
|
78
|
-
|
|
79
|
-
# Get routes mapped to this controller
|
|
80
|
-
routes_output = RailsMcpServer::RunProcess.execute_rails_command(
|
|
81
|
-
active_project_path,
|
|
82
|
-
"bin/rails routes -c #{controller_class}"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
routes = {}
|
|
86
|
-
if routes_output && !routes_output.empty?
|
|
87
|
-
routes_output.split("\n").each do |line|
|
|
88
|
-
next if line.include?("(erb):") || line.include?("Prefix") || line.strip.empty?
|
|
89
|
-
parts = line.strip.split(/\s+/)
|
|
90
|
-
if parts.size >= 4
|
|
91
|
-
# Get action name from the rails routes output
|
|
92
|
-
action = parts[1].to_s.strip.downcase
|
|
93
|
-
if actions.include?(action)
|
|
94
|
-
verb = parts[0].to_s.strip
|
|
95
|
-
path = parts[2].to_s.strip
|
|
96
|
-
routes[action] = {verb: verb, path: path}
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Find views for each action
|
|
103
|
-
views_dir = File.join(active_project_path, "app", "views", File.basename(file_path, "_controller.rb"))
|
|
104
|
-
views = {}
|
|
105
|
-
|
|
106
|
-
if File.directory?(views_dir)
|
|
107
|
-
actions.each do |action|
|
|
108
|
-
# Look for view templates with various extensions
|
|
109
|
-
view_files = Dir.glob(File.join(views_dir, "#{action}.*"))
|
|
110
|
-
if view_files.any?
|
|
111
|
-
views[action] = {
|
|
112
|
-
templates: view_files.map { |f| f.sub("#{active_project_path}/", "") },
|
|
113
|
-
partials: []
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
# Look for partials used in this template
|
|
117
|
-
view_files.each do |view_file|
|
|
118
|
-
if File.file?(view_file)
|
|
119
|
-
view_content = File.read(view_file)
|
|
120
|
-
# Find render calls with partials
|
|
121
|
-
partial_matches = view_content.scan(/render\s+(?:partial:|:partial\s+=>\s+|:partial\s*=>|partial:)\s*["']([^"']+)["']/).flatten
|
|
122
|
-
views[action][:partials] += partial_matches if partial_matches.any?
|
|
123
|
-
|
|
124
|
-
# Find instance variables used in the view
|
|
125
|
-
instance_vars = view_content.scan(/@([a-zA-Z0-9_]+)/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
|
126
|
-
views[action][:instance_variables] = instance_vars if instance_vars.any?
|
|
127
|
-
|
|
128
|
-
# Look for Stimulus controllers
|
|
129
|
-
stimulus_controllers = view_content.scan(/data-controller="([^"]+)"/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
|
130
|
-
views[action][:stimulus_controllers] = stimulus_controllers if stimulus_controllers.any?
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Extract instance variables set in the controller action
|
|
138
|
-
instance_vars_in_controller = {}
|
|
139
|
-
actions.each do |action|
|
|
140
|
-
# Find the action method in the controller
|
|
141
|
-
action_match = file_content.match(/def\s+#{action}\b(.*?)(?:(?:def|private|protected|public)\b|\z)/m)
|
|
142
|
-
if action_match && action_match[1]
|
|
143
|
-
action_body = action_match[1]
|
|
144
|
-
# Find instance variable assignments
|
|
145
|
-
vars = action_body.scan(/@([a-zA-Z0-9_]+)\s*=/).flatten.uniq # rubocop:disable Performance/ChainArrayAllocation
|
|
146
|
-
instance_vars_in_controller[action] = vars if vars.any?
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
controllers_data[controller_class] = {
|
|
151
|
-
file: file_path.sub("#{active_project_path}/", ""),
|
|
152
|
-
actions: actions,
|
|
153
|
-
routes: routes,
|
|
154
|
-
views: views,
|
|
155
|
-
instance_variables: instance_vars_in_controller
|
|
156
|
-
}
|
|
157
|
-
rescue => e
|
|
158
|
-
log(:error, "Error parsing controller #{file_path}: #{e.message}")
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Format the output
|
|
162
|
-
output = []
|
|
163
|
-
|
|
164
|
-
controllers_data.each do |controller, data|
|
|
165
|
-
output << "Controller: #{controller}"
|
|
166
|
-
output << " File: #{data[:file]}"
|
|
167
|
-
output << " Actions: #{data[:actions].size}"
|
|
168
|
-
|
|
169
|
-
data[:actions].each do |action|
|
|
170
|
-
output << " Action: #{action}"
|
|
171
|
-
|
|
172
|
-
# Show route if available
|
|
173
|
-
if data[:routes] && data[:routes][action]
|
|
174
|
-
route = data[:routes][action]
|
|
175
|
-
output << " Route: [#{route[:verb]}] #{route[:path]}"
|
|
176
|
-
else
|
|
177
|
-
output << " Route: Not mapped to a route"
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Show view templates if available
|
|
181
|
-
if data[:views] && data[:views][action]
|
|
182
|
-
view_data = data[:views][action]
|
|
183
|
-
|
|
184
|
-
output << " View Templates:"
|
|
185
|
-
view_data[:templates].each do |template|
|
|
186
|
-
output << " - #{template}"
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Show partials
|
|
190
|
-
if view_data[:partials]&.any?
|
|
191
|
-
output << " Partials Used:"
|
|
192
|
-
view_data[:partials].uniq.each do |partial|
|
|
193
|
-
output << " - #{partial}"
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Show Stimulus controllers
|
|
198
|
-
if view_data[:stimulus_controllers]&.any?
|
|
199
|
-
output << " Stimulus Controllers:"
|
|
200
|
-
view_data[:stimulus_controllers].each do |controller|
|
|
201
|
-
output << " - #{controller}"
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Show instance variables used in views
|
|
206
|
-
if view_data[:instance_variables]&.any?
|
|
207
|
-
output << " Instance Variables Used in View:"
|
|
208
|
-
view_data[:instance_variables].sort.each do |var|
|
|
209
|
-
output << " - @#{var}"
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
else
|
|
213
|
-
output << " View: No view template found"
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Show instance variables set in controller
|
|
217
|
-
if data[:instance_variables] && data[:instance_variables][action]
|
|
218
|
-
output << " Instance Variables Set in Controller:"
|
|
219
|
-
data[:instance_variables][action].sort.each do |var|
|
|
220
|
-
output << " - @#{var}"
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
output << ""
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
output << "-------------------------"
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
output.join("\n")
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
private
|
|
234
|
-
|
|
235
|
-
def camelize(string)
|
|
236
|
-
string.split("_").map(&:capitalize).join
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
end
|