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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -279
  3. data/changelog.md +69 -0
  4. data/docs/DEBUGGING.md +5 -5
  5. data/docs/README.md +130 -142
  6. data/exe/rails-active-mcp-server +153 -76
  7. data/lib/generators/rails_active_mcp/install/install_generator.rb +19 -39
  8. data/lib/generators/rails_active_mcp/install/templates/README.md +30 -164
  9. data/lib/generators/rails_active_mcp/install/templates/initializer.rb +37 -38
  10. data/lib/generators/rails_active_mcp/install/templates/mcp.ru +7 -3
  11. data/lib/rails_active_mcp/configuration.rb +37 -98
  12. data/lib/rails_active_mcp/console_executor.rb +13 -3
  13. data/lib/rails_active_mcp/engine.rb +36 -24
  14. data/lib/rails_active_mcp/sdk/server.rb +183 -0
  15. data/lib/rails_active_mcp/sdk/tools/console_execute_tool.rb +103 -0
  16. data/lib/rails_active_mcp/sdk/tools/dry_run_tool.rb +73 -0
  17. data/lib/rails_active_mcp/sdk/tools/model_info_tool.rb +106 -0
  18. data/lib/rails_active_mcp/sdk/tools/safe_query_tool.rb +77 -0
  19. data/lib/rails_active_mcp/version.rb +1 -1
  20. data/lib/rails_active_mcp.rb +5 -11
  21. data/rails_active_mcp.gemspec +4 -1
  22. metadata +22 -11
  23. data/app/controllers/rails_active_mcp/mcp_controller.rb +0 -80
  24. data/lib/rails_active_mcp/mcp_server.rb +0 -383
  25. data/lib/rails_active_mcp/railtie.rb +0 -70
  26. data/lib/rails_active_mcp/stdio_server.rb +0 -517
  27. data/lib/rails_active_mcp/tools/console_execute_tool.rb +0 -61
  28. data/lib/rails_active_mcp/tools/dry_run_tool.rb +0 -41
  29. data/lib/rails_active_mcp/tools/model_info_tool.rb +0 -70
  30. data/lib/rails_active_mcp/tools/safe_query_tool.rb +0 -41
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-active-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 2.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandyn Britton
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '6.1'
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
- version: '8.0'
35
+ version: '9.0'
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,21 @@ dependencies:
42
42
  version: '6.1'
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
- version: '8.0'
45
+ version: '9.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mcp
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: 0.1.0
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 0.1.0
46
60
  - !ruby/object:Gem::Dependency
47
61
  name: json
48
62
  requirement: !ruby/object:Gem::Requirement
@@ -218,7 +232,6 @@ files:
218
232
  - ".idea/rails-active-mcp-gem.iml"
219
233
  - ".idea/vcs.xml"
220
234
  - README.md
221
- - app/controllers/rails_active_mcp/mcp_controller.rb
222
235
  - changelog.md
223
236
  - claude_desktop_config.json
224
237
  - docs/DEBUGGING.md
@@ -233,15 +246,13 @@ files:
233
246
  - lib/rails_active_mcp/configuration.rb
234
247
  - lib/rails_active_mcp/console_executor.rb
235
248
  - lib/rails_active_mcp/engine.rb
236
- - lib/rails_active_mcp/mcp_server.rb
237
- - lib/rails_active_mcp/railtie.rb
238
249
  - lib/rails_active_mcp/safety_checker.rb
239
- - lib/rails_active_mcp/stdio_server.rb
250
+ - lib/rails_active_mcp/sdk/server.rb
251
+ - lib/rails_active_mcp/sdk/tools/console_execute_tool.rb
252
+ - lib/rails_active_mcp/sdk/tools/dry_run_tool.rb
253
+ - lib/rails_active_mcp/sdk/tools/model_info_tool.rb
254
+ - lib/rails_active_mcp/sdk/tools/safe_query_tool.rb
240
255
  - lib/rails_active_mcp/tasks.rake
241
- - lib/rails_active_mcp/tools/console_execute_tool.rb
242
- - lib/rails_active_mcp/tools/dry_run_tool.rb
243
- - lib/rails_active_mcp/tools/model_info_tool.rb
244
- - lib/rails_active_mcp/tools/safe_query_tool.rb
245
256
  - lib/rails_active_mcp/version.rb
246
257
  - mcp.ru
