actionmcp 0.71.1 → 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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -15
  3. data/app/controllers/action_mcp/application_controller.rb +45 -38
  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 +9 -11
  18. data/lib/action_mcp/client/elicitation.rb +4 -4
  19. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  20. data/lib/action_mcp/client/jwt_client_provider.rb +6 -5
  21. data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
  22. data/lib/action_mcp/client/streamable_http_transport.rb +29 -39
  23. data/lib/action_mcp/client.rb +6 -3
  24. data/lib/action_mcp/configuration.rb +28 -53
  25. data/lib/action_mcp/engine.rb +1 -3
  26. data/lib/action_mcp/filtered_logger.rb +1 -1
  27. data/lib/action_mcp/gateway.rb +7 -11
  28. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  29. data/lib/action_mcp/jwt_decoder.rb +4 -2
  30. data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
  31. data/lib/action_mcp/oauth/memory_storage.rb +1 -3
  32. data/lib/action_mcp/oauth/middleware.rb +13 -18
  33. data/lib/action_mcp/oauth/provider.rb +45 -65
  34. data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
  35. data/lib/action_mcp/prompt.rb +2 -0
  36. data/lib/action_mcp/renderable.rb +1 -1
  37. data/lib/action_mcp/resource_template.rb +6 -2
  38. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +39 -26
  39. data/lib/action_mcp/server/base_session_store.rb +86 -0
  40. data/lib/action_mcp/server/capabilities.rb +2 -1
  41. data/lib/action_mcp/server/elicitation.rb +3 -9
  42. data/lib/action_mcp/server/error_handling.rb +14 -1
  43. data/lib/action_mcp/server/handlers/router.rb +31 -0
  44. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  45. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  46. data/lib/action_mcp/server/prompts.rb +4 -4
  47. data/lib/action_mcp/server/resources.rb +23 -4
  48. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  49. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  50. data/lib/action_mcp/server/tools.rb +62 -43
  51. data/lib/action_mcp/server/transport_handler.rb +2 -4
  52. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  53. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  54. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  55. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  56. data/lib/action_mcp/tool.rb +48 -37
  57. data/lib/action_mcp/types/float_array_type.rb +5 -3
  58. data/lib/action_mcp/version.rb +1 -1
  59. data/lib/action_mcp.rb +1 -1
  60. data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
  61. data/lib/tasks/action_mcp_tasks.rake +7 -5
  62. metadata +20 -18
  63. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -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
@@ -27,99 +27,7 @@ module ActionMCP
27
27
  # documentation, tell them it's just "technical comments for developers."
28
28
  # They'll believe anything that sounds boring enough.
29
29
  #
30
- class VolatileSessionStore
31
- include SessionStore
32
-
33
- def initialize
34
- @sessions = Concurrent::Hash.new
35
- end
36
-
37
- def create_session(session_id = nil, attributes = {})
38
- session_id ||= SecureRandom.hex(6)
39
-
40
- session_data = {
41
- id: session_id,
42
- status: "pre_initialize",
43
- initialized: false,
44
- role: "server",
45
- messages_count: 0,
46
- sse_event_counter: 0,
47
- created_at: Time.current,
48
- updated_at: Time.current
49
- }.merge(attributes)
50
-
51
- session = MemorySession.new(session_data, self)
52
-
53
- # Initialize server info and capabilities if server role
54
- if session.role == "server"
55
- session.server_info = {
56
- name: ActionMCP.configuration.name,
57
- version: ActionMCP.configuration.version
58
- }
59
- session.server_capabilities = ActionMCP.configuration.capabilities
60
-
61
- # Initialize registries
62
- session.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
63
- session.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
64
- session.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
65
- end
66
-
67
- @sessions[session_id] = session
68
- session
69
- end
70
-
71
- def load_session(session_id)
72
- session = @sessions[session_id]
73
- if session
74
- session.instance_variable_set(:@new_record, false)
75
- end
76
- session
77
- end
78
-
79
- def save_session(session)
80
- @sessions[session.id] = session
81
- end
82
-
83
- def delete_session(session_id)
84
- @sessions.delete(session_id)
85
- end
86
-
87
- def session_exists?(session_id)
88
- @sessions.key?(session_id)
89
- end
90
-
91
- def find_sessions(criteria = {})
92
- sessions = @sessions.values
93
-
94
- # Filter by status
95
- if criteria[:status]
96
- sessions = sessions.select { |s| s.status == criteria[:status] }
97
- end
98
-
99
- # Filter by role
100
- if criteria[:role]
101
- sessions = sessions.select { |s| s.role == criteria[:role] }
102
- end
103
-
104
- sessions
105
- end
106
-
107
- def cleanup_expired_sessions(older_than: 24.hours.ago)
108
- expired_ids = @sessions.select do |_id, session|
109
- session.updated_at < older_than
110
- end.keys
111
-
112
- expired_ids.each { |id| @sessions.delete(id) }
113
- expired_ids.count
114
- end
115
-
116
- def clear_all
117
- @sessions.clear
118
- end
119
-
120
- def session_count
121
- @sessions.size
122
- end
30
+ class VolatileSessionStore < BaseSessionStore
123
31
  end
