ruby-mcp-client 0.7.0 → 0.7.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 +4 -4
- data/README.md +71 -0
- data/lib/mcp_client/auth/oauth_provider.rb +514 -0
- data/lib/mcp_client/auth.rb +315 -0
- data/lib/mcp_client/http_transport_base.rb +41 -4
- data/lib/mcp_client/oauth_client.rb +127 -0
- data/lib/mcp_client/server_http.rb +63 -61
- data/lib/mcp_client/server_streamable_http.rb +63 -69
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +2 -0
- metadata +5 -2
@@ -0,0 +1,315 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
5
|
+
require 'base64'
|
6
|
+
require 'digest'
|
7
|
+
require 'securerandom'
|
8
|
+
require 'time'
|
9
|
+
|
10
|
+
module MCPClient
|
11
|
+
# OAuth 2.1 implementation for MCP client authentication
|
12
|
+
module Auth
|
13
|
+
# OAuth token model representing access/refresh tokens
|
14
|
+
class Token
|
15
|
+
attr_reader :access_token, :token_type, :expires_in, :scope, :refresh_token, :expires_at
|
16
|
+
|
17
|
+
# @param access_token [String] The access token
|
18
|
+
# @param token_type [String] Token type (default: "Bearer")
|
19
|
+
# @param expires_in [Integer, nil] Token lifetime in seconds
|
20
|
+
# @param scope [String, nil] Token scope
|
21
|
+
# @param refresh_token [String, nil] Refresh token for renewal
|
22
|
+
def initialize(access_token:, token_type: 'Bearer', expires_in: nil, scope: nil, refresh_token: nil)
|
23
|
+
@access_token = access_token
|
24
|
+
@token_type = token_type
|
25
|
+
@expires_in = expires_in
|
26
|
+
@scope = scope
|
27
|
+
@refresh_token = refresh_token
|
28
|
+
@expires_at = expires_in ? Time.now + expires_in : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if the token is expired
|
32
|
+
# @return [Boolean] true if token is expired
|
33
|
+
def expired?
|
34
|
+
return false unless @expires_at
|
35
|
+
|
36
|
+
Time.now >= @expires_at
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if the token is close to expiring (within 5 minutes)
|
40
|
+
# @return [Boolean] true if token expires soon
|
41
|
+
def expires_soon?
|
42
|
+
return false unless @expires_at
|
43
|
+
|
44
|
+
Time.now >= (@expires_at - 300) # 5 minutes buffer
|
45
|
+
end
|
46
|
+
|
47
|
+
# Convert token to authorization header value
|
48
|
+
# @return [String] Authorization header value
|
49
|
+
def to_header
|
50
|
+
"#{@token_type} #{@access_token}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert to hash for serialization
|
54
|
+
# @return [Hash] Hash representation
|
55
|
+
def to_h
|
56
|
+
{
|
57
|
+
access_token: @access_token,
|
58
|
+
token_type: @token_type,
|
59
|
+
expires_in: @expires_in,
|
60
|
+
scope: @scope,
|
61
|
+
refresh_token: @refresh_token,
|
62
|
+
expires_at: @expires_at&.iso8601
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create token from hash
|
67
|
+
# @param data [Hash] Token data
|
68
|
+
# @return [Token] New token instance
|
69
|
+
def self.from_h(data)
|
70
|
+
token = new(
|
71
|
+
access_token: data[:access_token] || data['access_token'],
|
72
|
+
token_type: data[:token_type] || data['token_type'] || 'Bearer',
|
73
|
+
expires_in: data[:expires_in] || data['expires_in'],
|
74
|
+
scope: data[:scope] || data['scope'],
|
75
|
+
refresh_token: data[:refresh_token] || data['refresh_token']
|
76
|
+
)
|
77
|
+
|
78
|
+
# Set expires_at if provided
|
79
|
+
if (expires_at_str = data[:expires_at] || data['expires_at'])
|
80
|
+
token.instance_variable_set(:@expires_at, Time.parse(expires_at_str))
|
81
|
+
end
|
82
|
+
|
83
|
+
token
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# OAuth client metadata for registration and authorization
|
88
|
+
class ClientMetadata
|
89
|
+
attr_reader :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types, :scope
|
90
|
+
|
91
|
+
# @param redirect_uris [Array<String>] List of valid redirect URIs
|
92
|
+
# @param token_endpoint_auth_method [String] Authentication method for token endpoint
|
93
|
+
# @param grant_types [Array<String>] Supported grant types
|
94
|
+
# @param response_types [Array<String>] Supported response types
|
95
|
+
# @param scope [String, nil] Requested scope
|
96
|
+
def initialize(redirect_uris:, token_endpoint_auth_method: 'none',
|
97
|
+
grant_types: %w[authorization_code refresh_token],
|
98
|
+
response_types: ['code'], scope: nil)
|
99
|
+
@redirect_uris = redirect_uris
|
100
|
+
@token_endpoint_auth_method = token_endpoint_auth_method
|
101
|
+
@grant_types = grant_types
|
102
|
+
@response_types = response_types
|
103
|
+
@scope = scope
|
104
|
+
end
|
105
|
+
|
106
|
+
# Convert to hash for HTTP requests
|
107
|
+
# @return [Hash] Hash representation
|
108
|
+
def to_h
|
109
|
+
{
|
110
|
+
redirect_uris: @redirect_uris,
|
111
|
+
token_endpoint_auth_method: @token_endpoint_auth_method,
|
112
|
+
grant_types: @grant_types,
|
113
|
+
response_types: @response_types,
|
114
|
+
scope: @scope
|
115
|
+
}.compact
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Registered OAuth client information
|
120
|
+
class ClientInfo
|
121
|
+
attr_reader :client_id, :client_secret, :client_id_issued_at, :client_secret_expires_at, :metadata
|
122
|
+
|
123
|
+
# @param client_id [String] OAuth client ID
|
124
|
+
# @param client_secret [String, nil] OAuth client secret (for confidential clients)
|
125
|
+
# @param client_id_issued_at [Integer, nil] Unix timestamp when client ID was issued
|
126
|
+
# @param client_secret_expires_at [Integer, nil] Unix timestamp when client secret expires
|
127
|
+
# @param metadata [ClientMetadata] Client metadata
|
128
|
+
def initialize(client_id:, metadata:, client_secret: nil, client_id_issued_at: nil,
|
129
|
+
client_secret_expires_at: nil)
|
130
|
+
@client_id = client_id
|
131
|
+
@client_secret = client_secret
|
132
|
+
@client_id_issued_at = client_id_issued_at
|
133
|
+
@client_secret_expires_at = client_secret_expires_at
|
134
|
+
@metadata = metadata
|
135
|
+
end
|
136
|
+
|
137
|
+
# Check if client secret is expired
|
138
|
+
# @return [Boolean] true if client secret is expired
|
139
|
+
def client_secret_expired?
|
140
|
+
return false unless @client_secret_expires_at
|
141
|
+
|
142
|
+
Time.now.to_i >= @client_secret_expires_at
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert to hash for serialization
|
146
|
+
# @return [Hash] Hash representation
|
147
|
+
def to_h
|
148
|
+
{
|
149
|
+
client_id: @client_id,
|
150
|
+
client_secret: @client_secret,
|
151
|
+
client_id_issued_at: @client_id_issued_at,
|
152
|
+
client_secret_expires_at: @client_secret_expires_at,
|
153
|
+
metadata: @metadata.to_h
|
154
|
+
}.compact
|
155
|
+
end
|
156
|
+
|
157
|
+
# Create client info from hash
|
158
|
+
# @param data [Hash] Client info data
|
159
|
+
# @return [ClientInfo] New client info instance
|
160
|
+
def self.from_h(data)
|
161
|
+
metadata_data = data[:metadata] || data['metadata'] || {}
|
162
|
+
metadata = build_metadata_from_hash(metadata_data)
|
163
|
+
|
164
|
+
new(
|
165
|
+
client_id: data[:client_id] || data['client_id'],
|
166
|
+
client_secret: data[:client_secret] || data['client_secret'],
|
167
|
+
client_id_issued_at: data[:client_id_issued_at] || data['client_id_issued_at'],
|
168
|
+
client_secret_expires_at: data[:client_secret_expires_at] || data['client_secret_expires_at'],
|
169
|
+
metadata: metadata
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Build ClientMetadata from hash data
|
174
|
+
# @param metadata_data [Hash] Metadata hash
|
175
|
+
# @return [ClientMetadata] Client metadata instance
|
176
|
+
def self.build_metadata_from_hash(metadata_data)
|
177
|
+
ClientMetadata.new(
|
178
|
+
redirect_uris: metadata_data[:redirect_uris] || metadata_data['redirect_uris'] || [],
|
179
|
+
token_endpoint_auth_method: extract_auth_method(metadata_data),
|
180
|
+
grant_types: metadata_data[:grant_types] || metadata_data['grant_types'] ||
|
181
|
+
%w[authorization_code refresh_token],
|
182
|
+
response_types: metadata_data[:response_types] || metadata_data['response_types'] || ['code'],
|
183
|
+
scope: metadata_data[:scope] || metadata_data['scope']
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Extract token endpoint auth method from metadata
|
188
|
+
# @param metadata_data [Hash] Metadata hash
|
189
|
+
# @return [String] Authentication method
|
190
|
+
def self.extract_auth_method(metadata_data)
|
191
|
+
metadata_data[:token_endpoint_auth_method] ||
|
192
|
+
metadata_data['token_endpoint_auth_method'] || 'none'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# OAuth authorization server metadata
|
197
|
+
class ServerMetadata
|
198
|
+
attr_reader :issuer, :authorization_endpoint, :token_endpoint, :registration_endpoint,
|
199
|
+
:scopes_supported, :response_types_supported, :grant_types_supported
|
200
|
+
|
201
|
+
# @param issuer [String] Issuer identifier URL
|
202
|
+
# @param authorization_endpoint [String] Authorization endpoint URL
|
203
|
+
# @param token_endpoint [String] Token endpoint URL
|
204
|
+
# @param registration_endpoint [String, nil] Client registration endpoint URL
|
205
|
+
# @param scopes_supported [Array<String>, nil] Supported OAuth scopes
|
206
|
+
# @param response_types_supported [Array<String>, nil] Supported response types
|
207
|
+
# @param grant_types_supported [Array<String>, nil] Supported grant types
|
208
|
+
def initialize(issuer:, authorization_endpoint:, token_endpoint:, registration_endpoint: nil,
|
209
|
+
scopes_supported: nil, response_types_supported: nil, grant_types_supported: nil)
|
210
|
+
@issuer = issuer
|
211
|
+
@authorization_endpoint = authorization_endpoint
|
212
|
+
@token_endpoint = token_endpoint
|
213
|
+
@registration_endpoint = registration_endpoint
|
214
|
+
@scopes_supported = scopes_supported
|
215
|
+
@response_types_supported = response_types_supported
|
216
|
+
@grant_types_supported = grant_types_supported
|
217
|
+
end
|
218
|
+
|
219
|
+
# Check if dynamic client registration is supported
|
220
|
+
# @return [Boolean] true if registration endpoint is available
|
221
|
+
def supports_registration?
|
222
|
+
!@registration_endpoint.nil?
|
223
|
+
end
|
224
|
+
|
225
|
+
# Convert to hash
|
226
|
+
# @return [Hash] Hash representation
|
227
|
+
def to_h
|
228
|
+
{
|
229
|
+
issuer: @issuer,
|
230
|
+
authorization_endpoint: @authorization_endpoint,
|
231
|
+
token_endpoint: @token_endpoint,
|
232
|
+
registration_endpoint: @registration_endpoint,
|
233
|
+
scopes_supported: @scopes_supported,
|
234
|
+
response_types_supported: @response_types_supported,
|
235
|
+
grant_types_supported: @grant_types_supported
|
236
|
+
}.compact
|
237
|
+
end
|
238
|
+
|
239
|
+
# Create server metadata from hash
|
240
|
+
# @param data [Hash] Server metadata
|
241
|
+
# @return [ServerMetadata] New server metadata instance
|
242
|
+
def self.from_h(data)
|
243
|
+
new(
|
244
|
+
issuer: data[:issuer] || data['issuer'],
|
245
|
+
authorization_endpoint: data[:authorization_endpoint] || data['authorization_endpoint'],
|
246
|
+
token_endpoint: data[:token_endpoint] || data['token_endpoint'],
|
247
|
+
registration_endpoint: data[:registration_endpoint] || data['registration_endpoint'],
|
248
|
+
scopes_supported: data[:scopes_supported] || data['scopes_supported'],
|
249
|
+
response_types_supported: data[:response_types_supported] || data['response_types_supported'],
|
250
|
+
grant_types_supported: data[:grant_types_supported] || data['grant_types_supported']
|
251
|
+
)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Protected resource metadata for authorization server discovery
|
256
|
+
class ResourceMetadata
|
257
|
+
attr_reader :resource, :authorization_servers
|
258
|
+
|
259
|
+
# @param resource [String] Resource server identifier
|
260
|
+
# @param authorization_servers [Array<String>] List of authorization server URLs
|
261
|
+
def initialize(resource:, authorization_servers:)
|
262
|
+
@resource = resource
|
263
|
+
@authorization_servers = authorization_servers
|
264
|
+
end
|
265
|
+
|
266
|
+
# Convert to hash
|
267
|
+
# @return [Hash] Hash representation
|
268
|
+
def to_h
|
269
|
+
{
|
270
|
+
resource: @resource,
|
271
|
+
authorization_servers: @authorization_servers
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
# Create resource metadata from hash
|
276
|
+
# @param data [Hash] Resource metadata
|
277
|
+
# @return [ResourceMetadata] New resource metadata instance
|
278
|
+
def self.from_h(data)
|
279
|
+
new(
|
280
|
+
resource: data[:resource] || data['resource'],
|
281
|
+
authorization_servers: data[:authorization_servers] || data['authorization_servers']
|
282
|
+
)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# PKCE (Proof Key for Code Exchange) helper
|
287
|
+
class PKCE
|
288
|
+
attr_reader :code_verifier, :code_challenge, :code_challenge_method
|
289
|
+
|
290
|
+
# Generate PKCE parameters
|
291
|
+
def initialize
|
292
|
+
@code_verifier = generate_code_verifier
|
293
|
+
@code_challenge = generate_code_challenge(@code_verifier)
|
294
|
+
@code_challenge_method = 'S256'
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
# Generate a cryptographically random code verifier
|
300
|
+
# @return [String] Base64url-encoded code verifier
|
301
|
+
def generate_code_verifier
|
302
|
+
# Generate 32 random bytes (256 bits) and base64url encode
|
303
|
+
Base64.urlsafe_encode64(SecureRandom.random_bytes(32), padding: false)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Generate code challenge from verifier using SHA256
|
307
|
+
# @param verifier [String] Code verifier
|
308
|
+
# @return [String] Base64url-encoded SHA256 hash
|
309
|
+
def generate_code_challenge(verifier)
|
310
|
+
digest = Digest::SHA256.digest(verifier)
|
311
|
+
Base64.urlsafe_encode64(digest, padding: false)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'json_rpc_common'
|
4
|
+
require_relative 'auth/oauth_provider'
|
4
5
|
|
5
6
|
module MCPClient
|
6
7
|
# Base module for HTTP-based JSON-RPC transports
|
@@ -172,18 +173,17 @@ module MCPClient
|
|
172
173
|
|
173
174
|
begin
|
174
175
|
response = conn.post(@endpoint) do |req|
|
175
|
-
|
176
|
-
@headers.each { |k, v| req.headers[k] = v }
|
176
|
+
apply_request_headers(req, request)
|
177
177
|
req.body = request.to_json
|
178
178
|
end
|
179
179
|
|
180
180
|
handle_http_error_response(response) unless response.success?
|
181
|
+
handle_successful_response(response, request)
|
181
182
|
|
182
183
|
log_response(response)
|
183
184
|
response
|
184
185
|
rescue Faraday::UnauthorizedError, Faraday::ForbiddenError => e
|
185
|
-
|
186
|
-
raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
|
186
|
+
handle_auth_error(e)
|
187
187
|
rescue Faraday::ConnectionFailed => e
|
188
188
|
raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
|
189
189
|
rescue Faraday::Error => e
|
@@ -191,6 +191,43 @@ module MCPClient
|
|
191
191
|
end
|
192
192
|
end
|
193
193
|
|
194
|
+
# Apply headers to the HTTP request (can be overridden by subclasses)
|
195
|
+
# @param req [Faraday::Request] HTTP request
|
196
|
+
# @param _request [Hash] JSON-RPC request
|
197
|
+
def apply_request_headers(req, _request)
|
198
|
+
# Apply all headers including custom ones
|
199
|
+
@headers.each { |k, v| req.headers[k] = v }
|
200
|
+
|
201
|
+
# Apply OAuth authorization if available
|
202
|
+
@logger.debug("OAuth provider present: #{@oauth_provider ? 'yes' : 'no'}")
|
203
|
+
@oauth_provider&.apply_authorization(req)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Handle successful HTTP response (can be overridden by subclasses)
|
207
|
+
# @param response [Faraday::Response] HTTP response
|
208
|
+
# @param _request [Hash] JSON-RPC request
|
209
|
+
def handle_successful_response(response, _request)
|
210
|
+
# Default: no additional handling
|
211
|
+
end
|
212
|
+
|
213
|
+
# Handle authentication errors
|
214
|
+
# @param error [Faraday::UnauthorizedError, Faraday::ForbiddenError] Auth error
|
215
|
+
# @raise [MCPClient::Errors::ConnectionError] Connection error
|
216
|
+
def handle_auth_error(error)
|
217
|
+
# Handle OAuth authorization challenges
|
218
|
+
if error.response && @oauth_provider
|
219
|
+
resource_metadata = @oauth_provider.handle_unauthorized_response(error.response)
|
220
|
+
if resource_metadata
|
221
|
+
@logger.debug('Received OAuth challenge, discovered resource metadata')
|
222
|
+
# Re-raise the error to trigger OAuth flow in calling code
|
223
|
+
raise MCPClient::Errors::ConnectionError, "OAuth authorization required: HTTP #{error.response[:status]}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
error_status = error.response ? error.response[:status] : 'unknown'
|
228
|
+
raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
|
229
|
+
end
|
230
|
+
|
194
231
|
# Handle HTTP error responses
|
195
232
|
# @param response [Faraday::Response] the error response
|
196
233
|
# @raise [MCPClient::Errors::ConnectionError] for auth errors
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'auth/oauth_provider'
|
4
|
+
require_relative 'server_http'
|
5
|
+
require_relative 'server_streamable_http'
|
6
|
+
|
7
|
+
module MCPClient
|
8
|
+
# Utility class for creating OAuth-enabled MCP clients
|
9
|
+
class OAuthClient
|
10
|
+
# Create an OAuth-enabled HTTP server
|
11
|
+
# @param server_url [String] The MCP server URL
|
12
|
+
# @param options [Hash] Configuration options
|
13
|
+
# @option options [String] :redirect_uri OAuth redirect URI (default: 'http://localhost:8080/callback')
|
14
|
+
# @option options [String, nil] :scope OAuth scope
|
15
|
+
# @option options [String] :endpoint JSON-RPC endpoint path (default: '/rpc')
|
16
|
+
# @option options [Hash] :headers Additional headers to include in requests
|
17
|
+
# @option options [Integer] :read_timeout Read timeout in seconds (default: 30)
|
18
|
+
# @option options [Integer] :retries Retry attempts on transient errors (default: 3)
|
19
|
+
# @option options [Numeric] :retry_backoff Base delay for exponential backoff (default: 1)
|
20
|
+
# @option options [String, nil] :name Optional name for this server
|
21
|
+
# @option options [Logger, nil] :logger Optional logger
|
22
|
+
# @option options [Object, nil] :storage Storage backend for OAuth tokens and client info
|
23
|
+
# @return [ServerHTTP] OAuth-enabled HTTP server
|
24
|
+
def self.create_http_server(server_url:, **options)
|
25
|
+
opts = default_server_options.merge(options)
|
26
|
+
|
27
|
+
oauth_provider = Auth::OAuthProvider.new(
|
28
|
+
server_url: server_url,
|
29
|
+
redirect_uri: opts[:redirect_uri],
|
30
|
+
scope: opts[:scope],
|
31
|
+
logger: opts[:logger],
|
32
|
+
storage: opts[:storage]
|
33
|
+
)
|
34
|
+
|
35
|
+
ServerHTTP.new(
|
36
|
+
base_url: server_url,
|
37
|
+
endpoint: opts[:endpoint],
|
38
|
+
headers: opts[:headers],
|
39
|
+
read_timeout: opts[:read_timeout],
|
40
|
+
retries: opts[:retries],
|
41
|
+
retry_backoff: opts[:retry_backoff],
|
42
|
+
name: opts[:name],
|
43
|
+
logger: opts[:logger],
|
44
|
+
oauth_provider: oauth_provider
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create an OAuth-enabled Streamable HTTP server
|
49
|
+
# @param server_url [String] The MCP server URL
|
50
|
+
# @param options [Hash] Configuration options (same as create_http_server)
|
51
|
+
# @return [ServerStreamableHTTP] OAuth-enabled Streamable HTTP server
|
52
|
+
def self.create_streamable_http_server(server_url:, **options)
|
53
|
+
opts = default_server_options.merge(options)
|
54
|
+
|
55
|
+
oauth_provider = Auth::OAuthProvider.new(
|
56
|
+
server_url: server_url,
|
57
|
+
redirect_uri: opts[:redirect_uri],
|
58
|
+
scope: opts[:scope],
|
59
|
+
logger: opts[:logger],
|
60
|
+
storage: opts[:storage]
|
61
|
+
)
|
62
|
+
|
63
|
+
ServerStreamableHTTP.new(
|
64
|
+
base_url: server_url,
|
65
|
+
endpoint: opts[:endpoint],
|
66
|
+
headers: opts[:headers],
|
67
|
+
read_timeout: opts[:read_timeout],
|
68
|
+
retries: opts[:retries],
|
69
|
+
retry_backoff: opts[:retry_backoff],
|
70
|
+
name: opts[:name],
|
71
|
+
logger: opts[:logger],
|
72
|
+
oauth_provider: oauth_provider
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Perform OAuth authorization flow for a server
|
77
|
+
# This is a helper method that can be used to manually perform the OAuth flow
|
78
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
79
|
+
# @return [String] Authorization URL to redirect user to
|
80
|
+
# @raise [ArgumentError] if server doesn't have OAuth provider
|
81
|
+
def self.start_oauth_flow(server)
|
82
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
83
|
+
raise ArgumentError, 'Server does not have OAuth provider configured' unless oauth_provider
|
84
|
+
|
85
|
+
oauth_provider.start_authorization_flow
|
86
|
+
end
|
87
|
+
|
88
|
+
# Complete OAuth authorization flow with authorization code
|
89
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
90
|
+
# @param code [String] Authorization code from callback
|
91
|
+
# @param state [String] State parameter from callback
|
92
|
+
# @return [Auth::Token] Access token
|
93
|
+
# @raise [ArgumentError] if server doesn't have OAuth provider
|
94
|
+
def self.complete_oauth_flow(server, code, state)
|
95
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
96
|
+
raise ArgumentError, 'Server does not have OAuth provider configured' unless oauth_provider
|
97
|
+
|
98
|
+
oauth_provider.complete_authorization_flow(code, state)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if server has a valid OAuth access token
|
102
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
103
|
+
# @return [Boolean] true if server has valid access token
|
104
|
+
def self.valid_token?(server)
|
105
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
106
|
+
return false unless oauth_provider
|
107
|
+
|
108
|
+
token = oauth_provider.access_token
|
109
|
+
!!(token && !token.expired?)
|
110
|
+
end
|
111
|
+
|
112
|
+
private_class_method def self.default_server_options
|
113
|
+
{
|
114
|
+
redirect_uri: 'http://localhost:8080/callback',
|
115
|
+
scope: nil,
|
116
|
+
endpoint: '/rpc',
|
117
|
+
headers: {},
|
118
|
+
read_timeout: 30,
|
119
|
+
retries: 3,
|
120
|
+
retry_backoff: 1,
|
121
|
+
name: nil,
|
122
|
+
logger: nil,
|
123
|
+
storage: nil
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|