ferrum-mcp 1.0.0 → 1.1.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 +4 -4
- data/.env.example +37 -0
- data/CHANGELOG.md +72 -1
- data/README.md +30 -9
- data/docs/API_REFERENCE.md +300 -21
- data/docs/CONFIGURATION.md +34 -1
- data/docs/DEPLOYMENT.md +1 -0
- data/docs/DOCKER_BOTBROWSER.md +4 -0
- data/lib/ferrum_mcp/browser_manager.rb +56 -32
- data/lib/ferrum_mcp/cli/command_handler.rb +4 -3
- data/lib/ferrum_mcp/cli/server_runner.rb +19 -6
- data/lib/ferrum_mcp/configuration.rb +77 -18
- data/lib/ferrum_mcp/image_resizer.rb +43 -0
- data/lib/ferrum_mcp/server.rb +58 -91
- data/lib/ferrum_mcp/session.rb +67 -9
- data/lib/ferrum_mcp/session_manager.rb +15 -15
- data/lib/ferrum_mcp/tools/accept_cookies_tool.rb +11 -31
- data/lib/ferrum_mcp/tools/base_tool.rb +79 -38
- data/lib/ferrum_mcp/tools/clear_cookies_tool.rb +14 -43
- data/lib/ferrum_mcp/tools/click_tool.rb +58 -181
- data/lib/ferrum_mcp/tools/close_session_tool.rb +8 -30
- data/lib/ferrum_mcp/tools/close_tab_tool.rb +44 -0
- data/lib/ferrum_mcp/tools/create_session_tool.rb +38 -110
- data/lib/ferrum_mcp/tools/definition.rb +106 -0
- data/lib/ferrum_mcp/tools/drag_and_drop_tool.rb +43 -145
- data/lib/ferrum_mcp/tools/evaluate_js_tool.rb +6 -28
- data/lib/ferrum_mcp/tools/execute_script_tool.rb +6 -30
- data/lib/ferrum_mcp/tools/fill_form_tool.rb +37 -52
- data/lib/ferrum_mcp/tools/find_by_text_tool.rb +39 -114
- data/lib/ferrum_mcp/tools/get_attribute_tool.rb +10 -39
- data/lib/ferrum_mcp/tools/get_cookies_tool.rb +23 -52
- data/lib/ferrum_mcp/tools/get_html_tool.rb +22 -35
- data/lib/ferrum_mcp/tools/get_session_info_tool.rb +5 -21
- data/lib/ferrum_mcp/tools/get_text_tool.rb +22 -46
- data/lib/ferrum_mcp/tools/get_title_tool.rb +5 -28
- data/lib/ferrum_mcp/tools/get_url_tool.rb +5 -25
- data/lib/ferrum_mcp/tools/go_back_tool.rb +7 -30
- data/lib/ferrum_mcp/tools/go_forward_tool.rb +7 -30
- data/lib/ferrum_mcp/tools/hover_tool.rb +10 -51
- data/lib/ferrum_mcp/tools/list_sessions_tool.rb +5 -19
- data/lib/ferrum_mcp/tools/list_tabs_tool.rb +20 -0
- data/lib/ferrum_mcp/tools/navigate_tool.rb +17 -37
- data/lib/ferrum_mcp/tools/new_tab_tool.rb +36 -0
- data/lib/ferrum_mcp/tools/press_key_tool.rb +18 -63
- data/lib/ferrum_mcp/tools/query_shadow_dom_tool.rb +50 -205
- data/lib/ferrum_mcp/tools/refresh_tool.rb +7 -30
- data/lib/ferrum_mcp/tools/screenshot_tool.rb +29 -86
- data/lib/ferrum_mcp/tools/scroll_tool.rb +64 -0
- data/lib/ferrum_mcp/tools/select_option_tool.rb +56 -0
- data/lib/ferrum_mcp/tools/session_tool.rb +10 -9
- data/lib/ferrum_mcp/tools/set_cookie_tool.rb +15 -56
- data/lib/ferrum_mcp/tools/set_viewport_tool.rb +32 -0
- data/lib/ferrum_mcp/tools/snapshot_tool.rb +175 -0
- data/lib/ferrum_mcp/tools/solve_captcha_tool.rb +17 -35
- data/lib/ferrum_mcp/tools/switch_tab_tool.rb +29 -0
- data/lib/ferrum_mcp/tools/tab_tool.rb +40 -0
- data/lib/ferrum_mcp/tools/upload_file_tool.rb +50 -0
- data/lib/ferrum_mcp/tools/wait_for_network_idle_tool.rb +32 -0
- data/lib/ferrum_mcp/tools/wait_for_selector_tool.rb +59 -0
- data/lib/ferrum_mcp/tools/wait_for_text_tool.rb +53 -0
- data/lib/ferrum_mcp/transport/api_key_authenticator.rb +112 -0
- data/lib/ferrum_mcp/transport/client_address.rb +23 -0
- data/lib/ferrum_mcp/transport/http_server.rb +11 -1
- data/lib/ferrum_mcp/transport/rate_limiter.rb +2 -6
- data/lib/ferrum_mcp/transport/stdio_server.rb +8 -30
- data/lib/ferrum_mcp/url_policy.rb +88 -0
- data/lib/ferrum_mcp/version.rb +1 -1
- metadata +21 -3
|
@@ -2,37 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Get page title
|
|
6
6
|
class GetTitleTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.description
|
|
12
|
-
'Get the title of the current page'
|
|
13
|
-
end
|
|
7
|
+
tool_name 'get_title'
|
|
8
|
+
description 'Get the title of the current page'
|
|
14
9
|
|
|
15
|
-
def
|
|
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)
|
|
10
|
+
def perform(_params)
|
|
29
11
|
ensure_browser_active
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
success_response(
|
|
33
|
-
title: browser.title,
|
|
34
|
-
url: browser.url
|
|
35
|
-
)
|
|
12
|
+
success_response(title: page.title, url: page.url)
|
|
36
13
|
rescue StandardError => e
|
|
37
14
|
logger.error "Get title failed: #{e.message}"
|
|
38
15
|
error_response("Failed to get title: #{e.message}")
|
|
@@ -2,34 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Get current URL
|
|
6
6
|
class GetURLTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.description
|
|
12
|
-
'Get the current URL of the page'
|
|
13
|
-
end
|
|
7
|
+
tool_name 'get_url'
|
|
8
|
+
description 'Get the current page URL'
|
|
14
9
|
|
|
15
|
-
def
|
|
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)
|
|
10
|
+
def perform(_params)
|
|
29
11
|
ensure_browser_active
|
|
30
|
-
|
|
31
|
-
logger.info 'Getting current URL'
|
|
32
|
-
success_response(url: browser.url)
|
|
12
|
+
success_response(url: page.url)
|
|
33
13
|
rescue StandardError => e
|
|
34
14
|
logger.error "Get URL failed: #{e.message}"
|
|
35
15
|
error_response("Failed to get URL: #{e.message}")
|
|
@@ -2,41 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Go back to the previous page in browser history
|
|
6
6
|
class GoBackTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.description
|
|
12
|
-
'Go back to the previous page in browser history'
|
|
13
|
-
end
|
|
7
|
+
tool_name 'go_back'
|
|
8
|
+
description 'Go back to the previous page in browser history'
|
|
14
9
|
|
|
15
|
-
def
|
|
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)
|
|
10
|
+
def perform(_params)
|
|
29
11
|
ensure_browser_active
|
|
30
12
|
logger.info 'Going back'
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Wait for network to be idle to ensure page is loaded
|
|
34
|
-
browser.network.wait_for_idle(timeout: 30)
|
|
13
|
+
page.back
|
|
14
|
+
page.network.wait_for_idle(timeout: 30)
|
|
35
15
|
|
|
36
|
-
success_response(
|
|
37
|
-
url: browser.url,
|
|
38
|
-
title: browser.title
|
|
39
|
-
)
|
|
16
|
+
success_response(url: page.url, title: page.title)
|
|
40
17
|
rescue Ferrum::TimeoutError => e
|
|
41
18
|
logger.error "Go back timeout: #{e.message}"
|
|
42
19
|
error_response("Go back timed out: #{e.message}")
|
|
@@ -2,41 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Go forward to the next page in browser history
|
|
6
6
|
class GoForwardTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.description
|
|
12
|
-
'Go forward to the next page in browser history'
|
|
13
|
-
end
|
|
7
|
+
tool_name 'go_forward'
|
|
8
|
+
description 'Go forward to the next page in browser history'
|
|
14
9
|
|
|
15
|
-
def
|
|
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)
|
|
10
|
+
def perform(_params)
|
|
29
11
|
ensure_browser_active
|
|
30
12
|
logger.info 'Going forward'
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Wait for network to be idle to ensure page is loaded
|
|
34
|
-
browser.network.wait_for_idle(timeout: 30)
|
|
13
|
+
page.forward
|
|
14
|
+
page.network.wait_for_idle(timeout: 30)
|
|
35
15
|
|
|
36
|
-
success_response(
|
|
37
|
-
url: browser.url,
|
|
38
|
-
title: browser.title
|
|
39
|
-
)
|
|
16
|
+
success_response(url: page.url, title: page.title)
|
|
40
17
|
rescue Ferrum::TimeoutError => e
|
|
41
18
|
logger.error "Go forward timeout: #{e.message}"
|
|
42
19
|
error_response("Go forward timed out: #{e.message}")
|
|
@@ -2,51 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Hover over an element
|
|
6
6
|
class HoverTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
7
|
+
tool_name 'hover'
|
|
8
|
+
description 'Hover over an element (CSS selector, XPath or snapshot ref)'
|
|
31
9
|
|
|
32
|
-
|
|
33
|
-
selector = param(params, :selector)
|
|
10
|
+
param :selector, type: :string, required: true, description: 'Selector of the element to hover over'
|
|
34
11
|
|
|
12
|
+
def perform(params)
|
|
13
|
+
selector = params[:selector]
|
|
35
14
|
logger.info "Hovering over element: #{selector}"
|
|
36
15
|
|
|
37
|
-
# First ensure element exists and is ready
|
|
38
16
|
element = find_element(selector)
|
|
17
|
+
element.scroll_into_view
|
|
39
18
|
|
|
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
19
|
begin
|
|
45
20
|
element.hover
|
|
46
|
-
logger.debug 'Native hover successful'
|
|
47
21
|
rescue StandardError => e
|
|
48
22
|
logger.debug "Native hover failed, using JavaScript: #{e.message}"
|
|
49
|
-
|
|
23
|
+
page.execute(<<~JS, element)
|
|
24
|
+
arguments[0].dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window }));
|
|
25
|
+
JS
|
|
50
26
|
end
|
|
51
27
|
|
|
52
28
|
success_response(message: "Hovered over #{selector}")
|
|
@@ -54,23 +30,6 @@ module FerrumMCP
|
|
|
54
30
|
logger.error "Hover failed: #{e.message}"
|
|
55
31
|
error_response("Failed to hover: #{e.message}")
|
|
56
32
|
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
33
|
end
|
|
75
34
|
end
|
|
76
35
|
end
|
|
@@ -2,28 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
+
# List all active sessions
|
|
5
6
|
class ListSessionsTool < SessionTool
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
7
|
+
tool_name 'list_sessions'
|
|
8
|
+
description 'List all active browser sessions with their information (id, status, type, uptime, etc.)'
|
|
20
9
|
|
|
21
|
-
def
|
|
10
|
+
def perform(_params)
|
|
22
11
|
sessions = session_manager.list_sessions
|
|
23
|
-
success_response(
|
|
24
|
-
count: sessions.size,
|
|
25
|
-
sessions: sessions
|
|
26
|
-
)
|
|
12
|
+
success_response(count: sessions.size, sessions: sessions)
|
|
27
13
|
rescue StandardError => e
|
|
28
14
|
logger.error "Failed to list sessions: #{e.message}"
|
|
29
15
|
error_response("Failed to list sessions: #{e.message}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FerrumMCP
|
|
4
|
+
module Tools
|
|
5
|
+
# List open tabs
|
|
6
|
+
class ListTabsTool < TabTool
|
|
7
|
+
tool_name 'list_tabs'
|
|
8
|
+
description 'List the open tabs of the session with their tab_id, url and title; marks the current one'
|
|
9
|
+
|
|
10
|
+
def perform(_params)
|
|
11
|
+
ensure_browser_active
|
|
12
|
+
list = tabs.map.with_index { |tab, index| describe(tab, index) }
|
|
13
|
+
success_response(count: list.length, tabs: list)
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
logger.error "List tabs failed: #{e.message}"
|
|
16
|
+
error_response("Failed to list tabs: #{e.message}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -2,51 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Navigate to a URL
|
|
6
6
|
class NavigateTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.description
|
|
12
|
-
'Navigate to a specific URL in the browser'
|
|
13
|
-
end
|
|
7
|
+
tool_name 'navigate'
|
|
8
|
+
description 'Navigate to a specific URL in the browser and wait for the network to settle'
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
10
|
+
param :url, type: :string, required: true,
|
|
11
|
+
description: 'The URL to navigate to (must include protocol: http:// or https://)'
|
|
12
|
+
param :wait_for_idle, type: :boolean, default: true,
|
|
13
|
+
description: 'Wait for network idle after navigation (default: true)'
|
|
14
|
+
param :timeout, type: :number, default: 30,
|
|
15
|
+
description: 'Seconds to wait for the network to become idle (default: 30)'
|
|
31
16
|
|
|
32
|
-
def
|
|
17
|
+
def perform(params)
|
|
33
18
|
ensure_browser_active
|
|
34
|
-
url =
|
|
19
|
+
url = params[:url].to_s
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
raise ToolError, 'URL must start with http:// or https://' unless %r{^https?://}.match?(url)
|
|
21
|
+
raise ToolError, 'URL must start with http:// or https://' unless %r{\Ahttps?://}.match?(url)
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
browser.goto(url)
|
|
23
|
+
@browser_manager.config.url_policy.check!(url)
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
logger.info "Navigating to: #{url}"
|
|
26
|
+
page.goto(url)
|
|
27
|
+
page.network.wait_for_idle(timeout: params[:timeout]) if params[:wait_for_idle]
|
|
45
28
|
|
|
46
|
-
success_response(
|
|
47
|
-
url: browser.url,
|
|
48
|
-
title: browser.title
|
|
49
|
-
)
|
|
29
|
+
success_response(url: page.url, title: page.title)
|
|
50
30
|
rescue Ferrum::TimeoutError => e
|
|
51
31
|
logger.error "Navigation timeout: #{e.message}"
|
|
52
32
|
error_response("Navigation timed out: #{e.message}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FerrumMCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Open a new tab
|
|
6
|
+
class NewTabTool < TabTool
|
|
7
|
+
tool_name 'new_tab'
|
|
8
|
+
description 'Open a new tab (optionally at a URL) and make it the current tab'
|
|
9
|
+
|
|
10
|
+
param :url, type: :string, description: 'Optional: URL to open in the new tab'
|
|
11
|
+
|
|
12
|
+
def perform(params)
|
|
13
|
+
ensure_browser_active
|
|
14
|
+
url = params[:url]
|
|
15
|
+
if url
|
|
16
|
+
raise ToolError, 'URL must start with http:// or https://' unless %r{\Ahttps?://}.match?(url)
|
|
17
|
+
|
|
18
|
+
@browser_manager.config.url_policy.check!(url)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
logger.info "Opening new tab#{" at #{url}" if url}"
|
|
22
|
+
tab = @browser_manager.create_page
|
|
23
|
+
@browser_manager.select_page(tab)
|
|
24
|
+
if url
|
|
25
|
+
tab.goto(url)
|
|
26
|
+
tab.network.wait_for_idle(timeout: 30)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
success_response(tab_id: tab.target_id, url: tab.url, title: tab.title, count: tabs.length)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
logger.error "New tab failed: #{e.message}"
|
|
32
|
+
error_response("Failed to open tab: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,53 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module FerrumMCP
|
|
4
4
|
module Tools
|
|
5
|
-
#
|
|
5
|
+
# Press keyboard keys
|
|
6
6
|
class PressKeyTool < BaseTool
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
7
|
+
tool_name 'press_key'
|
|
8
|
+
description 'Press a keyboard key (e.g. Enter, Tab, Escape, ArrowDown) or type a character'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
10
|
+
param :key, type: :string, required: true, description: 'Key to press (Enter, Tab, Escape, ArrowDown, etc.)'
|
|
11
|
+
param :selector, type: :string, description: 'Optional: selector of the element to focus before pressing the key'
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
13
|
+
KEY_ALIASES = {
|
|
14
|
+
'enter' => :Enter, 'return' => :Enter, 'tab' => :Tab, 'escape' => :Escape, 'esc' => :Escape,
|
|
15
|
+
'backspace' => :Backspace, 'delete' => :Delete, 'del' => :Delete,
|
|
16
|
+
'arrowdown' => :Down, 'down' => :Down, 'arrowup' => :Up, 'up' => :Up,
|
|
17
|
+
'arrowleft' => :Left, 'left' => :Left, 'arrowright' => :Right, 'right' => :Right,
|
|
18
|
+
'space' => ' ', 'pageup' => :PageUp, 'pagedown' => :PageDown, 'home' => :Home, 'end' => :End
|
|
19
|
+
}.freeze
|
|
35
20
|
|
|
36
|
-
def
|
|
37
|
-
key =
|
|
38
|
-
selector =
|
|
21
|
+
def perform(params)
|
|
22
|
+
key = params[:key].to_s
|
|
23
|
+
selector = params[:selector]
|
|
39
24
|
|
|
40
25
|
if selector
|
|
41
26
|
logger.info "Focusing element: #{selector}"
|
|
42
|
-
|
|
43
|
-
element.focus
|
|
27
|
+
find_element(selector).focus
|
|
44
28
|
end
|
|
45
29
|
|
|
46
30
|
logger.info "Pressing key: #{key}"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Use keyboard.type for key presses
|
|
50
|
-
# This handles both special keys and regular characters correctly
|
|
51
|
-
browser.keyboard.type(normalized_key)
|
|
31
|
+
page.keyboard.type(normalize_key(key))
|
|
52
32
|
|
|
53
33
|
success_response(message: "Pressed key: #{key}")
|
|
54
34
|
rescue StandardError => e
|
|
@@ -59,32 +39,7 @@ module FerrumMCP
|
|
|
59
39
|
private
|
|
60
40
|
|
|
61
41
|
def normalize_key(key)
|
|
62
|
-
|
|
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
|
|
42
|
+
KEY_ALIASES.fetch(key.downcase) { key.length == 1 ? key : key.to_sym }
|
|
88
43
|
end
|
|
89
44
|
end
|
|
90
45
|
end
|