@mobilenext/mobile-mcp 0.0.30-beta → 0.0.30

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
@@ -83,7 +83,7 @@ More details in our [wiki page](https://github.com/mobile-next/mobile-mcp/wiki)
83
83
 
84
84
  ## Installation and configuration
85
85
 
86
- Setup our MCP with Cline, Cursor, Claude, VS Code, Github Copilot:
86
+ **Standard config** works in most of the tools:
87
87
 
88
88
  ```json
89
89
  {
@@ -94,23 +94,74 @@ Setup our MCP with Cline, Cursor, Claude, VS Code, Github Copilot:
94
94
  }
95
95
  }
96
96
  }
97
-
98
97
  ```
99
- [Cline:](https://docs.cline.bot/mcp/configuring-mcp-servers) To setup Cline, just add the json above to your MCP settings file.
98
+
99
+ <details>
100
+ <summary>Cline</summary>
101
+
102
+ To setup Cline, just add the json above to your MCP settings file.
103
+
100
104
  [More in our wiki](https://github.com/mobile-next/mobile-mcp/wiki/Cline)
101
105
 
102
- [Claude Code:](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
106
+ </details>
103
107
 
104
- ```
105
- claude mcp add mobile -- npx -y @mobilenext/mobile-mcp@latest
106
- ```
108
+ <details>
109
+ <summary>Claude Code</summary>
107
110
 
108
- [Gemini CLI:](https://cloud.google.com/gemini/docs/codeassist/gemini-cli)
111
+ Use the Claude Code CLI to add the Mobile MCP server:
109
112
 
113
+ ```bash
114
+ claude mcp add mobile-mcp -- npx -y @mobilenext/mobile-mcp@latest
110
115
  ```
111
- gemini mcp add mobile npx -y @mobilenext/mobile-mcp@latest
116
+
117
+ </details>
118
+
119
+ <details>
120
+ <summary>Cursor</summary>
121
+
122
+ #### Click the button to install:
123
+
124
+ [<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=Mobile%20MCP&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtb2JpbGVuZXh0L21vYmlsZS1tY3BAbGF0ZXN0Il19)
125
+
126
+ #### Or install manually:
127
+
128
+ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx -y @mobilenext/mobile-mcp@latest`. You can also verify config or add command like arguments via clicking `Edit`.
129
+
130
+ </details>
131
+
132
+ <details>
133
+ <summary>Gemini CLI</summary>
134
+
135
+ Use the Gemini CLI to add the Mobile MCP server:
136
+
137
+ ```bash
138
+ gemini mcp add mobile-mcp npx -y @mobilenext/mobile-mcp@latest
112
139
  ```
113
140
 
141
+ </details>
142
+
143
+ <details>
144
+ <summary>Goose</summary>
145
+
146
+ #### Click the button to install:
147
+
148
+ [![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=-y&arg=%40mobilenext%2Fmobile-mcp%40latest&id=mobile-mcp&name=Mobile%20MCP&description=Mobile%20automation%20and%20development%20for%20iOS%2C%20Android%2C%20simulators%2C%20emulators%2C%20and%20real%20devices)
149
+
150
+ #### Or install manually:
151
+
152
+ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx -y @mobilenext/mobile-mcp@latest`. Click "Add Extension".
153
+
154
+ </details>
155
+
156
+ <details>
157
+ <summary>Qodo Gen</summary>
158
+
159
+ Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the standard config above.
160
+
161
+ Click <code>Save</code>.
162
+
163
+ </details>
164
+
114
165
  [Read more in our wiki](https://github.com/mobile-next/mobile-mcp/wiki)! 🚀
115
166
 
116
167
 
package/lib/android.js CHANGED
@@ -273,6 +273,28 @@ class AndroidRobot {
273
273
  async terminateApp(packageName) {
274
274
  this.adb("shell", "am", "force-stop", packageName);
275
275
  }
276
+ async installApp(path) {
277
+ try {
278
+ this.adb("install", "-r", path);
279
+ }
280
+ catch (error) {
281
+ const stdout = error.stdout ? error.stdout.toString() : "";
282
+ const stderr = error.stderr ? error.stderr.toString() : "";
283
+ const output = (stdout + stderr).trim();
284
+ throw new robot_1.ActionableError(output || error.message);
285
+ }
286
+ }
287
+ async uninstallApp(bundleId) {
288
+ try {
289
+ this.adb("uninstall", bundleId);
290
+ }
291
+ catch (error) {
292
+ const stdout = error.stdout ? error.stdout.toString() : "";
293
+ const stderr = error.stderr ? error.stderr.toString() : "";
294
+ const output = (stdout + stderr).trim();
295
+ throw new robot_1.ActionableError(output || error.message);
296
+ }
297
+ }
276
298
  async openUrl(url) {
277
299
  this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
278
300
  }
package/lib/ios.js CHANGED
@@ -101,6 +101,30 @@ class IosRobot {
101
101
  await this.assertTunnelRunning();
102
102
  await this.ios("kill", packageName);
103
103
  }
104
+ async installApp(path) {
105
+ await this.assertTunnelRunning();
106
+ try {
107
+ await this.ios("install", "--path", path);
108
+ }
109
+ catch (error) {
110
+ const stdout = error.stdout ? error.stdout.toString() : "";
111
+ const stderr = error.stderr ? error.stderr.toString() : "";
112
+ const output = (stdout + stderr).trim();
113
+ throw new robot_1.ActionableError(output || error.message);
114
+ }
115
+ }
116
+ async uninstallApp(bundleId) {
117
+ await this.assertTunnelRunning();
118
+ try {
119
+ await this.ios("uninstall", "--bundleid", bundleId);
120
+ }
121
+ catch (error) {
122
+ const stdout = error.stdout ? error.stdout.toString() : "";
123
+ const stderr = error.stderr ? error.stderr.toString() : "";
124
+ const output = (stdout + stderr).trim();
125
+ throw new robot_1.ActionableError(output || error.message);
126
+ }
127
+ }
104
128
  async openUrl(url) {
105
129
  const wda = await this.wda();
106
130
  await wda.openUrl(url);
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SimctlManager = exports.Simctl = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
5
8
  const logger_1 = require("./logger");
6
9
  const webdriver_agent_1 = require("./webdriver-agent");
7
10
  const robot_1 = require("./robot");
@@ -73,6 +76,87 @@ class Simctl {
73
76
  async terminateApp(packageName) {
74
77
  this.simctl("terminate", this.simulatorUuid, packageName);
75
78
  }
79
+ findAppBundle(dir) {
80
+ const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
81
+ for (const entry of entries) {
82
+ if (entry.isDirectory() && entry.name.endsWith(".app")) {
83
+ return (0, node_path_1.join)(dir, entry.name);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ validateZipPaths(zipPath) {
89
+ const output = (0, node_child_process_1.execFileSync)("/usr/bin/zipinfo", ["-1", zipPath], {
90
+ timeout: TIMEOUT,
91
+ maxBuffer: MAX_BUFFER_SIZE,
92
+ }).toString();
93
+ const invalidPath = output
94
+ .split("\n")
95
+ .map(s => s.trim())
96
+ .filter(s => s)
97
+ .find(s => s.startsWith("/") || s.includes(".."));
98
+ if (invalidPath) {
99
+ throw new robot_1.ActionableError(`Security violation: File path '${invalidPath}' contains invalid characters`);
100
+ }
101
+ }
102
+ async installApp(path) {
103
+ let tempDir = null;
104
+ let installPath = path;
105
+ try {
106
+ // zip files need to be extracted prior to installation
107
+ if ((0, node_path_1.extname)(path).toLowerCase() === ".zip") {
108
+ (0, logger_1.trace)(`Detected .zip file, validating contents`);
109
+ // before extracting, let's make sure there's no zip-slip bombs here
110
+ this.validateZipPaths(path);
111
+ tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "ios-app-"));
112
+ try {
113
+ (0, node_child_process_1.execFileSync)("unzip", ["-q", path, "-d", tempDir], {
114
+ timeout: TIMEOUT,
115
+ });
116
+ }
117
+ catch (error) {
118
+ throw new robot_1.ActionableError(`Failed to unzip file: ${error.message}`);
119
+ }
120
+ const appBundle = this.findAppBundle(tempDir);
121
+ if (!appBundle) {
122
+ throw new robot_1.ActionableError("No .app bundle found in the .zip file, please visit wiki at https://github.com/mobile-next/mobile-mcp/wiki for assistance.");
123
+ }
124
+ installPath = appBundle;
125
+ (0, logger_1.trace)(`Found .app bundle at: ${(0, node_path_1.basename)(appBundle)}`);
126
+ }
127
+ // continue with installation
128
+ this.simctl("install", this.simulatorUuid, installPath);
129
+ }
130
+ catch (error) {
131
+ const stdout = error.stdout ? error.stdout.toString() : "";
132
+ const stderr = error.stderr ? error.stderr.toString() : "";
133
+ const output = (stdout + stderr).trim();
134
+ throw new robot_1.ActionableError(output || error.message);
135
+ }
136
+ finally {
137
+ // Clean up temporary directory if it was created
138
+ if (tempDir) {
139
+ try {
140
+ (0, logger_1.trace)(`Cleaning up temporary directory`);
141
+ (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
142
+ }
143
+ catch (cleanupError) {
144
+ (0, logger_1.trace)(`Warning: Failed to cleanup temporary directory: ${cleanupError}`);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ async uninstallApp(bundleId) {
150
+ try {
151
+ this.simctl("uninstall", this.simulatorUuid, bundleId);
152
+ }
153
+ catch (error) {
154
+ const stdout = error.stdout ? error.stdout.toString() : "";
155
+ const stderr = error.stderr ? error.stderr.toString() : "";
156
+ const output = (stdout + stderr).trim();
157
+ throw new robot_1.ActionableError(output || error.message);
158
+ }
159
+ }
76
160
  async listApps() {
77
161
  const text = this.simctl("listapps", this.simulatorUuid).toString();
78
162
  const result = (0, node_child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
package/lib/server.js CHANGED
@@ -34,8 +34,6 @@ const createMcpServer = () => {
34
34
  });
35
35
  // an empty object to satisfy windsurf
36
36
  const noParams = zod_1.z.object({});
37
- // will be replaced later by 'initialize' jsonrpc request
38
- let clientName = "unknown";
39
37
  const tool = (name, description, paramsSchema, cb) => {
40
38
  const wrappedCb = async (args) => {
41
39
  try {
@@ -76,7 +74,6 @@ const createMcpServer = () => {
76
74
  Product: "mobile-mcp",
77
75
  Version: (0, exports.getAgentVersion)(),
78
76
  NodeVersion: process.version,
79
- AgentName: clientName,
80
77
  };
81
78
  await fetch(url, {
82
79
  method: "POST",
@@ -98,20 +95,21 @@ const createMcpServer = () => {
98
95
  // ignore
99
96
  }
100
97
  };
101
- const reportMobilecliVersion = async () => {
98
+ const getMobilecliVersion = () => {
102
99
  try {
103
100
  const path = (0, mobilecli_1.getMobilecliPath)();
104
101
  const output = (0, node_child_process_1.execFileSync)(path, ["--version"], { encoding: "utf8" }).toString().trim();
105
- const version = output.startsWith("mobilecli version ")
106
- ? output.split(" ").pop()
107
- : output;
108
- await posthog("mobilecli_check", { MobilecliVersion: version || output });
102
+ if (output.startsWith("mobilecli version ")) {
103
+ return output.substring("mobilecli version ".length);
104
+ }
105
+ return "failed";
109
106
  }
110
107
  catch (error) {
111
- await posthog("mobilecli_check", { MobilecliVersion: "failed" });
108
+ return "failed " + error.message;
112
109
  }
113
110
  };
114
- posthog("launch", {}).then();
111
+ const mobilecliVersion = getMobilecliVersion();
112
+ posthog("launch", { "MobilecliVersion": mobilecliVersion }).then();
115
113
  const simulatorManager = new iphone_simulator_1.SimctlManager();
116
114
  const getRobotFromDevice = (device) => {
117
115
  const iosManager = new ios_1.IosManager();
@@ -186,6 +184,22 @@ const createMcpServer = () => {
186
184
  await robot.terminateApp(packageName);
187
185
  return `Terminated app ${packageName}`;
188
186
  });
187
+ tool("mobile_install_app", "Install an app on mobile device", {
188
+ device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
189
+ 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"),
190
+ }, async ({ device, path }) => {
191
+ const robot = getRobotFromDevice(device);
192
+ await robot.installApp(path);
193
+ return `Installed app from ${path}`;
194
+ });
195
+ tool("mobile_uninstall_app", "Uninstall an app from mobile device", {
196
+ device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
197
+ bundle_id: zod_1.z.string().describe("Bundle identifier (iOS) or package name (Android) of the app to be uninstalled"),
198
+ }, async ({ device, bundle_id }) => {
199
+ const robot = getRobotFromDevice(device);
200
+ await robot.uninstallApp(bundle_id);
201
+ return `Uninstalled app ${bundle_id}`;
202
+ });
189
203
  tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
190
204
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
191
205
  }, async ({ device }) => {
@@ -349,21 +363,6 @@ const createMcpServer = () => {
349
363
  const orientation = await robot.getOrientation();
350
364
  return `Current device orientation is ${orientation}`;
351
365
  });
352
- // async report mobilecli version
353
- reportMobilecliVersion().then();
354
- const hook = server.connect;
355
- server.connect = (transport) => {
356
- transport.onmessage = (message) => {
357
- if ("method" in message) {
358
- const request = message;
359
- if (request.method === "initialize") {
360
- const initialize = request;
361
- clientName = initialize.params.clientInfo.name || "unknown";
362
- }
363
- }
364
- };
365
- return hook.apply(server, [transport]);
366
- };
367
366
  return server;
368
367
  };
369
368
  exports.createMcpServer = createMcpServer;
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.30-beta",
4
+ "version": "0.0.30",
5
5
  "description": "Mobile MCP",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,8 +28,10 @@
28
28
  "commander": "14.0.0",
29
29
  "express": "5.1.0",
30
30
  "fast-xml-parser": "5.2.5",
31
- "zod-to-json-schema": "3.24.6",
32
- "@mobilenext/mobilecli": "0.0.25"
31
+ "zod-to-json-schema": "3.24.6"
32
+ },
33
+ "optionalDependencies": {
34
+ "@mobilenext/mobilecli": "0.0.27"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@eslint/eslintrc": "^3.2.0",