@mobilenext/mobile-mcp 0.0.18 β 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -32
- package/lib/android.js +85 -6
- package/lib/ios.js +17 -16
- package/lib/iphone-simulator.js +7 -1
- package/lib/server.js +104 -13
- package/lib/webdriver-agent.js +124 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Mobile Next - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and physical devices
|
|
2
2
|
|
|
3
3
|
This is a [Model Context Protocol (MCP) server](https://github.com/modelcontextprotocol) that enables scalable mobile automation, development through a platform-agnostic interface, eliminating the need for distinct iOS or Android knowledge. You can run it on emulators, simulators, and physical devices (iOS and Android).
|
|
4
|
-
This server allows Agents and LLMs to interact with native iOS/Android applications and devices through structured accessibility snapshots or coordinate-based taps based on screenshots.
|
|
4
|
+
This server allows Agents and LLMs to interact with native iOS/Android applications and devices through structured accessibility snapshots or coordinate-based taps based on screenshots.
|
|
5
5
|
|
|
6
6
|
<h4 align="center">
|
|
7
7
|
<a href="https://github.com/mobile-next/mobile-mcp">
|
|
8
8
|
<img src="https://img.shields.io/github/stars/mobile-next/mobile-mcp" alt="Mobile Next Stars" />
|
|
9
|
-
</a>
|
|
9
|
+
</a>
|
|
10
10
|
<a href="https://github.com/mobile-next/mobile-mcp">
|
|
11
11
|
<img src="https://img.shields.io/github/contributors/mobile-next/mobile-mcp?color=green" alt="Mobile Next Downloads" />
|
|
12
12
|
</a>
|
|
@@ -18,14 +18,14 @@ This server allows Agents and LLMs to interact with native iOS/Android applicati
|
|
|
18
18
|
</a>
|
|
19
19
|
<a href="https://github.com/mobile-next/mobile-mcp/blob/main/LICENSE">
|
|
20
20
|
<img src="https://img.shields.io/badge/license-Apache 2.0-blue.svg" alt="Mobile MCP is released under the Apache-2.0 License">
|
|
21
|
-
</a>
|
|
22
|
-
|
|
21
|
+
</a>
|
|
22
|
+
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
<h4 align="center">
|
|
26
26
|
<a href="http://mobilenexthq.com/join-slack">
|
|
27
27
|
<img src="https://img.shields.io/badge/join-Slack-blueviolet?logo=slack&style=flat" alt="Slack community channel" />
|
|
28
|
-
</a>
|
|
28
|
+
</a>
|
|
29
29
|
</p>
|
|
30
30
|
|
|
31
31
|
https://github.com/user-attachments/assets/c4e89c4f-cc71-4424-8184-bdbc8c638fa1
|
|
@@ -38,7 +38,7 @@ https://github.com/user-attachments/assets/c4e89c4f-cc71-4424-8184-bdbc8c638fa1
|
|
|
38
38
|
|
|
39
39
|
### π Mobile MCP Roadmap: Building the Future of Mobile
|
|
40
40
|
|
|
41
|
-
Join us on our journey as we continuously enhance Mobile MCP!
|
|
41
|
+
Join us on our journey as we continuously enhance Mobile MCP!
|
|
42
42
|
Check out our detailed roadmap to see upcoming features, improvements, and milestones. Your feedback is invaluable in shaping the future of mobile automation.
|
|
43
43
|
|
|
44
44
|
π [Explore the Roadmap](https://github.com/orgs/mobile-next/projects/3)
|
|
@@ -48,7 +48,7 @@ Check out our detailed roadmap to see upcoming features, improvements, and miles
|
|
|
48
48
|
|
|
49
49
|
How we help to scale mobile automation:
|
|
50
50
|
|
|
51
|
-
- π² Native app automation (iOS and Android) for testing or data-entry scenarios.
|
|
51
|
+
- π² Native app automation (iOS and Android) for testing or data-entry scenarios.
|
|
52
52
|
- π Scripted flows and form interactions without manually controlling simulators/emulators or physical devices (iPhone, Samsung, Google Pixel etc)
|
|
53
53
|
- π§ Automating multi-step user journeys driven by an LLM
|
|
54
54
|
- π General-purpose mobile application interaction for agent-based frameworks
|
|
@@ -56,11 +56,11 @@ How we help to scale mobile automation:
|
|
|
56
56
|
|
|
57
57
|
## Main Features
|
|
58
58
|
|
|
59
|
-
- π **Fast and lightweight**: Uses native accessibility trees for most interactions, or screenshot based coordinates where a11y labels are not available.
|
|
59
|
+
- π **Fast and lightweight**: Uses native accessibility trees for most interactions, or screenshot based coordinates where a11y labels are not available.
|
|
60
60
|
- π€ **LLM-friendly**: No computer vision model required in Accessibility (Snapshot).
|
|
61
61
|
- π§Ώ **Visual Sense**: Evaluates and analyses whatβs actually rendered on screen to decide the next action. If accessibility data or view-hierarchy coordinates are unavailable, it falls back to screenshot-based analysis.
|
|
62
62
|
- π **Deterministic tool application**: Reduces ambiguity found in purely screenshot-based approaches by relying on structured data whenever possible.
|
|
63
|
-
- πΊ **Extract structured data**: Enables you to extract structred data from anything visible on screen.
|
|
63
|
+
- πΊ **Extract structured data**: Enables you to extract structred data from anything visible on screen.
|
|
64
64
|
|
|
65
65
|
## ποΈ Mobile MCP Architecture
|
|
66
66
|
|
|
@@ -71,7 +71,7 @@ How we help to scale mobile automation:
|
|
|
71
71
|
</p>
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
## π Wiki page
|
|
74
|
+
## π Wiki page
|
|
75
75
|
|
|
76
76
|
More details in our [wiki page](https://github.com/mobile-next/mobile-mcp/wiki) for setup, configuration and debugging related questions.
|
|
77
77
|
|
|
@@ -91,8 +91,8 @@ Setup our MCP with Cline, Cursor, Claude, VS Code, Github Copilot:
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
```
|
|
94
|
-
[Cline:](https://docs.cline.bot/mcp/configuring-mcp-servers) To setup Cline, just add the json above to your MCP settings file.
|
|
95
|
-
[More in our wiki](https://github.com/mobile-next/mobile-mcp/wiki/Cline)
|
|
94
|
+
[Cline:](https://docs.cline.bot/mcp/configuring-mcp-servers) To setup Cline, just add the json above to your MCP settings file.
|
|
95
|
+
[More in our wiki](https://github.com/mobile-next/mobile-mcp/wiki/Cline)
|
|
96
96
|
|
|
97
97
|
[Claude Code:](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
|
|
98
98
|
|
|
@@ -105,7 +105,7 @@ claude mcp add mobile -- npx -y @mobilenext/mobile-mcp@latest
|
|
|
105
105
|
|
|
106
106
|
### π οΈ How to Use π
|
|
107
107
|
|
|
108
|
-
After adding the MCP server to your IDE/Client, you can instruct your AI assistant to use the available tools.
|
|
108
|
+
After adding the MCP server to your IDE/Client, you can instruct your AI assistant to use the available tools.
|
|
109
109
|
For example, in Cursor's agent mode, you could use the prompts below to quickly validate, test and iterate on UI intereactions, read information from screen, go through complex workflows.
|
|
110
110
|
Be descriptive, straight to the point.
|
|
111
111
|
|
|
@@ -117,47 +117,55 @@ You can specifiy detailed workflows in a single prompt, verify business logic, s
|
|
|
117
117
|
|
|
118
118
|
**Search for a video, comment, like and share it.**
|
|
119
119
|
```
|
|
120
|
-
Find the video called " Beginner Recipe for Tonkotsu Ramen" by Way of
|
|
120
|
+
Find the video called " Beginner Recipe for Tonkotsu Ramen" by Way of
|
|
121
|
+
Ramen, click on like video, after liking write a comment " this was
|
|
122
|
+
delicious, will make it next Friday", share the video with the first
|
|
123
|
+
contact in your whatsapp list.
|
|
121
124
|
```
|
|
122
125
|
|
|
123
|
-
**Download a successful step counter app, register, setup workout and 5
|
|
126
|
+
**Download a successful step counter app, register, setup workout and 5-star the app**
|
|
124
127
|
```
|
|
125
|
-
Find and Download a free "Pomodoro" app that has more
|
|
126
|
-
Launch the app, register with my email, after registration find how to
|
|
127
|
-
When the pomodoro timer started, go back to the
|
|
128
|
-
and leave a comment how useful the
|
|
128
|
+
Find and Download a free "Pomodoro" app that has more than 1k stars.
|
|
129
|
+
Launch the app, register with my email, after registration find how to
|
|
130
|
+
start a pomodoro timer. When the pomodoro timer started, go back to the
|
|
131
|
+
app store and rate the app 5 stars, and leave a comment how useful the
|
|
132
|
+
app is.
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
**Search in Substack, read, highlight, comment and save an article**
|
|
132
136
|
```
|
|
133
|
-
Open Substack website, search for "Latest trends in AI automation 2025",
|
|
134
|
-
highlight the section titled "Emerging AI trends",
|
|
135
|
-
comment a random
|
|
137
|
+
Open Substack website, search for "Latest trends in AI automation 2025",
|
|
138
|
+
open the first article, highlight the section titled "Emerging AI trends",
|
|
139
|
+
and save article to reading list for later review, comment a random
|
|
140
|
+
paragraph summary.
|
|
136
141
|
```
|
|
137
142
|
|
|
138
143
|
**Reserve a workout class, set timer**
|
|
139
144
|
```
|
|
140
|
-
Open ClassPass, search for yoga classes tomorrow morning within 2 miles,
|
|
145
|
+
Open ClassPass, search for yoga classes tomorrow morning within 2 miles,
|
|
141
146
|
book the highest-rated class at 7 AM, confirm reservation,
|
|
142
|
-
|
|
147
|
+
setup a timer for the booked slot in the phone
|
|
143
148
|
```
|
|
144
149
|
|
|
145
150
|
**Find a local event, setup calendar event**
|
|
146
151
|
```
|
|
147
|
-
Open Eventbrite, search for AI startup meetup events happening this
|
|
148
|
-
select the most popular one, register and RSVP
|
|
152
|
+
Open Eventbrite, search for AI startup meetup events happening this
|
|
153
|
+
weekend in "Austin, TX", select the most popular one, register and RSVP
|
|
154
|
+
yes to the event, setup a calendar event as a reminder.
|
|
149
155
|
```
|
|
150
156
|
|
|
151
157
|
**Check weather forecast and send a Whatsapp/Telegram/Slack message**
|
|
152
158
|
```
|
|
153
|
-
Open Weather app, check tomorrow's weather forecast for "Berlin", and
|
|
154
|
-
via Whatsapp/Telegram/Slack to contact "Lauren Trown",
|
|
159
|
+
Open Weather app, check tomorrow's weather forecast for "Berlin", and
|
|
160
|
+
send the summary via Whatsapp/Telegram/Slack to contact "Lauren Trown",
|
|
161
|
+
thumbs up their response.
|
|
155
162
|
```
|
|
156
163
|
|
|
157
164
|
- **Schedule a meeting in Zoom and share invite via email**
|
|
158
165
|
```
|
|
159
|
-
Open Zoom app, schedule a meeting titled "AI Hackathon" for tomorrow at
|
|
160
|
-
copy the invitation link, and send it via
|
|
166
|
+
Open Zoom app, schedule a meeting titled "AI Hackathon" for tomorrow at
|
|
167
|
+
10AM with a duration of 1 hour, copy the invitation link, and send it via
|
|
168
|
+
Gmail to contacts "team@example.com".
|
|
161
169
|
```
|
|
162
170
|
[More prompt examples can be found here.](https://github.com/mobile-next/mobile-mcp/wiki/Prompt-Example-repo-list)
|
|
163
171
|
|
|
@@ -167,7 +175,7 @@ What you will need to connect MCP with your agent and mobile devices:
|
|
|
167
175
|
|
|
168
176
|
- [Xcode command line tools](https://developer.apple.com/xcode/resources/)
|
|
169
177
|
- [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools)
|
|
170
|
-
- [node.js](https://nodejs.org/en/download/)
|
|
178
|
+
- [node.js](https://nodejs.org/en/download/) v22+
|
|
171
179
|
- [MCP](https://modelcontextprotocol.io/introduction) supported foundational models or agents, like [Claude MCP](https://modelcontextprotocol.io/quickstart/server), [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/mcp/), [Copilot Studio](https://www.microsoft.com/en-us/microsoft-copilot/blog/copilot-studio/introducing-model-context-protocol-mcp-in-copilot-studio-simplified-integration-with-ai-apps-and-agents/)
|
|
172
180
|
|
|
173
181
|
### Simulators, Emulators, and Physical Devices
|
|
@@ -193,7 +201,7 @@ On iOS, you'll need Xcode and to run the Simulator before using Mobile MCP with
|
|
|
193
201
|
|
|
194
202
|
# Thanks to all contributors β€οΈ
|
|
195
203
|
|
|
196
|
-
### We appreciate everyone who has helped improve this project.
|
|
204
|
+
### We appreciate everyone who has helped improve this project.
|
|
197
205
|
|
|
198
206
|
<a href = "https://github.com/mobile-next/mobile-mcp/graphs/contributors">
|
|
199
207
|
<img src = "https://contrib.rocks/image?repo=mobile-next/mobile-mcp"/>
|
package/lib/android.js
CHANGED
|
@@ -94,6 +94,7 @@ class AndroidRobot {
|
|
|
94
94
|
return { width, height, scale };
|
|
95
95
|
}
|
|
96
96
|
async listApps() {
|
|
97
|
+
// only apps that have a launcher activity are returned
|
|
97
98
|
return this.adb("shell", "cmd", "package", "query-activities", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER")
|
|
98
99
|
.toString()
|
|
99
100
|
.split("\n")
|
|
@@ -106,6 +107,13 @@ class AndroidRobot {
|
|
|
106
107
|
appName: packageName,
|
|
107
108
|
}));
|
|
108
109
|
}
|
|
110
|
+
async listPackages() {
|
|
111
|
+
return this.adb("shell", "pm", "list", "packages")
|
|
112
|
+
.toString()
|
|
113
|
+
.split("\n")
|
|
114
|
+
.filter(line => line.startsWith("package:"))
|
|
115
|
+
.map(line => line.substring("package:".length));
|
|
116
|
+
}
|
|
109
117
|
async launchApp(packageName) {
|
|
110
118
|
this.adb("shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1");
|
|
111
119
|
}
|
|
@@ -120,7 +128,6 @@ class AndroidRobot {
|
|
|
120
128
|
async swipe(direction) {
|
|
121
129
|
const screenSize = await this.getScreenSize();
|
|
122
130
|
const centerX = screenSize.width >> 1;
|
|
123
|
-
// const centerY = screenSize[1] >> 1;
|
|
124
131
|
let x0, y0, x1, y1;
|
|
125
132
|
switch (direction) {
|
|
126
133
|
case "up":
|
|
@@ -133,6 +140,50 @@ class AndroidRobot {
|
|
|
133
140
|
y0 = Math.floor(screenSize.height * 0.20);
|
|
134
141
|
y1 = Math.floor(screenSize.height * 0.80);
|
|
135
142
|
break;
|
|
143
|
+
case "left":
|
|
144
|
+
x0 = Math.floor(screenSize.width * 0.80);
|
|
145
|
+
x1 = Math.floor(screenSize.width * 0.20);
|
|
146
|
+
y0 = y1 = Math.floor(screenSize.height * 0.50);
|
|
147
|
+
break;
|
|
148
|
+
case "right":
|
|
149
|
+
x0 = Math.floor(screenSize.width * 0.20);
|
|
150
|
+
x1 = Math.floor(screenSize.width * 0.80);
|
|
151
|
+
y0 = y1 = Math.floor(screenSize.height * 0.50);
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
|
|
155
|
+
}
|
|
156
|
+
this.adb("shell", "input", "swipe", `${x0}`, `${y0}`, `${x1}`, `${y1}`, "1000");
|
|
157
|
+
}
|
|
158
|
+
async swipeFromCoordinate(x, y, direction, distance) {
|
|
159
|
+
const screenSize = await this.getScreenSize();
|
|
160
|
+
let x0, y0, x1, y1;
|
|
161
|
+
// Use provided distance or default to 30% of screen dimension
|
|
162
|
+
const defaultDistanceY = Math.floor(screenSize.height * 0.3);
|
|
163
|
+
const defaultDistanceX = Math.floor(screenSize.width * 0.3);
|
|
164
|
+
const swipeDistanceY = distance || defaultDistanceY;
|
|
165
|
+
const swipeDistanceX = distance || defaultDistanceX;
|
|
166
|
+
switch (direction) {
|
|
167
|
+
case "up":
|
|
168
|
+
x0 = x1 = x;
|
|
169
|
+
y0 = y;
|
|
170
|
+
y1 = Math.max(0, y - swipeDistanceY);
|
|
171
|
+
break;
|
|
172
|
+
case "down":
|
|
173
|
+
x0 = x1 = x;
|
|
174
|
+
y0 = y;
|
|
175
|
+
y1 = Math.min(screenSize.height, y + swipeDistanceY);
|
|
176
|
+
break;
|
|
177
|
+
case "left":
|
|
178
|
+
x0 = x;
|
|
179
|
+
x1 = Math.max(0, x - swipeDistanceX);
|
|
180
|
+
y0 = y1 = y;
|
|
181
|
+
break;
|
|
182
|
+
case "right":
|
|
183
|
+
x0 = x;
|
|
184
|
+
x1 = Math.min(screenSize.width, x + swipeDistanceX);
|
|
185
|
+
y0 = y1 = y;
|
|
186
|
+
break;
|
|
136
187
|
default:
|
|
137
188
|
throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
|
|
138
189
|
}
|
|
@@ -186,10 +237,37 @@ class AndroidRobot {
|
|
|
186
237
|
async openUrl(url) {
|
|
187
238
|
this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
|
|
188
239
|
}
|
|
240
|
+
isAscii(text) {
|
|
241
|
+
return /^[\x00-\x7F]*$/.test(text);
|
|
242
|
+
}
|
|
243
|
+
async isDeviceKitInstalled() {
|
|
244
|
+
const packages = await this.listPackages();
|
|
245
|
+
return packages.includes("com.mobilenext.devicekit");
|
|
246
|
+
}
|
|
189
247
|
async sendKeys(text) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
if (text === "") {
|
|
249
|
+
// bailing early, so we don't run adb shell with empty string.
|
|
250
|
+
// this happens when you prompt with a simple "submit".
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.isAscii(text)) {
|
|
254
|
+
// adb shell input only supports ascii characters. and
|
|
255
|
+
// some of the keys have to be escaped.
|
|
256
|
+
const _text = text.replace(/ /g, "\\ ");
|
|
257
|
+
this.adb("shell", "input", "text", _text);
|
|
258
|
+
}
|
|
259
|
+
else if (await this.isDeviceKitInstalled()) {
|
|
260
|
+
// try sending over clipboard
|
|
261
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
262
|
+
// send clipboard over and immediately paste it
|
|
263
|
+
this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.set", "-e", "encoding", "base64", "-e", "text", base64, "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
|
|
264
|
+
this.adb("shell", "input", "keyevent", "KEYCODE_PASTE");
|
|
265
|
+
// clear clipboard when we're done
|
|
266
|
+
this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.clear", "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
throw new robot_1.ActionableError("Non-ASCII text is not supported on Android, please install mobilenext devicekit, see https://github.com/mobile-next/devicekit-android");
|
|
270
|
+
}
|
|
193
271
|
}
|
|
194
272
|
async pressButton(button) {
|
|
195
273
|
if (!BUTTON_MAP[button]) {
|
|
@@ -202,8 +280,9 @@ class AndroidRobot {
|
|
|
202
280
|
}
|
|
203
281
|
async setOrientation(orientation) {
|
|
204
282
|
const orientationValue = orientation === "portrait" ? 0 : 1;
|
|
205
|
-
|
|
283
|
+
// disable auto-rotation prior to setting the orientation
|
|
206
284
|
this.adb("shell", "settings", "put", "system", "accelerometer_rotation", "0");
|
|
285
|
+
this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${orientationValue}`);
|
|
207
286
|
}
|
|
208
287
|
async getOrientation() {
|
|
209
288
|
const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
|
|
@@ -219,7 +298,7 @@ class AndroidRobot {
|
|
|
219
298
|
// console.error("Failed to get UIAutomator XML. Here's a screenshot: " + screenshot.toString("base64"));
|
|
220
299
|
continue;
|
|
221
300
|
}
|
|
222
|
-
return dump;
|
|
301
|
+
return dump.substring(dump.indexOf("<?xml"));
|
|
223
302
|
}
|
|
224
303
|
throw new robot_1.ActionableError("Failed to get UIAutomator XML");
|
|
225
304
|
}
|
package/lib/ios.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.IosManager = exports.IosRobot = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const os_1 = require("os");
|
|
9
|
-
const crypto_1 = require("crypto");
|
|
10
|
-
const fs_1 = require("fs");
|
|
11
4
|
const child_process_1 = require("child_process");
|
|
12
5
|
const net_1 = require("net");
|
|
13
6
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
@@ -83,6 +76,10 @@ class IosRobot {
|
|
|
83
76
|
const wda = await this.wda();
|
|
84
77
|
await wda.swipe(direction);
|
|
85
78
|
}
|
|
79
|
+
async swipeFromCoordinate(x, y, direction, distance) {
|
|
80
|
+
const wda = await this.wda();
|
|
81
|
+
await wda.swipeFromCoordinate(x, y, direction, distance);
|
|
82
|
+
}
|
|
86
83
|
async listApps() {
|
|
87
84
|
await this.assertTunnelRunning();
|
|
88
85
|
const output = await this.ios("apps", "--all", "--list");
|
|
@@ -125,12 +122,16 @@ class IosRobot {
|
|
|
125
122
|
return await wda.getElementsOnScreen();
|
|
126
123
|
}
|
|
127
124
|
async getScreenshot() {
|
|
125
|
+
const wda = await this.wda();
|
|
126
|
+
return await wda.getScreenshot();
|
|
127
|
+
/* alternative:
|
|
128
128
|
await this.assertTunnelRunning();
|
|
129
|
-
const tmpFilename =
|
|
129
|
+
const tmpFilename = path.join(tmpdir(), `screenshot-${randomBytes(8).toString("hex")}.png`);
|
|
130
130
|
await this.ios("screenshot", "--output", tmpFilename);
|
|
131
|
-
const buffer =
|
|
132
|
-
|
|
131
|
+
const buffer = readFileSync(tmpFilename);
|
|
132
|
+
unlinkSync(tmpFilename);
|
|
133
133
|
return buffer;
|
|
134
|
+
*/
|
|
134
135
|
}
|
|
135
136
|
async setOrientation(orientation) {
|
|
136
137
|
const wda = await this.wda();
|
|
@@ -153,23 +154,23 @@ class IosManager {
|
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
getDeviceName(deviceId) {
|
|
157
158
|
const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
|
|
158
159
|
const json = JSON.parse(output);
|
|
159
160
|
return json.DeviceName;
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
-
if (!
|
|
162
|
+
listDevices() {
|
|
163
|
+
if (!this.isGoIosInstalled()) {
|
|
163
164
|
console.error("go-ios is not installed, no physical iOS devices can be detected");
|
|
164
165
|
return [];
|
|
165
166
|
}
|
|
166
167
|
const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
|
|
167
168
|
const json = JSON.parse(output);
|
|
168
|
-
const devices = json.deviceList.map(
|
|
169
|
+
const devices = json.deviceList.map(device => ({
|
|
169
170
|
deviceId: device,
|
|
170
|
-
deviceName:
|
|
171
|
+
deviceName: this.getDeviceName(device),
|
|
171
172
|
}));
|
|
172
|
-
return
|
|
173
|
+
return devices;
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
exports.IosManager = IosManager;
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -26,7 +26,9 @@ class Simctl {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
async getScreenshot() {
|
|
29
|
-
|
|
29
|
+
const wda = await this.wda();
|
|
30
|
+
return await wda.getScreenshot();
|
|
31
|
+
// alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-");
|
|
30
32
|
}
|
|
31
33
|
async openUrl(url) {
|
|
32
34
|
const wda = await this.wda();
|
|
@@ -62,6 +64,10 @@ class Simctl {
|
|
|
62
64
|
const wda = await this.wda();
|
|
63
65
|
return wda.swipe(direction);
|
|
64
66
|
}
|
|
67
|
+
async swipeFromCoordinate(x, y, direction, distance) {
|
|
68
|
+
const wda = await this.wda();
|
|
69
|
+
return wda.swipeFromCoordinate(x, y, direction, distance);
|
|
70
|
+
}
|
|
65
71
|
async tap(x, y) {
|
|
66
72
|
const wda = await this.wda();
|
|
67
73
|
return wda.tap(x, y);
|
package/lib/server.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.createMcpServer = exports.getAgentVersion = void 0;
|
|
4
7
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
8
|
const zod_1 = require("zod");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
6
11
|
const logger_1 = require("./logger");
|
|
7
12
|
const android_1 = require("./android");
|
|
8
13
|
const robot_1 = require("./robot");
|
|
@@ -41,6 +46,8 @@ const createMcpServer = () => {
|
|
|
41
46
|
tools: {},
|
|
42
47
|
},
|
|
43
48
|
});
|
|
49
|
+
// an empty object to satisfy windsurf
|
|
50
|
+
const noParams = zod_1.z.object({});
|
|
44
51
|
const tool = (name, description, paramsSchema, cb) => {
|
|
45
52
|
const wrappedCb = async (args) => {
|
|
46
53
|
try {
|
|
@@ -69,6 +76,27 @@ const createMcpServer = () => {
|
|
|
69
76
|
};
|
|
70
77
|
server.tool(name, description, paramsSchema, args => wrappedCb(args));
|
|
71
78
|
};
|
|
79
|
+
const posthog = (event, properties) => {
|
|
80
|
+
const url = "https://us.i.posthog.com/i/v0/e/";
|
|
81
|
+
const api_key = "phc_KHRTZmkDsU7A8EbydEK8s4lJpPoTDyyBhSlwer694cS";
|
|
82
|
+
const distinct_id = node_crypto_1.default.createHash("sha256").update(process.execPath).digest("hex");
|
|
83
|
+
fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json"
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
api_key,
|
|
90
|
+
event,
|
|
91
|
+
properties,
|
|
92
|
+
distinct_id,
|
|
93
|
+
})
|
|
94
|
+
}).then().catch();
|
|
95
|
+
};
|
|
96
|
+
posthog("launch", {
|
|
97
|
+
Product: "mobile-mcp",
|
|
98
|
+
Version: (0, exports.getAgentVersion)(),
|
|
99
|
+
});
|
|
72
100
|
let robot;
|
|
73
101
|
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
74
102
|
const requireRobot = () => {
|
|
@@ -76,11 +104,44 @@ const createMcpServer = () => {
|
|
|
76
104
|
throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
|
|
77
105
|
}
|
|
78
106
|
};
|
|
79
|
-
tool("
|
|
107
|
+
tool("mobile_use_default_device", "Use the default device. This is a shortcut for mobile_use_device with deviceType=simulator and device=simulator_name", {
|
|
108
|
+
noParams
|
|
109
|
+
}, async () => {
|
|
110
|
+
const iosManager = new ios_1.IosManager();
|
|
111
|
+
const androidManager = new android_1.AndroidDeviceManager();
|
|
112
|
+
const simulators = simulatorManager.listBootedSimulators();
|
|
113
|
+
const androidDevices = androidManager.getConnectedDevices();
|
|
114
|
+
const iosDevices = iosManager.listDevices();
|
|
115
|
+
const sum = simulators.length + androidDevices.length + iosDevices.length;
|
|
116
|
+
if (sum === 0) {
|
|
117
|
+
throw new robot_1.ActionableError("No devices found. Please connect a device and try again.");
|
|
118
|
+
}
|
|
119
|
+
else if (sum >= 2) {
|
|
120
|
+
throw new robot_1.ActionableError("Multiple devices found. Please use the mobile_list_available_devices tool to list available devices and select one.");
|
|
121
|
+
}
|
|
122
|
+
// only one device connected, let's find it now
|
|
123
|
+
if (simulators.length === 1) {
|
|
124
|
+
robot = simulatorManager.getSimulator(simulators[0].name);
|
|
125
|
+
return `Selected default device: ${simulators[0].name}`;
|
|
126
|
+
}
|
|
127
|
+
else if (androidDevices.length === 1) {
|
|
128
|
+
robot = new android_1.AndroidRobot(androidDevices[0].deviceId);
|
|
129
|
+
return `Selected default device: ${androidDevices[0].deviceId}`;
|
|
130
|
+
}
|
|
131
|
+
else if (iosDevices.length === 1) {
|
|
132
|
+
robot = new ios_1.IosRobot(iosDevices[0].deviceId);
|
|
133
|
+
return `Selected default device: ${iosDevices[0].deviceId}`;
|
|
134
|
+
}
|
|
135
|
+
// how did this happen?
|
|
136
|
+
throw new robot_1.ActionableError("No device selected. Please use the mobile_list_available_devices tool to list available devices and select one.");
|
|
137
|
+
});
|
|
138
|
+
tool("mobile_list_available_devices", "List all available devices. This includes both physical devices and simulators. If there is more than one device returned, you need to let the user select one of them.", {
|
|
139
|
+
noParams
|
|
140
|
+
}, async ({}) => {
|
|
80
141
|
const iosManager = new ios_1.IosManager();
|
|
81
142
|
const androidManager = new android_1.AndroidDeviceManager();
|
|
82
|
-
const
|
|
83
|
-
const simulatorNames =
|
|
143
|
+
const simulators = simulatorManager.listBootedSimulators();
|
|
144
|
+
const simulatorNames = simulators.map(d => d.name);
|
|
84
145
|
const androidDevices = androidManager.getConnectedDevices();
|
|
85
146
|
const iosDevices = await iosManager.listDevices();
|
|
86
147
|
const iosDeviceNames = iosDevices.map(d => d.deviceId);
|
|
@@ -118,7 +179,9 @@ const createMcpServer = () => {
|
|
|
118
179
|
}
|
|
119
180
|
return `Selected device: ${device}`;
|
|
120
181
|
});
|
|
121
|
-
tool("mobile_list_apps", "List all the installed apps on the device", {
|
|
182
|
+
tool("mobile_list_apps", "List all the installed apps on the device", {
|
|
183
|
+
noParams
|
|
184
|
+
}, async ({}) => {
|
|
122
185
|
requireRobot();
|
|
123
186
|
const result = await robot.listApps();
|
|
124
187
|
return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
|
|
@@ -137,12 +200,14 @@ const createMcpServer = () => {
|
|
|
137
200
|
await robot.terminateApp(packageName);
|
|
138
201
|
return `Terminated app ${packageName}`;
|
|
139
202
|
});
|
|
140
|
-
tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
|
|
203
|
+
tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
|
|
204
|
+
noParams
|
|
205
|
+
}, async ({}) => {
|
|
141
206
|
requireRobot();
|
|
142
207
|
const screenSize = await robot.getScreenSize();
|
|
143
208
|
return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
|
|
144
209
|
});
|
|
145
|
-
tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates", {
|
|
210
|
+
tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates. If clicking on an element, use the list_elements_on_screen tool to find the coordinates.", {
|
|
146
211
|
x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
|
|
147
212
|
y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
|
|
148
213
|
}, async ({ x, y }) => {
|
|
@@ -150,7 +215,9 @@ const createMcpServer = () => {
|
|
|
150
215
|
await robot.tap(x, y);
|
|
151
216
|
return `Clicked on screen at coordinates: ${x}, ${y}`;
|
|
152
217
|
});
|
|
153
|
-
tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
|
|
218
|
+
tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
|
|
219
|
+
noParams
|
|
220
|
+
}, async ({}) => {
|
|
154
221
|
requireRobot();
|
|
155
222
|
const elements = await robot.getElementsOnScreen();
|
|
156
223
|
const result = elements.map(element => {
|
|
@@ -190,11 +257,23 @@ const createMcpServer = () => {
|
|
|
190
257
|
return `Opened URL: ${url}`;
|
|
191
258
|
});
|
|
192
259
|
tool("swipe_on_screen", "Swipe on the screen", {
|
|
193
|
-
direction: zod_1.z.enum(["up", "down"]).describe("The direction to swipe"),
|
|
194
|
-
|
|
260
|
+
direction: zod_1.z.enum(["up", "down", "left", "right"]).describe("The direction to swipe"),
|
|
261
|
+
x: zod_1.z.number().optional().describe("The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
262
|
+
y: zod_1.z.number().optional().describe("The y coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
263
|
+
distance: zod_1.z.number().optional().describe("The distance to swipe in pixels. Defaults to 400 pixels for iOS or 30% of screen dimension for Android"),
|
|
264
|
+
}, async ({ direction, x, y, distance }) => {
|
|
195
265
|
requireRobot();
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
if (x !== undefined && y !== undefined) {
|
|
267
|
+
// Use coordinate-based swipe
|
|
268
|
+
await robot.swipeFromCoordinate(x, y, direction, distance);
|
|
269
|
+
const distanceText = distance ? ` ${distance} pixels` : "";
|
|
270
|
+
return `Swiped ${direction}${distanceText} from coordinates: ${x}, ${y}`;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Use center-based swipe
|
|
274
|
+
await robot.swipe(direction);
|
|
275
|
+
return `Swiped ${direction} on screen`;
|
|
276
|
+
}
|
|
198
277
|
});
|
|
199
278
|
tool("mobile_type_keys", "Type text into the focused element", {
|
|
200
279
|
text: zod_1.z.string().describe("The text to type"),
|
|
@@ -207,7 +286,17 @@ const createMcpServer = () => {
|
|
|
207
286
|
}
|
|
208
287
|
return `Typed text: ${text}`;
|
|
209
288
|
});
|
|
210
|
-
|
|
289
|
+
tool("mobile_save_screenshot", "Save a screenshot of the mobile device to a file", {
|
|
290
|
+
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
291
|
+
}, async ({ saveTo }) => {
|
|
292
|
+
requireRobot();
|
|
293
|
+
const screenshot = await robot.getScreenshot();
|
|
294
|
+
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
295
|
+
return `Screenshot saved to: ${saveTo}`;
|
|
296
|
+
});
|
|
297
|
+
server.tool("mobile_take_screenshot", "Take a screenshot of the mobile device. Use this to understand what's on screen, if you need to press an element that is available through view hierarchy then you must list elements on screen instead. Do not cache this result.", {
|
|
298
|
+
noParams
|
|
299
|
+
}, async ({}) => {
|
|
211
300
|
requireRobot();
|
|
212
301
|
try {
|
|
213
302
|
const screenSize = await robot.getScreenSize();
|
|
@@ -251,7 +340,9 @@ const createMcpServer = () => {
|
|
|
251
340
|
await robot.setOrientation(orientation);
|
|
252
341
|
return `Changed device orientation to ${orientation}`;
|
|
253
342
|
});
|
|
254
|
-
tool("mobile_get_orientation", "Get the current screen orientation of the device", {
|
|
343
|
+
tool("mobile_get_orientation", "Get the current screen orientation of the device", {
|
|
344
|
+
noParams
|
|
345
|
+
}, async () => {
|
|
255
346
|
requireRobot();
|
|
256
347
|
const orientation = await robot.getOrientation();
|
|
257
348
|
return `Current device orientation is ${orientation}`;
|
package/lib/webdriver-agent.js
CHANGED
|
@@ -29,7 +29,14 @@ class WebDriverAgent {
|
|
|
29
29
|
},
|
|
30
30
|
body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }),
|
|
31
31
|
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorText = await response.text();
|
|
34
|
+
throw new robot_1.ActionableError(`Failed to create WebDriver session: ${response.status} ${errorText}`);
|
|
35
|
+
}
|
|
32
36
|
const json = await response.json();
|
|
37
|
+
if (!json.value || !json.value.sessionId) {
|
|
38
|
+
throw new robot_1.ActionableError(`Invalid session response: ${JSON.stringify(json)}`);
|
|
39
|
+
}
|
|
33
40
|
return json.value.sessionId;
|
|
34
41
|
}
|
|
35
42
|
async deleteSession(sessionId) {
|
|
@@ -44,8 +51,8 @@ class WebDriverAgent {
|
|
|
44
51
|
await this.deleteSession(sessionId);
|
|
45
52
|
return result;
|
|
46
53
|
}
|
|
47
|
-
async getScreenSize() {
|
|
48
|
-
|
|
54
|
+
async getScreenSize(sessionUrl) {
|
|
55
|
+
if (sessionUrl) {
|
|
49
56
|
const url = `${sessionUrl}/wda/screen`;
|
|
50
57
|
const response = await fetch(url);
|
|
51
58
|
const json = await response.json();
|
|
@@ -54,7 +61,19 @@ class WebDriverAgent {
|
|
|
54
61
|
height: json.value.screenSize.height,
|
|
55
62
|
scale: json.value.scale || 1,
|
|
56
63
|
};
|
|
57
|
-
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return this.withinSession(async (sessionUrlInner) => {
|
|
67
|
+
const url = `${sessionUrlInner}/wda/screen`;
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
const json = await response.json();
|
|
70
|
+
return {
|
|
71
|
+
width: json.value.screenSize.width,
|
|
72
|
+
height: json.value.screenSize.height,
|
|
73
|
+
scale: json.value.scale || 1,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
58
77
|
}
|
|
59
78
|
async sendKeys(keys) {
|
|
60
79
|
await this.withinSession(async (sessionUrl) => {
|
|
@@ -172,19 +191,47 @@ class WebDriverAgent {
|
|
|
172
191
|
});
|
|
173
192
|
});
|
|
174
193
|
}
|
|
194
|
+
async getScreenshot() {
|
|
195
|
+
const url = `http://${this.host}:${this.port}/screenshot`;
|
|
196
|
+
const response = await fetch(url);
|
|
197
|
+
const json = await response.json();
|
|
198
|
+
return Buffer.from(json.value, "base64");
|
|
199
|
+
}
|
|
175
200
|
async swipe(direction) {
|
|
176
201
|
await this.withinSession(async (sessionUrl) => {
|
|
177
|
-
const
|
|
178
|
-
let y0
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
const screenSize = await this.getScreenSize(sessionUrl);
|
|
203
|
+
let x0, y0, x1, y1;
|
|
204
|
+
// Use 60% of the width/height for swipe distance
|
|
205
|
+
const verticalDistance = Math.floor(screenSize.height * 0.6);
|
|
206
|
+
const horizontalDistance = Math.floor(screenSize.width * 0.6);
|
|
207
|
+
const centerX = Math.floor(screenSize.width / 2);
|
|
208
|
+
const centerY = Math.floor(screenSize.height / 2);
|
|
209
|
+
switch (direction) {
|
|
210
|
+
case "up":
|
|
211
|
+
x0 = x1 = centerX;
|
|
212
|
+
y0 = centerY + Math.floor(verticalDistance / 2);
|
|
213
|
+
y1 = centerY - Math.floor(verticalDistance / 2);
|
|
214
|
+
break;
|
|
215
|
+
case "down":
|
|
216
|
+
x0 = x1 = centerX;
|
|
217
|
+
y0 = centerY - Math.floor(verticalDistance / 2);
|
|
218
|
+
y1 = centerY + Math.floor(verticalDistance / 2);
|
|
219
|
+
break;
|
|
220
|
+
case "left":
|
|
221
|
+
y0 = y1 = centerY;
|
|
222
|
+
x0 = centerX + Math.floor(horizontalDistance / 2);
|
|
223
|
+
x1 = centerX - Math.floor(horizontalDistance / 2);
|
|
224
|
+
break;
|
|
225
|
+
case "right":
|
|
226
|
+
y0 = y1 = centerY;
|
|
227
|
+
x0 = centerX - Math.floor(horizontalDistance / 2);
|
|
228
|
+
x1 = centerX + Math.floor(horizontalDistance / 2);
|
|
229
|
+
break;
|
|
230
|
+
default:
|
|
231
|
+
throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
|
|
185
232
|
}
|
|
186
233
|
const url = `${sessionUrl}/actions`;
|
|
187
|
-
await fetch(url, {
|
|
234
|
+
const response = await fetch(url, {
|
|
188
235
|
method: "POST",
|
|
189
236
|
headers: {
|
|
190
237
|
"Content-Type": "application/json",
|
|
@@ -198,14 +245,77 @@ class WebDriverAgent {
|
|
|
198
245
|
actions: [
|
|
199
246
|
{ type: "pointerMove", duration: 0, x: x0, y: y0 },
|
|
200
247
|
{ type: "pointerDown", button: 0 },
|
|
201
|
-
{ type: "pointerMove", duration:
|
|
202
|
-
{ type: "pause", duration: 1000 },
|
|
248
|
+
{ type: "pointerMove", duration: 1000, x: x1, y: y1 },
|
|
203
249
|
{ type: "pointerUp", button: 0 }
|
|
204
250
|
]
|
|
205
251
|
}
|
|
206
252
|
]
|
|
207
253
|
}),
|
|
208
254
|
});
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
const errorText = await response.text();
|
|
257
|
+
throw new robot_1.ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`);
|
|
258
|
+
}
|
|
259
|
+
// Clear actions to ensure they complete
|
|
260
|
+
await fetch(`${sessionUrl}/actions`, {
|
|
261
|
+
method: "DELETE",
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async swipeFromCoordinate(x, y, direction, distance = 400) {
|
|
266
|
+
await this.withinSession(async (sessionUrl) => {
|
|
267
|
+
// Use simple coordinates like the working swipe method
|
|
268
|
+
const x0 = x;
|
|
269
|
+
const y0 = y;
|
|
270
|
+
let x1 = x;
|
|
271
|
+
let y1 = y;
|
|
272
|
+
// Calculate target position based on direction and distance
|
|
273
|
+
switch (direction) {
|
|
274
|
+
case "up":
|
|
275
|
+
y1 = y - distance; // Move up by specified distance
|
|
276
|
+
break;
|
|
277
|
+
case "down":
|
|
278
|
+
y1 = y + distance; // Move down by specified distance
|
|
279
|
+
break;
|
|
280
|
+
case "left":
|
|
281
|
+
x1 = x - distance; // Move left by specified distance
|
|
282
|
+
break;
|
|
283
|
+
case "right":
|
|
284
|
+
x1 = x + distance; // Move right by specified distance
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
|
|
288
|
+
}
|
|
289
|
+
const url = `${sessionUrl}/actions`;
|
|
290
|
+
const response = await fetch(url, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
},
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
actions: [
|
|
297
|
+
{
|
|
298
|
+
type: "pointer",
|
|
299
|
+
id: "finger1",
|
|
300
|
+
parameters: { pointerType: "touch" },
|
|
301
|
+
actions: [
|
|
302
|
+
{ type: "pointerMove", duration: 0, x: x0, y: y0 },
|
|
303
|
+
{ type: "pointerDown", button: 0 },
|
|
304
|
+
{ type: "pointerMove", duration: 1000, x: x1, y: y1 },
|
|
305
|
+
{ type: "pointerUp", button: 0 }
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
311
|
+
if (!response.ok) {
|
|
312
|
+
const errorText = await response.text();
|
|
313
|
+
throw new robot_1.ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`);
|
|
314
|
+
}
|
|
315
|
+
// Clear actions to ensure they complete
|
|
316
|
+
await fetch(`${sessionUrl}/actions`, {
|
|
317
|
+
method: "DELETE",
|
|
318
|
+
});
|
|
209
319
|
});
|
|
210
320
|
}
|
|
211
321
|
async setOrientation(orientation) {
|