@mobilenext/mobile-mcp 0.0.37 → 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 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;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SimctlManager = exports.Simctl = void 0;
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.getMobilecliPath = void 0;
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 getMobilecliPath = () => {
7
- if (process.env.MOBILECLI_PATH) {
8
- return process.env.MOBILECLI_PATH;
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
- const arch = process.arch;
11
- const platform = process.platform;
12
- let binaryName = "mobilecli";
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
- // Check if mobile-mcp is installed as a package
37
- const currentPath = __filename;
38
- const pathParts = currentPath.split(node_path_1.sep);
39
- const lastNodeModulesIndex = pathParts.lastIndexOf("node_modules");
40
- if (lastNodeModulesIndex !== -1) {
41
- // We're inside node_modules, go to the last node_modules in the path
42
- const nodeModulesParts = pathParts.slice(0, lastNodeModulesIndex + 1);
43
- const lastNodeModulesPath = nodeModulesParts.join(node_path_1.sep);
44
- const mobilecliPath = (0, node_path_1.join)(lastNodeModulesPath, "@mobilenext", "mobilecli", "bin", binaryName);
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
- // Not in node_modules, look one directory up from current script
50
- const scriptDir = (0, node_path_1.dirname)(__filename);
51
- const parentDir = (0, node_path_1.dirname)(scriptDir);
52
- const mobilecliPath = (0, node_path_1.join)(parentDir, "node_modules", "@mobilenext", "mobilecli", "bin", binaryName);
53
- if ((0, node_fs_1.existsSync)(mobilecliPath)) {
54
- return mobilecliPath;
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
- throw new Error(`Could not find mobilecli binary for platform: ${platform}`);
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;
@@ -48,9 +47,11 @@ const createMcpServer = () => {
48
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
  };
@@ -109,105 +110,108 @@ const createMcpServer = () => {
109
110
  // ignore
110
111
  }
111
112
  };
112
- const getMobilecliVersion = () => {
113
+ const mobilecli = new mobilecli_1.Mobilecli();
114
+ posthog("launch", {}).then();
115
+ const ensureMobilecliAvailable = () => {
113
116
  try {
114
- const path = (0, mobilecli_1.getMobilecliPath)();
115
- const output = (0, node_child_process_1.execFileSync)(path, ["--version"], { encoding: "utf8" }).toString().trim();
116
- if (output.startsWith("mobilecli version ")) {
117
- return output.substring("mobilecli version ".length);
117
+ const version = mobilecli.getVersion();
118
+ if (version.startsWith("failed")) {
119
+ throw new Error("mobilecli version check failed");
118
120
  }
119
- return "failed";
120
121
  }
121
122
  catch (error) {
122
- return "failed " + error.message;
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`);
123
124
  }
124
125
  };
125
- const getMobilecliDevices = () => {
126
- const mobilecliPath = (0, mobilecli_1.getMobilecliPath)();
127
- const mobilecliOutput = (0, node_child_process_1.execFileSync)(mobilecliPath, ["devices"], { encoding: "utf8" }).toString().trim();
128
- return JSON.parse(mobilecliOutput);
129
- };
130
- const mobilecliVersion = getMobilecliVersion();
131
- posthog("launch", { "MobilecliVersion": mobilecliVersion }).then();
132
- const simulatorManager = new iphone_simulator_1.SimctlManager();
133
- 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
134
130
  const iosManager = new ios_1.IosManager();
135
- const androidManager = new android_1.AndroidDeviceManager();
136
- const simulators = simulatorManager.listBootedSimulators();
137
- const androidDevices = androidManager.getConnectedDevices();
138
131
  const iosDevices = iosManager.listDevices();
139
- // Check if it's a simulator
140
- const simulator = simulators.find(s => s.name === device);
141
- if (simulator) {
142
- return simulatorManager.getSimulator(device);
132
+ const iosDevice = iosDevices.find(d => d.deviceId === deviceId);
133
+ if (iosDevice) {
134
+ return new ios_1.IosRobot(deviceId);
143
135
  }
144
136
  // Check if it's an Android device
145
- const androidDevice = androidDevices.find(d => d.deviceId === device);
137
+ const androidManager = new android_1.AndroidDeviceManager();
138
+ const androidDevices = androidManager.getConnectedDevices();
139
+ const androidDevice = androidDevices.find(d => d.deviceId === deviceId);
146
140
  if (androidDevice) {
147
- return new android_1.AndroidRobot(device);
141
+ return new android_1.AndroidRobot(deviceId);
148
142
  }
149
- // Check if it's an iOS device
150
- const iosDevice = iosDevices.find(d => d.deviceId === device);
151
- if (iosDevice) {
152
- return new ios_1.IosRobot(device);
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
+ }
153
155
  }
154
- throw new robot_1.ActionableError(`Device "${device}" not found. Use the mobile_list_available_devices tool to see available devices.`);
156
+ throw new robot_1.ActionableError(`Device "${deviceId}" not found. Use the mobile_list_available_devices tool to see available devices.`);
155
157
  };
156
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.", {
157
159
  noParams
158
160
  }, async ({}) => {
161
+ // from today onward, we must have mobilecli working
162
+ ensureMobilecliAvailable();
159
163
  const iosManager = new ios_1.IosManager();
160
164
  const androidManager = new android_1.AndroidDeviceManager();
161
- const simulators = simulatorManager.listBootedSimulators();
162
- const simulatorNames = simulators.map(d => d.name);
163
- const androidDevices = androidManager.getConnectedDevices();
164
- const iosDevices = await iosManager.listDevices();
165
- const iosDeviceNames = iosDevices.map(d => d.deviceId);
166
- const androidTvDevices = androidDevices.filter(d => d.deviceType === "tv").map(d => d.deviceId);
167
- const androidMobileDevices = androidDevices.filter(d => d.deviceType === "mobile").map(d => d.deviceId);
168
- if (true) {
169
- // gilm: this is new code to verify first that mobilecli detects more or equal number of devices.
170
- // in an attempt to make the smoothest transition from go-ios+xcrun+adb+iproxy+sips+imagemagick+wda to
171
- // a single cli tool.
172
- const deviceCount = simulators.length + iosDevices.length + androidDevices.length;
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(",")}]`);
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
+ });
200
177
  }
201
- if (iosDevices.length > 0) {
202
- resp.push(`iOS devices: [${iosDeviceNames.join(",")}]`);
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
+ }
203
191
  }
204
- if (androidMobileDevices.length > 0) {
205
- resp.push(`Android devices: [${androidMobileDevices.join(",")}]`);
192
+ catch (error) {
193
+ // If go-ios is not available, silently skip
206
194
  }
207
- if (androidTvDevices.length > 0) {
208
- resp.push(`Android TV devices: [${androidTvDevices.join(",")}]`);
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
+ }
209
212
  }
210
- return resp.join("\n");
213
+ const out = { devices };
214
+ return JSON.stringify(out);
211
215
  });
212
216
  tool("mobile_list_apps", "List Apps", "List all the installed apps on the device", {
213
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.")
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.37",
4
+ "version": "0.0.38",
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.38"
35
+ "@mobilenext/mobilecli": "0.0.46"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/eslintrc": "^3.2.0",