Skip to content

Commit cad0636

Browse files
authored
Merge pull request #125 from patvice/dark-mode-logo-support
Updated branding assets for dark mode compatibility in docs and README
2 parents 9f1fffc + 29b2dc3 commit cad0636

File tree

9 files changed

+229
-44
lines changed

9 files changed

+229
-44
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<div align="center">
22

3-
<img src="docs/assets/images/rubyllm-mcp-logo-text.svg" alt="RubyLLM::MCP" height="120" width="250">
3+
<img src="docs/assets/images/rubyllm-mcp-logo-text.svg#gh-light-mode-only" alt="RubyLLM::MCP" height="120" width="250">
4+
<img src="docs/assets/images/rubyllm-mcp-logo-text-white.svg#gh-dark-mode-only" alt="RubyLLM::MCP" height="120" width="250">
45

56
<strong>MCP for Ruby and RubyLLM, as easy as possible.</strong>
67

docs/_includes/head_custom.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
}
3333

3434
function applyTheme(theme) {
35-
if (!window.jtd || typeof window.jtd.setTheme !== 'function') return;
36-
window.jtd.setTheme(theme);
35+
document.documentElement.setAttribute('data-rubyllm-theme', theme);
36+
if (window.jtd && typeof window.jtd.setTheme === 'function') {
37+
window.jtd.setTheme(theme);
38+
}
3739
syncToggle(theme);
3840
}
3941

docs/_sass/custom/custom.scss

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,48 @@
55
gap: 1em;
66
}
77

