actionmcp 0.52.0 → 0.52.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 123d51b56f85e45cb622885fb6bfcf6caf6d310292302a6ad4a9886e84c36af2
4
- data.tar.gz: 15eb5bd01e1985a4eac9c6d83b463d0f939c02ccd330f72366fa0d08603c6f2b
3
+ metadata.gz: ba6892aa3c28876f79be1ab3b365b2ed70b449e6f55781de9ea565e3f26d4f8e
4
+ data.tar.gz: ba861b9e0ec4dcdfcf743a70e8fed177a98a169c86c56f9f5d7b50c4bf7bf7b6
5
5
  SHA512:
6
- metadata.gz: 724a4dc93cc887ce3dc735fe6a00b375a36012887f2aa081b01a45b314460a297ebd3b17cfa423a55276462cca5736feea816a8691fcc0b6006bfb7a385b0f9b
7
- data.tar.gz: 451af9c7919a5e65d80af1f3cc3b36e666148bc4b1351049a7ae33e01fd26daaad13b5d72b6a32476e9709c8b44a0291c2ad304f25d9607c99082bf59d99db02
6
+ metadata.gz: de7d1b0f9db37da9cc7be60ec42aac243ffb521cf878ba4c537269c0afeabdf22c5a2431b6eb5a1a00e79d574bdddbd444d63a1230543fb364b31d8d98ef36da
7
+ data.tar.gz: eafee3ce33017cfde2175f655caf5cc0e292b37a70427d36acf474e21503abea3ea71b124430c69cb8c29168f8993cac385f5f227c05863482a65e9b8f323e6e
data/README.md CHANGED
@@ -330,6 +330,115 @@ production:
330
330
  max_queue: 500 # Maximum number of tasks that can be queued
