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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/controllers/action_mcp/application_controller.rb +20 -12
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +42 -119
- data/config/routes.rb +0 -13
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
- data/lib/action_mcp/client.rb +2 -25
- data/lib/action_mcp/configuration.rb +51 -24
- data/lib/action_mcp/engine.rb +0 -7
- data/lib/action_mcp/filtered_logger.rb +2 -6
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/server/base_session.rb +2 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -6
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- metadata +13 -97
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
- data/app/models/action_mcp/oauth_client.rb +0 -159
- data/app/models/action_mcp/oauth_token.rb +0 -142
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -28
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -132
- data/lib/action_mcp/oauth/middleware.rb +0 -128
- data/lib/action_mcp/oauth/provider.rb +0 -406
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -162
data/lib/action_mcp/client.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
296
|
+
if config["adapter"]
|
273
297
|
# This will be handled by the pub/sub system, we just store it for now
|
274
|
-
@adapter =
|
298
|
+
@adapter = config["adapter"]
|
275
299
|
end
|
276
300
|
|
277
301
|
# Extract thread pool settings
|
278
|
-
@min_threads =
|
302
|
+
@min_threads = config["min_threads"] if config["min_threads"]
|
279
303
|
|
280
|
-
@max_threads =
|
304
|
+
@max_threads = config["max_threads"] if config["max_threads"]
|
281
305
|
|
282
|
-
@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 =
|
309
|
+
@polling_interval = config["polling_interval"] if config["polling_interval"]
|
286
310
|
|
287
311
|
# Extract connects_to setting
|
288
|
-
@connects_to =
|
312
|
+
@connects_to = config["connects_to"] if config["connects_to"]
|
289
313
|
|
290
314
|
# Extract verbose logging setting
|
291
|
-
@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 =
|
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 =
|
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
|
301
|
-
@client_session_store_type =
|
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
|
331
|
+
return unless config["server_session_store_type"]
|
305
332
|
|
306
|
-
@server_session_store_type =
|
333
|
+
@server_session_store_type = config["server_session_store_type"].to_sym
|
307
334
|
end
|
308
335
|
|
309
336
|
def should_include_all?(type)
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -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
|
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
|
9
|
-
attr_reader :identifier_name
|
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
|
-
#
|
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
|