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