actionmcp 0.52.1 → 0.53.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.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ class SessionStoreFactory
6
+ def self.create(type = nil, **options)
7
+ type ||= default_type
8
+
9
+ case type.to_sym
10
+ when :volatile, :memory
11
+ VolatileSessionStore.new
12
+ when :active_record, :persistent
13
+ ActiveRecordSessionStore.new
14
+ when :test
15
+ TestSessionStore.new
16
+ else
17
+ raise ArgumentError, "Unknown session store type: #{type}"
18
+ end
19
+ end
20
+
21
+ def self.default_type
22
+ if Rails.env.test?
23
+ :volatile # Use volatile for tests unless explicitly using :test
24
+ elsif Rails.env.production?
25
+ :active_record
26
+ else
27
+ :volatile
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ # Test session store that tracks all operations for assertions
6
+ class TestSessionStore < VolatileSessionStore
7
+ attr_reader :operations, :created_sessions, :loaded_sessions,
8
+ :saved_sessions, :deleted_sessions, :notifications_sent
9
+
10
+ def initialize
11
+ super
12
+ @operations = Concurrent::Array.new
13
+ @created_sessions = Concurrent::Array.new
14
+ @loaded_sessions = Concurrent::Array.new
15
+ @saved_sessions = Concurrent::Array.new
16
+ @deleted_sessions = Concurrent::Array.new
17
+ @notifications_sent = Concurrent::Array.new
18
+ @notification_callbacks = Concurrent::Array.new
19
+ end
20
+
21
+ def create_session(session_id = nil, attributes = {})
22
+ session = super
23
+ @operations << { type: :create, session_id: session.id, attributes: attributes }
24
+ @created_sessions << session.id
25
+
26
+ # Hook into the session's write method to capture notifications
27
+ intercept_session_write(session)
28
+
29
+ session
30
+ end
31
+
32
+ def load_session(session_id)
33
+ session = super
34
+ @operations << { type: :load, session_id: session_id, found: !session.nil? }
35
+ @loaded_sessions << session_id if session
36
+
37
+ # Hook into the session's write method to capture notifications
38
+ intercept_session_write(session) if session
39
+
40
+ session
41
+ end
42
+
43
+ def save_session(session)
44
+ super
45
+ @operations << { type: :save, session_id: session.id }
46
+ @saved_sessions << session.id
47
+ end
48
+
49
+ def delete_session(session_id)
50
+ result = super
51
+ @operations << { type: :delete, session_id: session_id }
52
+ @deleted_sessions << session_id
53
+ result
54
+ end
55
+
56
+ def cleanup_expired_sessions(older_than: 24.hours.ago)
57
+ count = super
58
+ @operations << { type: :cleanup, older_than: older_than, count: count }
59
+ count
60
+ end
61
+
62
+ # Test helper methods
63
+ def session_created?(session_id)
64
+ @created_sessions.include?(session_id)
65
+ end
66
+
67
+ def session_loaded?(session_id)
68
+ @loaded_sessions.include?(session_id)
69
+ end
70
+
71
+ def session_saved?(session_id)
72
+ @saved_sessions.include?(session_id)
73
+ end
74
+
75
+ def session_deleted?(session_id)
76
+ @deleted_sessions.include?(session_id)
77
+ end
78
+
79
+ def operation_count(type = nil)
80
+ if type
81
+ @operations.count { |op| op[:type] == type }
82
+ else
83
+ @operations.size
84
+ end
85
+ end
86
+
87
+ # Notification tracking methods
88
+ def track_notification(notification)
89
+ @notifications_sent << notification
90
+ @notification_callbacks.each { |cb| cb.call(notification) }
91
+ end
92
+
93
+ def on_notification(&block)
94
+ @notification_callbacks << block
95
+ end
96
+
97
+ def notifications_for_token(token)
98
+ @notifications_sent.select do |n|
99
+ n.params[:progressToken] == token
100
+ end
101
+ end
102
+
103
+ def clear_notifications
104
+ @notifications_sent.clear
105
+ end
106
+
107
+ def reset_tracking!
108
+ @operations.clear
109
+ @created_sessions.clear
110
+ @loaded_sessions.clear
111
+ @saved_sessions.clear
112
+ @deleted_sessions.clear
113
+ @notifications_sent.clear
114
+ @notification_callbacks.clear
115
+ end
116
+
117
+ private
118
+
119
+ def intercept_session_write(session)
120
+ return unless session
121
+
122
+ # Skip if already intercepted
123
+ return if session.singleton_methods.include?(:write)
124
+
125
+ test_store = self
126
+
127
+ # Intercept write method to capture all notifications
128
+ original_write = session.method(:write)
129
+
130
+ session.define_singleton_method(:write) do |data|
131
+ # Track progress notifications before calling original write
132
+ if data.is_a?(JSON_RPC::Notification) && data.method == "notifications/progress"
133
+ test_store.track_notification(data)
134
+ end
135
+
136
+ original_write.call(data)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ # Volatile session store for development (data lost on restart)
6
+ class VolatileSessionStore
7
+ include SessionStore
8
+
9
+ def initialize
10
+ @sessions = Concurrent::Hash.new
11
+ end
12
+
13
+ def create_session(session_id = nil, attributes = {})
14
+ session_id ||= SecureRandom.hex(6)
15
+
16
+ session_data = {
17
+ id: session_id,
18
+ status: "pre_initialize",
19
+ initialized: false,
20
+ role: "server",
21
+ messages_count: 0,
22
+ sse_event_counter: 0,
23
+ created_at: Time.current,
24
+ updated_at: Time.current
25
+ }.merge(attributes)
26
+
27
+ session = MemorySession.new(session_data, self)
28
+
29
+ # Initialize server info and capabilities if server role
30
+ if session.role == "server"
31
+ session.server_info = {
32
+ name: ActionMCP.configuration.name,
33
+ version: ActionMCP.configuration.version
34
+ }
35
+ session.server_capabilities = ActionMCP.configuration.capabilities
36
+
37
+ # Initialize registries
38
+ session.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
39
+ session.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
40
+ session.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
41
+ end
42
+
43
+ @sessions[session_id] = session
44
+ session
45
+ end
46
+
47
+ def load_session(session_id)
48
+ session = @sessions[session_id]
49
+ if session
50
+ session.instance_variable_set(:@new_record, false)
51
+ end
52
+ session
53
+ end
54
+
55
+ def save_session(session)
56
+ @sessions[session.id] = session
57
+ end
58
+
59
+ def delete_session(session_id)
60
+ @sessions.delete(session_id)
61
+ end
62
+
63
+ def session_exists?(session_id)
64
+ @sessions.key?(session_id)
65
+ end
66
+
67
+ def find_sessions(criteria = {})
68
+ sessions = @sessions.values
69
+
70
+ # Filter by status
71
+ if criteria[:status]
72
+ sessions = sessions.select { |s| s.status == criteria[:status] }
73
+ end
74
+
75
+ # Filter by role
76
+ if criteria[:role]
77
+ sessions = sessions.select { |s| s.role == criteria[:role] }
78
+ end
79
+
80
+ sessions
81
+ end
82
+
83
+ def cleanup_expired_sessions(older_than: 24.hours.ago)
84
+ expired_ids = @sessions.select do |_id, session|
85
+ session.updated_at < older_than
86
+ end.keys
87
+
88
+ expired_ids.each { |id| @sessions.delete(id) }
89
+ expired_ids.count
90
+ end
91
+
92
+ def clear_all
93
+ @sessions.clear
94
+ end
95
+
96
+ def session_count
97
+ @sessions.size
98
+ end
99
+ end
100
+ end
101
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.52.1"
5
+ VERSION = "0.53.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -13,6 +13,10 @@ require "action_mcp/log_subscriber"
13
13
  require "action_mcp/engine"
