mcp iframe updates

This commit is contained in:
Josh at WLTechBlog 2025-08-16 07:13:33 -05:00
parent cf34368901
commit e1f2c45c3a
11 changed files with 239 additions and 13 deletions

View File

@ -527,7 +527,8 @@ func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout
// SwitchToIframe switches the context to an iframe for subsequent commands
// 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{
"selector": selector,
}
@ -537,6 +538,11 @@ func (c *Client) SwitchToIframe(tabID, selector string) error {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("switch-iframe", params)
if err != nil {
return err

View File

@ -462,8 +462,17 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
case "switch-iframe":
tabID := cmd.Params["tab"]
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 {
response = Response{Success: false, Error: err.Error()}
} 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
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)
actualTabID, err := d.getTabID(tabID)
if err != nil {
d.debugLog("Failed to get tab ID: %v", err)
return err
}
@ -1691,49 +1703,155 @@ func (d *Daemon) switchToIframe(tabID, selector string) error {
// Get the main page (bypass iframe context)
mainPage, exists := d.tabs[actualTabID]
if !exists {
d.debugLog("Tab %s not in cache, trying to find it", actualTabID)
// Try to find it
mainPage, err = d.findPageByID(actualTabID)
if err != nil {
d.debugLog("Failed to find tab %s: %v", actualTabID, err)
return err
}
if mainPage == nil {
d.debugLog("Tab %s not found", actualTabID)
return fmt.Errorf("tab not found: %s", actualTabID)
}
d.tabs[actualTabID] = mainPage
}
// Find the iframe element
iframeElement, err := mainPage.Element(selector)
d.debugLog("Found main page for tab %s, looking for iframe element", actualTabID)
// 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 {
d.debugLog("Failed to find iframe element: %v", err)
return fmt.Errorf("failed to find iframe element: %w", err)
}
// Get the iframe's page context
iframePage, err := iframeElement.Frame()
d.debugLog("Found iframe element, getting frame context")
// 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 {
d.debugLog("Failed to get iframe context: %v", err)
return fmt.Errorf("failed to get iframe context: %w", err)
}
// Store the iframe page context
d.iframePages[actualTabID] = iframePage
d.debugLog("Successfully switched to iframe context for tab %s", actualTabID)
return nil
}
// switchToMain switches back to the main page context
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)
actualTabID, err := d.getTabID(tabID)
if err != nil {
d.debugLog("Failed to get tab ID: %v", err)
return err
}
d.mu.Lock()
defer d.mu.Unlock()
// Remove the iframe context for this tab
delete(d.iframePages, actualTabID)
// Check if there was an iframe context to remove
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
}

View File

@ -116,6 +116,7 @@ func main() {
// switch-iframe flags
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")
switchIframeTimeout := switchIframeCmd.Int("timeout", 5, "Timeout in seconds for iframe switching")
switchIframeHost := switchIframeCmd.String("host", "localhost", "Daemon host")
switchIframePort := switchIframeCmd.Int("port", 8989, "Daemon port")
@ -366,7 +367,7 @@ func main() {
c := client.NewClient(*switchIframeHost, *switchIframePort)
// Switch to iframe
err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector)
err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector, *switchIframeTimeout)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)

View File

@ -112,17 +112,21 @@ Switch iframe context for subsequent operations.
- `action` (required): One of "enter", "exit"
- `selector` (optional): Iframe CSS selector (required for "enter" action)
- `tab` (optional): Specific tab ID to use
- `timeout` (optional): Timeout in seconds (default: 5)
**Example Usage:**
```
web_iframe_cremotemcp:
action: "enter"
selector: "iframe#payment-form"
timeout: 10
web_iframe_cremotemcp:
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`
Upload files from the client to the container for use in form uploads.

View File

@ -750,7 +750,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err
if selector == "" {
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
data = map[string]string{"action": "entered", "selector": selector}

View File

@ -636,7 +636,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err
if selector == "" {
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
data = map[string]string{"action": "entered", "selector": selector}

Binary file not shown.

Binary file not shown.

View File

@ -538,6 +538,11 @@ func main() {
"type": "string",
"description": "Tab ID (optional)",
},
"timeout": map[string]any{
"type": "integer",
"description": "Timeout in seconds",
"default": 5,
},
},
Required: []string{"action"},
},
@ -551,6 +556,7 @@ func main() {
action := getStringParam(params, "action", "")
selector := getStringParam(params, "selector", "")
tab := getStringParam(params, "tab", cremoteServer.currentTab)
timeout := getIntParam(params, "timeout", 5)
if action == "" {
return nil, fmt.Errorf("action parameter is required")
@ -567,7 +573,7 @@ func main() {
if selector == "" {
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
message = fmt.Sprintf("Entered iframe: %s", selector)

71
test-iframe-timeout.sh Executable file
View File

@ -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!"

20
test-iframe.html Normal file
View File

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