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.
- checksums.yaml +4 -4
- data/README.md +187 -16
- data/app/controllers/action_mcp/application_controller.rb +64 -49
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +71 -113
- data/config/routes.rb +0 -11
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +9 -11
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
- data/lib/action_mcp/client.rb +6 -26
- data/lib/action_mcp/configuration.rb +65 -63
- data/lib/action_mcp/engine.rb +1 -10
- data/lib/action_mcp/filtered_logger.rb +3 -7
- data/lib/action_mcp/gateway.rb +7 -11
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -7
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +18 -100
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
- data/app/models/action_mcp/oauth_client.rb +0 -157
- data/app/models/action_mcp/oauth_token.rb +0 -141
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -26
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -134
- data/lib/action_mcp/oauth/middleware.rb +0 -133
- data/lib/action_mcp/oauth/provider.rb +0 -426
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
- 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
|
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,
|
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
|
-
|
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(
|
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
|
6
|
-
|
7
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
23
|
+
Rails.application.reloader.wrap do
|
24
|
+
prompt.call
|
25
|
+
end
|
26
26
|
else
|
27
|
-
|
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
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
97
|
+
Rails.logger.debug "Registered Resource Templates: #{ActionMCP::ResourceTemplatesRegistry.resource_templates.keys}"
|
79
98
|
end
|
80
99
|
end
|
81
100
|
end
|
@@ -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.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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]
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
87
|
-
|
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
|
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
|
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
|