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
@@ -3,8 +3,6 @@
3
3
  require_relative "client/transport"
4
4
  require_relative "client/session_store"
5
5
  require_relative "client/streamable_http_transport"
6
- require_relative "client/oauth_client_provider"
7
- require_relative "client/jwt_client_provider"
8
6
 
9
7
  module ActionMCP
10
8
  # Creates a client appropriate for the given endpoint.
@@ -13,8 +11,6 @@ module ActionMCP
13
11
  # @param transport [Symbol] The transport type to use (:streamable_http, :sse for legacy)
14
12
  # @param session_store [Symbol] The session store type (:memory, :active_record)
15
13
  # @param session_id [String] Optional session ID for resuming connections
16
- # @param oauth_provider [ActionMCP::Client::OauthClientProvider] Optional OAuth provider for authentication
17
- # @param jwt_provider [ActionMCP::Client::JwtClientProvider] Optional JWT provider for authentication
18
14
  # @param protocol_version [String] The MCP protocol version to use (defaults to ActionMCP::DEFAULT_PROTOCOL_VERSION)
19
15
  # @param logger [Logger] The logger to use. Default is Logger.new($stdout).
20
16
  # @param options [Hash] Additional options to pass to the client constructor.
@@ -39,27 +35,8 @@ module ActionMCP
39
35
  # session_store: :memory
40
36
  # )
41
37
  #
42
- # @example With OAuth authentication
43
- # oauth_provider = ActionMCP::Client::OauthClientProvider.new(
44
- # authorization_server_url: "https://oauth.example.com",
45
- # redirect_url: "http://localhost:3000/callback",
46
- # client_metadata: { client_name: "My App" }
47
- # )
48
- # client = ActionMCP.create_client(
49
- # "http://127.0.0.1:3001/action_mcp",
50
- # oauth_provider: oauth_provider
51
- # )
52
- #
53
- # @example With JWT authentication
54
- # jwt_provider = ActionMCP::Client::JwtClientProvider.new(
55
- # token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
56
- # )
57
- # client = ActionMCP.create_client(
58
- # "http://127.0.0.1:3001/action_mcp",
59
- # jwt_provider: jwt_provider
60
- # )
61
38
  def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil,
62
- oauth_provider: nil, jwt_provider: nil, protocol_version: nil, logger: Logger.new($stdout), **options)
39
+ protocol_version: nil, logger: Logger.new($stdout), **options)
63
40
  unless endpoint =~ %r{\Ahttps?://}
64
41
  raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
65
42
  end
@@ -69,7 +46,7 @@ module ActionMCP
69
46
 
70
47
  # Create transport
71
48
  transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id,
72
- oauth_provider: oauth_provider, jwt_provider: jwt_provider, protocol_version: protocol_version, logger: logger, **options)
49
+ protocol_version: protocol_version, logger: logger, **options)
73
50
 
74
51
  logger.info("Creating #{transport} client for endpoint: #{endpoint}")
75
52
  # Pass session_id and protocol_version to the client
@@ -29,7 +29,6 @@ module ActionMCP
29
29
  :verbose_logging,
30
30
  # --- Authentication Options ---
31
31
  :authentication_methods,
32
- :oauth_config,
33
32
  # --- Transport Options ---
34
33
  :sse_heartbeat_interval,
35
34
  :post_response_preference, # :json or :sse
@@ -61,9 +60,8 @@ module ActionMCP
61
60
  @active_profile = :primary
62
61
  @profiles = default_profiles
63
62
 
64
- # Authentication defaults
65
- @authentication_methods = Rails.env.production? ? [ "jwt" ] : [ "none" ]
66
- @oauth_config = HashWithIndifferentAccess.new
63
+ # Authentication defaults - empty means all configured identifiers will be tried
64
+ @authentication_methods = []
67
65
 
68
66
  @sse_heartbeat_interval = 30
69
67
  @post_response_preference = :json
@@ -110,6 +108,9 @@ module ActionMCP
110
108
  # First load defaults from the gem
111
109
  @profiles = default_profiles
112
110
 
