@mobilenext/mobile-mcp 0.0.48 → 0.0.50

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
@@ -7,24 +7,15 @@ This server allows Agents and LLMs to interact with native iOS/Android applicati
7
7
  <a href="https://github.com/mobile-next/mobile-mcp">
8
8
  <img src="https://img.shields.io/github/stars/mobile-next/mobile-mcp" alt="Mobile Next Stars" />
9
9
  </a>
10
- <a href="https://github.com/mobile-next/mobile-mcp">
11
- <img src="https://img.shields.io/github/contributors/mobile-next/mobile-mcp?color=green" alt="Mobile Next Downloads" />
12
- </a>
13
10
  <a href="https://www.npmjs.com/package/@mobilenext/mobile-mcp">
14
11
  <img src="https://img.shields.io/npm/dm/@mobilenext/mobile-mcp?logo=npm&style=flat&color=red" alt="npm" />
15
12
  </a>
16
13
  <a href="https://github.com/mobile-next/mobile-mcp/releases">
17
14
  <img src="https://img.shields.io/github/release/mobile-next/mobile-mcp" />
18
15
  </a>
19
- <a href="https://github.com/mobile-next/mobile-mcp/blob/main/LICENSE">
20
- <img src="https://img.shields.io/badge/license-Apache 2.0-blue.svg" alt="Mobile MCP is released under the Apache-2.0 License" />
21
- </a>
22
16
  <a href="https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%7B%22name%22%3A%22mobile-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40mobilenext%2Fmobile-mcp%40latest%22%5D%7D">
23
17
  <img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code" />
24
18
  </a>
25
- </h4>
26
-
27
- <h4 align="center">
28
19
  <a href="https://github.com/mobile-next/mobile-mcp/wiki">
29
20
  <img src="https://img.shields.io/badge/documentation-wiki-blue" alt="wiki" />
30
21
  </a>
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;
@@ -348,6 +351,10 @@ const createMcpServer = () => {
348
351
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
349
352
  url: zod_1.z.string().describe("The URL to open"),
350
353
  }, { destructiveHint: true }, async ({ device, url }) => {
354
+ const allowUnsafeUrls = process.env.MOBILEMCP_ALLOW_UNSAFE_URLS === "1";
355
+ if (!allowUnsafeUrls && !url.startsWith("http://") && !url.startsWith("https://")) {
356
+ throw new robot_1.ActionableError("Only http:// and https:// URLs are allowed. Set MOBILEMCP_ALLOW_UNSAFE_URLS=1 to allow other URL schemes.");
357
+ }
351
358
  const robot = getRobotFromDevice(device);
352
359
  await robot.openUrl(url);
353
360
  return `Opened URL: ${url}`;
@@ -386,8 +393,10 @@ const createMcpServer = () => {
386
393
  });
387
394
  tool("mobile_save_screenshot", "Save Screenshot", "Save a screenshot of the mobile device to a file", {
388
395
  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"),
396
+ saveTo: zod_1.z.string().describe("The path to save the screenshot to. Filename must end with .png, .jpg, or .jpeg"),
390
397
  }, { destructiveHint: true }, async ({ device, saveTo }) => {
398
+ (0, utils_1.validateFileExtension)(saveTo, ALLOWED_SCREENSHOT_EXTENSIONS, "save_screenshot");
399
+ (0, utils_1.validateOutputPath)(saveTo);
391
400
  const robot = getRobotFromDevice(device);
392
401
  const screenshot = await robot.getScreenshot();
393
402
  node_fs_1.default.writeFileSync(saveTo, screenshot);
@@ -463,9 +472,13 @@ const createMcpServer = () => {
463
472
  });
464
473
  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
474
  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."),
475
+ 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
476
  timeLimit: zod_1.z.coerce.number().optional().describe("Maximum recording duration in seconds. The recording will stop automatically after this time."),
468
477
  }, { destructiveHint: true }, async ({ device, output, timeLimit }) => {
478
+ if (output) {
479
+ (0, utils_1.validateFileExtension)(output, ALLOWED_RECORDING_EXTENSIONS, "start_screen_recording");
480
+ (0, utils_1.validateOutputPath)(output);
481
+ }
469
482
  getRobotFromDevice(device);
470
483
  if (activeRecordings.has(device)) {
471
484
  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.50",
5
5
  "description": "Mobile MCP",
6
6
  "repository": {
7
7
  "type": "git",