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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +90 -0
  3. data/CHANGELOG.md +229 -0
  4. data/CONTRIBUTING.md +469 -0
  5. data/LICENSE +21 -0
  6. data/README.md +334 -0
  7. data/SECURITY.md +286 -0
  8. data/bin/ferrum-mcp +66 -0
  9. data/bin/lint +10 -0
  10. data/bin/serve +3 -0
  11. data/bin/test +4 -0
  12. data/docs/API_REFERENCE.md +1410 -0
  13. data/docs/CONFIGURATION.md +254 -0
  14. data/docs/DEPLOYMENT.md +846 -0
  15. data/docs/DOCKER.md +836 -0
  16. data/docs/DOCKER_BOTBROWSER.md +455 -0
  17. data/docs/GETTING_STARTED.md +249 -0
  18. data/docs/TROUBLESHOOTING.md +677 -0
  19. data/lib/ferrum_mcp/browser_manager.rb +101 -0
  20. data/lib/ferrum_mcp/cli/command_handler.rb +99 -0
  21. data/lib/ferrum_mcp/cli/server_runner.rb +166 -0
  22. data/lib/ferrum_mcp/configuration.rb +229 -0
  23. data/lib/ferrum_mcp/resource_manager.rb +223 -0
  24. data/lib/ferrum_mcp/server.rb +254 -0
  25. data/lib/ferrum_mcp/session.rb +227 -0
  26. data/lib/ferrum_mcp/session_manager.rb +183 -0
  27. data/lib/ferrum_mcp/tools/accept_cookies_tool.rb +458 -0
  28. data/lib/ferrum_mcp/tools/base_tool.rb +114 -0
  29. data/lib/ferrum_mcp/tools/clear_cookies_tool.rb +66 -0
  30. data/lib/ferrum_mcp/tools/click_tool.rb +218 -0
  31. data/lib/ferrum_mcp/tools/close_session_tool.rb +49 -0
  32. data/lib/ferrum_mcp/tools/create_session_tool.rb +146 -0
  33. data/lib/ferrum_mcp/tools/drag_and_drop_tool.rb +171 -0
  34. data/lib/ferrum_mcp/tools/evaluate_js_tool.rb +46 -0
  35. data/lib/ferrum_mcp/tools/execute_script_tool.rb +48 -0
  36. data/lib/ferrum_mcp/tools/fill_form_tool.rb +78 -0
  37. data/lib/ferrum_mcp/tools/find_by_text_tool.rb +153 -0
  38. data/lib/ferrum_mcp/tools/get_attribute_tool.rb +56 -0
  39. data/lib/ferrum_mcp/tools/get_cookies_tool.rb +70 -0
  40. data/lib/ferrum_mcp/tools/get_html_tool.rb +52 -0
  41. data/lib/ferrum_mcp/tools/get_session_info_tool.rb +40 -0
  42. data/lib/ferrum_mcp/tools/get_text_tool.rb +67 -0
  43. data/lib/ferrum_mcp/tools/get_title_tool.rb +42 -0
  44. data/lib/ferrum_mcp/tools/get_url_tool.rb +39 -0
  45. data/lib/ferrum_mcp/tools/go_back_tool.rb +49 -0
  46. data/lib/ferrum_mcp/tools/go_forward_tool.rb +49 -0
  47. data/lib/ferrum_mcp/tools/hover_tool.rb +76 -0
  48. data/lib/ferrum_mcp/tools/list_sessions_tool.rb +33 -0
  49. data/lib/ferrum_mcp/tools/navigate_tool.rb +59 -0
  50. data/lib/ferrum_mcp/tools/press_key_tool.rb +91 -0
  51. data/lib/ferrum_mcp/tools/query_shadow_dom_tool.rb +225 -0
  52. data/lib/ferrum_mcp/tools/refresh_tool.rb +49 -0
  53. data/lib/ferrum_mcp/tools/screenshot_tool.rb +121 -0
  54. data/lib/ferrum_mcp/tools/session_tool.rb +37 -0
  55. data/lib/ferrum_mcp/tools/set_cookie_tool.rb +77 -0
  56. data/lib/ferrum_mcp/tools/solve_captcha_tool.rb +528 -0
  57. data/lib/ferrum_mcp/transport/http_server.rb +93 -0
  58. data/lib/ferrum_mcp/transport/rate_limiter.rb +79 -0
  59. data/lib/ferrum_mcp/transport/stdio_server.rb +63 -0
  60. data/lib/ferrum_mcp/version.rb +5 -0
  61. data/lib/ferrum_mcp/whisper_service.rb +222 -0
  62. data/lib/ferrum_mcp.rb +35 -0
  63. 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