actionmcp 0.72.0 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/action_mcp/application_controller.rb +20 -12
  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 +42 -119
  9. data/config/routes.rb +0 -13
  10. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  11. data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
  12. data/lib/action_mcp/client.rb +2 -25
  13. data/lib/action_mcp/configuration.rb +51 -24
  14. data/lib/action_mcp/engine.rb +0 -7
  15. data/lib/action_mcp/filtered_logger.rb +2 -6
  16. data/lib/action_mcp/gateway_identifier.rb +187 -3
  17. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  18. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  19. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  20. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  21. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  22. data/lib/action_mcp/server/base_session.rb +2 -0
  23. data/lib/action_mcp/version.rb +1 -1
  24. data/lib/action_mcp.rb +1 -6
  25. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  26. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  27. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  28. data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
  29. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  30. metadata +13 -97
  31. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
  32. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
  33. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
  34. data/app/models/action_mcp/oauth_client.rb +0 -159
  35. data/app/models/action_mcp/oauth_token.rb +0 -142
  36. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
  37. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
  38. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
  39. data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
  40. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  41. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  42. data/lib/action_mcp/jwt_decoder.rb +0 -28
  43. data/lib/action_mcp/jwt_identifier.rb +0 -28
  44. data/lib/action_mcp/none_identifier.rb +0 -19
  45. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  46. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  47. data/lib/action_mcp/oauth/error.rb +0 -79
  48. data/lib/action_mcp/oauth/memory_storage.rb +0 -132
  49. data/lib/action_mcp/oauth/middleware.rb +0 -128
  50. data/lib/action_mcp/oauth/provider.rb +0 -406
  51. data/lib/action_mcp/oauth.rb +0 -12
  52. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -162