111
+ # Preserve any settings that were already set via Rails config
112
+ preserved_name = @name
113
+
113
114
  # Try to load from config/mcp.yml in the Rails app using Rails.config_for
114
115
  begin
115
116
  app_config = Rails.application.config_for(:mcp)
@@ -117,16 +118,28 @@ module ActionMCP
117
118
  raise "Invalid MCP config file" unless app_config.is_a?(Hash)
118
119
 
119
120
  # Extract authentication configuration if present
120
- @authentication_methods = Array(app_config["authentication"]) if app_config["authentication"]
121
-
122
- # Extract OAuth configuration if present
123
- @oauth_config = HashWithIndifferentAccess.new(app_config["oauth"]) if app_config["oauth"]
121
+ # Handle both symbol and string keys
122
+ @authentication_methods = Array(app_config[:authentication] || app_config["authentication"]) if app_config[:authentication] || app_config["authentication"]
124
123
 
125
124
  # Extract other top-level configuration settings
126
125
  extract_top_level_settings(app_config)
127
126
 
128
- # Extract profiles configuration
129
- @profiles = app_config["profiles"] if app_config["profiles"]
127
+ # Extract profiles configuration - merge with defaults instead of replacing
128
+ # Rails.config_for returns OrderedOptions which uses symbol keys
129
+ if app_config[:profiles] || app_config["profiles"]
130
+ # Get profiles with either symbol or string key
131
+ app_profiles = app_config[:profiles] || app_config["profiles"]
132
+
133
+ # Convert to regular hash and deep symbolize keys
134
+ if app_profiles.is_a?(ActiveSupport::OrderedOptions)
135
+ app_profiles = app_profiles.to_h.deep_symbolize_keys
136
+ elsif app_profiles.respond_to?(:deep_symbolize_keys)
137
+ app_profiles = app_profiles.deep_symbolize_keys
138
+ end
139
+
140
+ Rails.logger.debug "[Configuration] Merging profiles: #{app_profiles.inspect}"
141
+ @profiles = @profiles.deep_merge(app_profiles)
142
+ end
130
143
  rescue StandardError => e
131
144
  # If the config file doesn't exist in the Rails app, just use the defaults
132
145
  Rails.logger.warn "[Configuration] Failed to load MCP config: #{e.class} - #{e.message}"
@@ -134,8 +147,13 @@ module ActionMCP
134
147
  end
135
148
 
136
149
  # Apply the active profile
150
+ Rails.logger.info "[ActionMCP] Loaded profiles: #{@profiles.keys.join(', ')}"
151
+ Rails.logger.info "[ActionMCP] Using profile: #{@active_profile}"
137
152
  use_profile(@active_profile)
138
153
 
154
+ # Restore preserved settings
155
+ @name = preserved_name if preserved_name
156
+
139
157
  self
140
158
  end
141
159
 
@@ -184,6 +202,9 @@ module ActionMCP
184
202
  capabilities = {}
185
203
  profile = @profiles[active_profile]
186
204
 
205
+ Rails.logger.debug "[ActionMCP] Generating capabilities for profile: #{active_profile}"
206
+ Rails.logger.debug "[ActionMCP] Profile config: #{profile.inspect}"
207
+
187
208
  # Check profile configuration instead of registry contents
188
209
  # If profile includes tools (either "all" or specific tools), advertise tools capability
189
210
  capabilities[:tools] = { listChanged: @list_changed } if profile && profile[:tools]&.any?
@@ -268,42 +289,48 @@ module ActionMCP
268
289
  end
269
290
 
270
291
  def extract_top_level_settings(app_config)
292
+ # Create a wrapper that handles both symbol and string keys
293
+ config = HashWithIndifferentAccess.new(app_config)
294
+
271
295
  # Extract adapter configuration
272
- if app_config["adapter"]
296
+ if config["adapter"]
273
297
  # This will be handled by the pub/sub system, we just store it for now
274
- @adapter = app_config["adapter"]
298
+ @adapter = config["adapter"]
275
299
  end
276
300
 
277
301
  # Extract thread pool settings
278
- @min_threads = app_config["min_threads"] if app_config["min_threads"]
302
+ @min_threads = config["min_threads"] if config["min_threads"]
279
303
 
