@mobilenext/mobile-mcp 0.0.44 → 0.0.46
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/lib/android.js +4 -1
- package/lib/ios.js +3 -0
- package/lib/iphone-simulator.js +3 -0
- package/lib/mobilecli.js +6 -0
- package/lib/server.js +59 -8
- package/lib/utils.js +9 -0
- package/package.json +3 -3
package/lib/android.js
CHANGED
|
@@ -42,6 +42,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
42
42
|
const node_fs_1 = require("node:fs");
|
|
43
43
|
const xml = __importStar(require("fast-xml-parser"));
|
|
44
44
|
const robot_1 = require("./robot");
|
|
45
|
+
const utils_1 = require("./utils");
|
|
45
46
|
const getAdbPath = () => {
|
|
46
47
|
const exeName = process.env.platform === "win32" ? "adb.exe" : "adb";
|
|
47
48
|
if (process.env.ANDROID_HOME) {
|
|
@@ -137,6 +138,7 @@ class AndroidRobot {
|
|
|
137
138
|
.map(line => line.substring("package:".length));
|
|
138
139
|
}
|
|
139
140
|
async launchApp(packageName) {
|
|
141
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
140
142
|
try {
|
|
141
143
|
this.silentAdb("shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1");
|
|
142
144
|
}
|
|
@@ -326,6 +328,7 @@ class AndroidRobot {
|
|
|
326
328
|
return elements;
|
|
327
329
|
}
|
|
328
330
|
async terminateApp(packageName) {
|
|
331
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
329
332
|
this.adb("shell", "am", "force-stop", packageName);
|
|
330
333
|
}
|
|
331
334
|
async installApp(path) {
|
|
@@ -351,7 +354,7 @@ class AndroidRobot {
|
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
356
|
async openUrl(url) {
|
|
354
|
-
this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
|
|
357
|
+
this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", this.escapeShellText(url));
|
|
355
358
|
}
|
|
356
359
|
isAscii(text) {
|
|
357
360
|
return /^[\x00-\x7F]*$/.test(text);
|
package/lib/ios.js
CHANGED
|
@@ -5,6 +5,7 @@ const node_net_1 = require("node:net");
|
|
|
5
5
|
const node_child_process_1 = require("node:child_process");
|
|
6
6
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
7
7
|
const robot_1 = require("./robot");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
8
9
|
const WDA_PORT = 8100;
|
|
9
10
|
const IOS_TUNNEL_PORT = 60105;
|
|
10
11
|
const getGoIosPath = () => {
|
|
@@ -94,10 +95,12 @@ class IosRobot {
|
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
async launchApp(packageName) {
|
|
98
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
97
99
|
await this.assertTunnelRunning();
|
|
98
100
|
await this.ios("launch", packageName);
|
|
99
101
|
}
|
|
100
102
|
async terminateApp(packageName) {
|
|
103
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
101
104
|
await this.assertTunnelRunning();
|
|
102
105
|
await this.ios("kill", packageName);
|
|
103
106
|
}
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -8,6 +8,7 @@ const node_path_1 = require("node:path");
|
|
|
8
8
|
const logger_1 = require("./logger");
|
|
9
9
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
10
10
|
const robot_1 = require("./robot");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
11
12
|
const TIMEOUT = 30000;
|
|
12
13
|
const WDA_PORT = 8100;
|
|
13
14
|
const MAX_BUFFER_SIZE = 1024 * 1024 * 8;
|
|
@@ -71,9 +72,11 @@ class Simctl {
|
|
|
71
72
|
// alternative: this.simctl("openurl", this.simulatorUuid, url);
|
|
72
73
|
}
|
|
73
74
|
async launchApp(packageName) {
|
|
75
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
74
76
|
this.simctl("launch", this.simulatorUuid, packageName);
|
|
75
77
|
}
|
|
76
78
|
async terminateApp(packageName) {
|
|
79
|
+
(0, utils_1.validatePackageName)(packageName);
|
|
77
80
|
this.simctl("terminate", this.simulatorUuid, packageName);
|
|
78
81
|
}
|
|
79
82
|
findAppBundle(dir) {
|
package/lib/mobilecli.js
CHANGED
|
@@ -19,6 +19,12 @@ class Mobilecli {
|
|
|
19
19
|
const path = this.getPath();
|
|
20
20
|
return (0, node_child_process_1.execFileSync)(path, args, { encoding: "utf8" }).toString().trim();
|
|
21
21
|
}
|
|
22
|
+
spawnCommand(args) {
|
|
23
|
+
const binaryPath = this.getPath();
|
|
24
|
+
return (0, node_child_process_1.spawn)(binaryPath, args, {
|
|
25
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
22
28
|
executeCommandBuffer(args) {
|
|
23
29
|
const path = this.getPath();
|
|
24
30
|
return (0, node_child_process_1.execFileSync)(path, args, {
|
package/lib/server.js
CHANGED
|
@@ -8,6 +8,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
8
8
|
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
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
12
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
13
|
const logger_1 = require("./logger");
|
|
13
14
|
const android_1 = require("./android");
|
|
@@ -27,8 +28,6 @@ const createMcpServer = () => {
|
|
|
27
28
|
name: "mobile-mcp",
|
|
28
29
|
version: (0, exports.getAgentVersion)(),
|
|
29
30
|
});
|
|
30
|
-
// an empty object to satisfy windsurf
|
|
31
|
-
const noParams = zod_1.z.object({});
|
|
32
31
|
const getClientName = () => {
|
|
33
32
|
try {
|
|
34
33
|
const clientInfo = server.server.getClientVersion();
|
|
@@ -112,6 +111,7 @@ const createMcpServer = () => {
|
|
|
112
111
|
}
|
|
113
112
|
};
|
|
114
113
|
const mobilecli = new mobilecli_1.Mobilecli();
|
|
114
|
+
const activeRecordings = new Map();
|
|
115
115
|
posthog("launch", {}).then();
|
|
116
116
|
const ensureMobilecliAvailable = () => {
|
|
117
117
|
try {
|
|
@@ -156,9 +156,7 @@ const createMcpServer = () => {
|
|
|
156
156
|
}
|
|
157
157
|
throw new robot_1.ActionableError(`Device "${deviceId}" not found. Use the mobile_list_available_devices tool to see available devices.`);
|
|
158
158
|
};
|
|
159
|
-
tool("mobile_list_available_devices", "List Devices", "List all available devices. This includes both physical devices and simulators.
|
|
160
|
-
noParams
|
|
161
|
-
}, { readOnlyHint: true }, async ({}) => {
|
|
159
|
+
tool("mobile_list_available_devices", "List Devices", "List all available devices. This includes both physical mobile devices and mobile simulators and emulators. It returns both Android and iOS devices.", {}, { readOnlyHint: true }, async ({}) => {
|
|
162
160
|
// from today onward, we must have mobilecli working
|
|
163
161
|
ensureMobilecliAvailable();
|
|
164
162
|
const iosManager = new ios_1.IosManager();
|
|
@@ -215,9 +213,7 @@ const createMcpServer = () => {
|
|
|
215
213
|
return JSON.stringify(out);
|
|
216
214
|
});
|
|
217
215
|
if (process.env.MOBILEFLEET_ENABLE === "1") {
|
|
218
|
-
tool("mobile_list_fleet_devices", "List Fleet Devices", "List devices available in the remote fleet", {
|
|
219
|
-
noParams
|
|
220
|
-
}, { readOnlyHint: true }, async ({}) => {
|
|
216
|
+
tool("mobile_list_fleet_devices", "List Fleet Devices", "List devices available in the remote fleet", {}, { readOnlyHint: true }, async ({}) => {
|
|
221
217
|
ensureMobilecliAvailable();
|
|
222
218
|
const result = mobilecli.fleetListDevices();
|
|
223
219
|
return result;
|
|
@@ -464,6 +460,61 @@ const createMcpServer = () => {
|
|
|
464
460
|
const orientation = await robot.getOrientation();
|
|
465
461
|
return `Current device orientation is ${orientation}`;
|
|
466
462
|
});
|
|
463
|
+
tool("mobile_start_screen_recording", "Start Screen Recording", "Start recording the screen of a mobile device. The recording runs in the background until stopped with mobile_stop_screen_recording. Returns the path where the recording will be saved.", {
|
|
464
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
465
|
+
output: zod_1.z.string().optional().describe("The file path to save the recording to. If not provided, a temporary path will be used."),
|
|
466
|
+
timeLimit: zod_1.z.number().optional().describe("Maximum recording duration in seconds. The recording will stop automatically after this time."),
|
|
467
|
+
}, { destructiveHint: true }, async ({ device, output, timeLimit }) => {
|
|
468
|
+
getRobotFromDevice(device);
|
|
469
|
+
if (activeRecordings.has(device)) {
|
|
470
|
+
throw new robot_1.ActionableError(`Device "${device}" is already being recorded. Stop the current recording first with mobile_stop_screen_recording.`);
|
|
471
|
+
}
|
|
472
|
+
const outputPath = output || node_path_1.default.join(node_os_1.default.tmpdir(), `screen-recording-${Date.now()}.mp4`);
|
|
473
|
+
const args = ["screenrecord", "--device", device, "--output", outputPath, "--silent"];
|
|
474
|
+
if (timeLimit !== undefined) {
|
|
475
|
+
args.push("--time-limit", String(timeLimit));
|
|
476
|
+
}
|
|
477
|
+
const child = mobilecli.spawnCommand(args);
|
|
478
|
+
const cleanup = () => {
|
|
479
|
+
activeRecordings.delete(device);
|
|
480
|
+
};
|
|
481
|
+
child.on("error", cleanup);
|
|
482
|
+
child.on("exit", cleanup);
|
|
483
|
+
activeRecordings.set(device, {
|
|
484
|
+
process: child,
|
|
485
|
+
outputPath,
|
|
486
|
+
startedAt: Date.now(),
|
|
487
|
+
});
|
|
488
|
+
return `Screen recording started. Output will be saved to: ${outputPath}`;
|
|
489
|
+
});
|
|
490
|
+
tool("mobile_stop_screen_recording", "Stop Screen Recording", "Stop an active screen recording on a mobile device. Returns the file path, size, and approximate duration of the recording.", {
|
|
491
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
492
|
+
}, { destructiveHint: true }, async ({ device }) => {
|
|
493
|
+
const recording = activeRecordings.get(device);
|
|
494
|
+
if (!recording) {
|
|
495
|
+
throw new robot_1.ActionableError(`No active recording found for device "${device}". Start a recording first with mobile_start_screen_recording.`);
|
|
496
|
+
}
|
|
497
|
+
const { process: child, outputPath, startedAt } = recording;
|
|
498
|
+
activeRecordings.delete(device);
|
|
499
|
+
child.kill("SIGINT");
|
|
500
|
+
await new Promise(resolve => {
|
|
501
|
+
const timeout = setTimeout(() => {
|
|
502
|
+
child.kill("SIGKILL");
|
|
503
|
+
resolve();
|
|
504
|
+
}, 5 * 60 * 1000);
|
|
505
|
+
child.on("close", () => {
|
|
506
|
+
clearTimeout(timeout);
|
|
507
|
+
resolve();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
const durationSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
511
|
+
if (!node_fs_1.default.existsSync(outputPath)) {
|
|
512
|
+
return `Recording stopped after ~${durationSeconds}s but the output file was not found at: ${outputPath}`;
|
|
513
|
+
}
|
|
514
|
+
const stats = node_fs_1.default.statSync(outputPath);
|
|
515
|
+
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
516
|
+
return `Recording stopped. File: ${outputPath} (${fileSizeMB} MB, ~${durationSeconds}s)`;
|
|
517
|
+
});
|
|
467
518
|
return server;
|
|
468
519
|
};
|
|
469
520
|
exports.createMcpServer = createMcpServer;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePackageName = validatePackageName;
|
|
4
|
+
const robot_1 = require("./robot");
|
|
5
|
+
function validatePackageName(packageName) {
|
|
6
|
+
if (!/^[a-zA-Z0-9._]+$/.test(packageName)) {
|
|
7
|
+
throw new robot_1.ActionableError(`Invalid package name: "${packageName}"`);
|
|
8
|
+
}
|
|
9
|
+
}
|
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.46",
|
|
5
5
|
"description": "Mobile MCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"ajv": "^8.18.0",
|
|
29
29
|
"commander": "14.0.0",
|
|
30
30
|
"express": "5.1.0",
|
|
31
|
-
"fast-xml-parser": "5.3.
|
|
31
|
+
"fast-xml-parser": "5.3.8",
|
|
32
32
|
"qs": "^6.15.0",
|
|
33
33
|
"zod": "^4.1.13",
|
|
34
34
|
"zod-to-json-schema": "3.25.0"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@mobilenext/mobilecli": "0.1.
|
|
37
|
+
"@mobilenext/mobilecli": "0.1.60"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/eslintrc": "^3.2.0",
|