@mobilenext/mobile-mcp 0.0.30 → 0.0.31-beta
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 +9 -60
- package/lib/android.js +0 -22
- package/lib/ios.js +0 -24
- package/lib/iphone-simulator.js +0 -84
- package/lib/server.js +32 -24
- package/package.json +3 -5
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
|
-
|
|
86
|
+
Setup our MCP with Cline, Cursor, Claude, VS Code, Github Copilot:
|
|
87
87
|
|
|
88
88
|
```json
|
|
89
89
|
{
|
|
@@ -94,73 +94,22 @@ More details in our [wiki page](https://github.com/mobile-next/mobile-mcp/wiki)
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
<details>
|
|
100
|
-
<summary>Cline</summary>
|
|
101
|
-
|
|
102
|
-
To setup Cline, just add the json above to your MCP settings file.
|
|
103
97
|
|
|
98
|
+
```
|
|
99
|
+
[Cline:](https://docs.cline.bot/mcp/configuring-mcp-servers) To setup Cline, just add the json above to your MCP settings file.
|
|
104
100
|
[More in our wiki](https://github.com/mobile-next/mobile-mcp/wiki/Cline)
|
|
105
101
|
|
|
106
|
-
|
|
102
|
+
[Claude Code:](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
|
|
107
103
|
|
|
108
|
-
<details>
|
|
109
|
-
<summary>Claude Code</summary>
|
|
110
|
-
|
|
111
|
-
Use the Claude Code CLI to add the Mobile MCP server:
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
claude mcp add mobile-mcp -- npx -y @mobilenext/mobile-mcp@latest
|
|
115
104
|
```
|
|
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
|
|
105
|
+
claude mcp add mobile -- npx -y @mobilenext/mobile-mcp@latest
|
|
139
106
|
```
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<details>
|
|
144
|
-
<summary>Goose</summary>
|
|
145
|
-
|
|
146
|
-
#### Click the button to install:
|
|
147
|
-
|
|
148
|
-
[](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".
|
|
108
|
+
[Gemini CLI:](https://cloud.google.com/gemini/docs/codeassist/gemini-cli)
|
|
153
109
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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>
|
|
110
|
+
```
|
|
111
|
+
gemini mcp add mobile npx -y @mobilenext/mobile-mcp@latest
|
|
112
|
+
```
|
|
164
113
|
|
|
165
114
|
[Read more in our wiki](https://github.com/mobile-next/mobile-mcp/wiki)! 🚀
|
|
166
115
|
|
package/lib/android.js
CHANGED
|
@@ -273,28 +273,6 @@ 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
|
-
}
|
|
298
276
|
async openUrl(url) {
|
|
299
277
|
this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
|
|
300
278
|
}
|
package/lib/ios.js
CHANGED
|
@@ -101,30 +101,6 @@ 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
|
-
}
|
|
128
104
|
async openUrl(url) {
|
|
129
105
|
const wda = await this.wda();
|
|
130
106
|
await wda.openUrl(url);
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
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");
|
|
8
5
|
const logger_1 = require("./logger");
|
|
9
6
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
10
7
|
const robot_1 = require("./robot");
|
|
@@ -76,87 +73,6 @@ class Simctl {
|
|
|
76
73
|
async terminateApp(packageName) {
|
|
77
74
|
this.simctl("terminate", this.simulatorUuid, packageName);
|
|
78
75
|
}
|
|
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
|
-
}
|
|
160
76
|
async listApps() {
|
|
161
77
|
const text = this.simctl("listapps", this.simulatorUuid).toString();
|
|
162
78
|
const result = (0, node_child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
|
package/lib/server.js
CHANGED
|
@@ -34,6 +34,8 @@ 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";
|
|
37
39
|
const tool = (name, description, paramsSchema, cb) => {
|
|
38
40
|
const wrappedCb = async (args) => {
|
|
39
41
|
try {
|
|
@@ -74,6 +76,7 @@ const createMcpServer = () => {
|
|
|
74
76
|
Product: "mobile-mcp",
|
|
75
77
|
Version: (0, exports.getAgentVersion)(),
|
|
76
78
|
NodeVersion: process.version,
|
|
79
|
+
AgentName: clientName,
|
|
77
80
|
};
|
|
78
81
|
await fetch(url, {
|
|
79
82
|
method: "POST",
|
|
@@ -95,21 +98,20 @@ const createMcpServer = () => {
|
|
|
95
98
|
// ignore
|
|
96
99
|
}
|
|
97
100
|
};
|
|
98
|
-
const
|
|
101
|
+
const reportMobilecliVersion = async () => {
|
|
99
102
|
try {
|
|
100
103
|
const path = (0, mobilecli_1.getMobilecliPath)();
|
|
101
104
|
const output = (0, node_child_process_1.execFileSync)(path, ["--version"], { encoding: "utf8" }).toString().trim();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const version = output.startsWith("mobilecli version ")
|
|
106
|
+
? output.split(" ").pop()
|
|
107
|
+
: output;
|
|
108
|
+
await posthog("mobilecli_check", { MobilecliVersion: version || output });
|
|
106
109
|
}
|
|
107
110
|
catch (error) {
|
|
108
|
-
|
|
111
|
+
await posthog("mobilecli_check", { MobilecliVersion: "failed" });
|
|
109
112
|
}
|
|
110
113
|
};
|
|
111
|
-
|
|
112
|
-
posthog("launch", { "MobilecliVersion": mobilecliVersion }).then();
|
|
114
|
+
posthog("launch", {}).then();
|
|
113
115
|
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
114
116
|
const getRobotFromDevice = (device) => {
|
|
115
117
|
const iosManager = new ios_1.IosManager();
|
|
@@ -184,22 +186,6 @@ const createMcpServer = () => {
|
|
|
184
186
|
await robot.terminateApp(packageName);
|
|
185
187
|
return `Terminated app ${packageName}`;
|
|
186
188
|
});
|
|
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
|
-
});
|
|
203
189
|
tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
|
|
204
190
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
205
191
|
}, async ({ device }) => {
|
|
@@ -363,6 +349,28 @@ const createMcpServer = () => {
|
|
|
363
349
|
const orientation = await robot.getOrientation();
|
|
364
350
|
return `Current device orientation is ${orientation}`;
|
|
365
351
|
});
|
|
352
|
+
tool("mobile_test_mobilecli", "Test mobilecli integration and report status", {
|
|
353
|
+
noParams,
|
|
354
|
+
}, async () => {
|
|
355
|
+
const path = (0, mobilecli_1.getMobilecliPath)();
|
|
356
|
+
const output = (0, node_child_process_1.execFileSync)(path, ["devices"], { encoding: "utf8" }).toString().trim();
|
|
357
|
+
return `Here are the available devices: ${output}`;
|
|
358
|
+
});
|
|
359
|
+
// async report mobilecli version
|
|
360
|
+
reportMobilecliVersion().then();
|
|
361
|
+
const hook = server.connect;
|
|
362
|
+
server.connect = (transport) => {
|
|
363
|
+
transport.onmessage = (message) => {
|
|
364
|
+
if ("method" in message) {
|
|
365
|
+
const request = message;
|
|
366
|
+
if (request.method === "initialize") {
|
|
367
|
+
const initialize = request;
|
|
368
|
+
clientName = initialize.params.clientInfo.name || "unknown";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return hook.apply(server, [transport]);
|
|
373
|
+
};
|
|
366
374
|
return server;
|
|
367
375
|
};
|
|
368
376
|
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.
|
|
4
|
+
"version": "0.0.31-beta",
|
|
5
5
|
"description": "Mobile MCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,10 +28,8 @@
|
|
|
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
|
-
|
|
33
|
-
"optionalDependencies": {
|
|
34
|
-
"@mobilenext/mobilecli": "0.0.27"
|
|
31
|
+
"zod-to-json-schema": "3.24.6",
|
|
32
|
+
"@mobilenext/mobilecli": "0.0.25"
|
|
35
33
|
},
|
|
36
34
|
"devDependencies": {
|
|
37
35
|
"@eslint/eslintrc": "^3.2.0",
|