280
- @max_threads = app_config["max_threads"] if app_config["max_threads"]
304
+ @max_threads = config["max_threads"] if config["max_threads"]
281
305
 
282
- @max_queue = app_config["max_queue"] if app_config["max_queue"]
306
+ @max_queue = config["max_queue"] if config["max_queue"]
283
307
 
284
308
  # Extract polling interval for solid_cable
285
- @polling_interval = app_config["polling_interval"] if app_config["polling_interval"]
309
+ @polling_interval = config["polling_interval"] if config["polling_interval"]
286
310
 
287
311
  # Extract connects_to setting
288
- @connects_to = app_config["connects_to"] if app_config["connects_to"]
312
+ @connects_to = config["connects_to"] if config["connects_to"]
289
313
 
290
314
  # Extract verbose logging setting
291
- @verbose_logging = app_config["verbose_logging"] if app_config.key?("verbose_logging")
315
+ @verbose_logging = config["verbose_logging"] if app_config.key?("verbose_logging")
292
316
 
293
317
  # Extract gateway class configuration
294
- @gateway_class_name = app_config["gateway_class"] if app_config["gateway_class"]
318
+ @gateway_class_name = config["gateway_class"] if config["gateway_class"]
319
+
320
+ # Extract active profile setting
321
+ @active_profile = config["profile"].to_sym if config["profile"]
295
322
 
296
323
  # Extract session store configuration
297
- @session_store_type = app_config["session_store_type"].to_sym if app_config["session_store_type"]
324
+ @session_store_type = config["session_store_type"].to_sym if config["session_store_type"]
298
325
 
299
326
  # Extract client and server session store types
300
- if app_config["client_session_store_type"]
301
- @client_session_store_type = app_config["client_session_store_type"].to_sym
327
+ if config["client_session_store_type"]
328
+ @client_session_store_type = config["client_session_store_type"].to_sym
302
329
  end
303
330
 
304
- return unless app_config["server_session_store_type"]
331
+ return unless config["server_session_store_type"]
305
332
 
306
- @server_session_store_type = app_config["server_session_store_type"].to_sym
333
+ @server_session_store_type = config["server_session_store_type"].to_sym
307
334
  end
308
335
 
309
336
  def should_include_all?(type)
@@ -12,7 +12,6 @@ module ActionMCP
12
12
  ActiveSupport::Inflector.inflections(:en) do |inflect|
13
13
  inflect.acronym "SSE"
14
14
  inflect.acronym "MCP"
15
- inflect.acronym "OAuth"
16
15
  end
17
16
 
18
17
  # Provide a configuration namespace for ActionMCP
@@ -54,12 +53,6 @@ module ActionMCP
54
53
  ActionMCP.configuration.load_profiles
55
54
  end
56
55
 
57
- # Add OAuth middleware if OAuth is configured
58
- initializer "action_mcp.oauth_middleware", after: "action_mcp.load_profiles" do
59
- if ActionMCP.configuration.authentication_methods&.include?("oauth")
60
- config.middleware.use ActionMCP::OAuth::Middleware
61
- end
62
- end
63
56
 
64
57
  # Configure autoloading for the mcp/tools directory and identifiers
65
58
  initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
@@ -3,11 +3,7 @@
3
3
  module ActionMCP
4
4
  # Custom logger that filters out repetitive MCP requests
5
5
  class FilteredLogger < ActiveSupport::Logger
6
- FILTERED_PATHS = [
7
- "/oauth/authorize",
8
- "/.well-known/oauth-protected-resource",
9
- "/.well-known/oauth-authorization-server"
10
- ].freeze
6
+ FILTERED_PATHS = [].freeze
11
7
 
12
8
  FILTERED_METHODS = [
13
9
  "notifications/initialized",
@@ -15,7 +11,7 @@ module ActionMCP
15
11
  ].freeze
16
12
 
17
13
  def add(severity, message = nil, progname = nil, &block)
18
- # Filter out repetitive OAuth metadata requests
14
+ # Filter out specific paths
19
15
  if message.is_a?(String)