14
14
  require "zeitwerk"
15
15
 
16
+ # OAuth 2.1 support via Omniauth
17
+ require "omniauth"
18
+ require "omniauth-oauth2"
19
+
16
20
  lib = File.dirname(__FILE__)
17
21
 
18
22
  Zeitwerk::Loader.for_gem.tap do |loader|
@@ -25,8 +29,9 @@ Zeitwerk::Loader.for_gem.tap do |loader|
25
29
 
26
30
  loader.inflector.inflect("action_mcp" => "ActionMCP")
27
31
  loader.inflector.inflect("sse_client" => "SSEClient")
28
- loader.inflector.inflect("sse_server" => "SSEServer")
29
32
  loader.inflector.inflect("sse_listener" => "SSEListener")
33
+ loader.inflector.inflect("oauth" => "OAuth")
34
+ loader.inflector.inflect("mcp_strategy" => "MCPStrategy")
30
35
  end.setup
31
36
 
32
37
  module ActionMCP
@@ -1,9 +1,41 @@
1
1
  # ActionMCP Configuration
2
- # This file contains configuration for the ActionMCP pub/sub system.
3
- # Different environments can use different adapters.
2
+ # This file contains configuration for the ActionMCP server including
3
+ # authentication, profiles, and pub/sub system settings.
4
4
 