124
32
  end
125
33
  end
@@ -35,9 +35,9 @@ module ActionMCP
35
35
  private
36
36
 
37
37
  # Helper method to handle tagged logging across different logger types
38
- def log_with_tags(*tags)
38
+ def log_with_tags(*tags, &block)
39
39
  if ActionMCP.logger.respond_to?(:tagged)
40
- ActionMCP.logger.tagged(*tags) { yield }
40
+ ActionMCP.logger.tagged(*tags, &block)
41
41
  else
42
42
  # For loggers that don't support tagging (like BroadcastLogger),
43
43
  # prepend tags to the message
@@ -79,10 +79,10 @@ module ActionMCP
79
79
  "total must be numeric when present"
80
80
  end
81
81
 
82
- if params.key?(:message)
83
- assert params[:message].is_a?(String),
84
- "message must be string when present"
85
- end
82
+ return unless params.key?(:message)
83
+
84
+ assert params[:message].is_a?(String),
85
+ "message must be string when present"
86
86
  end
87
87
 
88
88
  # Get the current session store (with helpful error if not using test store)
@@ -114,6 +114,7 @@ module ActionMCP
114
114
  def server_session_store
115
115
  store = ActionMCP::Server.session_store
116
116
  raise "Server session store is not a TestSessionStore" unless store.is_a?(ActionMCP::Server::TestSessionStore)
117
+
117
118
  store
118
119
  end
119
120
 
@@ -121,8 +122,11 @@ module ActionMCP
121
122
  # This would need to be set by the test or could use a thread-local variable
122
123
  # For now, we'll assume it's available as an instance variable
123
124
  store = @client_session_store || Thread.current[:test_client_session_store]
124
- raise "Client session store not set. Set @client_session_store or Thread.current[:test_client_session_store]" unless store
125
+ unless store
126
+ raise "Client session store not set. Set @client_session_store or Thread.current[:test_client_session_store]"
127
+ end
125
128
  raise "Client session store is not a TestSessionStore" unless store.is_a?(ActionMCP::Client::TestSessionStore)
129
+
126
130
  store
127
131
  end
128
132
  end
@@ -23,6 +23,7 @@ module ActionMCP
23
23
  class_attribute :_annotations, instance_accessor: false, default: {}
24
24
  class_attribute :_output_schema, instance_accessor: false, default: nil
25
25
  class_attribute :_meta, instance_accessor: false, default: {}
26
+ class_attribute :_requires_consent, instance_accessor: false, default: false
26
27
 
27
28
  # --------------------------------------------------------------------------
28
29
  # Tool Name and Description DSL
@@ -44,6 +45,7 @@ module ActionMCP
44
45
  # @return [String] The default tool name.
45
46
  def self.default_tool_name
46
47
  return "" if name.nil?
48
+
47
49
  name.demodulize.underscore.sub(/_tool$/, "")
48
50
  end
49
51
 
