Skip to content

Commit a373847

Browse files
committed
fixed lint issues
1 parent 0cac06f commit a373847

File tree

11 files changed

+148
-140
lines changed

11 files changed

+148
-140
lines changed

examples/oauth/browser_oauth.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,3 @@
8282
puts "4. Uncomment the authentication code above"
8383
puts "5. Run: ruby #{__FILE__}"
8484
puts "=" * 60
85-
86-
87-

examples/oauth/custom_storage.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,3 @@ def get_token(server_url)
275275
EXAMPLES
276276

277277
puts "=" * 60
278-
279-
280-

examples/oauth/standard_oauth.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,3 @@
126126
puts "4. Uncomment the interactive code above"
127127
puts "5. Run: ruby #{__FILE__}"
128128
puts "=" * 60
129-
130-
131-

lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ def initialize(url:, headers: {}, version: :http2, request_timeout: 10_000)
1212

1313
@native_transport = RubyLLM::MCP::Native::Transports::SSE.new(
1414
url: url,
15-
headers: headers,
16-
version: version,
1715
coordinator: @coordinator,
18-
request_timeout: request_timeout
16+
request_timeout: request_timeout,
17+
options: {
18+
headers: headers,
19+
version: version
20+
}
1921
)
2022
end
2123

lib/ruby_llm/mcp/auth/oauth_provider.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,17 @@ def handle_authentication_challenge(www_authenticate: nil, resource_metadata_url
164164
logger.debug(" Requested scope: #{requested_scope}") if requested_scope
165165

166166
# Parse WWW-Authenticate header if provided
167+
final_requested_scope = requested_scope
167168
if www_authenticate
168169
challenge_info = parse_www_authenticate(www_authenticate)
169-
resource_metadata_url ||= challenge_info[:resource_metadata_url]
170-
requested_scope ||= challenge_info[:scope]
170+
final_requested_scope ||= challenge_info[:scope]
171+
# NOTE: resource_metadata_url from challenge_info could be used for future discovery
171172
end
172173

173174
# Update scope if server requested different scope
174-
if requested_scope && requested_scope != scope
175-
logger.debug("Updating scope from '#{scope}' to '#{requested_scope}'")
176-
self.scope = requested_scope
175+
if final_requested_scope && final_requested_scope != scope
176+
logger.debug("Updating scope from '#{scope}' to '#{final_requested_scope}'")
177+
self.scope = final_requested_scope
177178
end
178179

179180
# Try to refresh existing token

lib/ruby_llm/mcp/native/transport.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,23 @@ def build_transport
5858
transport_config.merge!(options)
5959
end
6060

61+
# Handle SSE transport specially - it uses options hash pattern
62+
if transport_type == :sse
63+
url = transport_config.delete(:url) || transport_config.delete("url")
64+
request_timeout = transport_config.delete(:request_timeout) ||
65+
transport_config.delete("request_timeout") ||
66+
MCP.config.request_timeout
67+
# Everything else goes into options
68+
options_hash = transport_config.dup
69+
transport_config.clear
70+
transport_config[:url] = url
71+
transport_config[:request_timeout] = request_timeout
72+
transport_config[:options] = options_hash
73+
end
74+
6175
# Remove OAuth-specific params from transports that don't support them
6276
# This allows other arbitrary params (like timeout) to pass through for testing
63-
unless %i[streamable streamable_http].include?(transport_type)
77+
unless %i[streamable streamable_http sse].include?(transport_type)
6478
transport_config.delete(:oauth_provider)
6579
transport_config.delete(:oauth)
6680
end

lib/ruby_llm/mcp/native/transports/sse.rb

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ class SSE
99

1010
attr_reader :headers, :id, :coordinator
1111

12-
def initialize(url:, coordinator:, request_timeout:, version: :http2, headers: {}, oauth_provider: nil, options: {})
12+
def initialize(url:, coordinator:, request_timeout:, options: {})
1313
@event_url = url
1414
@messages_url = nil
1515
@coordinator = coordinator
1616
@request_timeout = request_timeout
17-
@version = version
1817

19-
# Extract oauth_provider from options if present
18+
# Extract options
2019
extracted_options = options.dup
21-
oauth_provider = extracted_options.delete(:oauth_provider) || oauth_provider
20+
@version = extracted_options.delete(:version) || :http2
21+
headers = extracted_options.delete(:headers) || {}
22+
oauth_provider = extracted_options.delete(:oauth_provider)
2223

2324
uri = URI.parse(url)
2425
@root_url = "#{uri.scheme}://#{uri.host}"
@@ -118,7 +119,7 @@ def send_request(body, request_id)
118119
case response.status
119120
when 200, 202
120121
# Success
121-
return
122+
nil
122123
when 401
123124
handle_authentication_challenge(response, body, request_id)
124125
else
@@ -150,61 +151,65 @@ def build_request_headers
150151
end
151152

152153
def handle_authentication_challenge(response, original_body, request_id)
153-
# If we've already attempted auth retry, don't try again
154-
if @auth_retry_attempted
155-
RubyLLM::MCP.logger.warn("Authentication retry already attempted, raising error")
156-
@auth_retry_attempted = false
157-
raise Errors::AuthenticationRequiredError.new(
158-
message: "OAuth authentication required (401 Unauthorized) - retry failed"
159-
)
160-
end
161-
162-
unless @oauth_provider
163-
raise Errors::AuthenticationRequiredError.new(
164-
message: "OAuth authentication required (401 Unauthorized) but no OAuth provider configured"
165-
)
166-
end
154+
check_retry_guard!
155+
check_oauth_provider_configured!
167156

168157
RubyLLM::MCP.logger.info("Received 401 Unauthorized, attempting automatic authentication")
169158

170159
www_authenticate = response.headers["www-authenticate"]
171160
resource_metadata_url = response.headers["mcp-resource-metadata-url"]
172161
@resource_metadata_url = resource_metadata_url if resource_metadata_url
173162

174-
begin
175-
@auth_retry_attempted = true
163+
attempt_authentication_retry(www_authenticate, resource_metadata_url, original_body, request_id)
164+
end
176165

177-
success = @oauth_provider.handle_authentication_challenge(
178-
www_authenticate: www_authenticate,
179-
resource_metadata_url: resource_metadata_url,
180-
requested_scope: nil
181-
)
166+
def check_retry_guard!
167+
return unless @auth_retry_attempted
182168

183-
if success
184-
RubyLLM::MCP.logger.info("Authentication challenge handled successfully, retrying request")
169+
RubyLLM::MCP.logger.warn("Authentication retry already attempted, raising error")
170+
@auth_retry_attempted = false
171+
raise Errors::AuthenticationRequiredError.new(
172+
message: "OAuth authentication required (401 Unauthorized) - retry failed"
173+
)
174+
end
185175

186-
# Retry the original request (flag stays true to prevent loop)
187-
send_request(original_body, request_id)
176+
def check_oauth_provider_configured!
177+
return if @oauth_provider
188178

189-
# Only reset flag after successful retry
190-
@auth_retry_attempted = false
191-
return
192-
end
193-
rescue Errors::AuthenticationRequiredError => e
194-
@auth_retry_attempted = false
195-
raise e
196-
rescue StandardError => e
179+
raise Errors::AuthenticationRequiredError.new(
180+
message: "OAuth authentication required (401 Unauthorized) but no OAuth provider configured"
181+
)
182+
end
183+
184+
def attempt_authentication_retry(www_authenticate, resource_metadata_url, original_body, request_id)
185+
@auth_retry_attempted = true
186+
187+
success = @oauth_provider.handle_authentication_challenge(
188+
www_authenticate: www_authenticate,
189+
resource_metadata_url: resource_metadata_url,
190+
requested_scope: nil
191+
)
192+
193+
if success
194+
RubyLLM::MCP.logger.info("Authentication challenge handled successfully, retrying request")
195+
send_request(original_body, request_id)
197196
@auth_retry_attempted = false
198-
RubyLLM::MCP.logger.error("Authentication challenge handling failed: #{e.message}")
199-
raise Errors::AuthenticationRequiredError.new(
200-
message: "OAuth authentication failed: #{e.message}"
201-
)
197+
return
202198
end
203199

204200
@auth_retry_attempted = false
205201
raise Errors::AuthenticationRequiredError.new(
206202
message: "OAuth authentication required (401 Unauthorized)"
207203
)
204+
rescue Errors::AuthenticationRequiredError => e
205+
@auth_retry_attempted = false
206+
raise e
207+
rescue StandardError => e
208+
@auth_retry_attempted = false
209+
RubyLLM::MCP.logger.error("Authentication challenge handling failed: #{e.message}")
210+
raise Errors::AuthenticationRequiredError.new(
211+
message: "OAuth authentication failed: #{e.message}"
212+
)
208213
end
209214

210215
def start_sse_listener

lib/ruby_llm/mcp/native/transports/streamable_http.rb

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,24 @@ def initialize( # rubocop:disable Metrics/ParameterLists
7373
@protocol_version = nil
7474
@session_id = session_id
7575

76-
@resource_metadata_url = nil
77-
@client_id = SecureRandom.uuid
78-
79-
@reconnection_options = ReconnectionOptions.new(**reconnection)
80-
@oauth_provider = oauth_provider
81-
@rate_limiter = Support::RateLimiter.new(**rate_limit) if rate_limit
82-
83-
@id_counter = 0
84-
@id_mutex = Mutex.new
85-
@pending_requests = {}
86-
@pending_mutex = Mutex.new
87-
@running = true
88-
@abort_controller = nil
89-
@sse_thread = nil
90-
@sse_mutex = Mutex.new
91-
92-
# Track if we've attempted auth flow to prevent infinite loops
93-
@auth_retry_attempted = false
76+
@resource_metadata_url = nil
77+
@client_id = SecureRandom.uuid
78+
79+
@reconnection_options = ReconnectionOptions.new(**reconnection)
80+
@oauth_provider = oauth_provider
81+
@rate_limiter = Support::RateLimiter.new(**rate_limit) if rate_limit
82+
83+
@id_counter = 0
84+
@id_mutex = Mutex.new
85+
@pending_requests = {}
86+
@pending_mutex = Mutex.new
87+
@running = true
88+
@abort_controller = nil
89+
@sse_thread = nil
90+
@sse_mutex = Mutex.new
91+
92+
# Track if we've attempted auth flow to prevent infinite loops
93+
@auth_retry_attempted = false
9494

9595
# Thread-safe collection of all HTTPX clients
9696
@clients = []
@@ -489,67 +489,64 @@ def extract_resource_metadata_url(response)
489489
end
490490

491491
def handle_authentication_challenge(response, request_id, original_message)
492-
# If we've already attempted auth retry, don't try again (prevent infinite loop)
493-
if @auth_retry_attempted
494-
RubyLLM::MCP.logger.warn("Authentication retry already attempted, raising error")
495-
@auth_retry_attempted = false # Reset for next request
496-
raise Errors::AuthenticationRequiredError.new(
497-
message: "OAuth authentication required (401 Unauthorized) - retry failed"
498-
)
499-
end
500-
501-
# No OAuth provider configured - can't handle challenge
502-
unless @oauth_provider
503-
raise Errors::AuthenticationRequiredError.new(
504-
message: "OAuth authentication required (401 Unauthorized) but no OAuth provider configured"
505-
)
506-
end
492+
check_retry_guard!
493+
check_oauth_provider_configured!
507494

508495
RubyLLM::MCP.logger.info("Received 401 Unauthorized, attempting automatic authentication")
509496

510-
# Extract challenge information from response
511497
www_authenticate = response.headers["www-authenticate"]
512498
resource_metadata_url = extract_resource_metadata_url(response)
513499

514-
begin
515-
# Set flag to prevent infinite retry loop
516-
@auth_retry_attempted = true
517-
518-
# Ask OAuth provider to handle the challenge
519-
success = @oauth_provider.handle_authentication_challenge(
520-
www_authenticate: www_authenticate,
521-
resource_metadata_url: resource_metadata_url&.to_s,
522-
requested_scope: nil
523-
)
500+
attempt_authentication_retry(www_authenticate, resource_metadata_url, request_id, original_message)
501+
end
524502

525-
if success
526-
RubyLLM::MCP.logger.info("Authentication challenge handled successfully, retrying request")
503+
def check_retry_guard!
504+
return unless @auth_retry_attempted
527505

528-
# Retry the original request with new auth (flag stays true to prevent loop)
529-
result = send_http_request(original_message, request_id, is_initialization: false)
506+
RubyLLM::MCP.logger.warn("Authentication retry already attempted, raising error")
507+
@auth_retry_attempted = false
508+
raise Errors::AuthenticationRequiredError.new(
509+
message: "OAuth authentication required (401 Unauthorized) - retry failed"
510+
)
511+
end
530512

531-
# Only reset flag after successful retry
532-
@auth_retry_attempted = false
533-
return result
534-
end
535-
rescue Errors::AuthenticationRequiredError => e
536-
# Reset flag and re-raise
537-
@auth_retry_attempted = false
538-
raise e
539-
rescue StandardError => e
540-
# Reset flag and wrap error
513+
def check_oauth_provider_configured!
514+
return if @oauth_provider
515+
516+
raise Errors::AuthenticationRequiredError.new(
517+
message: "OAuth authentication required (401 Unauthorized) but no OAuth provider configured"
518+
)
519+
end
520+
521+
def attempt_authentication_retry(www_authenticate, resource_metadata_url, request_id, original_message)
522+
@auth_retry_attempted = true
523+
524+
success = @oauth_provider.handle_authentication_challenge(
525+
www_authenticate: www_authenticate,
526+
resource_metadata_url: resource_metadata_url&.to_s,
527+
requested_scope: nil
528+
)
529+
530+
if success
531+
RubyLLM::MCP.logger.info("Authentication challenge handled successfully, retrying request")
532+
result = send_http_request(original_message, request_id, is_initialization: false)
541533
@auth_retry_attempted = false
542-
RubyLLM::MCP.logger.error("Authentication challenge handling failed: #{e.message}")
543-
raise Errors::AuthenticationRequiredError.new(
544-
message: "OAuth authentication failed: #{e.message}"
545-
)
534+
return result
546535
end
547536

548-
# If we get here, authentication didn't succeed
549537
@auth_retry_attempted = false
550538
raise Errors::AuthenticationRequiredError.new(
551539
message: "OAuth authentication required (401 Unauthorized)"
552540
)
541+
rescue Errors::AuthenticationRequiredError => e
542+
@auth_retry_attempted = false
543+
raise e
544+
rescue StandardError => e
545+
@auth_retry_attempted = false
546+
RubyLLM::MCP.logger.error("Authentication challenge handling failed: #{e.message}")
547+
raise Errors::AuthenticationRequiredError.new(
548+
message: "OAuth authentication failed: #{e.message}"
549+
)
553550
end
554551

555552
def start_sse_stream(options = StartSSEOptions.new)

spec/ruby_llm/mcp/auth/browser_oauth_provider_spec.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,20 +1238,19 @@
12381238
before do
12391239
allow(oauth_provider).to receive(:handle_authentication_challenge)
12401240
.and_raise(RubyLLM::MCP::Errors::AuthenticationRequiredError.new(message: "Interactive auth required"))
1241-
allow(oauth_provider).to receive(:start_authorization_flow).and_return(auth_url)
12421241
allow(TCPServer).to receive(:new).and_return(tcp_server)
12431242
allow(tcp_server).to receive(:close)
1244-
allow(tcp_server).to receive(:closed?).and_return(false)
12451243
allow(tcp_server).to receive(:wait_readable).and_return(true, false)
1246-
allow(tcp_server).to receive(:accept).and_return(client_socket)
1244+
allow(tcp_server).to receive_messages(closed?: false, accept: client_socket)
12471245
allow(client_socket).to receive(:setsockopt)
12481246
allow(client_socket).to receive(:gets).and_return(
12491247
"GET /callback?code=test&state=test HTTP/1.1\r\n",
12501248
"\r\n"
12511249
)
12521250
allow(client_socket).to receive(:write)
12531251
allow(client_socket).to receive(:close)
1254-
allow(oauth_provider).to receive(:complete_authorization_flow).and_return(token)
1252+
allow(oauth_provider).to receive_messages(start_authorization_flow: auth_url,
1253+
complete_authorization_flow: token)
12551254
end
12561255

12571256
it "falls back to browser-based authentication" do

0 commit comments

Comments
 (0)