actionmcp 0.71.0 → 0.72.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 +186 -15
- data/app/controllers/action_mcp/application_controller.rb +47 -40
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
- data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
- data/app/models/action_mcp/oauth_client.rb +7 -5
- data/app/models/action_mcp/oauth_token.rb +2 -1
- data/app/models/action_mcp/session.rb +40 -5
- data/config/routes.rb +4 -2
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +12 -13
- data/lib/action_mcp/client/collection.rb +3 -3
- 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/jwt_client_provider.rb +6 -5
- data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
- data/lib/action_mcp/client/streamable_http_transport.rb +63 -27
- data/lib/action_mcp/client.rb +19 -4
- data/lib/action_mcp/configuration.rb +28 -53
- data/lib/action_mcp/engine.rb +5 -1
- data/lib/action_mcp/filtered_logger.rb +1 -1
- data/lib/action_mcp/gateway.rb +47 -137
- data/lib/action_mcp/gateway_identifier.rb +29 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/jwt_decoder.rb +4 -2
- data/lib/action_mcp/jwt_identifier.rb +28 -0
- data/lib/action_mcp/none_identifier.rb +19 -0
- data/lib/action_mcp/o_auth_identifier.rb +34 -0
- data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
- data/lib/action_mcp/oauth/memory_storage.rb +1 -3
- data/lib/action_mcp/oauth/middleware.rb +13 -18
- data/lib/action_mcp/oauth/provider.rb +45 -65
- data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
- 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} +39 -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 +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +24 -18
- data/lib/action_mcp/server/notifications.rb +0 -58
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module Server
|
5
|
-
#
|
6
|
-
class
|
5
|
+
# Base session object that mimics ActiveRecord Session with common functionality
|
6
|
+
class BaseSession
|
7
7
|
attr_accessor :id, :status, :initialized, :role, :messages_count,
|
8
8
|
:sse_event_counter, :protocol_version, :client_info,
|
9
9
|
:client_capabilities, :server_info, :server_capabilities,
|
10
10
|
:tool_registry, :prompt_registry, :resource_registry,
|
11
11
|
:created_at, :updated_at, :ended_at, :last_event_id,
|
12
|
-
:session_data
|
12
|
+
:session_data, :consents
|
13
13
|
|
14
14
|
def initialize(attributes = {}, store = nil)
|
15
15
|
@store = store
|
@@ -21,12 +21,15 @@ module ActionMCP
|
|
21
21
|
@message_counter = Concurrent::AtomicFixnum.new(0)
|
22
22
|
@new_record = true
|
23
23
|
|
24
|
+
# Initialize consents as empty hash if not provided
|
25
|
+
@consents = {}
|
26
|
+
|
24
27
|
attributes.each do |key, value|
|
25
28
|
send("#{key}=", value) if respond_to?("#{key}=")
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
29
|
-
#
|
32
|
+
# ActiveRecord-like interface
|
30
33
|
def new_record?
|
31
34
|
@new_record
|
32
35
|
end
|
@@ -37,7 +40,7 @@ module ActionMCP
|
|
37
40
|
|
38
41
|
def save
|
39
42
|
self.updated_at = Time.current
|
40
|
-
@store
|
43
|
+
@store&.save_session(self)
|
41
44
|
@new_record = false
|
42
45
|
true
|
43
46
|
end
|
@@ -58,7 +61,7 @@ module ActionMCP
|
|
58
61
|
end
|
59
62
|
|
60
63
|
def destroy
|
61
|
-
@store
|
64
|
+
@store&.delete_session(id)
|
62
65
|
end
|
63
66
|
|
64
67
|
def reload
|
@@ -133,17 +136,13 @@ module ActionMCP
|
|
133
136
|
event = { event_id: event_id, data: data, created_at: Time.current }
|
134
137
|
@sse_events << event
|
135
138
|
|
136
|
-
|
137
|
-
while @sse_events.size > max_events
|
138
|
-
@sse_events.shift
|
139
|
-
end
|
139
|
+
@sse_events.shift while @sse_events.size > max_events
|
140
140
|
|
141
141
|
event
|
142
142
|
end
|
143
143
|
|
144
144
|
def get_sse_events_after(last_event_id, limit = 50)
|
145
|
-
@sse_events.select { |e| e[:event_id] > last_event_id }
|
146
|
-
.first(limit)
|
145
|
+
@sse_events.select { |e| e[:event_id] > last_event_id }.first(limit)
|
147
146
|
end
|
148
147
|
|
149
148
|
def cleanup_old_sse_events(max_age = 15.minutes)
|
@@ -151,14 +150,10 @@ module ActionMCP
|
|
151
150
|
@sse_events.delete_if { |e| e[:created_at] < cutoff_time }
|
152
151
|
end
|
153
152
|
|
154
|
-
# Calculates the maximum number of SSE events to store based on configuration
|
155
|
-
# @return [Integer] The maximum number of events
|
156
153
|
def max_stored_sse_events
|
157
154
|
ActionMCP.configuration.max_stored_sse_events || 100
|
158
155
|
end
|
159
156
|
|
160
|
-
# Returns the SSE event retention period from configuration
|
161
|
-
# @return [ActiveSupport::Duration] The retention period (default: 15 minutes)
|
162
157
|
def sse_event_retention_period
|
163
158
|
ActionMCP.configuration.sse_event_retention_period || 15.minutes
|
164
159
|
end
|
@@ -196,9 +191,9 @@ module ActionMCP
|
|
196
191
|
|
197
192
|
# Subscription management
|
198
193
|
def resource_subscribe(uri)
|
199
|
-
|
200
|
-
|
201
|
-
|
194
|
+
return if @subscriptions.any? { |s| s[:uri] == uri }
|
195
|
+
|
196
|
+
@subscriptions << { uri: uri, created_at: Time.current }
|
202
197
|
end
|
203
198
|
|
204
199
|
def resource_unsubscribe(uri)
|
@@ -310,6 +305,29 @@ module ActionMCP
|
|
310
305
|
end
|
311
306
|
end
|
312
307
|
|
308
|
+
# Consent management methods
|
309
|
+
def consent_granted_for?(key)
|
310
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
311
|
+
consents_hash ||= {}
|
312
|
+
consents_hash[key] == true
|
313
|
+
end
|
314
|
+
|
315
|
+
def grant_consent(key)
|
316
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
317
|
+
consents_hash ||= {}
|
318
|
+
consents_hash[key] = true
|
319
|
+
self.consents = consents_hash
|
320
|
+
save!
|
321
|
+
end
|
322
|
+
|
323
|
+
def revoke_consent(key)
|
324
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
325
|
+
consents_hash ||= {}
|
326
|
+
consents_hash.delete(key)
|
327
|
+
self.consents = consents_hash
|
328
|
+
save!
|
329
|
+
end
|
330
|
+
|
313
331
|
private
|
314
332
|
|
315
333
|
def normalize_name(class_or_name, type)
|
@@ -352,7 +370,6 @@ module ActionMCP
|
|
352
370
|
end
|
353
371
|
|
354
372
|
def send_tools_list_changed_notification
|
355
|
-
# Only send if server capabilities allow it
|
356
373
|
return unless server_capabilities.dig("tools", "listChanged")
|
357
374
|
|
358
375
|
write(JSON_RPC::Notification.new(method: "notifications/tools/list_changed"))
|
@@ -370,9 +387,7 @@ module ActionMCP
|
|
370
387
|
write(JSON_RPC::Notification.new(method: "notifications/resources/list_changed"))
|
371
388
|
end
|
372
389
|
|
373
|
-
|
374
|
-
|
375
|
-
# Simple collection classes to mimic ActiveRecord associations
|
390
|
+
# Collection classes
|
376
391
|
class MessageCollection < Array
|
377
392
|
def create!(attributes)
|
378
393
|
self << attributes
|
@@ -380,7 +395,6 @@ module ActionMCP
|
|
380
395
|
end
|
381
396
|
|
382
397
|
def order(field)
|
383
|
-
# Simple ordering implementation
|
384
398
|
sort_by { |msg| msg[field] || msg[field.to_s] }
|
385
399
|
end
|
386
400
|
end
|
@@ -413,8 +427,7 @@ module ActionMCP
|
|
413
427
|
size
|
414
428
|
end
|
415
429
|
|
416
|
-
def where(
|
417
|
-
# Simple implementation for "event_id > ?" condition
|
430
|
+
def where(_condition, value)
|
418
431
|
select { |e| e[:event_id] > value }
|
419
432
|
end
|
420
433
|
|
@@ -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
|
|