rails-active-mcp 0.1.6 → 2.0.7

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -279
  3. data/changelog.md +69 -0
  4. data/claude_desktop_config.json +12 -0
  5. data/docs/DEBUGGING.md +40 -8
  6. data/docs/GENERATOR_TESTING.md +121 -0
  7. data/docs/README.md +130 -142
  8. data/exe/rails-active-mcp-server +176 -65
  9. data/lib/generators/rails_active_mcp/install/install_generator.rb +123 -3
  10. data/lib/generators/rails_active_mcp/install/templates/README.md +34 -128
  11. data/lib/generators/rails_active_mcp/install/templates/initializer.rb +37 -38
  12. data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -3
  13. data/lib/rails_active_mcp/configuration.rb +37 -98
  14. data/lib/rails_active_mcp/console_executor.rb +202 -78
  15. data/lib/rails_active_mcp/engine.rb +36 -8
  16. data/lib/rails_active_mcp/sdk/server.rb +183 -0
  17. data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +103 -0
  18. data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +73 -0
  19. data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +106 -0
  20. data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +77 -0
  21. data/lib/rails_active_mcp/version.rb +1 -1
  22. data/lib/rails_active_mcp.rb +10 -11
  23. data/rails_active_mcp.gemspec +8 -4
  24. metadata +43 -17
  25. data/lib/rails_active_mcp/mcp_server.rb +0 -374
  26. data/lib/rails_active_mcp/railtie.rb +0 -48
  27. data/lib/rails_active_mcp/stdio_server.rb +0 -467
  28. data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
  29. data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
  30. data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
  31. data/lib/rails_active_mcp/tools/safe_query_tool.rb +0 -41
