@mobilenext/mobile-mcp 0.0.36 → 0.0.38
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 +52 -0
- package/lib/ios.js +22 -0
- package/lib/iphone-simulator.js +1 -35
- package/lib/mobile-device.js +136 -0
- package/lib/mobilecli.js +86 -47
- package/lib/server.js +114 -107
- package/package.json +6 -5
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
|
@@ -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
|
@@ -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");
|
|
@@ -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) {
|
|
113
|
+
this.runCommand(["io", "longpress", `${x},${y}`]);
|
|
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;
|
|
@@ -27,10 +26,6 @@ const createMcpServer = () => {
|
|
|
27
26
|
const server = new mcp_js_1.McpServer({
|
|
28
27
|
name: "mobile-mcp",
|
|
29
28
|
version: (0, exports.getAgentVersion)(),
|
|
30
|
-
capabilities: {
|
|
31
|
-
resources: {},
|
|
32
|
-
tools: {},
|
|
33
|
-
},
|
|
34
29
|
});
|
|
35
30
|
// an empty object to satisfy windsurf
|
|
36
31
|
const noParams = zod_1.z.object({});
|
|
@@ -44,13 +39,19 @@ const createMcpServer = () => {
|
|
|
44
39
|
return "unknown";
|
|
45
40
|
}
|
|
46
41
|
};
|
|
47
|
-
const tool = (name, description, paramsSchema, cb) => {
|
|
48
|
-
|
|
42
|
+
const tool = (name, title, description, paramsSchema, cb) => {
|
|
43
|
+
server.registerTool(name, {
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
inputSchema: paramsSchema,
|
|
47
|
+
}, (async (args, _extra) => {
|
|
49
48
|
try {
|
|
50
49
|
(0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
|
|
50
|
+
const start = +new Date();
|
|
51
51
|
const response = await cb(args);
|
|
52
|
+
const duration = +new Date() - start;
|
|
52
53
|
(0, logger_1.trace)(`=> ${response}`);
|
|
53
|
-
posthog("tool_invoked", { "ToolName": name }).then();
|
|
54
|
+
posthog("tool_invoked", { "ToolName": name, "Duration": duration }).then();
|
|
54
55
|
return {
|
|
55
56
|
content: [{ type: "text", text: response }],
|
|
56
57
|
};
|
|
@@ -71,8 +72,7 @@ const createMcpServer = () => {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
};
|
|
75
|
-
server.tool(name, description, paramsSchema, args => wrappedCb(args));
|
|
75
|
+
}));
|
|
76
76
|
};
|
|
77
77
|
const posthog = async (event, properties) => {
|
|
78
78
|
try {
|
|
@@ -110,114 +110,117 @@ const createMcpServer = () => {
|
|
|
110
110
|
// ignore
|
|
111
111
|
}
|
|
112
112
|
};
|
|
113
|
-
const
|
|
113
|
+
const mobilecli = new mobilecli_1.Mobilecli();
|
|
114
|
+
posthog("launch", {}).then();
|
|
115
|
+
const ensureMobilecliAvailable = () => {
|
|
114
116
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return output.substring("mobilecli version ".length);
|
|
117
|
+
const version = mobilecli.getVersion();
|
|
118
|
+
if (version.startsWith("failed")) {
|
|
119
|
+
throw new Error("mobilecli version check failed");
|
|
119
120
|
}
|
|
120
|
-
return "failed";
|
|
121
121
|
}
|
|
122
122
|
catch (error) {
|
|
123
|
-
|
|
123
|
+
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`);
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
const mobilecliVersion = getMobilecliVersion();
|
|
132
|
-
posthog("launch", { "MobilecliVersion": mobilecliVersion }).then();
|
|
133
|
-
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
134
|
-
const getRobotFromDevice = (device) => {
|
|
126
|
+
const getRobotFromDevice = (deviceId) => {
|
|
127
|
+
// from now on, we must have mobilecli working
|
|
128
|
+
ensureMobilecliAvailable();
|
|
129
|
+
// Check if it's an iOS device
|
|
135
130
|
const iosManager = new ios_1.IosManager();
|
|
136
|
-
const androidManager = new android_1.AndroidDeviceManager();
|
|
137
|
-
const simulators = simulatorManager.listBootedSimulators();
|
|
138
|
-
const androidDevices = androidManager.getConnectedDevices();
|
|
139
131
|
const iosDevices = iosManager.listDevices();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return simulatorManager.getSimulator(device);
|
|
132
|
+
const iosDevice = iosDevices.find(d => d.deviceId === deviceId);
|
|
133
|
+
if (iosDevice) {
|
|
134
|
+
return new ios_1.IosRobot(deviceId);
|
|
144
135
|
}
|
|
145
136
|
// Check if it's an Android device
|
|
146
|
-
const
|
|
137
|
+
const androidManager = new android_1.AndroidDeviceManager();
|
|
138
|
+
const androidDevices = androidManager.getConnectedDevices();
|
|
139
|
+
const androidDevice = androidDevices.find(d => d.deviceId === deviceId);
|
|
147
140
|
if (androidDevice) {
|
|
148
|
-
return new android_1.AndroidRobot(
|
|
141
|
+
return new android_1.AndroidRobot(deviceId);
|
|
149
142
|
}
|
|
150
|
-
// Check if it's
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
// Check if it's a simulator (will later replace all other device types as well)
|
|
144
|
+
const response = mobilecli.getDevices({
|
|
145
|
+
platform: "ios",
|
|
146
|
+
type: "simulator",
|
|
147
|
+
includeOffline: false,
|
|
148
|
+
});
|
|
149
|
+
if (response.status === "ok" && response.data && response.data.devices) {
|
|
150
|
+
for (const device of response.data.devices) {
|
|
151
|
+
if (device.id === deviceId) {
|
|
152
|
+
return new mobile_device_1.MobileDevice(deviceId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
154
155
|
}
|
|
155
|
-
throw new robot_1.ActionableError(`Device "${
|
|
156
|
+
throw new robot_1.ActionableError(`Device "${deviceId}" not found. Use the mobile_list_available_devices tool to see available devices.`);
|
|
156
157
|
};
|
|
157
|
-
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.", {
|
|
158
|
+
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.", {
|
|
158
159
|
noParams
|
|
159
160
|
}, async ({}) => {
|
|
161
|
+
// from today onward, we must have mobilecli working
|
|
162
|
+
ensureMobilecliAvailable();
|
|
160
163
|
const iosManager = new ios_1.IosManager();
|
|
161
164
|
const androidManager = new android_1.AndroidDeviceManager();
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const androidDevices = androidManager.
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let mobilecliDeviceCount = 0;
|
|
175
|
-
try {
|
|
176
|
-
const response = getMobilecliDevices();
|
|
177
|
-
if (response.status === "ok" && response.data && response.data.devices) {
|
|
178
|
-
mobilecliDeviceCount = response.data.devices.length;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
// if mobilecli fails, we'll just set count to 0
|
|
183
|
-
}
|
|
184
|
-
if (deviceCount === mobilecliDeviceCount) {
|
|
185
|
-
posthog("debug_mobilecli_same_number_of_devices", {
|
|
186
|
-
"DeviceCount": deviceCount,
|
|
187
|
-
"MobilecliDeviceCount": mobilecliDeviceCount,
|
|
188
|
-
}).then();
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
posthog("debug_mobilecli_different_number_of_devices", {
|
|
192
|
-
"DeviceCount": deviceCount,
|
|
193
|
-
"MobilecliDeviceCount": mobilecliDeviceCount,
|
|
194
|
-
"DeviceCountDifference": deviceCount - mobilecliDeviceCount,
|
|
195
|
-
}).then();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const resp = ["Found these devices:"];
|
|
199
|
-
if (simulatorNames.length > 0) {
|
|
200
|
-
resp.push(`iOS simulators: [${simulatorNames.join(",")}]`);
|
|
165
|
+
const devices = [];
|
|
166
|
+
// Get Android devices with details
|
|
167
|
+
const androidDevices = androidManager.getConnectedDevicesWithDetails();
|
|
168
|
+
for (const device of androidDevices) {
|
|
169
|
+
devices.push({
|
|
170
|
+
id: device.deviceId,
|
|
171
|
+
name: device.name,
|
|
172
|
+
platform: "android",
|
|
173
|
+
type: "emulator",
|
|
174
|
+
version: device.version,
|
|
175
|
+
state: "online",
|
|
176
|
+
});
|
|
201
177
|
}
|
|
202
|
-
|
|
203
|
-
|
|
178
|
+
// Get iOS physical devices with details
|
|
179
|
+
try {
|
|
180
|
+
const iosDevices = iosManager.listDevicesWithDetails();
|
|
181
|
+
for (const device of iosDevices) {
|
|
182
|
+
devices.push({
|
|
183
|
+
id: device.deviceId,
|
|
184
|
+
name: device.deviceName,
|
|
185
|
+
platform: "ios",
|
|
186
|
+
type: "real",
|
|
187
|
+
version: device.version,
|
|
188
|
+
state: "online",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
204
191
|
}
|
|
205
|
-
|
|
206
|
-
|
|
192
|
+
catch (error) {
|
|
193
|
+
// If go-ios is not available, silently skip
|
|
207
194
|
}
|
|
208
|
-
|
|
209
|
-
|
|
195
|
+
// Get iOS simulators from mobilecli (excluding offline devices)
|
|
196
|
+
const response = mobilecli.getDevices({
|
|
197
|
+
platform: "ios",
|
|
198
|
+
type: "simulator",
|
|
199
|
+
includeOffline: false,
|
|
200
|
+
});
|
|
201
|
+
if (response.status === "ok" && response.data && response.data.devices) {
|
|
202
|
+
for (const device of response.data.devices) {
|
|
203
|
+
devices.push({
|
|
204
|
+
id: device.id,
|
|
205
|
+
name: device.name,
|
|
206
|
+
platform: device.platform,
|
|
207
|
+
type: device.type,
|
|
208
|
+
version: device.version,
|
|
209
|
+
state: "online",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
const out = { devices };
|
|
214
|
+
return JSON.stringify(out);
|
|
212
215
|
});
|
|
213
|
-
tool("mobile_list_apps", "List all the installed apps on the device", {
|
|
216
|
+
tool("mobile_list_apps", "List Apps", "List all the installed apps on the device", {
|
|
214
217
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
215
218
|
}, async ({ device }) => {
|
|
216
219
|
const robot = getRobotFromDevice(device);
|
|
217
220
|
const result = await robot.listApps();
|
|
218
221
|
return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
|
|
219
222
|
});
|
|
220
|
-
tool("mobile_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.", {
|
|
223
|
+
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.", {
|
|
221
224
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
222
225
|
packageName: zod_1.z.string().describe("The package name of the app to launch"),
|
|
223
226
|
}, async ({ device, packageName }) => {
|
|
@@ -225,7 +228,7 @@ const createMcpServer = () => {
|
|
|
225
228
|
await robot.launchApp(packageName);
|
|
226
229
|
return `Launched app ${packageName}`;
|
|
227
230
|
});
|
|
228
|
-
tool("mobile_terminate_app", "Stop and terminate an app on mobile device", {
|
|
231
|
+
tool("mobile_terminate_app", "Terminate App", "Stop and terminate an app on mobile device", {
|
|
229
232
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
230
233
|
packageName: zod_1.z.string().describe("The package name of the app to terminate"),
|
|
231
234
|
}, async ({ device, packageName }) => {
|
|
@@ -233,7 +236,7 @@ const createMcpServer = () => {
|
|
|
233
236
|
await robot.terminateApp(packageName);
|
|
234
237
|
return `Terminated app ${packageName}`;
|
|
235
238
|
});
|
|
236
|
-
tool("mobile_install_app", "Install an app on mobile device", {
|
|
239
|
+
tool("mobile_install_app", "Install App", "Install an app on mobile device", {
|
|
237
240
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
238
241
|
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"),
|
|
239
242
|
}, async ({ device, path }) => {
|
|
@@ -241,7 +244,7 @@ const createMcpServer = () => {
|
|
|
241
244
|
await robot.installApp(path);
|
|
242
245
|
return `Installed app from ${path}`;
|
|
243
246
|
});
|
|
244
|
-
tool("mobile_uninstall_app", "Uninstall an app from mobile device", {
|
|
247
|
+
tool("mobile_uninstall_app", "Uninstall App", "Uninstall an app from mobile device", {
|
|
245
248
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
246
249
|
bundle_id: zod_1.z.string().describe("Bundle identifier (iOS) or package name (Android) of the app to be uninstalled"),
|
|
247
250
|
}, async ({ device, bundle_id }) => {
|
|
@@ -249,14 +252,14 @@ const createMcpServer = () => {
|
|
|
249
252
|
await robot.uninstallApp(bundle_id);
|
|
250
253
|
return `Uninstalled app ${bundle_id}`;
|
|
251
254
|
});
|
|
252
|
-
tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
|
|
255
|
+
tool("mobile_get_screen_size", "Get Screen Size", "Get the screen size of the mobile device in pixels", {
|
|
253
256
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
254
257
|
}, async ({ device }) => {
|
|
255
258
|
const robot = getRobotFromDevice(device);
|
|
256
259
|
const screenSize = await robot.getScreenSize();
|
|
257
260
|
return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
|
|
258
261
|
});
|
|
259
|
-
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.", {
|
|
262
|
+
tool("mobile_click_on_screen_at_coordinates", "Click Screen", "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.", {
|
|
260
263
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
261
264
|
x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
|
|
262
265
|
y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
|
|
@@ -265,7 +268,7 @@ const createMcpServer = () => {
|
|
|
265
268
|
await robot.tap(x, y);
|
|
266
269
|
return `Clicked on screen at coordinates: ${x}, ${y}`;
|
|
267
270
|
});
|
|
268
|
-
tool("mobile_double_tap_on_screen", "Double-tap on the screen at given x,y coordinates.", {
|
|
271
|
+
tool("mobile_double_tap_on_screen", "Double Tap Screen", "Double-tap on the screen at given x,y coordinates.", {
|
|
269
272
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
270
273
|
x: zod_1.z.number().describe("The x coordinate to double-tap, in pixels"),
|
|
271
274
|
y: zod_1.z.number().describe("The y coordinate to double-tap, in pixels"),
|
|
@@ -274,7 +277,7 @@ const createMcpServer = () => {
|
|
|
274
277
|
await robot.doubleTap(x, y);
|
|
275
278
|
return `Double-tapped on screen at coordinates: ${x}, ${y}`;
|
|
276
279
|
});
|
|
277
|
-
tool("mobile_long_press_on_screen_at_coordinates", "Long press on the screen at given x,y coordinates. If long pressing on an element, use the list_elements_on_screen tool to find the coordinates.", {
|
|
280
|
+
tool("mobile_long_press_on_screen_at_coordinates", "Long Press Screen", "Long press on the screen at given x,y coordinates. If long pressing on an element, use the list_elements_on_screen tool to find the coordinates.", {
|
|
278
281
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
279
282
|
x: zod_1.z.number().describe("The x coordinate to long press on the screen, in pixels"),
|
|
280
283
|
y: zod_1.z.number().describe("The y coordinate to long press on the screen, in pixels"),
|
|
@@ -283,7 +286,7 @@ const createMcpServer = () => {
|
|
|
283
286
|
await robot.longPress(x, y);
|
|
284
287
|
return `Long pressed on screen at coordinates: ${x}, ${y}`;
|
|
285
288
|
});
|
|
286
|
-
tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
|
|
289
|
+
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.", {
|
|
287
290
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
288
291
|
}, async ({ device }) => {
|
|
289
292
|
const robot = getRobotFromDevice(device);
|
|
@@ -310,7 +313,7 @@ const createMcpServer = () => {
|
|
|
310
313
|
});
|
|
311
314
|
return `Found these elements on screen: ${JSON.stringify(result)}`;
|
|
312
315
|
});
|
|
313
|
-
tool("mobile_press_button", "Press a button on device", {
|
|
316
|
+
tool("mobile_press_button", "Press Button", "Press a button on device", {
|
|
314
317
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
315
318
|
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)"),
|
|
316
319
|
}, async ({ device, button }) => {
|
|
@@ -318,7 +321,7 @@ const createMcpServer = () => {
|
|
|
318
321
|
await robot.pressButton(button);
|
|
319
322
|
return `Pressed the button: ${button}`;
|
|
320
323
|
});
|
|
321
|
-
tool("mobile_open_url", "Open a URL in browser on device", {
|
|
324
|
+
tool("mobile_open_url", "Open URL", "Open a URL in browser on device", {
|
|
322
325
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
323
326
|
url: zod_1.z.string().describe("The URL to open"),
|
|
324
327
|
}, async ({ device, url }) => {
|
|
@@ -326,7 +329,7 @@ const createMcpServer = () => {
|
|
|
326
329
|
await robot.openUrl(url);
|
|
327
330
|
return `Opened URL: ${url}`;
|
|
328
331
|
});
|
|
329
|
-
tool("mobile_swipe_on_screen", "Swipe on the screen", {
|
|
332
|
+
tool("mobile_swipe_on_screen", "Swipe Screen", "Swipe on the screen", {
|
|
330
333
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
331
334
|
direction: zod_1.z.enum(["up", "down", "left", "right"]).describe("The direction to swipe"),
|
|
332
335
|
x: zod_1.z.number().optional().describe("The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
@@ -346,7 +349,7 @@ const createMcpServer = () => {
|
|
|
346
349
|
return `Swiped ${direction} on screen`;
|
|
347
350
|
}
|
|
348
351
|
});
|
|
349
|
-
tool("mobile_type_keys", "Type text into the focused element", {
|
|
352
|
+
tool("mobile_type_keys", "Type Text", "Type text into the focused element", {
|
|
350
353
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
351
354
|
text: zod_1.z.string().describe("The text to type"),
|
|
352
355
|
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."),
|
|
@@ -358,7 +361,7 @@ const createMcpServer = () => {
|
|
|
358
361
|
}
|
|
359
362
|
return `Typed text: ${text}`;
|
|
360
363
|
});
|
|
361
|
-
tool("mobile_save_screenshot", "Save a screenshot of the mobile device to a file", {
|
|
364
|
+
tool("mobile_save_screenshot", "Save Screenshot", "Save a screenshot of the mobile device to a file", {
|
|
362
365
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
363
366
|
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
364
367
|
}, async ({ device, saveTo }) => {
|
|
@@ -367,8 +370,12 @@ const createMcpServer = () => {
|
|
|
367
370
|
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
368
371
|
return `Screenshot saved to: ${saveTo}`;
|
|
369
372
|
});
|
|
370
|
-
server.
|
|
371
|
-
|
|
373
|
+
server.registerTool("mobile_take_screenshot", {
|
|
374
|
+
title: "Take Screenshot",
|
|
375
|
+
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.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
378
|
+
}
|
|
372
379
|
}, async ({ device }) => {
|
|
373
380
|
try {
|
|
374
381
|
const robot = getRobotFromDevice(device);
|
|
@@ -413,7 +420,7 @@ const createMcpServer = () => {
|
|
|
413
420
|
};
|
|
414
421
|
}
|
|
415
422
|
});
|
|
416
|
-
tool("mobile_set_orientation", "Change the screen orientation of the device", {
|
|
423
|
+
tool("mobile_set_orientation", "Set Orientation", "Change the screen orientation of the device", {
|
|
417
424
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
418
425
|
orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
|
|
419
426
|
}, async ({ device, orientation }) => {
|
|
@@ -421,7 +428,7 @@ const createMcpServer = () => {
|
|
|
421
428
|
await robot.setOrientation(orientation);
|
|
422
429
|
return `Changed device orientation to ${orientation}`;
|
|
423
430
|
});
|
|
424
|
-
tool("mobile_get_orientation", "Get the current screen orientation of the device", {
|
|
431
|
+
tool("mobile_get_orientation", "Get Orientation", "Get the current screen orientation of the device", {
|
|
425
432
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
426
433
|
}, async ({ device }) => {
|
|
427
434
|
const robot = getRobotFromDevice(device);
|
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.38",
|
|
5
5
|
"description": "Mobile MCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -24,14 +24,15 @@
|
|
|
24
24
|
"lib"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "1.
|
|
27
|
+
"@modelcontextprotocol/sdk": "1.24.2",
|
|
28
28
|
"commander": "14.0.0",
|
|
29
29
|
"express": "5.1.0",
|
|
30
30
|
"fast-xml-parser": "5.2.5",
|
|
31
|
-
"zod
|
|
31
|
+
"zod": "^4.1.13",
|
|
32
|
+
"zod-to-json-schema": "3.25.0"
|
|
32
33
|
},
|
|
33
34
|
"optionalDependencies": {
|
|
34
|
-
"@mobilenext/mobilecli": "0.0.
|
|
35
|
+
"@mobilenext/mobilecli": "0.0.46"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@eslint/eslintrc": "^3.2.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"mocha": "^11.1.0",
|
|
53
54
|
"nyc": "^17.1.0",
|
|
54
55
|
"ts-node": "^10.9.2",
|
|
55
|
-
"typescript": "
|
|
56
|
+
"typescript": "5.8.2"
|
|
56
57
|
},
|
|
57
58
|
"main": "index.js",
|
|
58
59
|
"bin": {
|