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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Get information about a specific session
6
+ class GetSessionInfoTool < SessionTool
7
+ def self.tool_name
8
+ 'get_session_info'
9
+ end
10
+
11
+ def self.description
12
+ 'Get detailed information about a specific browser session'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ session_id: {
20
+ type: 'string',
21
+ description: 'The ID of the session (omit for default session)'
22
+ }
23
+ }
24
+ }
25
+ end
26
+
27
+ def execute(params)
28
+ session_id = params[:session_id] || params['session_id']
29
+ session = session_manager.get_session(session_id)
30
+
31
+ return error_response("Session not found: #{session_id}") unless session
32
+
33
+ success_response(session.info)
34
+ rescue StandardError => e
35
+ logger.error "Failed to get session info: #{e.message}"
36
+ error_response("Failed to get session info: #{e.message}")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to extract text from elements
6
+ class GetTextTool < BaseTool
7
+ def self.tool_name
8
+ 'get_text'
9
+ end
10
+
11
+ def self.description
12
+ 'Extract text content from one or more elements'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ selector: {
20
+ type: 'string',
21
+ description: 'CSS selector or XPath of element(s) to extract text from (use xpath: prefix for XPath)'
22
+ },
23
+ multiple: {
24
+ type: 'boolean',
25
+ description: 'Extract from all matching elements (default: false)',
26
+ default: false
27
+ },
28
+ session_id: {
29
+ type: 'string',
30
+ description: 'Session ID to use for this operation'
31
+ }
32
+ },
33
+ required: %w[selector session_id]
34
+ }
35
+ end
36
+
37
+ def execute(params)
38
+ ensure_browser_active
39
+ selector = param(params, :selector)
40
+ multiple = param(params, :multiple) || false
41
+
42
+ logger.info "Extracting text from: #{selector}"
43
+
44
+ # Support both CSS and XPath selectors
45
+ if selector.start_with?('xpath:', '//')
46
+ xpath = selector.sub(/^xpath:/, '')
47
+ logger.debug "Using XPath: #{xpath}"
48
+ elements = browser.xpath(xpath)
49
+ raise ToolError, "Element not found with XPath: #{xpath}" if elements.empty?
50
+ else
51
+ elements = browser.css(selector)
52
+ raise ToolError, "Element not found: #{selector}" if elements.empty?
53
+ end
54
+
55
+ if multiple
56
+ texts = elements.map(&:text)
57
+ success_response(texts: texts, count: texts.length)
58
+ else
59
+ success_response(text: elements.first.text)
60
+ end
61
+ rescue StandardError => e
62
+ logger.error "Get text failed: #{e.message}"
63
+ error_response("Failed to get text: #{e.message}")
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to get page title
6
+ class GetTitleTool < BaseTool
7
+ def self.tool_name
8
+ 'get_title'
9
+ end
10
+
11
+ def self.description
12
+ 'Get the title of the current page'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ session_id: {
20
+ type: 'string',
21
+ description: 'Session ID to use for this operation'
22
+ }
23
+ },
24
+ required: ['session_id']
25
+ }
26
+ end
27
+
28
+ def execute(_params)
29
+ ensure_browser_active
30
+ logger.info 'Getting page title'
31
+
32
+ success_response(
33
+ title: browser.title,
34
+ url: browser.url
35
+ )
36
+ rescue StandardError => e
37
+ logger.error "Get title failed: #{e.message}"
38
+ error_response("Failed to get title: #{e.message}")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to get current URL
6
+ class GetURLTool < BaseTool
7
+ def self.tool_name
8
+ 'get_url'
9
+ end
10
+
11
+ def self.description
12
+ 'Get the current URL of the page'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ session_id: {
20
+ type: 'string',
21
+ description: 'Session ID to use for this operation'
22
+ }
23
+ },
24
+ required: ['session_id']
25
+ }
26
+ end
27
+
28
+ def execute(_params)
29
+ ensure_browser_active
30
+
31
+ logger.info 'Getting current URL'
32
+ success_response(url: browser.url)
33
+ rescue StandardError => e
34
+ logger.error "Get URL failed: #{e.message}"
35
+ error_response("Failed to get URL: #{e.message}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to go back in browser history
6
+ class GoBackTool < BaseTool
7
+ def self.tool_name
8
+ 'go_back'
9
+ end
10
+
11
+ def self.description
12
+ 'Go back to the previous page in browser history'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ session_id: {
20
+ type: 'string',
21
+ description: 'Session ID to use for this operation'
22
+ }
23
+ },
24
+ required: ['session_id']
25
+ }
26
+ end
27
+
28
+ def execute(_params)
29
+ ensure_browser_active
30
+ logger.info 'Going back'
31
+ browser.back
32
+
33
+ # Wait for network to be idle to ensure page is loaded
34
+ browser.network.wait_for_idle(timeout: 30)
35
+
36
+ success_response(
37
+ url: browser.url,
38
+ title: browser.title
39
+ )
40
+ rescue Ferrum::TimeoutError => e
41
+ logger.error "Go back timeout: #{e.message}"
42
+ error_response("Go back timed out: #{e.message}")
43
+ rescue StandardError => e
44
+ logger.error "Go back failed: #{e.message}"
45
+ error_response("Failed to go back: #{e.message}")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to go forward in browser history
6
+ class GoForwardTool < BaseTool
7
+ def self.tool_name
8
+ 'go_forward'
9
+ end
10
+
11
+ def self.description
12
+ 'Go forward to the next page in browser history'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ session_id: {
20
+ type: 'string',
21
+ description: 'Session ID to use for this operation'
22
+ }
23
+ },
24
+ required: ['session_id']
25
+ }
26
+ end
27
+
28
+ def execute(_params)
29
+ ensure_browser_active
30
+ logger.info 'Going forward'
31
+ browser.forward
32
+
33
+ # Wait for network to be idle to ensure page is loaded
34
+ browser.network.wait_for_idle(timeout: 30)
35
+
36
+ success_response(
37
+ url: browser.url,
38
+ title: browser.title
39
+ )
40
+ rescue Ferrum::TimeoutError => e
41
+ logger.error "Go forward timeout: #{e.message}"
42
+ error_response("Go forward timed out: #{e.message}")
43
+ rescue StandardError => e
44
+ logger.error "Go forward failed: #{e.message}"
45
+ error_response("Failed to go forward: #{e.message}")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to hover over an element
6
+ class HoverTool < BaseTool
7
+ def self.tool_name
8
+ 'hover'
9
+ end
10
+
11
+ def self.description
12
+ 'Hover over an element using a CSS selector'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ selector: {
20
+ type: 'string',
21
+ description: 'CSS selector of the element to hover over'
22
+ },
23
+ session_id: {
24
+ type: 'string',
25
+ description: 'Session ID to use for this operation'
26
+ }
27
+ },
28
+ required: %w[selector session_id]
29
+ }
30
+ end
31
+
32
+ def execute(params)
33
+ selector = param(params, :selector)
34
+
35
+ logger.info "Hovering over element: #{selector}"
36
+
37
+ # First ensure element exists and is ready
38
+ element = find_element(selector)
39
+
40
+ # Scroll into view if supported
41
+ element.scroll_into_view if element.respond_to?(:scroll_into_view)
42
+
43
+ # Try native hover first, fallback to JavaScript
44
+ begin
45
+ element.hover
46
+ logger.debug 'Native hover successful'
47
+ rescue StandardError => e
48
+ logger.debug "Native hover failed, using JavaScript: #{e.message}"
49
+ hover_with_javascript(selector)
50
+ end
51
+
52
+ success_response(message: "Hovered over #{selector}")
53
+ rescue StandardError => e
54
+ logger.error "Hover failed: #{e.message}"
55
+ error_response("Failed to hover: #{e.message}")
56
+ end
57
+
58
+ private
59
+
60
+ def hover_with_javascript(selector)
61
+ # Use inspect to properly escape the selector for JavaScript (prevents XSS)
62
+ script = <<~JS
63
+ const element = document.querySelector(#{selector.inspect});
64
+ if (!element) {
65
+ throw new Error('Element not found: ' + #{selector.inspect});
66
+ }
67
+ const event = new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window });
68
+ element.dispatchEvent(event);
69
+ JS
70
+
71
+ browser.execute(script)
72
+ logger.debug 'JavaScript hover successful'
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ class ListSessionsTool < SessionTool
6
+ def self.tool_name
7
+ 'list_sessions'
8
+ end
9
+
10
+ def self.description
11
+ 'List all active browser sessions with their information (id, status, type, uptime, etc.)'
12
+ end
13
+
14
+ def self.input_schema
15
+ {
16
+ type: 'object',
17
+ properties: {}
18
+ }
19
+ end
20
+
21
+ def execute(_params)
22
+ sessions = session_manager.list_sessions
23
+ success_response(
24
+ count: sessions.size,
25
+ sessions: sessions
26
+ )
27
+ rescue StandardError => e
28
+ logger.error "Failed to list sessions: #{e.message}"
29
+ error_response("Failed to list sessions: #{e.message}")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to navigate to a URL
6
+ class NavigateTool < BaseTool
7
+ def self.tool_name
8
+ 'navigate'
9
+ end
10
+
11
+ def self.description
12
+ 'Navigate to a specific URL in the browser'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ url: {
20
+ type: 'string',
21
+ description: 'The URL to navigate to (must include protocol: http:// or https://)'
22
+ },
23
+ session_id: {
24
+ type: 'string',
25
+ description: 'Session ID to use for this operation'
26
+ }
27
+ },
28
+ required: %w[url session_id]
29
+ }
30
+ end
31
+
32
+ def execute(params)
33
+ ensure_browser_active
34
+ url = param(params, :url)
35
+
36
+ # Validate URL format
37
+ raise ToolError, 'URL must start with http:// or https://' unless %r{^https?://}.match?(url)
38
+
39
+ logger.info "Navigating to: #{url}"
40
+ browser.goto(url)
41
+
42
+ # Wait for network to be idle to ensure page is loaded
43
+ # This prevents race conditions with subsequent tool calls
44
+ browser.network.wait_for_idle(timeout: 30)
45
+
46
+ success_response(
47
+ url: browser.url,
48
+ title: browser.title
49
+ )
50
+ rescue Ferrum::TimeoutError => e
51
+ logger.error "Navigation timeout: #{e.message}"
52
+ error_response("Navigation timed out: #{e.message}")
53
+ rescue StandardError => e
54
+ logger.error "Navigation failed: #{e.message}"
55
+ error_response("Failed to navigate: #{e.message}")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FerrumMCP
4
+ module Tools
5
+ # Tool to press keyboard keys
6
+ class PressKeyTool < BaseTool
7
+ def self.tool_name
8
+ 'press_key'
9
+ end
10
+
11
+ def self.description
12
+ 'Press keyboard keys (e.g., Enter, Tab, Escape)'
13
+ end
14
+
15
+ def self.input_schema
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ key: {
20
+ type: 'string',
21
+ description: 'Key to press (Enter, Tab, Escape, ArrowDown, etc.)'
22
+ },
23
+ selector: {
24
+ type: 'string',
25
+ description: 'Optional: CSS selector to focus before pressing key'
26
+ },
27
+ session_id: {
28
+ type: 'string',
29
+ description: 'Session ID to use for this operation'
30
+ }
31
+ },
32
+ required: %w[key session_id]
33
+ }
34
+ end
35
+
36
+ def execute(params)
37
+ key = param(params, :key)
38
+ selector = param(params, :selector)
39
+
40
+ if selector
41
+ logger.info "Focusing element: #{selector}"
42
+ element = find_element(selector)
43
+ element.focus
44
+ end
45
+
46
+ logger.info "Pressing key: #{key}"
47
+ normalized_key = normalize_key(key)
48
+
49
+ # Use keyboard.type for key presses
50
+ # This handles both special keys and regular characters correctly
51
+ browser.keyboard.type(normalized_key)
52
+
53
+ success_response(message: "Pressed key: #{key}")
54
+ rescue StandardError => e
55
+ logger.error "Press key failed: #{e.message}"
56
+ error_response("Failed to press key: #{e.message}")
57
+ end
58
+
59
+ private
60
+
61
+ def normalize_key(key)
62
+ # Convert common key names to Ferrum format
63
+ case key.to_s.downcase
64
+ when 'enter', 'return'
65
+ :Enter
66
+ when 'tab'
67
+ :Tab
68
+ when 'escape', 'esc'
69
+ :Escape
70
+ when 'backspace'
71
+ :Backspace
72
+ when 'delete', 'del'
73
+ :Delete
74
+ when 'arrowdown', 'down'
75
+ :Down
76
+ when 'arrowup', 'up'
77
+ :Up
78
+ when 'arrowleft', 'left'
79
+ :Left
80
+ when 'arrowright', 'right'
81
+ :Right
82
+ when 'space'
83
+ ' '
84
+ else
85
+ # If already a symbol, return as-is, otherwise try to convert
86
+ key.is_a?(Symbol) ? key : key.to_sym
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end