Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ MCP Appium provides a comprehensive set of tools organized into the following ca
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `create_session` | Create a new mobile automation session for Android, iOS, or `general` capabilities (see 'general' mode above). If a remote Appium server is referenced, `create_session` forwards the final capabilities to that server via the WebDriver `newSession` API - include device selection (e.g., `appium:udid`) in `capabilities` when targeting a remote server. |
| `delete_session` | Delete the current mobile session and clean up resources |
| `appium_mobile_shake` | Shake gesture (`mobile: shake`) on **iOS Simulator only** (XCUITest). Not supported on Android or physical iOS devices. |
| `appium_mobile_device_control` | Control device behavior: lock/unlock the screen, shake the device, or open the notifications panel (`action`: `lock` \| `unlock` \| `shake` \| `open_notifications`). `shake` is iOS only; `open_notifications` is Android only; `seconds` is optional for timed lock. |
| `appium_get_settings` | Read current Appium driver session settings (idle timeouts, animation-related flags, selector waits, etc.). Helps diagnose and tune flaky automation. |
| `appium_update_settings` | Merge key-value updates into driver session settings (driver-specific keys; use `appium_get_settings` to inspect). |

Expand Down
5 changes: 1 addition & 4 deletions src/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ This directory contains all MCP tools available in MCP Appium.

- `create-session.ts` - Create mobile automation sessions
- `delete-session.ts` - Clean up sessions
- `open-notifications.ts` - Open notifications panel (Android only)
- `shake.ts` - iOS Simulator shake via `mobile: shake` (`appium_mobile_shake`; not Android, not physical iOS)
- `lock.ts` - Lock device (`appium_mobile_lock`); optionally lock for N seconds (Android & iOS)
- `lock.ts` - Unlock device (`appium_mobile_unlock`)
- `device-control.ts` - Device controls in one tool (`appium_mobile_device_control`; `action=lock|unlock|shake|open_notifications`)
- `select-device.ts` - Discover devices and select one (auto-selects if only one found)
- `file-transfer.ts` - Push/pull files on device (`appium_mobile_file` with `action=push|pull`)
- `driver-settings.ts` - Read/update Appium driver session settings (`appium_get_settings`, `appium_update_settings`)
Expand Down
9 changes: 2 additions & 7 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import listSessions from './session/list-sessions.js';
import selectSession from './session/select-session.js';
import generateLocators from './test-generation/locators.js';
import selectDevice from './session/select-device.js';
import openNotifications from './session/open-notifications.js';
import shakeDevice from './session/shake.js';
import { lockDevice, unlockDevice } from './session/lock.js';
import mobileDeviceControl from './session/device-control.js';
import geolocation from './session/geolocation.js';
import deviceInfo from './session/device-info.js';
import fileTransfer from './session/file-transfer.js';
Expand Down Expand Up @@ -130,10 +128,7 @@ export default function registerTools(server: FastMCP): void {
listSessions(server);
selectSession(server);
deleteSession(server);
openNotifications(server);
shakeDevice(server);
lockDevice(server);
unlockDevice(server);
mobileDeviceControl(server);
geolocation(server);
deviceInfo(server);
fileTransfer(server);
Expand Down
141 changes: 141 additions & 0 deletions src/tools/session/device-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { ContentResult, FastMCP } from 'fastmcp';
import type { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
import { z } from 'zod';
import {
getDriver,
getPlatformName,
isAndroidUiautomator2DriverSession,
isRemoteDriverSession,
isXCUITestDriverSession,
PLATFORM,
type DriverInstance,
} from '../../session-store.js';
import { execute } from '../../command.js';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like a giant. Can we do something like this?

async function handleLock(driver, seconds?: number): Promise { ... }
async function handleUnlock(driver): Promise { ... }
async function handleShake(driver): Promise { ... }
async function handleOpenNotifications(driver): Promise { ... }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch as usual, done extracted handleLock, handleUnlock, handleShake, and handleOpenNotifications into separate functions, the execute block is now a thin switch/case dispatcher

const deviceControlSchema = z.object({
action: z
.enum(['lock', 'unlock', 'shake', 'open_notifications'])
.describe(
'Action to perform. ' +
'lock: lock the device (optional seconds for timed lock). ' +
'unlock: unlock the device. ' +
'shake: perform shake gesture (iOS only). ' +
'open_notifications: open notifications panel (Android only).'
),
sessionId: z
.string()
.optional()
.describe('Session ID to target. If omitted, uses the active session.'),
seconds: z
.number()
.int()
.min(1)
.optional()
.describe(
'Only for action=lock: lock duration in seconds before auto-unlock. Omit to remain locked until unlock.'
),
});

async function handleLock(
driver: DriverInstance,
seconds?: number
): Promise<ContentResult> {
const params: { seconds?: number } = {};
if (seconds !== undefined) {
params.seconds = seconds;
}
await execute(driver, 'mobile: lock', params);
const msg =
seconds !== undefined
? `Device locked for ${seconds} second(s).`
: 'Device locked.';
return { content: [{ type: 'text', text: msg }] };
}

async function handleUnlock(driver: DriverInstance): Promise<ContentResult> {
await execute(driver, 'mobile: unlock', {});
return { content: [{ type: 'text', text: 'Device unlocked.' }] };
}

async function handleShake(driver: DriverInstance): Promise<ContentResult> {
if (!isXCUITestDriverSession(driver)) {
throw new Error('Shake is supported only with XCUITest driver sessions.');
}
await (driver as any).mobileShake();
return { content: [{ type: 'text', text: 'Shake action performed.' }] };
}

async function handleOpenNotifications(
driver: DriverInstance
): Promise<ContentResult> {
const platform = getPlatformName(driver);
if (platform !== PLATFORM.android) {
throw new Error(
`Unsupported platform: ${platform}. Open notifications is supported on Android only.`
);
}

if (isAndroidUiautomator2DriverSession(driver)) {
await (driver as AndroidUiautomator2Driver).openNotifications();
} else if (isRemoteDriverSession(driver)) {
await execute(driver, 'mobile: openNotifications', {});
} else {
throw new Error('Unsupported Android driver for open notifications');
}

return {
content: [
{ type: 'text', text: 'Successfully opened notifications panel.' },
],
};
}

export default function mobileDeviceControl(server: FastMCP): void {
server.addTool({
name: 'appium_mobile_device_control',
description:
'Control device behavior: lock/unlock the screen, shake the device, or open the notifications panel. Use the action parameter to choose what to do.',
parameters: deviceControlSchema,
annotations: {
readOnlyHint: false,
openWorldHint: false,
},
execute: async (
args: z.infer<typeof deviceControlSchema>,
_context: Record<string, unknown> | undefined
): Promise<ContentResult> => {
try {
const driver = getDriver(args.sessionId);
if (!driver) {
throw new Error('No driver found');
}

if (args.action !== 'lock' && args.seconds !== undefined) {
throw new Error('seconds is only valid when action is lock');
}

switch (args.action) {
case 'lock':
return await handleLock(driver, args.seconds);
case 'unlock':
return await handleUnlock(driver);
case 'shake':
return await handleShake(driver);
case 'open_notifications':
return await handleOpenNotifications(driver);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
return {
content: [
{
type: 'text',
text: `Failed device control action ${args.action}. err: ${message}`,
},
],
isError: true,
};
}
},
});
}
112 changes: 0 additions & 112 deletions src/tools/session/lock.ts

This file was deleted.

80 changes: 0 additions & 80 deletions src/tools/session/open-notifications.ts

This file was deleted.

Loading
Loading