@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 +21 -19
- package/lib/android.js +53 -19
- package/lib/ios.js +4 -0
- package/lib/iphone-simulator.js +4 -0
- package/lib/server.js +8 -0
- package/lib/webdriver-agent.js +26 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
# Mobile Next - MCP server for Mobile Development and Automation
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
47
|
+
return node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
|
|
48
48
|
}
|
|
49
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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:${
|
|
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();
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -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 ({}) => {
|
package/lib/webdriver-agent.js
CHANGED
|
@@ -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.
|
|
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",
|