actionmcp 0.71.0 → 0.71.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f504b6f7171acf10239a3873a3cf967243ed8fa598603b72ec3e265cf9ba7a
4
- data.tar.gz: 4b50494bcf216f93243d2068c2d9f211c25a9fe5166fb01f21518321716a4d54
3
+ metadata.gz: 203f9e44a8802e007a19f41ddb89e2961fae87fb14bc0157e398dd6b80561cc3
4
+ data.tar.gz: d644c46fe4e73c1c5532d14f2e1541d511e4311479c15ef5d946aa135c4f290d
5
5
  SHA512:
6
- metadata.gz: e2f3c857fdfd8404660d508ff620500676c71f629b5ac0a7ab0e7190248d25ea81d15eef679cbb14cba5f15ff119e882502de28d830f6c67a10c8ee7254a4301
7
- data.tar.gz: 3d44ee314a37df260fbc00c5d7488330f0d0e5e637c5b200a784bf62301b2feb7b330796cd2ff0268a96de70f498d946983aa89c745e6e636e915b37f5d81587
6
+ metadata.gz: c9472a20f2aafc0c4ac4b74d8924e741b4cb94dd69154d44b060d2a75fcc01edcda857be40611c7c24b6d1c4ee5410705b776999534c35521301fef737e57d35
7
+ data.tar.gz: 78afc85939383e260a89726dbc10c516a4c7da3dded794d55545c51ed98ba2179b433d402a552c1964f15a95c9e51c0e2e6ac531f18e1e145ae4a9ebe47b893e
@@ -512,8 +512,8 @@ module ActionMCP
512
512
  gateway_class = ActionMCP.configuration.gateway_class
513
513
  return unless gateway_class # Skip if no gateway configured
514
514
 
515
- gateway = gateway_class.new
516
- gateway.call(request)
515
+ gateway = gateway_class.new(request)
516
+ gateway.call
517
517
  rescue ActionMCP::UnauthorizedError => e
518
518
  render_unauthorized(e.message)
519
519
  end
@@ -23,11 +23,12 @@ module ActionMCP
23
23
 
24
24
  delegate :connected?, :ready?, to: :transport
25
25
 
26
- def initialize(transport:, logger: ActionMCP.logger, **options)
26
+ def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
27
27
  @logger = logger
28
28
  @transport = transport
29
29
  @session = nil # Session will be created/loaded based on server response
30
30
  @session_id = options[:session_id] # Optional session ID for resumption
31
+ @protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
31
32
  @server_capabilities = nil
32
33
  @connection_error = nil
33
34
  @initialized = false
@@ -180,7 +181,7 @@ module ActionMCP
180
181
  end
181
182
 
182
183
  params = {
183
- protocolVersion: ActionMCP::DEFAULT_PROTOCOL_VERSION,
184
+ protocolVersion: @protocol_version,
184
185
  capabilities: client_capabilities,
185
186
  clientInfo: client_info
186
187
  }
@@ -143,14 +143,14 @@ module ActionMCP
143
143
  def silence_logs
144
144
  return yield unless @silence_sql
145
145
 
146
- original_log_level = Session.logger&.level
146
+ original_log_level = ActionMCP::Session.logger&.level
147
147
  begin
148
148
  # Temporarily increase log level to suppress SQL queries
149
- Session.logger.level = Logger::WARN if Session.logger
149
+ ActionMCP::Session.logger.level = Logger::WARN if ActionMCP::Session.logger
150
150
  yield
151
151
  ensure
152
152
  # Restore original log level
153
- Session.logger.level = original_log_level if Session.logger
153
+ ActionMCP::Session.logger.level = original_log_level if ActionMCP::Session.logger && original_log_level
154
154
  end
155
155
  end
156
156
 
@@ -13,12 +13,15 @@ module ActionMCP
13
13
  SSE_TIMEOUT = 10
14
14
  ENDPOINT_TIMEOUT = 5
15
15
 
16
- attr_reader :session_id, :last_event_id
16
+ attr_reader :session_id, :last_event_id, :protocol_version
17
17
 
18
- def initialize(url, session_store:, session_id: nil, oauth_provider: nil, **options)
18
+ def initialize(url, session_store:, session_id: nil, oauth_provider: nil, jwt_provider: nil, protocol_version: nil, **options)
19
19
  super(url, session_store: session_store, **options)
