ferrum-mcp 1.0.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/.env.example +90 -0
- data/CHANGELOG.md +229 -0
- data/CONTRIBUTING.md +469 -0
- data/LICENSE +21 -0
- data/README.md +334 -0
- data/SECURITY.md +286 -0
- data/bin/ferrum-mcp +66 -0
- data/bin/lint +10 -0
- data/bin/serve +3 -0
- data/bin/test +4 -0
- data/docs/API_REFERENCE.md +1410 -0
- data/docs/CONFIGURATION.md +254 -0
- data/docs/DEPLOYMENT.md +846 -0
- data/docs/DOCKER.md +836 -0
- data/docs/DOCKER_BOTBROWSER.md +455 -0
- data/docs/GETTING_STARTED.md +249 -0
- data/docs/TROUBLESHOOTING.md +677 -0
- data/lib/ferrum_mcp/browser_manager.rb +101 -0
- data/lib/ferrum_mcp/cli/command_handler.rb +99 -0
- data/lib/ferrum_mcp/cli/server_runner.rb +166 -0
- data/lib/ferrum_mcp/configuration.rb +229 -0
- data/lib/ferrum_mcp/resource_manager.rb +223 -0
- data/lib/ferrum_mcp/server.rb +254 -0
- data/lib/ferrum_mcp/session.rb +227 -0
- data/lib/ferrum_mcp/session_manager.rb +183 -0
- data/lib/ferrum_mcp/tools/accept_cookies_tool.rb +458 -0
- data/lib/ferrum_mcp/tools/base_tool.rb +114 -0
- data/lib/ferrum_mcp/tools/clear_cookies_tool.rb +66 -0
- data/lib/ferrum_mcp/tools/click_tool.rb +218 -0
- data/lib/ferrum_mcp/tools/close_session_tool.rb +49 -0
- data/lib/ferrum_mcp/tools/create_session_tool.rb +146 -0
- data/lib/ferrum_mcp/tools/drag_and_drop_tool.rb +171 -0
- data/lib/ferrum_mcp/tools/evaluate_js_tool.rb +46 -0
- data/lib/ferrum_mcp/tools/execute_script_tool.rb +48 -0
- data/lib/ferrum_mcp/tools/fill_form_tool.rb +78 -0
- data/lib/ferrum_mcp/tools/find_by_text_tool.rb +153 -0
- data/lib/ferrum_mcp/tools/get_attribute_tool.rb +56 -0
- data/lib/ferrum_mcp/tools/get_cookies_tool.rb +70 -0
- data/lib/ferrum_mcp/tools/get_html_tool.rb +52 -0
- data/lib/ferrum_mcp/tools/get_session_info_tool.rb +40 -0
- data/lib/ferrum_mcp/tools/get_text_tool.rb +67 -0
- data/lib/ferrum_mcp/tools/get_title_tool.rb +42 -0
- data/lib/ferrum_mcp/tools/get_url_tool.rb +39 -0
- data/lib/ferrum_mcp/tools/go_back_tool.rb +49 -0
- data/lib/ferrum_mcp/tools/go_forward_tool.rb +49 -0
- data/lib/ferrum_mcp/tools/hover_tool.rb +76 -0
- data/lib/ferrum_mcp/tools/list_sessions_tool.rb +33 -0
- data/lib/ferrum_mcp/tools/navigate_tool.rb +59 -0
- data/lib/ferrum_mcp/tools/press_key_tool.rb +91 -0
- data/lib/ferrum_mcp/tools/query_shadow_dom_tool.rb +225 -0
- data/lib/ferrum_mcp/tools/refresh_tool.rb +49 -0
- data/lib/ferrum_mcp/tools/screenshot_tool.rb +121 -0
- data/lib/ferrum_mcp/tools/session_tool.rb +37 -0
- data/lib/ferrum_mcp/tools/set_cookie_tool.rb +77 -0
- data/lib/ferrum_mcp/tools/solve_captcha_tool.rb +528 -0
- data/lib/ferrum_mcp/transport/http_server.rb +93 -0
- data/lib/ferrum_mcp/transport/rate_limiter.rb +79 -0
- data/lib/ferrum_mcp/transport/stdio_server.rb +63 -0
- data/lib/ferrum_mcp/version.rb +5 -0
- data/lib/ferrum_mcp/whisper_service.rb +222 -0
- data/lib/ferrum_mcp.rb +35 -0
- metadata +248 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FerrumMCP
|
|
4
|
+
# Manages Ferrum browser lifecycle with BotBrowser integration
|
|
5
|
+
class BrowserManager
|
|
6
|
+
attr_reader :browser, :config, :logger
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
@logger = config.logger
|
|
11
|
+
@browser = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
raise BrowserError, 'Browser path is invalid' unless config.valid?
|
|
16
|
+
|
|
17
|
+
if config.using_botbrowser?
|
|
18
|
+
logger.info 'Starting browser with BotBrowser (anti-detection mode)...'
|
|
19
|
+
else
|
|
20
|
+
logger.info 'Starting browser with standard Chrome/Chromium...'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
browser_options_hash = {
|
|
24
|
+
browser_options: computed_browser_options,
|
|
25
|
+
headless: config.headless,
|
|
26
|
+
timeout: config.timeout,
|
|
27
|
+
process_timeout: ENV['CI'] ? 120 : config.timeout,
|
|
28
|
+
pending_connection_errors: false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Only set browser_path if explicitly configured
|
|
32
|
+
browser_options_hash[:browser_path] = config.browser_path if config.browser_path
|
|
33
|
+
|
|
34
|
+
@browser = Ferrum::Browser.new(**browser_options_hash)
|
|
35
|
+
|
|
36
|
+
logger.info 'Browser started successfully'
|
|
37
|
+
@browser
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
logger.error "Failed to start browser: #{e.message}"
|
|
40
|
+
raise BrowserError, "Failed to start browser: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop
|
|
44
|
+
return unless @browser
|
|
45
|
+
|
|
46
|
+
logger.info 'Stopping browser...'
|
|
47
|
+
@browser.quit
|
|
48
|
+
@browser = nil
|
|
49
|
+
logger.info 'Browser stopped'
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
logger.error "Error stopping browser: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def restart
|
|
55
|
+
stop
|
|
56
|
+
start
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def active?
|
|
60
|
+
!@browser.nil?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Compute browser options, merging defaults with session-specific options
|
|
66
|
+
def computed_browser_options
|
|
67
|
+
# Use merged options if config supports it (SessionConfiguration)
|
|
68
|
+
options = if config.respond_to?(:merged_browser_options)
|
|
69
|
+
config.merged_browser_options
|
|
70
|
+
else
|
|
71
|
+
browser_options
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Log BotBrowser profile usage
|
|
75
|
+
if config.using_botbrowser? && config.botbrowser_profile && File.exist?(config.botbrowser_profile)
|
|
76
|
+
logger.info "Using BotBrowser profile: #{config.botbrowser_profile}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
options
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def browser_options
|
|
83
|
+
options = {
|
|
84
|
+
'no-sandbox' => nil,
|
|
85
|
+
'disable-dev-shm-usage' => nil,
|
|
86
|
+
'disable-blink-features' => 'AutomationControlled',
|
|
87
|
+
'disable-gpu' => nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Additional options for CI environments
|
|
91
|
+
options['disable-setuid-sandbox'] = nil if ENV['CI']
|
|
92
|
+
|
|
93
|
+
# Add BotBrowser profile if configured
|
|
94
|
+
if config.using_botbrowser? && config.botbrowser_profile && File.exist?(config.botbrowser_profile)
|
|
95
|
+
options['bot-profile'] = config.botbrowser_profile
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
options
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'server_runner'
|
|
4
|
+
require_relative '../version'
|
|
5
|
+
|
|
6
|
+
module FerrumMCP
|
|
7
|
+
module CLI
|
|
8
|
+
# Handles CLI commands (help, version, start)
|
|
9
|
+
class CommandHandler
|
|
10
|
+
def self.handle(command, options)
|
|
11
|
+
case command
|
|
12
|
+
when 'start', nil
|
|
13
|
+
start_server(options)
|
|
14
|
+
when 'version', '-v', '--version'
|
|
15
|
+
show_version
|
|
16
|
+
when 'help', '-h', '--help'
|
|
17
|
+
show_help
|
|
18
|
+
else
|
|
19
|
+
show_unknown_command(command)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.start_server(options)
|
|
24
|
+
# Load dependencies only when starting server
|
|
25
|
+
require 'bundler/setup'
|
|
26
|
+
require 'dotenv/load'
|
|
27
|
+
|
|
28
|
+
# Set environment variables from options
|
|
29
|
+
ENV['MCP_SERVER_HOST'] = options[:host]
|
|
30
|
+
ENV['MCP_SERVER_PORT'] = options[:port].to_s
|
|
31
|
+
ENV['LOG_LEVEL'] = options[:log_level]
|
|
32
|
+
|
|
33
|
+
runner = ServerRunner.new(options)
|
|
34
|
+
runner.start
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.show_version
|
|
38
|
+
puts "FerrumMCP #{VERSION}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.show_help
|
|
42
|
+
puts <<~HELP
|
|
43
|
+
FerrumMCP - Browser Automation Server for Model Context Protocol
|
|
44
|
+
|
|
45
|
+
USAGE:
|
|
46
|
+
ferrum-mcp [COMMAND] [OPTIONS]
|
|
47
|
+
|
|
48
|
+
COMMANDS:
|
|
49
|
+
start Start the FerrumMCP server (default)
|
|
50
|
+
version Show version information
|
|
51
|
+
help Show this help message
|
|
52
|
+
|
|
53
|
+
OPTIONS:
|
|
54
|
+
-t, --transport TYPE Transport type: http or stdio (default: http)
|
|
55
|
+
-H, --host HOST Server host (default: 0.0.0.0)
|
|
56
|
+
-p, --port PORT Server port (default: 3000)
|
|
57
|
+
-l, --log-level LEVEL Log level: debug, info, warn, error (default: info)
|
|
58
|
+
-h, --help Show this help message
|
|
59
|
+
-v, --version Show version
|
|
60
|
+
|
|
61
|
+
EXAMPLES:
|
|
62
|
+
# Start HTTP server on default port
|
|
63
|
+
ferrum-mcp start
|
|
64
|
+
|
|
65
|
+
# Start HTTP server on custom port
|
|
66
|
+
ferrum-mcp start --port 8080
|
|
67
|
+
|
|
68
|
+
# Start STDIO server (for Claude Desktop)
|
|
69
|
+
ferrum-mcp start --transport stdio
|
|
70
|
+
|
|
71
|
+
# Start with debug logging
|
|
72
|
+
ferrum-mcp start --log-level debug
|
|
73
|
+
|
|
74
|
+
ENVIRONMENT VARIABLES:
|
|
75
|
+
MCP_SERVER_HOST Server host (default: 0.0.0.0)
|
|
76
|
+
MCP_SERVER_PORT Server port (default: 3000)
|
|
77
|
+
BROWSER_HEADLESS Run browser in headless mode (default: true)
|
|
78
|
+
BROWSER_TIMEOUT Browser timeout in seconds (default: 60)
|
|
79
|
+
LOG_LEVEL Log level (default: info)
|
|
80
|
+
|
|
81
|
+
See .env.example for all configuration options.
|
|
82
|
+
|
|
83
|
+
DOCUMENTATION:
|
|
84
|
+
Getting Started: https://github.com/Eth3rnit3/FerrumMCP/blob/main/docs/GETTING_STARTED.md
|
|
85
|
+
API Reference: https://github.com/Eth3rnit3/FerrumMCP/blob/main/docs/API_REFERENCE.md
|
|
86
|
+
Configuration: https://github.com/Eth3rnit3/FerrumMCP/blob/main/docs/CONFIGURATION.md
|
|
87
|
+
|
|
88
|
+
For more information, visit: https://github.com/Eth3rnit3/FerrumMCP
|
|
89
|
+
HELP
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.show_unknown_command(command)
|
|
93
|
+
warn "Unknown command: #{command}"
|
|
94
|
+
warn "Run 'ferrum-mcp help' for usage information"
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: ferrum_mcp is loaded in bin/ferrum-mcp before this file
|
|
4
|
+
require_relative '../transport/http_server'
|
|
5
|
+
require_relative '../transport/stdio_server'
|
|
6
|
+
|
|
7
|
+
module FerrumMCP
|
|
8
|
+
module CLI
|
|
9
|
+
# Handles server startup and lifecycle
|
|
10
|
+
class ServerRunner
|
|
11
|
+
attr_reader :config, :mcp_server, :transport_server
|
|
12
|
+
|
|
13
|
+
def initialize(options = {})
|
|
14
|
+
@options = options
|
|
15
|
+
@config = Configuration.new(transport: options[:transport])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate!
|
|
19
|
+
return if config.valid?
|
|
20
|
+
|
|
21
|
+
puts 'ERROR: Invalid browser configuration'
|
|
22
|
+
puts 'The specified BROWSER_PATH does not exist'
|
|
23
|
+
puts ''
|
|
24
|
+
puts 'Options:'
|
|
25
|
+
puts ' 1. Remove BROWSER_PATH to use system Chrome/Chromium'
|
|
26
|
+
puts ' 2. Set BROWSER_PATH to a valid browser executable'
|
|
27
|
+
puts ''
|
|
28
|
+
puts 'Example: export BROWSER_PATH=/path/to/chrome'
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start
|
|
33
|
+
validate!
|
|
34
|
+
setup_servers
|
|
35
|
+
setup_signal_handlers
|
|
36
|
+
log_startup_info
|
|
37
|
+
run
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
config.logger.error "ERROR: #{e.message}"
|
|
40
|
+
config.logger.error e.backtrace.join("\n")
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def setup_servers
|
|
47
|
+
@mcp_server = Server.new(config)
|
|
48
|
+
@transport_server = create_transport
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_transport
|
|
52
|
+
case @options[:transport]
|
|
53
|
+
when 'stdio'
|
|
54
|
+
Transport::StdioServer.new(mcp_server, config)
|
|
55
|
+
when 'http'
|
|
56
|
+
Transport::HTTPServer.new(mcp_server, config)
|
|
57
|
+
else
|
|
58
|
+
raise "Unknown transport: #{@options[:transport]}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def setup_signal_handlers
|
|
63
|
+
trap('INT') { shutdown }
|
|
64
|
+
trap('TERM') { shutdown }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def shutdown
|
|
68
|
+
config.logger.info 'Shutting down...'
|
|
69
|
+
transport_server.stop
|
|
70
|
+
mcp_server.stop_browser
|
|
71
|
+
exit 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def log_startup_info
|
|
75
|
+
logger = config.logger
|
|
76
|
+
logger.info '=' * 60
|
|
77
|
+
logger.info "Ferrum MCP Server v#{VERSION}"
|
|
78
|
+
logger.info '=' * 60
|
|
79
|
+
logger.info ''
|
|
80
|
+
|
|
81
|
+
log_configuration
|
|
82
|
+
log_transport_info
|
|
83
|
+
|
|
84
|
+
logger.info ''
|
|
85
|
+
logger.info '=' * 60
|
|
86
|
+
logger.info 'Server starting...'
|
|
87
|
+
logger.info '=' * 60
|
|
88
|
+
logger.info ''
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def log_configuration
|
|
92
|
+
logger = config.logger
|
|
93
|
+
logger.info 'Configuration:'
|
|
94
|
+
|
|
95
|
+
log_browsers
|
|
96
|
+
log_user_profiles
|
|
97
|
+
log_bot_profiles
|
|
98
|
+
log_browser_options
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def log_browsers
|
|
102
|
+
logger = config.logger
|
|
103
|
+
logger.info " Browsers (#{config.browsers.count}):"
|
|
104
|
+
config.browsers.each do |browser|
|
|
105
|
+
default_marker = browser == config.default_browser ? ' [default]' : ''
|
|
106
|
+
browser_path = browser.path || 'auto-detect'
|
|
107
|
+
logger.info " - #{browser.id}: #{browser.name} (#{browser.type})#{default_marker}"
|
|
108
|
+
logger.info " Path: #{browser_path}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_user_profiles
|
|
113
|
+
return unless config.user_profiles.any?
|
|
114
|
+
|
|
115
|
+
logger = config.logger
|
|
116
|
+
logger.info " User Profiles (#{config.user_profiles.count}):"
|
|
117
|
+
config.user_profiles.each do |profile|
|
|
118
|
+
logger.info " - #{profile.id}: #{profile.name}"
|
|
119
|
+
logger.info " Path: #{profile.path}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def log_bot_profiles
|
|
124
|
+
logger = config.logger
|
|
125
|
+
if config.bot_profiles.any?
|
|
126
|
+
logger.info ' BotBrowser (anti-detection enabled) ✓'
|
|
127
|
+
logger.info " Bot Profiles (#{config.bot_profiles.count}):"
|
|
128
|
+
config.bot_profiles.each do |profile|
|
|
129
|
+
encrypted_marker = profile.encrypted ? ' [encrypted]' : ''
|
|
130
|
+
logger.info " - #{profile.id}: #{profile.name}#{encrypted_marker}"
|
|
131
|
+
logger.info " Path: #{profile.path}"
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
logger.info ' BotBrowser: Not configured (consider using for better stealth)'
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def log_browser_options
|
|
139
|
+
logger = config.logger
|
|
140
|
+
logger.info " Headless: #{config.headless}"
|
|
141
|
+
logger.info " Timeout: #{config.timeout}s"
|
|
142
|
+
logger.info ''
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def log_transport_info
|
|
146
|
+
logger = config.logger
|
|
147
|
+
logger.info 'Transport:'
|
|
148
|
+
logger.info " Protocol: #{@options[:transport].upcase}"
|
|
149
|
+
|
|
150
|
+
if @options[:transport] == 'http'
|
|
151
|
+
logger.info " Host: #{config.server_host}"
|
|
152
|
+
logger.info " Port: #{config.server_port}"
|
|
153
|
+
logger.info " MCP Endpoint: http://#{config.server_host}:#{config.server_port}/mcp"
|
|
154
|
+
else
|
|
155
|
+
logger.info ' Mode: Standard input/output'
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def run
|
|
160
|
+
transport_server.start
|
|
161
|
+
# Keep main thread alive for HTTP (stdio blocks automatically)
|
|
162
|
+
sleep if @options[:transport] == 'http'
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FerrumMCP
|
|
4
|
+
# Configuration class for Ferrum MCP Server
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :headless, :timeout, :server_host, :server_port, :log_level, :transport, :max_sessions,
|
|
7
|
+
:rate_limit_enabled, :rate_limit_max_requests, :rate_limit_window
|
|
8
|
+
attr_reader :browsers, :user_profiles, :bot_profiles
|
|
9
|
+
|
|
10
|
+
# Browser configuration structure
|
|
11
|
+
BrowserConfig = Struct.new(:id, :name, :path, :type, :description, keyword_init: true) do
|
|
12
|
+
def to_h
|
|
13
|
+
super.compact
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# User profile configuration structure
|
|
18
|
+
UserProfileConfig = Struct.new(:id, :name, :path, :description, keyword_init: true) do
|
|
19
|
+
def to_h
|
|
20
|
+
super.compact
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# BotBrowser profile configuration structure
|
|
25
|
+
BotProfileConfig = Struct.new(:id, :name, :path, :encrypted, :description, keyword_init: true) do
|
|
26
|
+
def to_h
|
|
27
|
+
super.compact
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(transport: 'http')
|
|
32
|
+
# Server configuration
|
|
33
|
+
@headless = ENV.fetch('BROWSER_HEADLESS', 'false') == 'true'
|
|
34
|
+
@timeout = ENV.fetch('BROWSER_TIMEOUT', '60').to_i
|
|
35
|
+
@server_host = ENV.fetch('MCP_SERVER_HOST', '0.0.0.0')
|
|
36
|
+
@server_port = ENV.fetch('MCP_SERVER_PORT', '3000').to_i
|
|
37
|
+
@log_level = ENV.fetch('LOG_LEVEL', 'debug').to_sym
|
|
38
|
+
@transport = transport
|
|
39
|
+
@max_sessions = ENV.fetch('MAX_CONCURRENT_SESSIONS', '10').to_i
|
|
40
|
+
|
|
41
|
+
# Rate limiting configuration
|
|
42
|
+
@rate_limit_enabled = ENV.fetch('RATE_LIMIT_ENABLED', 'true') == 'true'
|
|
43
|
+
@rate_limit_max_requests = ENV.fetch('RATE_LIMIT_MAX_REQUESTS', '100').to_i
|
|
44
|
+
@rate_limit_window = ENV.fetch('RATE_LIMIT_WINDOW', '60').to_i
|
|
45
|
+
|
|
46
|
+
# Load multi-browser configurations
|
|
47
|
+
@browsers = load_browsers
|
|
48
|
+
@user_profiles = load_user_profiles
|
|
49
|
+
@bot_profiles = load_bot_profiles
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def valid?
|
|
53
|
+
# Valid if at least one browser is configured
|
|
54
|
+
browsers.any? && browsers.all? { |b| b.path.nil? || File.exist?(b.path) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get default browser (first in list or system Chrome)
|
|
58
|
+
def default_browser
|
|
59
|
+
browsers.first
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Find browser by ID
|
|
63
|
+
def find_browser(id)
|
|
64
|
+
browsers.find { |b| b.id == id }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Find user profile by ID
|
|
68
|
+
def find_user_profile(id)
|
|
69
|
+
user_profiles.find { |p| p.id == id }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Find bot profile by ID
|
|
73
|
+
def find_bot_profile(id)
|
|
74
|
+
bot_profiles.find { |p| p.id == id }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check if any BotBrowser profile is configured
|
|
78
|
+
def using_botbrowser?
|
|
79
|
+
bot_profiles.any?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def logger
|
|
83
|
+
@logger ||= create_multi_logger
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Environment variable keys to skip when loading browsers
|
|
87
|
+
RESERVED_BROWSER_ENV_KEYS = %w[BROWSER_PATH BROWSER_HEADLESS BROWSER_TIMEOUT].freeze
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Load browser configurations from environment variables
|
|
92
|
+
# Format: BROWSER_<ID>=type:path:name:description
|
|
93
|
+
# Example: BROWSER_CHROME=chrome:/usr/bin/google-chrome:Google Chrome:Standard Chrome browser
|
|
94
|
+
# Example: BROWSER_BOTBROWSER=botbrowser:/opt/botbrowser/chrome:BotBrowser:Anti-detection browser
|
|
95
|
+
def load_browsers
|
|
96
|
+
browsers = []
|
|
97
|
+
browsers.concat(load_custom_browsers)
|
|
98
|
+
browsers << load_legacy_browser if legacy_browser_configured?
|
|
99
|
+
browsers << create_system_browser if browsers.empty?
|
|
100
|
+
browsers
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def load_custom_browsers
|
|
104
|
+
ENV.each_with_object([]) do |(key, value), browsers|
|
|
105
|
+
next unless key.start_with?('BROWSER_')
|
|
106
|
+
next if RESERVED_BROWSER_ENV_KEYS.include?(key)
|
|
107
|
+
|
|
108
|
+
browsers << parse_browser_config(key, value)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_browser_config(key, value)
|
|
113
|
+
id = key.sub('BROWSER_', '').downcase
|
|
114
|
+
type, path, name, description = value.split(':', 4)
|
|
115
|
+
|
|
116
|
+
BrowserConfig.new(
|
|
117
|
+
id: id,
|
|
118
|
+
name: name || id.capitalize,
|
|
119
|
+
path: path.empty? ? nil : path,
|
|
120
|
+
type: type || 'chrome',
|
|
121
|
+
description: description
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def legacy_browser_configured?
|
|
126
|
+
ENV['BROWSER_PATH'] || ENV.fetch('BOTBROWSER_PATH', nil)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def load_legacy_browser
|
|
130
|
+
legacy_path = ENV.fetch('BROWSER_PATH', nil) || ENV.fetch('BOTBROWSER_PATH', nil)
|
|
131
|
+
legacy_type = ENV['BOTBROWSER_PATH'] ? 'botbrowser' : 'chrome'
|
|
132
|
+
|
|
133
|
+
BrowserConfig.new(
|
|
134
|
+
id: 'default',
|
|
135
|
+
name: 'Default Browser',
|
|
136
|
+
path: legacy_path,
|
|
137
|
+
type: legacy_type,
|
|
138
|
+
description: 'Legacy browser configuration'
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def create_system_browser
|
|
143
|
+
BrowserConfig.new(
|
|
144
|
+
id: 'system',
|
|
145
|
+
name: 'System Chrome',
|
|
146
|
+
path: nil,
|
|
147
|
+
type: 'chrome',
|
|
148
|
+
description: 'Auto-detected system Chrome/Chromium'
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Load user profile configurations from environment variables
|
|
153
|
+
# Format: USER_PROFILE_<ID>=path:name:description
|
|
154
|
+
# Example: USER_PROFILE_DEV=/home/user/.chrome-dev:Development:Dev profile with extensions
|
|
155
|
+
def load_user_profiles
|
|
156
|
+
profiles = []
|
|
157
|
+
|
|
158
|
+
ENV.each do |key, value|
|
|
159
|
+
next unless key.start_with?('USER_PROFILE_')
|
|
160
|
+
|
|
161
|
+
id = key.sub('USER_PROFILE_', '').downcase
|
|
162
|
+
path, name, description = value.split(':', 3)
|
|
163
|
+
|
|
164
|
+
next if path.nil? || path.empty?
|
|
165
|
+
|
|
166
|
+
profiles << UserProfileConfig.new(
|
|
167
|
+
id: id,
|
|
168
|
+
name: name || id.capitalize,
|
|
169
|
+
path: path,
|
|
170
|
+
description: description
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
profiles
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Load BotBrowser profile configurations from environment variables
|
|
178
|
+
# Format: BOT_PROFILE_<ID>=path:name:description
|
|
179
|
+
# Example: BOT_PROFILE_US=/profiles/us_chrome.enc:US Chrome:US-based Chrome profile
|
|
180
|
+
def load_bot_profiles
|
|
181
|
+
profiles = []
|
|
182
|
+
|
|
183
|
+
ENV.each do |key, value|
|
|
184
|
+
next unless key.start_with?('BOT_PROFILE_')
|
|
185
|
+
|
|
186
|
+
id = key.sub('BOT_PROFILE_', '').downcase
|
|
187
|
+
path, name, description = value.split(':', 3)
|
|
188
|
+
|
|
189
|
+
next if path.nil? || path.empty?
|
|
190
|
+
|
|
191
|
+
profiles << BotProfileConfig.new(
|
|
192
|
+
id: id,
|
|
193
|
+
name: name || id.capitalize,
|
|
194
|
+
path: path,
|
|
195
|
+
encrypted: path.end_with?('.enc'),
|
|
196
|
+
description: description
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Add legacy BOTBROWSER_PROFILE for backward compatibility
|
|
201
|
+
if ENV['BOTBROWSER_PROFILE'] && !ENV['BOTBROWSER_PROFILE'].empty?
|
|
202
|
+
legacy_path = ENV['BOTBROWSER_PROFILE']
|
|
203
|
+
|
|
204
|
+
profiles << BotProfileConfig.new(
|
|
205
|
+
id: 'default',
|
|
206
|
+
name: 'Default BotBrowser Profile',
|
|
207
|
+
path: legacy_path,
|
|
208
|
+
encrypted: legacy_path.end_with?('.enc'),
|
|
209
|
+
description: 'Legacy BotBrowser profile'
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
profiles
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def create_multi_logger
|
|
217
|
+
# Create log directory relative to the project root
|
|
218
|
+
# Use __FILE__ to get the gem's location, then go up to project root
|
|
219
|
+
project_root = File.expand_path('../..', __dir__)
|
|
220
|
+
log_dir = File.join(project_root, 'logs')
|
|
221
|
+
FileUtils.mkdir_p(log_dir) unless File.directory?(log_dir)
|
|
222
|
+
|
|
223
|
+
log_file = File.join(log_dir, 'ferrum_mcp.log')
|
|
224
|
+
|
|
225
|
+
# Only write to file, no console output
|
|
226
|
+
Logger.new(log_file, level: log_level)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|