@mobilenext/mobile-mcp 0.0.37 → 0.0.39
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 +143 -1
- package/lib/android.js +54 -2
- package/lib/ios.js +24 -2
- package/lib/iphone-simulator.js +3 -37
- package/lib/mobile-device.js +136 -0
- package/lib/mobilecli.js +86 -47
- package/lib/server.js +111 -101
- package/lib/webdriver-agent.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -137,6 +137,32 @@ More details in our [wiki page](https://github.com/mobile-next/mobile-mcp/wiki)
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
<details>
|
|
141
|
+
<summary>Amp</summary>
|
|
142
|
+
|
|
143
|
+
Add via the Amp VS Code extension settings screen or by updating your `settings.json` file:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
"amp.mcpServers": {
|
|
147
|
+
"mobile-mcp": {
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": [
|
|
150
|
+
"@mobilenext/mobile-mcp@latest"
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Amp CLI:**
|
|
157
|
+
|
|
158
|
+
Run the following command in your terminal:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
amp mcp add mobile-mcp -- npx @mobilenext/mobile-mcp@latest
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
</details>
|
|
165
|
+
|
|
140
166
|
<details>
|
|
141
167
|
<summary>Cline</summary>
|
|
142
168
|
|
|
@@ -154,6 +180,65 @@ Use the Claude Code CLI to add the Mobile MCP server:
|
|
|
154
180
|
```bash
|
|
155
181
|
claude mcp add mobile-mcp -- npx -y @mobilenext/mobile-mcp@latest
|
|
156
182
|
```
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary>Claude Desktop</summary>
|
|
187
|
+
|
|
188
|
+
Follow the [MCP install guide](https://modelcontextprotocol.io/quickstart/user), use json configuration above.
|
|
189
|
+
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
<details>
|
|
193
|
+
<summary>Codex</summary>
|
|
194
|
+
|
|
195
|
+
Use the Codex CLI to add the Mobile MCP server:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
codex mcp add mobile-mcp npx "@mobilenext/mobile-mcp@latest"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Alternatively, create or edit the configuration file `~/.codex/config.toml` and add:
|
|
202
|
+
|
|
203
|
+
```toml
|
|
204
|
+
[mcp_servers.mobile-mcp]
|
|
205
|
+
command = "npx"
|
|
206
|
+
args = ["@mobilenext/mobile-mcp@latest"]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
For more information, see the Codex MCP documentation.
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
<details>
|
|
214
|
+
<summary>Copilot</summary>
|
|
215
|
+
|
|
216
|
+
Use the Copilot CLI to interactively add the Mobile MCP server:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
/mcp add
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
You can edit the configuration file `~/.copilot/mcp-config.json` and add:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"mcpServers": {
|
|
227
|
+
"mobile-mcp": {
|
|
228
|
+
"type": "local",
|
|
229
|
+
"command": "npx",
|
|
230
|
+
"tools": [
|
|
231
|
+
"*"
|
|
232
|
+
],
|
|
233
|
+
"args": [
|
|
234
|
+
"@mobilenext/mobile-mcp@latest"
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
For more information, see the Copilot CLI documentation.
|
|
157
242
|
|
|
158
243
|
</details>
|
|
159
244
|
|
|
@@ -194,6 +279,49 @@ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to you
|
|
|
194
279
|
|
|
195
280
|
</details>
|
|
196
281
|
|
|
282
|
+
<details>
|
|
283
|
+
<summary>Kiro</summary>
|
|
284
|
+
|
|
285
|
+
Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"mcpServers": {
|
|
290
|
+
"mobile-mcp": {
|
|
291
|
+
"command": "npx",
|
|
292
|
+
"args": [
|
|
293
|
+
"@mobilenext/mobile-mcp@latest"
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
</details>
|
|
301
|
+
|
|
302
|
+
<details>
|
|
303
|
+
<summary>opencode</summary>
|
|
304
|
+
|
|
305
|
+
Follow the MCP Servers documentation. For example in `~/.config/opencode/opencode.json`:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"$schema": "https://opencode.ai/config.json",
|
|
310
|
+
"mcp": {
|
|
311
|
+
"mobile-mcp": {
|
|
312
|
+
"type": "local",
|
|
313
|
+
"command": [
|
|
314
|
+
"npx",
|
|
315
|
+
"@mobilenext/mobile-mcp@latest"
|
|
316
|
+
],
|
|
317
|
+
"enabled": true
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
</details>
|
|
324
|
+
|
|
197
325
|
<details>
|
|
198
326
|
<summary>Qodo Gen</summary>
|
|
199
327
|
|
|
@@ -203,6 +331,21 @@ Click <code>Save</code>.
|
|
|
203
331
|
|
|
204
332
|
</details>
|
|
205
333
|
|
|
334
|
+
|
|
335
|
+
<details>
|
|
336
|
+
<summary>Windsurf</summary>
|
|
337
|
+
|
|
338
|
+
Open Windsurf settings, navigate to MCP servers, and add a new server using the `command` type with:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
npx @mobilenext/mobile-mcp@latest
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Or add the standard config under `mcpServers` in your settings as shown above.
|
|
345
|
+
|
|
346
|
+
</details>
|
|
347
|
+
|
|
348
|
+
|
|
206
349
|
[Read more in our wiki](https://github.com/mobile-next/mobile-mcp/wiki)! 🚀
|
|
207
350
|
|
|
208
351
|
|
|
@@ -309,4 +452,3 @@ On iOS, you'll need Xcode and to run the Simulator before using Mobile MCP with
|
|
|
309
452
|
<a href = "https://github.com/mobile-next/mobile-mcp/graphs/contributors">
|
|
310
453
|
<img src = "https://contrib.rocks/image?repo=mobile-next/mobile-mcp"/>
|
|
311
454
|
</a>
|
|
312
|
-
|
package/lib/android.js
CHANGED
|
@@ -399,9 +399,9 @@ class AndroidRobot {
|
|
|
399
399
|
async tap(x, y) {
|
|
400
400
|
this.adb("shell", "input", "tap", `${x}`, `${y}`);
|
|
401
401
|
}
|
|
402
|
-
async longPress(x, y) {
|
|
402
|
+
async longPress(x, y, duration) {
|
|
403
403
|
// a long press is a swipe with no movement and a long duration
|
|
404
|
-
this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`,
|
|
404
|
+
this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`, `${duration}`);
|
|
405
405
|
}
|
|
406
406
|
async doubleTap(x, y) {
|
|
407
407
|
await this.tap(x, y);
|
|
@@ -461,6 +461,37 @@ class AndroidDeviceManager {
|
|
|
461
461
|
}
|
|
462
462
|
return "mobile";
|
|
463
463
|
}
|
|
464
|
+
getDeviceVersion(deviceId) {
|
|
465
|
+
try {
|
|
466
|
+
const output = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.build.version.release"], {
|
|
467
|
+
timeout: 5000,
|
|
468
|
+
}).toString().trim();
|
|
469
|
+
return output;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
return "unknown";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
getDeviceName(deviceId) {
|
|
476
|
+
try {
|
|
477
|
+
// Try getting AVD name first (for emulators)
|
|
478
|
+
const avdName = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.boot.qemu.avd_name"], {
|
|
479
|
+
timeout: 5000,
|
|
480
|
+
}).toString().trim();
|
|
481
|
+
if (avdName !== "") {
|
|
482
|
+
// Replace underscores with spaces (e.g., "Pixel_9_Pro" -> "Pixel 9 Pro")
|
|
483
|
+
return avdName.replace(/_/g, " ");
|
|
484
|
+
}
|
|
485
|
+
// Fall back to product model
|
|
486
|
+
const output = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.product.model"], {
|
|
487
|
+
timeout: 5000,
|
|
488
|
+
}).toString().trim();
|
|
489
|
+
return output;
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return deviceId;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
464
495
|
getConnectedDevices() {
|
|
465
496
|
try {
|
|
466
497
|
const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
|
|
@@ -480,5 +511,26 @@ class AndroidDeviceManager {
|
|
|
480
511
|
return [];
|
|
481
512
|
}
|
|
482
513
|
}
|
|
514
|
+
getConnectedDevicesWithDetails() {
|
|
515
|
+
try {
|
|
516
|
+
const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
|
|
517
|
+
.toString()
|
|
518
|
+
.split("\n")
|
|
519
|
+
.map(line => line.trim())
|
|
520
|
+
.filter(line => line !== "")
|
|
521
|
+
.filter(line => !line.startsWith("List of devices attached"))
|
|
522
|
+
.map(line => line.split("\t")[0]);
|
|
523
|
+
return names.map(deviceId => ({
|
|
524
|
+
deviceId,
|
|
525
|
+
deviceType: this.getDeviceType(deviceId),
|
|
526
|
+
version: this.getDeviceVersion(deviceId),
|
|
527
|
+
name: this.getDeviceName(deviceId),
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error("Could not execute adb command, maybe ANDROID_HOME is not set?");
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
483
535
|
}
|
|
484
536
|
exports.AndroidDeviceManager = AndroidDeviceManager;
|
package/lib/ios.js
CHANGED
|
@@ -145,9 +145,9 @@ class IosRobot {
|
|
|
145
145
|
const wda = await this.wda();
|
|
146
146
|
await wda.doubleTap(x, y);
|
|
147
147
|
}
|
|
148
|
-
async longPress(x, y) {
|
|
148
|
+
async longPress(x, y, duration) {
|
|
149
149
|
const wda = await this.wda();
|
|
150
|
-
await wda.longPress(x, y);
|
|
150
|
+
await wda.longPress(x, y, duration);
|
|
151
151
|
}
|
|
152
152
|
async getElementsOnScreen() {
|
|
153
153
|
const wda = await this.wda();
|
|
@@ -191,6 +191,11 @@ class IosManager {
|
|
|
191
191
|
const json = JSON.parse(output);
|
|
192
192
|
return json.DeviceName;
|
|
193
193
|
}
|
|
194
|
+
getDeviceInfo(deviceId) {
|
|
195
|
+
const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
|
|
196
|
+
const json = JSON.parse(output);
|
|
197
|
+
return json;
|
|
198
|
+
}
|
|
194
199
|
listDevices() {
|
|
195
200
|
if (!this.isGoIosInstalled()) {
|
|
196
201
|
console.error("go-ios is not installed, no physical iOS devices can be detected");
|
|
@@ -204,5 +209,22 @@ class IosManager {
|
|
|
204
209
|
}));
|
|
205
210
|
return devices;
|
|
206
211
|
}
|
|
212
|
+
listDevicesWithDetails() {
|
|
213
|
+
if (!this.isGoIosInstalled()) {
|
|
214
|
+
console.error("go-ios is not installed, no physical iOS devices can be detected");
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
|
|
218
|
+
const json = JSON.parse(output);
|
|
219
|
+
const devices = json.deviceList.map(device => {
|
|
220
|
+
const info = this.getDeviceInfo(device);
|
|
221
|
+
return {
|
|
222
|
+
deviceId: device,
|
|
223
|
+
deviceName: info.DeviceName,
|
|
224
|
+
version: info.ProductVersion,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return devices;
|
|
228
|
+
}
|
|
207
229
|
}
|
|
208
230
|
exports.IosManager = IosManager;
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.Simctl = void 0;
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
6
|
const node_os_1 = require("node:os");
|
|
@@ -192,9 +192,9 @@ class Simctl {
|
|
|
192
192
|
const wda = await this.wda();
|
|
193
193
|
await wda.doubleTap(x, y);
|
|
194
194
|
}
|
|
195
|
-
async longPress(x, y) {
|
|
195
|
+
async longPress(x, y, duration) {
|
|
196
196
|
const wda = await this.wda();
|
|
197
|
-
return wda.longPress(x, y);
|
|
197
|
+
return wda.longPress(x, y, duration);
|
|
198
198
|
}
|
|
199
199
|
async pressButton(button) {
|
|
200
200
|
const wda = await this.wda();
|
|
@@ -214,37 +214,3 @@ class Simctl {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
exports.Simctl = Simctl;
|
|
217
|
-
class SimctlManager {
|
|
218
|
-
listSimulators() {
|
|
219
|
-
// detect if this is a mac
|
|
220
|
-
if (process.platform !== "darwin") {
|
|
221
|
-
// don't even try to run xcrun
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const text = (0, node_child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
|
|
226
|
-
const json = JSON.parse(text);
|
|
227
|
-
return Object.values(json.devices).flatMap(device => {
|
|
228
|
-
return device.map(d => {
|
|
229
|
-
return {
|
|
230
|
-
name: d.name,
|
|
231
|
-
uuid: d.udid,
|
|
232
|
-
state: d.state,
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
console.error("Error listing simulators", error);
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
listBootedSimulators() {
|
|
243
|
-
return this.listSimulators()
|
|
244
|
-
.filter(simulator => simulator.state === "Booted");
|
|
245
|
-
}
|
|
246
|
-
getSimulator(uuid) {
|
|
247
|
-
return new Simctl(uuid);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
exports.SimctlManager = SimctlManager;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MobileDevice = void 0;
|
|
4
|
+
const mobilecli_1 = require("./mobilecli");
|
|
5
|
+
class MobileDevice {
|
|
6
|
+
deviceId;
|
|
7
|
+
mobilecli;
|
|
8
|
+
constructor(deviceId) {
|
|
9
|
+
this.deviceId = deviceId;
|
|
10
|
+
this.mobilecli = new mobilecli_1.Mobilecli();
|
|
11
|
+
}
|
|
12
|
+
runCommand(args) {
|
|
13
|
+
const fullArgs = [...args, "--device", this.deviceId];
|
|
14
|
+
return this.mobilecli.executeCommand(fullArgs);
|
|
15
|
+
}
|
|
16
|
+
async getScreenSize() {
|
|
17
|
+
const response = JSON.parse(this.runCommand(["device", "info"]));
|
|
18
|
+
if (response.data.device.screenSize) {
|
|
19
|
+
return response.data.device.screenSize;
|
|
20
|
+
}
|
|
21
|
+
return { width: 0, height: 0, scale: 1.0 };
|
|
22
|
+
}
|
|
23
|
+
async swipe(direction) {
|
|
24
|
+
const screenSize = await this.getScreenSize();
|
|
25
|
+
const centerX = Math.floor(screenSize.width / 2);
|
|
26
|
+
const centerY = Math.floor(screenSize.height / 2);
|
|
27
|
+
const distance = 400; // Default distance in pixels
|
|
28
|
+
let startX = centerX;
|
|
29
|
+
let startY = centerY;
|
|
30
|
+
let endX = centerX;
|
|
31
|
+
let endY = centerY;
|
|
32
|
+
switch (direction) {
|
|
33
|
+
case "up":
|
|
34
|
+
startY = centerY + distance / 2;
|
|
35
|
+
endY = centerY - distance / 2;
|
|
36
|
+
break;
|
|
37
|
+
case "down":
|
|
38
|
+
startY = centerY - distance / 2;
|
|
39
|
+
endY = centerY + distance / 2;
|
|
40
|
+
break;
|
|
41
|
+
case "left":
|
|
42
|
+
startX = centerX + distance / 2;
|
|
43
|
+
endX = centerX - distance / 2;
|
|
44
|
+
break;
|
|
45
|
+
case "right":
|
|
46
|
+
startX = centerX - distance / 2;
|
|
47
|
+
endX = centerX + distance / 2;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
this.runCommand(["io", "swipe", `${startX},${startY},${endX},${endY}`]);
|
|
51
|
+
}
|
|
52
|
+
async swipeFromCoordinate(x, y, direction, distance) {
|
|
53
|
+
const swipeDistance = distance || 400;
|
|
54
|
+
let endX = x;
|
|
55
|
+
let endY = y;
|
|
56
|
+
switch (direction) {
|
|
57
|
+
case "up":
|
|
58
|
+
endY = y - swipeDistance;
|
|
59
|
+
break;
|
|
60
|
+
case "down":
|
|
61
|
+
endY = y + swipeDistance;
|
|
62
|
+
break;
|
|
63
|
+
case "left":
|
|
64
|
+
endX = x - swipeDistance;
|
|
65
|
+
break;
|
|
66
|
+
case "right":
|
|
67
|
+
endX = x + swipeDistance;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
this.runCommand(["io", "swipe", `${x},${y},${endX},${endY}`]);
|
|
71
|
+
}
|
|
72
|
+
async getScreenshot() {
|
|
73
|
+
const fullArgs = ["screenshot", "--device", this.deviceId, "--format", "png", "--output", "-"];
|
|
74
|
+
return this.mobilecli.executeCommandBuffer(fullArgs);
|
|
75
|
+
}
|
|
76
|
+
async listApps() {
|
|
77
|
+
const response = JSON.parse(this.runCommand(["apps", "list"]));
|
|
78
|
+
return response.data.map(app => ({
|
|
79
|
+
appName: app.appName || app.packageName,
|
|
80
|
+
packageName: app.packageName,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
async launchApp(packageName) {
|
|
84
|
+
this.runCommand(["apps", "launch", packageName]);
|
|
85
|
+
}
|
|
86
|
+
async terminateApp(packageName) {
|
|
87
|
+
this.runCommand(["apps", "terminate", packageName]);
|
|
88
|
+
}
|
|
89
|
+
async installApp(path) {
|
|
90
|
+
this.runCommand(["apps", "install", path]);
|
|
91
|
+
}
|
|
92
|
+
async uninstallApp(bundleId) {
|
|
93
|
+
this.runCommand(["apps", "uninstall", bundleId]);
|
|
94
|
+
}
|
|
95
|
+
async openUrl(url) {
|
|
96
|
+
this.runCommand(["url", url]);
|
|
97
|
+
}
|
|
98
|
+
async sendKeys(text) {
|
|
99
|
+
this.runCommand(["io", "text", text]);
|
|
100
|
+
}
|
|
101
|
+
async pressButton(button) {
|
|
102
|
+
this.runCommand(["io", "button", button]);
|
|
103
|
+
}
|
|
104
|
+
async tap(x, y) {
|
|
105
|
+
this.runCommand(["io", "tap", `${x},${y}`]);
|
|
106
|
+
}
|
|
107
|
+
async doubleTap(x, y) {
|
|
108
|
+
// TODO: should move into mobilecli itself as "io doubletap"
|
|
109
|
+
await this.tap(x, y);
|
|
110
|
+
await this.tap(x, y);
|
|
111
|
+
}
|
|
112
|
+
async longPress(x, y, duration) {
|
|
113
|
+
this.runCommand(["io", "longpress", `${x},${y}`, "--duration", `${duration}`]);
|
|
114
|
+
}
|
|
115
|
+
async getElementsOnScreen() {
|
|
116
|
+
const response = JSON.parse(this.runCommand(["dump", "ui"]));
|
|
117
|
+
return response.data.elements.map(element => ({
|
|
118
|
+
type: element.type,
|
|
119
|
+
label: element.label,
|
|
120
|
+
text: element.text,
|
|
121
|
+
name: element.name,
|
|
122
|
+
value: element.value,
|
|
123
|
+
identifier: element.identifier,
|
|
124
|
+
rect: element.rect,
|
|
125
|
+
focused: element.focused,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
async setOrientation(orientation) {
|
|
129
|
+
this.runCommand(["device", "orientation", "set", orientation]);
|
|
130
|
+
}
|
|
131
|
+
async getOrientation() {
|
|
132
|
+
const response = JSON.parse(this.runCommand(["device", "orientation", "get"]));
|
|
133
|
+
return response.data.orientation;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.MobileDevice = MobileDevice;
|
package/lib/mobilecli.js
CHANGED
|
@@ -1,58 +1,97 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.Mobilecli = void 0;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const TIMEOUT = 30000;
|
|
8
|
+
const MAX_BUFFER_SIZE = 1024 * 1024 * 4;
|
|
9
|
+
class Mobilecli {
|
|
10
|
+
path = null;
|
|
11
|
+
constructor() { }
|
|
12
|
+
getPath() {
|
|
13
|
+
if (!this.path) {
|
|
14
|
+
this.path = Mobilecli.getMobilecliPath();
|
|
15
|
+
}
|
|
16
|
+
return this.path;
|
|
9
17
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
switch (platform) {
|
|
14
|
-
case "darwin":
|
|
15
|
-
if (arch === "arm64") {
|
|
16
|
-
binaryName += "-darwin-arm64";
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
binaryName += "-darwin-amd64";
|
|
20
|
-
}
|
|
21
|
-
break;
|
|
22
|
-
case "linux":
|
|
23
|
-
if (arch === "arm64") {
|
|
24
|
-
binaryName += "-linux-arm64";
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
binaryName += "-linux-amd64";
|
|
28
|
-
}
|
|
29
|
-
break;
|
|
30
|
-
case "win32":
|
|
31
|
-
binaryName += "-windows-amd64.exe";
|
|
32
|
-
break;
|
|
33
|
-
default:
|
|
34
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
18
|
+
executeCommand(args) {
|
|
19
|
+
const path = this.getPath();
|
|
20
|
+
return (0, node_child_process_1.execFileSync)(path, args, { encoding: "utf8" }).toString().trim();
|
|
35
21
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
executeCommandBuffer(args) {
|
|
23
|
+
const path = this.getPath();
|
|
24
|
+
return (0, node_child_process_1.execFileSync)(path, args, {
|
|
25
|
+
encoding: "buffer",
|
|
26
|
+
maxBuffer: MAX_BUFFER_SIZE,
|
|
27
|
+
timeout: TIMEOUT,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
static getMobilecliPath() {
|
|
31
|
+
if (process.env.MOBILECLI_PATH) {
|
|
32
|
+
return process.env.MOBILECLI_PATH;
|
|
33
|
+
}
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
const arch = process.arch;
|
|
36
|
+
const normalizedPlatform = platform === "win32" ? "windows" : platform;
|
|
37
|
+
const normalizedArch = arch === "arm64" ? "arm64" : "amd64";
|
|
38
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
39
|
+
const binaryName = `mobilecli-${normalizedPlatform}-${normalizedArch}${ext}`;
|
|
40
|
+
// Check if mobile-mcp is installed as a package
|
|
41
|
+
const currentPath = __filename;
|
|
42
|
+
const pathParts = currentPath.split(node_path_1.sep);
|
|
43
|
+
const lastNodeModulesIndex = pathParts.lastIndexOf("node_modules");
|
|
44
|
+
if (lastNodeModulesIndex !== -1) {
|
|
45
|
+
// We're inside node_modules, go to the last node_modules in the path
|
|
46
|
+
const nodeModulesParts = pathParts.slice(0, lastNodeModulesIndex + 1);
|
|
47
|
+
const lastNodeModulesPath = nodeModulesParts.join(node_path_1.sep);
|
|
48
|
+
const mobilecliPath = (0, node_path_1.join)(lastNodeModulesPath, "@mobilenext", "mobilecli", "bin", binaryName);
|
|
49
|
+
if ((0, node_fs_1.existsSync)(mobilecliPath)) {
|
|
50
|
+
return mobilecliPath;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Not in node_modules, look one directory up from current script
|
|
54
|
+
const scriptDir = (0, node_path_1.dirname)(__filename);
|
|
55
|
+
const parentDir = (0, node_path_1.dirname)(scriptDir);
|
|
56
|
+
const mobilecliPath = (0, node_path_1.join)(parentDir, "node_modules", "@mobilenext", "mobilecli", "bin", binaryName);
|
|
45
57
|
if ((0, node_fs_1.existsSync)(mobilecliPath)) {
|
|
46
58
|
return mobilecliPath;
|
|
47
59
|
}
|
|
60
|
+
throw new Error(`Could not find mobilecli binary for platform: ${platform}`);
|
|
48
61
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
getVersion() {
|
|
63
|
+
try {
|
|
64
|
+
const output = this.executeCommand(["--version"]);
|
|
65
|
+
if (output.startsWith("mobilecli version ")) {
|
|
66
|
+
return output.substring("mobilecli version ".length);
|
|
67
|
+
}
|
|
68
|
+
return "failed";
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return "failed " + error.message;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
getDevices(options) {
|
|
75
|
+
const args = ["devices"];
|
|
76
|
+
if (options) {
|
|
77
|
+
if (options.includeOffline) {
|
|
78
|
+
args.push("--include-offline");
|
|
79
|
+
}
|
|
80
|
+
if (options.platform) {
|
|
81
|
+
if (options.platform !== "ios" && options.platform !== "android") {
|
|
82
|
+
throw new Error(`Invalid platform: ${options.platform}. Must be "ios" or "android"`);
|
|
83
|
+
}
|
|
84
|
+
args.push("--platform", options.platform);
|
|
85
|
+
}
|
|
86
|
+
if (options.type) {
|
|
87
|
+
if (options.type !== "real" && options.type !== "emulator" && options.type !== "simulator") {
|
|
88
|
+
throw new Error(`Invalid type: ${options.type}. Must be "real", "emulator", or "simulator"`);
|
|
89
|
+
}
|
|
90
|
+
args.push("--type", options.type);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const mobilecliOutput = this.executeCommand(args);
|
|
94
|
+
return JSON.parse(mobilecliOutput);
|
|
55
95
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
exports.getMobilecliPath = getMobilecliPath;
|
|
96
|
+
}
|
|
97
|
+
exports.Mobilecli = Mobilecli;
|
package/lib/server.js
CHANGED
|
@@ -9,15 +9,14 @@ const zod_1 = require("zod");
|
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
11
11
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
|
-
const node_child_process_1 = require("node:child_process");
|
|
13
12
|
const logger_1 = require("./logger");
|
|
14
13
|
const android_1 = require("./android");
|
|
15
14
|
const robot_1 = require("./robot");
|
|
16
|
-
const iphone_simulator_1 = require("./iphone-simulator");
|
|
17
15
|
const ios_1 = require("./ios");
|
|
18
16
|
const png_1 = require("./png");
|
|
19
17
|
const image_utils_1 = require("./image-utils");
|
|
20
18
|
const mobilecli_1 = require("./mobilecli");
|
|
19
|
+
const mobile_device_1 = require("./mobile-device");
|
|
21
20
|
const getAgentVersion = () => {
|
|
22
21
|
const json = require("../package.json");
|
|
23
22
|
return json.version;
|
|
@@ -40,17 +39,20 @@ const createMcpServer = () => {
|
|
|
40
39
|
return "unknown";
|
|
41
40
|
}
|
|
42
41
|
};
|
|
43
|
-
const tool = (name, title, description, paramsSchema, cb) => {
|
|
42
|
+
const tool = (name, title, description, paramsSchema, annotations, cb) => {
|
|
44
43
|
server.registerTool(name, {
|
|
45
44
|
title,
|
|
46
45
|
description,
|
|
47
46
|
inputSchema: paramsSchema,
|
|
47
|
+
annotations,
|
|
48
48
|
}, (async (args, _extra) => {
|
|
49
49
|
try {
|
|
50
50
|
(0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
|
|
51
|
+
const start = +new Date();
|
|
51
52
|
const response = await cb(args);
|
|
53
|
+
const duration = +new Date() - start;
|
|
52
54
|
(0, logger_1.trace)(`=> ${response}`);
|
|
53
|
-
posthog("tool_invoked", { "ToolName": name }).then();
|
|
55
|
+
posthog("tool_invoked", { "ToolName": name, "Duration": duration }).then();
|
|
54
56
|
return {
|
|
55
57
|
content: [{ type: "text", text: response }],
|
|
56
58
|
};
|
|
@@ -109,109 +111,112 @@ const createMcpServer = () => {
|
|
|
109
111
|
// ignore
|
|
110
112
|
}
|
|
111
113
|
};
|
|
112
|
-
const
|
|
114
|
+
const mobilecli = new mobilecli_1.Mobilecli();
|
|
115
|
+
posthog("launch", {}).then();
|
|
116
|
+
const ensureMobilecliAvailable = () => {
|
|
113
117
|
try {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return output.substring("mobilecli version ".length);
|
|
118
|
+
const version = mobilecli.getVersion();
|
|
119
|
+
if (version.startsWith("failed")) {
|
|
120
|
+
throw new Error("mobilecli version check failed");
|
|
118
121
|
}
|
|
119
|
-
return "failed";
|
|
120
122
|
}
|
|
121
123
|
catch (error) {
|
|
122
|
-
|
|
124
|
+
throw new robot_1.ActionableError(`mobilecli is not available or not working properly. Please review the documentation at https://github.com/mobile-next/mobile-mcp/wiki for installation instructions`);
|
|
123
125
|
}
|
|
124
126
|
};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
const mobilecliVersion = getMobilecliVersion();
|
|
131
|
-
posthog("launch", { "MobilecliVersion": mobilecliVersion }).then();
|
|
132
|
-
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
133
|
-
const getRobotFromDevice = (device) => {
|
|
127
|
+
const getRobotFromDevice = (deviceId) => {
|
|
128
|
+
// from now on, we must have mobilecli working
|
|
129
|
+
ensureMobilecliAvailable();
|
|
130
|
+
// Check if it's an iOS device
|
|
134
131
|
const iosManager = new ios_1.IosManager();
|
|
135
|
-
const androidManager = new android_1.AndroidDeviceManager();
|
|
136
|
-
const simulators = simulatorManager.listBootedSimulators();
|
|
137
|
-
const androidDevices = androidManager.getConnectedDevices();
|
|
138
132
|
const iosDevices = iosManager.listDevices();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return simulatorManager.getSimulator(device);
|
|
133
|
+
const iosDevice = iosDevices.find(d => d.deviceId === deviceId);
|
|
134
|
+
if (iosDevice) {
|
|
135
|
+
return new ios_1.IosRobot(deviceId);
|
|
143
136
|
}
|
|
144
137
|
// Check if it's an Android device
|
|
145
|
-
const
|
|
138
|
+
const androidManager = new android_1.AndroidDeviceManager();
|
|
139
|
+
const androidDevices = androidManager.getConnectedDevices();
|
|
140
|
+
const androidDevice = androidDevices.find(d => d.deviceId === deviceId);
|
|
146
141
|
if (androidDevice) {
|
|
147
|
-
return new android_1.AndroidRobot(
|
|
142
|
+
return new android_1.AndroidRobot(deviceId);
|
|
148
143
|
}
|
|
149
|
-
// Check if it's
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
144
|
+
// Check if it's a simulator (will later replace all other device types as well)
|
|
145
|
+
const response = mobilecli.getDevices({
|
|
146
|
+
platform: "ios",
|
|
147
|
+
type: "simulator",
|
|
148
|
+
includeOffline: false,
|
|
149
|
+
});
|
|
150
|
+
if (response.status === "ok" && response.data && response.data.devices) {
|
|
151
|
+
for (const device of response.data.devices) {
|
|
152
|
+
if (device.id === deviceId) {
|
|
153
|
+
return new mobile_device_1.MobileDevice(deviceId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
153
156
|
}
|
|
154
|
-
throw new robot_1.ActionableError(`Device "${
|
|
157
|
+
throw new robot_1.ActionableError(`Device "${deviceId}" not found. Use the mobile_list_available_devices tool to see available devices.`);
|
|
155
158
|
};
|
|
156
159
|
tool("mobile_list_available_devices", "List 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.", {
|
|
157
160
|
noParams
|
|
158
|
-
}, async ({}) => {
|
|
161
|
+
}, { readOnlyHint: true }, async ({}) => {
|
|
162
|
+
// from today onward, we must have mobilecli working
|
|
163
|
+
ensureMobilecliAvailable();
|
|
159
164
|
const iosManager = new ios_1.IosManager();
|
|
160
165
|
const androidManager = new android_1.AndroidDeviceManager();
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const androidDevices = androidManager.
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let mobilecliDeviceCount = 0;
|
|
174
|
-
try {
|
|
175
|
-
const response = getMobilecliDevices();
|
|
176
|
-
if (response.status === "ok" && response.data && response.data.devices) {
|
|
177
|
-
mobilecliDeviceCount = response.data.devices.length;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
// if mobilecli fails, we'll just set count to 0
|
|
182
|
-
}
|
|
183
|
-
if (deviceCount === mobilecliDeviceCount) {
|
|
184
|
-
posthog("debug_mobilecli_same_number_of_devices", {
|
|
185
|
-
"DeviceCount": deviceCount,
|
|
186
|
-
"MobilecliDeviceCount": mobilecliDeviceCount,
|
|
187
|
-
}).then();
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
posthog("debug_mobilecli_different_number_of_devices", {
|
|
191
|
-
"DeviceCount": deviceCount,
|
|
192
|
-
"MobilecliDeviceCount": mobilecliDeviceCount,
|
|
193
|
-
"DeviceCountDifference": deviceCount - mobilecliDeviceCount,
|
|
194
|
-
}).then();
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const resp = ["Found these devices:"];
|
|
198
|
-
if (simulatorNames.length > 0) {
|
|
199
|
-
resp.push(`iOS simulators: [${simulatorNames.join(",")}]`);
|
|
166
|
+
const devices = [];
|
|
167
|
+
// Get Android devices with details
|
|
168
|
+
const androidDevices = androidManager.getConnectedDevicesWithDetails();
|
|
169
|
+
for (const device of androidDevices) {
|
|
170
|
+
devices.push({
|
|
171
|
+
id: device.deviceId,
|
|
172
|
+
name: device.name,
|
|
173
|
+
platform: "android",
|
|
174
|
+
type: "emulator",
|
|
175
|
+
version: device.version,
|
|
176
|
+
state: "online",
|
|
177
|
+
});
|
|
200
178
|
}
|
|
201
|
-
|
|
202
|
-
|
|
179
|
+
// Get iOS physical devices with details
|
|
180
|
+
try {
|
|
181
|
+
const iosDevices = iosManager.listDevicesWithDetails();
|
|
182
|
+
for (const device of iosDevices) {
|
|
183
|
+
devices.push({
|
|
184
|
+
id: device.deviceId,
|
|
185
|
+
name: device.deviceName,
|
|
186
|
+
platform: "ios",
|
|
187
|
+
type: "real",
|
|
188
|
+
version: device.version,
|
|
189
|
+
state: "online",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
203
192
|
}
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
catch (error) {
|
|
194
|
+
// If go-ios is not available, silently skip
|
|
206
195
|
}
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
// Get iOS simulators from mobilecli (excluding offline devices)
|
|
197
|
+
const response = mobilecli.getDevices({
|
|
198
|
+
platform: "ios",
|
|
199
|
+
type: "simulator",
|
|
200
|
+
includeOffline: false,
|
|
201
|
+
});
|
|
202
|
+
if (response.status === "ok" && response.data && response.data.devices) {
|
|
203
|
+
for (const device of response.data.devices) {
|
|
204
|
+
devices.push({
|
|
205
|
+
id: device.id,
|
|
206
|
+
name: device.name,
|
|
207
|
+
platform: device.platform,
|
|
208
|
+
type: device.type,
|
|
209
|
+
version: device.version,
|
|
210
|
+
state: "online",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
209
213
|
}
|
|
210
|
-
|
|
214
|
+
const out = { devices };
|
|
215
|
+
return JSON.stringify(out);
|
|
211
216
|
});
|
|
212
217
|
tool("mobile_list_apps", "List Apps", "List all the installed apps on the device", {
|
|
213
218
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
214
|
-
}, async ({ device }) => {
|
|
219
|
+
}, { readOnlyHint: true }, async ({ device }) => {
|
|
215
220
|
const robot = getRobotFromDevice(device);
|
|
216
221
|
const result = await robot.listApps();
|
|
217
222
|
return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
|
|
@@ -219,7 +224,7 @@ const createMcpServer = () => {
|
|
|
219
224
|
tool("mobile_launch_app", "Launch App", "Launch an app on mobile device. Use this to open a specific app. You can find the package name of the app by calling list_apps_on_device.", {
|
|
220
225
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
221
226
|
packageName: zod_1.z.string().describe("The package name of the app to launch"),
|
|
222
|
-
}, async ({ device, packageName }) => {
|
|
227
|
+
}, { destructiveHint: true }, async ({ device, packageName }) => {
|
|
223
228
|
const robot = getRobotFromDevice(device);
|
|
224
229
|
await robot.launchApp(packageName);
|
|
225
230
|
return `Launched app ${packageName}`;
|
|
@@ -227,7 +232,7 @@ const createMcpServer = () => {
|
|
|
227
232
|
tool("mobile_terminate_app", "Terminate App", "Stop and terminate an app on mobile device", {
|
|
228
233
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
229
234
|
packageName: zod_1.z.string().describe("The package name of the app to terminate"),
|
|
230
|
-
}, async ({ device, packageName }) => {
|
|
235
|
+
}, { destructiveHint: true }, async ({ device, packageName }) => {
|
|
231
236
|
const robot = getRobotFromDevice(device);
|
|
232
237
|
await robot.terminateApp(packageName);
|
|
233
238
|
return `Terminated app ${packageName}`;
|
|
@@ -235,7 +240,7 @@ const createMcpServer = () => {
|
|
|
235
240
|
tool("mobile_install_app", "Install App", "Install an app on mobile device", {
|
|
236
241
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
237
242
|
path: zod_1.z.string().describe("The path to the app file to install. For iOS simulators, provide a .zip file or a .app directory. For Android provide an .apk file. For iOS real devices provide an .ipa file"),
|
|
238
|
-
}, async ({ device, path }) => {
|
|
243
|
+
}, { destructiveHint: true }, async ({ device, path }) => {
|
|
239
244
|
const robot = getRobotFromDevice(device);
|
|
240
245
|
await robot.installApp(path);
|
|
241
246
|
return `Installed app from ${path}`;
|
|
@@ -243,14 +248,14 @@ const createMcpServer = () => {
|
|
|
243
248
|
tool("mobile_uninstall_app", "Uninstall App", "Uninstall an app from mobile device", {
|
|
244
249
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
245
250
|
bundle_id: zod_1.z.string().describe("Bundle identifier (iOS) or package name (Android) of the app to be uninstalled"),
|
|
246
|
-
}, async ({ device, bundle_id }) => {
|
|
251
|
+
}, { destructiveHint: true }, async ({ device, bundle_id }) => {
|
|
247
252
|
const robot = getRobotFromDevice(device);
|
|
248
253
|
await robot.uninstallApp(bundle_id);
|
|
249
254
|
return `Uninstalled app ${bundle_id}`;
|
|
250
255
|
});
|
|
251
256
|
tool("mobile_get_screen_size", "Get Screen Size", "Get the screen size of the mobile device in pixels", {
|
|
252
257
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
253
|
-
}, async ({ device }) => {
|
|
258
|
+
}, { readOnlyHint: true }, async ({ device }) => {
|
|
254
259
|
const robot = getRobotFromDevice(device);
|
|
255
260
|
const screenSize = await robot.getScreenSize();
|
|
256
261
|
return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
|
|
@@ -259,7 +264,7 @@ const createMcpServer = () => {
|
|
|
259
264
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
260
265
|
x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
|
|
261
266
|
y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
|
|
262
|
-
}, async ({ device, x, y }) => {
|
|
267
|
+
}, { destructiveHint: true }, async ({ device, x, y }) => {
|
|
263
268
|
const robot = getRobotFromDevice(device);
|
|
264
269
|
await robot.tap(x, y);
|
|
265
270
|
return `Clicked on screen at coordinates: ${x}, ${y}`;
|
|
@@ -268,7 +273,7 @@ const createMcpServer = () => {
|
|
|
268
273
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
269
274
|
x: zod_1.z.number().describe("The x coordinate to double-tap, in pixels"),
|
|
270
275
|
y: zod_1.z.number().describe("The y coordinate to double-tap, in pixels"),
|
|
271
|
-
}, async ({ device, x, y }) => {
|
|
276
|
+
}, { destructiveHint: true }, async ({ device, x, y }) => {
|
|
272
277
|
const robot = getRobotFromDevice(device);
|
|
273
278
|
await robot.doubleTap(x, y);
|
|
274
279
|
return `Double-tapped on screen at coordinates: ${x}, ${y}`;
|
|
@@ -277,14 +282,16 @@ const createMcpServer = () => {
|
|
|
277
282
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
278
283
|
x: zod_1.z.number().describe("The x coordinate to long press on the screen, in pixels"),
|
|
279
284
|
y: zod_1.z.number().describe("The y coordinate to long press on the screen, in pixels"),
|
|
280
|
-
|
|
285
|
+
duration: zod_1.z.number().min(1).max(10000).optional().describe("Duration of the long press in milliseconds. Defaults to 500ms."),
|
|
286
|
+
}, { destructiveHint: true }, async ({ device, x, y, duration }) => {
|
|
281
287
|
const robot = getRobotFromDevice(device);
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
const pressDuration = duration ?? 500;
|
|
289
|
+
await robot.longPress(x, y, pressDuration);
|
|
290
|
+
return `Long pressed on screen at coordinates: ${x}, ${y} for ${pressDuration}ms`;
|
|
284
291
|
});
|
|
285
292
|
tool("mobile_list_elements_on_screen", "List Screen Elements", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
|
|
286
293
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
287
|
-
}, async ({ device }) => {
|
|
294
|
+
}, { readOnlyHint: true }, async ({ device }) => {
|
|
288
295
|
const robot = getRobotFromDevice(device);
|
|
289
296
|
const elements = await robot.getElementsOnScreen();
|
|
290
297
|
const result = elements.map(element => {
|
|
@@ -312,7 +319,7 @@ const createMcpServer = () => {
|
|
|
312
319
|
tool("mobile_press_button", "Press Button", "Press a button on device", {
|
|
313
320
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
314
321
|
button: zod_1.z.string().describe("The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER, DPAD_CENTER (android tv only), DPAD_UP (android tv only), DPAD_DOWN (android tv only), DPAD_LEFT (android tv only), DPAD_RIGHT (android tv only)"),
|
|
315
|
-
}, async ({ device, button }) => {
|
|
322
|
+
}, { destructiveHint: true }, async ({ device, button }) => {
|
|
316
323
|
const robot = getRobotFromDevice(device);
|
|
317
324
|
await robot.pressButton(button);
|
|
318
325
|
return `Pressed the button: ${button}`;
|
|
@@ -320,7 +327,7 @@ const createMcpServer = () => {
|
|
|
320
327
|
tool("mobile_open_url", "Open URL", "Open a URL in browser on device", {
|
|
321
328
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
322
329
|
url: zod_1.z.string().describe("The URL to open"),
|
|
323
|
-
}, async ({ device, url }) => {
|
|
330
|
+
}, { destructiveHint: true }, async ({ device, url }) => {
|
|
324
331
|
const robot = getRobotFromDevice(device);
|
|
325
332
|
await robot.openUrl(url);
|
|
326
333
|
return `Opened URL: ${url}`;
|
|
@@ -331,7 +338,7 @@ const createMcpServer = () => {
|
|
|
331
338
|
x: zod_1.z.number().optional().describe("The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
332
339
|
y: zod_1.z.number().optional().describe("The y coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
333
340
|
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"),
|
|
334
|
-
}, async ({ device, direction, x, y, distance }) => {
|
|
341
|
+
}, { destructiveHint: true }, async ({ device, direction, x, y, distance }) => {
|
|
335
342
|
const robot = getRobotFromDevice(device);
|
|
336
343
|
if (x !== undefined && y !== undefined) {
|
|
337
344
|
// Use coordinate-based swipe
|
|
@@ -349,7 +356,7 @@ const createMcpServer = () => {
|
|
|
349
356
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
350
357
|
text: zod_1.z.string().describe("The text to type"),
|
|
351
358
|
submit: zod_1.z.boolean().describe("Whether to submit the text. If true, the text will be submitted as if the user pressed the enter key."),
|
|
352
|
-
}, async ({ device, text, submit }) => {
|
|
359
|
+
}, { destructiveHint: true }, async ({ device, text, submit }) => {
|
|
353
360
|
const robot = getRobotFromDevice(device);
|
|
354
361
|
await robot.sendKeys(text);
|
|
355
362
|
if (submit) {
|
|
@@ -360,7 +367,7 @@ const createMcpServer = () => {
|
|
|
360
367
|
tool("mobile_save_screenshot", "Save Screenshot", "Save a screenshot of the mobile device to a file", {
|
|
361
368
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
362
369
|
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
363
|
-
}, async ({ device, saveTo }) => {
|
|
370
|
+
}, { destructiveHint: true }, async ({ device, saveTo }) => {
|
|
364
371
|
const robot = getRobotFromDevice(device);
|
|
365
372
|
const screenshot = await robot.getScreenshot();
|
|
366
373
|
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
@@ -371,7 +378,10 @@ const createMcpServer = () => {
|
|
|
371
378
|
description: "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.",
|
|
372
379
|
inputSchema: {
|
|
373
380
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
374
|
-
}
|
|
381
|
+
},
|
|
382
|
+
annotations: {
|
|
383
|
+
readOnlyHint: true,
|
|
384
|
+
},
|
|
375
385
|
}, async ({ device }) => {
|
|
376
386
|
try {
|
|
377
387
|
const robot = getRobotFromDevice(device);
|
|
@@ -419,14 +429,14 @@ const createMcpServer = () => {
|
|
|
419
429
|
tool("mobile_set_orientation", "Set Orientation", "Change the screen orientation of the device", {
|
|
420
430
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
421
431
|
orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
|
|
422
|
-
}, async ({ device, orientation }) => {
|
|
432
|
+
}, { destructiveHint: true }, async ({ device, orientation }) => {
|
|
423
433
|
const robot = getRobotFromDevice(device);
|
|
424
434
|
await robot.setOrientation(orientation);
|
|
425
435
|
return `Changed device orientation to ${orientation}`;
|
|
426
436
|
});
|
|
427
437
|
tool("mobile_get_orientation", "Get Orientation", "Get the current screen orientation of the device", {
|
|
428
438
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
429
|
-
}, async ({ device }) => {
|
|
439
|
+
}, { readOnlyHint: true }, async ({ device }) => {
|
|
430
440
|
const robot = getRobotFromDevice(device);
|
|
431
441
|
const orientation = await robot.getOrientation();
|
|
432
442
|
return `Current device orientation is ${orientation}`;
|
package/lib/webdriver-agent.js
CHANGED
|
@@ -172,7 +172,7 @@ class WebDriverAgent {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
|
-
async longPress(x, y) {
|
|
175
|
+
async longPress(x, y, duration) {
|
|
176
176
|
await this.withinSession(async (sessionUrl) => {
|
|
177
177
|
const url = `${sessionUrl}/actions`;
|
|
178
178
|
await fetch(url, {
|
|
@@ -189,7 +189,7 @@ class WebDriverAgent {
|
|
|
189
189
|
actions: [
|
|
190
190
|
{ type: "pointerMove", duration: 0, x, y },
|
|
191
191
|
{ type: "pointerDown", button: 0 },
|
|
192
|
-
{ type: "pause", duration
|
|
192
|
+
{ type: "pause", duration },
|
|
193
193
|
{ type: "pointerUp", button: 0 }
|
|
194
194
|
]
|
|
195
195
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobilenext/mobile-mcp",
|
|
3
3
|
"mcpName": "io.github.mobile-next/mobile-mcp",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.39",
|
|
5
5
|
"description": "Mobile MCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"zod-to-json-schema": "3.25.0"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@mobilenext/mobilecli": "0.0.
|
|
35
|
+
"@mobilenext/mobilecli": "0.0.49"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@eslint/eslintrc": "^3.2.0",
|