20
16
  return if FILTERED_PATHS.any? { |path| message.include?(path) && message.include?("200 OK") }
21
17
 
@@ -1,29 +1,213 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMCP
4
+ # Base class for Gateway authentication identifiers.
5
+ #
6
+ # Gateway identifiers provide a clean interface for authentication by reading
7
+ # from request.env keys set by upstream middleware (like Warden, Devise, or custom auth).
8
+ #
9
+ # @example Warden/Devise Integration
10
+ # class WardenIdentifier < ActionMCP::GatewayIdentifier
11
+ # identifier :user
12
+ # authenticates :warden
13
+ #
14
+ # def resolve
15
+ # # Warden sets 'warden.user' in request.env after authentication
16
+ # user = user_from_middleware
17
+ # user || raise(Unauthorized, "No authenticated user found")
18
+ # end
19
+ # end
20
+ #
21
+ # @example API Key Authentication
22
+ # class ApiKeyIdentifier < ActionMCP::GatewayIdentifier
23
+ # identifier :user
24
+ # authenticates :api_key
25
+ #
26
+ # def resolve
27
+ # # Check for API key in header or query param
28
+ # api_key = @request.env['HTTP_X_API_KEY'] ||
29
+ # @request.params['api_key']
30
+ # return raise(Unauthorized, "Missing API key") unless api_key
31
+ #
32
+ # user = User.find_by(api_key: api_key)
33
+ # user || raise(Unauthorized, "Invalid API key")
34
+ # end
35
+ # end
36
+ #
37
+ # @example Session-based Authentication
38
+ # class SessionIdentifier < ActionMCP::GatewayIdentifier
39
+ # identifier :user
40
+ # authenticates :session
41
+ #
42
+ # def resolve
43
+ # user_id = session&.[]('user_id')
44
+ # return raise(Unauthorized, "No user session") unless user_id
45
+ #
46
+ # user = User.find_by(id: user_id)
47
+ # user || raise(Unauthorized, "Invalid session")
48
+ # end
49
+ # end
50
+ #
51
+ # @example Multi-tenant with Organization
52
+ # class TenantIdentifier < ActionMCP::GatewayIdentifier
53
+ # identifier :user
54
+ # authenticates :tenant
55
+ #
56
+ # def resolve
57
+ # # Get user from middleware
58
+ # user = user_from_middleware
59
+ # return raise(Unauthorized, "No user found") unless user
60
+ #
61
+ # # Check tenant header
62
+ # tenant_id = @request.env['HTTP_X_TENANT_ID']
63
+ # return raise(Unauthorized, "Missing tenant") unless tenant_id
64
+ #
65
+ # # Verify user has access to tenant
66
+ # unless user.tenants.exists?(id: tenant_id)
67
+ # raise Unauthorized, "Access denied for tenant"
68
+ # end
69
+ #
70
+ # # Set current tenant for the request
71
+ # Current.tenant = Tenant.find(tenant_id)
72
+ # user
73
+ # end
74
+ # end
75
+ #
76
+ # @example Development/Testing Bypass
77
+ # class DevIdentifier < ActionMCP::GatewayIdentifier
78
+ # identifier :user
79
+ # authenticates :dev
80
+ #
81
+ # def resolve
82
+ # return raise(Unauthorized, "Dev auth disabled in production") unless development_env?
83
+ #
84
+ # # Create or find dev user
85
+ # User.find_or_create_by!(email: "dev@localhost") do |user|
86
+ # user.name = "Development User"
87
+ # end
88
+ # end
89
+ # end
4
90
  class GatewayIdentifier
5
91
  class Unauthorized < StandardError; end
6
92
 
7
93
  class << self
8
- # e.g. JwtIdentifier.identifier_name => :user
9
- attr_reader :identifier_name, :auth_method
94
+ # @return [Symbol] The name of the identity this identifier provides (e.g., :user, :admin)
95
+ attr_reader :identifier_name
10
96
 
97
+ # @return [String] The authentication method this identifier handles (e.g., "session", "api_key")
98
+ attr_reader :auth_method
99
+
100
+ # Declares what identity attribute this identifier provides.
101
+ # This becomes the accessor name on the Gateway instance.
102
+ #
103
+ # @param name [Symbol, String] The identity name (e.g., :user, :admin)
104
+ # @example
105
+ # identifier :user
106
+ # # Gateway instance will have gateway.user accessor
11
107
  def identifier(name)
12
108
  @identifier_name = name.to_sym
13
109
  end
14
110
 
111
+ # Declares what authentication method this identifier handles.
112
+ # This should match values in your authentication_methods configuration.
113
+ #
114
+ # @param method [Symbol, String] The auth method name (e.g., :session, :api_key)
115
+ # @example
116
+ # authenticates :api_key
117
+ # # Matches authentication_methods: ["api_key"] in config
15
118
  def authenticates(method)
16
119
  @auth_method = method.to_s
17
120
  end
18
121
  end
19
122
 
123
+ # @param request [ActionDispatch::Request] The request object containing env hash
20
124
  def initialize(request)
21
125
  @request = request
22
126
  end
23
127
 
24
- # must return a truthy identity object, or raise Unauthorized
128
+ # Resolves the identity for this authentication method.
129
+ # Must return a truthy identity object, or raise Unauthorized.
130
+ #
131
+ # Common request.env keys set by popular auth middleware:
132
+ # - 'warden.user' - Warden (used by Devise)
133
+ # - 'devise.user' - Devise direct
134
+ # - 'rack.session' - Rack session hash
135
+ # - 'HTTP_AUTHORIZATION' - Authorization header
136
+ # - Custom keys set by your middleware
137
+ #
138
+ # @return [Object] The authenticated identity (User, Admin, etc.)
139
+ # @raise [Unauthorized] When authentication fails
140
+ # @abstract Subclasses must implement this method
25
141
  def resolve
26
142
  raise NotImplementedError, "#{self.class}#resolve must be implemented"
27
143
  end
144
+
145
+ protected
146
+
147
+ # Helper method to extract Bearer token from Authorization header
148
+ # @return [String, nil] The token without "Bearer " prefix
149
+ def extract_bearer_token
150
+ header = @request.env["HTTP_AUTHORIZATION"] || ""
151
+ header[/\ABearer (.+)\z/, 1]
152
+ end
153
+
154
+ # Helper method to extract Basic auth credentials from Authorization header
155
+ # @return [Array<String>, nil] [username, password] or nil if not Basic auth
156
+ def extract_basic_auth
157
+ header = @request.env["HTTP_AUTHORIZATION"] || ""
158
+ return nil unless header.start_with?("Basic ")
159
+
160
+ encoded = header[6..-1] # Remove "Basic " prefix
161
+ decoded = Base64.decode64(encoded)
162
+ decoded.split(":", 2) # Split on first colon only
163
+ rescue StandardError
164
+ nil
165
+ end
166
+
167
+ # Helper method to get user from common middleware env keys
168
+ # @return [Object, nil] User from Warden/Devise or nil
169
+ def user_from_middleware
170
+ @request.env["warden.user"] || @request.env["devise.user"]
171
+ end
172
+
173
+ # Helper method to get session hash
174
+ # @return [Hash, nil] Rack session hash or nil
175
+ def session
176
+ @request.env["rack.session"]
177
+ end
178
+
179
+ # Helper method to read custom env key with optional fallbacks
180
+ # @param keys [String, Array<String>] Primary key or array of keys to try
181
+ # @return [Object, nil] Value from request.env or nil
182
+ def env_value(*keys)
183
+ keys.flatten.each do |key|
184
+ value = @request.env[key]
185
+ return value if value
186
+ end
187
+ nil
188
+ end
189
+
190
+ # Helper method to extract API key from various common locations
191
+ # @param header_name [String] Custom header name (default: 'HTTP_X_API_KEY')
192
+ # @param param_name [String] Query/form parameter name (default: 'api_key')
193
+ # @return [String, nil] The API key or nil
194
+ def extract_api_key(header_name: "HTTP_X_API_KEY", param_name: "api_key")
195
+ # Try custom header first
196
+ api_key = @request.env[header_name]
197
+ return api_key if api_key
198
+
199
+ # Try Authorization header with "Bearer" prefix
200
+ bearer_token = extract_bearer_token
201
+ return bearer_token if bearer_token
202
+
203
+ # Try request parameters (query string or form data)
204
+ @request.params[param_name] if @request.respond_to?(:params)
205
+ end
206
+
207
+ # Helper method to check if user is in a development environment
208
+ # @return [Boolean] true if Rails.env.development?
209
+ def development_env?
210
+ Rails.env.development?
211
+ end
28
212
  end
