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,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
|