@mobilenext/mobile-mcp 0.0.48 → 0.0.49
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/server.js +11 -2
- package/lib/utils.js +65 -0
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -18,6 +18,9 @@ const png_1 = require("./png");
|
|
|
18
18
|
const image_utils_1 = require("./image-utils");
|
|
19
19
|
const mobilecli_1 = require("./mobilecli");
|
|
20
20
|
const mobile_device_1 = require("./mobile-device");
|
|
21
|
+
const utils_1 = require("./utils");
|
|
22
|
+
const ALLOWED_SCREENSHOT_EXTENSIONS = [".png", ".jpg", ".jpeg"];
|
|
23
|
+
const ALLOWED_RECORDING_EXTENSIONS = [".mp4"];
|
|
21
24
|
const getAgentVersion = () => {
|
|
22
25
|
const json = require("../package.json");
|
|
23
26
|
return json.version;
|
|
@@ -386,8 +389,10 @@ const createMcpServer = () => {
|
|
|
386
389
|
});
|
|
387
390
|
tool("mobile_save_screenshot", "Save Screenshot", "Save a screenshot of the mobile device to a file", {
|
|
388
391
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
389
|
-
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
392
|
+
saveTo: zod_1.z.string().describe("The path to save the screenshot to. Filename must end with .png, .jpg, or .jpeg"),
|
|
390
393
|
}, { destructiveHint: true }, async ({ device, saveTo }) => {
|
|
394
|
+
(0, utils_1.validateFileExtension)(saveTo, ALLOWED_SCREENSHOT_EXTENSIONS, "save_screenshot");
|
|
395
|
+
(0, utils_1.validateOutputPath)(saveTo);
|
|
391
396
|
const robot = getRobotFromDevice(device);
|
|
392
397
|
const screenshot = await robot.getScreenshot();
|
|
393
398
|
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
@@ -463,9 +468,13 @@ const createMcpServer = () => {
|
|
|
463
468
|
});
|
|
464
469
|
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.", {
|
|
465
470
|
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
466
|
-
output: zod_1.z.string().optional().describe("The file path to save the recording to. If not provided, a temporary path will be used."),
|
|
471
|
+
output: zod_1.z.string().optional().describe("The file path to save the recording to. Filename must end with .mp4. If not provided, a temporary path will be used."),
|
|
467
472
|
timeLimit: zod_1.z.coerce.number().optional().describe("Maximum recording duration in seconds. The recording will stop automatically after this time."),
|
|
468
473
|
}, { destructiveHint: true }, async ({ device, output, timeLimit }) => {
|
|
474
|
+
if (output) {
|
|
475
|
+
(0, utils_1.validateFileExtension)(output, ALLOWED_RECORDING_EXTENSIONS, "start_screen_recording");
|
|
476
|
+
(0, utils_1.validateOutputPath)(output);
|
|
477
|
+
}
|
|
469
478
|
getRobotFromDevice(device);
|
|
470
479
|
if (activeRecordings.has(device)) {
|
|
471
480
|
throw new robot_1.ActionableError(`Device "${device}" is already being recorded. Stop the current recording first with mobile_stop_screen_recording.`);
|
package/lib/utils.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.validatePackageName = validatePackageName;
|
|
4
7
|
exports.validateLocale = validateLocale;
|
|
8
|
+
exports.validateFileExtension = validateFileExtension;
|
|
9
|
+
exports.validateOutputPath = validateOutputPath;
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
5
13
|
const robot_1 = require("./robot");
|
|
6
14
|
function validatePackageName(packageName) {
|
|
7
15
|
if (!/^[a-zA-Z0-9._]+$/.test(packageName)) {
|
|
@@ -13,3 +21,60 @@ function validateLocale(locale) {
|
|
|
13
21
|
throw new robot_1.ActionableError(`Invalid locale: "${locale}"`);
|
|
14
22
|
}
|
|
15
23
|
}
|
|
24
|
+
function getAllowedRoots() {
|
|
25
|
+
const roots = [
|
|
26
|
+
node_os_1.default.tmpdir(),
|
|
27
|
+
process.cwd(),
|
|
28
|
+
];
|
|
29
|
+
// macOS /tmp is a symlink to /private/tmp, add both to be safe
|
|
30
|
+
if (process.platform === "darwin") {
|
|
31
|
+
roots.push("/tmp");
|
|
32
|
+
roots.push("/private/tmp");
|
|
33
|
+
}
|
|
34
|
+
return roots.map(r => node_path_1.default.resolve(r));
|
|
35
|
+
}
|
|
36
|
+
function isPathUnderRoot(filePath, root) {
|
|
37
|
+
const relative = node_path_1.default.relative(root, filePath);
|
|
38
|
+
if (relative === "") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (node_path_1.default.isAbsolute(relative)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (relative.startsWith("..")) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function validateFileExtension(filePath, allowedExtensions, toolName) {
|
|
50
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
51
|
+
if (!allowedExtensions.includes(ext)) {
|
|
52
|
+
throw new robot_1.ActionableError(`${toolName} requires a ${allowedExtensions.join(", ")} file extension, got: "${ext || "(none)"}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function resolveWithSymlinks(filePath) {
|
|
56
|
+
const resolved = node_path_1.default.resolve(filePath);
|
|
57
|
+
const dir = node_path_1.default.dirname(resolved);
|
|
58
|
+
const filename = node_path_1.default.basename(resolved);
|
|
59
|
+
try {
|
|
60
|
+
return node_path_1.default.join(node_fs_1.default.realpathSync(dir), filename);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function validateOutputPath(filePath) {
|
|
67
|
+
const resolved = resolveWithSymlinks(filePath);
|
|
68
|
+
const allowedRoots = getAllowedRoots();
|
|
69
|
+
const isWindows = process.platform === "win32";
|
|
70
|
+
const isAllowed = allowedRoots.some(root => {
|
|
71
|
+
if (isWindows) {
|
|
72
|
+
return isPathUnderRoot(resolved.toLowerCase(), root.toLowerCase());
|
|
73
|
+
}
|
|
74
|
+
return isPathUnderRoot(resolved, root);
|
|
75
|
+
});
|
|
76
|
+
if (!isAllowed) {
|
|
77
|
+
const dir = node_path_1.default.dirname(resolved);
|
|
78
|
+
throw new robot_1.ActionableError(`"${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.`);
|
|
79
|
+
}
|
|
80
|
+
}
|