actionmcp 0.71.0 → 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.
- checksums.yaml +4 -4
- data/README.md +186 -15
- data/app/controllers/action_mcp/application_controller.rb +47 -40
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
- data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
- data/app/models/action_mcp/oauth_client.rb +7 -5
- data/app/models/action_mcp/oauth_token.rb +2 -1
- data/app/models/action_mcp/session.rb +40 -5
- data/config/routes.rb +4 -2
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +12 -13
- data/lib/action_mcp/client/collection.rb +3 -3
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/jwt_client_provider.rb +6 -5
- data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
- data/lib/action_mcp/client/streamable_http_transport.rb +63 -27
- data/lib/action_mcp/client.rb +19 -4
- data/lib/action_mcp/configuration.rb +28 -53
- data/lib/action_mcp/engine.rb +5 -1
- data/lib/action_mcp/filtered_logger.rb +1 -1
- data/lib/action_mcp/gateway.rb +47 -137
- data/lib/action_mcp/gateway_identifier.rb +29 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/jwt_decoder.rb +4 -2
- data/lib/action_mcp/jwt_identifier.rb +28 -0
- data/lib/action_mcp/none_identifier.rb +19 -0
- data/lib/action_mcp/o_auth_identifier.rb +34 -0
- data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
- data/lib/action_mcp/oauth/memory_storage.rb +1 -3
- data/lib/action_mcp/oauth/middleware.rb +13 -18
- data/lib/action_mcp/oauth/provider.rb +45 -65
- data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +39 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +24 -18
- data/lib/action_mcp/server/notifications.rb +0 -58
@@ -41,14 +41,17 @@ module ActionMCP
|
|
41
41
|
|
42
42
|
# Scopes
|
43
43
|
scope :active, -> { where(active: true) }
|
44
|
-
scope :expired,
|
44
|
+
scope :expired, lambda {
|
45
|
+
where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ])
|
46
|
+
}
|
45
47
|
|
46
48
|
# Callbacks
|
47
49
|
before_create :set_issued_at
|
48
50
|
|
49
51
|
# Check if client secret is expired
|
50
52
|
def secret_expired?
|
51
|
-
return false if client_secret_expires_at.nil? || client_secret_expires_at
|
53
|
+
return false if client_secret_expires_at.nil? || client_secret_expires_at.zero?
|
54
|
+
|
52
55
|
Time.current.to_i > client_secret_expires_at
|
53
56
|
end
|
54
57
|
|
@@ -65,6 +68,7 @@ module ActionMCP
|
|
65
68
|
# Validate redirect URI against registered URIs
|
66
69
|
def valid_redirect_uri?(uri)
|
67
70
|
return false if redirect_uris.blank?
|
71
|
+
|
68
72
|
redirect_uris.include?(uri)
|
69
73
|
end
|
70
74
|
|
@@ -133,9 +137,7 @@ module ActionMCP
|
|
133
137
|
end
|
134
138
|
|
135
139
|
# Generate client secret for confidential clients
|
136
|
-
if client.confidential_client?
|
137
|
-
client.client_secret = SecureRandom.urlsafe_base64(32)
|
138
|
-
end
|
140
|
+
client.client_secret = SecureRandom.urlsafe_base64(32) if client.confidential_client?
|
139
141
|
|
140
142
|
# Store any additional metadata
|
141
143
|
known_fields = %w[
|
@@ -90,7 +90,8 @@ module ActionMCP
|
|
90
90
|
end
|
91
91
|
|
92
92
|
# Create authorization code
|
93
|
-
def self.create_authorization_code(client_id:, user_id:, redirect_uri:, scope:, code_challenge: nil,
|
93
|
+
def self.create_authorization_code(client_id:, user_id:, redirect_uri:, scope:, code_challenge: nil,
|
94
|
+
code_challenge_method: nil)
|
94
95
|
create!(
|
95
96
|
token: SecureRandom.urlsafe_base64(32),
|
96
97
|
token_type: AUTHORIZATION_CODE,
|
@@ -8,6 +8,7 @@
|
|
8
8
|
# authentication_method :string default("none")
|
9
9
|
# client_capabilities :json
|
10
10
|
# client_info :json
|
11
|
+
# consents :json not null
|
11
12
|
# ended_at :datetime
|
12
13
|
# initialized :boolean default(FALSE), not null
|
13
14
|
# messages_count :integer default(0), not null
|
@@ -40,6 +41,10 @@ module ActionMCP
|
|
40
41
|
# such as client and server capabilities, protocol version, and session status.
|
41
42
|
# It also manages the association with messages and subscriptions related to the session.
|
42
43
|
class Session < ApplicationRecord
|
44
|
+
after_initialize do
|
45
|
+
self.consents = {} if consents == "{}" || consents.nil?
|
46
|
+
end
|
47
|
+
|
43
48
|
include MCPConsoleHelpers
|
44
49
|
attribute :id, :string, default: -> { SecureRandom.hex(6) }
|
45
50
|
has_many :messages,
|
@@ -180,9 +185,7 @@ module ActionMCP
|
|
180
185
|
# Maintain cache limit by removing oldest events if needed
|
181
186
|
count = sse_events.count
|
182
187
|
excess = count - max_events
|
183
|
-
if excess.positive?
|
184
|
-
sse_events.order(event_id: :asc).limit(excess).delete_all
|
185
|
-
end
|
188
|
+
sse_events.order(event_id: :asc).limit(excess).delete_all if excess.positive?
|
186
189
|
|
187
190
|
event
|
188
191
|
end
|
@@ -363,7 +366,7 @@ module ActionMCP
|
|
363
366
|
# Required by MCP 2025-03-26 specification for session binding
|
364
367
|
|
365
368
|
# Store OAuth token and user context in session
|
366
|
-
def store_oauth_token(access_token:, refresh_token: nil,
|
369
|
+
def store_oauth_token(access_token:, expires_at:, refresh_token: nil, user_context: {})
|
367
370
|
update!(
|
368
371
|
oauth_access_token: access_token,
|
369
372
|
oauth_refresh_token: refresh_token,
|
@@ -406,7 +409,7 @@ module ActionMCP
|
|
406
409
|
end
|
407
410
|
|
408
411
|
# Update OAuth token (for refresh flow)
|
409
|
-
def update_oauth_token(access_token:, refresh_token: nil
|
412
|
+
def update_oauth_token(access_token:, expires_at:, refresh_token: nil)
|
410
413
|
update!(
|
411
414
|
oauth_access_token: access_token,
|
412
415
|
oauth_refresh_token: refresh_token,
|
@@ -447,6 +450,38 @@ module ActionMCP
|
|
447
450
|
)
|
448
451
|
end
|
449
452
|
|
453
|
+
# Consent management methods as per MCP specification
|
454
|
+
# These methods manage user consents for tools and resources
|
455
|
+
|
456
|
+
# Checks if consent has been granted for a specific key
|
457
|
+
# @param key [String] The consent key (e.g., tool name or resource URI)
|
458
|
+
# @return [Boolean] true if consent is granted, false otherwise
|
459
|
+
def consent_granted_for?(key)
|
460
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
461
|
+
consents_hash&.key?(key) && consents_hash[key] == true
|
462
|
+
end
|
463
|
+
|
464
|
+
# Grants consent for a specific key
|
465
|
+
# @param key [String] The consent key to grant
|
466
|
+
# @return [Boolean] true if saved successfully
|
467
|
+
def grant_consent(key)
|
468
|
+
self.consents = JSON.parse(consents) if consents.is_a?(String)
|
469
|
+
self.consents ||= {}
|
470
|
+
self.consents[key] = true
|
471
|
+
save!
|
472
|
+
end
|
473
|
+
|
474
|
+
# Revokes consent for a specific key
|
475
|
+
# @param key [String] The consent key to revoke
|
476
|
+
# @return [void]
|
477
|
+
def revoke_consent(key)
|
478
|
+
self.consents = JSON.parse(self.consents) if self.consents.is_a?(String)
|
479
|
+
return unless consents&.key?(key)
|
480
|
+
|
481
|
+
consents.delete(key)
|
482
|
+
save!
|
483
|
+
end
|
484
|
+
|
450
485
|
private
|
451
486
|
|
452
487
|
# if this session is from a server, the writer is the client
|
data/config/routes.rb
CHANGED
@@ -4,8 +4,10 @@ ActionMCP::Engine.routes.draw do
|
|
4
4
|
get "/up", to: "/rails/health#show", as: :action_mcp_health_check
|
5
5
|
|
6
6
|
# OAuth 2.1 metadata endpoints
|
7
|
-
get "/.well-known/oauth-authorization-server", to: "oauth/metadata#authorization_server",
|
8
|
-
|
7
|
+
get "/.well-known/oauth-authorization-server", to: "oauth/metadata#authorization_server",
|
8
|
+
as: :oauth_authorization_server_metadata
|
9
|
+
get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource",
|
10
|
+
as: :oauth_protected_resource_metadata
|
9
11
|
|
10
12
|
# OAuth 2.1 endpoints
|
11
13
|
get "/oauth/authorize", to: "oauth/endpoints#authorize", as: :oauth_authorize
|
@@ -133,9 +133,9 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
133
133
|
return unless column_exists?(:action_mcp_session_messages, :direction)
|
134
134
|
|
135
135
|
# SQLite3 doesn't support changing column comments
|
136
|
-
|
137
|
-
|
138
|
-
|
136
|
+
return unless connection.adapter_name.downcase != 'sqlite'
|
137
|
+
|
138
|
+
change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
|
139
139
|
end
|
140
140
|
|
141
141
|
private
|
@@ -5,15 +5,24 @@ class AddOAuthToSessions < ActiveRecord::Migration[8.0]
|
|
5
5
|
# Use json for all databases (PostgreSQL, SQLite3, MySQL) for consistency
|
6
6
|
json_type = :json
|
7
7
|
|
8
|
-
add_column :action_mcp_sessions, :oauth_access_token, :string unless column_exists?(:action_mcp_sessions,
|
9
|
-
|
10
|
-
add_column :action_mcp_sessions, :
|
11
|
-
|
12
|
-
add_column :action_mcp_sessions, :
|
8
|
+
add_column :action_mcp_sessions, :oauth_access_token, :string unless column_exists?(:action_mcp_sessions,
|
9
|
+
:oauth_access_token)
|
10
|
+
add_column :action_mcp_sessions, :oauth_refresh_token, :string unless column_exists?(:action_mcp_sessions,
|
11
|
+
:oauth_refresh_token)
|
12
|
+
add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime unless column_exists?(:action_mcp_sessions,
|
13
|
+
:oauth_token_expires_at)
|
14
|
+
add_column :action_mcp_sessions, :oauth_user_context, json_type unless column_exists?(:action_mcp_sessions,
|
15
|
+
:oauth_user_context)
|
16
|
+
add_column :action_mcp_sessions, :authentication_method, :string, default: 'none' unless column_exists?(
|
17
|
+
:action_mcp_sessions, :authentication_method
|
18
|
+
)
|
13
19
|
|
14
20
|
# Add indexes for performance
|
15
|
-
add_index :action_mcp_sessions, :oauth_access_token, unique: true unless index_exists?(:action_mcp_sessions,
|
16
|
-
|
17
|
-
add_index :action_mcp_sessions, :
|
21
|
+
add_index :action_mcp_sessions, :oauth_access_token, unique: true unless index_exists?(:action_mcp_sessions,
|
22
|
+
:oauth_access_token)
|
23
|
+
add_index :action_mcp_sessions, :oauth_token_expires_at unless index_exists?(:action_mcp_sessions,
|
24
|
+
:oauth_token_expires_at)
|
25
|
+
add_index :action_mcp_sessions, :authentication_method unless index_exists?(:action_mcp_sessions,
|
26
|
+
:authentication_method)
|
18
27
|
end
|
19
28
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
|
2
4
|
def change
|
3
5
|
create_table :action_mcp_oauth_clients do |t|
|
@@ -8,16 +10,16 @@ class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
|
|
8
10
|
# Store arrays as JSON for database compatibility
|
9
11
|
if connection.adapter_name.downcase.include?('postgresql')
|
10
12
|
t.text :redirect_uris, array: true, default: []
|
11
|
-
t.text :grant_types, array: true, default: [
|
12
|
-
t.text :response_types, array: true, default: [
|
13
|
+
t.text :grant_types, array: true, default: [ 'authorization_code' ]
|
14
|
+
t.text :response_types, array: true, default: [ 'code' ]
|
13
15
|
else
|
14
16
|
# For SQLite and other databases, use JSON
|
15
17
|
t.json :redirect_uris, default: []
|
16
|
-
t.json :grant_types, default: [
|
17
|
-
t.json :response_types, default: [
|
18
|
+
t.json :grant_types, default: [ 'authorization_code' ]
|
19
|
+
t.json :response_types, default: [ 'code' ]
|
18
20
|
end
|
19
21
|
|
20
|
-
t.string :token_endpoint_auth_method, default:
|
22
|
+
t.string :token_endpoint_auth_method, default: 'client_secret_basic'
|
21
23
|
t.text :scope
|
22
24
|
t.boolean :active, default: true
|
23
25
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class CreateActionMCPOAuthTokens < ActiveRecord::Migration[7.2]
|
2
4
|
def change
|
3
5
|
create_table :action_mcp_oauth_tokens do |t|
|
@@ -32,6 +34,6 @@ class CreateActionMCPOAuthTokens < ActiveRecord::Migration[7.2]
|
|
32
34
|
add_index :action_mcp_oauth_tokens, :user_id
|
33
35
|
add_index :action_mcp_oauth_tokens, :expires_at
|
34
36
|
add_index :action_mcp_oauth_tokens, :revoked
|
35
|
-
add_index :action_mcp_oauth_tokens, [
|
37
|
+
add_index :action_mcp_oauth_tokens, %i[token_type expires_at]
|
36
38
|
end
|
37
39
|
end
|
@@ -23,11 +23,12 @@ module ActionMCP
|
|
23
23
|
|
24
24
|
delegate :connected?, :ready?, to: :transport
|
25
25
|
|
26
|
-
def initialize(transport:, logger: ActionMCP.logger, **options)
|
26
|
+
def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
|
27
27
|
@logger = logger
|
28
28
|
@transport = transport
|
29
|
-
@session = nil
|
30
|
-
@session_id = options[:session_id]
|
29
|
+
@session = nil # Session will be created/loaded based on server response
|
30
|
+
@session_id = options[:session_id] # Optional session ID for resumption
|
31
|
+
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
31
32
|
@server_capabilities = nil
|
32
33
|
@connection_error = nil
|
33
34
|
@initialized = false
|
@@ -90,7 +91,7 @@ module ActionMCP
|
|
90
91
|
|
91
92
|
begin
|
92
93
|
# Only write to session if it exists (after initialization)
|
93
|
-
session
|
94
|
+
session&.write(payload)
|
94
95
|
data = payload.to_json unless payload.is_a?(String)
|
95
96
|
@transport.send_message(data)
|
96
97
|
true
|
@@ -108,11 +109,11 @@ module ActionMCP
|
|
108
109
|
end
|
109
110
|
|
110
111
|
# Only update session if it exists
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
112
|
+
return unless @session
|
113
|
+
|
114
|
+
@session.server_capabilities = server.capabilities
|
115
|
+
@session.server_info = server.server_info
|
116
|
+
@session.save
|
116
117
|
end
|
117
118
|
|
118
119
|
def initialized?
|
@@ -175,12 +176,10 @@ module ActionMCP
|
|
175
176
|
log_debug("Sending client capabilities")
|
176
177
|
|
177
178
|
# If we have a session_id, we're trying to resume
|
178
|
-
if @session_id
|
179
|
-
log_debug("Attempting to resume session: #{@session_id}")
|
180
|
-
end
|
179
|
+
log_debug("Attempting to resume session: #{@session_id}") if @session_id
|
181
180
|
|
182
181
|
params = {
|
183
|
-
protocolVersion:
|
182
|
+
protocolVersion: @protocol_version,
|
184
183
|
capabilities: client_capabilities,
|
185
184
|
clientInfo: client_info
|
186
185
|
}
|
@@ -143,14 +143,14 @@ module ActionMCP
|
|
143
143
|
def silence_logs
|
144
144
|
return yield unless @silence_sql
|
145
145
|
|
146
|
-
original_log_level = Session.logger&.level
|
146
|
+
original_log_level = ActionMCP::Session.logger&.level
|
147
147
|
begin
|
148
148
|
# Temporarily increase log level to suppress SQL queries
|
149
|
-
Session.logger.level = Logger::WARN if Session.logger
|
149
|
+
ActionMCP::Session.logger.level = Logger::WARN if ActionMCP::Session.logger
|
150
150
|
yield
|
151
151
|
ensure
|
152
152
|
# Restore original log level
|
153
|
-
Session.logger.level = original_log_level if Session.logger
|
153
|
+
ActionMCP::Session.logger.level = original_log_level if ActionMCP::Session.logger && original_log_level
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
@@ -8,15 +8,15 @@ module ActionMCP
|
|
8
8
|
# @param id [String, Integer] The request ID
|
9
9
|
# @param params [Hash] The elicitation parameters
|
10
10
|
def process_elicitation_request(id, params)
|
11
|
-
|
12
|
-
|
11
|
+
params["message"]
|
12
|
+
params["requestedSchema"]
|
13
13
|
|
14
14
|
# In a real implementation, this would prompt the user
|
15
15
|
# For now, we'll just return a decline response
|
16
16
|
# Actual implementations should override this method
|
17
17
|
send_jsonrpc_response(id, result: {
|
18
|
-
|
19
|
-
|
18
|
+
action: "decline"
|
19
|
+
})
|
20
20
|
end
|
21
21
|
|
22
22
|
# Send elicitation response
|
@@ -53,15 +53,13 @@ module ActionMCP
|
|
53
53
|
case rpc_method
|
54
54
|
when Methods::ELICITATION_CREATE
|
55
55
|
client.process_elicitation_request(id, params)
|
56
|
-
when
|
56
|
+
when %r{^roots/}
|
57
57
|
process_roots(rpc_method, id)
|
58
|
-
when
|
58
|
+
when %r{^sampling/}
|
59
59
|
process_sampling(rpc_method, id, params)
|
60
60
|
else
|
61
61
|
common_result = handle_common_methods(rpc_method, id, params)
|
62
|
-
if common_result.nil?
|
63
|
-
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}")
|
64
|
-
end
|
62
|
+
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}") if common_result.nil?
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
@@ -161,7 +159,7 @@ module ActionMCP
|
|
161
159
|
client.log_error("Unknown error: #{id} #{error}")
|
162
160
|
end
|
163
161
|
|
164
|
-
def handle_initialize_response(
|
162
|
+
def handle_initialize_response(_request_id, result)
|
165
163
|
# Session ID comes from HTTP headers, not the response body
|
166
164
|
# The transport should have already extracted it
|
167
165
|
session_id = transport.instance_variable_get(:@session_id)
|
@@ -179,13 +177,13 @@ module ActionMCP
|
|
179
177
|
else
|
180
178
|
# Create a new session with the server-provided ID
|
181
179
|
client.instance_variable_set(:@session, ActionMCP::Session.from_client.new(
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
180
|
+
id: session_id,
|
181
|
+
protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
|
182
|
+
client_info: client.client_info,
|
183
|
+
client_capabilities: client.client_capabilities,
|
184
|
+
server_info: result["serverInfo"],
|
185
|
+
server_capabilities: result["capabilities"]
|
186
|
+
))
|
189
187
|
client.session.save
|
190
188
|
client.log_info("Created new session: #{session_id}")
|
191
189
|
end
|
@@ -18,9 +18,9 @@ module ActionMCP
|
|
18
18
|
@logger = logger
|
19
19
|
|
20
20
|
# If token provided during initialization, store it
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
return unless token
|
22
|
+
|
23
|
+
save_token(token)
|
24
24
|
end
|
25
25
|
|
26
26
|
# Check if client has valid authentication
|
@@ -62,6 +62,7 @@ module ActionMCP
|
|
62
62
|
token = current_token
|
63
63
|
return nil unless token
|
64
64
|
return nil if token_expired?(token)
|
65
|
+
|
65
66
|
token
|
66
67
|
end
|
67
68
|
|
@@ -85,7 +86,7 @@ module ActionMCP
|
|
85
86
|
|
86
87
|
# Add 30 second buffer for clock skew
|
87
88
|
Time.at(exp) <= Time.now + 30
|
88
|
-
rescue => e
|
89
|
+
rescue StandardError => e
|
89
90
|
log_debug("Error checking token expiration: #{e.message}")
|
90
91
|
true # Treat invalid tokens as expired
|
91
92
|
end
|
@@ -103,7 +104,7 @@ module ActionMCP
|
|
103
104
|
|
104
105
|
payload_json = Base64.urlsafe_decode64(payload_base64)
|
105
106
|
JSON.parse(payload_json)
|
106
|
-
rescue => e
|
107
|
+
rescue StandardError => e
|
107
108
|
raise AuthenticationError, "Failed to decode JWT: #{e.message}"
|
108
109
|
end
|
109
110
|
|
@@ -173,16 +173,18 @@ module ActionMCP
|
|
173
173
|
well_known_url.path = "/.well-known/oauth-authorization-server"
|
174
174
|
|
175
175
|
response = @http_client.get(well_known_url)
|
176
|
-
unless response.success?
|
177
|
-
raise AuthenticationError, "Failed to fetch server metadata: #{response.status}"
|
178
|
-
end
|
176
|
+
raise AuthenticationError, "Failed to fetch server metadata: #{response.status}" unless response.success?
|
179
177
|
|
180
178
|
JSON.parse(response.body, symbolize_names: true)
|
181
179
|
end
|
182
180
|
|
183
181
|
def handle_token_response(response)
|
184
182
|
unless response.success?
|
185
|
-
error_body =
|
183
|
+
error_body = begin
|
184
|
+
JSON.parse(response.body)
|
185
|
+
rescue StandardError
|
186
|
+
{}
|
187
|
+
end
|
186
188
|
error_msg = error_body["error_description"] || error_body["error"] || "Token request failed"
|
187
189
|
raise AuthenticationError, "#{error_msg} (#{response.status})"
|
188
190
|
end
|
@@ -190,9 +192,7 @@ module ActionMCP
|
|
190
192
|
token_data = JSON.parse(response.body, symbolize_names: true)
|
191
193
|
|
192
194
|
# Calculate token expiration
|
193
|
-
if token_data[:expires_in]
|
194
|
-
token_data[:expires_at] = Time.now.to_i + token_data[:expires_in].to_i
|
195
|
-
end
|
195
|
+
token_data[:expires_at] = Time.now.to_i + token_data[:expires_in].to_i if token_data[:expires_in]
|
196
196
|
|
197
197
|
save_tokens(token_data)
|
198
198
|
log_debug("OAuth tokens obtained successfully")
|
@@ -219,7 +219,7 @@ module ActionMCP
|
|
219
219
|
client_name: "ActionMCP Client",
|
220
220
|
client_uri: "https://github.com/anthropics/action_mcp",
|
221
221
|
redirect_uris: [ @redirect_url.to_s ],
|
222
|
-
grant_types: [
|
222
|
+
grant_types: %w[authorization_code refresh_token],
|
223
223
|
response_types: [ "code" ],
|
224
224
|
token_endpoint_auth_method: "none", # Public client
|
225
225
|
code_challenge_methods_supported: [ "S256" ]
|