Skip to content

Commit 22587a8

Browse files
committed
feat(navigation): add swipe tool for horizontal and vertical swiping
- Support direction-based swipes (left, right, up, down) - Support custom coordinate-based swipes with configurable duration - Optimized horizontal swipe handling for Android RecyclerViews - Platform-specific implementations for Android and iOS - Updated documentation in README and tools README
1 parent 9f8ef3a commit 22587a8

File tree

4 files changed

+304
-0
lines changed

4 files changed

+304
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ MCP Appium provides a comprehensive set of tools organized into the following ca
195195
| `appium_screenshot` | Take a screenshot of the current screen and save as PNG |
196196
| `appium_scroll` | Scroll the screen vertically (up or down) |
197197
| `appium_scroll_to_element` | Scroll until a specific element becomes visible |
198+
| `appium_swipe` | Swipe the screen in a direction (left, right, up, down) or between custom coordinates |
198199
| `appium_get_page_source` | Get the page source (XML) from the current screen |
199200

200201
### App Management

src/tools/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This directory contains all MCP tools available in MCP Appium.
2121

2222
- `scroll.ts` - Scroll screens
2323
- `scroll-to-element.ts` - Scroll until element found
24+
- `swipe.ts` - Swipe screens in any direction or between custom coordinates
2425