20
20
  @session_id = session_id
21
21
  @oauth_provider = oauth_provider
22
+ @jwt_provider = jwt_provider
23
+ @protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
24
+ @negotiated_protocol_version = nil
22
25
  @last_event_id = nil
23
26
  @buffer = +""
24
27
  @current_event = nil
@@ -38,8 +41,9 @@ module ActionMCP
38
41
  # Start SSE stream if server supports it
39
42
  start_sse_stream
40
43
 
41
- set_connected(true)
44
+ # Set ready first, then connected (so transport is ready when on_connect fires)
42
45
  set_ready(true)
46
+ set_connected(true)
43
47
  log_debug("StreamableHTTP connection established")
44
48
  true
45
49
  rescue StandardError => e
@@ -98,7 +102,15 @@ module ActionMCP
98
102
  }
99
103
  headers["mcp-session-id"] = @session_id if @session_id
100
104
  headers["Last-Event-ID"] = @last_event_id if @last_event_id
105
+
106
+ # Add MCP-Protocol-Version header for GET requests when we have a negotiated version
107
+ if @negotiated_protocol_version
108
+ headers["MCP-Protocol-Version"] = @negotiated_protocol_version
109
+ end
110
+
101
111
  headers.merge!(oauth_headers)
112
+ headers.merge!(jwt_headers)
113
+ log_debug("Final GET headers: #{headers}")
102
114
  headers
103
115
  end
104
116
 
@@ -108,7 +120,16 @@ module ActionMCP
108
120
  "Accept" => "application/json, text/event-stream"
109
121
  }
110
122
  headers["mcp-session-id"] = @session_id if @session_id
123
+
124
+ # Add MCP-Protocol-Version header as per 2025-06-18 spec
125
+ # Only include when we have a negotiated version from previous handshake
126
+ if @negotiated_protocol_version
127
+ headers["MCP-Protocol-Version"] = @negotiated_protocol_version
128
+ end
129
+
111
130
  headers.merge!(oauth_headers)
131
+ headers.merge!(jwt_headers)
132
+ log_debug("Final POST headers: #{headers}")
112
133
  headers
113
134
  end
114
135
 
@@ -218,6 +239,13 @@ module ActionMCP
218
239
  def handle_json_response(response)
219
240
  begin
220
241
  message = MultiJson.load(response.body)
242
+
243
+ # Check if this is an initialize response to capture negotiated protocol version
244
+ if message.is_a?(Hash) && message["result"] && message["result"]["protocolVersion"]
245
+ @negotiated_protocol_version = message["result"]["protocolVersion"]
246
+ log_debug("Negotiated protocol version: #{@negotiated_protocol_version}")
247
+ end
248
+
221
249
  handle_message(message)
222
250
  rescue MultiJson::ParseError => e
223
251
  log_error("Failed to parse JSON response: #{e}")
@@ -232,7 +260,7 @@ module ActionMCP
232
260
  end
233
261
 
234
262
  def handle_error_response(response)
235
- error_msg = "HTTP #{response.status}: #{response.reason_phrase}"
263
+ error_msg = +"HTTP #{response.status}: #{response.reason_phrase}"
236
264
  if response.body && !response.body.empty?
237
265
  error_msg << " - #{response.body}"
238
266
  end
@@ -280,7 +308,7 @@ module ActionMCP
280
308
  id: @session_id,
281
309
  last_event_id: @last_event_id,
282
310
  session_data: {},
283
- protocol_version: PROTOCOL_VERSION
311
+ protocol_version: @protocol_version
284
312
  }
285
313
 
286
314
  @session_store.save_session(@session_id, session_data)
@@ -290,20 +318,38 @@ module ActionMCP
290
318
  def oauth_headers
291
319
  return {} unless @oauth_provider&.authenticated?
292
320
 
293
- @oauth_provider.authorization_headers
321
+ headers = @oauth_provider.authorization_headers
322
+ log_debug("OAuth headers: #{headers}") unless headers.empty?
323
+ headers
294
324
  rescue StandardError => e
295
325
  log_error("Failed to get OAuth headers: #{e.message}")
