actionmcp 0.71.1 → 0.80.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +187 -16
  3. data/app/controllers/action_mcp/application_controller.rb +64 -49
  4. data/app/models/action_mcp/session/message.rb +31 -20
  5. data/app/models/action_mcp/session/resource.rb +35 -20
  6. data/app/models/action_mcp/session/sse_event.rb +23 -17
  7. data/app/models/action_mcp/session/subscription.rb +22 -15
  8. data/app/models/action_mcp/session.rb +71 -113
  9. data/config/routes.rb +0 -11
  10. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  11. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/base_response.rb +1 -1
  14. data/lib/action_mcp/client/base.rb +9 -11
  15. data/lib/action_mcp/client/elicitation.rb +4 -4
  16. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  17. data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
  18. data/lib/action_mcp/client.rb +6 -26
  19. data/lib/action_mcp/configuration.rb +65 -63
  20. data/lib/action_mcp/engine.rb +1 -10
  21. data/lib/action_mcp/filtered_logger.rb +3 -7
  22. data/lib/action_mcp/gateway.rb +7 -11
  23. data/lib/action_mcp/gateway_identifier.rb +187 -3
  24. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  25. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  26. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  27. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  28. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  29. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  30. data/lib/action_mcp/prompt.rb +2 -0
  31. data/lib/action_mcp/renderable.rb +1 -1
  32. data/lib/action_mcp/resource_template.rb +6 -2
  33. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
  34. data/lib/action_mcp/server/base_session_store.rb +86 -0
  35. data/lib/action_mcp/server/capabilities.rb +2 -1
  36. data/lib/action_mcp/server/elicitation.rb +3 -9
  37. data/lib/action_mcp/server/error_handling.rb +14 -1
  38. data/lib/action_mcp/server/handlers/router.rb +31 -0
  39. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  40. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  41. data/lib/action_mcp/server/prompts.rb +4 -4
  42. data/lib/action_mcp/server/resources.rb +23 -4
  43. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  44. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  45. data/lib/action_mcp/server/tools.rb +62 -43
  46. data/lib/action_mcp/server/transport_handler.rb +2 -4
  47. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  48. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  49. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  50. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  51. data/lib/action_mcp/tool.rb +48 -37
  52. data/lib/action_mcp/types/float_array_type.rb +5 -3
  53. data/lib/action_mcp/version.rb +1 -1
  54. data/lib/action_mcp.rb +2 -7
  55. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  56. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  57. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  58. data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
  59. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  60. data/lib/tasks/action_mcp_tasks.rake +7 -5
  61. metadata +18 -100
  62. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
  63. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
  64. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
  65. data/app/models/action_mcp/oauth_client.rb +0 -157
  66. data/app/models/action_mcp/oauth_token.rb +0 -141
  67. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
  68. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
  69. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
  70. data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
  71. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  72. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  73. data/lib/action_mcp/jwt_decoder.rb +0 -26
  74. data/lib/action_mcp/jwt_identifier.rb +0 -28
  75. data/lib/action_mcp/none_identifier.rb +0 -19
  76. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  77. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  78. data/lib/action_mcp/oauth/error.rb +0 -79
  79. data/lib/action_mcp/oauth/memory_storage.rb +0 -134
  80. data/lib/action_mcp/oauth/middleware.rb +0 -133
  81. data/lib/action_mcp/oauth/provider.rb +0 -426
  82. data/lib/action_mcp/oauth.rb +0 -12
  83. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
  84. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # Controller for OAuth 2.1 metadata endpoints