8+
.logo-container .home-logo {
9+
display: block;
10+
width: 250px;
11+
max-width: 100%;
12+
height: auto;
13+
}
14+
15+
.logo-container .home-logo-dark {
16+
display: none;
17+
}
18+
19+
html[data-rubyllm-theme="dark"] .logo-container .home-logo-light {
20+
display: none;
21+
}
22+
23+
html[data-rubyllm-theme="dark"] .logo-container .home-logo-dark {
24+
display: block;
25+
}
26+
27+
/* Fallback to OS preference only when explicit app theme is not set to light. */
28+
@media (prefers-color-scheme: dark) {
29+
html:not([data-rubyllm-theme="light"]) .logo-container .home-logo-light {
30+
display: none;
31+
}
32+
33+
html:not([data-rubyllm-theme="light"]) .logo-container .home-logo-dark {
34+
display: block;
35+
}
36+
}
37+
38+
.logo-container .github-stars-badge {
39+
display: inline-flex;
40+
align-items: center;
41+
line-height: 1;
42+
}
43+
44+
.logo-container .github-stars-badge img {
45+
display: block;
46+
height: 30px;
47+
width: auto;
48+
}
49+
850
.badge-container {
951
display: flex;
1052
align-items: center;

docs/assets/images/rubyllm-mcp-logo-text-white.svg

Lines changed: 22 additions & 0 deletions
Loading

docs/assets/images/rubyllm-mcp-logo-text.svg

Lines changed: 13 additions & 1 deletion
Loading

docs/index.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ permalink: /
88

99
<h1>
1010
<div class="logo-container">
11-
<img src="/assets/images/rubyllm-mcp-logo-text.svg" alt="RubyLLM::MCP" height="120" width="250">
12-
<iframe src="https://ghbtns.com/github-btn.html?user=patvice&repo=ruby_llm-mcp&type=star&count=true&size=large" frameborder="0" scrolling="0" width="170" height="30" title="GitHub" style="vertical-align: middle; display: inline-block;"></iframe>
11+
<img class="home-logo home-logo-light" src="/assets/images/rubyllm-mcp-logo-text.svg" alt="RubyLLM::MCP" height="120" width="250">
12+
<img class="home-logo home-logo-dark" src="/assets/images/rubyllm-mcp-logo-text-white.svg" alt="RubyLLM::MCP" height="120" width="250">
13+
<a class="github-stars-badge" href="https://github.com/patvice/ruby_llm-mcp" aria-label="GitHub stars for patvice/ruby_llm-mcp">
14+
<img src="https://img.shields.io/github/stars/patvice/ruby_llm-mcp?style=for-the-badge&logo=github&label=GitHub%20Stars&color=2F81F7" alt="GitHub stars">
15+
</a>
1316
</div>
1417
</h1>
1518

lib/ruby_llm/mcp/auth/browser/callback_server.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ module Browser
77
# Callback server wrapper for clean shutdown
88
# Manages server lifecycle and thread coordination
99
class CallbackServer
10-
def initialize(server, thread, stop_proc)
10+
def initialize(server, thread, stop_proc, start_proc = nil)
1111
@server = server
1212
@thread = thread
1313
@stop_proc = stop_proc
14+
@start_proc = start_proc || -> {}
15+
end
16+
17+
# Start callback processing loop
18+
def start
19+
@start_proc.call
1420
end
1521

1622
# Shutdown server and cleanup resources

lib/ruby_llm/mcp/auth/browser_oauth_provider.rb

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,11 @@ def authenticate(timeout: 300, auto_open_browser: true)
139139
server = start_callback_server(result, mutex, condition)
140140

141141
begin
142-
# 4. Open browser to authorization URL
143-
if auto_open_browser
144-
@opener.open_browser(auth_url)
145-
@synchronized_logger.info("\nOpening browser for authorization...")
146-
@synchronized_logger.info("If browser doesn't open automatically, visit this URL:")
147-
else
148-
@synchronized_logger.info("\nPlease visit this URL to authorize:")
149-
end
150-
@synchronized_logger.info(auth_url)
151-
@synchronized_logger.info("\nWaiting for authorization...")
142+
announce_authorization_flow(auth_url, auto_open_browser)
143+
144+
# Allow callback worker to begin processing only after setup logging/browser open
145+
# to reduce cross-thread test-double races under JRuby.
146+
server.start
152147

153148
# 5. Wait for callback with timeout
154149
mutex.synchronize do
@@ -267,29 +262,13 @@ def start_callback_server(result, mutex, condition)
267262
server = @http_server.start_server
268263
@synchronized_logger.debug("Started callback server on http://127.0.0.1:#{@callback_port}#{@callback_path}")
269264

270-
running = true
271-
272-
# Start server in background thread
273-
thread = Thread.new do
274-
while running
275-
begin
276-
# Use wait_readable with timeout to allow checking running flag
277-
next unless server.wait_readable(0.5)
278-
279-
client = server.accept
280-
handle_http_request(client, result, mutex, condition)
281-
rescue IOError, Errno::EBADF
282-
# Server was closed, exit loop
283-
break
284-
rescue StandardError => e
285-
mark_callback_failure(result, mutex, condition, e)
286-
break
287-
end
288-
end
289-
end
265+
control = build_callback_thread_control
266+
thread = build_callback_worker_thread(server, result, mutex, condition, control)
267+
stop_proc = -> { stop_callback_worker(control) }
268+
start_proc = -> { start_callback_worker(control) }
290269

291270
# Return wrapper with shutdown method
292-
Browser::CallbackServer.new(server, thread, -> { running = false })
271+
Browser::CallbackServer.new(server, thread, stop_proc, start_proc)
293272
end
294273

295274
# Handle incoming HTTP request on callback server
@@ -298,6 +277,8 @@ def start_callback_server(result, mutex, condition)
298277
# @param mutex [Mutex] synchronization mutex
299278
# @param condition [ConditionVariable] wait condition
300279
def handle_http_request(client, result, mutex, condition)
280+
callback_result = nil
281+
301282
@http_server.configure_client_socket(client)
302283

303284
request_line = @http_server.read_request_line(client)
@@ -317,18 +298,17 @@ def handle_http_request(client, result, mutex, condition)
317298
# Parse and extract OAuth parameters
318299
params = @callback_handler.parse_callback_params(path, @http_server)
319300
oauth_params = @callback_handler.extract_oauth_params(params)
320-
321-
# Update result with OAuth parameters
322-
@callback_handler.update_result_with_oauth_params(oauth_params, result, mutex, condition)
301+
callback_result = build_callback_result(oauth_params)
323302

324303
# Send response
325-
if result[:error]
326-
@http_server.send_http_response(client, 400, "text/html", @pages.error_page(result[:error]))
304+
if callback_result[:error]
305+
@http_server.send_http_response(client, 400, "text/html", @pages.error_page(callback_result[:error]))
327306
else
328307
@http_server.send_http_response(client, 200, "text/html", @pages.success_page)
329308
end
330309
ensure
331-
client&.close
310+
apply_callback_result(callback_result, result, mutex, condition) if callback_result
311+
close_callback_client(client)
332312
end
333313

334314
# Wake the waiting authentication flow with a deterministic error when callback
@@ -344,6 +324,103 @@ def mark_callback_failure(result, mutex, condition, error)
344324

345325
@synchronized_logger.warn("OAuth callback worker failed: #{error.class}: #{error.message}")
346326
end
327+
328+
def build_callback_result(oauth_params)
329+
if oauth_params[:error]
330+
{ code: nil, state: nil, error: oauth_params[:error_description] || oauth_params[:error] }
331+
elsif oauth_params[:code] && oauth_params[:state]
332+
{ code: oauth_params[:code], state: oauth_params[:state], error: nil }
333+
else
334+
{ code: nil, state: nil, error: "Invalid callback: missing code or state parameter" }
335+
end
336+
end
337+
338+
def apply_callback_result(callback_result, result, mutex, condition)
339+
mutex.synchronize do
340+
return if result[:completed]
341+
342+
result[:code] = callback_result[:code]
343+
result[:state] = callback_result[:state]
344+
result[:error] = callback_result[:error]
345+
result[:completed] = true
346+
condition.signal
347+
end
348+
end
349+
350+
def close_callback_client(client)
351+
client&.close
352+
rescue IOError, SystemCallError => e
353+
@synchronized_logger.debug("Error closing OAuth callback client socket: #{e.class}: #{e.message}")
354+
end
355+
356+
def announce_authorization_flow(auth_url, auto_open_browser)
357+
if auto_open_browser
358+
@opener.open_browser(auth_url)
359+
@synchronized_logger.info("\nOpening browser for authorization...")
360+
@synchronized_logger.info("If browser doesn't open automatically, visit this URL:")
361+
else
362+
@synchronized_logger.info("\nPlease visit this URL to authorize:")
363+
end
364+
@synchronized_logger.info(auth_url)
365+
@synchronized_logger.info("\nWaiting for authorization...")
366+
end
367+
368+
def build_callback_thread_control
369+
{
370+
mutex: Mutex.new,
371+
condition: ConditionVariable.new,
372+
running: true,
373+
accepting: false
374+
}
375+
end
376+
377+
def build_callback_worker_thread(server, result, result_mutex, condition, control)
378+
Thread.new do
379+
wait_for_callback_worker_start(control)
380+
381+
while callback_worker_running?(control)
382+
begin
383+
# Use wait_readable with timeout to allow checking stop signal
384+
next unless server.wait_readable(0.5)
385+
386+
client = server.accept
387+
handle_http_request(client, result, result_mutex, condition)
388+
break if result_mutex.synchronize { result[:completed] }
389+
rescue IOError, Errno::EBADF
390+
# Server was closed, exit loop
391+
break
392+
rescue StandardError => e
393+
mark_callback_failure(result, result_mutex, condition, e)
394+
break
395+
end
396+
end
397+
end
398+
end
399+
400+
def wait_for_callback_worker_start(control)
401+
control[:mutex].synchronize do
402+
control[:condition].wait(control[:mutex]) until control[:accepting] || !control[:running]
403+
end
404+
end
405+
406+
def callback_worker_running?(control)
407+
control[:mutex].synchronize { control[:running] }
408+
end
409+
410+
def start_callback_worker(control)
411+
control[:mutex].synchronize do
412+
control[:accepting] = true
413+
control[:condition].signal
414+
end
415+
end
416+
417+
def stop_callback_worker(control)
418+
control[:mutex].synchronize do
419+
control[:running] = false
420+
control[:accepting] = true
421+
control[:condition].broadcast
422+
end
423+
end
347424
end
348425
end
349426
end

0 commit comments

Comments
 (0)