actionmcp 0.51.0 → 0.52.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.
- checksums.yaml +4 -4
- data/README.md +83 -0
- data/app/controllers/action_mcp/application_controller.rb +12 -6
- data/lib/action_mcp/client/session_store.rb +117 -26
- 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 +25 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 123d51b56f85e45cb622885fb6bfcf6caf6d310292302a6ad4a9886e84c36af2
|
4
|
+
data.tar.gz: 15eb5bd01e1985a4eac9c6d83b463d0f939c02ccd330f72366fa0d08603c6f2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 724a4dc93cc887ce3dc735fe6a00b375a36012887f2aa081b01a45b314460a297ebd3b17cfa423a55276462cca5736feea816a8691fcc0b6006bfb7a385b0f9b
|
7
|
+
data.tar.gz: 451af9c7919a5e65d80af1f3cc3b36e666148bc4b1351049a7ae33e01fd26daaad13b5d72b6a32476e9709c8b44a0291c2ad304f25d9607c99082bf59d99db02
|
data/README.md
CHANGED
@@ -380,6 +380,89 @@ This will create `config/mcp.yml` with example configurations for all environmen
|
|
380
380
|
|
381
381
|
> **Note:** Authentication and authorization are not included. You are responsible for securing the endpoint.
|
382
382
|
|
383
|
+
## Authentication with Gateway
|
384
|
+
|
385
|
+
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.
|
386
|
+
|
387
|
+
### Creating an ApplicationGateway
|
388
|
+
|
389
|
+
When you run the install generator, it creates an `ApplicationGateway` class:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
# app/mcp/application_gateway.rb
|
393
|
+
class ApplicationGateway < ActionMCP::Gateway
|
394
|
+
# Specify what attributes identify a connection
|
395
|
+
identified_by :user
|
396
|
+
|
397
|
+
protected
|
398
|
+
|
399
|
+
def authenticate!
|
400
|
+
token = extract_bearer_token
|
401
|
+
raise ActionMCP::UnauthorizedError, "Missing token" unless token
|
402
|
+
|
403
|
+
payload = ActionMCP::JwtDecoder.decode(token)
|
404
|
+
user = resolve_user(payload)
|
405
|
+
|
406
|
+
raise ActionMCP::UnauthorizedError, "Unauthorized" unless user
|
407
|
+
|
408
|
+
# Return a hash with all identified_by attributes
|
409
|
+
{ user: user }
|
410
|
+
end
|
411
|
+
|
412
|
+
private
|
413
|
+
|
414
|
+
def resolve_user(payload)
|
415
|
+
user_id = payload["user_id"] || payload["sub"]
|
416
|
+
User.find_by(id: user_id) if user_id
|
417
|
+
end
|
418
|
+
end
|
419
|
+
```
|
420
|
+
|
421
|
+
### Using Multiple Identifiers
|
422
|
+
|
423
|
+
You can identify connections by multiple attributes:
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
class ApplicationGateway < ActionMCP::Gateway
|
427
|
+
identified_by :user, :organization
|
428
|
+
|
429
|
+
protected
|
430
|
+
|
431
|
+
def authenticate!
|
432
|
+
# ... authentication logic ...
|
433
|
+
|
434
|
+
{
|
435
|
+
user: user,
|
436
|
+
organization: user.organization
|
437
|
+
}
|
438
|
+
end
|
439
|
+
end
|
440
|
+
```
|
441
|
+
|
442
|
+
### Accessing Current User in Components
|
443
|
+
|
444
|
+
Once authenticated, the current user (and other identifiers) are available in your tools, prompts, and resource templates:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
class MyTool < ApplicationMCPTool
|
448
|
+
def perform
|
449
|
+
# Access the authenticated user
|
450
|
+
if current_user
|
451
|
+
render text: "Hello, #{current_user.name}!"
|
452
|
+
else
|
453
|
+
render text: "Hi Stranger! It's been a while "
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
```
|
458
|
+
|
459
|
+
### Current Attributes
|
460
|
+
|
461
|
+
ActionMCP uses Rails' CurrentAttributes to store the authenticated context. The `ActionMCP::Current` class provides:
|
462
|
+
- `ActionMCP::Current.user` - The authenticated user
|
463
|
+
- `ActionMCP::Current.gateway` - The gateway instance
|
464
|
+
- Any other attributes you define with `identified_by`
|
465
|
+
|
383
466
|
### 1. Create `mcp.ru`
|
384
467
|
|
385
468
|
```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
|
@@ -31,41 +31,41 @@ module ActionMCP
|
|
31
31
|
|
32
32
|
session_data.merge!(attributes)
|
33
33
|
save_session(session_id, session_data)
|
34
|
-
|
34
|
+
# Return the reloaded session to get the actual saved values
|
35
|
+
load_session(session_id)
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
#
|
39
|
-
class
|
39
|
+
# Volatile session store for development (data lost on restart)
|
40
|
+
class VolatileSessionStore
|
40
41
|
include SessionStore
|
41
42
|
|
42
43
|
def initialize
|
43
|
-
@sessions =
|
44
|
-
@mutex = Mutex.new
|
44
|
+
@sessions = Concurrent::Hash.new
|
45
45
|
end
|
46
46
|
|
47
47
|
def load_session(session_id)
|
48
|
-
@
|
48
|
+
@sessions[session_id]
|
49
49
|
end
|
50
50
|
|
51
51
|
def save_session(session_id, session_data)
|
52
|
-
@
|
52
|
+
@sessions[session_id] = session_data.dup
|
53
53
|
end
|
54
54
|
|
55
55
|
def delete_session(session_id)
|
56
|
-
@
|
56
|
+
@sessions.delete(session_id)
|
57
57
|
end
|
58
58
|
|
59
59
|
def session_exists?(session_id)
|
60
|
-
@
|
60
|
+
@sessions.key?(session_id)
|
61
61
|
end
|
62
62
|
|
63
63
|
def clear_all
|
64
|
-
@
|
64
|
+
@sessions.clear
|
65
65
|
end
|
66
66
|
|
67
67
|
def session_count
|
68
|
-
@
|
68
|
+
@sessions.size
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
@@ -84,8 +84,6 @@ module ActionMCP
|
|
84
84
|
client_capabilities: session.client_capabilities,
|
85
85
|
server_info: session.server_info,
|
86
86
|
server_capabilities: session.server_capabilities,
|
87
|
-
last_event_id: session.last_event_id,
|
88
|
-
session_data: session.session_data || {},
|
89
87
|
created_at: session.created_at,
|
90
88
|
updated_at: session.updated_at
|
91
89
|
}
|
@@ -94,16 +92,18 @@ module ActionMCP
|
|
94
92
|
def save_session(session_id, session_data)
|
95
93
|
session = ActionMCP::Session.find_or_initialize_by(id: session_id)
|
96
94
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
106
105
|
|
106
|
+
session.assign_attributes(attributes)
|
107
107
|
session.save!
|
108
108
|
session_data
|
109
109
|
end
|
@@ -121,20 +121,111 @@ module ActionMCP
|
|
121
121
|
end
|
122
122
|
end
|
123
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
|
+
|
124
203
|
# Factory for creating session stores
|
125
204
|
class SessionStoreFactory
|
126
205
|
def self.create(type = nil, **options)
|
127
|
-
type ||=
|
206
|
+
type ||= default_type
|
128
207
|
|
129
208
|
case type.to_sym
|
130
|
-
when :memory
|
131
|
-
|
132
|
-
when :active_record
|
209
|
+
when :volatile, :memory
|
210
|
+
VolatileSessionStore.new
|
211
|
+
when :active_record, :persistent
|
133
212
|
ActiveRecordSessionStore.new
|
213
|
+
when :test
|
214
|
+
TestSessionStore.new
|
134
215
|
else
|
135
216
|
raise ArgumentError, "Unknown session store type: #{type}"
|
136
217
|
end
|
137
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
|
138
229
|
end
|
139
230
|
end
|
140
231
|
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
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class UnauthorizedError < StandardError; end
|
5
|
+
|
6
|
+
class Gateway
|
7
|
+
class << self
|
8
|
+
def identified_by(*attrs)
|
9
|
+
@identifiers ||= []
|
10
|
+
@identifiers.concat(attrs.map(&:to_sym)).uniq!
|
11
|
+
attr_accessor(*attrs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def identifiers
|
15
|
+
@identifiers ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
identified_by :user
|
20
|
+
|
21
|
+
attr_reader :request
|
22
|
+
|
23
|
+
def call(request)
|
24
|
+
@request = request
|
25
|
+
connect
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def connect
|
30
|
+
identities = authenticate!
|
31
|
+
reject_unauthorized_connection unless identities.is_a?(Hash)
|
32
|
+
|
33
|
+
# Assign all identities (e.g., :user, :account)
|
34
|
+
self.class.identifiers.each do |id|
|
35
|
+
value = identities[id]
|
36
|
+
reject_unauthorized_connection unless value
|
37
|
+
|
38
|
+
public_send("#{id}=", value)
|
39
|
+
|
40
|
+
# Set to ActionMCP::Current
|
41
|
+
ActionMCP::Current.public_send("#{id}=", value)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Also set the gateway instance itself
|
45
|
+
ActionMCP::Current.gateway = self
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def authenticate!
|
52
|
+
token = extract_bearer_token
|
53
|
+
raise UnauthorizedError, "Missing token" unless token
|
54
|
+
|
55
|
+
payload = ActionMCP::JwtDecoder.decode(token)
|
56
|
+
resolve_user(payload)
|
57
|
+
rescue ActionMCP::JwtDecoder::DecodeError => e
|
58
|
+
raise UnauthorizedError, e.message
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_bearer_token
|
62
|
+
header = request.headers["Authorization"] || request.headers["authorization"]
|
63
|
+
return nil unless header&.start_with?("Bearer ")
|
64
|
+
header.split(" ", 2).last
|
65
|
+
end
|
66
|
+
|
67
|
+
def resolve_user(payload)
|
68
|
+
return nil unless payload.is_a?(Hash)
|
69
|
+
user_id = payload["user_id"] || payload["sub"]
|
70
|
+
return nil unless user_id
|
71
|
+
user = User.find_by(id: user_id)
|
72
|
+
return nil unless user
|
73
|
+
|
74
|
+
# Return a hash with all identified_by attributes
|
75
|
+
self.class.identifiers.each_with_object({}) do |identifier, hash|
|
76
|
+
hash[identifier] = user if identifier == :user
|
77
|
+
# Add support for other identifiers as needed
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def reject_unauthorized_connection
|
82
|
+
raise UnauthorizedError, "Unauthorized"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -51,13 +51,18 @@ module ActionMCP
|
|
51
51
|
# @param rpc_method [String]
|
52
52
|
# @param id [String, Integer]
|
53
53
|
# @param params [Hash]
|
54
|
-
# @return [
|
54
|
+
# @return [JSON_RPC::Response, nil] Response if handled, nil otherwise
|
55
55
|
def handle_common_methods(rpc_method, id, params)
|
56
56
|
case rpc_method
|
57
57
|
when Methods::PING
|
58
58
|
transport.send_pong(id)
|
59
|
+
# In return mode, get the response that was just created
|
60
|
+
transport.messaging_mode == :return ? transport.get_last_response : true
|
59
61
|
when %r{^notifications/}
|
60
62
|
process_notifications(rpc_method, params)
|
63
|
+
true
|
64
|
+
else
|
65
|
+
nil
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "jwt"
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class JwtDecoder
|
5
|
+
class DecodeError < StandardError; end
|
6
|
+
|
7
|
+
# Configurable defaults
|
8
|
+
class << self
|
9
|
+
attr_accessor :secret, :algorithm
|
10
|
+
|
11
|
+
def decode(token)
|
12
|
+
payload, _header = JWT.decode(token, secret, true, { algorithm: algorithm })
|
13
|
+
payload
|
14
|
+
rescue JWT::ExpiredSignature
|
15
|
+
raise DecodeError, "Token has expired"
|
16
|
+
rescue JWT::DecodeError => e
|
17
|
+
# Simplify the error message for invalid tokens
|
18
|
+
raise DecodeError, "Invalid token"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defaults (can be overridden in an initializer)
|
23
|
+
self.secret = ENV.fetch("ACTION_MCP_JWT_SECRET") { "change-me" }
|
24
|
+
self.algorithm = "HS256"
|
25
|
+
end
|
26
|
+
end
|
data/lib/action_mcp/prompt.rb
CHANGED
@@ -4,6 +4,7 @@ module ActionMCP
|
|
4
4
|
# Abstract base class for Prompts
|
5
5
|
class Prompt < Capability
|
6
6
|
include ActionMCP::Callbacks
|
7
|
+
include ActionMCP::CurrentHelpers
|
7
8
|
class_attribute :_argument_definitions, instance_accessor: false, default: []
|
8
9
|
|
9
10
|
# ---------------------------------------------------
|
@@ -24,7 +24,14 @@ module ActionMCP
|
|
24
24
|
def with_error_handling(request_id)
|
25
25
|
yield
|
26
26
|
rescue JSON_RPC::JsonRpcError => e
|
27
|
-
|
27
|
+
if transport.messaging_mode == :return
|
28
|
+
response = error_response(request_id, e)
|
29
|
+
transport.write_message(response)
|
30
|
+
response
|
31
|
+
else
|
32
|
+
transport.send_jsonrpc_response(request_id, error: e)
|
33
|
+
nil
|
34
|
+
end
|
28
35
|
end
|
29
36
|
end
|
30
37
|
end
|
@@ -36,7 +36,8 @@ module ActionMCP
|
|
36
36
|
def handle_tools_call(id, params)
|
37
37
|
name = validate_required_param(params, "name", "Tool name is required")
|
38
38
|
arguments = extract_arguments(params)
|
39
|
-
|
39
|
+
_meta = params["_meta"] || params[:_meta] || {}
|
40
|
+
transport.send_tools_call(id, name, arguments, _meta)
|
40
41
|
end
|
41
42
|
|
42
43
|
def extract_arguments(params)
|
@@ -31,11 +31,18 @@ module ActionMCP
|
|
31
31
|
rpc_method = request.method
|
32
32
|
params = request.params
|
33
33
|
|
34
|
-
with_error_handling(id) do
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
result = with_error_handling(id) do
|
35
|
+
common_result = handle_common_methods(rpc_method, id, params)
|
36
|
+
if common_result
|
37
|
+
common_result
|
38
|
+
else
|
39
|
+
route_to_handler(rpc_method, id, params)
|
40
|
+
# In return mode, get the last response that was collected
|
41
|
+
transport.messaging_mode == :return ? transport.get_last_response : nil
|
42
|
+
end
|
38
43
|
end
|
44
|
+
|
45
|
+
result
|
39
46
|
end
|
40
47
|
|
41
48
|
def route_to_handler(rpc_method, id, params)
|
@@ -64,6 +71,7 @@ module ActionMCP
|
|
64
71
|
params = notification.params || {}
|
65
72
|
|
66
73
|
process_notifications(method_name, params)
|
74
|
+
# Notifications don't expect a response
|
67
75
|
nil
|
68
76
|
end
|
69
77
|
|