actionmcp 0.71.1 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +187 -16
  3. data/app/controllers/action_mcp/application_controller.rb +64 -49
  4. data/app/models/action_mcp/session/message.rb +31 -20
  5. data/app/models/action_mcp/session/resource.rb +35 -20
  6. data/app/models/action_mcp/session/sse_event.rb +23 -17
  7. data/app/models/action_mcp/session/subscription.rb +22 -15
  8. data/app/models/action_mcp/session.rb +71 -113
  9. data/config/routes.rb +0 -11
  10. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  11. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/base_response.rb +1 -1
  14. data/lib/action_mcp/client/base.rb +9 -11
  15. data/lib/action_mcp/client/elicitation.rb +4 -4
  16. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  17. data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
  18. data/lib/action_mcp/client.rb +6 -26
  19. data/lib/action_mcp/configuration.rb +65 -63
  20. data/lib/action_mcp/engine.rb +1 -10
  21. data/lib/action_mcp/filtered_logger.rb +3 -7
  22. data/lib/action_mcp/gateway.rb +7 -11
  23. data/lib/action_mcp/gateway_identifier.rb +187 -3
  24. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  25. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  26. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  27. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  28. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  29. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  30. data/lib/action_mcp/prompt.rb +2 -0
  31. data/lib/action_mcp/renderable.rb +1 -1
  32. data/lib/action_mcp/resource_template.rb +6 -2
  33. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
  34. data/lib/action_mcp/server/base_session_store.rb +86 -0
  35. data/lib/action_mcp/server/capabilities.rb +2 -1
  36. data/lib/action_mcp/server/elicitation.rb +3 -9
  37. data/lib/action_mcp/server/error_handling.rb +14 -1
  38. data/lib/action_mcp/server/handlers/router.rb +31 -0
  39. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  40. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  41. data/lib/action_mcp/server/prompts.rb +4 -4
  42. data/lib/action_mcp/server/resources.rb +23 -4
  43. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  44. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  45. data/lib/action_mcp/server/tools.rb +62 -43
  46. data/lib/action_mcp/server/transport_handler.rb +2 -4
  47. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  48. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  49. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  50. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  51. data/lib/action_mcp/tool.rb +48 -37
  52. data/lib/action_mcp/types/float_array_type.rb +5 -3
  53. data/lib/action_mcp/version.rb +1 -1
  54. data/lib/action_mcp.rb +2 -7
  55. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  56. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  57. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  58. data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
  59. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  60. data/lib/tasks/action_mcp_tasks.rake +7 -5
  61. metadata +18 -100
  62. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
  63. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
  64. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
  65. data/app/models/action_mcp/oauth_client.rb +0 -157
  66. data/app/models/action_mcp/oauth_token.rb +0 -141
  67. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
  68. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
  69. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
  70. data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
  71. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  72. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  73. data/lib/action_mcp/jwt_decoder.rb +0 -26
  74. data/lib/action_mcp/jwt_identifier.rb +0 -28
  75. data/lib/action_mcp/none_identifier.rb +0 -19
  76. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  77. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  78. data/lib/action_mcp/oauth/error.rb +0 -79
  79. data/lib/action_mcp/oauth/memory_storage.rb +0 -134
  80. data/lib/action_mcp/oauth/middleware.rb +0 -133
  81. data/lib/action_mcp/oauth/provider.rb +0 -426
  82. data/lib/action_mcp/oauth.rb +0 -12
  83. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
  84. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ class BaseSessionStore
