This commit is contained in:
Josh at WLTechBlog
2025-10-19 10:07:47 -05:00
parent 1b01b1e857
commit 4f64dc879b
3 changed files with 740 additions and 40 deletions

View File

@@ -61,6 +61,50 @@ func getIntParam(params map[string]any, key string, defaultValue int) int {
return defaultValue
}
// handleOptionalNavigation handles optional URL navigation and cache clearing before tool execution
// Returns the tab ID to use (may create a new tab if needed)
func handleOptionalNavigation(cremoteServer *CremoteServer, params map[string]any, timeout int) (string, error) {
url := getStringParam(params, "url", "")
clearCache := getBoolParam(params, "clear_cache", false)
tab := getStringParam(params, "tab", cremoteServer.currentTab)
// If URL is provided, navigate to it
if url != "" {
// If no tab specified and no current tab, create a new one
if tab == "" {
newTab, err := cremoteServer.client.OpenTab(timeout)
if err != nil {
return "", fmt.Errorf("failed to create new tab: %w", err)
}
tab = newTab
cremoteServer.currentTab = tab
cremoteServer.tabHistory = append(cremoteServer.tabHistory, tab)
}
// Clear cache if requested
if clearCache {
err := cremoteServer.client.ClearCache(tab, timeout)
if err != nil {
return "", fmt.Errorf("failed to clear cache: %w", err)
}
}
// Load the URL
err := cremoteServer.client.LoadURL(tab, url, timeout)
if err != nil {
return "", fmt.Errorf("failed to load URL: %w", err)
}
} else if clearCache && tab != "" {
// Clear cache even if not navigating
err := cremoteServer.client.ClearCache(tab, timeout)
if err != nil {
return "", fmt.Errorf("failed to clear cache: %w", err)
}
}
return tab, nil
}
func main() {
// Get cremote daemon connection settings
cremoteHost := os.Getenv("CREMOTE_HOST")
@@ -111,7 +155,7 @@ func main() {
// Register web_navigate tool
mcpServer.AddTool(mcp.Tool{
Name: "web_navigate_cremotemcp",
Description: "Navigate to a URL and optionally take a screenshot",
Description: "Navigate to a URL with optional cache clearing, screenshot, and data extraction in a single call",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
@@ -128,10 +172,28 @@ func main() {
"description": "Timeout in seconds",
"default": 5,
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before navigation (default: false)",
"default": false,
},
"screenshot": map[string]any{
"type": "boolean",
"description": "Take screenshot after navigation",
},
"extract": map[string]any{
"type": "string",
"description": "Extract data after navigation: 'source' (page HTML), 'element' (requires extract_selector), 'javascript' (requires extract_code)",
"enum": []any{"source", "element", "javascript"},
},
"extract_selector": map[string]any{
"type": "string",
"description": "CSS selector for element extraction (required when extract='element')",
},
"extract_code": map[string]any{
"type": "string",
"description": "JavaScript code to execute (required when extract='javascript')",
},
},
Required: []string{"url"},
},
@@ -149,7 +211,11 @@ func main() {
tab := getStringParam(params, "tab", cremoteServer.currentTab)
timeout := getIntParam(params, "timeout", 5)
clearCache := getBoolParam(params, "clear_cache", false)
takeScreenshot := getBoolParam(params, "screenshot", false)
extractType := getStringParam(params, "extract", "")
extractSelector := getStringParam(params, "extract_selector", "")
extractCode := getStringParam(params, "extract_code", "")
// If no tab specified and no current tab, create a new one
if tab == "" {
@@ -162,6 +228,14 @@ func main() {
cremoteServer.tabHistory = append(cremoteServer.tabHistory, tab)
}
// Clear cache if requested
if clearCache {
err := cremoteServer.client.ClearCache(tab, timeout)
if err != nil {
return nil, fmt.Errorf("failed to clear cache: %w", err)
}
}
// Load the URL
err := cremoteServer.client.LoadURL(tab, url, timeout)
if err != nil {
@@ -169,6 +243,45 @@ func main() {
}
message := fmt.Sprintf("Successfully navigated to %s in tab %s", url, tab)
var extractedData string
// Extract data if requested
if extractType != "" {
switch extractType {
case "source":
data, err := cremoteServer.client.GetPageSource(tab, timeout)
if err != nil {
return nil, fmt.Errorf("failed to extract page source: %w", err)
}
extractedData = data
message += "\n\nExtracted page source"
case "element":
if extractSelector == "" {
return nil, fmt.Errorf("extract_selector is required when extract='element'")
}
data, err := cremoteServer.client.GetElementHTML(tab, extractSelector, timeout)
if err != nil {
return nil, fmt.Errorf("failed to extract element: %w", err)
}
extractedData = data
message += fmt.Sprintf("\n\nExtracted element: %s", extractSelector)
case "javascript":
if extractCode == "" {
return nil, fmt.Errorf("extract_code is required when extract='javascript'")
}
data, err := cremoteServer.client.EvalJS(tab, extractCode, timeout)
if err != nil {
return nil, fmt.Errorf("failed to execute JavaScript: %w", err)
}
extractedData = data
message += "\n\nExecuted JavaScript"
default:
return nil, fmt.Errorf("unknown extraction type: %s", extractType)
}
}
// Take screenshot if requested
if takeScreenshot {
@@ -176,14 +289,18 @@ func main() {
err = cremoteServer.client.TakeScreenshot(tab, screenshotPath, false, timeout)
if err == nil {
cremoteServer.screenshots = append(cremoteServer.screenshots, screenshotPath)
message += fmt.Sprintf(" (screenshot saved to %s)", screenshotPath)
message += fmt.Sprintf("\n\nScreenshot saved to %s", screenshotPath)
}
}
// Build response content
content := []mcp.Content{mcp.NewTextContent(message)}
if extractedData != "" {
content = append(content, mcp.NewTextContent(fmt.Sprintf("\n\nExtracted data:\n%s", extractedData)))
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(message),
},
Content: content,
IsError: false,
}, nil
})
@@ -318,7 +435,7 @@ func main() {
// Register web_extract tool
mcpServer.AddTool(mcp.Tool{
Name: "web_extract_cremotemcp",
Description: "Extract data from the page (source, element HTML, or execute JavaScript)",
Description: "Extract data from the page (source, element HTML, or execute JavaScript). Optionally navigate to URL first with cache clearing.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
@@ -335,6 +452,15 @@ func main() {
"type": "string",
"description": "JavaScript code (for javascript type)",
},
"url": map[string]any{
"type": "string",
"description": "Optional URL to navigate to before extraction",
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before operation (default: false)",
"default": false,
},
"tab": map[string]any{
"type": "string",
"description": "Tab ID (optional)",
@@ -357,18 +483,23 @@ func main() {
extractType := getStringParam(params, "type", "")
selector := getStringParam(params, "selector", "")
code := getStringParam(params, "code", "")
tab := getStringParam(params, "tab", cremoteServer.currentTab)
timeout := getIntParam(params, "timeout", 5)
if extractType == "" {
return nil, fmt.Errorf("type parameter is required")
}
// Handle optional navigation and cache clearing
tab, err := handleOptionalNavigation(cremoteServer, params, timeout)
if err != nil {
return nil, err
}
if tab == "" {
return nil, fmt.Errorf("no tab available - navigate to a page first")
return nil, fmt.Errorf("no tab available - navigate to a page first or provide url parameter")
}
var data string
var err error
switch extractType {
case "source":
@@ -976,7 +1107,7 @@ func main() {
// Register web_element_check tool
mcpServer.AddTool(mcp.Tool{
Name: "web_element_check_cremotemcp",
Description: "Check existence, visibility, enabled state, count elements",
Description: "Check existence, visibility, enabled state, count elements. Optionally navigate to URL first with cache clearing.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
@@ -990,6 +1121,15 @@ func main() {
"enum": []any{"exists", "visible", "enabled", "focused", "selected", "all"},
"default": "exists",
},
"url": map[string]any{
"type": "string",
"description": "Optional URL to navigate to before checking element",
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before operation (default: false)",
"default": false,
},
"tab": map[string]any{
"type": "string",
"description": "Tab ID (optional, uses current tab)",
@@ -1011,14 +1151,20 @@ func main() {
selector := getStringParam(params, "selector", "")
checkType := getStringParam(params, "check_type", "exists")
tab := getStringParam(params, "tab", cremoteServer.currentTab)
timeout := getIntParam(params, "timeout", 5)
if selector == "" {
return nil, fmt.Errorf("selector parameter is required")
}
// Handle optional navigation and cache clearing
tab, err := handleOptionalNavigation(cremoteServer, params, timeout)
if err != nil {
return nil, err
}
if tab == "" {
return nil, fmt.Errorf("no tab available - navigate to a page first")
return nil, fmt.Errorf("no tab available - navigate to a page first or provide url parameter")
}
result, err := cremoteServer.client.CheckElement(tab, selector, checkType, timeout)
@@ -1040,7 +1186,7 @@ func main() {
// Register web_element_attributes tool
mcpServer.AddTool(mcp.Tool{
Name: "web_element_attributes_cremotemcp",
Description: "Get attributes, properties, computed styles of an element",
Description: "Get attributes, properties, computed styles of an element. Optionally navigate to URL first with cache clearing.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
@@ -1053,6 +1199,15 @@ func main() {
"description": "Comma-separated list of attributes or 'all' for common attributes. Use 'style_' prefix for computed styles, 'prop_' for JavaScript properties",
"default": "all",
},
"url": map[string]any{
"type": "string",
"description": "Optional URL to navigate to before getting attributes",
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before operation (default: false)",
"default": false,
},
"tab": map[string]any{
"type": "string",
"description": "Tab ID (optional, uses current tab)",
@@ -1074,14 +1229,20 @@ func main() {
selector := getStringParam(params, "selector", "")
attributes := getStringParam(params, "attributes", "all")
tab := getStringParam(params, "tab", cremoteServer.currentTab)
timeout := getIntParam(params, "timeout", 5)
if selector == "" {
return nil, fmt.Errorf("selector parameter is required")
}
// Handle optional navigation and cache clearing
tab, err := handleOptionalNavigation(cremoteServer, params, timeout)
if err != nil {
return nil, err
}
if tab == "" {
return nil, fmt.Errorf("no tab available - navigate to a page first")
return nil, fmt.Errorf("no tab available - navigate to a page first or provide url parameter")
}
result, err := cremoteServer.client.GetElementAttributes(tab, selector, attributes, timeout)
@@ -1599,10 +1760,19 @@ func main() {
// Register web_page_info tool
mcpServer.AddTool(mcp.Tool{
Name: "web_page_info_cremotemcp",
Description: "Get comprehensive page metadata and state information",
Description: "Get comprehensive page metadata and state information. Optionally navigate to URL first with cache clearing.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"url": map[string]any{
"type": "string",
"description": "Optional URL to navigate to before getting page info",
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before operation (default: false)",
"default": false,
},
"tab": map[string]any{
"type": "string",
"description": "Tab ID (optional, uses current tab if not specified)",
@@ -1621,9 +1791,14 @@ func main() {
return nil, fmt.Errorf("invalid arguments format")
}
tabID := getStringParam(params, "tab", "")
timeout := getIntParam(params, "timeout", 5)
// Handle optional navigation and cache clearing
tabID, err := handleOptionalNavigation(cremoteServer, params, timeout)
if err != nil {
return nil, err
}
result, err := cremoteServer.client.GetPageInfo(tabID, timeout)
if err != nil {
return &mcp.CallToolResult{
@@ -5128,6 +5303,119 @@ func main() {
}, nil
})
// Register web_accessibility_full_audit tool - Master accessibility tool with navigation
mcpServer.AddTool(mcp.Tool{
Name: "web_accessibility_full_audit_cremotemcp",
Description: "Master accessibility tool: Navigate to URL, clear cache, and run comprehensive accessibility assessment in a single call. Combines navigation, cache clearing, and all accessibility tests (axe-core, contrast, keyboard, forms) with token-efficient reporting. Automatically injects axe-core if not already loaded.",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"url": map[string]any{
"type": "string",
"description": "URL to navigate to and test (required)",
},
"clear_cache": map[string]any{
"type": "boolean",
"description": "Clear browser cache before navigation (default: true)",
"default": true,
},
"tab": map[string]any{
"type": "string",
"description": "Tab ID (optional, creates new tab if not specified)",
},
"tests": map[string]any{
"type": "array",
"description": "Array of test types to run (e.g., ['wcag', 'contrast', 'keyboard', 'forms']). Defaults to 'all'",
"items": map[string]any{
"type": "string",
},
},
"standard": map[string]any{
"type": "string",
"description": "WCAG standard to test against (default: WCAG21AA)",
"default": "WCAG21AA",
},
"include_screenshots": map[string]any{
"type": "boolean",
"description": "Whether to capture screenshots of violations (default: false)",
"default": false,
},
"timeout": map[string]any{
"type": "integer",
"description": "Timeout in seconds for each operation (default: 30)",
"default": 30,
},
},
Required: []string{"url"},
},
}, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
params, ok := request.Params.Arguments.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid arguments format")
}
url := getStringParam(params, "url", "")
if url == "" {
return nil, fmt.Errorf("url parameter is required")
}
clearCache := getBoolParam(params, "clear_cache", true)
standard := getStringParam(params, "standard", "WCAG21AA")
includeScreenshots := getBoolParam(params, "include_screenshots", false)
timeout := getIntParam(params, "timeout", 30)
// Parse tests array
var tests []string
if testsParam, ok := params["tests"].([]interface{}); ok {
for _, t := range testsParam {
if testStr, ok := t.(string); ok {
tests = append(tests, testStr)
}
}
}
// Handle navigation with cache clearing
tab, err := handleOptionalNavigation(cremoteServer, params, timeout)
if err != nil {
return nil, fmt.Errorf("failed to navigate: %w", err)
}
if tab == "" {
return nil, fmt.Errorf("failed to create or identify tab")
}
// Run comprehensive accessibility assessment
result, err := cremoteServer.client.GetPageAccessibilityReport(tab, tests, standard, includeScreenshots, timeout)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("Failed to get page accessibility report: %v", err)),
},
IsError: true,
}, nil
}
// Format results as JSON
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal results: %w", err)
}
// Build response message
message := fmt.Sprintf("Accessibility audit completed for %s", url)
if clearCache {
message += " (cache cleared)"
}
message += "\n\n" + string(resultJSON)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(message),
},
IsError: false,
}, nil
})
// Register web_contrast_audit tool
mcpServer.AddTool(mcp.Tool{
Name: "web_contrast_audit_cremotemcp",