6
- # Provides server discovery information as per RFC 8414
7
- class MetadataController < ActionController::Base
8
- before_action :check_oauth_enabled
9
-
10
- # GET /.well-known/oauth-authorization-server
11
- # Returns OAuth Authorization Server Metadata as per RFC 8414
12
- def authorization_server
13
- metadata = {
14
- issuer: issuer_url,
15
- authorization_endpoint: authorization_endpoint,
16
- token_endpoint: token_endpoint,
17
- introspection_endpoint: introspection_endpoint,
18
- revocation_endpoint: revocation_endpoint,
19
- response_types_supported: response_types_supported,
20
- grant_types_supported: grant_types_supported,
21
- token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
22
- scopes_supported: scopes_supported,
23
- code_challenge_methods_supported: code_challenge_methods_supported,
24
- service_documentation: service_documentation
25
- }
26
-
27
- # Add optional fields based on configuration
28
- if oauth_config[:enable_dynamic_registration]
29
- metadata[:registration_endpoint] = registration_endpoint
30
- end
31
-
32
- if oauth_config[:jwks_uri]
33
- metadata[:jwks_uri] = oauth_config[:jwks_uri]
34
- end
35
-
36
- render json: metadata
37
- end
38
-
39
- # GET /.well-known/oauth-protected-resource
40
- # Returns Protected Resource Metadata as per RFC 8705
41
- def protected_resource
42
- metadata = {
43
- resource: issuer_url,
44
- authorization_servers: [ issuer_url ],
45
- scopes_supported: scopes_supported,
46
- bearer_methods_supported: [ "header" ],
47
- resource_documentation: resource_documentation
48
- }
49
-
50
- render json: metadata
51
- end
52
-
53
- private
54
-
55
- def check_oauth_enabled
56
- auth_methods = ActionMCP.configuration.authentication_methods
57
- unless auth_methods&.include?("oauth")
58
- head :not_found
59
- end
60
- end
61
-
62
- def oauth_config
63
- @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
64
- end
65
-
66
- def issuer_url
67
- @issuer_url ||= oauth_config.fetch(:issuer_url, request.base_url)
68
- end
69
-
70
- def authorization_endpoint
71
- "#{issuer_url}/oauth/authorize"
72
- end
73
-
74
- def token_endpoint
75
- "#{issuer_url}/oauth/token"
76
- end
77
-
78
- def introspection_endpoint
79
- "#{issuer_url}/oauth/introspect"
80
- end
81
-
82
- def revocation_endpoint
83
- "#{issuer_url}/oauth/revoke"
84
- end
85
-
86
- def registration_endpoint
87
- "#{issuer_url}/oauth/register"
88
- end
89
-
90
- def response_types_supported
91
- [ "code" ]
92
- end
93
-
94
- def grant_types_supported
95
- grants = [ "authorization_code" ]
96
- grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
97
- grants << "client_credentials" if oauth_config[:enable_client_credentials]
98
- grants
99
- end
100
-
101
- def token_endpoint_auth_methods_supported
102
- methods = [ "client_secret_basic", "client_secret_post" ]
103
- methods << "none" if oauth_config[:allow_public_clients]
104
- methods
105
- end
106
-
107
- def scopes_supported
108
- oauth_config.fetch(:scopes_supported, [ "mcp:tools", "mcp:resources", "mcp:prompts" ])
109
- end
110
-
111
- def code_challenge_methods_supported
112
- methods = []
113
- if oauth_config[:pkce_required] || oauth_config[:pkce_supported]
114
- methods << "S256"
115
- methods << "plain" if oauth_config[:allow_plain_pkce]
116
- end
117
- methods
118
- end
119
-
120
- def service_documentation
121
- oauth_config.fetch(:service_documentation, "#{request.base_url}/docs")
122
- end
123
-
124
- def resource_documentation
125
- oauth_config.fetch(:resource_documentation, "#{request.base_url}/docs/api")
126
- end
127
- end
128
- end
129
- end
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # OAuth 2.0 Dynamic Client Registration Controller (RFC 7591)
6
- # Allows clients to dynamically register with the authorization server
7
- class RegistrationController < ActionController::Base
8
- protect_from_forgery with: :null_session
9
- before_action :check_oauth_enabled
10
- before_action :check_registration_enabled
11
-
12
- # POST /oauth/register
13
- # Dynamic client registration endpoint as per RFC 7591
14
- def create
15
- # Parse client metadata from request body
16
- client_metadata = parse_client_metadata
17
-
18
- # Validate required fields
19
- validate_client_metadata(client_metadata)
20
-
21
- # Generate client credentials
22
- client_id = generate_client_id
23
- client_secret = nil # Public clients by default
24
-
25
- # Generate client secret for confidential clients
26
- if client_metadata["token_endpoint_auth_method"] != "none"
27
- client_secret = generate_client_secret
28
- end
29
-
30
- # Store client registration
31
- client_info = {
32
- client_id: client_id,
33
- client_secret: client_secret,
34
- client_id_issued_at: Time.current.to_i,
35
- client_metadata: client_metadata,
36
- created_at: Time.current
37
- }
38
-
39
- # Save client registration (delegated to provider)
40
- ActionMCP::OAuth::Provider.register_client(client_info)
41
-
42
- # Build response according to RFC 7591
43
- response_data = {
44
- client_id: client_id,
45
- client_id_issued_at: client_info[:client_id_issued_at]
46
- }
47
-
48
- # Include client secret for confidential clients
49
- if client_secret
50
- response_data[:client_secret] = client_secret
51
- response_data[:client_secret_expires_at] = 0 # Never expires
52
- end
53
-
54
- # Include all client metadata in response
55
- response_data.merge!(client_metadata)
56
-
57
- # Add registration management fields if enabled
58
- if oauth_config[:enable_registration_management]
59
- response_data[:registration_access_token] = generate_registration_access_token(client_id)
60
- response_data[:registration_client_uri] = registration_client_url(client_id)
61
- end
62
-
63
- render json: response_data, status: :created
64
- rescue ActionMCP::OAuth::Error => e
65
- render_registration_error(e.oauth_error_code, e.message)
66
- rescue StandardError => e
67
- Rails.logger.error "Registration error: #{e.message}"
68
- render_registration_error("invalid_client_metadata", "Invalid client metadata")
69
- end
70
-
71
- private
72
-
73
- def check_oauth_enabled
74
- auth_methods = ActionMCP.configuration.authentication_methods
75
- unless auth_methods&.include?("oauth")
76
- head :not_found
77
- end
78
- end
79
-
80
- def check_registration_enabled
81
- unless oauth_config[:enable_dynamic_registration]
82
- head :not_found
83
- end
84
- end
85
-
86
- def oauth_config
87
- @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
88
- end
89
-
90
- def parse_client_metadata
91
- # RFC 7591 requires JSON request body
92
- unless request.content_type&.include?("application/json")
93
- raise ActionMCP::OAuth::InvalidRequestError, "Content-Type must be application/json"
94
- end
95
-
96
- JSON.parse(request.body.read)
97
- rescue JSON::ParserError
98
- raise ActionMCP::OAuth::InvalidRequestError, "Invalid JSON"
99
- end
100
-
101
- def validate_client_metadata(metadata)
102
- # Validate redirect URIs (required for authorization code flow)
103
- if metadata["grant_types"]&.include?("authorization_code") ||
104
- metadata["response_types"]&.include?("code")
105
- unless metadata["redirect_uris"].is_a?(Array) && metadata["redirect_uris"].any?
106
- raise ActionMCP::OAuth::InvalidClientMetadataError, "redirect_uris required for authorization code flow"
107
- end
108
-
109
- # Validate redirect URI format
110
- metadata["redirect_uris"].each do |uri|
111
- validate_redirect_uri(uri)
112
- end
113
- end
114
-
115
- # Validate grant types
116
- if metadata["grant_types"]
117
- unsupported = metadata["grant_types"] - supported_grant_types
118
- if unsupported.any?
119
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported grant types: #{unsupported.join(', ')}"
120
- end
121
- end
122
-
123
- # Validate response types
124
- if metadata["response_types"]
125
- unsupported = metadata["response_types"] - supported_response_types
126
- if unsupported.any?
127
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported response types: #{unsupported.join(', ')}"
128
- end
129
- end
130
-
131
- # Validate token endpoint auth method
132
- if metadata["token_endpoint_auth_method"]
133
- unless supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
134
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
135
- end
136
- end
137
- end
138
-
139
- def validate_redirect_uri(uri)
140
- parsed = URI.parse(uri)
141
-
142
- # Must be absolute URI
143
- unless parsed.absolute?
144
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute"
145
- end
146
-
147
- # For non-localhost, must use HTTPS
148
- unless parsed.host == "localhost" || parsed.host == "127.0.0.1" || parsed.scheme == "https"
149
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
150
- end
151
- rescue URI::InvalidURIError
152
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Invalid redirect URI format"
153
- end
154
-
155
- def generate_client_id
156
- # Generate a unique client identifier
157
- "mcp_#{SecureRandom.hex(16)}"
158
- end
159
-
160
- def generate_client_secret
161
- # Generate a secure client secret
162
- SecureRandom.urlsafe_base64(32)
163
- end
164
-
165
- def generate_registration_access_token(client_id)
166
- # Generate a token for managing this registration
167
- SecureRandom.urlsafe_base64(32)
168
- end
169
-
170
- def registration_client_url(client_id)
171
- "#{request.base_url}/oauth/register/#{client_id}"
172
- end
173
-
174
- def supported_grant_types
175
- grants = [ "authorization_code" ]
176
- grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
177
- grants << "client_credentials" if oauth_config[:enable_client_credentials]
178
- grants
179
- end
180
-
181
- def supported_response_types
182
- [ "code" ]
183
- end
184
-
185
- def supported_auth_methods
186
- methods = [ "client_secret_basic", "client_secret_post" ]
187
- methods << "none" if oauth_config.fetch(:allow_public_clients, true)
188
- methods
189
- end
190
-
191
- def render_registration_error(error_code, description)
192
- render json: {
193
- error: error_code,
194
- error_description: description
195
- }, status: :bad_request
196
- end
197
- end
198
-
199
- # Custom error for invalid client metadata
200
- class InvalidClientMetadataError < Error
201
- def initialize(message = "Invalid client metadata")
202
- super(message, "invalid_client_metadata")
203
- end
204
- end
205
- end
206
- end
@@ -1,157 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # OAuth 2.0 Client model for storing registered clients
5
- # == Schema Information
6
- #
7
- # Table name: action_mcp_oauth_clients
8
- #
9
- # id :integer not null, primary key
10
- # active :boolean default(TRUE)
11
- # client_id_issued_at :integer
12
- # client_name :string
13
- # client_secret :string
14
- # client_secret_expires_at :integer
15
- # grant_types :json
16
- # metadata :json
17
- # redirect_uris :json
18
- # registration_access_token :string
19
- # response_types :json
20
- # scope :text
21
- # token_endpoint_auth_method :string default("client_secret_basic")
22
- # created_at :datetime not null
23
- # updated_at :datetime not null
24
- # client_id :string not null
25
- #
26
- # Indexes
27
- #
28
- # index_action_mcp_oauth_clients_on_active (active)
29
- # index_action_mcp_oauth_clients_on_client_id (client_id) UNIQUE
30
- # index_action_mcp_oauth_clients_on_client_id_issued_at (client_id_issued_at)
31
- #
32
- # Implements RFC 7591 Dynamic Client Registration
33
- class OAuthClient < ApplicationRecord
34
- self.table_name = "action_mcp_oauth_clients"
35
-
36
- # Validations
37
- validates :client_id, presence: true, uniqueness: true
38
- validates :token_endpoint_auth_method, inclusion: {
39
- in: %w[none client_secret_basic client_secret_post client_secret_jwt private_key_jwt]
40
- }
41
-
42
- # Scopes
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 ]) }
45
-
46
- # Callbacks
47
- before_create :set_issued_at
48
-
49
- # Check if client secret is expired
50
- def secret_expired?
51
- return false if client_secret_expires_at.nil? || client_secret_expires_at == 0
52
- Time.current.to_i > client_secret_expires_at
53
- end
54
-
55
- # Check if client is public (no authentication required)
56
- def public_client?
57
- token_endpoint_auth_method == "none"
58
- end
59
-
60
- # Check if client is confidential (authentication required)
61
- def confidential_client?
62
- !public_client?
63
- end
64
-
65
- # Validate redirect URI against registered URIs
66
- def valid_redirect_uri?(uri)
67
- return false if redirect_uris.blank?
68
- redirect_uris.include?(uri)
69
- end
70
-
71
- # Check if grant type is supported by this client
72
- def supports_grant_type?(grant_type)
73
- grant_types.include?(grant_type)
74
- end
75
-
76
- # Check if response type is supported by this client
77
- def supports_response_type?(response_type)
78
- response_types.include?(response_type)
79
- end
80
-
81
- # Check if scope is allowed for this client
82
- def valid_scope?(requested_scope)
83
- return true if scope.blank? # No scope restrictions
84
-
85
- requested_scopes = requested_scope.split(" ")
86
- allowed_scopes = scope.split(" ")
87
-
88
- # All requested scopes must be in allowed scopes
89
- (requested_scopes - allowed_scopes).empty?
90
- end
91
-
92
- # Convert to hash for API responses
93
- def to_api_response
94
- response = {
95
- client_id: client_id,
96
- client_id_issued_at: client_id_issued_at
97
- }
98
-
99
- # Include client secret for confidential clients
100
- if client_secret.present?
101
- response[:client_secret] = client_secret
102
- response[:client_secret_expires_at] = client_secret_expires_at || 0
103
- end
104
-
105
- # Include metadata fields
106
- %w[
107
- client_name redirect_uris grant_types response_types
108
- token_endpoint_auth_method scope
109
- ].each do |field|
110
- value = send(field)
111
- response[field.to_sym] = value if value.present?
112
- end
113
-
114
- # Include additional metadata
115
- response.merge!(metadata) if metadata.present?
116
-
117
- response
118
- end
119
-
120
- # Create from registration request
121
- def self.create_from_registration(client_metadata)
122
- client = new
123
-
124
- # Set basic fields
125
- client.client_id = "mcp_#{SecureRandom.hex(16)}"
126
-
127
- # Set metadata fields
128
- %w[
129
- client_name redirect_uris grant_types response_types
130
- token_endpoint_auth_method scope
131
- ].each do |field|
132
- client.send("#{field}=", client_metadata[field]) if client_metadata[field]
133
- end
134
-
135
- # Generate client secret for confidential clients
136
- if client.confidential_client?
137
- client.client_secret = SecureRandom.urlsafe_base64(32)
138
- end
139
-
140
- # Store any additional metadata
141
- known_fields = %w[
142
- client_name redirect_uris grant_types response_types
143
- token_endpoint_auth_method scope
144
- ]
145
- additional_metadata = client_metadata.except(*known_fields)
146
- client.metadata = additional_metadata if additional_metadata.present?
147
-
148
- client
149
- end
150
-
151
- private
152
-
153
- def set_issued_at
154
- self.client_id_issued_at ||= Time.current.to_i
155
- end
156
- end
157
- end
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # == Schema Information
5
- #
6
- # Table name: action_mcp_oauth_tokens
7
- #
8
- # id :integer not null, primary key
9
- # access_token :string
10
- # code_challenge :string
11
- # code_challenge_method :string
12
- # expires_at :datetime
13
- # metadata :json
14
- # redirect_uri :string
15
- # revoked :boolean default(FALSE)
16
- # scope :text
17
- # token :string not null
18
- # token_type :string not null
19
- # created_at :datetime not null
20
- # updated_at :datetime not null
21
- # client_id :string not null
22
- # user_id :string
23
- #
24
- # Indexes
25
- #
26
- # index_action_mcp_oauth_tokens_on_client_id (client_id)
27
- # index_action_mcp_oauth_tokens_on_expires_at (expires_at)
28
- # index_action_mcp_oauth_tokens_on_revoked (revoked)
29
- # index_action_mcp_oauth_tokens_on_token (token) UNIQUE
30
- # index_action_mcp_oauth_tokens_on_token_type (token_type)
31
- # index_action_mcp_oauth_tokens_on_token_type_and_expires_at (token_type,expires_at)
32
- # index_action_mcp_oauth_tokens_on_user_id (user_id)
33
- #
34
- # OAuth 2.0 Token model for storing access tokens, refresh tokens, and authorization codes
35
- class OAuthToken < ApplicationRecord
36
- self.table_name = "action_mcp_oauth_tokens"
37
-
38
- # Token types
39
- ACCESS_TOKEN = "access_token"
40
- REFRESH_TOKEN = "refresh_token"
41
- AUTHORIZATION_CODE = "authorization_code"
42
-
43
- # Validations
44
- validates :token, presence: true, uniqueness: true
45
- validates :token_type, presence: true, inclusion: { in: [ ACCESS_TOKEN, REFRESH_TOKEN, AUTHORIZATION_CODE ] }
46
- validates :client_id, presence: true
47
- validates :expires_at, presence: true
48
-
49
- # Scopes
50
- scope :active, -> { where(revoked: false).where("expires_at > ?", Time.current) }
51
- scope :expired, -> { where("expires_at <= ?", Time.current) }
52
- scope :access_tokens, -> { where(token_type: ACCESS_TOKEN) }
53
- scope :refresh_tokens, -> { where(token_type: REFRESH_TOKEN) }
54
- scope :authorization_codes, -> { where(token_type: AUTHORIZATION_CODE) }
55
-
56
- # Check if token is still valid
57
- def still_valid?
58
- !revoked? && !expired?
59
- end
60
-
61
- # Check if token is expired
62
- def expired?
63
- expires_at <= Time.current
64
- end
65
-
66
- # Revoke the token
67
- def revoke!
68
- update!(revoked: true)
69
- end
70
-
71
- # Convert to introspection response
72
- def to_introspection_response
73
- if still_valid?
74
- {
75
- active: true,
76
- scope: scope,
77
- client_id: client_id,
78
- username: user_id,
79
- token_type: token_type == ACCESS_TOKEN ? "Bearer" : token_type,
80
- exp: expires_at.to_i,
81
- iat: created_at.to_i,
82
- nbf: created_at.to_i,
83
- sub: user_id,
84
- aud: client_id,
85
- iss: ActionMCP.configuration.oauth_config&.dig("issuer_url")
86
- }.compact
87
- else
88
- { active: false }
89
- end
90
- end
91
-
92
- # Create authorization code
93
- def self.create_authorization_code(client_id:, user_id:, redirect_uri:, scope:, code_challenge: nil, code_challenge_method: nil)
94
- create!(
95
- token: SecureRandom.urlsafe_base64(32),
96
- token_type: AUTHORIZATION_CODE,
97
- client_id: client_id,
98
- user_id: user_id,
99
- redirect_uri: redirect_uri,
100
- scope: scope,
101
- code_challenge: code_challenge,
102
- code_challenge_method: code_challenge_method,
103
- expires_at: 10.minutes.from_now
104
- )
105
- end
106
-
107
- # Create access token
108
- def self.create_access_token(client_id:, user_id:, scope:)
109
- expires_in = ActionMCP.configuration.oauth_config&.dig("access_token_expires_in") || 3600
110
-
111
- create!(
112
- token: SecureRandom.urlsafe_base64(32),
113
- token_type: ACCESS_TOKEN,
114
- client_id: client_id,
115
- user_id: user_id,
116
- scope: scope,
117
- expires_at: expires_in.seconds.from_now
118
- )
119
- end
120
-
121
- # Create refresh token
122
- def self.create_refresh_token(client_id:, user_id:, scope:, access_token:)
123
- expires_in = ActionMCP.configuration.oauth_config&.dig("refresh_token_expires_in") || 7.days.to_i
124
-
125
- create!(
126
- token: SecureRandom.urlsafe_base64(32),
127
- token_type: REFRESH_TOKEN,
128
- client_id: client_id,
129
- user_id: user_id,
130
- scope: scope,
131
- access_token: access_token,
132
- expires_at: expires_in.seconds.from_now
133
- )
134
- end
135
-
136
- # Clean up expired tokens
137
- def self.cleanup_expired
138
- expired.delete_all
139
- end
140
- end
141
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddOAuthToSessions < ActiveRecord::Migration[8.0]
4
- def change
5
- # Use json for all databases (PostgreSQL, SQLite3, MySQL) for consistency
6
- json_type = :json
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)
13
-
14
- # 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)
18
- end
19
- end
@@ -1,42 +0,0 @@
1
- class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
2
- def change
3
- create_table :action_mcp_oauth_clients do |t|
4
- t.string :client_id, null: false, index: { unique: true }
5
- t.string :client_secret
6
- t.string :client_name
7
-
8
- # Store arrays as JSON for database compatibility
9
- if connection.adapter_name.downcase.include?('postgresql')
10
- 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
- else
14
- # For SQLite and other databases, use JSON
15
- t.json :redirect_uris, default: []
16
- t.json :grant_types, default: [ "authorization_code" ]
17
- t.json :response_types, default: [ "code" ]
18
- end
19
-
20
- t.string :token_endpoint_auth_method, default: "client_secret_basic"
21
- t.text :scope
22
- t.boolean :active, default: true
23
-
24
- # Registration metadata
25
- t.integer :client_id_issued_at
26
- t.integer :client_secret_expires_at
27
- t.string :registration_access_token # OAuth 2.1 Dynamic Client Registration
28
-
29
- # Additional metadata as JSON for database compatibility
30
- if connection.adapter_name.downcase.include?('postgresql')
31
- t.jsonb :metadata, default: {}
32
- else
33
- t.json :metadata, default: {}
34
- end
35
-
36
- t.timestamps
37
- end
38
-
39
- add_index :action_mcp_oauth_clients, :active
40
- add_index :action_mcp_oauth_clients, :client_id_issued_at
41
- end
42
- end