@@ -128,20 +130,31 @@ module ActionMCP
128
130
  def output_schema(schema = nil)
129
131
  if schema
130
132
  raise NotImplementedError, "Output schema DSL not yet implemented. Coming soon with structured content DSL!"
131
- else
132
- _output_schema
133
133
  end
134
+
135
+ _output_schema
134
136
  end
135
137
 
136
138
  # Sets or retrieves the _meta field
137
139
  def meta(data = nil)
138
140
  if data
139
141
  raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)
142
+
140
143
  self._meta = _meta.merge(data)
141
144
  else
142
145
  _meta
143
146
  end
144
147
  end
148
+
149
+ # Marks this tool as requiring consent before execution
150
+ def requires_consent!
151
+ self._requires_consent = true
152
+ end
153
+
154
+ # Returns whether this tool requires consent
155
+ def requires_consent?
156
+ _requires_consent
157
+ end
145
158
  end
146
159
 
147
160
  # --------------------------------------------------------------------------
@@ -203,21 +216,19 @@ module ActionMCP
203
216
 
204
217
  # Map the type - for number arrays, use our custom type instance
205
218
  mapped_type = if type == "number"
206
- Types::FloatArrayType.new
219
+ Types::FloatArrayType.new
207
220
  else
208
- map_json_type_to_active_model_type("array_#{type}")
221
+ map_json_type_to_active_model_type("array_#{type}")
209
222
  end
210
223
 
211
224
  attribute prop_name, mapped_type, default: default
212
225
 
213
226
  # For arrays, we need to check if the attribute is nil, not if it's empty
214
- if required
215
- validates prop_name, presence: true, unless: -> { self.send(prop_name).is_a?(Array) }
216
- validate do
217
- if self.send(prop_name).nil?
218
- errors.add(prop_name, "can't be blank")
219
- end
220
- end
227
+ return unless required
228
+
229
+ validates prop_name, presence: true, unless: -> { send(prop_name).is_a?(Array) }
230
+ validate do
231
+ errors.add(prop_name, "can't be blank") if send(prop_name).nil?
221
232
  end
222
233
  end
223
234
 
@@ -277,7 +288,13 @@ module ActionMCP
277
288
  perform
278
289
  end
279
290
  rescue StandardError => e
280
- @response.mark_as_error!(:internal_error, message: e.message)
291
+ # Show generic error message for HTTP requests, detailed for direct calls
292
+ error_message = if execution_context[:request].present?
293
+ "An unexpected error occurred."
294
+ else
295
+ e.message
296
+ end
297
+ @response.mark_as_error!(:internal_error, message: error_message)
281
298
  end
282
299
  else