6
+ include SessionStore
7
+
8
+ def initialize
9
+ @sessions = Concurrent::Hash.new
10
+ end
11
+
12
+ def create_session(session_id = nil, attributes = {})
13
+ session_id ||= SecureRandom.hex(6)
14
+
15
+ session_data = {
16
+ id: session_id,
17
+ status: "pre_initialize",
18
+ initialized: false,
19
+ role: "server",
20
+ messages_count: 0,
21
+ sse_event_counter: 0,
22
+ created_at: Time.current,
23
+ updated_at: Time.current
24
+ }.merge(attributes)
25
+
26
+ session = BaseSession.new(session_data, self)
27
+
28
+ if session.role == "server"
29
+ session.server_info = {
30
+ name: ActionMCP.configuration.name,
31
+ version: ActionMCP.configuration.version
32
+ }
33
+ session.server_capabilities = ActionMCP.configuration.capabilities
34
+
35
+ session.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
36
+ session.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
37
+ session.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
38
+ end
39
+
40
+ @sessions[session_id] = session
41
+ session
42
+ end
43
+
44
+ def load_session(session_id)
45
+ session = @sessions[session_id]
46
+ session&.instance_variable_set(:@new_record, false)
47
+ session
48
+ end
49
+
50
+ def save_session(session)
51
+ @sessions[session.id] = session
52
+ end
53
+
54
+ def delete_session(session_id)
55
+ @sessions.delete(session_id)
56
+ end
57
+
58
+ def session_exists?(session_id)
59
+ @sessions.key?(session_id)
60
+ end
61
+
62
+ def find_sessions(criteria = {})
63
+ sessions = @sessions.values
64
+
65
+ sessions = sessions.select { |s| s.status == criteria[:status] } if criteria[:status]
66
+ sessions = sessions.select { |s| s.role == criteria[:role] } if criteria[:role]
67
+
68
+ sessions
69
+ end
70
+
71
+ def cleanup_expired_sessions(older_than: 24.hours.ago)
72
+ expired_ids = @sessions.select { |_id, session| session.updated_at < older_than }.keys
73
+ expired_ids.each { |id| @sessions.delete(id) }
74
+ expired_ids.count
75
+ end
76
+
77
+ def clear_all
78
+ @sessions.clear
79
+ end
80
+
81
+ def session_count
82
+ @sessions.size
83
+ end
84
+ end
85
+ end
86
+ end
@@ -18,6 +18,7 @@ module ActionMCP
18
18
  unless client_protocol_version.is_a?(String) && client_protocol_version.present?
19
19
  return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
20
20
  end
21
+
21
22
  unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
22
23
  error_message = "Unsupported protocol version. Client requested '#{client_protocol_version}' but server supports #{ActionMCP::SUPPORTED_VERSIONS.join(', ')}"
