@mobilenext/mobile-mcp 0.0.23 → 0.0.25

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
@@ -1,38 +1,40 @@
1
- # Mobile Next - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and physical devices
1
+ # Mobile Next - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and Real Devices
2
2
 
3
- This is a [Model Context Protocol (MCP) server](https://github.com/modelcontextprotocol) that enables scalable mobile automation, development through a platform-agnostic interface, eliminating the need for distinct iOS or Android knowledge. You can run it on emulators, simulators, and physical devices (iOS and Android).
3
+ This is a [Model Context Protocol (MCP) server](https://github.com/modelcontextprotocol) that enables scalable mobile automation, development through a platform-agnostic interface, eliminating the need for distinct iOS or Android knowledge. You can run it on emulators, simulators, and real devices (iOS and Android).
4
4
  This server allows Agents and LLMs to interact with native iOS/Android applications and devices through structured accessibility snapshots or coordinate-based taps based on screenshots.
5
5
 
6
6
  <h4 align="center">
7
- <a href="https://github.com/mobile-next/mobile-mcp">
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">
10
+ <a href="https://github.com/mobile-next/mobile-mcp">
11
11
  <img src="https://img.shields.io/github/contributors/mobile-next/mobile-mcp?color=green" alt="Mobile Next Downloads" />
12
12
  </a>
13
13
  <a href="https://www.npmjs.com/package/@mobilenext/mobile-mcp">
14
- <img src="https://img.shields.io/npm/dm/@mobilenext/mobile-mcp?logo=npm&style=flat&color=red" alt="npm">
14
+ <img src="https://img.shields.io/npm/dm/@mobilenext/mobile-mcp?logo=npm&style=flat&color=red" alt="npm" />
15
15
  </a>
16
- <a href="https://github.com/mobile-next/mobile-mcp/releases">
17
- <img src="https://img.shields.io/github/release/mobile-next/mobile-mcp">
16
+ <a href="https://github.com/mobile-next/mobile-mcp/releases">
17
+ <img src="https://img.shields.io/github/release/mobile-next/mobile-mcp" />
18
18
  </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">
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
21
  </a>
22
-
23
- </p>
22
+ <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
+ <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
+ </a>
25
+ </h4>
24
26
 
25
27
  <h4 align="center">
26
- <a href="http://mobilenexthq.com/join-slack">
28
+ <a href="http://mobilenexthq.com/join-slack">
27
29
  <img src="https://img.shields.io/badge/join-Slack-blueviolet?logo=slack&style=flat" alt="Slack community channel" />
28
- </a>
29
- </p>
30
+ </a>
31
+ </h4>
30
32
 
31
33
  https://github.com/user-attachments/assets/c4e89c4f-cc71-4424-8184-bdbc8c638fa1
32
34
 
33
35
  <p align="center">
34
36
  <a href="https://github.com/mobile-next/">
35
- <img alt="mobile-mcp" src="https://raw.githubusercontent.com/mobile-next/mobile-next-assets/refs/heads/main/mobile-mcp-banner.png" width="600">
37
+ <img alt="mobile-mcp" src="https://raw.githubusercontent.com/mobile-next/mobile-next-assets/refs/heads/main/mobile-mcp-banner.png" width="600" />
36
38
  </a>
37
39
  </p>
38
40
 
@@ -49,7 +51,7 @@ Check out our detailed roadmap to see upcoming features, improvements, and miles
49
51
  How we help to scale mobile automation:
50
52
 
51
53
  - 📲 Native app automation (iOS and Android) for testing or data-entry scenarios.
52
- - 📝 Scripted flows and form interactions without manually controlling simulators/emulators or physical devices (iPhone, Samsung, Google Pixel etc)
54
+ - 📝 Scripted flows and form interactions without manually controlling simulators/emulators or real devices (iPhone, Samsung, Google Pixel etc)
53
55
  - 🧭 Automating multi-step user journeys driven by an LLM
54
56
  - 👆 General-purpose mobile application interaction for agent-based frameworks
55
57
  - 🤖 Enables agent-to-agent communication for mobile automation usecases, data extraction
@@ -178,18 +180,18 @@ What you will need to connect MCP with your agent and mobile devices:
178
180
  - [node.js](https://nodejs.org/en/download/) v22+
179
181
  - [MCP](https://modelcontextprotocol.io/introduction) supported foundational models or agents, like [Claude MCP](https://modelcontextprotocol.io/quickstart/server), [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/mcp/), [Copilot Studio](https://www.microsoft.com/en-us/microsoft-copilot/blog/copilot-studio/introducing-model-context-protocol-mcp-in-copilot-studio-simplified-integration-with-ai-apps-and-agents/)
180
182
 
181
- ### Simulators, Emulators, and Physical Devices
183
+ ### Simulators, Emulators, and Real Devices
182
184
 
183
185
  When launched, Mobile MCP can connect to:
184
186
  - iOS Simulators on macOS/Linux
185
187
  - Android Emulators on Linux/Windows/macOS
186
- - Physical iOS or Android devices (requires proper platform tools and drivers)
188
+ - iOS or Android real devices (requires proper platform tools and drivers)
187
189
 
188
190
  Make sure you have your mobile platform SDKs (Xcode, Android SDK) installed and configured properly before running Mobile Next Mobile MCP.
189
191
 
190
192
  ### Running in "headless" mode on Simulators/Emulators
191
193
 
192
- When you do not have a physical phone connected to your machine, you can run Mobile MCP with an emulator or simulator in the background.
194
+ When you do not have a real device connected to your machine, you can run Mobile MCP with an emulator or simulator in the background.
193
195
 
194
196
  For example, on Android:
195
197
  1. Start an emulator (avdmanager / emulator command).
package/lib/android.js CHANGED
@@ -39,14 +39,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.AndroidDeviceManager = exports.AndroidRobot = void 0;
40
40
  const node_path_1 = __importDefault(require("node:path"));
41
41
  const node_child_process_1 = require("node:child_process");
42
+ const node_fs_1 = require("node:fs");
42
43
  const xml = __importStar(require("fast-xml-parser"));
43
44
  const robot_1 = require("./robot");
44
45
  const getAdbPath = () => {
45
- let executable = "adb";
46
46
  if (process.env.ANDROID_HOME) {
47
- executable = node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
47
+ return node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
48
48
  }
49
- return executable;
49
+ const defaultAndroidSdk = node_path_1.default.join(process.env.HOME || "", "Library", "Android", "sdk", "platform-tools", "adb");
50
+ if ((0, node_fs_1.existsSync)(defaultAndroidSdk)) {
51
+ return defaultAndroidSdk;
52
+ }
53
+ return "adb";
50
54
  };
51
55
  const BUTTON_MAP = {
52
56
  "BACK": "KEYCODE_BACK",
@@ -73,11 +77,6 @@ class AndroidRobot {
73
77
  timeout: TIMEOUT,
74
78
  });
75
79
  }
76
- getFirstDisplayId() {
77
- const output = this.adb("shell", "dumpsys", "SurfaceFlinger", "--display-id").toString();
78
- const match = output.match(/Display (\d+) \(/);
79
- return match ? match[1] : null;
80
- }
81
80
  getSystemFeatures() {
82
81
  return this.adb("shell", "pm", "list", "features")
83
82
  .toString()
@@ -195,16 +194,42 @@ class AndroidRobot {
195
194
  }
196
195
  this.adb("shell", "input", "swipe", `${x0}`, `${y0}`, `${x1}`, `${y1}`, "1000");
197
196
  }
198
- async getScreenshot() {
199
- const displayId = this.getFirstDisplayId();
200
- if (displayId !== null) {
201
- // always good to provide displayId. required for multi-display devices such as fold
202
- return this.adb("exec-out", "screencap", "-p", "-d", displayId);
197
+ getDisplayCount() {
198
+ return this.adb("shell", "dumpsys", "SurfaceFlinger", "--display-id")
199
+ .toString()
200
+ .split("\n")
201
+ .filter(s => s.startsWith("Display "))
202
+ .length;
203
+ }
204
+ getFirstDisplayId() {
205
+ const displays = this.adb("shell", "cmd", "display", "get-displays")
206
+ .toString()
207
+ .split("\n")
208
+ .filter(s => s.startsWith("Display id "))
209
+ // filter for state ON even though get-displays only returns turned on displays
210
+ .filter(s => s.indexOf(", state ON,") >= 0)
211
+ // another paranoia check
212
+ .filter(s => s.indexOf(", uniqueId ") >= 0);
213
+ if (displays.length > 0) {
214
+ const m = displays[0].match(/uniqueId \"([^\"]+)\"/);
215
+ if (m !== null) {
216
+ const displayId = m[1];
217
+ if (displayId.indexOf("local:") === 0) {
218
+ return displayId.split(":")[1];
219
+ }
220
+ return displayId;
221
+ }
203
222
  }
204
- else {
205
- // backward compatibility for android 10 and below
223
+ return null;
224
+ }
225
+ async getScreenshot() {
226
+ if (this.getDisplayCount() <= 1) {
227
+ // backward compatibility for android 10 and below, and for single display devices
206
228
  return this.adb("exec-out", "screencap", "-p");
207
229
  }
230
+ // find the first display that is turned on, and capture that one
231
+ const displayId = this.getFirstDisplayId();
232
+ return this.adb("exec-out", "screencap", "-p", "-d", `${displayId}`);
208
233
  }
209
234
  collectElements(node) {
210
235
  const elements = [];
@@ -254,6 +279,10 @@ class AndroidRobot {
254
279
  isAscii(text) {
255
280
  return /^[\x00-\x7F]*$/.test(text);
256
281
  }
282
+ escapeShellText(text) {
283
+ // escape all shell special characters that could be used for injection
284
+ return text.replace(/[\\'"` \t\n\r|&;()<>{}[\]$*?]/g, "\\$&");
285
+ }
257
286
  async isDeviceKitInstalled() {
258
287
  const packages = await this.listPackages();
259
288
  return packages.includes("com.mobilenext.devicekit");
@@ -267,7 +296,7 @@ class AndroidRobot {
267
296
  if (this.isAscii(text)) {
268
297
  // adb shell input only supports ascii characters. and
269
298
  // some of the keys have to be escaped.
270
- const _text = text.replace(/ /g, "\\ ");
299
+ const _text = this.escapeShellText(text);
271
300
  this.adb("shell", "input", "text", _text);
272
301
  }
273
302
  else if (await this.isDeviceKitInstalled()) {
@@ -287,16 +316,21 @@ class AndroidRobot {
287
316
  if (!BUTTON_MAP[button]) {
288
317
  throw new robot_1.ActionableError(`Button "${button}" is not supported`);
289
318
  }
290
- this.adb("shell", "input", "keyevent", BUTTON_MAP[button]);
319
+ const mapped = BUTTON_MAP[button];
320
+ this.adb("shell", "input", "keyevent", mapped);
291
321
  }
292
322
  async tap(x, y) {
293
323
  this.adb("shell", "input", "tap", `${x}`, `${y}`);
294
324
  }
325
+ async longPress(x, y) {
326
+ // a long press is a swipe with no movement and a long duration
327
+ this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`, "500");
328
+ }
295
329
  async setOrientation(orientation) {
296
- const orientationValue = orientation === "portrait" ? 0 : 1;
330
+ const value = orientation === "portrait" ? 0 : 1;
297
331
  // disable auto-rotation prior to setting the orientation
298
332
  this.adb("shell", "settings", "put", "system", "accelerometer_rotation", "0");
299
- this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${orientationValue}`);
333
+ this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${value}`);
300
334
  }
301
335
  async getOrientation() {
302
336
  const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
package/lib/ios.js CHANGED
@@ -117,6 +117,10 @@ class IosRobot {
117
117
  const wda = await this.wda();
118
118
  await wda.tap(x, y);
119
119
  }
120
+ async longPress(x, y) {
121
+ const wda = await this.wda();
122
+ await wda.longPress(x, y);
123
+ }
120
124
  async getElementsOnScreen() {
121
125
  const wda = await this.wda();
122
126
  return await wda.getElementsOnScreen();
@@ -104,6 +104,10 @@ class Simctl {
104
104
  const wda = await this.wda();
105
105
  return wda.tap(x, y);
106
106
  }
107
+ async longPress(x, y) {
108
+ const wda = await this.wda();
109
+ return wda.longPress(x, y);
110
+ }
107
111
  async pressButton(button) {
108
112
  const wda = await this.wda();
109
113
  return wda.pressButton(button);
package/lib/server.js CHANGED
@@ -229,6 +229,14 @@ const createMcpServer = () => {
229
229
  await robot.tap(x, y);
230
230
  return `Clicked on screen at coordinates: ${x}, ${y}`;
231
231
  });
232
+ tool("mobile_long_press_on_screen_at_coordinates", "Long press on the screen at given x,y coordinates. If long pressing on an element, use the list_elements_on_screen tool to find the coordinates.", {
233
+ x: zod_1.z.number().describe("The x coordinate to long press on the screen, in pixels"),
234
+ y: zod_1.z.number().describe("The y coordinate to long press on the screen, in pixels"),
235
+ }, async ({ x, y }) => {
236
+ requireRobot();
237
+ await robot.longPress(x, y);
238
+ return `Long pressed on screen at coordinates: ${x}, ${y}`;
239
+ });
232
240
  tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
233
241
  noParams
234
242
  }, async ({}) => {
@@ -142,6 +142,32 @@ class WebDriverAgent {
142
142
  });
143
143
  });
144
144
  }
145
+ async longPress(x, y) {
146
+ await this.withinSession(async (sessionUrl) => {
147
+ const url = `${sessionUrl}/actions`;
148
+ await fetch(url, {
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/json",
152
+ },
153
+ body: JSON.stringify({
154
+ actions: [
155
+ {
156
+ type: "pointer",
157
+ id: "finger1",
158
+ parameters: { pointerType: "touch" },
159
+ actions: [
160
+ { type: "pointerMove", duration: 0, x, y },
161
+ { type: "pointerDown", button: 0 },
162
+ { type: "pause", duration: 500 },
163
+ { type: "pointerUp", button: 0 }
164
+ ]
165
+ }
166
+ ]
167
+ }),
168
+ });
169
+ });
170
+ }
145
171
  isVisible(rect) {
146
172
  return rect.x >= 0 && rect.y >= 0;
147
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobilenext/mobile-mcp",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "Mobile MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,7 @@
13
13
  "scripts": {
14
14
  "build": "tsc && chmod +x lib/index.js",
15
15
  "lint": "eslint .",
16
+ "fixlint": "eslint . --fix",
16
17
  "test": "nyc mocha --require ts-node/register test/*.ts",
17
18
  "watch": "tsc --watch",
18
19
  "clean": "rm -rf lib",