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.
- checksums.yaml +4 -4
- data/README.md +106 -279
- data/changelog.md +69 -0
- data/claude_desktop_config.json +12 -0
- data/docs/DEBUGGING.md +40 -8
- data/docs/GENERATOR_TESTING.md +121 -0
- data/docs/README.md +130 -142
- data/exe/rails-active-mcp-server +176 -65
- data/lib/generators/rails_active_mcp/install/install_generator.rb +123 -3
- data/lib/generators/rails_active_mcp/install/templates/README.md +34 -128
- data/lib/generators/rails_active_mcp/install/templates/initializer.rb +37 -38
- data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -3
- data/lib/rails_active_mcp/configuration.rb +37 -98
- data/lib/rails_active_mcp/console_executor.rb +202 -78
- data/lib/rails_active_mcp/engine.rb +36 -8
- data/lib/rails_active_mcp/sdk/server.rb +183 -0
- data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +103 -0
- data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +73 -0
- data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +106 -0
- data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +77 -0
- data/lib/rails_active_mcp/version.rb +1 -1
- data/lib/rails_active_mcp.rb +10 -11
- data/rails_active_mcp.gemspec +8 -4
- metadata +43 -17
- data/lib/rails_active_mcp/mcp_server.rb +0 -374
- data/lib/rails_active_mcp/railtie.rb +0 -48
- data/lib/rails_active_mcp/stdio_server.rb +0 -467
- data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
- data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
- data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
- 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
|