23
24
  error_data = {
@@ -37,7 +38,7 @@ module ActionMCP
37
38
  # Handle session resumption if sessionId provided
38
39
  if session_id
39
40
  existing_session = ActionMCP::Session.find_by(id: session_id)
40
- if existing_session && existing_session.initialized?
41
+ if existing_session&.initialized?
41
42
  # Resume existing session - update transport reference
42
43
  transport.instance_variable_set(:@session, existing_session)
43
44
 
@@ -31,9 +31,7 @@ module ActionMCP
31
31
  end
32
32
 
33
33
  properties = schema[:properties]
34
- unless properties.is_a?(Hash)
35
- raise ArgumentError, "Elicitation schema must have properties"
36
- end
34
+ raise ArgumentError, "Elicitation schema must have properties" unless properties.is_a?(Hash)
37
35
 
38
36
  properties.each do |key, prop_schema|
39
37
  validate_primitive_schema!(key, prop_schema)
@@ -42,17 +40,13 @@ module ActionMCP
42
40
 
43
41
  # Validates individual property schemas are primitive types
44
42
  def validate_primitive_schema!(key, schema)
45
- unless schema.is_a?(Hash)
46
- raise ArgumentError, "Property '#{key}' must have a schema definition"
47
- end
43
+ raise ArgumentError, "Property '#{key}' must have a schema definition" unless schema.is_a?(Hash)
48
44
 
49
45
  type = schema[:type]
50
46
  case type
51
47
  when "string"
52
48
  # Valid string schema, check for enums
53
- if schema[:enum] && !schema[:enum].is_a?(Array)
54
- raise ArgumentError, "Property '#{key}' enum must be an array"
55
- end
49
+ raise ArgumentError, "Property '#{key}' enum must be an array" if schema[:enum] && !schema[:enum].is_a?(Array)
56
50
  when "number", "integer", "boolean"
57
51
  # Valid primitive types
58
52
  else
@@ -22,10 +22,23 @@ module ActionMCP
22
22
  def error_response_from_exception(id, exception)
23
23
  if exception.is_a?(JSON_RPC::JsonRpcError)
24
24
  error_response(id, exception)
25
+ elsif Rails.env.development?
26
+ # Provide more detailed error information in development
27
+ error_response(id, :internal_error, exception.message, {
28
+ class: exception.class.name,
29
+ backtrace: exception.backtrace&.first(5)
30
+ })
25
31
  else
26
- error_response(id, :internal_error, exception.message)
32
+ error_response(id, :internal_error, "An unexpected error occurred")
27
33
  end
28
34
  end
35
+
36
+ # Enhanced error logging
37
+ def log_error(exception, context = {})
38
+ Rails.logger.error "[MCP Error] #{exception.class}: #{exception.message}"
39
+ Rails.logger.error "Context: #{context.inspect}" if context.present?
40
+ Rails.logger.error exception.backtrace&.first(10)&.join("\n") if Rails.env.development?
41
+ end
29
42
  end
30
43
  end
31
44
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ module Handlers
6
+ class Router
7
+ def initialize(handler)
8
+ @handler = handler
9
+ end
10
+
11
+ def route(rpc_method, id, params)
12
+ case rpc_method
13
+ when "initialize"
14
+ @handler.handle_initialize(id, params)
15
+ when %r{^prompts/}
16
+ @handler.process_prompts(rpc_method, id, params)
17
+ when %r{^resources/}
18
+ @handler.process_resources(rpc_method, id, params)
19
+ when %r{^tools/}
20
+ @handler.process_tools(rpc_method, id, params)
21
+ when "completion/complete"
22
+ @handler.process_completion_complete(id, params)
23
+ else
24
+ raise ActionMCP::Server::JSON_RPC::JsonRpcError.new(:method_not_found,
25
+ message: "Method not found: #{rpc_method}")
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -31,7 +31,7 @@ module ActionMCP
31
31
  rpc_method = request.method
32
32
  params = request.params
33
33
 
34
- result = with_error_handling(id) do
34
+ with_error_handling(id) do
35
35
  common_result = handle_common_methods(rpc_method, id, params)
36
36
  if common_result
37
37
  common_result
@@ -41,8 +41,6 @@ module ActionMCP
41
41
  transport.messaging_mode == :return ? transport.get_last_response : nil
42
42
  end
43
43
  end
44
-
45
- result
46
44
  end
47
45
 
48
46
  def route_to_handler(rpc_method, id, params)
@@ -80,7 +78,6 @@ module ActionMCP
80
78
  response
81
79
  end
82
80
 
83
-
84
81
  def process_completion_complete(id, params)
85
82
  # Extract context if provided
86
83
  context = params["context"] if params.is_a?(Hash)
@@ -105,7 +102,7 @@ module ActionMCP
105
102
  }
106
103
  end
107
104
 
108
- def build_completion_result(params = {}, context = nil)
105
+ def build_completion_result(_params = {}, _context = nil)
109
106
  # In a real implementation, this would use the params and context
110
107
  # to generate appropriate completion suggestions
111
108
  # For now, we just return an empty result
@@ -2,10 +2,9 @@
2
2
 
3
3
  module ActionMCP
4
4
  module Server
5
- module Messaging
6
- # Operation mode for the messaging module
7
- # :write - writes messages directly (default, for SSE)
8
- # :return - returns messages without writing (for JSON responses)
5
+ module MessagingService
6
+ include BaseMessaging # For write_message
7
+
9
8
  attr_accessor :messaging_mode
10
9
 
11
10
  def send_jsonrpc_request(method, params: nil, id: SecureRandom.uuid_v7)
@@ -13,7 +12,6 @@ module ActionMCP
13
12
  end
14
13
 
15
14
  def send_jsonrpc_response(request_id, result: nil, error: nil)
16
- # Only pass the parameters that are actually provided
17
15
  args = { id: request_id }
18
16
  args[:result] = result unless result.nil?
19
17
  args[:error] = error unless error.nil?
@@ -29,9 +27,39 @@ module ActionMCP
29
27
  send_jsonrpc_response(request_id, error: error)
30
28
  end
31
29
 
