Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ MCP Appium provides a comprehensive set of tools organized into the following ca
| `appium_drag_and_drop` | Perform a drag and drop gesture from a source location to a target location (supports element-to-element, element-to-coordinates, coordinates-to-element, and coordinates-to-coordinates) |
| `appium_set_value` | Enter text into an input field |
| `appium_get_text` | Get text content from an element |
| `appium_handle_alert` | Accept or dismiss system/permission alerts, or click a dialog button by label |

### Screen & Navigation

Expand Down
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import dragAndDrop from './interactions/drag-and-drop.js';
import setValue from './interactions/set-value.js';
import getText from './interactions/get-text.js';
import getPageSource from './interactions/get-page-source.js';
import handleAlert from './interactions/handle-alert.js';
import { screenshot, elementScreenshot } from './interactions/screenshot.js';
import activateApp from './app-management/activate-app.js';
import installApp from './app-management/install-app.js';
Expand Down Expand Up @@ -136,6 +137,7 @@ export default function registerTools(server: FastMCP): void {
setValue(server);
getText(server);
getPageSource(server);
handleAlert(server);
screenshot(server);
elementScreenshot(server);

Expand Down
168 changes: 168 additions & 0 deletions src/tools/interactions/handle-alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
import { generateAllElementLocators } from '../../locators/generate-all-locators.js';
import { getDriver, getPlatformName, PLATFORM } from '../../session-store.js';

export const handleAlertSchema = z.object({
action: z
.enum(['accept', 'dismiss'])
.describe('Action to perform on the alert: accept or dismiss'),
buttonLabel: z
.string()
.optional()
.describe(
`Optional label of the button to click. Common permission dialog buttons:
Android: "While using the app", "Only this time", "Don't allow"
iOS: "Always" or "Allow Always", "Once" or "Allow Once", "Don't allow"
Standard: "OK", "Cancel", "Allow", "Deny"
If not provided, uses default button based on action.
Use appium_get_page_source or generate_locators to inspect the screen and discover exact labels.`
),
});

const ANDROID_LOCATOR_STRATEGY_ORDER = [
'id',
'accessibility id',
'xpath',
'-android uiautomator',
'class name',
];

async function handleAndroidAlert(
driver: any,
action: string,
buttonLabel?: string
): Promise<void> {
if (buttonLabel) {
const pageSource = await driver.getPageSource();
const elements = generateAllElementLocators(
pageSource,
true,
'uiautomator2',
{
fetchableOnly: true,
}
);
const normalizedLabel = buttonLabel.trim();
const match =
elements.find(
(el) =>
(el.text?.trim() === normalizedLabel ||
el.contentDesc?.trim() === normalizedLabel) &&
el.clickable
) ??
elements.find(
(el) =>
el.text?.trim() === normalizedLabel ||
el.contentDesc?.trim() === normalizedLabel
);

if (!match) {
throw new Error(
`No element found with text or content-desc "${buttonLabel}"`
);
}

let button: any = null;
for (const strategy of ANDROID_LOCATOR_STRATEGY_ORDER) {
const selector = match.locators[strategy];
if (!selector) {
continue;
}
try {
button = await driver.findElement(strategy, selector);
break;
} catch {
continue;
}
}
if (!button) {
throw new Error(
'Could not find element with any generated locator; it may have disappeared'
);
}
const buttonUUID = button.ELEMENT || button;
await driver.click(buttonUUID);
} else {
if (action === 'accept') {
await driver.execute('mobile: acceptAlert', {});
} else {
await driver.execute('mobile: dismissAlert', {});
}
}
}

async function handleiOSAlert(
driver: any,
action: string,
buttonLabel?: string
): Promise<void> {
const params: any = { action };
if (buttonLabel) {
params.buttonLabel = buttonLabel;
}
await driver.execute('mobile: alert', params);
}

export default function handleAlert(server: FastMCP): void {
server.addTool({
name: 'appium_handle_alert',
description: `Handle system alerts or dialogs that do not belong to the app.
Use this to dismiss or accept alerts programmatically instead of using autoDismissAlerts capability.
Supports permission dialogs with buttons like:
- Android: "While using the app", "Only this time", "Don't allow"
- iOS: "Always", "Allow Once", "Don't allow"
For iOS: Uses mobile: alert execute command.
For Android: Uses mobile: acceptAlert/dismissAlert or searches the current page source for an element whose text or content-desc matches the label, then uses generated locators to find and click it (no hardcoded resource IDs or XPaths).
If no alert is present, the error is caught and returned gracefully.
To discover button labels and screen structure first, use appium_get_page_source (XML hierarchy) or generate_locators (interactable elements with text/content-desc).`,
parameters: handleAlertSchema,
annotations: {
readOnlyHint: false,
openWorldHint: false,
},
execute: async (args: any, context: any): Promise<any> => {
const driver = getDriver();
if (!driver) {
throw new Error('No driver found');
}

try {
const platform = getPlatformName(driver);

if (platform === PLATFORM.android) {
await handleAndroidAlert(driver, args.action, args.buttonLabel);
} else if (platform === PLATFORM.ios) {
await handleiOSAlert(driver, args.action, args.buttonLabel);
} else {
throw new Error(
`Unsupported platform: ${platform}. Only Android and iOS are supported.`
);
}

return {
content: [
{
type: 'text',
text: `Successfully ${args.action}ed alert${
args.buttonLabel ? ` with button "${args.buttonLabel}"` : ''
}`,
},
],
};
} catch (err: any) {
const contextStr = args.buttonLabel
? `action=${args.action}, buttonLabel="${args.buttonLabel}"`
: `action=${args.action}`;
return {
content: [
{
type: 'text',
text: `Failed to handle alert (${contextStr}). err: ${err.toString()}`,
},
],
};
}
},
});
}