296
326
  {}
297
327
  end
298
328
 
299
- def handle_authentication_error(response)
300
- return unless @oauth_provider
329
+ def jwt_headers
330
+ return {} unless @jwt_provider&.authenticated?
301
331
 
332
+ headers = @jwt_provider.authorization_headers
333
+ log_debug("JWT headers: #{headers}") unless headers.empty?
334
+ headers
335
+ rescue StandardError => e
336
+ log_error("Failed to get JWT headers: #{e.message}")
337
+ {}
338
+ end
339
+
340
+ def handle_authentication_error(response)
302
341
  # Check for OAuth challenge in WWW-Authenticate header
303
342
  www_auth = response.headers["www-authenticate"]
304
343
  if www_auth&.include?("Bearer")
305
- log_debug("Received OAuth challenge, clearing tokens")
306
- @oauth_provider.clear_tokens!
344
+ if @oauth_provider
345
+ log_debug("Received OAuth challenge, clearing OAuth tokens")
346
+ @oauth_provider.clear_tokens!
347
+ end
348
+
349
+ if @jwt_provider
350
+ log_debug("Received Bearer challenge, clearing JWT tokens")
351
+ @jwt_provider.clear_tokens!
352
+ end
307
353
  end
308
354
  end
309
355
 
@@ -4,6 +4,7 @@ require_relative "client/transport"
4
4
  require_relative "client/session_store"
5
5
  require_relative "client/streamable_http_transport"
6
6
  require_relative "client/oauth_client_provider"
7
+ require_relative "client/jwt_client_provider"
7
8
 
8
9
  module ActionMCP
9
10
  # Creates a client appropriate for the given endpoint.
@@ -13,6 +14,8 @@ module ActionMCP
13
14
  # @param session_store [Symbol] The session store type (:memory, :active_record)
14
15
  # @param session_id [String] Optional session ID for resuming connections
15
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
+ # @param protocol_version [String] The MCP protocol version to use (defaults to ActionMCP::DEFAULT_PROTOCOL_VERSION)
16
19
  # @param logger [Logger] The logger to use. Default is Logger.new($stdout).
17
20
  # @param options [Hash] Additional options to pass to the client constructor.
18
21
  #
@@ -46,7 +49,16 @@ module ActionMCP
46
49
  # "http://127.0.0.1:3001/action_mcp",
47
50
  # oauth_provider: oauth_provider
48
51
  # )
49
- def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil, oauth_provider: nil, logger: Logger.new($stdout), **options)
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
+ def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil, oauth_provider: nil, jwt_provider: nil, protocol_version: nil, logger: Logger.new($stdout), **options)
50
62
  unless endpoint =~ %r{\Ahttps?://}
51
63
  raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
52
64
  end
@@ -55,11 +67,11 @@ module ActionMCP
55
67
  store = Client::SessionStoreFactory.create(session_store, **options)
56
68
 
57
69
  # Create transport
58
- transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, oauth_provider: oauth_provider, logger: logger, **options)
70
+ transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, oauth_provider: oauth_provider, jwt_provider: jwt_provider, protocol_version: protocol_version, logger: logger, **options)
59
71
 
60
72
  logger.info("Creating #{transport} client for endpoint: #{endpoint}")
61
- # Pass session_id to the client
62
- Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id, **options)
73
+ # Pass session_id and protocol_version to the client
74
+ Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id, protocol_version: protocol_version, **options)
63
75
  end
64
76
 
65
77
  private_class_method def self.create_transport(type, endpoint, **options)
@@ -61,9 +61,10 @@ module ActionMCP
61
61
  end
62
62
  end
63
63
 
64
- # Configure autoloading for the mcp/tools directory
64
+ # Configure autoloading for the mcp/tools directory and identifiers
65
65
  initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
66
66
  mcp_path = app.root.join("app/mcp")
67
+ identifiers_path = app.root.join("app/identifiers")
67
68
 
68
69
  if mcp_path.exist?
69
70
  # First add the parent mcp directory
@@ -74,6 +75,11 @@ module ActionMCP
74
75
  app.autoloaders.main.collapse(dir)
75
76
  end
76
77
  end