2526
### Element Interactions (`interactions/`)
2627

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import installWDA from './ios/install-wda.js';
2626
import generateTest from './test-generation/generate-tests.js';
2727
import scroll from './navigations/scroll.js';
2828
import scrollToElement from './navigations/scroll-to-element.js';
29+
import swipe from './navigations/swipe.js';
2930
import findElement from './interactions/find.js';
3031
import clickElement from './interactions/click.js';
3132
import doubleTap from './interactions/double-tap.js';
@@ -120,6 +121,7 @@ export default function registerTools(server: FastMCP): void {
120121
// Navigation
121122
scroll(server);
122123
scrollToElement(server);
124+
swipe(server);
123125

124126
// Element Interactions
125127
findElement(server);

src/tools/navigations/swipe.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { z } from 'zod';
2+
import { getDriver, getPlatformName } from '../../session-store.js';
3+
import log from '../../logger.js';
4+
5+
function calculateSwipeCoordinates(
6+
direction: 'left' | 'right' | 'up' | 'down',
7+
width: number,
8+
height: number
9+
): { startX: number; startY: number; endX: number; endY: number } {
10+
const centerX = Math.floor(width / 2);
11+
const centerY = Math.floor(height / 2);
12+
13+
switch (direction) {
14+
case 'left':
15+
return {
16+
startX: Math.floor(width * 0.8),
17+
startY: centerY,
18+
endX: Math.floor(width * 0.2),
19+
endY: centerY,
20+
};
21+
case 'right':
22+
return {
23+
startX: Math.floor(width * 0.2),
24+
startY: centerY,
25+
endX: Math.floor(width * 0.8),
26+
endY: centerY,
27+
};
28+
case 'up':
29+
return {
30+
startX: centerX,
31+
startY: Math.floor(height * 0.8),
32+
endX: centerX,
33+
endY: Math.floor(height * 0.2),
34+
};
35+
case 'down':
36+
return {
37+
startX: centerX,
38+
startY: Math.floor(height * 0.2),
39+
endX: centerX,
40+
endY: Math.floor(height * 0.8),
41+
};
42+
default:
43+
throw new Error(`Invalid direction: ${direction}`);
44+
}
45+
}
46+
47+
async function performAndroidSwipe(
48+
driver: any,
49+
startX: number,
50+
startY: number,
51+
endX: number,
52+
endY: number,
53+
duration: number
54+
): Promise<void> {
55+
await driver.performActions([
56+
{
57+
type: 'pointer',
58+
id: 'finger1',
59+
parameters: { pointerType: 'touch' },
60+
actions: [
61+
{ type: 'pointerMove', duration: 0, x: startX, y: startY },
62+
{ type: 'pointerDown', button: 0 },
63+
{ type: 'pause', duration: 250 },
64+
{ type: 'pointerMove', duration: duration, x: endX, y: endY },
65+
{ type: 'pointerUp', button: 0 },
66+
],
67+
},
68+
]);
69+
}
70+
71+
async function performiOSSwipe(
72+
driver: any,
73+
startX: number,
74+
startY: number,
75+
endX: number,
76+
endY: number,
77+
duration: number
78+
): Promise<void> {
79+
try {
80+
await driver.execute('mobile: dragFromToForDuration', {
81+
fromX: startX,
82+
fromY: startY,
83+
toX: endX,
84+
toY: endY,
85+
duration: duration / 1000,
86+
});
87+
log.info('iOS swipe completed using mobile: dragFromToForDuration');
88+
} catch (dragError) {
89+
log.info('mobile: dragFromToForDuration failed, trying performActions');
90+
await driver.performActions([
91+
{
92+
type: 'pointer',
93+
id: 'finger1',
94+
parameters: { pointerType: 'touch' },
95+
actions: [
96+
{ type: 'pointerMove', duration: 0, x: startX, y: startY },
97+
{ type: 'pointerDown', button: 0 },
98+
{ type: 'pause', duration: 200 },
99+
{ type: 'pointerMove', duration: duration, x: endX, y: endY },
100+
{ type: 'pause', duration: 50 },
101+
{ type: 'pointerUp', button: 0 },
102+
],
103+
},
104+
]);
105+
log.info('iOS swipe completed using performActions');
106+
}
107+
}
108+
109+
export default function swipe(server: any): void {
110+
server.addTool({
111+
name: 'appium_swipe',
112+
description: `Swipe on the current screen in a specified direction or between custom coordinates.
113+
Supports four directions: left, right, up, down.
114+
Can also perform custom coordinate-based swipes for precise control.
115+
This is useful for navigating carousels, switching tabs, dismissing elements, or navigating between screens.`,
116+
parameters: z.object({
117+
direction: z
118+
.enum(['left', 'right', 'up', 'down'])
119+
.optional()
120+
.describe(
121+
'Direction to swipe. If provided, coordinates will be calculated automatically based on screen size. Either direction OR custom coordinates must be provided.'
122+
),
123+
startX: z
124+
.number()
125+
.int()
126+
.min(0)
127+
.optional()
128+
.describe(
129+
'Starting X coordinate for custom swipe. Required if direction is not provided.'
130+
),
131+
startY: z
132+
.number()
133+
.int()
134+
.min(0)
135+
.optional()
136+
.describe(
137+
'Starting Y coordinate for custom swipe. Required if direction is not provided.'
138+
),
139+
endX: z
140+
.number()
141+
.int()
142+
.min(0)
143+
.optional()
144+
.describe(
145+
'Ending X coordinate for custom swipe. Required if direction is not provided.'
146+
),
147+
endY: z
148+
.number()
149+
.int()
150+
.min(0)
151+
.optional()
152+
.describe(
153+
'Ending Y coordinate for custom swipe. Required if direction is not provided.'
154+
),
155+
duration: z
156+
.number()
157+
.int()
158+
.min(0)
159+
.max(5000)
160+
.default(600)
161+
.optional()
162+
.describe(
163+
'Duration of the swipe gesture in milliseconds. Default is 600ms. Higher values create slower swipes.'
164+
),
165+
}),
166+
annotations: {
167+
readOnlyHint: false,
168+
openWorldHint: false,
169+
},
170+
execute: async (args: any, context: any): Promise<any> => {
171+
const driver = getDriver();
172+
if (!driver) {
173+
throw new Error(
174+
'No active driver session. Please create a session first.'
175+
);
176+
}
177+
178+
try {
179+
const platform = getPlatformName(driver);
180+
let startX: number, startY: number, endX: number, endY: number;
181+
182+
if (args.direction) {
183+
const { width, height } = await driver.getWindowSize();
184+
log.info('Device screen size:', { width, height });
185+
const coords = calculateSwipeCoordinates(
186+
args.direction,
187+
width,
188+
height
189+
);
190+
startX = coords.startX;
191+
startY = coords.startY;
192+
endX = coords.endX;
193+
endY = coords.endY;
194+
} else if (
195+
args.startX !== undefined &&
196+
args.startY !== undefined &&
197+
args.endX !== undefined &&
198+
args.endY !== undefined
199+
) {
200+
startX = args.startX;
201+
startY = args.startY;
202+
endX = args.endX;
203+
endY = args.endY;
204+
} else {
205+
throw new Error(
206+
'Either direction or all custom coordinates (startX, startY, endX, endY) must be provided.'
207+
);
208+
}
209+
210+
const duration = args.duration || 600;
211+
212+
log.info('Swipe coordinates:', {
213+
startX,
214+
startY,
215+
endX,
216+
endY,
217+
duration,
218+
});
219+
220+
if (platform === 'Android') {
221+
if (startX !== endX && Math.abs(startY - endY) < 50) {
222+
const swipeDuration = Math.min(duration, 400);
223+
await driver.performActions([
224+
{
225+
type: 'pointer',
226+
id: 'finger1',
227+
parameters: { pointerType: 'touch' },
228+
actions: [
229+
{ type: 'pointerMove', duration: 0, x: startX, y: startY },
230+
{ type: 'pointerDown', button: 0 },
231+
{ type: 'pause', duration: 200 },
232+
{
233+
type: 'pointerMove',
234+
duration: swipeDuration,
235+
x: endX,
236+
y: endY,
237+
},
238+
{ type: 'pause', duration: 50 },
239+
{ type: 'pointerUp', button: 0 },
240+
],
241+
},
242+
]);
243+
log.info('Android horizontal swipe completed');
244+
} else {
245+
await performAndroidSwipe(
246+
driver,
247+
startX,
248+
startY,
249+
endX,
250+
endY,
251+
duration
252+
);
253+
}
254+
log.info('Android swipe action completed successfully.');
255+
} else if (platform === 'iOS') {
256+
if (args.direction) {
257+
try {
258+
await driver.execute('mobile: swipe', {
259+
direction: args.direction,
260+
});
261+
log.info(`iOS swipe completed using mobile: swipe (${args.direction})`);
262+
} catch (swipeError) {
263+
log.info('mobile: swipe failed, trying dragFromToForDuration');
264+
await performiOSSwipe(driver, startX, startY, endX, endY, duration);
265+
}
266+
} else {
267+
await performiOSSwipe(driver, startX, startY, endX, endY, duration);
268+
}
269+
log.info('iOS swipe action completed successfully.');
270+
} else {
271+
throw new Error(
272+
`Unsupported platform: ${platform}. Only Android and iOS are supported.`
273+
);
274+
}
275+
276+
const directionText = args.direction
277+
? ` ${args.direction}`
278+
: ` from (${startX}, ${startY}) to (${endX}, ${endY})`;
279+
280+
return {
281+
content: [
282+
{
283+
type: 'text',
284+
text: `Swiped${directionText} successfully.`,
285+
},
286+
],
287+
};
288+
} catch (err: any) {
289+
return {
290+
content: [
291+
{
292+
type: 'text',
293+
text: `Failed to perform swipe. Error: ${err.toString()}`,
294+
},
295+
],
296+
};
297+
}
298+
},
299+
});
300+
}

0 commit comments

Comments
 (0)