@mobilenext/mobile-mcp 0.0.19 → 0.0.21
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 +1 -1
- package/lib/android.js +48 -10
- package/lib/ios.js +20 -23
- package/lib/iphone-simulator.js +40 -6
- package/lib/logger.js +2 -2
- package/lib/server.js +83 -3
- package/lib/webdriver-agent.js +9 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ What you will need to connect MCP with your agent and mobile devices:
|
|
|
175
175
|
|
|
176
176
|
- [Xcode command line tools](https://developer.apple.com/xcode/resources/)
|
|
177
177
|
- [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools)
|
|
178
|
-
- [node.js](https://nodejs.org/en/download/)
|
|
178
|
+
- [node.js](https://nodejs.org/en/download/) v22+
|
|
179
179
|
- [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
180
|
|
|
181
181
|
### Simulators, Emulators, and Physical Devices
|
package/lib/android.js
CHANGED
|
@@ -37,14 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.AndroidDeviceManager = exports.AndroidRobot = void 0;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
42
|
const xml = __importStar(require("fast-xml-parser"));
|
|
43
43
|
const robot_1 = require("./robot");
|
|
44
44
|
const getAdbPath = () => {
|
|
45
45
|
let executable = "adb";
|
|
46
46
|
if (process.env.ANDROID_HOME) {
|
|
47
|
-
executable =
|
|
47
|
+
executable = node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
|
|
48
48
|
}
|
|
49
49
|
return executable;
|
|
50
50
|
};
|
|
@@ -68,7 +68,7 @@ class AndroidRobot {
|
|
|
68
68
|
this.deviceId = deviceId;
|
|
69
69
|
}
|
|
70
70
|
adb(...args) {
|
|
71
|
-
return (0,
|
|
71
|
+
return (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", this.deviceId, ...args], {
|
|
72
72
|
maxBuffer: MAX_BUFFER_SIZE,
|
|
73
73
|
timeout: TIMEOUT,
|
|
74
74
|
});
|
|
@@ -94,6 +94,7 @@ class AndroidRobot {
|
|
|
94
94
|
return { width, height, scale };
|
|
95
95
|
}
|
|
96
96
|
async listApps() {
|
|
97
|
+
// only apps that have a launcher activity are returned
|
|
97
98
|
return this.adb("shell", "cmd", "package", "query-activities", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER")
|
|
98
99
|
.toString()
|
|
99
100
|
.split("\n")
|
|
@@ -106,6 +107,14 @@ class AndroidRobot {
|
|
|
106
107
|
appName: packageName,
|
|
107
108
|
}));
|
|
108
109
|
}
|
|
110
|
+
async listPackages() {
|
|
111
|
+
return this.adb("shell", "pm", "list", "packages")
|
|
112
|
+
.toString()
|
|
113
|
+
.split("\n")
|
|
114
|
+
.map(line => line.trim())
|
|
115
|
+
.filter(line => line.startsWith("package:"))
|
|
116
|
+
.map(line => line.substring("package:".length));
|
|
117
|
+
}
|
|
109
118
|
async launchApp(packageName) {
|
|
110
119
|
this.adb("shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1");
|
|
111
120
|
}
|
|
@@ -229,10 +238,37 @@ class AndroidRobot {
|
|
|
229
238
|
async openUrl(url) {
|
|
230
239
|
this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
|
|
231
240
|
}
|
|
241
|
+
isAscii(text) {
|
|
242
|
+
return /^[\x00-\x7F]*$/.test(text);
|
|
243
|
+
}
|
|
244
|
+
async isDeviceKitInstalled() {
|
|
245
|
+
const packages = await this.listPackages();
|
|
246
|
+
return packages.includes("com.mobilenext.devicekit");
|
|
247
|
+
}
|
|
232
248
|
async sendKeys(text) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
if (text === "") {
|
|
250
|
+
// bailing early, so we don't run adb shell with empty string.
|
|
251
|
+
// this happens when you prompt with a simple "submit".
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (this.isAscii(text)) {
|
|
255
|
+
// adb shell input only supports ascii characters. and
|
|
256
|
+
// some of the keys have to be escaped.
|
|
257
|
+
const _text = text.replace(/ /g, "\\ ");
|
|
258
|
+
this.adb("shell", "input", "text", _text);
|
|
259
|
+
}
|
|
260
|
+
else if (await this.isDeviceKitInstalled()) {
|
|
261
|
+
// try sending over clipboard
|
|
262
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
263
|
+
// send clipboard over and immediately paste it
|
|
264
|
+
this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.set", "-e", "encoding", "base64", "-e", "text", base64, "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
|
|
265
|
+
this.adb("shell", "input", "keyevent", "KEYCODE_PASTE");
|
|
266
|
+
// clear clipboard when we're done
|
|
267
|
+
this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.clear", "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
throw new robot_1.ActionableError("Non-ASCII text is not supported on Android, please install mobilenext devicekit, see https://github.com/mobile-next/devicekit-android");
|
|
271
|
+
}
|
|
236
272
|
}
|
|
237
273
|
async pressButton(button) {
|
|
238
274
|
if (!BUTTON_MAP[button]) {
|
|
@@ -245,8 +281,9 @@ class AndroidRobot {
|
|
|
245
281
|
}
|
|
246
282
|
async setOrientation(orientation) {
|
|
247
283
|
const orientationValue = orientation === "portrait" ? 0 : 1;
|
|
248
|
-
|
|
284
|
+
// disable auto-rotation prior to setting the orientation
|
|
249
285
|
this.adb("shell", "settings", "put", "system", "accelerometer_rotation", "0");
|
|
286
|
+
this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${orientationValue}`);
|
|
250
287
|
}
|
|
251
288
|
async getOrientation() {
|
|
252
289
|
const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
|
|
@@ -297,11 +334,12 @@ class AndroidDeviceManager {
|
|
|
297
334
|
}
|
|
298
335
|
getConnectedDevices() {
|
|
299
336
|
try {
|
|
300
|
-
const names = (0,
|
|
337
|
+
const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
|
|
301
338
|
.toString()
|
|
302
339
|
.split("\n")
|
|
340
|
+
.map(line => line.trim())
|
|
341
|
+
.filter(line => line !== "")
|
|
303
342
|
.filter(line => !line.startsWith("List of devices attached"))
|
|
304
|
-
.filter(line => line.trim() !== "")
|
|
305
343
|
.map(line => line.split("\t")[0]);
|
|
306
344
|
return names.map(name => ({
|
|
307
345
|
deviceId: name,
|
package/lib/ios.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.IosManager = exports.IosRobot = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const crypto_1 = require("crypto");
|
|
10
|
-
const fs_1 = require("fs");
|
|
11
|
-
const child_process_1 = require("child_process");
|
|
12
|
-
const net_1 = require("net");
|
|
4
|
+
const node_net_1 = require("node:net");
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
6
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
14
7
|
const robot_1 = require("./robot");
|
|
15
8
|
const WDA_PORT = 8100;
|
|
@@ -28,7 +21,7 @@ class IosRobot {
|
|
|
28
21
|
}
|
|
29
22
|
isListeningOnPort(port) {
|
|
30
23
|
return new Promise((resolve, reject) => {
|
|
31
|
-
const client = new
|
|
24
|
+
const client = new node_net_1.Socket();
|
|
32
25
|
client.connect(port, "localhost", () => {
|
|
33
26
|
client.destroy();
|
|
34
27
|
resolve(true);
|
|
@@ -63,7 +56,7 @@ class IosRobot {
|
|
|
63
56
|
return wda;
|
|
64
57
|
}
|
|
65
58
|
async ios(...args) {
|
|
66
|
-
return (0,
|
|
59
|
+
return (0, node_child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
|
|
67
60
|
}
|
|
68
61
|
async getIosVersion() {
|
|
69
62
|
const output = await this.ios("info");
|
|
@@ -129,12 +122,16 @@ class IosRobot {
|
|
|
129
122
|
return await wda.getElementsOnScreen();
|
|
130
123
|
}
|
|
131
124
|
async getScreenshot() {
|
|
125
|
+
const wda = await this.wda();
|
|
126
|
+
return await wda.getScreenshot();
|
|
127
|
+
/* alternative:
|
|
132
128
|
await this.assertTunnelRunning();
|
|
133
|
-
const tmpFilename =
|
|
129
|
+
const tmpFilename = path.join(tmpdir(), `screenshot-${randomBytes(8).toString("hex")}.png`);
|
|
134
130
|
await this.ios("screenshot", "--output", tmpFilename);
|
|
135
|
-
const buffer =
|
|
136
|
-
|
|
131
|
+
const buffer = readFileSync(tmpFilename);
|
|
132
|
+
unlinkSync(tmpFilename);
|
|
137
133
|
return buffer;
|
|
134
|
+
*/
|
|
138
135
|
}
|
|
139
136
|
async setOrientation(orientation) {
|
|
140
137
|
const wda = await this.wda();
|
|
@@ -149,7 +146,7 @@ exports.IosRobot = IosRobot;
|
|
|
149
146
|
class IosManager {
|
|
150
147
|
async isGoIosInstalled() {
|
|
151
148
|
try {
|
|
152
|
-
const output = (0,
|
|
149
|
+
const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
|
|
153
150
|
const json = JSON.parse(output);
|
|
154
151
|
return json.version !== undefined && (json.version.startsWith("v") || json.version === "local-build");
|
|
155
152
|
}
|
|
@@ -157,23 +154,23 @@ class IosManager {
|
|
|
157
154
|
return false;
|
|
158
155
|
}
|
|
159
156
|
}
|
|
160
|
-
|
|
161
|
-
const output = (0,
|
|
157
|
+
getDeviceName(deviceId) {
|
|
158
|
+
const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
|
|
162
159
|
const json = JSON.parse(output);
|
|
163
160
|
return json.DeviceName;
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
if (!
|
|
162
|
+
listDevices() {
|
|
163
|
+
if (!this.isGoIosInstalled()) {
|
|
167
164
|
console.error("go-ios is not installed, no physical iOS devices can be detected");
|
|
168
165
|
return [];
|
|
169
166
|
}
|
|
170
|
-
const output = (0,
|
|
167
|
+
const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
|
|
171
168
|
const json = JSON.parse(output);
|
|
172
|
-
const devices = json.deviceList.map(
|
|
169
|
+
const devices = json.deviceList.map(device => ({
|
|
173
170
|
deviceId: device,
|
|
174
|
-
deviceName:
|
|
171
|
+
deviceName: this.getDeviceName(device),
|
|
175
172
|
}));
|
|
176
|
-
return
|
|
173
|
+
return devices;
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
176
|
exports.IosManager = IosManager;
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SimctlManager = exports.Simctl = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
5
6
|
const webdriver_agent_1 = require("./webdriver-agent");
|
|
6
7
|
const robot_1 = require("./robot");
|
|
7
8
|
const TIMEOUT = 30000;
|
|
@@ -12,21 +13,54 @@ class Simctl {
|
|
|
12
13
|
constructor(simulatorUuid) {
|
|
13
14
|
this.simulatorUuid = simulatorUuid;
|
|
14
15
|
}
|
|
16
|
+
async isWdaInstalled() {
|
|
17
|
+
const apps = await this.listApps();
|
|
18
|
+
return apps.map(app => app.packageName).includes("com.facebook.WebDriverAgentRunner.xctrunner");
|
|
19
|
+
}
|
|
20
|
+
async startWda() {
|
|
21
|
+
if (!(await this.isWdaInstalled())) {
|
|
22
|
+
// wda is not even installed, won't attempt to start it
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
(0, logger_1.trace)("Starting WebDriverAgent");
|
|
26
|
+
const webdriverPackageName = "com.facebook.WebDriverAgentRunner.xctrunner";
|
|
27
|
+
this.simctl("launch", this.simulatorUuid, webdriverPackageName);
|
|
28
|
+
// now we wait for wda to have a successful status
|
|
29
|
+
const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
|
|
30
|
+
// wait up to 10 seconds for wda to start
|
|
31
|
+
const timeout = +new Date() + 10 * 1000;
|
|
32
|
+
while (+new Date() < timeout) {
|
|
33
|
+
// cross fingers and see if wda is already running
|
|
34
|
+
if (await wda.isRunning()) {
|
|
35
|
+
(0, logger_1.trace)("WebDriverAgent is now running");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// wait 100ms before trying again
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
40
|
+
}
|
|
41
|
+
(0, logger_1.trace)("Could not start WebDriverAgent in time, giving up");
|
|
42
|
+
}
|
|
15
43
|
async wda() {
|
|
16
44
|
const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
|
|
17
45
|
if (!(await wda.isRunning())) {
|
|
18
|
-
|
|
46
|
+
await this.startWda();
|
|
47
|
+
if (!(await wda.isRunning())) {
|
|
48
|
+
throw new robot_1.ActionableError("WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/");
|
|
49
|
+
}
|
|
50
|
+
// was successfully started
|
|
19
51
|
}
|
|
20
52
|
return wda;
|
|
21
53
|
}
|
|
22
54
|
simctl(...args) {
|
|
23
|
-
return (0,
|
|
55
|
+
return (0, node_child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
|
|
24
56
|
timeout: TIMEOUT,
|
|
25
57
|
maxBuffer: MAX_BUFFER_SIZE,
|
|
26
58
|
});
|
|
27
59
|
}
|
|
28
60
|
async getScreenshot() {
|
|
29
|
-
|
|
61
|
+
const wda = await this.wda();
|
|
62
|
+
return await wda.getScreenshot();
|
|
63
|
+
// alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-");
|
|
30
64
|
}
|
|
31
65
|
async openUrl(url) {
|
|
32
66
|
const wda = await this.wda();
|
|
@@ -41,7 +75,7 @@ class Simctl {
|
|
|
41
75
|
}
|
|
42
76
|
async listApps() {
|
|
43
77
|
const text = this.simctl("listapps", this.simulatorUuid).toString();
|
|
44
|
-
const result = (0,
|
|
78
|
+
const result = (0, node_child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
|
|
45
79
|
input: text,
|
|
46
80
|
});
|
|
47
81
|
const output = JSON.parse(result.toString());
|
|
@@ -96,7 +130,7 @@ class SimctlManager {
|
|
|
96
130
|
return [];
|
|
97
131
|
}
|
|
98
132
|
try {
|
|
99
|
-
const text = (0,
|
|
133
|
+
const text = (0, node_child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
|
|
100
134
|
const json = JSON.parse(text);
|
|
101
135
|
return Object.values(json.devices).flatMap(device => {
|
|
102
136
|
return device.map(d => {
|
package/lib/logger.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.error = exports.trace = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
5
|
const writeLog = (message) => {
|
|
6
6
|
if (process.env.LOG_FILE) {
|
|
7
7
|
const logfile = process.env.LOG_FILE;
|
|
8
8
|
const timestamp = new Date().toISOString();
|
|
9
9
|
const levelStr = "INFO";
|
|
10
10
|
const logMessage = `[${timestamp}] ${levelStr} ${message}`;
|
|
11
|
-
(0,
|
|
11
|
+
(0, node_fs_1.appendFileSync)(logfile, logMessage + "\n");
|
|
12
12
|
}
|
|
13
13
|
console.error(message);
|
|
14
14
|
};
|
package/lib/server.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
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.createMcpServer = exports.getAgentVersion = void 0;
|
|
4
7
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
8
|
const zod_1 = require("zod");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
6
12
|
const logger_1 = require("./logger");
|
|
7
13
|
const android_1 = require("./android");
|
|
8
14
|
const robot_1 = require("./robot");
|
|
@@ -41,6 +47,7 @@ const createMcpServer = () => {
|
|
|
41
47
|
tools: {},
|
|
42
48
|
},
|
|
43
49
|
});
|
|
50
|
+
// an empty object to satisfy windsurf
|
|
44
51
|
const noParams = zod_1.z.object({});
|
|
45
52
|
const tool = (name, description, paramsSchema, cb) => {
|
|
46
53
|
const wrappedCb = async (args) => {
|
|
@@ -48,6 +55,7 @@ const createMcpServer = () => {
|
|
|
48
55
|
(0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
|
|
49
56
|
const response = await cb(args);
|
|
50
57
|
(0, logger_1.trace)(`=> ${response}`);
|
|
58
|
+
posthog("tool_invoked", {}).then();
|
|
51
59
|
return {
|
|
52
60
|
content: [{ type: "text", text: response }],
|
|
53
61
|
};
|
|
@@ -70,6 +78,39 @@ const createMcpServer = () => {
|
|
|
70
78
|
};
|
|
71
79
|
server.tool(name, description, paramsSchema, args => wrappedCb(args));
|
|
72
80
|
};
|
|
81
|
+
const posthog = async (event, properties) => {
|
|
82
|
+
try {
|
|
83
|
+
const url = "https://us.i.posthog.com/i/v0/e/";
|
|
84
|
+
const api_key = "phc_KHRTZmkDsU7A8EbydEK8s4lJpPoTDyyBhSlwer694cS";
|
|
85
|
+
const name = node_os_1.default.hostname() + process.execPath;
|
|
86
|
+
const distinct_id = node_crypto_1.default.createHash("sha256").update(name).digest("hex");
|
|
87
|
+
const systemProps = {
|
|
88
|
+
Platform: node_os_1.default.platform(),
|
|
89
|
+
Product: "mobile-mcp",
|
|
90
|
+
Version: (0, exports.getAgentVersion)(),
|
|
91
|
+
NodeVersion: process.version,
|
|
92
|
+
};
|
|
93
|
+
await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json"
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
api_key,
|
|
100
|
+
event,
|
|
101
|
+
properties: {
|
|
102
|
+
...systemProps,
|
|
103
|
+
...properties,
|
|
104
|
+
},
|
|
105
|
+
distinct_id,
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
posthog("launch", {}).then();
|
|
73
114
|
let robot;
|
|
74
115
|
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
75
116
|
const requireRobot = () => {
|
|
@@ -77,13 +118,44 @@ const createMcpServer = () => {
|
|
|
77
118
|
throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
|
|
78
119
|
}
|
|
79
120
|
};
|
|
121
|
+
tool("mobile_use_default_device", "Use the default device. This is a shortcut for mobile_use_device with deviceType=simulator and device=simulator_name", {
|
|
122
|
+
noParams
|
|
123
|
+
}, async () => {
|
|
124
|
+
const iosManager = new ios_1.IosManager();
|
|
125
|
+
const androidManager = new android_1.AndroidDeviceManager();
|
|
126
|
+
const simulators = simulatorManager.listBootedSimulators();
|
|
127
|
+
const androidDevices = androidManager.getConnectedDevices();
|
|
128
|
+
const iosDevices = iosManager.listDevices();
|
|
129
|
+
const sum = simulators.length + androidDevices.length + iosDevices.length;
|
|
130
|
+
if (sum === 0) {
|
|
131
|
+
throw new robot_1.ActionableError("No devices found. Please connect a device and try again.");
|
|
132
|
+
}
|
|
133
|
+
else if (sum >= 2) {
|
|
134
|
+
throw new robot_1.ActionableError("Multiple devices found. Please use the mobile_list_available_devices tool to list available devices and select one.");
|
|
135
|
+
}
|
|
136
|
+
// only one device connected, let's find it now
|
|
137
|
+
if (simulators.length === 1) {
|
|
138
|
+
robot = simulatorManager.getSimulator(simulators[0].name);
|
|
139
|
+
return `Selected default device: ${simulators[0].name}`;
|
|
140
|
+
}
|
|
141
|
+
else if (androidDevices.length === 1) {
|
|
142
|
+
robot = new android_1.AndroidRobot(androidDevices[0].deviceId);
|
|
143
|
+
return `Selected default device: ${androidDevices[0].deviceId}`;
|
|
144
|
+
}
|
|
145
|
+
else if (iosDevices.length === 1) {
|
|
146
|
+
robot = new ios_1.IosRobot(iosDevices[0].deviceId);
|
|
147
|
+
return `Selected default device: ${iosDevices[0].deviceId}`;
|
|
148
|
+
}
|
|
149
|
+
// how did this happen?
|
|
150
|
+
throw new robot_1.ActionableError("No device selected. Please use the mobile_list_available_devices tool to list available devices and select one.");
|
|
151
|
+
});
|
|
80
152
|
tool("mobile_list_available_devices", "List all available devices. This includes both physical devices and simulators. If there is more than one device returned, you need to let the user select one of them.", {
|
|
81
153
|
noParams
|
|
82
154
|
}, async ({}) => {
|
|
83
155
|
const iosManager = new ios_1.IosManager();
|
|
84
156
|
const androidManager = new android_1.AndroidDeviceManager();
|
|
85
|
-
const
|
|
86
|
-
const simulatorNames =
|
|
157
|
+
const simulators = simulatorManager.listBootedSimulators();
|
|
158
|
+
const simulatorNames = simulators.map(d => d.name);
|
|
87
159
|
const androidDevices = androidManager.getConnectedDevices();
|
|
88
160
|
const iosDevices = await iosManager.listDevices();
|
|
89
161
|
const iosDeviceNames = iosDevices.map(d => d.deviceId);
|
|
@@ -149,7 +221,7 @@ const createMcpServer = () => {
|
|
|
149
221
|
const screenSize = await robot.getScreenSize();
|
|
150
222
|
return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
|
|
151
223
|
});
|
|
152
|
-
tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates", {
|
|
224
|
+
tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates. If clicking on an element, use the list_elements_on_screen tool to find the coordinates.", {
|
|
153
225
|
x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
|
|
154
226
|
y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
|
|
155
227
|
}, async ({ x, y }) => {
|
|
@@ -228,6 +300,14 @@ const createMcpServer = () => {
|
|
|
228
300
|
}
|
|
229
301
|
return `Typed text: ${text}`;
|
|
230
302
|
});
|
|
303
|
+
tool("mobile_save_screenshot", "Save a screenshot of the mobile device to a file", {
|
|
304
|
+
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
305
|
+
}, async ({ saveTo }) => {
|
|
306
|
+
requireRobot();
|
|
307
|
+
const screenshot = await robot.getScreenshot();
|
|
308
|
+
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
309
|
+
return `Screenshot saved to: ${saveTo}`;
|
|
310
|
+
});
|
|
231
311
|
server.tool("mobile_take_screenshot", "Take a screenshot of the mobile device. Use this to understand what's on screen, if you need to press an element that is available through view hierarchy then you must list elements on screen instead. Do not cache this result.", {
|
|
232
312
|
noParams
|
|
233
313
|
}, async ({}) => {
|
package/lib/webdriver-agent.js
CHANGED
|
@@ -13,10 +13,11 @@ class WebDriverAgent {
|
|
|
13
13
|
const url = `http://${this.host}:${this.port}/status`;
|
|
14
14
|
try {
|
|
15
15
|
const response = await fetch(url);
|
|
16
|
-
|
|
16
|
+
const json = await response.json();
|
|
17
|
+
return response.status === 200 && json.value?.ready === true;
|
|
17
18
|
}
|
|
18
19
|
catch (error) {
|
|
19
|
-
console.error(`Failed to connect to WebDriverAgent: ${error}`);
|
|
20
|
+
// console.error(`Failed to connect to WebDriverAgent: ${error}`);
|
|
20
21
|
return false;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -191,6 +192,12 @@ class WebDriverAgent {
|
|
|
191
192
|
});
|
|
192
193
|
});
|
|
193
194
|
}
|
|
195
|
+
async getScreenshot() {
|
|
196
|
+
const url = `http://${this.host}:${this.port}/screenshot`;
|
|
197
|
+
const response = await fetch(url);
|
|
198
|
+
const json = await response.json();
|
|
199
|
+
return Buffer.from(json.value, "base64");
|
|
200
|
+
}
|
|
194
201
|
async swipe(direction) {
|
|
195
202
|
await this.withinSession(async (sessionUrl) => {
|
|
196
203
|
const screenSize = await this.getScreenSize(sessionUrl);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobilenext/mobile-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Mobile MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,8 +23,6 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
26
|
-
"@types/commander": "^2.12.0",
|
|
27
|
-
"@types/express": "^5.0.3",
|
|
28
26
|
"commander": "^14.0.0",
|
|
29
27
|
"express": "^5.1.0",
|
|
30
28
|
"fast-xml-parser": "^5.0.9",
|
|
@@ -34,6 +32,8 @@
|
|
|
34
32
|
"@eslint/eslintrc": "^3.2.0",
|
|
35
33
|
"@eslint/js": "^9.19.0",
|
|
36
34
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
35
|
+
"@types/commander": "^2.12.0",
|
|
36
|
+
"@types/express": "^5.0.3",
|
|
37
37
|
"@types/mocha": "^10.0.10",
|
|
38
38
|
"@types/node": "^22.13.10",
|
|
39
39
|
"@typescript-eslint/eslint-plugin": "^8.28.0",
|