ruby_llm_swarm-mcp 0.8.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- metadata +184 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module MCP
|
|
7
|
+
module Adapters
|
|
8
|
+
# RubyLLM Adapter - wraps the Native protocol implementation
|
|
9
|
+
# This is a thin bridge between the public API and Native::Client
|
|
10
|
+
class RubyLLMAdapter < BaseAdapter
|
|
11
|
+
extend Forwardable
|
|
12
|
+
|
|
13
|
+
supports :tools, :prompts, :resources, :resource_templates,
|
|
14
|
+
:completions, :logging, :sampling, :roots,
|
|
15
|
+
:notifications, :progress_tracking, :human_in_the_loop,
|
|
16
|
+
:elicitation, :subscriptions, :list_changed_notifications
|
|
17
|
+
|
|
18
|
+
supports_transport :stdio, :sse, :streamable, :streamable_http
|
|
19
|
+
|
|
20
|
+
attr_reader :native_client
|
|
21
|
+
|
|
22
|
+
def initialize(client, transport_type:, config: {})
|
|
23
|
+
validate_transport!(transport_type)
|
|
24
|
+
super
|
|
25
|
+
|
|
26
|
+
request_timeout = config.delete(:request_timeout)
|
|
27
|
+
transport_config = prepare_transport_config(config, transport_type)
|
|
28
|
+
|
|
29
|
+
@native_client = Native::Client.new(
|
|
30
|
+
name: client.name,
|
|
31
|
+
transport_type: transport_type,
|
|
32
|
+
transport_config: transport_config,
|
|
33
|
+
request_timeout: request_timeout,
|
|
34
|
+
human_in_the_loop_callback: build_human_in_the_loop_callback(client),
|
|
35
|
+
roots_callback: -> { client.roots.paths },
|
|
36
|
+
logging_enabled: client.logging_handler_enabled?,
|
|
37
|
+
logging_level: client.on_logging_level,
|
|
38
|
+
elicitation_enabled: client.elicitation_enabled?,
|
|
39
|
+
progress_tracking_enabled: client.tracking_progress?,
|
|
40
|
+
elicitation_callback: ->(elicitation) { client.on[:elicitation]&.call(elicitation) },
|
|
41
|
+
sampling_callback: ->(sample) { client.on[:sampling]&.call(sample) },
|
|
42
|
+
notification_callback: ->(notification) { NotificationHandler.new(client).execute(notification) }
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def_delegators :@native_client,
|
|
47
|
+
:start, :stop, :restart!, :alive?, :ping,
|
|
48
|
+
:capabilities, :client_capabilities, :protocol_version,
|
|
49
|
+
:tool_list, :execute_tool,
|
|
50
|
+
:resource_list, :resource_read, :resource_template_list,
|
|
51
|
+
:resources_subscribe,
|
|
52
|
+
:prompt_list, :execute_prompt,
|
|
53
|
+
:completion_resource, :completion_prompt,
|
|
54
|
+
:set_logging, :set_progress_tracking,
|
|
55
|
+
:initialize_notification, :cancelled_notification,
|
|
56
|
+
:roots_list_change_notification,
|
|
57
|
+
:ping_response, :roots_list_response,
|
|
58
|
+
:sampling_create_message_response,
|
|
59
|
+
:error_response, :elicitation_response,
|
|
60
|
+
:register_in_flight_request, :unregister_in_flight_request,
|
|
61
|
+
:cancel_in_flight_request
|
|
62
|
+
|
|
63
|
+
def register_resource(resource)
|
|
64
|
+
client.linked_resources << resource
|
|
65
|
+
client.resources[resource.name] = resource
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def prepare_transport_config(config, transport_type)
|
|
71
|
+
transport_config = config.dup
|
|
72
|
+
|
|
73
|
+
if %i[sse streamable streamable_http].include?(transport_type)
|
|
74
|
+
oauth_provider = Auth::TransportOauthHelper.create_oauth_provider(transport_config) if Auth::TransportOauthHelper.oauth_config_present?(transport_config)
|
|
75
|
+
|
|
76
|
+
Auth::TransportOauthHelper.prepare_http_transport_config(transport_config, oauth_provider)
|
|
77
|
+
elsif transport_type == :stdio
|
|
78
|
+
Auth::TransportOauthHelper.prepare_stdio_transport_config(transport_config)
|
|
79
|
+
else
|
|
80
|
+
transport_config
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_human_in_the_loop_callback(client)
|
|
85
|
+
lambda do |name, params|
|
|
86
|
+
!client.human_in_the_loop? || client.on[:human_in_the_loop].call(name, params)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Attachment < RubyLLM::Attachment
|
|
6
|
+
attr_reader :content, :mime_type
|
|
7
|
+
|
|
8
|
+
def initialize(content, mime_type) # rubocop:disable Lint/MissingSuper
|
|
9
|
+
@content = content
|
|
10
|
+
@mime_type = mime_type
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def encoded
|
|
14
|
+
@content
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Auth
|
|
6
|
+
module Browser
|
|
7
|
+
# Handles OAuth callback request processing
|
|
8
|
+
# Extracts and validates OAuth parameters from callback requests
|
|
9
|
+
class CallbackHandler
|
|
10
|
+
attr_reader :callback_path, :logger
|
|
11
|
+
|
|
12
|
+
def initialize(callback_path:, logger: nil)
|
|
13
|
+
@callback_path = callback_path
|
|
14
|
+
@logger = logger || MCP.logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Validate that the request path matches the expected callback path
|
|
18
|
+
# @param path [String] request path
|
|
19
|
+
# @return [Boolean] true if path is valid
|
|
20
|
+
def valid_callback_path?(path)
|
|
21
|
+
uri_path, = path.split("?", 2)
|
|
22
|
+
uri_path == @callback_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Parse callback parameters from path
|
|
26
|
+
# @param path [String] full request path with query string
|
|
27
|
+
# @param http_server [HttpServer] HTTP server instance for parsing
|
|
28
|
+
# @return [Hash] parsed parameters
|
|
29
|
+
def parse_callback_params(path, http_server)
|
|
30
|
+
_, query_string = path.split("?", 2)
|
|
31
|
+
params = http_server.parse_query_params(query_string || "")
|
|
32
|
+
@logger.debug("Callback params: #{params.keys.join(', ')}")
|
|
33
|
+
params
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extract OAuth parameters from parsed params
|
|
37
|
+
# @param params [Hash] parsed query parameters
|
|
38
|
+
# @return [Hash] OAuth parameters (code, state, error, error_description)
|
|
39
|
+
def extract_oauth_params(params)
|
|
40
|
+
{
|
|
41
|
+
code: params["code"],
|
|
42
|
+
state: params["state"],
|
|
43
|
+
error: params["error"],
|
|
44
|
+
error_description: params["error_description"]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Update result hash with OAuth parameters (thread-safe)
|
|
49
|
+
# @param oauth_params [Hash] OAuth parameters
|
|
50
|
+
# @param result [Hash] result container
|
|
51
|
+
# @param mutex [Mutex] synchronization mutex
|
|
52
|
+
# @param condition [ConditionVariable] wait condition
|
|
53
|
+
def update_result_with_oauth_params(oauth_params, result, mutex, condition)
|
|
54
|
+
mutex.synchronize do
|
|
55
|
+
if oauth_params[:error]
|
|
56
|
+
result[:error] = oauth_params[:error_description] || oauth_params[:error]
|
|
57
|
+
elsif oauth_params[:code] && oauth_params[:state]
|
|
58
|
+
result[:code] = oauth_params[:code]
|
|
59
|
+
result[:state] = oauth_params[:state]
|
|
60
|
+
else
|
|
61
|
+
result[:error] = "Invalid callback: missing code or state parameter"
|
|
62
|
+
end
|
|
63
|
+
result[:completed] = true
|
|
64
|
+
condition.signal
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Auth
|
|
6
|
+
module Browser
|
|
7
|
+
# Callback server wrapper for clean shutdown
|
|
8
|
+
# Manages server lifecycle and thread coordination
|
|
9
|
+
class CallbackServer
|
|
10
|
+
def initialize(server, thread, stop_proc)
|
|
11
|
+
@server = server
|
|
12
|
+
@thread = thread
|
|
13
|
+
@stop_proc = stop_proc
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Shutdown server and cleanup resources
|
|
17
|
+
# @return [nil] always returns nil
|
|
18
|
+
def shutdown
|
|
19
|
+
@stop_proc.call
|
|
20
|
+
@server.close unless @server.closed?
|
|
21
|
+
@thread.join(5) # Wait max 5 seconds for thread to finish
|
|
22
|
+
rescue StandardError
|
|
23
|
+
# Ignore shutdown errors
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Auth
|
|
6
|
+
module Browser
|
|
7
|
+
# HTTP server utilities for OAuth callback handling
|
|
8
|
+
# Provides lightweight HTTP server functionality without external dependencies
|
|
9
|
+
class HttpServer
|
|
10
|
+
attr_reader :port, :logger
|
|
11
|
+
|
|
12
|
+
def initialize(port:, logger: nil)
|
|
13
|
+
@port = port
|
|
14
|
+
@logger = logger || MCP.logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Start TCP server for OAuth callbacks
|
|
18
|
+
# @return [TCPServer] TCP server instance
|
|
19
|
+
# @raise [Errors::TransportError] if server cannot start
|
|
20
|
+
def start_server
|
|
21
|
+
TCPServer.new("127.0.0.1", @port)
|
|
22
|
+
rescue Errno::EADDRINUSE
|
|
23
|
+
raise Errors::TransportError.new(
|
|
24
|
+
message: "Cannot start OAuth callback server: port #{@port} is already in use. " \
|
|
25
|
+
"Please close the application using this port or choose a different callback_port."
|
|
26
|
+
)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
raise Errors::TransportError.new(
|
|
29
|
+
message: "Failed to start OAuth callback server on port #{@port}: #{e.message}"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Configure client socket with timeout
|
|
34
|
+
# @param client [TCPSocket] client socket
|
|
35
|
+
def configure_client_socket(client)
|
|
36
|
+
client.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [5, 0].pack("l_2"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Read HTTP request line from client
|
|
40
|
+
# @param client [TCPSocket] client socket
|
|
41
|
+
# @return [String, nil] request line
|
|
42
|
+
def read_request_line(client)
|
|
43
|
+
client.gets
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extract method and path from request line
|
|
47
|
+
# @param request_line [String] HTTP request line
|
|
48
|
+
# @return [Array<String, String>, nil] method and path, or nil if invalid
|
|
49
|
+
def extract_request_parts(request_line)
|
|
50
|
+
parts = request_line.split
|
|
51
|
+
return nil unless parts.length >= 2
|
|
52
|
+
|
|
53
|
+
parts[0..1]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Read HTTP headers from client
|
|
57
|
+
# @param client [TCPSocket] client socket
|
|
58
|
+
def read_http_headers(client)
|
|
59
|
+
header_count = 0
|
|
60
|
+
loop do
|
|
61
|
+
break if header_count >= 100
|
|
62
|
+
|
|
63
|
+
line = client.gets
|
|
64
|
+
break if line.nil? || line.strip.empty?
|
|
65
|
+
|
|
66
|
+
header_count += 1
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Parse URL query parameters
|
|
71
|
+
# @param query_string [String] query string
|
|
72
|
+
# @return [Hash] parsed parameters
|
|
73
|
+
def parse_query_params(query_string)
|
|
74
|
+
params = {}
|
|
75
|
+
query_string.split("&").each do |param|
|
|
76
|
+
next if param.empty?
|
|
77
|
+
|
|
78
|
+
key, value = param.split("=", 2)
|
|
79
|
+
params[CGI.unescape(key)] = CGI.unescape(value || "")
|
|
80
|
+
end
|
|
81
|
+
params
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Send HTTP response to client
|
|
85
|
+
# @param client [TCPSocket] client socket
|
|
86
|
+
# @param status [Integer] HTTP status code
|
|
87
|
+
# @param content_type [String] content type
|
|
88
|
+
# @param body [String] response body
|
|
89
|
+
def send_http_response(client, status, content_type, body)
|
|
90
|
+
status_text = case status
|
|
91
|
+
when 200 then "OK"
|
|
92
|
+
when 400 then "Bad Request"
|
|
93
|
+
when 404 then "Not Found"
|
|
94
|
+
else "Unknown"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
response = "HTTP/1.1 #{status} #{status_text}\r\n"
|
|
98
|
+
response += "Content-Type: #{content_type}\r\n"
|
|
99
|
+
response += "Content-Length: #{body.bytesize}\r\n"
|
|
100
|
+
response += "Connection: close\r\n"
|
|
101
|
+
response += "\r\n"
|
|
102
|
+
response += body
|
|
103
|
+
|
|
104
|
+
client.write(response)
|
|
105
|
+
rescue IOError, Errno::EPIPE => e
|
|
106
|
+
@logger.debug("Error sending response: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Auth
|
|
6
|
+
module Browser
|
|
7
|
+
# Browser opening utilities for different operating systems
|
|
8
|
+
# Handles cross-platform browser launching
|
|
9
|
+
class Opener
|
|
10
|
+
attr_reader :logger
|
|
11
|
+
|
|
12
|
+
def initialize(logger: nil)
|
|
13
|
+
@logger = logger || MCP.logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Open browser to URL
|
|
17
|
+
# @param url [String] URL to open
|
|
18
|
+
# @return [Boolean] true if successful
|
|
19
|
+
def open_browser(url)
|
|
20
|
+
case RbConfig::CONFIG["host_os"]
|
|
21
|
+
when /darwin/
|
|
22
|
+
system("open", url)
|
|
23
|
+
when /linux|bsd/
|
|
24
|
+
system("xdg-open", url)
|
|
25
|
+
when /mswin|mingw|cygwin/
|
|
26
|
+
system("start", url)
|
|
27
|
+
else
|
|
28
|
+
@logger.warn("Unknown operating system, cannot open browser automatically")
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
@logger.warn("Failed to open browser: #{e.message}")
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|