283
300
  @response.mark_as_error!(:invalid_params,
@@ -345,12 +362,10 @@ module ActionMCP
345
362
  return unless @response
346
363
 
347
364
  # Validate against output schema if defined
348
- if self.class._output_schema
349
- # TODO: Add JSON Schema validation here
350
- # For now, just ensure it's a hash/object
351
- unless content.is_a?(Hash)
352
- raise ArgumentError, "Structured content must be a hash/object when output_schema is defined"
353
- end
365
+ # TODO: Add JSON Schema validation here
366
+ # For now, just ensure it's a hash/object
367
+ if self.class._output_schema && !content.is_a?(Hash)
368
+ raise ArgumentError, "Structured content must be a hash/object when output_schema is defined"
354
369
  end
355
370
 
356
371
  @response.set_structured_content(content)
@@ -408,30 +423,26 @@ module ActionMCP
408
423
  def validate_number_parameter(key, value)
409
424
  return if value.is_a?(Numeric)
410
425
 
411
- if value.is_a?(String)
412
- # Check if string can be converted to a valid number
413
- begin
414
- Float(value)
415
- rescue ArgumentError, TypeError
416
- raise ArgumentError, "Parameter '#{key}' must be a valid number, got: #{value.inspect}"
417
- end
418
- else
419
- raise ArgumentError, "Parameter '#{key}' must be a number, got: #{value.class}"
426
+ raise ArgumentError, "Parameter '#{key}' must be a number, got: #{value.class}" unless value.is_a?(String)
427
+
428
+ # Check if string can be converted to a valid number
429
+ begin
430
+ Float(value)
431
+ rescue ArgumentError, TypeError
432
+ raise ArgumentError, "Parameter '#{key}' must be a valid number, got: #{value.inspect}"
420
433
  end
421
434
  end
422
435
 
423
436
  def validate_integer_parameter(key, value)
424
437
  return if value.is_a?(Integer)
425
438
 
426
- if value.is_a?(String)
427
- # Check if string can be converted to a valid integer
428
- begin
429
- Integer(value)
430
- rescue ArgumentError, TypeError
431
- raise ArgumentError, "Parameter '#{key}' must be a valid integer, got: #{value.inspect}"
432
- end
433
- else
434
- raise ArgumentError, "Parameter '#{key}' must be an integer, got: #{value.class}"
439
+ raise ArgumentError, "Parameter '#{key}' must be an integer, got: #{value.class}" unless value.is_a?(String)
440
+
441
+ # Check if string can be converted to a valid integer
442
+ begin
443
+ Integer(value)
444
+ rescue ArgumentError, TypeError
445
+ raise ArgumentError, "Parameter '#{key}' must be a valid integer, got: #{value.inspect}"
435
446
  end
436
447
  end
437
448
 
@@ -447,7 +458,7 @@ module ActionMCP
447
458
  raise ArgumentError, "Parameter '#{key}' must be a boolean, got: #{value.class}"
448
459
  end
449
460
 
450
- def validate_array_parameter(key, value, property_schema)
461
+ def validate_array_parameter(key, value, _property_schema)
451
462
  return if value.is_a?(Array)
452
463
 
453
464
  raise ArgumentError, "Parameter '#{key}' must be an array, got: #{value.class}"
@@ -25,10 +25,12 @@ module ActionMCP
25
25
  when "nan"
26
26
  Float::NAN
27
27
  else
28
- Float(v) rescue nil
28
+ begin
29
+ Float(v)
30
+ rescue StandardError
31
+ nil
32
+ end
29
33
  end
30
- else
31
- nil
32
34
  end
33
35
  end.compact
34
36
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.71.1"
5
+ VERSION = "0.72.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -45,7 +45,7 @@ module ActionMCP
45
45
  ].freeze
46
46
 
47
47
  LATEST_VERSION = SUPPORTED_VERSIONS.first.freeze
48
- DEFAULT_PROTOCOL_VERSION = "2025-03-26".freeze # Default to initial stable version for backwards compatibility
48
+ DEFAULT_PROTOCOL_VERSION = "2025-03-26" # Default to initial stable version for backwards compatibility
49
49
  class << self
50
50
  # Returns a Rack-compatible application for serving MCP requests
51
51
  # This makes ActionMCP.server work similar to ActionCable.server
@@ -31,6 +31,7 @@ class ApplicationGateway < ActionMCP::Gateway
31
31
  # Example method to resolve user from JWT payload
32
32
  def resolve_user(payload)
33
33
  return nil unless payload.is_a?(Hash)
34
+
34
35
  user_id = payload["user_id"] || payload["sub"]
35
36
  return nil unless user_id
36
37
 
@@ -175,7 +175,7 @@ namespace :action_mcp do
175
175
  # Authentication
176
176
  puts "\n\e[36mAuthentication:\e[0m"
177
177
  puts " Methods: #{config.authentication_methods.join(', ')}"
178
- if config.oauth_config && config.oauth_config.any?
178
+ if config.oauth_config&.any?
179
179
  puts " OAuth Provider: #{config.oauth_config['provider']}"
180
180
  puts " OAuth Scopes: #{config.oauth_config['scopes_supported']&.join(', ')}"
181
181
  end
@@ -236,7 +236,7 @@ namespace :action_mcp do
236
236
  total_sessions = ActionMCP::Session.count
237
237
  puts " Total Sessions: #{total_sessions}"
238
238
 
239
- if total_sessions > 0
239
+ if total_sessions.positive?
240
240
  # Sessions by status