30
+ # Specific notifications
31
+ def send_resources_list_changed_notification
32
+ send_jsonrpc_notification("notifications/resources/list_changed")
33
+ end
34
+
35
+ def send_resource_updated_notification(uri)
36
+ send_jsonrpc_notification("notifications/resources/updated", { uri: uri })
37
+ end
38
+
39
+ def send_tools_list_changed_notification
40
+ send_jsonrpc_notification("notifications/tools/list_changed")
41
+ end
42
+
43
+ def send_prompts_list_changed_notification
44
+ send_jsonrpc_notification("notifications/prompts/list_changed")
45
+ end
46
+
47
+ def send_logging_message_notification(level:, data:, logger: nil)
48
+ params = { level: level, data: data }
49
+ params[:logger] = logger if logger.present?
50
+ send_jsonrpc_notification("notifications/logging/message", params)
51
+ end
52
+
53
+ def send_progress_notification(progressToken:, progress:, total: nil, message: nil, **options)
54
+ params = { progressToken: progressToken, progress: progress }
55
+ params[:total] = total unless total.nil?
56
+ params[:message] = message if message.present?
57
+ params.merge!(options) if options.any?
58
+ send_jsonrpc_notification("notifications/progress", params)
59
+ end
60
+
32
61
  private
33
62
 
34
- # Factory method to create and send appropriate JSON-RPC message
35
63
  def send_message(type, **args)
36
64
  message = case type
37
65
  when :request
@@ -41,7 +69,6 @@ module ActionMCP
41
69
  params: args[:params]
42
70
  )
43
71
  when :response
44
- # Pass only the provided parameters to avoid validation errors
45
72
  response_args = { id: args[:id] }
46
73
  response_args[:result] = args[:result] if args.key?(:result)
47
74
  response_args[:error] = args[:error] if args.key?(:error)
@@ -53,13 +80,10 @@ module ActionMCP
53
80
  )
54
81
  end
55
82
 
56
- if messaging_mode == :return
57
- write_message(message) # This will be intercepted by ResponseCollector
58
- message
59
- else
60
- write_message(message)
61
- nil
62
- end
83
+ write_message(message)
84
+ return unless messaging_mode == :return
85
+
86
+ message
63
87
  end
64
88
  end
65
89
  end
@@ -20,11 +20,11 @@ module ActionMCP
20
20
 
21
21
  # Wrap prompt execution with Rails reloader for development
22
22
  result = if Rails.env.development? && defined?(Rails.application.reloader)
23
- Rails.application.reloader.wrap do
24
- prompt.call
25
- end
23
+ Rails.application.reloader.wrap do
24
+ prompt.call
25
+ end
26
26
  else
27
- prompt.call
27
+ prompt.call
28
28
  end
29
29
 
30
30
  if result.is_error
@@ -45,7 +45,25 @@ module ActionMCP
45
45
  # @example Output:
46
46
  # # Sends: {"jsonrpc":"2.0","id":"req-789","result":{"contents":[{"uri":"file:///example.txt","text":"Example content"}]}}
47
47
  def send_resource_read(id, params)
48
- if (template = ResourceTemplatesRegistry.find_template_for_uri(params[:uri]))
48
+ template = ResourceTemplatesRegistry.find_template_for_uri(params[:uri])
49
+
50
+ unless template
51
+ send_jsonrpc_error(id, :resource_not_found, "No resource template found for URI: #{params[:uri]}")
52
+ return
53
+ end
54
+
55
+ # Check if resource requires consent and if consent is granted
56
+ if template.respond_to?(:requires_consent?) && template.requires_consent? && !session.consent_granted_for?("resource:#{template.name}")
57
+ # Use custom error response for consent required (-32002)
58
+ error = {
59
+ code: -32_002,
60
+ message: "Consent required for resource template '#{template.name}'"
61
+ }
62
+ send_jsonrpc_response(id, error: error)
63
+ return
64
+ end
65
+
66
+ begin
49
67
  # Create template instance and set execution context
50
68
  record = template.process(params[:uri])
51
69
  record.with_context({ session: session })
@@ -60,8 +78,9 @@ module ActionMCP
60
78
  # Handle successful response - ResourceResponse.contents is already an array
61
79
  send_jsonrpc_response(id, result: { contents: response.contents.map(&:to_h) })
62
80
  end
63
- else
64
- send_jsonrpc_error(id, :invalid_params, "Invalid resource URI")
81
+ rescue StandardError => e
82
+ log_error(e, { resource_uri: params[:uri], template: template.name })
83
+ send_jsonrpc_error(id, :internal_error, "Failed to read resource: #{e.message}")
65
84
  end
66
85
  end
67
86
 
