Class: MCPClient::Auth::Token

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_client/auth.rb

Overview

OAuth token model representing access/refresh tokens

Constant Summary collapse

UNREADABLE_EXPIRY =

An expiry this client cannot read is recorded as this instant. An unreadable expiry is not "no expiry": read that way, a mangled lifetime would make a token that never expires and is never refreshed. Read as an instant long past, the record is refreshed or re-authorized instead — and says so again after a round trip through storage.

Time.at(0).utc.freeze
RETIRED_ISSUER =

Issuer value marking a token that must never be presented again (retired after an authorization server change, on a storage backend that cannot delete it).

'urn:mcp:retired-token'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_token:, token_type: 'Bearer', expires_in: nil, scope: nil, refresh_token: nil, issuer: nil, expires_at: nil) ⇒ Token

Returns a new instance of Token.

Parameters:

  • access_token (String) —

    The access token

  • token_type (String) (defaults to: 'Bearer') —

    Token type (default: "Bearer")

  • expires_in (Integer, nil) (defaults to: nil) —

    Token lifetime in seconds

  • scope (String, nil) (defaults to: nil) —

    Token scope

  • refresh_token (String, nil) (defaults to: nil) —

    Refresh token for renewal

  • issuer (String, nil) (defaults to: nil) —

    issuer identifier of the authorization server that issued the token (MCP 2026-07-28: tokens are per authorization server and never presented to another)

  • expires_at (Time, String, nil) (defaults to: nil) —

    an already-known expiry, as persisted by #to_h; it takes precedence over expires_in



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/mcp_client/auth.rb', line 26

def initialize(access_token:, token_type: 'Bearer', expires_in: nil, scope: nil, refresh_token: nil,
               issuer: nil, expires_at: nil)
  @access_token = access_token
  @token_type = token_type
  # A record read back from a storage backend that persists plain
  # hashes carries whatever was written (or edited) there. The lifetime
  # is validated BEFORE it is added to a Time: `Time.now + "3600"` is a
  # TypeError out of `OAuthProvider#access_token` — a crash in the
  # request path, over a record the read-path checks were about to
  # refuse anyway.
  @expires_in = self.class.usable_lifetime(expires_in)
  @scope = scope
  @issuer = issuer
  @refresh_token = refresh_token
  @expires_at = resolve_expiry(expires_in, expires_at)
end

Instance Attribute Details

#access_token ⇒ Object (readonly)

Returns the value of attribute access_token.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def access_token
  @access_token
end

#expires_at ⇒ Object (readonly)

Returns the value of attribute expires_at.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def expires_at
  @expires_at
end

#expires_in ⇒ Object (readonly)

Returns the value of attribute expires_in.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def expires_in
  @expires_in
end

#issuer ⇒ Object (readonly)

Returns the value of attribute issuer.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def issuer
  @issuer
end

#refresh_token ⇒ Object (readonly)

Returns the value of attribute refresh_token.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def refresh_token
  @refresh_token
end

#scope ⇒ Object (readonly)

Returns the value of attribute scope.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def scope
  @scope
end

#token_type ⇒ Object (readonly)

Returns the value of attribute token_type.



15
16
17
# File 'lib/mcp_client/auth.rb', line 15

def token_type
  @token_type
end

Class Method Details

.from_h(data) ⇒ Token

Create token from hash

Parameters:

  • data (Hash) —

    Token data

Returns:

  • (Token) —

    New token instance



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/mcp_client/auth.rb', line 155

def self.from_h(data)
  new(
    access_token: data[:access_token] || data['access_token'],
    token_type: data[:token_type] || data['token_type'] || 'Bearer',
    expires_in: data[:expires_in] || data['expires_in'],
    scope: data[:scope] || data['scope'],
    refresh_token: data[:refresh_token] || data['refresh_token'],
    issuer: data[:issuer] || data['issuer'],
    expires_at: data[:expires_at] || data['expires_at']
  )
end

.usable_expiry(value) ⇒ Time?

An expiry instant, as persisted by #to_h (an ISO 8601 string) or as given. Unparseable text, or a value of another type, is no expiry.

Parameters:

  • value (Object, nil)

Returns:

  • (Time, nil)


63
64
65
66
67
68
69
70
71
72
# File 'lib/mcp_client/auth.rb', line 63

def self.usable_expiry(value)
  return value if value.is_a?(Time)
  return nil unless value.is_a?(String)

  begin
    Time.parse(value)
  rescue ArgumentError
    nil
  end
end

.usable_lifetime(value) ⇒ Integer, ...

A lifetime that can be added to a Time. RFC 6749 Section 5.1 makes expires_in a number of seconds; anything else says nothing.

Parameters:

  • value (Object, nil)

Returns:

  • (Integer, Float, nil)


55
56
57
# File 'lib/mcp_client/auth.rb', line 55

def self.usable_lifetime(value)
  value if value.is_a?(Integer) || value.is_a?(Float)
end

Instance Method Details

#expired? ⇒ Boolean

Check if the token is expired

Returns:

  • (Boolean) —

    true if token is expired



76
77
78
79
80
# File 'lib/mcp_client/auth.rb', line 76

def expired?
  return false unless @expires_at

  Time.now >= @expires_at
end

#expires_soon? ⇒ Boolean

Check if the token is close to expiring (within 5 minutes)

Returns:

  • (Boolean) —

    true if token expires soon



84
85
86
87
88
# File 'lib/mcp_client/auth.rb', line 84

def expires_soon?
  return false unless @expires_at

  Time.now >= (@expires_at - 300) # 5 minutes buffer
end

#retired? ⇒ Boolean

Returns whether the token was retired.

Returns:

  • (Boolean) —

    whether the token was retired



103
104
105
# File 'lib/mcp_client/auth.rb', line 103

def retired?
  @issuer == RETIRED_ISSUER
end

#to_h ⇒ Hash

Convert to hash for serialization

Returns:

  • (Hash) —

    Hash representation



115
116
117
118
119
120
121
122
123
124
# File 'lib/mcp_client/auth.rb', line 115

def to_h
  {
    access_token: @access_token,
    token_type: @token_type,
    expires_in: @expires_in,
    scope: @scope,
    refresh_token: @refresh_token,
    expires_at: @expires_at&.iso8601
  }.tap { |hash| hash[:issuer] = @issuer if @issuer }
end

#to_header ⇒ String

Convert token to authorization header value

Returns:

  • (String) —

    Authorization header value



109
110
111
# File 'lib/mcp_client/auth.rb', line 109

def to_header
  "#{@token_type.capitalize} #{@access_token}"
end

#with_issuer(issuer) ⇒ Token

A copy of this token bound to an authorization server (or retired).

Parameters:

  • issuer (String)

Returns:



98
99
100
# File 'lib/mcp_client/auth.rb', line 98

def with_issuer(issuer)
  self.class.from_h(to_h.merge(issuer: issuer))
end