actionmcp 0.51.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 +4 -4
- data/README.md +192 -0
- data/app/controllers/action_mcp/application_controller.rb +12 -6
- data/lib/action_mcp/client/active_record_session_store.rb +57 -0
- data/lib/action_mcp/client/session_store.rb +2 -103
- data/lib/action_mcp/client/session_store_factory.rb +36 -0
- data/lib/action_mcp/client/test_session_store.rb +84 -0
- data/lib/action_mcp/client/volatile_session_store.rb +38 -0
- data/lib/action_mcp/configuration.rb +16 -1
- data/lib/action_mcp/current.rb +19 -0
- data/lib/action_mcp/current_helpers.rb +19 -0
- data/lib/action_mcp/gateway.rb +85 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +6 -1
- data/lib/action_mcp/jwt_decoder.rb +26 -0
- data/lib/action_mcp/prompt.rb +1 -0
- data/lib/action_mcp/resource_template.rb +1 -0
- data/lib/action_mcp/server/base_messaging.rb +14 -0
- data/lib/action_mcp/server/error_aware.rb +8 -1
- data/lib/action_mcp/server/handlers/tool_handler.rb +2 -1
- data/lib/action_mcp/server/json_rpc_handler.rb +12 -4
- data/lib/action_mcp/server/messaging.rb +12 -1
- data/lib/action_mcp/server/registry_management.rb +0 -1
- data/lib/action_mcp/server/response_collector.rb +40 -0
- data/lib/action_mcp/server/session_store.rb +762 -0
- data/lib/action_mcp/server/tools.rb +14 -3
- data/lib/action_mcp/server/transport_handler.rb +9 -5
- data/lib/action_mcp/server.rb +7 -0
- data/lib/action_mcp/tagged_stream_logging.rb +0 -4
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +105 -0
- data/lib/action_mcp/test_helper/session_store_assertions.rb +130 -0
- data/lib/action_mcp/test_helper.rb +4 -0
- data/lib/action_mcp/tool.rb +1 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +0 -1
- data/lib/generators/action_mcp/install/install_generator.rb +4 -0
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +40 -0
- metadata +29 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba6892aa3c28876f79be1ab3b365b2ed70b449e6f55781de9ea565e3f26d4f8e
|
4
|
+
data.tar.gz: ba861b9e0ec4dcdfcf743a70e8fed177a98a169c86c56f9f5d7b50c4bf7bf7b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
@@ -380,6 +489,89 @@ This will create `config/mcp.yml` with example configurations for all environmen
|
|
380
489
|
|
381
490
|
> **Note:** Authentication and authorization are not included. You are responsible for securing the endpoint.
|
382
491
|
|
492
|
+
## Authentication with Gateway
|
493
|
+
|
494
|
+
ActionMCP provides a Gateway system similar to ActionCable's Connection for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
|
495
|
+
|
496
|
+
### Creating an ApplicationGateway
|
497
|
+
|
498
|
+
When you run the install generator, it creates an `ApplicationGateway` class:
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
# app/mcp/application_gateway.rb
|
502
|
+
class ApplicationGateway < ActionMCP::Gateway
|
503
|
+
# Specify what attributes identify a connection
|
504
|
+
identified_by :user
|
505
|
+
|
506
|
+
protected
|
507
|
+
|
508
|
+
def authenticate!
|
509
|
+
token = extract_bearer_token
|
510
|
+
raise ActionMCP::UnauthorizedError, "Missing token" unless token
|
511
|
+
|
512
|
+
payload = ActionMCP::JwtDecoder.decode(token)
|
513
|
+
user = resolve_user(payload)
|
514
|
+
|
515
|
+
raise ActionMCP::UnauthorizedError, "Unauthorized" unless user
|
516
|
+
|
517
|
+
# Return a hash with all identified_by attributes
|
518
|
+
{ user: user }
|
519
|
+
end
|
520
|
+
|
521
|
+
private
|
522
|
+
|
523
|
+
def resolve_user(payload)
|
524
|
+
user_id = payload["user_id"] || payload["sub"]
|
525
|
+
User.find_by(id: user_id) if user_id
|
526
|
+
end
|
527
|
+
end
|
528
|
+
```
|
529
|
+
|
530
|
+
### Using Multiple Identifiers
|
531
|
+
|
532
|
+
You can identify connections by multiple attributes:
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
class ApplicationGateway < ActionMCP::Gateway
|
536
|
+
identified_by :user, :organization
|
537
|
+
|
538
|
+
protected
|
539
|
+
|
540
|
+
def authenticate!
|
541
|
+
# ... authentication logic ...
|
542
|
+
|
543
|
+
{
|
544
|
+
user: user,
|
545
|
+
organization: user.organization
|
546
|
+
}
|
547
|
+
end
|
548
|
+
end
|
549
|
+
```
|
550
|
+
|
551
|
+
### Accessing Current User in Components
|
552
|
+
|
553
|
+
Once authenticated, the current user (and other identifiers) are available in your tools, prompts, and resource templates:
|
554
|
+
|
555
|
+
```ruby
|
556
|
+
class MyTool < ApplicationMCPTool
|
557
|
+
def perform
|
558
|
+
# Access the authenticated user
|
559
|
+
if current_user
|
560
|
+
render text: "Hello, #{current_user.name}!"
|
561
|
+
else
|
562
|
+
render text: "Hi Stranger! It's been a while "
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
```
|
567
|
+
|
568
|
+
### Current Attributes
|
569
|
+
|
570
|
+
ActionMCP uses Rails' CurrentAttributes to store the authenticated context. The `ActionMCP::Current` class provides:
|
571
|
+
- `ActionMCP::Current.user` - The authenticated user
|
572
|
+
- `ActionMCP::Current.gateway` - The gateway instance
|
573
|
+
- Any other attributes you define with `identified_by`
|
574
|
+
|
383
575
|
### 1. Create `mcp.ru`
|
384
576
|
|
385
577
|
```ruby
|
@@ -158,11 +158,11 @@ module ActionMCP
|
|
158
158
|
response.headers[MCP_SESSION_ID_HEADER] = session.id
|
159
159
|
end
|
160
160
|
|
161
|
-
|
161
|
+
# Use return mode for the transport handler when we need to capture responses
|
162
|
+
transport_handler = Server::TransportHandler.new(session, messaging_mode: :return)
|
162
163
|
json_rpc_handler = Server::JsonRpcHandler.new(transport_handler)
|
163
164
|
|
164
165
|
result = json_rpc_handler.call(jsonrpc_params)
|
165
|
-
|
166
166
|
process_handler_results(result, session, session_initially_missing, is_initialize_request)
|
167
167
|
rescue ActionController::Live::ClientDisconnected, IOError => e
|
168
168
|
Rails.logger.debug "Unified SSE (POST): Client disconnected during response: #{e.message}"
|
@@ -182,7 +182,7 @@ module ActionMCP
|
|
182
182
|
session_id_from_header = extract_session_id
|
183
183
|
return render_bad_request("Mcp-Session-Id header is required for DELETE requests.") unless session_id_from_header
|
184
184
|
|
185
|
-
session =
|
185
|
+
session = Server.session_store.load_session(session_id_from_header)
|
186
186
|
if session.nil?
|
187
187
|
return render_not_found("Session not found.")
|
188
188
|
elsif session.status == "closed"
|
@@ -206,7 +206,7 @@ module ActionMCP
|
|
206
206
|
def find_or_initialize_session
|
207
207
|
session_id = extract_session_id
|
208
208
|
if session_id
|
209
|
-
session =
|
209
|
+
session = Server.session_store.load_session(session_id)
|
210
210
|
if session
|
211
211
|
if ActionMCP.configuration.vibed_ignore_version
|
212
212
|
if session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
|
@@ -218,7 +218,7 @@ module ActionMCP
|
|
218
218
|
end
|
219
219
|
session
|
220
220
|
else
|
221
|
-
|
221
|
+
Server.session_store.create_session(nil, protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
|
222
222
|
end
|
223
223
|
end
|
224
224
|
|
@@ -266,7 +266,13 @@ module ActionMCP
|
|
266
266
|
end
|
267
267
|
|
268
268
|
# Convert to hash for rendering
|
269
|
-
payload = result.
|
269
|
+
payload = if result.respond_to?(:to_h)
|
270
|
+
result.to_h
|
271
|
+
elsif result.respond_to?(:to_json)
|
272
|
+
JSON.parse(result.to_json)
|
273
|
+
else
|
274
|
+
result
|
275
|
+
end
|
270
276
|
|
271
277
|
# Determine response format
|
272
278
|
server_preference = ActionMCP.configuration.post_response_preference
|
@@ -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
|
@@ -31,109 +31,8 @@ module ActionMCP
|
|
31
31
|
|
32
32
|
session_data.merge!(attributes)
|
33
33
|
save_session(session_id, session_data)
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
# In-memory session store for development/testing
|
39
|
-
class MemorySessionStore
|
40
|
-
include SessionStore
|
41
|
-
|
42
|
-
def initialize
|
43
|
-
@sessions = {}
|
44
|
-
@mutex = Mutex.new
|
45
|
-
end
|
46
|
-
|
47
|
-
def load_session(session_id)
|
48
|
-
@mutex.synchronize { @sessions[session_id] }
|
49
|
-
end
|
50
|
-
|
51
|
-
def save_session(session_id, session_data)
|
52
|
-
@mutex.synchronize { @sessions[session_id] = session_data.dup }
|
53
|
-
end
|
54
|
-
|
55
|
-
def delete_session(session_id)
|
56
|
-
@mutex.synchronize { @sessions.delete(session_id) }
|
57
|
-
end
|
58
|
-
|
59
|
-
def session_exists?(session_id)
|
60
|
-
@mutex.synchronize { @sessions.key?(session_id) }
|
61
|
-
end
|
62
|
-
|
63
|
-
def clear_all
|
64
|
-
@mutex.synchronize { @sessions.clear }
|
65
|
-
end
|
66
|
-
|
67
|
-
def session_count
|
68
|
-
@mutex.synchronize { @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
|
-
last_event_id: session.last_event_id,
|
88
|
-
session_data: session.session_data || {},
|
89
|
-
created_at: session.created_at,
|
90
|
-
updated_at: session.updated_at
|
91
|
-
}
|
92
|
-
end
|
93
|
-
|
94
|
-
def save_session(session_id, session_data)
|
95
|
-
session = ActionMCP::Session.find_or_initialize_by(id: session_id)
|
96
|
-
|
97
|
-
session.assign_attributes(
|
98
|
-
protocol_version: session_data[:protocol_version],
|
99
|
-
client_info: session_data[:client_info],
|
100
|
-
client_capabilities: session_data[:client_capabilities],
|
101
|
-
server_info: session_data[:server_info],
|
102
|
-
server_capabilities: session_data[:server_capabilities],
|
103
|
-
last_event_id: session_data[:last_event_id],
|
104
|
-
session_data: session_data[:session_data] || {}
|
105
|
-
)
|
106
|
-
|
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
|
-
# Factory for creating session stores
|
125
|
-
class SessionStoreFactory
|
126
|
-
def self.create(type = nil, **options)
|
127
|
-
type ||= Rails.env.production? ? :active_record : :memory
|
128
|
-
|
129
|
-
case type.to_sym
|
130
|
-
when :memory
|
131
|
-
MemorySessionStore.new
|
132
|
-
when :active_record
|
133
|
-
ActiveRecordSessionStore.new
|
134
|
-
else
|
135
|
-
raise ArgumentError, "Unknown session store type: #{type}"
|
136
|
-
end
|
34
|
+
# Return the reloaded session to get the actual saved values
|
35
|
+
load_session(session_id)
|
137
36
|
end
|
138
37
|
end
|
139
38
|
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
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "gateway"
|
4
|
+
require "active_support/core_ext/integer/time"
|
5
|
+
|
3
6
|
module ActionMCP
|
4
7
|
# Configuration class to hold settings for the ActionMCP server.
|
5
8
|
class Configuration
|
@@ -30,7 +33,12 @@ module ActionMCP
|
|
30
33
|
:vibed_ignore_version,
|
31
34
|
# --- SSE Resumability Options ---
|
32
35
|
:sse_event_retention_period,
|
33
|
-
:max_stored_sse_events
|
36
|
+
:max_stored_sse_events,
|
37
|
+
# --- Gateway Options ---
|
38
|
+
:gateway_class,
|
39
|
+
:current_class,
|
40
|
+
# --- Session Store Options ---
|
41
|
+
:session_store_type
|
34
42
|
|
35
43
|
def initialize
|
36
44
|
@logging_enabled = true
|
@@ -47,6 +55,13 @@ module ActionMCP
|
|
47
55
|
# Resumability defaults
|
48
56
|
@sse_event_retention_period = 15.minutes
|
49
57
|
@max_stored_sse_events = 100
|
58
|
+
|
59
|
+
# Gateway - default to ApplicationGateway if it exists, otherwise ActionMCP::Gateway
|
60
|
+
@gateway_class = defined?(::ApplicationGateway) ? ::ApplicationGateway : ActionMCP::Gateway
|
61
|
+
@current_class = nil
|
62
|
+
|
63
|
+
# Session Store
|
64
|
+
@session_store_type = Rails.env.production? ? :active_record : :volatile
|
50
65
|
end
|
51
66
|
|
52
67
|
def name
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class Current < ActiveSupport::CurrentAttributes
|
5
|
+
attribute :user
|
6
|
+
attribute :gateway
|
7
|
+
|
8
|
+
def user=(user)
|
9
|
+
super
|
10
|
+
set_user_time_zone if user.respond_to?(:time_zone)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def set_user_time_zone
|
16
|
+
Time.zone = user.time_zone
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module CurrentHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
# Access the current user from ActionMCP::Current
|
10
|
+
def current_user
|
11
|
+
ActionMCP::Current.user
|
12
|
+
end
|
13
|
+
|
14
|
+
# Access the current gateway from ActionMCP::Current
|
15
|
+
def current_gateway
|
16
|
+
ActionMCP::Current.gateway
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|