@@ -75,7 +94,7 @@ module ActionMCP
75
94
  # @example Output:
76
95
  # # Logs: "Registered Resource Templates: ["db://{table}", "file://{path}"]"
77
96
  def log_resource_templates
78
- # Resource templates: #{ActionMCP::ResourceTemplatesRegistry.resource_templates.keys}
97
+ Rails.logger.debug "Registered Resource Templates: #{ActionMCP::ResourceTemplatesRegistry.resource_templates.keys}"
79
98
  end
80
99
  end
81
100
  end
@@ -3,7 +3,7 @@
3
3
  module ActionMCP
4
4
  module Server
5
5
  class SessionStoreFactory
6
- def self.create(type = nil, **options)
6
+ def self.create(type = nil, **_options)
7
7
  type ||= default_type
8
8
 
9
9
  case type.to_sym
@@ -38,13 +38,13 @@ module ActionMCP
38
38
  ensure_pubsub.subscribe(session_id) do |message|
39
39
  # Message from SolidMCP includes event_type, data, and id
40
40
  # Deliver to all callbacks for this session
41
- @subscriptions.each do |sub_id, subscription|
42
- if subscription[:session_id] == session_id && subscription[:message_callback]
43
- begin
44
- subscription[:message_callback].call(message[:data])
45
- rescue StandardError => e
46
- log_error("Error in message callback: #{e.message}")
47
- end
41
+ @subscriptions.each_value do |subscription|
42
+ next unless subscription[:session_id] == session_id && subscription[:message_callback]
43
+
44
+ begin
45
+ subscription[:message_callback].call(message[:data])
46
+ rescue StandardError => e
47
+ log_error("Error in message callback: #{e.message}")
48
48
  end
49
49
  end
50
50
  end
@@ -80,7 +80,7 @@ module ActionMCP
80
80
  end
81
81
 
82
82
  # Only unsubscribe from SolidMCP if no more callbacks for this session
83
- if @session_callbacks[session_id]&.empty?
83
+ if @session_callbacks[session_id] && @session_callbacks[session_id].empty?
84
84
  ensure_pubsub.unsubscribe(session_id)
85
85
  @session_callbacks.delete(session_id)
86
86
  end
@@ -125,7 +125,7 @@ module ActionMCP
125
125
  private
126
126
 
127
127
  def ensure_pubsub
128
- @pubsub ||= SolidMCP::PubSub.new(@options)
128
+ @ensure_pubsub ||= SolidMCP::PubSub.new(@options)
129
129
  end
130
130
 
131
131
  def extract_session_id(channel)
@@ -141,7 +141,6 @@ module ActionMCP
141
141
  "message"
142
142
  end
143
143
 
144
-
145
144
  def log_subscription_event(channel, action, subscription_id = nil)
146
145
  return unless defined?(Rails) && Rails.respond_to?(:logger)
147
146
 
@@ -10,7 +10,7 @@ module ActionMCP
10
10
 
11
11
  # Send initial progress notification if token is provided
12
12
  if progress_token