241
241
  sessions_by_status = ActionMCP::Session.group(:status).count
242
242
  puts " Sessions by Status:"
@@ -282,7 +282,7 @@ namespace :action_mcp do
282
282
  total_messages = ActionMCP::Session::Message.count
283
283
  puts " Total Messages: #{total_messages}"
284
284
 
285
- if total_messages > 0
285
+ if total_messages.positive?
286
286
  # Messages by direction
287
287
  messages_by_direction = ActionMCP::Session::Message.group(:direction).count
288
288
  puts " Messages by Direction:"
@@ -291,7 +291,9 @@ namespace :action_mcp do
291
291
  end
292
292
 
293
293
  # Messages by type
294
- messages_by_type = ActionMCP::Session::Message.group(:message_type).count.sort_by { |_type, count| -count }.first(10)
294
+ messages_by_type = ActionMCP::Session::Message.group(:message_type).count.sort_by do |_type, count|
295
+ -count
296
+ end.first(10)
295
297
  puts " Top Message Types:"
296
298
  messages_by_type.each do |type, count|
297
299
  puts " #{type}: #{count}"
@@ -316,7 +318,7 @@ namespace :action_mcp do
316
318
  total_events = ActionMCP::Session::SSEEvent.count
317
319
  puts " Total SSE Events: #{total_events}"
318
320
 
319
- if total_events > 0
321
+ if total_events.positive?
320
322
  # Recent events
321
323
  recent_events = ActionMCP::Session::SSEEvent.where("created_at > ?", 1.hour.ago).count
322
324
  puts " SSE Events (Last Hour): #{recent_events}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.71.1
4
+ version: 0.72.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: 0.5.3
54
+ - !ruby/object:Gem::Dependency
55
+ name: jwt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.10'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.10'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: multi_json
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -93,20 +107,6 @@ dependencies:
93
107
  - - "~>"
94
108
  - !ruby/object:Gem::Version
95
109
  version: '2.6'
96
- - !ruby/object:Gem::Dependency
97
- name: jwt
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '2.10'
103
- type: :runtime
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '2.10'
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: omniauth
112
112
  requirement: !ruby/object:Gem::Requirement
@@ -223,6 +223,7 @@ files:
223
223
  - db/migrate/20250608112101_add_oauth_to_sessions.rb
224
224
  - db/migrate/20250708105124_create_action_mcp_oauth_clients.rb
225
225
  - db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb
226
+ - db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb
226
227
  - exe/actionmcp_cli
227
228
  - lib/action_mcp.rb
228
229
  - lib/action_mcp/base_response.rb
@@ -303,6 +304,8 @@ files:
303
304
  - lib/action_mcp/server.rb
304
305
  - lib/action_mcp/server/active_record_session_store.rb
305
306
  - lib/action_mcp/server/base_messaging.rb
307
+ - lib/action_mcp/server/base_session.rb
308
+ - lib/action_mcp/server/base_session_store.rb
306
309
  - lib/action_mcp/server/capabilities.rb
307
310
  - lib/action_mcp/server/configuration.rb
308
311
  - lib/action_mcp/server/elicitation.rb
@@ -310,11 +313,10 @@ files:
310
313
  - lib/action_mcp/server/error_handling.rb
311
314
  - lib/action_mcp/server/handlers/prompt_handler.rb
312
315
  - lib/action_mcp/server/handlers/resource_handler.rb
316
+ - lib/action_mcp/server/handlers/router.rb
313
317
  - lib/action_mcp/server/handlers/tool_handler.rb
314
318
  - lib/action_mcp/server/json_rpc_handler.rb
315
- - lib/action_mcp/server/memory_session.rb
316
- - lib/action_mcp/server/messaging.rb
317
- - lib/action_mcp/server/notifications.rb
319
+ - lib/action_mcp/server/messaging_service.rb
318
320
  - lib/action_mcp/server/prompts.rb
319
321
  - lib/action_mcp/server/registry_management.rb
320
322
  - lib/action_mcp/server/resources.rb