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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -15
  3. data/app/controllers/action_mcp/application_controller.rb +47 -40
  4. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
  5. data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
  6. data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
  7. data/app/models/action_mcp/oauth_client.rb +7 -5
  8. data/app/models/action_mcp/oauth_token.rb +2 -1
  9. data/app/models/action_mcp/session.rb +40 -5
  10. data/config/routes.rb +4 -2
  11. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  12. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
  13. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
  14. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
  15. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  16. data/lib/action_mcp/base_response.rb +1 -1
  17. data/lib/action_mcp/client/base.rb +12 -13
  18. data/lib/action_mcp/client/collection.rb +3 -3
  19. data/lib/action_mcp/client/elicitation.rb +4 -4
  20. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  21. data/lib/action_mcp/client/jwt_client_provider.rb +6 -5
  22. data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
  23. data/lib/action_mcp/client/streamable_http_transport.rb +63 -27
  24. data/lib/action_mcp/client.rb +19 -4
  25. data/lib/action_mcp/configuration.rb +28 -53
  26. data/lib/action_mcp/engine.rb +5 -1
  27. data/lib/action_mcp/filtered_logger.rb +1 -1
  28. data/lib/action_mcp/gateway.rb +47 -137
  29. data/lib/action_mcp/gateway_identifier.rb +29 -0
  30. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  31. data/lib/action_mcp/jwt_decoder.rb +4 -2
  32. data/lib/action_mcp/jwt_identifier.rb +28 -0
  33. data/lib/action_mcp/none_identifier.rb +19 -0
  34. data/lib/action_mcp/o_auth_identifier.rb +34 -0
  35. data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
  36. data/lib/action_mcp/oauth/memory_storage.rb +1 -3
  37. data/lib/action_mcp/oauth/middleware.rb +13 -18
  38. data/lib/action_mcp/oauth/provider.rb +45 -65
  39. data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
  40. data/lib/action_mcp/prompt.rb +2 -0
  41. data/lib/action_mcp/renderable.rb +1 -1
  42. data/lib/action_mcp/resource_template.rb +6 -2
  43. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +39 -26
  44. data/lib/action_mcp/server/base_session_store.rb +86 -0
  45. data/lib/action_mcp/server/capabilities.rb +2 -1
  46. data/lib/action_mcp/server/elicitation.rb +3 -9
  47. data/lib/action_mcp/server/error_handling.rb +14 -1
  48. data/lib/action_mcp/server/handlers/router.rb +31 -0
  49. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  50. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  51. data/lib/action_mcp/server/prompts.rb +4 -4
  52. data/lib/action_mcp/server/resources.rb +23 -4
  53. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  54. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  55. data/lib/action_mcp/server/tools.rb +62 -43
  56. data/lib/action_mcp/server/transport_handler.rb +2 -4
  57. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  58. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  59. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  60. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  61. data/lib/action_mcp/tool.rb +48 -37
  62. data/lib/action_mcp/types/float_array_type.rb +5 -3
  63. data/lib/action_mcp/version.rb +1 -1
  64. data/lib/action_mcp.rb +1 -1
  65. data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
  66. data/lib/tasks/action_mcp_tasks.rake +7 -5
  67. metadata +24 -18
  68. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -2,14 +2,14 @@
2
2
 
3
3
  module ActionMCP
4
4
  module Server
5
- # Memory-based session object that mimics ActiveRecord Session
6
- class MemorySession
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
- # Mimic ActiveRecord interface
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.save_session(self) if @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.delete_session(id) if @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
- # Maintain cache limit
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
- unless @subscriptions.any? { |s| s[:uri] == uri }
200
- @subscriptions << { uri: uri, created_at: Time.current }
201
- end
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
- public
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(condition, value)
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 && 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