247
258
  - rails_active_mcp.gemspec
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsActiveMcp
4
- class McpController < ApplicationController
5
- protect_from_forgery with: :null_session
6
- before_action :check_enabled
7
- before_action :set_cors_headers
8
-
9
- def handle
10
- return head :method_not_allowed unless request.post?
11
- return head :bad_request unless json_request?
12
-
13
- begin
14
- body = request.body.read
15
- data = JSON.parse(body)
16
-
17
- mcp_server = RailsActiveMcp::McpServer.new
18
- response_data = mcp_server.handle_jsonrpc_request(data)
19
-
20
- render json: response_data
21
- rescue JSON::ParserError
22
- render json: { error: 'Invalid JSON' }, status: :bad_request
23
- rescue StandardError => e
24
- RailsActiveMcp.logger.error "MCP Controller Error: #{e.message}"
25
- render json: { error: 'Internal Server Error' }, status: :internal_server_error
26
- end
27
- end
28
-
29
- def sse
30
- response.headers['Content-Type'] = 'text/event-stream'
31
- response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
32
- response.headers['Connection'] = 'keep-alive'
33
- response.headers['X-Accel-Buffering'] = 'no'
34
-
35
- # Send initial connection established event
36
- render plain: ": SSE connection established\n\nevent: endpoint\ndata: #{request.base_url}#{rails_active_mcp.root_path}messages\n\nretry: 100\n\n"
37
- end
38
-
39
- def health
40
- status = RailsActiveMcp.config.enabled ? 'healthy' : 'disabled'
41
- render json: {
42
- status: status,
43
- version: RailsActiveMcp::VERSION,
44
- timestamp: Time.current.iso8601
45
- }
46
- end
47
-
48
- def info
49
- render json: {
50
- name: 'Rails Active MCP',
51
- version: RailsActiveMcp::VERSION,
52
- description: 'Rails Console access via Model Context Protocol (MCP)',
53
- endpoints: {
54
- mcp: rails_active_mcp.root_path,
55
- health: rails_active_mcp.root_path + 'health'
56
- },
57
- enabled: RailsActiveMcp.config.enabled
58
- }
59
- end
60
-
61
- private
62
-
63
- def check_enabled
64
- return if RailsActiveMcp.config.enabled
65
-
66
- render json: { error: 'Rails Active MCP is disabled' }, status: :service_unavailable
67
- end
68
-
69
- def json_request?
70
- request.content_type&.include?('application/json')
71
- end
72
-
73
- def set_cors_headers
74
- response.headers['Access-Control-Allow-Origin'] = '*'
75
- response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
76
- response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
77
- response.headers['Access-Control-Max-Age'] = '86400'
78
- end
79
- end
80
- end
@@ -1,383 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'rack'
5
-
6
- module RailsActiveMcp
7
- class McpServer
8
- JSONRPC_VERSION = '2.0'
9
- MCP_VERSION = '2025-06-18'
10
-
11
- def initialize(app = nil)
12
- @app = app
13
- @tools = {}
14
- @resources = {}
15
- register_default_tools
16
- end
17
-
18
- def call(env)
19
- request = Rack::Request.new(env)
20
-
21
- return [405, {}, ['Method Not Allowed']] unless request.post?
22
- return [400, {}, ['Invalid Content-Type']] unless json_request?(request)
23
-
24
- begin
25
- body = request.body.read
26
- data = JSON.parse(body)
27
- response = handle_jsonrpc_request(data)
28
-
29
- [200, { 'Content-Type' => 'application/json' }, [response.to_json]]
30
- rescue JSON::ParserError
31
- error_response(400, 'Invalid JSON')
32
- rescue StandardError => e
33
- RailsActiveMcp.logger.error "MCP Server Error: #{e.message}"
34
- error_response(500, 'Internal Server Error')
35
- end
36
- end
37
-
38
- def handle_jsonrpc_request(data)
39
- case data['method']
40
- when 'initialize'
41
- handle_initialize(data)
42
- when 'tools/list'
43
- handle_tools_list(data)
44
- when 'tools/call'
45
- handle_tools_call(data)
46
- when 'resources/list'
47
- handle_resources_list(data)
48
- when 'resources/read'
49
- handle_resources_read(data)
50
- when 'ping'
51
- handle_ping(data)
52
- else
53
- jsonrpc_error(data['id'], -32_601, 'Method not found')
54
- end
55
- end
56
-
57
- def handle_ping(data)
58
- {
59
- jsonrpc: JSONRPC_VERSION,
60
- id: data['id'],
61
- result: {}
62
- }
63
- end
64
-
65
- def handle_initialize(data)
66
- {
67
- jsonrpc: JSONRPC_VERSION,
68
- id: data['id'],
69
- result: {
70
- protocolVersion: MCP_VERSION,
71
- capabilities: {
72
- tools: {
73
- list: true,
74
- call: true
75
- },
76
- resources: {
77
- read: true,
78
- list: true
79
- }
80
- },
81
- serverInfo: {
82
- name: 'rails-active-mcp',
83
- version: RailsActiveMcp::VERSION
84
- }
85
- }
86
- }
87
- end
88
-
89
- def handle_tools_list(data)
90
- tools_array = @tools.values.map do |tool|
91
- tool_def = {
92
- name: tool[:name],
93
- description: tool[:description],
94
- inputSchema: tool[:input_schema]
95
- }
96
-
97
- # Add annotations if present
98
- tool_def[:annotations] = tool[:annotations] if tool[:annotations] && !tool[:annotations].empty?
99
-
100
- tool_def
101
- end
102
-
103
- {
104
- jsonrpc: JSONRPC_VERSION,
105
- id: data['id'],
106
- result: { tools: tools_array }
107
- }
108
- end
109
-
110
- def handle_tools_call(data)
111
- tool_name = data.dig('params', 'name')
112
- arguments = data.dig('params', 'arguments') || {}
113
-
114
- tool = @tools[tool_name]
115
- return jsonrpc_error(data['id'], -32_602, "Tool '#{tool_name}' not found") unless tool
116
-
117
- begin
118
- result = tool[:handler].call(arguments)
119
- {
120
- jsonrpc: JSONRPC_VERSION,
121
- id: data['id'],
122
- result: { content: [{ type: 'text', text: result.to_s }] }
123
- }
124
- rescue StandardError => e
125
- jsonrpc_error(data['id'], -32_603, "Tool execution failed: #{e.message}")
126
- end
127
- end
128
-
129
- def handle_resources_list(data)
130
- {
131
- jsonrpc: JSONRPC_VERSION,
132
- id: data['id'],
133
- result: { resources: [] }
134
- }
135
- end
136
-
137
- def handle_resources_read(data)
138
- {
139
- jsonrpc: JSONRPC_VERSION,
140
- id: data['id'],
141
- result: { contents: [] }
142
- }
143
- end
144
-
145
- def register_tool(name, description, input_schema, annotations = {}, &handler)
146
- @tools[name] = {
147
- name: name,
148
- description: description,
149
- input_schema: input_schema,
150
- annotations: annotations,
151
- handler: handler
152
- }
153
- end
154
-
155
- private
156
-
157
- def json_request?(request)
158
- request.content_type&.include?('application/json')
159
- end
160
-
161
- def register_default_tools
162
- register_tool(
163
- 'rails_console_execute',
164
- 'Execute Ruby code in Rails console context',
165
- {
166
- type: 'object',
167
- properties: {
168
- code: { type: 'string', description: 'Ruby code to execute' },
169
- timeout: { type: 'number', description: 'Timeout in seconds', default: 30 },
170
- safe_mode: { type: 'boolean', description: 'Enable safety checks', default: true },
171
- capture_output: { type: 'boolean', description: 'Capture console output', default: true }
172
- },
173
- required: ['code']
174
- },
175
- # Add MCP tool annotations
176
- {
177
- title: 'Rails Console Executor',
178
- readOnlyHint: false,
179
- destructiveHint: true,
180
- idempotentHint: false,
181
- openWorldHint: false
182
- }
183
- ) do |args|
184
- execute_console_code(args)
185
- end
186
-
187
- register_tool(
188
- 'rails_model_info',
189
- 'Get information about Rails models',
190
- {
191
- type: 'object',
192
- properties: {
193
- model_name: { type: 'string', description: 'Name of the model to inspect' }
194
- },
195
- required: ['model_name']
196
- },
197
- # Safe read-only tool
198
- {
199
- title: 'Rails Model Inspector',
200
- readOnlyHint: true,
201
- destructiveHint: false,
202
- idempotentHint: true,
203
- openWorldHint: false
204
- }
205
- ) do |args|
206
- get_model_info(args['model_name'])
207
- end
208
-
209
- register_tool(
210
- 'rails_safe_query',
211
- 'Execute safe read-only database queries',
212
- {
213
- type: 'object',
214
- properties: {
215
- query: { type: 'string', description: 'Safe query to execute' },
216
- model: { type: 'string', description: 'Model class name' }
217
- },
218
- required: %w[query model]
219
- },
220
- # Safe read-only query tool
221
- {
222
- title: 'Rails Safe Query Executor',
223
- readOnlyHint: true,
224
- destructiveHint: false,
225
- idempotentHint: true,
226
- openWorldHint: false
227
- }
228
- ) do |args|
229
- execute_safe_query(args)
230
- end
231
-
232
- register_tool(
233
- 'rails_dry_run',
234
- 'Analyze Ruby code safety without execution',
235
- {
236
- type: 'object',
237
- properties: {
238
- code: { type: 'string', description: 'Ruby code to analyze' }
239
- },
240
- required: ['code']
241
- },
242
- {
243
- title: 'Rails Code Safety Analyzer',
244
- readOnlyHint: true,
245
- destructiveHint: false,
246
- idempotentHint: true,
247
- openWorldHint: false
248
- }
249
- ) do |args|
250
- dry_run_analysis(args['code'])
251
- end
252
- end
253
-
254
- def execute_console_code(args)
255
- return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
256
-
257
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
258
-
259
- begin
260
- result = executor.execute(
261
- args['code'],
262
- timeout: args['timeout'] || 30,
263
- safe_mode: args['safe_mode'] != false,
264
- capture_output: args['capture_output'] != false
265
- )
266
-
267
- if result[:success]
268
- format_success_result(result)
269
- else
270
- "Error: #{result[:error]} (#{result[:error_class]})"
271
- end
272
- rescue RailsActiveMcp::SafetyError => e
273
- "Safety check failed: #{e.message}"
274
- rescue RailsActiveMcp::TimeoutError => e
275
- "Execution timed out: #{e.message}"
276
- rescue StandardError => e
277
- "Execution failed: #{e.message}"
278
- end
279
- end
280
-
281
- def get_model_info(model_name)
282
- return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
283
-
284
- begin
285
- model_class = model_name.constantize
286
- return "#{model_name} is not an ActiveRecord model" unless model_class < ActiveRecord::Base
287
-
288
- info = []
289
- info << "Model: #{model_class.name}"
290
- info << "Table: #{model_class.table_name}"
291
- info << "Columns: #{model_class.column_names.join(', ')}"
292
- info << "Associations: #{model_class.reflect_on_all_associations.map(&:name).join(', ')}"
293
- info.join("\n")
294
- rescue NameError
295
- "Model '#{model_name}' not found"
296
- rescue StandardError => e
297
- "Error getting model info: #{e.message}"
298
- end
299
- end
300
-
301
- def execute_safe_query(args)
302
- return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
303
-
304
- begin
305
- model_class = args['model'].constantize
306
- return "#{args['model']} is not an ActiveRecord model" unless model_class < ActiveRecord::Base
307
-
308
- # Only allow safe read-only methods
309
- safe_methods = %w[find find_by where select count sum average maximum minimum first last pluck ids exists?
310
- empty? any? many? include?]
311
- query_method = args['query'].split('.').first
312
-
313
- return "Unsafe query method: #{query_method}" unless safe_methods.include?(query_method)
314
-
315
- result = model_class.instance_eval(args['query'])
316
- result.to_s
317
- rescue NameError
318
- "Model '#{args['model']}' not found"
319
- rescue StandardError => e
320
- "Error executing query: #{e.message}"
321
- end
322
- end
323
-
324
- def dry_run_analysis(code)
325
- return 'Rails Active MCP is disabled' unless RailsActiveMcp.config.enabled
326
-
327
- executor = RailsActiveMcp::ConsoleExecutor.new(RailsActiveMcp.config)
328
-
329
- begin
330
- analysis = executor.dry_run(code)
331
-
332
- output = []
333
- output << "Code: #{analysis[:code]}"
334
- output << "Safe: #{analysis[:safety_analysis][:safe] ? 'Yes' : 'No'}"
335
- output << "Read-only: #{analysis[:safety_analysis][:read_only] ? 'Yes' : 'No'}"
336
- output << "Risk level: #{analysis[:estimated_risk]}"
337
- output << "Would execute: #{analysis[:would_execute] ? 'Yes' : 'No'}"
338
- output << "Summary: #{analysis[:safety_analysis][:summary]}"
339
-
340
- if analysis[:safety_analysis][:violations].any?
341
- output << "\nViolations:"
342
- analysis[:safety_analysis][:violations].each do |violation|
343
- output << " - #{violation[:description]} (#{violation[:severity]})"
344
- end
345
- end
346
-
347
- if analysis[:recommendations].any?
348
- output << "\nRecommendations:"
349
- analysis[:recommendations].each do |rec|
350
- output << " - #{rec}"
351
- end
352
- end
353
-
354
- output.join("\n")
355
- rescue StandardError => e
356
- "Analysis failed: #{e.message}"
357
- end
358
- end
359
-
360
- def format_success_result(result)
361
- output = []
362
- output << "Code: #{result[:code]}"
363
- output << "Result: #{result[:return_value_string] || result[:return_value]}"
364
- output << "Output: #{result[:output]}" if result[:output].present?
365
- output << "Execution time: #{result[:execution_time]}s" if result[:execution_time]
366
- output << "Note: #{result[:note]}" if result[:note]
367
- output.join("\n")
368
- end
369
-
370
- def jsonrpc_error(id, code, message)
371
- {
372
- jsonrpc: JSONRPC_VERSION,
373
- id: id,
374
- error: { code: code, message: message }
375
- }
376
- end
377
-
378
- def error_response(status, message)
379
- [status, { 'Content-Type' => 'application/json' },
380
- [{ error: message }.to_json]]
381
- end
382
- end
383
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsActiveMcp
4
- class Railtie < ::Rails::Railtie
5
- railtie_name :rails_active_mcp
6
-
7
- # Ensure configuration is available very early
8
- config.before_initialize do
9
- RailsActiveMcp.configure unless RailsActiveMcp.configuration
10
- end
11
-
12
- # Add rake tasks
13
- rake_tasks do
14
- load 'rails_active_mcp/tasks.rake'
15
- end
16
-
17
- # Add generators
18
- generators do
19
- # Generators are auto-discovered from lib/generators following Rails conventions
20
- end
21
-
22
- # Console hook for easier access
23
- console do
24
- # Add convenience methods to console
25
- Rails::ConsoleMethods.include(RailsActiveMcp::ConsoleMethods) if defined?(Rails::ConsoleMethods)
26
- end
27
-
28
- # Configure logging - Fixed for Rails 7.1 compatibility
29
- initializer 'rails_active_mcp.logger', after: :initialize_logger, before: :set_clear_dependencies_hook do
30
- # Only set logger if Rails logger is available and responds to logging methods
31
- RailsActiveMcp.logger = if defined?(Rails.logger) && Rails.logger.respond_to?(:info)
32
- # Check if Rails logger is using semantic logger or other custom loggers
33
- if Rails.logger.class.name.include?('SemanticLogger')
34
- # For semantic logger, we need to create a tagged logger
35
- Rails.logger.tagged('RailsActiveMcp')
36
- else
37
- # For standard Rails logger, use it directly
38
- Rails.logger
39
- end
40
- else
41
- # Fallback to our own logger if Rails logger is not available
42
- # This should not happen in normal Rails apps but provides safety
43
- Logger.new(STDERR).tap do |logger|
44
- logger.level = Rails.env.production? ? Logger::WARN : Logger::INFO
45
- logger.formatter = proc do |severity, datetime, progname, msg|
46
- "[#{datetime}] #{severity} -- RailsActiveMcp: #{msg}\n"
47
- end
48
- end
49
- end
50
-
51
- # Log that the logger has been initialized
52
- RailsActiveMcp.logger.info "Rails Active MCP logger initialized (#{RailsActiveMcp.logger.class.name})"
53
- end
54
- end
55
-
56
- # Console convenience methods
57
- module ConsoleMethods
58
- def mcp_execute(code, **options)
59
- RailsActiveMcp.execute(code, **options)
60
- end
61
-
62
- def mcp_safe?(code)
63
- RailsActiveMcp.safe?(code)
64
- end
65
-
66
- def mcp_config
67
- RailsActiveMcp.config
68
- end
69
- end
70
- end