mcp iframe updates
This commit is contained in:
parent
cf34368901
commit
e1f2c45c3a
|
@ -527,7 +527,8 @@ func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout
|
||||||
|
|
||||||
// SwitchToIframe switches the context to an iframe for subsequent commands
|
// SwitchToIframe switches the context to an iframe for subsequent commands
|
||||||
// If tabID is empty, the current tab will be used
|
// If tabID is empty, the current tab will be used
|
||||||
func (c *Client) SwitchToIframe(tabID, selector string) error {
|
// timeout is in seconds, 0 means no timeout
|
||||||
|
func (c *Client) SwitchToIframe(tabID, selector string, timeout int) error {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"selector": selector,
|
"selector": selector,
|
||||||
}
|
}
|
||||||
|
@ -537,6 +538,11 @@ func (c *Client) SwitchToIframe(tabID, selector string) error {
|
||||||
params["tab"] = tabID
|
params["tab"] = tabID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add timeout if specified
|
||||||
|
if timeout > 0 {
|
||||||
|
params["timeout"] = strconv.Itoa(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := c.SendCommand("switch-iframe", params)
|
resp, err := c.SendCommand("switch-iframe", params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
134
daemon/daemon.go
134
daemon/daemon.go
|
@ -462,8 +462,17 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||||
case "switch-iframe":
|
case "switch-iframe":
|
||||||
tabID := cmd.Params["tab"]
|
tabID := cmd.Params["tab"]
|
||||||
selector := cmd.Params["selector"]
|
selector := cmd.Params["selector"]
|
||||||
|
timeoutStr := cmd.Params["timeout"]
|
||||||
|
|
||||||
err := d.switchToIframe(tabID, selector)
|
// Parse timeout (default to 5 seconds if not specified)
|
||||||
|
timeout := 5
|
||||||
|
if timeoutStr != "" {
|
||||||
|
if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
|
||||||
|
timeout = parsedTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.switchToIframe(tabID, selector, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response = Response{Success: false, Error: err.Error()}
|
response = Response{Success: false, Error: err.Error()}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1678,10 +1687,13 @@ func (d *Daemon) takeScreenshot(tabID, outputPath string, fullPage bool, timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// switchToIframe switches the context to an iframe for subsequent commands
|
// switchToIframe switches the context to an iframe for subsequent commands
|
||||||
func (d *Daemon) switchToIframe(tabID, selector string) error {
|
func (d *Daemon) switchToIframe(tabID, selector string, timeout int) error {
|
||||||
|
d.debugLog("Switching to iframe: selector=%s, tab=%s, timeout=%d", selector, tabID, timeout)
|
||||||
|
|
||||||
// Get the main page first (not iframe context)
|
// Get the main page first (not iframe context)
|
||||||
actualTabID, err := d.getTabID(tabID)
|
actualTabID, err := d.getTabID(tabID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.debugLog("Failed to get tab ID: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1691,49 +1703,155 @@ func (d *Daemon) switchToIframe(tabID, selector string) error {
|
||||||
// Get the main page (bypass iframe context)
|
// Get the main page (bypass iframe context)
|
||||||
mainPage, exists := d.tabs[actualTabID]
|
mainPage, exists := d.tabs[actualTabID]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
d.debugLog("Tab %s not in cache, trying to find it", actualTabID)
|
||||||
// Try to find it
|
// Try to find it
|
||||||
mainPage, err = d.findPageByID(actualTabID)
|
mainPage, err = d.findPageByID(actualTabID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.debugLog("Failed to find tab %s: %v", actualTabID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mainPage == nil {
|
if mainPage == nil {
|
||||||
|
d.debugLog("Tab %s not found", actualTabID)
|
||||||
return fmt.Errorf("tab not found: %s", actualTabID)
|
return fmt.Errorf("tab not found: %s", actualTabID)
|
||||||
}
|
}
|
||||||
d.tabs[actualTabID] = mainPage
|
d.tabs[actualTabID] = mainPage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the iframe element
|
d.debugLog("Found main page for tab %s, looking for iframe element", actualTabID)
|
||||||
iframeElement, err := mainPage.Element(selector)
|
|
||||||
|
// Find the iframe element with timeout
|
||||||
|
var iframeElement *rod.Element
|
||||||
|
if timeout > 0 {
|
||||||
|
// Use timeout context for finding the iframe element
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create a channel to signal completion
|
||||||
|
done := make(chan struct {
|
||||||
|
element *rod.Element
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
|
||||||
|
// Execute the element search in a goroutine
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
done <- struct {
|
||||||
|
element *rod.Element
|
||||||
|
err error
|
||||||
|
}{nil, fmt.Errorf("iframe element search panicked: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
element, err := mainPage.Timeout(time.Duration(timeout) * time.Second).Element(selector)
|
||||||
|
done <- struct {
|
||||||
|
element *rod.Element
|
||||||
|
err error
|
||||||
|
}{element, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for either completion or timeout
|
||||||
|
select {
|
||||||
|
case result := <-done:
|
||||||
|
iframeElement = result.element
|
||||||
|
err = result.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
d.debugLog("Iframe element search timed out after %d seconds", timeout)
|
||||||
|
return fmt.Errorf("failed to find iframe element (timeout after %ds): %s", timeout, selector)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No timeout
|
||||||
|
iframeElement, err = mainPage.Element(selector)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.debugLog("Failed to find iframe element: %v", err)
|
||||||
return fmt.Errorf("failed to find iframe element: %w", err)
|
return fmt.Errorf("failed to find iframe element: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the iframe's page context
|
d.debugLog("Found iframe element, getting frame context")
|
||||||
iframePage, err := iframeElement.Frame()
|
|
||||||
|
// Get the iframe's page context with timeout
|
||||||
|
var iframePage *rod.Page
|
||||||
|
if timeout > 0 {
|
||||||
|
// Use timeout context for getting the frame
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create a channel to signal completion
|
||||||
|
done := make(chan struct {
|
||||||
|
page *rod.Page
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
|
||||||
|
// Execute the frame access in a goroutine
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
done <- struct {
|
||||||
|
page *rod.Page
|
||||||
|
err error
|
||||||
|
}{nil, fmt.Errorf("iframe frame access panicked: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
page, err := iframeElement.Frame()
|
||||||
|
done <- struct {
|
||||||
|
page *rod.Page
|
||||||
|
err error
|
||||||
|
}{page, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for either completion or timeout
|
||||||
|
select {
|
||||||
|
case result := <-done:
|
||||||
|
iframePage = result.page
|
||||||
|
err = result.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
d.debugLog("Iframe frame access timed out after %d seconds", timeout)
|
||||||
|
return fmt.Errorf("failed to get iframe context (timeout after %ds)", timeout)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No timeout
|
||||||
|
iframePage, err = iframeElement.Frame()
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.debugLog("Failed to get iframe context: %v", err)
|
||||||
return fmt.Errorf("failed to get iframe context: %w", err)
|
return fmt.Errorf("failed to get iframe context: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the iframe page context
|
// Store the iframe page context
|
||||||
d.iframePages[actualTabID] = iframePage
|
d.iframePages[actualTabID] = iframePage
|
||||||
|
d.debugLog("Successfully switched to iframe context for tab %s", actualTabID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// switchToMain switches back to the main page context
|
// switchToMain switches back to the main page context
|
||||||
func (d *Daemon) switchToMain(tabID string) error {
|
func (d *Daemon) switchToMain(tabID string) error {
|
||||||
|
d.debugLog("Switching back to main context: tab=%s", tabID)
|
||||||
|
|
||||||
// Get the tab ID to use (may be the current tab)
|
// Get the tab ID to use (may be the current tab)
|
||||||
actualTabID, err := d.getTabID(tabID)
|
actualTabID, err := d.getTabID(tabID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.debugLog("Failed to get tab ID: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
// Remove the iframe context for this tab
|
// Check if there was an iframe context to remove
|
||||||
delete(d.iframePages, actualTabID)
|
if _, exists := d.iframePages[actualTabID]; exists {
|
||||||
|
d.debugLog("Removing iframe context for tab %s", actualTabID)
|
||||||
|
// Remove the iframe context for this tab
|
||||||
|
delete(d.iframePages, actualTabID)
|
||||||
|
} else {
|
||||||
|
d.debugLog("No iframe context found for tab %s", actualTabID)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.debugLog("Successfully switched back to main context for tab %s", actualTabID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
main.go
3
main.go
|
@ -116,6 +116,7 @@ func main() {
|
||||||
// switch-iframe flags
|
// switch-iframe flags
|
||||||
switchIframeTabID := switchIframeCmd.String("tab", "", "Tab ID to switch iframe context in (optional, uses current tab if not specified)")
|
switchIframeTabID := switchIframeCmd.String("tab", "", "Tab ID to switch iframe context in (optional, uses current tab if not specified)")
|
||||||
switchIframeSelector := switchIframeCmd.String("selector", "", "CSS selector for the iframe element")
|
switchIframeSelector := switchIframeCmd.String("selector", "", "CSS selector for the iframe element")
|
||||||
|
switchIframeTimeout := switchIframeCmd.Int("timeout", 5, "Timeout in seconds for iframe switching")
|
||||||
switchIframeHost := switchIframeCmd.String("host", "localhost", "Daemon host")
|
switchIframeHost := switchIframeCmd.String("host", "localhost", "Daemon host")
|
||||||
switchIframePort := switchIframeCmd.Int("port", 8989, "Daemon port")
|
switchIframePort := switchIframeCmd.Int("port", 8989, "Daemon port")
|
||||||
|
|
||||||
|
@ -366,7 +367,7 @@ func main() {
|
||||||
c := client.NewClient(*switchIframeHost, *switchIframePort)
|
c := client.NewClient(*switchIframeHost, *switchIframePort)
|
||||||
|
|
||||||
// Switch to iframe
|
// Switch to iframe
|
||||||
err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector)
|
err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector, *switchIframeTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -112,17 +112,21 @@ Switch iframe context for subsequent operations.
|
||||||
- `action` (required): One of "enter", "exit"
|
- `action` (required): One of "enter", "exit"
|
||||||
- `selector` (optional): Iframe CSS selector (required for "enter" action)
|
- `selector` (optional): Iframe CSS selector (required for "enter" action)
|
||||||
- `tab` (optional): Specific tab ID to use
|
- `tab` (optional): Specific tab ID to use
|
||||||
|
- `timeout` (optional): Timeout in seconds (default: 5)
|
||||||
|
|
||||||
**Example Usage:**
|
**Example Usage:**
|
||||||
```
|
```
|
||||||
web_iframe_cremotemcp:
|
web_iframe_cremotemcp:
|
||||||
action: "enter"
|
action: "enter"
|
||||||
selector: "iframe#payment-form"
|
selector: "iframe#payment-form"
|
||||||
|
timeout: 10
|
||||||
|
|
||||||
web_iframe_cremotemcp:
|
web_iframe_cremotemcp:
|
||||||
action: "exit"
|
action: "exit"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** The timeout parameter is particularly important for iframe operations as they can hang if the iframe takes time to load or if the selector doesn't match any elements.
|
||||||
|
|
||||||
### 7. `file_upload_cremotemcp`
|
### 7. `file_upload_cremotemcp`
|
||||||
Upload files from the client to the container for use in form uploads.
|
Upload files from the client to the container for use in form uploads.
|
||||||
|
|
||||||
|
|
|
@ -750,7 +750,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err
|
||||||
if selector == "" {
|
if selector == "" {
|
||||||
return ToolResult{}, fmt.Errorf("selector parameter is required for enter action")
|
return ToolResult{}, fmt.Errorf("selector parameter is required for enter action")
|
||||||
}
|
}
|
||||||
err = s.client.SwitchToIframe(tab, selector)
|
err = s.client.SwitchToIframe(tab, selector, 5) // Default 5 second timeout
|
||||||
s.iframeMode = true
|
s.iframeMode = true
|
||||||
data = map[string]string{"action": "entered", "selector": selector}
|
data = map[string]string{"action": "entered", "selector": selector}
|
||||||
|
|
||||||
|
|
|
@ -636,7 +636,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err
|
||||||
if selector == "" {
|
if selector == "" {
|
||||||
return ToolResult{}, fmt.Errorf("selector parameter is required for enter action")
|
return ToolResult{}, fmt.Errorf("selector parameter is required for enter action")
|
||||||
}
|
}
|
||||||
err = s.client.SwitchToIframe(tab, selector)
|
err = s.client.SwitchToIframe(tab, selector, 5) // Default 5 second timeout
|
||||||
s.iframeMode = true
|
s.iframeMode = true
|
||||||
data = map[string]string{"action": "entered", "selector": selector}
|
data = map[string]string{"action": "entered", "selector": selector}
|
||||||
|
|
||||||
|
|
BIN
mcp/cremote-mcp
BIN
mcp/cremote-mcp
Binary file not shown.
BIN
mcp/cremote-mcp2
BIN
mcp/cremote-mcp2
Binary file not shown.
|
@ -538,6 +538,11 @@ func main() {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Tab ID (optional)",
|
"description": "Tab ID (optional)",
|
||||||
},
|
},
|
||||||
|
"timeout": map[string]any{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Timeout in seconds",
|
||||||
|
"default": 5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"action"},
|
Required: []string{"action"},
|
||||||
},
|
},
|
||||||
|
@ -551,6 +556,7 @@ func main() {
|
||||||
action := getStringParam(params, "action", "")
|
action := getStringParam(params, "action", "")
|
||||||
selector := getStringParam(params, "selector", "")
|
selector := getStringParam(params, "selector", "")
|
||||||
tab := getStringParam(params, "tab", cremoteServer.currentTab)
|
tab := getStringParam(params, "tab", cremoteServer.currentTab)
|
||||||
|
timeout := getIntParam(params, "timeout", 5)
|
||||||
|
|
||||||
if action == "" {
|
if action == "" {
|
||||||
return nil, fmt.Errorf("action parameter is required")
|
return nil, fmt.Errorf("action parameter is required")
|
||||||
|
@ -567,7 +573,7 @@ func main() {
|
||||||
if selector == "" {
|
if selector == "" {
|
||||||
return nil, fmt.Errorf("selector parameter is required for enter action")
|
return nil, fmt.Errorf("selector parameter is required for enter action")
|
||||||
}
|
}
|
||||||
err = cremoteServer.client.SwitchToIframe(tab, selector)
|
err = cremoteServer.client.SwitchToIframe(tab, selector, timeout)
|
||||||
cremoteServer.iframeMode = true
|
cremoteServer.iframeMode = true
|
||||||
message = fmt.Sprintf("Entered iframe: %s", selector)
|
message = fmt.Sprintf("Entered iframe: %s", selector)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test script for iframe timeout functionality
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting iframe timeout test..."
|
||||||
|
|
||||||
|
# Start the daemon in background
|
||||||
|
echo "Starting cremotedaemon..."
|
||||||
|
./cremotedaemon --debug &
|
||||||
|
DAEMON_PID=$!
|
||||||
|
|
||||||
|
# Wait for daemon to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Function to cleanup
|
||||||
|
cleanup() {
|
||||||
|
echo "Cleaning up..."
|
||||||
|
kill $DAEMON_PID 2>/dev/null || true
|
||||||
|
wait $DAEMON_PID 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set trap for cleanup
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Test 1: Basic iframe switching with timeout
|
||||||
|
echo "Test 1: Basic iframe switching with timeout"
|
||||||
|
TAB_ID=$(./cremote open-tab --timeout 5 | grep -o '"[^"]*"' | tr -d '"')
|
||||||
|
echo "Created tab: $TAB_ID"
|
||||||
|
|
||||||
|
# Load test page
|
||||||
|
./cremote load-url --tab "$TAB_ID" --url "file://$(pwd)/test-iframe.html" --timeout 10
|
||||||
|
echo "Loaded test page"
|
||||||
|
|
||||||
|
# Switch to iframe with timeout
|
||||||
|
echo "Switching to iframe with 5 second timeout..."
|
||||||
|
./cremote switch-iframe --tab "$TAB_ID" --selector "#test-iframe" --timeout 5
|
||||||
|
echo "Successfully switched to iframe"
|
||||||
|
|
||||||
|
# Try to click button in iframe
|
||||||
|
echo "Clicking button in iframe..."
|
||||||
|
./cremote click-element --tab "$TAB_ID" --selector "#iframe-button" --selection-timeout 5 --action-timeout 5
|
||||||
|
echo "Successfully clicked iframe button"
|
||||||
|
|
||||||
|
# Switch back to main
|
||||||
|
echo "Switching back to main context..."
|
||||||
|
./cremote switch-main --tab "$TAB_ID"
|
||||||
|
echo "Successfully switched back to main"
|
||||||
|
|
||||||
|
# Try to click main button
|
||||||
|
echo "Clicking main page button..."
|
||||||
|
./cremote click-element --tab "$TAB_ID" --selector "#main-button" --selection-timeout 5 --action-timeout 5
|
||||||
|
echo "Successfully clicked main button"
|
||||||
|
|
||||||
|
# Test 2: Test timeout with non-existent iframe
|
||||||
|
echo ""
|
||||||
|
echo "Test 2: Testing timeout with non-existent iframe"
|
||||||
|
set +e # Allow command to fail
|
||||||
|
./cremote switch-iframe --tab "$TAB_ID" --selector "#non-existent-iframe" --timeout 2
|
||||||
|
RESULT=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $RESULT -eq 0 ]; then
|
||||||
|
echo "ERROR: Expected timeout failure but command succeeded"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "SUCCESS: Timeout correctly handled for non-existent iframe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All iframe timeout tests passed!"
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Iframe Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Main Page</h1>
|
||||||
|
<p>This is the main page content.</p>
|
||||||
|
|
||||||
|
<iframe id="test-iframe" src="data:text/html,<html><body><h2>Iframe Content</h2><button id='iframe-button'>Click Me</button><script>document.getElementById('iframe-button').onclick = function() { alert('Iframe button clicked!'); }</script></body></html>" width="400" height="200"></iframe>
|
||||||
|
|
||||||
|
<button id="main-button">Main Page Button</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('main-button').onclick = function() {
|
||||||
|
alert('Main page button clicked!');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue