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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -15
  3. data/app/controllers/action_mcp/application_controller.rb +47 -40
  4. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
  5. data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
  6. data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
  7. data/app/models/action_mcp/oauth_client.rb +7 -5
  8. data/app/models/action_mcp/oauth_token.rb +2 -1
  9. data/app/models/action_mcp/session.rb +40 -5
  10. data/config/routes.rb +4 -2
  11. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  12. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
  13. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
  14. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
  15. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  16. data/lib/action_mcp/base_response.rb +1 -1
  17. data/lib/action_mcp/client/base.rb +12 -13
  18. data/lib/action_mcp/client/collection.rb +3 -3
  19. data/lib/action_mcp/client/elicitation.rb +4 -4
  20. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  21. data/lib/action_mcp/client/jwt_client_provider.rb +6 -5
  22. data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
  23. data/lib/action_mcp/client/streamable_http_transport.rb +63 -27
  24. data/lib/action_mcp/client.rb +19 -4
  25. data/lib/action_mcp/configuration.rb +28 -53
  26. data/lib/action_mcp/engine.rb +5 -1
  27. data/lib/action_mcp/filtered_logger.rb +1 -1
  28. data/lib/action_mcp/gateway.rb +47 -137
  29. data/lib/action_mcp/gateway_identifier.rb +29 -0
  30. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  31. data/lib/action_mcp/jwt_decoder.rb +4 -2
  32. data/lib/action_mcp/jwt_identifier.rb +28 -0
  33. data/lib/action_mcp/none_identifier.rb +19 -0
  34. data/lib/action_mcp/o_auth_identifier.rb +34 -0
  35. data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
  36. data/lib/action_mcp/oauth/memory_storage.rb +1 -3
  37. data/lib/action_mcp/oauth/middleware.rb +13 -18
  38. data/lib/action_mcp/oauth/provider.rb +45 -65
  39. data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
  40. data/lib/action_mcp/prompt.rb +2 -0
  41. data/lib/action_mcp/renderable.rb +1 -1
  42. data/lib/action_mcp/resource_template.rb +6 -2
  43. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +39 -26
  44. data/lib/action_mcp/server/base_session_store.rb +86 -0
  45. data/lib/action_mcp/server/capabilities.rb +2 -1
  46. data/lib/action_mcp/server/elicitation.rb +3 -9
  47. data/lib/action_mcp/server/error_handling.rb +14 -1
  48. data/lib/action_mcp/server/handlers/router.rb +31 -0
  49. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  50. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  51. data/lib/action_mcp/server/prompts.rb +4 -4
  52. data/lib/action_mcp/server/resources.rb +23 -4
  53. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  54. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  55. data/lib/action_mcp/server/tools.rb +62 -43
  56. data/lib/action_mcp/server/transport_handler.rb +2 -4
  57. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  58. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  59. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  60. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  61. data/lib/action_mcp/tool.rb +48 -37
  62. data/lib/action_mcp/types/float_array_type.rb +5 -3
  63. data/lib/action_mcp/version.rb +1 -1
  64. data/lib/action_mcp.rb +1 -1
  65. data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
  66. data/lib/tasks/action_mcp_tasks.rake +7 -5
  67. metadata +24 -18
  68. 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, -> { where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ]) }
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 == 0
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, code_challenge_method: 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, expires_at:, user_context: {})
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, expires_at:)
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", as: :oauth_authorization_server_metadata
8
- get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource", as: :oauth_protected_resource_metadata
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
- if connection.adapter_name.downcase != 'sqlite'
137
- change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
138
- end
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, :oauth_access_token)
9
- add_column :action_mcp_sessions, :oauth_refresh_token, :string unless column_exists?(:action_mcp_sessions, :oauth_refresh_token)
10
- add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime unless column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
11
- add_column :action_mcp_sessions, :oauth_user_context, json_type unless column_exists?(:action_mcp_sessions, :oauth_user_context)
12
- add_column :action_mcp_sessions, :authentication_method, :string, default: "none" unless column_exists?(:action_mcp_sessions, :authentication_method)
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, :oauth_access_token)
16
- add_index :action_mcp_sessions, :oauth_token_expires_at unless index_exists?(:action_mcp_sessions, :oauth_token_expires_at)
17
- add_index :action_mcp_sessions, :authentication_method unless index_exists?(:action_mcp_sessions, :authentication_method)
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: [ "authorization_code" ]
12
- t.text :response_types, array: true, default: [ "code" ]
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: [ "authorization_code" ]
17
- t.json :response_types, default: [ "code" ]
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: "client_secret_basic"
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, [ :token_type, :expires_at ]
37
+ add_index :action_mcp_oauth_tokens, %i[token_type expires_at]
36
38
  end
37
39
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddConsentsToActionMCPSess < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :action_mcp_sessions, :consents, :json, default: {}, null: false
6
+ end
7
+ end
@@ -19,7 +19,7 @@ module ActionMCP
19
19
  end
20
20
 
21
21
  # Convert to hash format expected by MCP protocol
22
- def to_h(options = nil)
22
+ def to_h(_options = nil)
23
23
  if @is_error
24
24
  JSON_RPC::JsonRpcError.new(@symbol, message: @error_message, data: @error_data).to_h
25
25
  else
@@ -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 # Session will be created/loaded based on server response
30
- @session_id = options[:session_id] # Optional session ID for resumption
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.write(payload) if 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
- if @session
112
- @session.server_capabilities = server.capabilities
113
- @session.server_info = server.server_info
114
- @session.save
115
- end
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: ActionMCP::DEFAULT_PROTOCOL_VERSION,
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
- message = params["message"]
12
- requested_schema = params["requestedSchema"]
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
- action: "decline"
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 /^roots\//
56
+ when %r{^roots/}
57
57
  process_roots(rpc_method, id)
58
- when /^sampling\//
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(request_id, result)
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
- id: session_id,
183
- protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
184
- client_info: client.client_info,
185
- client_capabilities: client.client_capabilities,
186
- server_info: result["serverInfo"],
187
- server_capabilities: result["capabilities"]
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
- if token
22
- save_token(token)
23
- end
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 = JSON.parse(response.body) rescue {}
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: [ "authorization_code", "refresh_token" ],
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" ]