78
+
79
+ # Add identifiers directory for gateway identifiers
80
+ if identifiers_path.exist?
81
+ app.autoloaders.main.push_dir(identifiers_path, namespace: Object)
82
+ end
77
83
  end
78
84
 
79
85
  # Initialize the ActionMCP logger.
@@ -5,170 +5,84 @@ module ActionMCP
5
5
 
6
6
  class Gateway
7
7
  class << self
8
- def identified_by(*attrs)
9
- @identifiers ||= []
10
- @identifiers.concat(attrs.map(&:to_sym)).uniq!
11
- attr_accessor(*attrs)
8
+ # pluck in one or many GatewayIdentifier classes
9
+ def identified_by(*klasses)
10
+ @identifier_classes = klasses.flatten
12
11
  end
13
12
 
14
- def identifiers
15
- @identifiers ||= []
13
+ def identifier_classes
14
+ @identifier_classes || []
16
15
  end
17
16
  end
18
17
 
19
- identified_by :user
20
-
21
- attr_reader :request
22
-
23
- def call(request)
18
+ def initialize(request)
24
19
  @request = request
25
- connect
26
- self
27
20
  end
28
21
 
29
- def connect
22
+ # called by your rack/websocket layer
23
+ def call
30
24
  identities = authenticate!
31
- reject_unauthorized_connection unless identities.is_a?(Hash)
32
-
33
- # Assign all identities (e.g., :user, :account)
34
- self.class.identifiers.each do |id|
35
- value = identities[id]
36
- reject_unauthorized_connection unless value
37
-
38
- public_send("#{id}=", value)
39
-
40
- # Set to ActionMCP::Current
41
- ActionMCP::Current.public_send("#{id}=", value)
42
- end
43
-
44
- # Also set the gateway instance itself
45
- ActionMCP::Current.gateway = self
25
+ assign_identities(identities)
26
+ self
46
27
  end
47
28
 
48
-
49
29
  protected
50
30
 
51
31
  def authenticate!
52
- auth_methods = ActionMCP.configuration.authentication_methods || [ "jwt" ]
53
-
54
- auth_methods.each do |method|
55
- case method
56
- when "none"
57
- return default_user_identity
58
- when "jwt"
59
- result = jwt_authenticate
60
- return result if result
61
- when "oauth"
62
- result = oauth_authenticate
63
- return result if result
64
- end
65
- end
66
-
67
- raise UnauthorizedError, "No valid authentication found"
68
- end
69
-
70
- def extract_bearer_token
71
- header = request.headers["Authorization"] || request.headers["authorization"]
72
- return nil unless header&.start_with?("Bearer ")
73
- header.split(" ", 2).last
74
- end
32
+ active_identifiers = filter_active_identifiers
75
33
 
76
- def resolve_user(payload)
77
- return nil unless payload.is_a?(Hash)
78
- user_id = payload["user_id"] || payload["sub"]
79
- return nil unless user_id
80
- user = User.find_by(id: user_id)
81
- return nil unless user
82
-
83
- # Return a hash with all identified_by attributes
84
- self.class.identifiers.each_with_object({}) do |identifier, hash|
85
- hash[identifier] = user if identifier == :user
86
- # Add support for other identifiers as needed
34
+ if active_identifiers.empty?
35
+ raise ActionMCP::UnauthorizedError, "No authentication methods available"
87
36
  end
88
- end
89
-
90
- def reject_unauthorized_connection
91
- raise UnauthorizedError, "Unauthorized"
92
- end
93
37
 
94
- # Default user identity for "none" authentication
95
- def default_user_identity
96
- # Return a hash with all identified_by attributes set to a default user
97
- self.class.identifiers.each_with_object({}) do |identifier, hash|
98
- if identifier == :user
99
- # Create or find a default user for development
100
- hash[identifier] = find_or_create_default_user
38
+ # Try identifiers in order, use the first one that succeeds
39
+ last_error = nil
40
+ active_identifiers.each do |klass|
41
+ begin
42
+ result = klass.new(@request).resolve
43
+ return { klass.identifier_name => result }
44
+ rescue ActionMCP::GatewayIdentifier::Unauthorized => e
45
+ last_error = e
46
+ # Try next identifier
47
+ next
101
48
  end
