UNPKG

mcp-appium-visual

Version:

MCP Server for Appium mobile automation with visual recovery

1,403 lines 135 kB
import { z } from "zod"; import { AppiumHelper } from "../lib/appium/appiumHelper.js"; import * as fs from "fs/promises"; // Shared Appium instance for reuse across tool calls let appiumHelper = null; // Global configuration that can be set from server config let globalAppiumUrl; /** * Get the Appium helper, validating the session if it exists * This is a utility function to centralize session validation and recovery * * @returns The existing and validated appiumHelper or null if not initialized */ async function getValidAppiumHelper() { if (!appiumHelper) { return null; } try { // Validate the session and attempt recovery if needed const isSessionValid = await appiumHelper.validateSession(); if (!isSessionValid) { console.error("Appium session validation failed and could not be recovered automatically"); return null; } return appiumHelper; } catch (error) { console.error("Error validating Appium session:", error instanceof Error ? error.message : String(error)); return null; } } /** * Register mobile automation tools with the MCP server */ export function registerMobileTools(server, config) { // Store the appium URL from config for use in tools if (config?.appiumUrl) { globalAppiumUrl = config.appiumUrl; } // Tool: Initialize Appium driver server.tool("initialize-appium", "Initialize an Appium driver session for mobile automation", { platformName: z .enum(["Android", "iOS"]) .describe("The mobile platform to automate"), deviceName: z.string().describe("The name of the device to target"), udid: z .string() .optional() .describe("Device unique identifier (required for real devices)"), app: z .string() .optional() .describe("Path to the app to install (optional)"), appPackage: z.string().optional().describe("App package name (Android)"), appActivity: z .string() .optional() .describe("App activity name to launch (Android)"), bundleId: z.string().optional().describe("Bundle identifier (iOS)"), automationName: z .enum(["UiAutomator2", "XCUITest"]) .optional() .describe("Automation engine to use"), noReset: z .boolean() .optional() .describe("Preserve app state between sessions"), fullReset: z .boolean() .optional() .describe("Perform a full reset (uninstall app before starting)"), appiumUrl: z.string().optional().describe("URL of the Appium server"), screenshotDir: z .string() .optional() .describe("Directory to save screenshots"), }, async (params) => { try { // If there's an existing session, try to close it first if (appiumHelper) { try { await appiumHelper.closeDriver(); } catch (error) { console.warn("Error closing existing Appium session:", error instanceof Error ? error.message : String(error)); } } // Create capabilities object from parameters const capabilities = { platformName: params.platformName, deviceName: params.deviceName, }; // Add optional capabilities if (params.udid) capabilities.udid = params.udid; if (params.app) capabilities.app = params.app; if (params.appPackage) capabilities.appPackage = params.appPackage; if (params.appActivity) capabilities.appActivity = params.appActivity; if (params.bundleId) capabilities.bundleId = params.bundleId; if (params.automationName) capabilities.automationName = params.automationName; if (params.noReset !== undefined) capabilities.noReset = params.noReset; if (params.fullReset !== undefined) capabilities.fullReset = params.fullReset; // Set default automation based on platform if not specified if (!capabilities.automationName) { capabilities.automationName = params.platformName === "Android" ? "UiAutomator2" : "XCUITest"; } // Create and initialize Appium helper appiumHelper = new AppiumHelper(params.screenshotDir || "./screenshots"); // Use appiumUrl from params, or fall back to global config const appiumUrl = params.appiumUrl || globalAppiumUrl; await appiumHelper.initializeDriver(capabilities, appiumUrl); return { content: [ { type: "text", text: `Successfully initialized Appium session for ${params.platformName} device: ${params.deviceName}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error initializing Appium session: ${error.message}`, }, ], }; } }); // Tool: Close Appium driver session server.tool("close-appium", "Close the current Appium driver session", {}, async () => { try { if (!appiumHelper) { return { content: [ { type: "text", text: "No active Appium session to close.", }, ], }; } await appiumHelper.closeDriver(); appiumHelper = null; return { content: [ { type: "text", text: "Successfully closed Appium session.", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error closing Appium session: ${error.message}`, }, ], }; } }); // Tool: Take screenshot using Appium server.tool("appium-screenshot", "Take a screenshot using Appium", { name: z.string().describe("Base name for the screenshot file"), }, async ({ name }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const screenshotPath = await validAppiumHelper.takeScreenshot(name); return { content: [ { type: "text", text: `Screenshot saved to: ${screenshotPath}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error taking screenshot: ${error.message}`, }, ], }; } }); // Tool: Tap on element server.tool("tap-element", "Tap on a UI element identified by a selector", { selector: z.string().describe("Element selector (e.g., xpath, id)"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), }, async ({ selector, strategy }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } console.log(`MCP server: Attempting to tap element with selector "${selector}" using strategy "${strategy || "xpath"}"`); // Direct Approach: Find the element and click it directly try { const element = await validAppiumHelper.findElement(selector, strategy || "xpath"); console.log("MCP server: Element found, attempting direct click"); await element.waitForClickable({ timeout: 5000 }); await element.click(); console.log("MCP server: Direct element click successful"); return { content: [ { type: "text", text: `Successfully tapped on element: ${selector}`, }, ], }; } catch (clickError) { console.log(`MCP server: Direct click failed: ${clickError instanceof Error ? clickError.message : String(clickError)}`); // Fallback 1: Try using AppiumHelper.tapElement which has its own implementation try { console.log("MCP server: Attempting tap using AppiumHelper.tapElement"); const success = await validAppiumHelper.tapElement(selector, strategy || "xpath"); if (success) { console.log("MCP server: AppiumHelper.tapElement successful"); return { content: [ { type: "text", text: `Successfully tapped on element: ${selector} using tapElement method`, }, ], }; } } catch (tapError) { console.log(`MCP server: tapElement method failed: ${tapError instanceof Error ? tapError.message : String(tapError)}`); } // Fallback 2: Try using touchAction directly with coordinates try { console.log("MCP server: Attempting touchAction as final fallback"); const element = await validAppiumHelper.findElement(selector, strategy || "xpath"); const location = await element.getLocation(); const size = await element.getSize(); // Click in the center of the element const x = location.x + size.width / 2; const y = location.y + size.height / 2; console.log(`MCP server: Using touchAction at coordinates (${x}, ${y})`); await validAppiumHelper .getDriver() .touchAction([{ action: "press", x, y }, { action: "release" }]); console.log("MCP server: TouchAction successful"); return { content: [ { type: "text", text: `Successfully tapped on element: ${selector} (using touch coordinates)`, }, ], }; } catch (touchError) { console.log(`MCP server: TouchAction failed: ${touchError instanceof Error ? touchError.message : String(touchError)}`); throw touchError; } } } catch (error) { console.log(`MCP server: All tap attempts failed: ${error?.message}`); return { content: [ { type: "text", text: `Error tapping element: ${error.message}`, }, ], }; } }); // Tool: Send keys to element server.tool("send-keys", "Send text input to a UI element", { selector: z.string().describe("Element selector (e.g., xpath, id)"), text: z.string().describe("Text to input"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), }, async ({ selector, text, strategy }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const success = await validAppiumHelper.sendKeys(selector, text, strategy || "xpath"); if (success) { return { content: [ { type: "text", text: `Successfully sent text to element: ${selector}`, }, ], }; } else { return { content: [ { type: "text", text: `Failed to send text to element: ${selector}. Element might not be visible or present.`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error sending text to element: ${error.message}`, }, ], }; } }); // Tool: Get page source (UI XML) server.tool("get-page-source", "Get the XML representation of the current UI", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const source = await validAppiumHelper.getPageSource(); return { content: [ { type: "text", text: `UI Source XML:\n${source}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving page source: ${error.message}`, }, ], }; } }); // Tool: Swipe on screen server.tool("swipe", "Perform a swipe gesture on the screen", { startX: z.number().describe("Starting X coordinate"), startY: z.number().describe("Starting Y coordinate"), endX: z.number().describe("Ending X coordinate"), endY: z.number().describe("Ending Y coordinate"), duration: z .number() .optional() .describe("Duration of the swipe in milliseconds (default: 800)"), }, async ({ startX, startY, endX, endY, duration }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const success = await validAppiumHelper.swipe(startX, startY, endX, endY, duration || 800); if (success) { return { content: [ { type: "text", text: `Successfully performed swipe from (${startX},${startY}) to (${endX},${endY})`, }, ], }; } else { return { content: [ { type: "text", text: `Failed to perform swipe gesture`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error performing swipe: ${error.message}`, }, ], }; } }); // Tool: Wait for element server.tool("wait-for-element", "Wait for an element to be visible on screen", { selector: z.string().describe("Element selector (e.g., xpath, id)"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), timeoutMs: z .number() .optional() .describe("Timeout in milliseconds (default: 10000)"), }, async ({ selector, strategy, timeoutMs }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const success = await validAppiumHelper.waitForElement(selector, strategy || "xpath", timeoutMs || 10000); if (success) { return { content: [ { type: "text", text: `Element ${selector} is now visible`, }, ], }; } else { return { content: [ { type: "text", text: `Timed out waiting for element: ${selector}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error waiting for element: ${error.message}`, }, ], }; } }); // Tool: Long press on element server.tool("long-press", "Perform a long press gesture on an element", { selector: z.string().describe("Element selector (e.g., xpath, id)"), duration: z .number() .optional() .describe("Duration of the long press in milliseconds (default: 1000)"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), }, async ({ selector, duration, strategy }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const success = await validAppiumHelper.longPress(selector, duration || 1000, strategy || "xpath"); if (success) { return { content: [ { type: "text", text: `Successfully performed long press on element: ${selector}`, }, ], }; } else { return { content: [ { type: "text", text: `Failed to perform long press on element: ${selector}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error performing long press: ${error.message}`, }, ], }; } }); // Tool: Scroll to element server.tool("scroll-to-element", "Scroll until an element becomes visible", { selector: z .string() .describe("Element selector to scroll to (e.g., xpath)"), direction: z .enum(["up", "down", "left", "right"]) .optional() .describe("Direction to scroll (default: down)"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), maxScrolls: z .number() .optional() .describe("Maximum number of scroll attempts (default: 10)"), }, async ({ selector, direction, strategy, maxScrolls }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const success = await validAppiumHelper.scrollToElement(selector, direction || "down", strategy || "xpath", maxScrolls || 10); if (success) { return { content: [ { type: "text", text: `Successfully scrolled to element: ${selector}`, }, ], }; } else { return { content: [ { type: "text", text: `Failed to find element: ${selector} after scrolling`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error scrolling to element: ${error.message}`, }, ], }; } }); // Tool: Get device orientation server.tool("get-orientation", "Get the current device orientation", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const orientation = await validAppiumHelper.getOrientation(); return { content: [ { type: "text", text: `Current device orientation: ${orientation}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting device orientation: ${error.message}`, }, ], }; } }); // Tool: Set device orientation server.tool("set-orientation", "Set the device orientation", { orientation: z .enum(["PORTRAIT", "LANDSCAPE"]) .describe("Desired orientation: PORTRAIT or LANDSCAPE"), }, async ({ orientation }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.setOrientation(orientation); return { content: [ { type: "text", text: `Successfully set device orientation to: ${orientation}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting device orientation: ${error.message}`, }, ], }; } }); // Tool: Hide keyboard server.tool("hide-keyboard", "Hide the keyboard if it's currently visible", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.hideKeyboard(); return { content: [ { type: "text", text: "Keyboard hidden successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error hiding keyboard: ${error.message}`, }, ], }; } }); // Tool: Get current app package server.tool("get-current-package", "Get the current active app package name", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const packageName = await validAppiumHelper.getCurrentPackage(); return { content: [ { type: "text", text: `Current app package: ${packageName}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting current package: ${error.message}`, }, ], }; } }); // Tool: Get current activity (Android only) server.tool("get-current-activity", "Get the current Android activity name", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const activity = await validAppiumHelper.getCurrentActivity(); return { content: [ { type: "text", text: `Current activity: ${activity}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting current activity: ${error.message}`, }, ], }; } }); // Tool: Launch app server.tool("launch-appium-app", "Launch the app associated with the current Appium session", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.launchApp(); return { content: [ { type: "text", text: "App launched successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error launching app: ${error.message}`, }, ], }; } }); // Tool: Close app server.tool("close-app", "Close the app associated with the current Appium session", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.closeApp(); return { content: [ { type: "text", text: "App closed successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error closing app: ${error.message}`, }, ], }; } }); // Tool: Reset app server.tool("reset-app", "Reset the app (terminate and relaunch) associated with the current Appium session", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.resetApp(); return { content: [ { type: "text", text: "App reset successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error resetting app: ${error.message}`, }, ], }; } }); // Tool: Get device time server.tool("get-device-time", "Get the current device time", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const time = await validAppiumHelper.getDeviceTime(); return { content: [ { type: "text", text: `Current device time: ${time}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting device time: ${error.message}`, }, ], }; } }); // Tool: Lock device server.tool("lock-device", "Lock the device screen", { durationSec: z .number() .optional() .describe("Duration in seconds to lock the device for"), }, async ({ durationSec }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.lockDevice(durationSec); return { content: [ { type: "text", text: durationSec ? `Device locked for ${durationSec} seconds` : "Device locked", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error locking device: ${error.message}`, }, ], }; } }); // Tool: Check if device is locked server.tool("is-device-locked", "Check if the device is currently locked", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const isLocked = await validAppiumHelper.isDeviceLocked(); return { content: [ { type: "text", text: isLocked ? "Device is locked" : "Device is unlocked", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error checking device lock state: ${error.message}`, }, ], }; } }); // Tool: Unlock device server.tool("unlock-device", "Unlock the device screen", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.unlockDevice(); return { content: [ { type: "text", text: "Device unlocked successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error unlocking device: ${error.message}`, }, ], }; } }); // Tool: Press key code (Android only) server.tool("press-key-code", "Press an Android key code", { keycode: z.number().describe("Android keycode to press"), }, async ({ keycode }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.pressKeyCode(keycode); return { content: [ { type: "text", text: `Successfully pressed key code: ${keycode}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error pressing key code: ${error.message}`, }, ], }; } }); // Tool: Open notifications (Android only) server.tool("open-notifications", "Open the notifications panel (Android only)", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.openNotifications(); return { content: [ { type: "text", text: "Notifications panel opened successfully", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error opening notifications: ${error.message}`, }, ], }; } }); // Tool: Get available contexts server.tool("get-contexts", "Get all available contexts (NATIVE_APP, WEBVIEW, etc.)", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const contexts = await validAppiumHelper.getContexts(); return { content: [ { type: "text", text: `Available contexts: ${contexts.join(", ")}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting contexts: ${error.message}`, }, ], }; } }); // Tool: Switch context server.tool("switch-context", "Switch between contexts (e.g., NATIVE_APP, WEBVIEW)", { context: z.string().describe("Context to switch to"), }, async ({ context }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.switchContext(context); return { content: [ { type: "text", text: `Successfully switched to context: ${context}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error switching context: ${error.message}`, }, ], }; } }); // Tool: Get current context server.tool("get-current-context", "Get the current context", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const context = await validAppiumHelper.getCurrentContext(); return { content: [ { type: "text", text: `Current context: ${context}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting current context: ${error.message}`, }, ], }; } }); // Tool: Pull file from device server.tool("pull-file", "Pull a file from the device", { path: z.string().describe("Path to the file on the device"), }, async ({ path }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const fileContent = await validAppiumHelper.pullFile(path); return { content: [ { type: "text", text: `Successfully pulled file from ${path}. Content length: ${fileContent.length} bytes.`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error pulling file: ${error.message}`, }, ], }; } }); // Tool: Push file to device server.tool("push-file", "Push a file to the device", { path: z.string().describe("Path on the device to write the file"), data: z.string().describe("Base64-encoded file content"), }, async ({ path, data }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } await validAppiumHelper.pushFile(path, data); return { content: [ { type: "text", text: `Successfully pushed file to ${path}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error pushing file: ${error.message}`, }, ], }; } }); // Tool: Get battery info server.tool("get-battery-info", "Get the device battery information", {}, async () => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const batteryInfo = await validAppiumHelper.getBatteryInfo(); return { content: [ { type: "text", text: `Battery level: ${batteryInfo.level * 100}%, State: ${batteryInfo.state} (0: unknown, 1: charging, 2: discharging, 3: not charging, 4: full)`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting battery info: ${error.message}`, }, ], }; } }); // Tool: Check if element exists server.tool("element-exists", "Check if an element exists on the current page", { selector: z.string().describe("Element selector (e.g., xpath, id)"), strategy: z .string() .optional() .describe("Selector strategy: xpath, id, accessibility id, class name (default: xpath)"), }, async ({ selector, strategy }) => { try { const validAppiumHelper = await getValidAppiumHelper(); if (!validAppiumHelper) { return { content: [ { type: "text", text: "No active Appium session. Initialize one first with initialize-appium.", }, ], }; } const exists = await validAppiumHelper.elementExists(selector, strategy || "xpath"); return { content: [ { type: "text", text: exists ? `Element exists: ${selector}` : `Element does not exist: ${selector}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error checking if element exists: ${error.message}`, }, ], }; } }); // Tool: List iOS Simulators server.tool("list-ios-simulators", "Get list of available iOS simulators", {}, async () => { try { const validAppiumHelper = await getValidAppiumH