@@ -1,467 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'logger'
5
-
6
- module RailsActiveMcp
7
- class StdioServer
8
- JSONRPC_VERSION = '2.0'
9
- MCP_VERSION = '2025-06-18'
10
-
11
- def initialize
12
- @tools = {}
13
- @logger = Logger.new(STDERR) # Log to stderr to avoid interfering with stdout
14
- @logger.level = ENV['RAILS_MCP_DEBUG'] ? Logger::DEBUG : Logger::ERROR
15
- @logger.formatter = proc do |severity, datetime, progname, msg|
16
- "[#{datetime}] [RAILS-MCP] #{severity}: #{msg}\n"
17
- end
18
- register_default_tools
19
- @logger.info "Rails Active MCP Server initialized with #{@tools.size} tools"
20
- end
21
-
22
- def run
23
- @logger.info 'Starting Rails Active MCP Stdio Server'
24
- send_log_notification('info', 'Rails Active MCP Server started successfully')
25
-
26
- STDIN.each_line do |line|
27
- line = line.strip
28
- next if line.empty?
29
-
30
- @logger.debug "Received request: #{line}" if ENV['RAILS_MCP_DEBUG']
31
- data = JSON.parse(line)
32
-
33
- @logger.debug "Processing method: #{data['method']}" if ENV['RAILS_MCP_DEBUG']
34
- response = handle_jsonrpc_request(data)
35
-
36
- if response
37
- @logger.debug "Sending response: #{response.to_json}" if ENV['RAILS_MCP_DEBUG']
38
- puts response.to_json
39
- STDOUT.flush
40
- end
41
- rescue JSON::ParserError => e
42
- @logger.error "JSON Parse Error: #{e.message}"
43
- send_log_notification('error', "JSON Parse Error: #{e.message}")
44
- error_response = jsonrpc_error(nil, -32_700, 'Parse error')
45
- puts error_response.to_json
46
- STDOUT.flush
47
- rescue StandardError => e
48
- @logger.error "Unexpected error: #{e.message}"
49
- @logger.error e.backtrace.join("\n")
50
- send_log_notification('error', "Server error: #{e.message}")
51
- error_response = jsonrpc_error(nil, -32_603, 'Internal error')
52
- puts error_response.to_json
53
- STDOUT.flush
54
- end
55
- end
56
-
57
- private
58
-
59
- def handle_jsonrpc_request(data)
60
- case data['method']
61
- when 'initialize'
62
- handle_initialize(data)
63
- when 'tools/list'
64
- handle_tools_list(data)
65
- when 'tools/call'
66
- handle_tools_call(data)
67
- when 'resources/list'
68
- handle_resources_list(data)
69
- when 'resources/read'
70
- handle_resources_read(data)
71
- when 'ping'
72
- handle_ping(data)
73
- else
74
- jsonrpc_error(data['id'], -32_601, 'Method not found')
75
- end
76
- end
77
-
78
- def handle_initialize(data)
79
- {
80
- jsonrpc: JSONRPC_VERSION,
81
- id: data['id'],
82
- result: {
83
- protocolVersion: MCP_VERSION,
84
- capabilities: {
85
- tools: {},
86
- resources: {}
87
- },
88
- serverInfo: {
89
- name: 'rails-active-mcp',
90
- version: RailsActiveMcp::VERSION
91
- }
92
- }
93
- }
94
- end
95
-
96
- def handle_tools_list(data)
97
- tools_array = @tools.values.map do |tool|
98
- tool_def = {
99
- name: tool[:name],
100
- description: tool[:description],
101
- inputSchema: tool[:input_schema]
102
- }
103
-
104
- # Add annotations if present
105
- tool_def[:annotations] = tool[:annotations] if tool[:annotations] && !tool[:annotations].empty?
106
-
107
- tool_def
108
- end
109
-
110
- {
111
- jsonrpc: JSONRPC_VERSION,
112
- id: data['id'],
113
- result: { tools: tools_array }
114
- }
115
- end
116
-
117
- def handle_tools_call(data)
118
- tool_name = data.dig('params', 'name')
119
- arguments = data.dig('params', 'arguments') || {}
120
-
121
- tool = @tools[tool_name]
122
- return jsonrpc_error(data['id'], -32_602, "Tool '#{tool_name}' not found") unless tool
123
-
124
- @logger.info "Executing tool: #{tool_name}"
125
- send_log_notification('info', "Executing tool: #{tool_name}")
126
-
127
- begin
128
- start_time = Time.now
129
- result = tool[:handler].call(arguments)
130
- execution_time = Time.now - start_time
131
-
132
- @logger.info "Tool #{tool_name} completed in #{execution_time}s"
133
- send_log_notification('info', "Tool #{tool_name} completed successfully")
134
-
135
- {
136
- jsonrpc: JSONRPC_VERSION,
137
- id: data['id'],
138
- result: {
139
- content: [{ type: 'text', text: result.to_s }],
140
- isError: false
141
- }
142
- }
143
- rescue StandardError => e
144
- @logger.error "Tool execution error: #{e.message}"
145
- @logger.error e.backtrace.first(5).join("\n") if ENV['RAILS_MCP_DEBUG']
146
- send_log_notification('error', "Tool #{tool_name} failed: #{e.message}")
147
-
148
- {
149
- jsonrpc: JSONRPC_VERSION,
150
- id: data['id'],
151
- result: {
152
- content: [{ type: 'text', text: "Error: #{e.message}" }],
153
- isError: true
154
- }
155
- }
156
- end
157
- end
158
-
159
- def handle_resources_list(data)
160
- {
161
- jsonrpc: JSONRPC_VERSION,
162
- id: data['id'],
163
- result: { resources: [] }
164
- }
165
- end
166
-
167
- def handle_resources_read(data)
168
- {
169
- jsonrpc: JSONRPC_VERSION,
170
- id: data['id'],
171
- result: { contents: [] }
172
- }
173
- end
174
-
175
- def handle_ping(data)
176
- {
177
- jsonrpc: JSONRPC_VERSION,
178
- id: data['id'],
179
- result: {}
180
- }
181
- end
182
-
183
- def register_tool(name, description, input_schema, annotations = {}, &handler)
184
- @tools[name] = {
185
- name: name,
186
- description: description,
187
- input_schema: input_schema,
188
- annotations: annotations,
189
- handler: handler
190
- }
191
- end
192
-
193
- def register_default_tools
194
- register_tool(
195
- 'rails_console_execute',
196
- 'Execute Ruby code in Rails console context',
197
- {
198
- type: 'object',
199
- properties: {
200
- code: { type: 'string', description: 'Ruby code to execute' },
201
- timeout: { type: 'number', description: 'Timeout in seconds', default: 30 },
202
- safe_mode: { type: 'boolean', description: 'Enable safety checks', default: true },
203
- capture_output: { type: 'boolean', description: 'Capture console output', default: true }
204
- },
205
- required: ['code']
206
- },
207
- {
208
- title: 'Rails Console Executor',
209
- readOnlyHint: false,
210
- destructiveHint: true,
211
- idempotentHint: false,
212
- openWorldHint: false
213
- }
214
- ) do |args|
215
- execute_console_code(args)
216
- end
217
-
218
- register_tool(
219
- 'rails_model_info',
220
- 'Get information about Rails models including columns, associations, and table structure',
221
- {
222
- type: 'object',
223
- properties: {
224
- model_name: { type: 'string', description: 'Name of the Rails model class to inspect' }
225
- },
226
- required: ['model_name']
227
- },
228
- {
229
- title: 'Rails Model Inspector',
230
- readOnlyHint: true,
231
- destructiveHint: false,
232
- idempotentHint: true,
233
- openWorldHint: false
234
- }
235
- ) do |args|
236
- get_model_info(args['model_name'])
237
- end
238
-
239
- register_tool(
240
- 'rails_safe_query',
241
- 'Execute safe read-only database queries using ActiveRecord',
242
- {
243
- type: 'object',
244
- properties: {
245
- query: { type: 'string', description: 'ActiveRecord query to execute (read-only methods only)' },
246
- model: { type: 'string', description: 'Model class name to query against' }
247
- },
248
- required: %w[query model]
249
- },
250
- {
251
- title: 'Rails Safe Query Executor',
252
- readOnlyHint: true,
253
- destructiveHint: false,
254
- idempotentHint: true,
255
- openWorldHint: false
256
- }
257
- ) do |args|
258
- execute_safe_query(args)
259
- end
260
-
261
- register_tool(
262
- 'rails_dry_run',
263
- 'Analyze Ruby code for safety without executing it',
264
- {
265
- type: 'object',
266
- properties: {
267
- code: { type: 'string', description: 'Ruby code to analyze for safety and potential issues' }
268
- },
269
- required: ['code']
270
- },
271
- {
272
- title: 'Rails Code Safety Analyzer',
273
- readOnlyHint: true,
274
- destructiveHint: false,
275
- idempotentHint: true,
276
- openWorldHint: false
277
- }
278
- ) do |args|
279
- dry_run_analysis(args['code'])
280
- end
281
- end
282
-
283
- # Tool implementation methods (reused from McpServer)
284
- def execute_console_code(args)
285
- unless defined?(RailsActiveMcp) && RailsActiveMcp.respond_to?(:config) && RailsActiveMcp.config.enabled
286
- return 'Rails Active MCP is disabled. Enable it in your Rails configuration.'
287
- end
288
-
289
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
290
-
291
- begin
292
- result = executor.execute(
293
- args['code'],
294
- timeout: args['timeout'] || 30,
295
- safe_mode: args['safe_mode'] != false,
296
- capture_output: args['capture_output'] != false
297
- )
298
-
299
- if result[:success]
300
- format_success_result(result)
301
- else
302
- "Error: #{result[:error]} (#{result[:error_class]})"
303
- end
304
- rescue RailsActiveMcp::SafetyError => e
305
- "Safety check failed: #{e.message}"
306
- rescue RailsActiveMcp::TimeoutError => e
307
- "Execution timed out: #{e.message}"
308
- rescue StandardError => e
309
- "Execution failed: #{e.message}"
310
- end
311
- end
312
-
313
- def get_model_info(model_name)
314
- unless defined?(RailsActiveMcp) && RailsActiveMcp.respond_to?(:config) && RailsActiveMcp.config.enabled
315
- return 'Rails Active MCP is disabled. Enable it in your Rails configuration.'
316
- end
317
-
318
- begin
319
- # Try to load Rails environment if not already loaded
320
- require_relative '../../../config/environment' if !defined?(Rails) && File.exist?('config/environment.rb')
321
-
322
- model_class = model_name.constantize
323
- unless defined?(ActiveRecord) && model_class < ActiveRecord::Base
324
- return "#{model_name} is not an ActiveRecord model"
325
- end
326
-
327
- info = []
328
- info << "Model: #{model_class.name}"
329
- info << "Table: #{model_class.table_name}"
330
- info << "Columns: #{model_class.column_names.join(', ')}"
331
-
332
- associations = model_class.reflect_on_all_associations.map(&:name)
333
- info << "Associations: #{associations.any? ? associations.join(', ') : 'None'}"
334
-
335
- # Add validation info if available
336
- if model_class.respond_to?(:validators) && model_class.validators.any?
337
- validations = model_class.validators.map { |v| "#{v.attributes.join(', ')}: #{v.class.name.demodulize}" }.uniq
338
- info << "Validations: #{validations.join(', ')}"
339
- end
340
-
341
- info.join("\n")
342
- rescue NameError
343
- "Model '#{model_name}' not found. Make sure the model class exists and is properly defined."
344
- rescue StandardError => e
345
- "Error getting model info: #{e.message}"
346
- end
347
- end
348
-
349
- def execute_safe_query(args)
350
- unless defined?(RailsActiveMcp) && RailsActiveMcp.respond_to?(:config) && RailsActiveMcp.config.enabled
351
- return 'Rails Active MCP is disabled. Enable it in your Rails configuration.'
352
- end
353
-
354
- begin
355
- # Try to load Rails environment if not already loaded
356
- require_relative '../../../config/environment' if !defined?(Rails) && File.exist?('config/environment.rb')
357
-
358
- model_class = args['model'].constantize
359
- unless defined?(ActiveRecord) && model_class < ActiveRecord::Base
360
- return "#{args['model']} is not an ActiveRecord model"
361
- end
362
-
363
- # Only allow safe read-only methods
364
- safe_methods = %w[find find_by where select count sum average maximum minimum first last pluck ids exists?
365
- empty? any? many? include? limit offset order group having joins includes references distinct uniq readonly]
366
-
367
- # Extract the first method call to validate it's safe
368
- query_parts = args['query'].split('.')
369
- query_method = query_parts.first.split('(').first
370
-
371
- unless safe_methods.include?(query_method)
372
- return "Unsafe query method: #{query_method}. Only read-only methods are allowed."
373
- end
374
-
375
- result = model_class.instance_eval(args['query'])
376
-
377
- # Format result appropriately
378
- case result
379
- when ActiveRecord::Relation
380
- "Query returned #{result.count} records: #{result.limit(10).pluck(:id).join(', ')}#{result.count > 10 ? '...' : ''}"
381
- when Array
382
- "Array with #{result.length} items: #{result.take(5).inspect}#{result.length > 5 ? '...' : ''}"
383
- else
384
- result.to_s
385
- end
386
- rescue NameError
387
- "Model '#{args['model']}' not found. Make sure the model class exists and is properly defined."
388
- rescue StandardError => e
389
- "Error executing query: #{e.message}"
390
- end
391
- end
392
-
393
- def dry_run_analysis(code)
394
- unless defined?(RailsActiveMcp) && RailsActiveMcp.respond_to?(:config) && RailsActiveMcp.config.enabled
395
- return 'Rails Active MCP is disabled. Enable it in your Rails configuration.'
396
- end
397
-
398
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
399
-
400
- begin
401
- analysis = executor.dry_run(code)
402
-
403
- output = []
404
- output << 'Code Analysis Results:'
405
- output << "Code: #{analysis[:code]}"
406
- output << "Safe: #{analysis[:safety_analysis][:safe] ? 'Yes' : 'No'}"
407
- output << "Read-only: #{analysis[:safety_analysis][:read_only] ? 'Yes' : 'No'}"
408
- output << "Risk level: #{analysis[:estimated_risk]}"
409
- output << "Would execute: #{analysis[:would_execute] ? 'Yes' : 'No'}"
410
- output << "Summary: #{analysis[:safety_analysis][:summary]}"
411
-
412
- if analysis[:safety_analysis][:violations] && analysis[:safety_analysis][:violations].any?
413
- output << "\nSafety Violations:"
414
- analysis[:safety_analysis][:violations].each do |violation|
415
- output << " - #{violation[:description]} (#{violation[:severity]})"
416
- end
417
- end
418
-
419
- if analysis[:recommendations] && analysis[:recommendations].any?
420
- output << "\nRecommendations:"
421
- analysis[:recommendations].each do |rec|
422
- output << " - #{rec}"
423
- end
424
- end
425
-
426
- output.join("\n")
427
- rescue StandardError => e
428
- "Analysis failed: #{e.message}. Make sure the Rails environment is properly loaded."
429
- end
430
- end
431
-
432
- def format_success_result(result)
433
- output = []
434
- output << 'Execution Results:'
435
- output << "Code: #{result[:code]}"
436
- output << "Result: #{result[:return_value_string] || result[:return_value]}"
437
- output << "Output: #{result[:output]}" if result[:output] && !result[:output].empty?
438
- output << "Execution time: #{result[:execution_time]}s" if result[:execution_time]
439
- output << "Note: #{result[:note]}" if result[:note]
440
- output.join("\n")
441
- end
442
-
443
- def send_log_notification(level, message)
444
- notification = {
445
- jsonrpc: JSONRPC_VERSION,
446
- method: 'notifications/message',
447
- params: {
448
- level: level,
449
- data: message
450
- }
451
- }
452
-
453
- puts notification.to_json
454
- STDOUT.flush
455
- rescue StandardError => e
456
- @logger.error "Failed to send log notification: #{e.message}"
457
- end
458
-
459
- def jsonrpc_error(id, code, message)
460
- {
461
- jsonrpc: JSONRPC_VERSION,
462
- id: id,
463
- error: { code: code, message: message }
464
- }
465
- end
466
- end
467
- end
@@ -1,61 +0,0 @@
1
- module RailsActiveMcp
2
- module Tools
3
- class ConsoleExecuteTool < ApplicationMCPTool
4
- tool_name "console_execute"
5
- description "Execute Ruby code in Rails console with safety checks"
6
-
7
- property :code, type: "string", description: 'Ruby code to execute in Rails console', required: true
8
- property :safe_mode, type: "boolean", description: 'Enable safety checks (default: true)', required: false
9
- property :timeout, type: "integer", description: 'Timeout in seconds (default: 30)', required: false
10
- property :capture_output, type: "boolean", description: 'Capture console output (default: true)', required: false
11
-
12
- def perform
13
- code = properties[:code]
14
- safe_mode = properties[:safe_mode]
15
- timeout = properties[:timeout]
16
- capture_output = properties.fetch(:capture_output, true)
17
-
18
- return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
19
-
20
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
21
-
22
- begin
23
- result = executor.execute(
24
- code,
25
- timeout: timeout,
26
- safe_mode: safe_mode,
27
- capture_output: capture_output
28
- )
29
-
30
- if result[:success]
31
- render(text: format_success_result(result))
32
- else
33
- render(error: [format_error_result(result)])
34
- end
35
- rescue RailsActiveMcp::SafetyError => e
36
- render(error: ["Safety check failed: #{e.message}"])
37
- rescue RailsActiveMcp::TimeoutError => e
38
- render(error: ["Execution timed out: #{e.message}"])
39
- rescue => e
40
- render(error: ["Execution failed: #{e.message}"])
41
- end
42
- end
43
-
44
- private
45
-
46
- def format_success_result(result)
47
- output = []
48
- output << "Code: #{result[:code]}"
49
- output << "Result: #{result[:return_value_string] || result[:return_value]}"
50
- output << "Output: #{result[:output]}" if result[:output].present?
51
- output << "Execution time: #{result[:execution_time]}s" if result[:execution_time]
52
- output << "Note: #{result[:note]}" if result[:note]
53
- output.join("\n")
54
- end
55
-
56
- def format_error_result(result)
57
- "Error: #{result[:error]} (#{result[:error_class]})"
58
- end
59
- end
60
- end
61
- end
@@ -1,41 +0,0 @@
1
- module RailsActiveMcp
2
- module Tools
3
- class DryRunTool < ApplicationMCPTool
4
- tool_name "dry_run"
5
- description "Analyze Ruby code for safety without executing it"
6
-
7
- property :code, type: "string", description: 'Ruby code to analyze for safety', required: true
8
-
9
- def perform
10
- return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
11
-
12
- code = properties[:code]
13
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
14
- analysis = executor.dry_run(code)
15
-
16
- output = []
17
- output << "Code: #{analysis[:code]}"
18
- output << "Safe: #{analysis[:safety_analysis][:safe] ? 'Yes' : 'No'}"
19
- output << "Read-only: #{analysis[:safety_analysis][:read_only] ? 'Yes' : 'No'}"
20
- output << "Risk level: #{analysis[:estimated_risk]}"
21
- output << "Summary: #{analysis[:safety_analysis][:summary]}"
22
-
23
- if analysis[:safety_analysis][:violations].any?
24
- output << "\nViolations:"
25
- analysis[:safety_analysis][:violations].each do |violation|
26
- output << " - #{violation[:description]} (#{violation[:severity]})"
27
- end
28
- end
29
-
30
- if analysis[:recommendations].any?
31
- output << "\nRecommendations:"
32
- analysis[:recommendations].each do |rec|
33
- output << " - #{rec}"
34
- end
35
- end
36
-
37
- render(text: output.join("\n"))
38
- end
39
- end
40
- end
41
- end
@@ -1,70 +0,0 @@
1
- module RailsActiveMcp
2
- module Tools
3
- class ModelInfoTool < ApplicationMCPTool
4
- tool_name "model_info"
5
- description "Get information about Rails models including schema and associations"
6
-
7
- property :model, type: "string", description: 'Model class name', required: true
8
- property :include_schema, type: "boolean", description: 'Include database schema information', required: false
9
- property :include_associations, type: "boolean", description: 'Include model associations', required: false
10
- property :include_validations, type: "boolean", description: 'Include model validations', required: false
11
-
12
- def perform
13
- return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
14
-
15
- model = properties[:model]
16
- include_schema = properties.fetch(:include_schema, true)
17
- include_associations = properties.fetch(:include_associations, true)
18
- include_validations = properties.fetch(:include_validations, true)
19
-
20
- begin
21
- model_class = model.constantize
22
-
23
- output = []
24
- output << "Model: #{model}"
25
- output << "Table: #{model_class.table_name}"
26
- output << "Primary Key: #{model_class.primary_key}"
27
-
28
- if include_schema
29
- output << "\nSchema:"
30
- model_class.columns.each do |column|
31
- output << " #{column.name}: #{column.type} (#{column.sql_type})"
32
- output << " - Null: #{column.null}"
33
- output << " - Default: #{column.default}" if column.default
34
- end
35
- end
36
-
37
- if include_associations
38
- output << "\nAssociations:"
39
- model_class.reflections.each do |name, reflection|
40
- output << " #{name}: #{reflection.class.name.split('::').last} -> #{reflection.class_name}"
41
- end
42
- end
43
-
44
- if include_validations
45
- validations = {}
46
- model_class.validators.each do |validator|
47
- validator.attributes.each do |attribute|
48
- validations[attribute] ||= []
49
- validations[attribute] << validator.class.name.split('::').last
50
- end
51
- end
52
-
53
- if validations.any?
54
- output << "\nValidations:"
55
- validations.each do |attr, validators|
56
- output << " #{attr}: #{validators.join(', ')}"
57
- end
58
- end
59
- end
60
-
61
- render(text: output.join("\n"))
62
- rescue NameError
63
- render(error: ["Model '#{model}' not found"])
64
- rescue => e
65
- render(error: ["Error analyzing model: #{e.message}"])
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,41 +0,0 @@
1
- module RailsActiveMcp
2
- module Tools
3
- class SafeQueryTool < ApplicationMCPTool
4
- tool_name "safe_query"
5
- description "Execute safe read-only database queries on Rails models"
6
-
7
- property :model, type: "string", description: 'Model class name (e.g., "User", "Product")', required: true
8
- property :method, type: "string", description: 'Query method (find, where, count, etc.)', required: true
9
- property :args, type: "array", description: 'Arguments for the query method', required: false
10
- property :limit, type: "integer", description: 'Limit results (default: 100)', required: false
11
-
12
- def perform
13
- return render(error: "Rails Active MCP is disabled") unless RailsActiveMcp.config.enabled
14
-
15
- model = properties[:model]
16
- method = properties[:method]
17
- args = properties[:args] || []
18
- limit = properties[:limit]
19
-
20
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
21
-
22
- result = executor.execute_safe_query(
23
- model: model,
24
- method: method,
25
- args: args,
26
- limit: limit
27
- )
28
-
29
- if result[:success]
30
- output = []
31
- output << "Query: #{model}.#{method}(#{args.join(', ')})"
32
- output << "Count: #{result[:count]}"
33
- output << "Result: #{result[:result].inspect}"
34
- render(text: output.join("\n"))
35
- else
36
- render(error: [result[:error]])
37
- end
38
- end
39
- end
40
- end
41
- end