331
331
  ```
332
332
 
333
+ ## Session Storage
334
+
335
+ ActionMCP provides a pluggable session storage system that allows you to choose how sessions are persisted based on your environment and requirements.
336
+
337
+ ### Session Store Types
338
+
339
+ ActionMCP includes three session store implementations:
340
+
341
+ 1. **`:volatile`** - In-memory storage using Concurrent::Hash
342
+ - Default for development and test environments
343
+ - Sessions are lost on server restart
344
+ - Fast and lightweight for local development
345
+ - No external dependencies
346
+
347
+ 2. **`:active_record`** - Database-backed storage
348
+ - Default for production environment
349
+ - Sessions persist across server restarts
350
+ - Supports session resumability
351
+ - Requires database migrations
352
+
353
+ 3. **`:test`** - Special store for testing
354
+ - Tracks notifications and method calls
355
+ - Provides assertion helpers
356
+ - Automatically used in test environment when using TestHelper
357
+
358
+ ### Configuration
359
+
360
+ You can configure the session store type in your Rails configuration:
361
+
362
+ ```ruby
363
+ # config/application.rb or environment files
364
+ Rails.application.configure do
365
+ config.action_mcp.session_store_type = :active_record # or :volatile
366
+ end
367
+ ```
368
+
369
+ The defaults are:
370
+ - Production: `:active_record`
371
+ - Development: `:volatile`
372
+ - Test: `:volatile` (or `:test` when using TestHelper)
373
+
374
+ ### Using Different Session Stores
375
+
376
+ ```ruby
377
+ # The session store is automatically selected based on configuration
378
+ # You can access it directly if needed:
379
+ session_store = ActionMCP::Server.session_store
380
+
381
+ # Create a session
382
+ session = session_store.create_session(session_id, {
383
+ status: "initialized",
384
+ protocol_version: "2025-03-26",
385
+ # ... other session attributes
386
+ })
387
+
388
+ # Load a session
389
+ session = session_store.load_session(session_id)
390
+
391
+ # Update a session
392
+ session_store.update_session(session_id, { status: "active" })
393
+
394
+ # Delete a session
395
+ session_store.delete_session(session_id)
396
+ ```
397
+
398
+ ### Session Resumability
399
+
400
+ With the `:active_record` store, clients can resume sessions after disconnection:
401
+
402
+ ```ruby
403
+ # Client includes session ID in request headers
404
+ # Server automatically resumes the existing session
405
+ headers["Mcp-Session-Id"] = "existing-session-id"
406
+
407
+ # If the session exists, it will be resumed
408
+ # If not, a new session will be created
409
+ ```
410
+
411
+ ### Custom Session Stores
412
+
413
+ You can create custom session stores by inheriting from `ActionMCP::Server::SessionStore::Base`:
414
+
415
+ ```ruby
416
+ class MyCustomSessionStore < ActionMCP::Server::SessionStore::Base
417
+ def create_session(session_id, payload = {})
418
+ # Implementation
419
+ end
420
+
421
+ def load_session(session_id)
422
+ # Implementation
423
+ end
424
+
425
+ def update_session(session_id, updates)
426
+ # Implementation
427
+ end
428
+
429
+ def delete_session(session_id)
430
+ # Implementation
431
+ end
432
+
433
+ def exists?(session_id)
434
+ # Implementation
435
+ end
436
+ end
437
+
438
+ # Register your custom store
439
+ ActionMCP::Server.session_store = MyCustomSessionStore.new
440
+ ```
441
+
333
442
  ## Thread Pool Management
334
443
 
335
444
  ActionMCP uses thread pools to efficiently handle message callbacks. This prevents the system from being overwhelmed by too many threads under high load.
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Client
5
+ # ActiveRecord-backed session store for production
6
+ class ActiveRecordSessionStore
7
+ include SessionStore
8
+
9
+ def load_session(session_id)
10
+ session = ActionMCP::Session.find_by(id: session_id)
11
+ return nil unless session
12
+
13
+ {
14
+ id: session.id,
15
+ protocol_version: session.protocol_version,
16
+ client_info: session.client_info,
17
+ client_capabilities: session.client_capabilities,
18
+ server_info: session.server_info,
19
+ server_capabilities: session.server_capabilities,
20
+ created_at: session.created_at,
21
+ updated_at: session.updated_at
22
+ }
23
+ end
24
+
25
+ def save_session(session_id, session_data)
26
+ session = ActionMCP::Session.find_or_initialize_by(id: session_id)
27
+
28
+ # Only assign attributes that exist in the database
29
+ attributes = {}
30
+ attributes[:protocol_version] = session_data[:protocol_version] if session_data.key?(:protocol_version)
31
+ attributes[:client_info] = session_data[:client_info] if session_data.key?(:client_info)
32
+ attributes[:client_capabilities] = session_data[:client_capabilities] if session_data.key?(:client_capabilities)
33
+ attributes[:server_info] = session_data[:server_info] if session_data.key?(:server_info)
34
+ attributes[:server_capabilities] = session_data[:server_capabilities] if session_data.key?(:server_capabilities)
35
+
36
+ # Store any extra data in a jsonb column if available
37
+ # For now, we'll skip last_event_id and session_data as they don't exist in the DB
38
+
39
+ session.assign_attributes(attributes)
40
+ session.save!
41
+ session_data
42
+ end
43
+
44
+ def delete_session(session_id)
45
+ ActionMCP::Session.find_by(id: session_id)&.destroy
46
+ end
47
+
48
+ def session_exists?(session_id)
49
+ ActionMCP::Session.exists?(id: session_id)
50
+ end
51
+
52
+ def cleanup_expired_sessions(older_than: 24.hours.ago)
53
+ ActionMCP::Session.where("updated_at < ?", older_than).delete_all
54
+ end
55
+ end
56
+ end
57
+ end
@@ -35,197 +35,5 @@ module ActionMCP
35
35
  load_session(session_id)
36
36
  end
37
37
  end
38
-
39
- # Volatile session store for development (data lost on restart)
40
- class VolatileSessionStore
41
- include SessionStore
42
-
43
- def initialize
44
- @sessions = Concurrent::Hash.new
45
- end
46
-
47
- def load_session(session_id)
48
- @sessions[session_id]
49
- end
50
-
51
- def save_session(session_id, session_data)
52
- @sessions[session_id] = session_data.dup
53
- end
54
-
55
- def delete_session(session_id)
56
- @sessions.delete(session_id)
57
- end
58
-
59
- def session_exists?(session_id)
60
- @sessions.key?(session_id)
61
- end
62
-
63
- def clear_all
64
- @sessions.clear
65
- end
66
-
67
- def session_count
68
- @sessions.size
69
- end
70
- end
71
-
72
- # ActiveRecord-backed session store for production
73
- class ActiveRecordSessionStore
74
- include SessionStore
75
-
76
- def load_session(session_id)
77
- session = ActionMCP::Session.find_by(id: session_id)
78
- return nil unless session
79
-
80
- {
81
- id: session.id,
82
- protocol_version: session.protocol_version,
83
- client_info: session.client_info,
84
- client_capabilities: session.client_capabilities,
85
- server_info: session.server_info,
86
- server_capabilities: session.server_capabilities,
87
- created_at: session.created_at,
88
- updated_at: session.updated_at
89
- }
90
- end
91
-
92
- def save_session(session_id, session_data)
93
- session = ActionMCP::Session.find_or_initialize_by(id: session_id)
94
-
95
- # Only assign attributes that exist in the database
96
- attributes = {}
97
- attributes[:protocol_version] = session_data[:protocol_version] if session_data.key?(:protocol_version)
98
- attributes[:client_info] = session_data[:client_info] if session_data.key?(:client_info)
99
- attributes[:client_capabilities] = session_data[:client_capabilities] if session_data.key?(:client_capabilities)
100
- attributes[:server_info] = session_data[:server_info] if session_data.key?(:server_info)
101
- attributes[:server_capabilities] = session_data[:server_capabilities] if session_data.key?(:server_capabilities)
102
-
103
- # Store any extra data in a jsonb column if available
104
- # For now, we'll skip last_event_id and session_data as they don't exist in the DB
105
-
106
- session.assign_attributes(attributes)
107
- session.save!
108
- session_data
109
- end
110
-
111
- def delete_session(session_id)
112
- ActionMCP::Session.find_by(id: session_id)&.destroy
113
- end
114
-
115
- def session_exists?(session_id)
116
- ActionMCP::Session.exists?(id: session_id)
117
- end
118
-
119
- def cleanup_expired_sessions(older_than: 24.hours.ago)
120
- ActionMCP::Session.where("updated_at < ?", older_than).delete_all
121
- end
122
- end
123
-
124
- # Test session store that tracks all operations for assertions
125
- class TestSessionStore < VolatileSessionStore
126
- attr_reader :operations, :saved_sessions, :loaded_sessions,
127
- :deleted_sessions, :updated_sessions
128
-
129
- def initialize
130
- super
131
- @operations = Concurrent::Array.new
132
- @saved_sessions = Concurrent::Array.new
133
- @loaded_sessions = Concurrent::Array.new
134
- @deleted_sessions = Concurrent::Array.new
135
- @updated_sessions = Concurrent::Array.new
136
- end
137
-
138
- def load_session(session_id)
139
- session = super
140
- @operations << { type: :load, session_id: session_id, found: !session.nil? }
141
- @loaded_sessions << session_id if session
142
- session
143
- end
144
-
145
- def save_session(session_id, session_data)
146
- super
147
- @operations << { type: :save, session_id: session_id, data: session_data }
148
- @saved_sessions << session_id
149
- end
150
-
151
- def delete_session(session_id)
152
- result = super
153
- @operations << { type: :delete, session_id: session_id }
154
- @deleted_sessions << session_id
155
- result
156
- end
157
-
158
- def update_session(session_id, attributes)
159
- result = super
160
- @operations << { type: :update, session_id: session_id, attributes: attributes }
161
- @updated_sessions << session_id if result
162
- result
163
- end
164
-
165
- # Test helper methods
166
- def session_saved?(session_id)
167
- @saved_sessions.include?(session_id)
168
- end
169
-
170
- def session_loaded?(session_id)
171
- @loaded_sessions.include?(session_id)
172
- end
173
-
174
- def session_deleted?(session_id)
175
- @deleted_sessions.include?(session_id)
176
- end
177
-
178
- def session_updated?(session_id)
179
- @updated_sessions.include?(session_id)
180
- end
181
-
182
- def operation_count(type = nil)
183
- if type
184
- @operations.count { |op| op[:type] == type }
185
- else
186
- @operations.size
187
- end
188
- end
189
-
190
- def last_saved_data(session_id)
191
- @operations.reverse.find { |op| op[:type] == :save && op[:session_id] == session_id }&.dig(:data)
192
- end
193
-
194
- def reset_tracking!
195
- @operations.clear
196
- @saved_sessions.clear
197
- @loaded_sessions.clear
198
- @deleted_sessions.clear
199
- @updated_sessions.clear
200
- end
201
- end
202
-
203
- # Factory for creating session stores
204
- class SessionStoreFactory
205
- def self.create(type = nil, **options)
206
- type ||= default_type
207
-
208
- case type.to_sym
209
- when :volatile, :memory
210
- VolatileSessionStore.new
211
- when :active_record, :persistent
212
- ActiveRecordSessionStore.new
213
- when :test
214
- TestSessionStore.new
215
- else
216
- raise ArgumentError, "Unknown session store type: #{type}"
217
- end
218
- end
219
-
220
- def self.default_type
221
- if Rails.env.test?
222
- :volatile # Use volatile for tests unless explicitly using :test
223
- elsif Rails.env.production?
224
- :active_record
225
- else
226
- :volatile
227
- end
228
- end
229
- end
230
38
  end
231
39
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Client
5
+ # Factory for creating session stores
6
+ class SessionStoreFactory
7
+ def self.create(type = nil, **_options)
8
+ type ||= default_type
9
+
10
+ case type.to_sym
11
+ when :volatile, :memory
12
+ VolatileSessionStore.new
13
+ when :active_record, :persistent
14
+ ActiveRecordSessionStore.new
15
+ when :test
16
+ TestSessionStore.new
17
+ else
18
+ raise ArgumentError, "Unknown session store type: #{type}"
19
+ end
20
+ end
21
+
22
+ def self.default_type
23
+ # Ensure Rails is defined or provide a fallback if this code can run
24
+ # outside a Rails environment.
25
+ # Will refactor this soon
26
+ if defined?(Rails) && Rails.env.test?
27
+ :volatile # Use volatile for tests unless explicitly using :test
28
+ elsif defined?(Rails) && Rails.env.production?
29
+ :active_record
30
+ else
31
+ :volatile # Default for development or non-Rails environments
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Client
5
+ # Test session store that tracks all operations for assertions
6
+ class TestSessionStore < VolatileSessionStore
7
+ attr_reader :operations, :saved_sessions, :loaded_sessions,
8
+ :deleted_sessions, :updated_sessions
9
+
10
+ def initialize
11
+ super
12
+ @operations = Concurrent::Array.new
13
+ @saved_sessions = Concurrent::Array.new
14
+ @loaded_sessions = Concurrent::Array.new
15
+ @deleted_sessions = Concurrent::Array.new
16
+ @updated_sessions = Concurrent::Array.new
17
+ end
18
+
19
+ def load_session(session_id)
20
+ session = super
21
+ @operations << { type: :load, session_id: session_id, found: !session.nil? }
22
+ @loaded_sessions << session_id if session
23
+ session
24
+ end
25
+
26
+ def save_session(session_id, session_data)
27
+ super
28
+ @operations << { type: :save, session_id: session_id, data: session_data }
29
+ @saved_sessions << session_id
30
+ end
31
+
32
+ def delete_session(session_id)
33
+ result = super
34
+ @operations << { type: :delete, session_id: session_id }
35
+ @deleted_sessions << session_id
36
+ result
37
+ end
38
+
39
+ def update_session(session_id, attributes)
40
+ result = super
41
+ @operations << { type: :update, session_id: session_id, attributes: attributes }
42
+ @updated_sessions << session_id if result
43
+ result
44
+ end
45
+
46
+ # Test helper methods
47
+ def session_saved?(session_id)
48
+ @saved_sessions.include?(session_id)
49
+ end
50
+
51
+ def session_loaded?(session_id)
52
+ @loaded_sessions.include?(session_id)
53
+ end
54
+
55
+ def session_deleted?(session_id)
56
+ @deleted_sessions.include?(session_id)
57
+ end
58
+
59
+ def session_updated?(session_id)
60
+ @updated_sessions.include?(session_id)
61
+ end
62
+
63
+ def operation_count(type = nil)
64
+ if type
65
+ @operations.count { |op| op[:type] == type }
66
+ else
67
+ @operations.size
68
+ end
69
+ end
70
+
71
+ def last_saved_data(session_id)
72
+ @operations.reverse.find { |op| op[:type] == :save && op[:session_id] == session_id }&.dig(:data)
73
+ end
74
+
75
+ def reset_tracking!
76
+ @operations.clear
77
+ @saved_sessions.clear
78
+ @loaded_sessions.clear
79
+ @deleted_sessions.clear
80
+ @updated_sessions.clear
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Client
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 load_session(session_id)
14
+ @sessions[session_id]
15
+ end
16
+
17
+ def save_session(session_id, session_data)
18
+ @sessions[session_id] = session_data.dup
19
+ end
20
+
21
+ def delete_session(session_id)
22
+ @sessions.delete(session_id)
23
+ end
24
+
25
+ def session_exists?(session_id)
26
+ @sessions.key?(session_id)
27
+ end
28
+
29
+ def clear_all
30
+ @sessions.clear
31
+ end
32
+
33
+ def session_count
34
+ @sessions.size
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.52.0"
5
+ VERSION = "0.52.1"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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.0
4
+ version: 0.52.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -137,6 +137,7 @@ files:
137
137
  - lib/action_mcp/callbacks.rb
138
138
  - lib/action_mcp/capability.rb
139
139
  - lib/action_mcp/client.rb
140
+ - lib/action_mcp/client/active_record_session_store.rb
140
141
  - lib/action_mcp/client/base.rb
141
142
  - lib/action_mcp/client/blueprint.rb
142
143
  - lib/action_mcp/client/catalog.rb
@@ -151,11 +152,14 @@ files:
151
152
  - lib/action_mcp/client/roots.rb
152
153
  - lib/action_mcp/client/server.rb
153
154
  - lib/action_mcp/client/session_store.rb
155
+ - lib/action_mcp/client/session_store_factory.rb
154
156
  - lib/action_mcp/client/sse_client.rb
155
157
  - lib/action_mcp/client/streamable_http_transport.rb
158
+ - lib/action_mcp/client/test_session_store.rb
156
159
  - lib/action_mcp/client/toolbox.rb
157
160
  - lib/action_mcp/client/tools.rb
158
161
  - lib/action_mcp/client/transport.rb
162
+ - lib/action_mcp/client/volatile_session_store.rb
159
163
  - lib/action_mcp/configuration.rb
160
164
  - lib/action_mcp/console_detector.rb
161
165
  - lib/action_mcp/content.rb