5
5
  development:
6
- # In-memory adapter for development
6
+ # Authentication configuration - array of methods to try in order
7
+ authentication: ["none"] # No authentication required for development
8
+
9
+ # OAuth configuration (if using OAuth authentication)
10
+ # oauth:
11
+ # provider: "demo_oauth_provider"
12
+ # scopes_supported: ["mcp:tools", "mcp:resources", "mcp:prompts"]
13
+ # enable_dynamic_registration: true
14
+ # enable_token_revocation: true
15
+ # pkce_required: true
16
+
17
+ # MCP capability profiles
18
+ profiles:
19
+ primary:
20
+ tools: ["all"]
21
+ prompts: ["all"]
22
+ resources: ["all"]
23
+ options:
24
+ list_changed: false
25
+ logging_enabled: true
26
+ resources_subscribe: false
27
+
28
+ minimal:
29
+ tools: []
30
+ prompts: []
31
+ resources: []
32
+ options:
33
+ list_changed: false
34
+ logging_enabled: false
35
+ logging_level: :warn
36
+ resources_subscribe: false
37
+
38
+ # Pub/sub adapter configuration
7
39
  adapter: simple
8
40
  # Thread pool configuration (optional)
9
41
  # min_threads: 5 # Minimum number of threads in the pool
@@ -11,10 +43,46 @@ development:
11
43
  # max_queue: 100 # Maximum number of tasks that can be queued
12
44
 
13
45
  test:
46
+ # JWT authentication for testing
47
+ authentication: ["jwt"]
48
+
49
+ profiles:
50
+ primary:
51
+ tools: ["all"]
52
+ prompts: ["all"]
53
+ resources: ["all"]
54
+
14
55
  # Test adapter for testing
15
56
  adapter: test
16
57
 
17
58
  production:
59
+ # Multiple authentication methods - try OAuth first, fallback to JWT
60
+ authentication: ["oauth", "jwt"]
61
+
62
+ # OAuth configuration for production
63
+ oauth:
64
+ provider: "application_oauth_provider" # Your custom provider class
65
+ scopes_supported: ["mcp:tools", "mcp:resources", "mcp:prompts"]
66
+ enable_dynamic_registration: true
67
+ enable_token_revocation: true
68
+ pkce_required: true
69
+ # issuer_url: <%= ENV.fetch("OAUTH_ISSUER_URL") { "https://yourapp.com" } %>
70
+
71
+ profiles:
72
+ primary:
73
+ tools: ["all"]
74
+ prompts: ["all"]
75
+ resources: ["all"]
76
+ options:
77
+ list_changed: false
78
+ logging_enabled: true
79
+ resources_subscribe: false
80
+
81
+ external_clients:
82
+ tools: ["WeatherForecastTool"] # Limited tool access for external clients
83
+ prompts: []
84
+ resources: []
85
+
18
86
  # Choose one of the following adapters:
19
87
 
20
88
  # 1. Database-backed adapter (recommended)
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.52.1
4
+ version: 0.53.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -107,6 +107,76 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '2.10'
110
+ - !ruby/object:Gem::Dependency
111
+ name: omniauth
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.1'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.1'
124
+ - !ruby/object:Gem::Dependency
125
+ name: omniauth-oauth2
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.7'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '1.7'
138
+ - !ruby/object:Gem::Dependency
139
+ name: ostruct
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: faraday
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '2.7'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '2.7'
166
+ - !ruby/object:Gem::Dependency
167
+ name: pkce_challenge
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.0'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.0'
110
180
  description: It offers base classes and helpers for creating MCP applications, making