13
- session.send_progress_notification(
13
+ send_progress_notification(
14
14
  progressToken: progress_token,
15
15
  progress: 0,
16
16
  message: "Starting tools list retrieval"
@@ -26,7 +26,7 @@ module ActionMCP
26
26
 
27
27
  # Send completion progress notification if token is provided
28
28
  if progress_token
29
- session.send_progress_notification(
29
+ send_progress_notification(
30
30
  progressToken: progress_token,
31
31
  progress: 100,
32
32
  message: "Tools list retrieval complete"
@@ -40,51 +40,70 @@ module ActionMCP
40
40
  # Find tool in session's registry
41
41
  tool_class = session.registered_tools.find { |t| t.tool_name == tool_name }
42
42
 
43
- if tool_class
44
- begin
45
- # Create tool and set execution context with request info
46
- tool = tool_class.new(arguments)
47
- tool.with_context({
48
- session: session,
49
- request: {
50
- params: {
51
- name: tool_name,
52
- arguments: arguments,
53
- _meta: _meta
54
- }
55
- }
56
- })
43
+ unless tool_class
44
+ Rails.logger.error "Tool not found: #{tool_name}. Registered tools: #{session.registered_tools.map(&:tool_name).join(', ')}"
45
+ send_jsonrpc_error(request_id, :method_not_found,
46
+ "Tool '#{tool_name}' not found or not registered for this session")
47
+ return
48
+ end
49
+
50
+ # Check if tool requires consent and if consent is granted
51
+ if tool_class.respond_to?(:requires_consent?) && tool_class.requires_consent? && !session.consent_granted_for?(tool_name)
52
+ # Use custom error response for consent required (-32002)
53
+ error = {
54
+ code: -32_002,
55
+ message: "Consent required for tool '#{tool_name}'"
56
+ }
57
+ send_jsonrpc_response(request_id, error: error)
58
+ return
59
+ end
57
60
 
58
- # Wrap tool execution with Rails reloader for development
59
- result = if Rails.env.development?
60
- # Preserve Current attributes across reloader boundary
61
- current_user = ActionMCP::Current.user
62
- current_gateway = ActionMCP::Current.gateway
61
+ begin
62
+ # Create tool and set execution context with request info
63
+ tool = tool_class.new(arguments)
64
+ tool.with_context({
65
+ session: session,
66
+ request: {
67
+ params: {
68
+ name: tool_name,
69
+ arguments: arguments,
70
+ _meta: _meta
71
+ }
72
+ }
73
+ })
63
74
 
64
- Rails.application.reloader.wrap do
65
- # Restore Current attributes inside reloader
66
- ActionMCP::Current.user = current_user
67
- ActionMCP::Current.gateway = current_gateway
68
- tool.call
69
- end
70
- else
71
- tool.call
72
- end
75
+ # Wrap tool execution with Rails reloader for development
76
+ result = if Rails.env.development?
77
+ # Preserve Current attributes across reloader boundary
78
+ current_user = ActionMCP::Current.user
79
+ current_gateway = ActionMCP::Current.gateway
80
+
81
+ Rails.application.reloader.wrap do
82
+ # Restore Current attributes inside reloader
83
+ ActionMCP::Current.user = current_user
84
+ ActionMCP::Current.gateway = current_gateway
85
+ tool.call
86
+ end
87
+ else
88
+ tool.call
89
+ end
73
90
 
74
- if result.is_error
75
- # Convert ToolResponse error to proper JSON-RPC error format
76
- # Pass the error hash directly - the Response class will handle it
77
- error_hash = result.to_h
78
- send_jsonrpc_response(request_id, error: error_hash)
79
- else
80
- send_jsonrpc_response(request_id, result: result)
81
- end
82
- rescue ArgumentError => e
83
- # Handle parameter validation errors
84
- send_jsonrpc_error(request_id, :invalid_params, e.message)
91
+ if result.is_error
92
+ # Convert ToolResponse error to proper JSON-RPC error format
93
+ # Pass the error hash directly - the Response class will handle it
94
+ error_hash = result.to_h
95
+ send_jsonrpc_response(request_id, error: error_hash)
96
+ else
97
+ send_jsonrpc_response(request_id, result: result)
85
98
  end
86
- else
87
- send_jsonrpc_error(request_id, :method_not_found, "Tool '#{tool_name}' not available in this session")
99
+ rescue ArgumentError => e
100
+ # Handle parameter validation errors
101
+ send_jsonrpc_error(request_id, :invalid_params, e.message)
102
+ rescue StandardError => e
103
+ # Log the actual error for debugging
104
+ Rails.logger.error "Tool execution error: #{e.class} - #{e.message}"
105
+ Rails.logger.error e.backtrace.join("\n")
106
+ send_jsonrpc_error(request_id, :internal_error, "An unexpected error occurred.")
88
107
  end
89
108
  end
90
109
 
@@ -12,17 +12,15 @@ module ActionMCP
12
12
  delegate :read, :write, to: :session
13
13
  include Logging
14
14
 
15
- include BaseMessaging # Provides basic write_message
16
- include Messaging
15
+ include MessagingService
17
16
  include Capabilities
18
17
  include Tools
19
18
  include Prompts
20
19
  include Resources
21
- include Notifications
22
20
  include Sampling
23
21
  include Roots
24
22
  include Elicitation
25
- include ResponseCollector # Must be included last to override write_message
23
+ include ResponseCollector # Must be included last to override write_message
26
24
 
27
25
  # @param [ActionMCP::Session] session
28
26
  # @param messaging_mode [:write, :return] The mode for message handling