@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 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
+ }
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.48",
4
+ "version": "0.0.49",
5
5
  "description": "Mobile MCP",
6
6
  "repository": {
7
7
  "type": "git",