111
181
  it easier to integrate your Ruby/Rails application with the MCP standard
112
182
  email:
@@ -120,6 +190,8 @@ files:
120
190
  - README.md
121
191
  - Rakefile
122
192
  - app/controllers/action_mcp/application_controller.rb
193
+ - app/controllers/action_mcp/oauth/endpoints_controller.rb
194
+ - app/controllers/action_mcp/oauth/metadata_controller.rb
123
195
  - app/models/action_mcp.rb
124
196
  - app/models/action_mcp/application_record.rb
125
197
  - app/models/action_mcp/session.rb
@@ -131,6 +203,7 @@ files:
131
203
  - app/models/concerns/mcp_message_inspect.rb
132
204
  - config/routes.rb
133
205
  - db/migrate/20250512154359_consolidated_migration.rb
206
+ - db/migrate/20250608112101_add_oauth_to_sessions.rb
134
207
  - exe/actionmcp_cli
135
208
  - lib/action_mcp.rb
136
209
  - lib/action_mcp/base_response.rb
@@ -145,6 +218,8 @@ files:
145
218
  - lib/action_mcp/client/json_rpc_handler.rb
146
219
  - lib/action_mcp/client/logging.rb
147
220
  - lib/action_mcp/client/messaging.rb
221
+ - lib/action_mcp/client/oauth_client_provider.rb
222
+ - lib/action_mcp/client/oauth_client_provider/memory_storage.rb
148
223
  - lib/action_mcp/client/prompt_book.rb
149
224
  - lib/action_mcp/client/prompts.rb
150
225
  - lib/action_mcp/client/request_timeouts.rb
@@ -181,6 +256,11 @@ files:
181
256
  - lib/action_mcp/jwt_decoder.rb
182
257
  - lib/action_mcp/log_subscriber.rb
183
258
  - lib/action_mcp/logging.rb
259
+ - lib/action_mcp/oauth/error.rb
260
+ - lib/action_mcp/oauth/memory_storage.rb
261
+ - lib/action_mcp/oauth/middleware.rb
262
+ - lib/action_mcp/oauth/provider.rb
263
+ - lib/action_mcp/omniauth/mcp_strategy.rb
184
264
  - lib/action_mcp/prompt.rb
185
265
  - lib/action_mcp/prompt_response.rb
186
266
  - lib/action_mcp/prompts_registry.rb
@@ -191,6 +271,7 @@ files:
191
271
  - lib/action_mcp/resource_template.rb
192
272
  - lib/action_mcp/resource_templates_registry.rb
193
273
  - lib/action_mcp/server.rb
274
+ - lib/action_mcp/server/active_record_session_store.rb
194
275
  - lib/action_mcp/server/base_messaging.rb
195
276
  - lib/action_mcp/server/capabilities.rb
196
277
  - lib/action_mcp/server/configuration.rb
@@ -200,6 +281,7 @@ files:
200
281
  - lib/action_mcp/server/handlers/resource_handler.rb
201
282
  - lib/action_mcp/server/handlers/tool_handler.rb
202
283
  - lib/action_mcp/server/json_rpc_handler.rb
284
+ - lib/action_mcp/server/memory_session.rb
203
285
  - lib/action_mcp/server/messaging.rb
204
286
  - lib/action_mcp/server/notifications.rb
205
287
  - lib/action_mcp/server/prompts.rb
@@ -210,10 +292,13 @@ files:
210
292
  - lib/action_mcp/server/sampling.rb
211
293
  - lib/action_mcp/server/sampling_request.rb
212
294
  - lib/action_mcp/server/session_store.rb
295
+ - lib/action_mcp/server/session_store_factory.rb
213
296
  - lib/action_mcp/server/simple_pub_sub.rb
214
297
  - lib/action_mcp/server/solid_cable_adapter.rb
298
+ - lib/action_mcp/server/test_session_store.rb
215
299
  - lib/action_mcp/server/tools.rb
216
300
  - lib/action_mcp/server/transport_handler.rb
301
+ - lib/action_mcp/server/volatile_session_store.rb
217
302
  - lib/action_mcp/sse_listener.rb
218
303
  - lib/action_mcp/string_array.rb
219
304
  - lib/action_mcp/tagged_stream_logging.rb