@@ -1,265 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ostruct"
4
-
5
- module ActionMCP
6
- module OAuth
7
- # OAuth 2.1 endpoints controller
8
- # Handles authorization, token, introspection, and revocation endpoints
9
- class EndpointsController < ActionController::Base
10
- protect_from_forgery with: :null_session
11
- before_action :check_oauth_enabled
12
-
13
- # GET /oauth/authorize
14
- # Authorization endpoint for OAuth 2.1 authorization code flow
15
- def authorize
16
- # Extract parameters
17
- client_id = params[:client_id]
18
- redirect_uri = params[:redirect_uri]
19
- response_type = params[:response_type]
20
- scope = params[:scope]
21
- state = params[:state]
22
- code_challenge = params[:code_challenge]
23
- code_challenge_method = params[:code_challenge_method]
24
-
25
- # Validate required parameters
26
- if client_id.blank? || redirect_uri.blank? || response_type.blank?
27
- return render_error("invalid_request", "Missing required parameters")
28
- end
29
-
30
- # Validate response type
31
- unless response_type == "code"
32
- return render_error("unsupported_response_type", "Only authorization code flow supported")
33
- end
34
-
35
- # Validate PKCE if required
36
- if oauth_config["pkce_required"] && code_challenge.blank?
37
- return render_error("invalid_request", "PKCE required")
38
- end
39
-
40
- # In a real implementation, this would show a consent page
41
- # For now, we'll auto-approve for configured clients
42
- if auto_approve_client?(client_id)
43
- # Generate authorization code
44
- user_id = current_user&.id || "anonymous"
45
-
46
- begin
47
- code = ActionMCP::OAuth::Provider.generate_authorization_code(
48
- client_id: client_id,
49
- redirect_uri: redirect_uri,
50
- scope: scope || default_scope,
51
- code_challenge: code_challenge,
52
- code_challenge_method: code_challenge_method,
53
- user_id: user_id
54
- )
55
-
56
- # Redirect back to client with authorization code
57
- redirect_params = { code: code }
58
- redirect_params[:state] = state if state
59
- redirect_to "#{redirect_uri}?#{redirect_params.to_query}", allow_other_host: true
60
- rescue ActionMCP::OAuth::Error => e
61
- render_error(e.oauth_error_code, e.message)
62
- end
63
- else
64
- # In production, show consent page
65
- render_consent_page(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
66
- end
67
- end
68
-
69
- # POST /oauth/token
70
- # Token endpoint for exchanging authorization codes and refreshing tokens
71
- def token
72
- grant_type = params[:grant_type]
73
-
74
- case grant_type
75
- when "authorization_code"
76
- handle_authorization_code_grant
77
- when "refresh_token"
78
- handle_refresh_token_grant
79
- when "client_credentials"
80
- handle_client_credentials_grant
81
- else
82
- render_token_error("unsupported_grant_type", "Unsupported grant type")
83
- end
84
- rescue ActionMCP::OAuth::Error => e
85
- render_token_error(e.oauth_error_code, e.message)
86
- end
87
-
88
- # POST /oauth/introspect
89
- # Token introspection endpoint (RFC 7662)
90
- def introspect
91
- token = params[:token]
92
- return render_introspection_error unless token
93
-
94
- # Authenticate client for introspection
95
- client_id, = extract_client_credentials
96
- return render_introspection_error unless client_id
97
-
98
- begin
99
- token_info = ActionMCP::OAuth::Provider.introspect_token(token)
100
- render json: token_info
101
- rescue ActionMCP::OAuth::Error
102
- render json: { active: false }
103
- end
104
- end
105
-
106
- # POST /oauth/revoke
107
- # Token revocation endpoint (RFC 7009)
108
- def revoke
109
- token = params[:token]
110
- token_type_hint = params[:token_type_hint]
111
-
112
- return head :bad_request unless token
113
-
114
- # Authenticate client
115
- client_id, = extract_client_credentials
116
- return head :unauthorized unless client_id
117
-
118
- begin
119
- ActionMCP::OAuth::Provider.revoke_token(token, token_type_hint: token_type_hint)
120
- head :ok
121
- rescue ActionMCP::OAuth::Error
122
- head :bad_request
123
- end
124
- end
125
-
126
- private
127
-
128
- def check_oauth_enabled
129
- auth_methods = ActionMCP.configuration.authentication_methods
130
- return if auth_methods&.include?("oauth")
131
-
132
- head :not_found
133
- end
134
-
135
- def oauth_config
136
- @oauth_config ||= ActionMCP.configuration.oauth_config || {}
137
- end
138
-
139
- def handle_authorization_code_grant
140
- code = params[:code]
141
- client_id = params[:client_id]
142
- client_secret = params[:client_secret]
143
- redirect_uri = params[:redirect_uri]
144
- code_verifier = params[:code_verifier]
145
-
146
- # Extract client credentials from Authorization header if not in params
147
- client_id, client_secret = extract_client_credentials if client_id.blank?
148
-
149
- return render_token_error("invalid_request", "Missing required parameters") if code.blank? || client_id.blank?
150
-
151
- token_response = ActionMCP::OAuth::Provider.exchange_code_for_token(
152
- code: code,
153
- client_id: client_id,
154
- client_secret: client_secret,
155
- redirect_uri: redirect_uri,
156
- code_verifier: code_verifier
157
- )
158
-
159
- render json: token_response
160
- end
161
-
162
- def handle_refresh_token_grant
163
- refresh_token = params[:refresh_token]
164
- scope = params[:scope]
165
-
166
- # Extract client credentials
167
- client_id, client_secret = extract_client_credentials
168
- client_id ||= params[:client_id]
169
- client_secret ||= params[:client_secret]
170
-
171
- if refresh_token.blank? || client_id.blank?
172
- return render_token_error("invalid_request",
173
- "Missing required parameters")
174
- end
175
-
176
- token_response = ActionMCP::OAuth::Provider.refresh_access_token(
177
- refresh_token: refresh_token,
178
- client_id: client_id,
179
- client_secret: client_secret,
180
- scope: scope
181
- )
182
-
183
- render json: token_response
184
- end
185
-
186
- def handle_client_credentials_grant
187
- scope = params[:scope]
188
-
189
- # Extract client credentials
190
- client_id, client_secret = extract_client_credentials
191
- client_id ||= params[:client_id]
192
- client_secret ||= params[:client_secret]
193
-
194
- return render_token_error("invalid_request", "Missing client credentials") if client_id.blank?
195
-
196
- token_response = ActionMCP::OAuth::Provider.client_credentials_grant(
197
- client_id: client_id,
198
- client_secret: client_secret,
199
- scope: scope
200
- )
201
-
202
- render json: token_response
203
- end
204
-
205
- def extract_client_credentials
206
- auth_header = request.headers["Authorization"]
207
- if auth_header&.start_with?("Basic ")
208
- encoded = auth_header.split(" ", 2).last
209
- decoded = Base64.decode64(encoded)
210
- decoded.split(":", 2)
211
- else
212
- [ nil, nil ]
213
- end
214
- end
215
-
216
- def auto_approve_client?(client_id)
217
- # In development/testing, auto-approve known clients
218
- # In production, this should check a proper client registry
219
- Rails.env.development? || Rails.env.test? || oauth_config["auto_approve_clients"]&.include?(client_id)
220
- end
221
-
222
- def current_user
223
- # This should be implemented by the application
224
- # For now, return a default user for development
225
- if Rails.env.development? || Rails.env.test?
226
- OpenStruct.new(id: "dev_user", email: "dev@example.com")
227
- else
228
- # In production, this should integrate with your authentication system
229
- nil
230
- end
231
- end
232
-
233
- def default_scope
234
- oauth_config["default_scope"] || "mcp:tools mcp:resources mcp:prompts"
235
- end
236
-
237
- def render_error(error_code, description)
238
- render json: {
239
- error: error_code,
240
- error_description: description
241
- }, status: :bad_request
242
- end
243
-
244
- def render_token_error(error_code, description)
245
- render json: {
246
- error: error_code,
247
- error_description: description
248
- }, status: :bad_request
249
- end
250
-
251
- def render_introspection_error
252
- render json: { active: false }, status: :bad_request
253
- end
254
-
255
- def render_consent_page(_client_id, _redirect_uri, _scope, _state, _code_challenge, _code_challenge_method)
256
- # In production, this would render a proper consent page
257
- # For now, just auto-deny unknown clients
258
- render json: {
259
- error: "access_denied",
260
- error_description: "User denied authorization"
261
- }, status: :forbidden
262
- end
263
- end
264
- end
265
- end
@@ -1,125 +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
- metadata[:registration_endpoint] = registration_endpoint if oauth_config[:enable_dynamic_registration]
29
-
30
- metadata[:jwks_uri] = oauth_config[:jwks_uri] if oauth_config[:jwks_uri]
31
-
32
- render json: metadata
33
- end
34
-
35
- # GET /.well-known/oauth-protected-resource
36
- # Returns Protected Resource Metadata as per RFC 8705
37
- def protected_resource
38
- metadata = {
39
- resource: issuer_url,
40
- authorization_servers: [ issuer_url ],
41
- scopes_supported: scopes_supported,
42
- bearer_methods_supported: [ "header" ],
43
- resource_documentation: resource_documentation
44
- }
45
-
46
- render json: metadata
47
- end
48
-
49
- private
50
-
51
- def check_oauth_enabled
52
- auth_methods = ActionMCP.configuration.authentication_methods
53
- return if auth_methods&.include?("oauth")
54
-
55
- head :not_found
56
- end
57
-
58
- def oauth_config
59
- @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
60
- end
61
-
62
- def issuer_url
63
- @issuer_url ||= oauth_config.fetch(:issuer_url, request.base_url)
64
- end
65
-
66
- def authorization_endpoint
67
- "#{issuer_url}/oauth/authorize"
68
- end
69
-
70
- def token_endpoint
71
- "#{issuer_url}/oauth/token"
72
- end
73
-
74
- def introspection_endpoint
75
- "#{issuer_url}/oauth/introspect"
76
- end
77
-
78
- def revocation_endpoint
79
- "#{issuer_url}/oauth/revoke"
80
- end
81
-
82
- def registration_endpoint
83
- "#{issuer_url}/oauth/register"
84
- end
85
-
86
- def response_types_supported
87
- [ "code" ]
88
- end
89
-
90
- def grant_types_supported
91
- grants = [ "authorization_code" ]
92
- grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
93
- grants << "client_credentials" if oauth_config[:enable_client_credentials]
94
- grants
95
- end
96
-
97
- def token_endpoint_auth_methods_supported
98
- methods = %w[client_secret_basic client_secret_post]
99
- methods << "none" if oauth_config[:allow_public_clients]
100
- methods
101
- end
102
-
103
- def scopes_supported
104
- oauth_config.fetch(:scopes_supported, [ "mcp:tools", "mcp:resources", "mcp:prompts" ])
105
- end
106
-
107
- def code_challenge_methods_supported
108
- methods = []
109
- if oauth_config[:pkce_required] || oauth_config[:pkce_supported]
110
- methods << "S256"
111
- methods << "plain" if oauth_config[:allow_plain_pkce]
112
- end
113
- methods
114
- end
115
-
116
- def service_documentation
117
- oauth_config.fetch(:service_documentation, "#{request.base_url}/docs")
118
- end
119
-
120
- def resource_documentation
121
- oauth_config.fetch(:resource_documentation, "#{request.base_url}/docs/api")
122
- end
123
- end
124
- end
125
- end
@@ -1,201 +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
- client_secret = generate_client_secret if client_metadata["token_endpoint_auth_method"] != "none"
27
-
28
- # Store client registration
29
- client_info = {
30
- client_id: client_id,
31
- client_secret: client_secret,
32
- client_id_issued_at: Time.current.to_i,
33
- client_metadata: client_metadata,
34
- created_at: Time.current
35
- }
36
-
37
- # Save client registration (delegated to provider)
38
- ActionMCP::OAuth::Provider.register_client(client_info)
39
-
40
- # Build response according to RFC 7591
41
- response_data = {
42
- client_id: client_id,
43
- client_id_issued_at: client_info[:client_id_issued_at]
44
- }
45
-
46
- # Include client secret for confidential clients
47
- if client_secret
48
- response_data[:client_secret] = client_secret
49
- response_data[:client_secret_expires_at] = 0 # Never expires
50
- end
51
-
52
- # Include all client metadata in response
53
- response_data.merge!(client_metadata)
54
-
55
- # Add registration management fields if enabled
56
- if oauth_config[:enable_registration_management]
57
- response_data[:registration_access_token] = generate_registration_access_token(client_id)
58
- response_data[:registration_client_uri] = registration_client_url(client_id)
59
- end
60
-
61
- render json: response_data, status: :created
62
- rescue ActionMCP::OAuth::Error => e
63
- render_registration_error(e.oauth_error_code, e.message)
64
- rescue StandardError => e
65
- Rails.logger.error "Registration error: #{e.message}"
66
- render_registration_error("invalid_client_metadata", "Invalid client metadata")
67
- end
68
-
69
- private
70
-
71
- def check_oauth_enabled
72
- auth_methods = ActionMCP.configuration.authentication_methods
73
- return if auth_methods&.include?("oauth")
74
-
75
- head :not_found
76
- end
77
-
78
- def check_registration_enabled
79
- return if oauth_config[:enable_dynamic_registration]
80
-
81
- head :not_found
82
- end
83
-
84
- def oauth_config
85
- @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
86
- end
87
-
88
- def parse_client_metadata
89
- # RFC 7591 requires JSON request body
90
- unless request.content_type&.include?("application/json")
91
- raise ActionMCP::OAuth::InvalidRequestError, "Content-Type must be application/json"
92
- end
93
-
94
- JSON.parse(request.body.read)
95
- rescue JSON::ParserError
96
- raise ActionMCP::OAuth::InvalidRequestError, "Invalid JSON"
97
- end
98
-
99
- def validate_client_metadata(metadata)
100
- # Validate redirect URIs (required for authorization code flow)
101
- if metadata["grant_types"]&.include?("authorization_code") ||
102
- metadata["response_types"]&.include?("code")
103
- unless metadata["redirect_uris"].is_a?(Array) && metadata["redirect_uris"].any?
104
- raise ActionMCP::OAuth::InvalidClientMetadataError, "redirect_uris required for authorization code flow"
105
- end
106
-
107
- # Validate redirect URI format
108
- metadata["redirect_uris"].each do |uri|
109
- validate_redirect_uri(uri)
110
- end
111
- end
112
-
113
- # Validate grant types
114
- if metadata["grant_types"]
115
- unsupported = metadata["grant_types"] - supported_grant_types
116
- if unsupported.any?
117
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported grant types: #{unsupported.join(', ')}"
118
- end
119
- end
120
-
121
- # Validate response types
122
- if metadata["response_types"]
123
- unsupported = metadata["response_types"] - supported_response_types
124
- if unsupported.any?
125
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported response types: #{unsupported.join(', ')}"
126
- end
127
- end
128
-
129
- # Validate token endpoint auth method
130
- return unless metadata["token_endpoint_auth_method"]
131
- return if supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
132
-
133
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
134
- end
135
-
136
- def validate_redirect_uri(uri)
137
- parsed = URI.parse(uri)
138
-
139
- # Must be absolute URI
140
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute" unless parsed.absolute?
141
-
142
- # For non-localhost, must use HTTPS
143
- unless [ "localhost", "127.0.0.1" ].include?(parsed.host) || parsed.scheme == "https"
144
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
145
- end
146
- rescue URI::InvalidURIError
147
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Invalid redirect URI format"
148
- end
149
-
150
- def generate_client_id
151
- # Generate a unique client identifier
152
- "mcp_#{SecureRandom.hex(16)}"
153
- end
154
-
155
- def generate_client_secret
156
- # Generate a secure client secret
157
- SecureRandom.urlsafe_base64(32)
158
- end
159
-
160
- def generate_registration_access_token(_client_id)
161
- # Generate a token for managing this registration
162
- SecureRandom.urlsafe_base64(32)
163
- end
164
-
165
- def registration_client_url(client_id)
166
- "#{request.base_url}/oauth/register/#{client_id}"
167
- end
168
-
169
- def supported_grant_types
170
- grants = [ "authorization_code" ]
171
- grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
172
- grants << "client_credentials" if oauth_config[:enable_client_credentials]
173
- grants
174
- end
175
-
176
- def supported_response_types
177
- [ "code" ]
178
- end
179
-
180
- def supported_auth_methods
181
- methods = %w[client_secret_basic client_secret_post]
182
- methods << "none" if oauth_config.fetch(:allow_public_clients, true)
183
- methods
184
- end
185
-
186
- def render_registration_error(error_code, description)
187
- render json: {
188
- error: error_code,
189
- error_description: description
190
- }, status: :bad_request
191
- end
192
- end
193
-
194
- # Custom error for invalid client metadata
195
- class InvalidClientMetadataError < Error
196
- def initialize(message = "Invalid client metadata")
197
- super(message, "invalid_client_metadata")
198
- end
199
- end
200
- end
201
- end