29
213
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module GatewayIdentifiers
5
+ # Example Gateway identifier for API key-based authentication.
6
+ #
7
+ # This identifier looks for API keys in various locations:
8
+ # - Authorization header (Bearer token)
9
+ # - Custom X-API-Key header
10
+ # - Query parameters
11
+ #
12
+ # @example Usage in ApplicationGateway
13
+ # class ApplicationGateway < ActionMCP::Gateway
14
+ # identified_by ActionMCP::GatewayIdentifiers::ApiKeyIdentifier
15
+ # end
16
+ #
17
+ # @example Configuration
18
+ # # config/mcp.yml
19
+ # authentication_methods: ["api_key"]
20
+ #
21
+ # @example API Key usage
22
+ # # Via Authorization header:
23
+ # # Authorization: Bearer your-api-key-here
24
+ # #
25
+ # # Via custom header:
26
+ # # X-API-Key: your-api-key-here
27
+ # #
28
+ # # Via query parameter:
29
+ # # ?api_key=your-api-key-here
30
+ class ApiKeyIdentifier < ActionMCP::GatewayIdentifier
31
+ identifier :user
32
+ authenticates :api_key
33
+
34
+ def resolve
35
+ api_key = extract_api_key
36
+ raise Unauthorized, "Missing API key" unless api_key
37
+
38
+ # Look up user by API key
39
+ # Assumes you have an api_key or api_token field on your User model
40
+ user = User.find_by(api_key: api_key) || User.find_by(api_token: api_key)
41
+ raise Unauthorized, "Invalid API key" unless user
42
+
43
+ # Optional: Check if API key is still valid (not expired, user active, etc.)
44
+ if user.respond_to?(:api_key_expired?) && user.api_key_expired?
45
+ raise Unauthorized, "API key expired"
46
+ end
47
+
48
+ if user.respond_to?(:active?) && !user.active?
49
+ raise Unauthorized, "User account inactive"
50
+ end
51
+
52
+ user
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module GatewayIdentifiers
5
+ # Example Gateway identifier for direct Devise integration.
6
+ #
7
+ # This identifier looks for the user directly in request.env['devise.user'] which
8
+ # may be set by custom Devise middleware or helpers.
9
+ #
10
+ # @example Usage in ApplicationGateway
11
+ # class ApplicationGateway < ActionMCP::Gateway
12
+ # identified_by ActionMCP::GatewayIdentifiers::DeviseIdentifier
13
+ # end
14
+ #
15
+ # @example Configuration
16
+ # # config/mcp.yml
17
+ # authentication_methods: ["devise"]
18
+ #
19
+ # @example Custom Devise middleware setup
20
+ # # In your application, you might set this in a before_action:
21
+ # # request.env['devise.user'] = current_user if user_signed_in?
22
+ class DeviseIdentifier < ActionMCP::GatewayIdentifier
23
+ identifier :user
24
+ authenticates :devise
25
+
26
+ def resolve
27
+ user = @request.env["devise.user"]
28
+ raise Unauthorized, "Not authenticated" unless user
29
+
30
+ user
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module GatewayIdentifiers
5
+ # Example Gateway identifier for custom request environment-based authentication.
6
+ #
7
+ # This identifier looks for user information in custom request headers or environment
8
+ # variables. Useful for authentication set up by upstream proxies, API gateways,
9
+ # or custom middleware.
10
+ #
11
+ # @example Usage in ApplicationGateway
12
+ # class ApplicationGateway < ActionMCP::Gateway
13
+ # identified_by ActionMCP::GatewayIdentifiers::RequestEnvIdentifier
14
+ # end
15
+ #
16
+ # @example Configuration
17
+ # # config/mcp.yml
18
+ # authentication_methods: ["request_env"]
19
+ #
20
+ # @example Nginx/Proxy setup
21
+ # # Your proxy/gateway might set headers like:
22
+ # # X-User-ID: 123
23
+ # # X-User-Email: user@example.com
24
+ # # X-User-Roles: admin,user
25
+ class RequestEnvIdentifier < ActionMCP::GatewayIdentifier
26
+ identifier :user
27
+ authenticates :request_env
28
+
29
+ def resolve
30
+ user_id = @request.env["HTTP_X_USER_ID"]
31
+ raise Unauthorized, "User ID header missing" unless user_id
32
+
33
+ # You might also want to get additional user info from headers
34
+ email = @request.env["HTTP_X_USER_EMAIL"]
35
+ roles = @request.env["HTTP_X_USER_ROLES"]&.split(",") || []
36
+
37
+ # Option 1: Find user in database
38
+ begin
39
+ user = User.find(user_id)
40
+ # Optional: verify email matches if provided
41
+ if email && user.email != email
42
+ raise Unauthorized, "User email mismatch"
43
+ end
44
+ user
45
+ rescue ActiveRecord::RecordNotFound
46
+ raise Unauthorized, "Invalid user"
47
+ end
48
+
49
+ # Option 2: Create a simple user object from headers (if you don't want DB lookup)
50
+ # OpenStruct.new(
51
+ # id: user_id,
52
+ # email: email,
53
+ # roles: roles
54
+ # )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module GatewayIdentifiers
5
+ # Example Gateway identifier for Warden-based authentication.
6
+ #
7
+ # This identifier works with Warden middleware which is commonly used by Devise.
8
+ # Warden sets the authenticated user in request.env['warden.user'] after successful authentication.
9
+ #
10
+ # @example Usage in ApplicationGateway
11
+ # class ApplicationGateway < ActionMCP::Gateway
12
+ # identified_by ActionMCP::GatewayIdentifiers::WardenIdentifier
13
+ # end
14
+ #
15
+ # @example Configuration
16
+ # # config/mcp.yml
17
+ # authentication_methods: ["warden"]
18
+ #
19
+ # @example With Devise
20
+ # # In your controller or middleware, ensure Warden is configured:
21
+ # # devise_for :users
22
+ # # authenticate_user! # This sets up Warden env
23
+ class WardenIdentifier < ActionMCP::GatewayIdentifier
24
+ identifier :user
25
+ authenticates :warden
26
+
27
+ def resolve
28
+ warden = @request.env["warden"]
29
+ raise Unauthorized, "Warden not available" unless warden
30
+
31
+ user = warden.user
32
+ raise Unauthorized, "Not authenticated" unless user
33
+
34
+ user
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ # Example Gateway identifiers for common authentication patterns.
5
+ #
6
+ # These identifiers provide ready-to-use implementations for popular
7
+ # Rails authentication patterns. You can use them directly or as
8
+ # templates for your own custom identifiers.
9
+ #
10
+ # @example Using in ApplicationGateway
11
+ # class ApplicationGateway < ActionMCP::Gateway
12
+ # # Use multiple identifiers (tried in order)
13
+ # identified_by ActionMCP::GatewayIdentifiers::WardenIdentifier,
14
+ # ActionMCP::GatewayIdentifiers::ApiKeyIdentifier
15
+ # end
16
+ #
17
+ # @example Configuration
18
+ # # config/mcp.yml
19
+ # authentication_methods: ["warden", "api_key"]
20
+ module GatewayIdentifiers
21
+ autoload :WardenIdentifier, "action_mcp/gateway_identifiers/warden_identifier"
22
+ autoload :DeviseIdentifier, "action_mcp/gateway_identifiers/devise_identifier"
23
+ autoload :RequestEnvIdentifier, "action_mcp/gateway_identifiers/request_env_identifier"
24
+ autoload :ApiKeyIdentifier, "action_mcp/gateway_identifiers/api_key_identifier"
25
+ end
26
+ end