102
- # Add support for other identifiers as needed
103
- end
104
- end
105
-
106
- # JWT authentication (existing implementation)
107
- def jwt_authenticate
108
- token = extract_bearer_token
109
- unless token
110
- raise UnauthorizedError, "Missing token" if ActionMCP.configuration.authentication_methods == [ "jwt" ]
111
- return nil
112
49
  end
113
50
 
114
- payload = ActionMCP::JwtDecoder.decode(token)
115
- result = resolve_user(payload)
116
- unless result
117
- raise UnauthorizedError, "Unauthorized" if ActionMCP.configuration.authentication_methods == [ "jwt" ]
118
- return nil
119
- end
120
- result
121
- rescue ActionMCP::JwtDecoder::DecodeError => e
122
- if ActionMCP.configuration.authentication_methods == [ "jwt" ]
123
- raise UnauthorizedError, "Invalid token"
124
- else
125
- nil # Let it try other authentication methods
126
- end
51
+ # If we get here, all identifiers failed
52
+ # Use the last specific error message if available, otherwise generic message
53
+ error_message = last_error&.message || "Authentication failed"
54
+ raise ActionMCP::UnauthorizedError, error_message
127
55
  end
128
56
 
129
- # OAuth authentication via middleware
130
- def oauth_authenticate
131
- return nil unless oauth_enabled?
132
-
133
- # Check if OAuth middleware has already validated the token
134
- token_info = request.env["action_mcp.oauth_token_info"]
135
- return nil unless token_info && token_info["active"]
57
+ private
136
58
 
137
- resolve_user_from_oauth(token_info)
138
- rescue ActionMCP::OAuth::Error
139
- nil # Let it try other authentication methods
140
- end
59
+ def filter_active_identifiers
60
+ configured_methods = ActionMCP.configuration.authentication_methods || []
141
61
 
142
- def oauth_enabled?
143
- ActionMCP.configuration.authentication_methods&.include?("oauth") &&
144
- ActionMCP.configuration.oauth_config.present?
145
- end
62
+ # If no authentication methods configured, use all identifiers
63
+ return self.class.identifier_classes if configured_methods.empty?
146
64
 
147
- def resolve_user_from_oauth(token_info)
148
- return nil unless token_info.is_a?(Hash)
65
+ # Normalize configured methods to strings for consistent comparison
66
+ normalized_methods = configured_methods.map(&:to_s)
149
67
 
150
- user_id = token_info["sub"] || token_info["user_id"]
151
- return nil unless user_id
152
-
153
- user = User.find_by(id: user_id) || User.find_by(oauth_subject: user_id)
154
- return nil unless user
155
-
156
- # Return a hash with all identified_by attributes
157
- self.class.identifiers.each_with_object({}) do |identifier, hash|
158
- hash[identifier] = user if identifier == :user
159
- # Add support for other identifiers as needed
68
+ # Filter identifiers to only those matching configured authentication methods
69
+ self.class.identifier_classes.select do |klass|
70
+ normalized_methods.include?(klass.auth_method.to_s)
160
71
  end
161
72
  end
162
73
 
163
- def find_or_create_default_user
164
- # Only for development/testing with "none" authentication
165
- return nil unless Rails.env.development? || Rails.env.test?
74
+ def assign_identities(identities)
75
+ identities.each do |name, value|
76
+ # define accessor on the fly
77
+ self.class.attr_reader name unless respond_to?(name)
78
+ instance_variable_set("@#{name}", value)
166
79
 
167
- if defined?(User)
168
- User.find_or_create_by(email: "dev@localhost") do |user|
169
- user.name = "Development User" if user.respond_to?(:name=)
170
- end
80
+ # also set current context if you have one
81
+ ActionMCP::Current.public_send("#{name}=", value) if
82
+ ActionMCP::Current.respond_to?("#{name}=")
171
83
  end
84
+ ActionMCP::Current.gateway = self if
85
+ ActionMCP::Current.respond_to?(:gateway=)
172
86
  end
173
87
  end
174
88
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class GatewayIdentifier
5
+ class Unauthorized < StandardError; end
6
+
7
+ class << self
8
+ # e.g. JwtIdentifier.identifier_name => :user
9
+ attr_reader :identifier_name, :auth_method
10
+
11
+ def identifier(name)
12
+ @identifier_name = name.to_sym
13
+ end
14
+
15
+ def authenticates(method)
16
+ @auth_method = method.to_s
17
+ end
18
+ end
19
+
20
+ def initialize(request)
21
+ @request = request
22
+ end
23
+
24
+ # must return a truthy identity object, or raise Unauthorized
25
+ def resolve
26
+ raise NotImplementedError, "#{self.class}#resolve must be implemented"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class JwtIdentifier < GatewayIdentifier
5
+ identifier :user
6
+ authenticates :jwt
7
+
8
+ def resolve
9
+ token = extract_bearer_token
10
+ raise Unauthorized, "Missing JWT" unless token
11
+
12
+ payload = ActionMCP::JwtDecoder.decode(token)
13
+ user = User.find_by(id: payload["sub"] || payload["user_id"])
14
+ return user if user
15
+
16
+ raise Unauthorized, "Invalid JWT user"
17
+ rescue ActionMCP::JwtDecoder::DecodeError => e
18
+ raise Unauthorized, "Invalid JWT token: #{e.message}"
19
+ end
20
+
21
+ private
22
+
23
+ def extract_bearer_token
24
+ header = @request.env["HTTP_AUTHORIZATION"] || ""
25
+ header[/\ABearer (.+)\z/, 1]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class NoneIdentifier < GatewayIdentifier
5
+ identifier :user
6
+ authenticates :none
7
+
8
+ def resolve
9
+ Rails.env.production? &&
10
+ raise(Unauthorized, "No auth allowed in production")
11
+
12
+ return "anonymous_user" unless defined?(User)
13
+
14
+ User.find_or_create_by!(email: "dev@localhost") do |user|
15
+ user.name = "Development User" if user.respond_to?(:name=)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class OAuthIdentifier < GatewayIdentifier
5
+ identifier :user
6
+ authenticates :oauth
7
+
8
+ def resolve
9
+ info = @request.env["action_mcp.oauth_token_info"] or
10
+ raise Unauthorized, "Missing OAuth info"
11
+
12
+ uid = info["user_id"] || info["sub"] || info[:user_id]
13
+ raise Unauthorized, "Invalid OAuth info" unless uid
14
+
15
+ # Try to find existing user or create one for demo purposes
16
+ user = User.find_by(email: uid) ||
17
+ User.find_by(email: "#{uid}@example.com") ||
18
+ create_oauth_user(uid)
19
+
20
+ user || raise(Unauthorized, "Unable to resolve OAuth user")
21
+ end
22
+
23
+ private
24
+
25
+ def create_oauth_user(uid)
26
+ return nil unless defined?(User)
27
+
28
+ email = uid.include?("@") ? uid : "#{uid}@example.com"
29
+ User.create!(email: email)
30
+ rescue ActiveRecord::RecordInvalid
31
+ nil
32
+ end
33
+ end
34
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.71.0"
5
+ VERSION = "0.71.1"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.71.0
4
+ version: 0.71.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -270,6 +270,7 @@ files:
270
270
  - lib/action_mcp/engine.rb
271
271
  - lib/action_mcp/filtered_logger.rb
272
272
  - lib/action_mcp/gateway.rb
273
+ - lib/action_mcp/gateway_identifier.rb
273
274
  - lib/action_mcp/gem_version.rb
274
275
  - lib/action_mcp/instrumentation/controller_runtime.rb
275
276
  - lib/action_mcp/instrumentation/instrumentation.rb
@@ -277,8 +278,11 @@ files:
277
278
  - lib/action_mcp/integer_array.rb
278
279
  - lib/action_mcp/json_rpc_handler_base.rb
279
280
  - lib/action_mcp/jwt_decoder.rb
281
+ - lib/action_mcp/jwt_identifier.rb
280
282
  - lib/action_mcp/log_subscriber.rb
281
283
  - lib/action_mcp/logging.rb
284
+ - lib/action_mcp/none_identifier.rb
285
+ - lib/action_mcp/o_auth_identifier.rb
282
286
  - lib/action_mcp/oauth.rb
283
287
  - lib/action_mcp/oauth/active_record_storage.rb
284
288
  - lib/action_mcp/oauth/error.rb