@mobilenext/mobile-mcp 0.0.27 → 0.0.29
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 +10 -1
- package/lib/android.js +0 -116
- package/lib/ios.js +0 -36
- package/lib/iphone-simulator.js +0 -58
- package/lib/server.js +61 -101
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -25,8 +25,11 @@ This server allows Agents and LLMs to interact with native iOS/Android applicati
|
|
|
25
25
|
</h4>
|
|
26
26
|
|
|
27
27
|
<h4 align="center">
|
|
28
|
+
<a href="https://github.com/mobile-next/mobile-mcp/wiki">
|
|
29
|
+
<img src="https://img.shields.io/badge/documentation-wiki-blue" alt="wiki" />
|
|
30
|
+
</a>
|
|
28
31
|
<a href="http://mobilenexthq.com/join-slack">
|
|
29
|
-
<img src="https://img.shields.io/badge/join-Slack-blueviolet?logo=slack&style=flat" alt="
|
|
32
|
+
<img src="https://img.shields.io/badge/join-Slack-blueviolet?logo=slack&style=flat" alt="join on Slack" />
|
|
30
33
|
</a>
|
|
31
34
|
</h4>
|
|
32
35
|
|
|
@@ -102,6 +105,12 @@ Setup our MCP with Cline, Cursor, Claude, VS Code, Github Copilot:
|
|
|
102
105
|
claude mcp add mobile -- npx -y @mobilenext/mobile-mcp@latest
|
|
103
106
|
```
|
|
104
107
|
|
|
108
|
+
[Gemini CLI:](https://cloud.google.com/gemini/docs/codeassist/gemini-cli)
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
gemini mcp add mobile npx -y @mobilenext/mobile-mcp@latest
|
|
112
|
+
```
|
|
113
|
+
|
|
105
114
|
[Read more in our wiki](https://github.com/mobile-next/mobile-mcp/wiki)! 🚀
|
|
106
115
|
|
|
107
116
|
|
package/lib/android.js
CHANGED
|
@@ -336,122 +336,6 @@ class AndroidRobot {
|
|
|
336
336
|
const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
|
|
337
337
|
return rotation === "0" ? "portrait" : "landscape";
|
|
338
338
|
}
|
|
339
|
-
async getDeviceLogs(options) {
|
|
340
|
-
const timeWindow = options?.timeWindow || "1m";
|
|
341
|
-
const filter = options?.filter;
|
|
342
|
-
const processFilter = options?.process;
|
|
343
|
-
let packageFilter = null;
|
|
344
|
-
let searchQuery = null;
|
|
345
|
-
let effectiveFilter = filter;
|
|
346
|
-
// For Android: if both process and filter are provided, combine them as "package:<process> <filter>"
|
|
347
|
-
if (processFilter && filter && !filter.includes("package:")) {
|
|
348
|
-
effectiveFilter = `package:${processFilter} ${filter}`;
|
|
349
|
-
}
|
|
350
|
-
else if (processFilter && !filter) {
|
|
351
|
-
effectiveFilter = `package:${processFilter}`;
|
|
352
|
-
}
|
|
353
|
-
// Handle Android package filtering syntax
|
|
354
|
-
if (effectiveFilter) {
|
|
355
|
-
if (effectiveFilter.startsWith("package:mine")) {
|
|
356
|
-
// Filter to user apps only
|
|
357
|
-
const query = effectiveFilter.replace("package:mine", "").trim();
|
|
358
|
-
searchQuery = query || null;
|
|
359
|
-
// Will filter user packages in post-processing
|
|
360
|
-
}
|
|
361
|
-
else if (effectiveFilter.includes("package:")) {
|
|
362
|
-
// Handle specific package filters like package:com.example.app search_term
|
|
363
|
-
const packageMatch = effectiveFilter.match(/package:([^\s]+)(?:\s+(.+))?/);
|
|
364
|
-
if (packageMatch) {
|
|
365
|
-
packageFilter = packageMatch[1];
|
|
366
|
-
searchQuery = packageMatch[2] || null;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
// Regular search filter
|
|
371
|
-
searchQuery = effectiveFilter;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const args = ["shell", "logcat"];
|
|
375
|
-
if (timeWindow) {
|
|
376
|
-
// Calculate timestamp for time-based filtering using -T
|
|
377
|
-
const timeInSeconds = this.parseTimeWindow(timeWindow);
|
|
378
|
-
const startTime = new Date(Date.now() - (timeInSeconds * 1000));
|
|
379
|
-
// Format as MM-dd HH:mm:ss.mmm
|
|
380
|
-
const month = String(startTime.getMonth() + 1).padStart(2, "0");
|
|
381
|
-
const day = String(startTime.getDate()).padStart(2, "0");
|
|
382
|
-
const hours = String(startTime.getHours()).padStart(2, "0");
|
|
383
|
-
const minutes = String(startTime.getMinutes()).padStart(2, "0");
|
|
384
|
-
const seconds = String(startTime.getSeconds()).padStart(2, "0");
|
|
385
|
-
const milliseconds = String(startTime.getMilliseconds()).padStart(3, "0");
|
|
386
|
-
const timeFormat = `${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
387
|
-
args.push("-T", timeFormat);
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
args.push("-d");
|
|
391
|
-
}
|
|
392
|
-
// Add package filtering directly to logcat if we have a specific package
|
|
393
|
-
if (packageFilter && packageFilter !== "mine") {
|
|
394
|
-
// Use logcat's native package filtering with --pid
|
|
395
|
-
try {
|
|
396
|
-
// First get the PID(s) for this package
|
|
397
|
-
const pidOutput = this.adb("shell", "pidof", packageFilter).toString().trim();
|
|
398
|
-
if (pidOutput) {
|
|
399
|
-
const pids = pidOutput.split(/\s+/);
|
|
400
|
-
for (const pid of pids) {
|
|
401
|
-
args.push("--pid", pid);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
catch (error) {
|
|
406
|
-
// If pidof fails, fall back to post-processing
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const output = this.adb(...args).toString();
|
|
410
|
-
// Post-process filtering
|
|
411
|
-
const lines = output.split("\n").filter(line => line.trim());
|
|
412
|
-
let filteredLines = lines;
|
|
413
|
-
// Filter by specific package if provided (fallback if --pid didn't work)
|
|
414
|
-
if (packageFilter && packageFilter !== "mine") {
|
|
415
|
-
filteredLines = filteredLines.filter(line => {
|
|
416
|
-
return line.includes(packageFilter);
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
// Filter for user packages if package:mine
|
|
420
|
-
if (filter && filter.startsWith("package:mine")) {
|
|
421
|
-
filteredLines = filteredLines.filter(line => {
|
|
422
|
-
// Look for user app indicators - avoid system/Android logs
|
|
423
|
-
return !line.includes("com.android.") &&
|
|
424
|
-
!line.includes("android.") &&
|
|
425
|
-
!line.includes("system_") &&
|
|
426
|
-
(line.includes("com.") || line.includes("io.") || line.includes("net.") || line.includes("app."));
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
// Apply text search if provided
|
|
430
|
-
if (searchQuery) {
|
|
431
|
-
filteredLines = filteredLines.filter(line => {
|
|
432
|
-
return line.toLowerCase().includes(searchQuery.toLowerCase());
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
return filteredLines.join("\n");
|
|
436
|
-
}
|
|
437
|
-
parseTimeWindow(timeWindow) {
|
|
438
|
-
const match = timeWindow.match(/^(\d+)([smh])$/);
|
|
439
|
-
if (!match) {
|
|
440
|
-
return 60;
|
|
441
|
-
}
|
|
442
|
-
const value = parseInt(match[1], 10);
|
|
443
|
-
const unit = match[2];
|
|
444
|
-
switch (unit) {
|
|
445
|
-
case "s":
|
|
446
|
-
return value;
|
|
447
|
-
case "m":
|
|
448
|
-
return value * 60;
|
|
449
|
-
case "h":
|
|
450
|
-
return value * 3600;
|
|
451
|
-
default:
|
|
452
|
-
return 60;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
339
|
async getUiAutomatorDump() {
|
|
456
340
|
for (let tries = 0; tries < 10; tries++) {
|
|
457
341
|
const dump = this.adb("exec-out", "uiautomator", "dump", "/dev/tty").toString();
|
package/lib/ios.js
CHANGED
|
@@ -145,42 +145,6 @@ class IosRobot {
|
|
|
145
145
|
const wda = await this.wda();
|
|
146
146
|
return await wda.getOrientation();
|
|
147
147
|
}
|
|
148
|
-
async getDeviceLogs(options) {
|
|
149
|
-
await this.assertTunnelRunning();
|
|
150
|
-
const timeWindow = options?.timeWindow || "1m";
|
|
151
|
-
const filter = options?.filter;
|
|
152
|
-
const args = ["syslog"];
|
|
153
|
-
if (timeWindow) {
|
|
154
|
-
const timeInSeconds = this.parseTimeWindow(timeWindow);
|
|
155
|
-
args.push("--since");
|
|
156
|
-
args.push(`${timeInSeconds}s`);
|
|
157
|
-
}
|
|
158
|
-
let output = await this.ios(...args);
|
|
159
|
-
if (filter) {
|
|
160
|
-
const lines = output.split("\n");
|
|
161
|
-
const filteredLines = lines.filter(line => line.toLowerCase().includes(filter.toLowerCase()));
|
|
162
|
-
output = filteredLines.join("\n");
|
|
163
|
-
}
|
|
164
|
-
return output;
|
|
165
|
-
}
|
|
166
|
-
parseTimeWindow(timeWindow) {
|
|
167
|
-
const match = timeWindow.match(/^(\d+)([smh])$/);
|
|
168
|
-
if (!match) {
|
|
169
|
-
return 60;
|
|
170
|
-
}
|
|
171
|
-
const value = parseInt(match[1], 10);
|
|
172
|
-
const unit = match[2];
|
|
173
|
-
switch (unit) {
|
|
174
|
-
case "s":
|
|
175
|
-
return value;
|
|
176
|
-
case "m":
|
|
177
|
-
return value * 60;
|
|
178
|
-
case "h":
|
|
179
|
-
return value * 3600;
|
|
180
|
-
default:
|
|
181
|
-
return 60;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
148
|
}
|
|
185
149
|
exports.IosRobot = IosRobot;
|
|
186
150
|
class IosManager {
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -124,64 +124,6 @@ class Simctl {
|
|
|
124
124
|
const wda = await this.wda();
|
|
125
125
|
return wda.getOrientation();
|
|
126
126
|
}
|
|
127
|
-
async getDeviceLogs(options) {
|
|
128
|
-
const timeWindow = options?.timeWindow || "1m";
|
|
129
|
-
const filter = options?.filter;
|
|
130
|
-
const processFilter = options?.process;
|
|
131
|
-
const deviceUuid = this.simulatorUuid;
|
|
132
|
-
let predicate = "";
|
|
133
|
-
let currentApp = null;
|
|
134
|
-
// If a specific process is provided, use that
|
|
135
|
-
if (processFilter) {
|
|
136
|
-
currentApp = processFilter;
|
|
137
|
-
predicate = `subsystem == "${processFilter}"`;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
// Try to detect currently running user apps from installed apps
|
|
141
|
-
try {
|
|
142
|
-
const runningApps = await this.listApps();
|
|
143
|
-
// Filter to non-Apple user apps
|
|
144
|
-
const userApps = runningApps
|
|
145
|
-
.map((app) => app.packageName)
|
|
146
|
-
.filter((appId) => !appId.startsWith("com.apple.") && appId.includes("."));
|
|
147
|
-
if (userApps.length > 0) {
|
|
148
|
-
// For now, just use the first user app found
|
|
149
|
-
// In the future, we could try to detect which is actually running
|
|
150
|
-
currentApp = userApps[0];
|
|
151
|
-
predicate = `subsystem == "${currentApp}"`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
// Failed to get apps, continue with fallback
|
|
156
|
-
}
|
|
157
|
-
// If no user app detected, use broader filter for non-Apple apps
|
|
158
|
-
if (!predicate) {
|
|
159
|
-
predicate = "subsystem CONTAINS \"com.\" AND NOT subsystem BEGINSWITH \"com.apple.\"";
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (filter) {
|
|
163
|
-
predicate += ` AND composedMessage CONTAINS[c] "${filter}"`;
|
|
164
|
-
}
|
|
165
|
-
const args = [
|
|
166
|
-
"spawn", deviceUuid, "log", "show",
|
|
167
|
-
"--last", timeWindow,
|
|
168
|
-
"--predicate", predicate,
|
|
169
|
-
"--info",
|
|
170
|
-
"--debug"
|
|
171
|
-
];
|
|
172
|
-
try {
|
|
173
|
-
const logs = this.simctl(...args).toString();
|
|
174
|
-
const appInfo = currentApp ? ` (focused on: ${currentApp})` : " (all non-Apple apps)";
|
|
175
|
-
const debugInfo = `DEBUG: Using predicate: ${predicate}${appInfo}\n\n`;
|
|
176
|
-
return `${debugInfo}${logs}`;
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
if (error instanceof Error && error.message.includes("No logging subsystem")) {
|
|
180
|
-
return "No logs found for the current running applications.";
|
|
181
|
-
}
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
127
|
}
|
|
186
128
|
exports.Simctl = Simctl;
|
|
187
129
|
class SimctlManager {
|
package/lib/server.js
CHANGED
|
@@ -111,44 +111,30 @@ const createMcpServer = () => {
|
|
|
111
111
|
}
|
|
112
112
|
};
|
|
113
113
|
posthog("launch", {}).then();
|
|
114
|
-
let robot;
|
|
115
114
|
const simulatorManager = new iphone_simulator_1.SimctlManager();
|
|
116
|
-
const
|
|
117
|
-
if (!robot) {
|
|
118
|
-
throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
|
|
119
|
-
}
|
|
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 () => {
|
|
115
|
+
const getRobotFromDevice = (device) => {
|
|
124
116
|
const iosManager = new ios_1.IosManager();
|
|
125
117
|
const androidManager = new android_1.AndroidDeviceManager();
|
|
126
118
|
const simulators = simulatorManager.listBootedSimulators();
|
|
127
119
|
const androidDevices = androidManager.getConnectedDevices();
|
|
128
120
|
const iosDevices = iosManager.listDevices();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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}`;
|
|
121
|
+
// Check if it's a simulator
|
|
122
|
+
const simulator = simulators.find(s => s.name === device);
|
|
123
|
+
if (simulator) {
|
|
124
|
+
return simulatorManager.getSimulator(device);
|
|
140
125
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
126
|
+
// Check if it's an Android device
|
|
127
|
+
const androidDevice = androidDevices.find(d => d.deviceId === device);
|
|
128
|
+
if (androidDevice) {
|
|
129
|
+
return new android_1.AndroidRobot(device);
|
|
144
130
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
// Check if it's an iOS device
|
|
132
|
+
const iosDevice = iosDevices.find(d => d.deviceId === device);
|
|
133
|
+
if (iosDevice) {
|
|
134
|
+
return new ios_1.IosRobot(device);
|
|
148
135
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
136
|
+
throw new robot_1.ActionableError(`Device "${device}" not found. Use the mobile_list_available_devices tool to see available devices.`);
|
|
137
|
+
};
|
|
152
138
|
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.", {
|
|
153
139
|
noParams
|
|
154
140
|
}, async ({}) => {
|
|
@@ -176,71 +162,58 @@ const createMcpServer = () => {
|
|
|
176
162
|
}
|
|
177
163
|
return resp.join("\n");
|
|
178
164
|
});
|
|
179
|
-
tool("mobile_use_device", "Select a device to use. This can be a simulator or an Android device. Use the list_available_devices tool to get a list of available devices.", {
|
|
180
|
-
device: zod_1.z.string().describe("The name of the device to select"),
|
|
181
|
-
deviceType: zod_1.z.enum(["simulator", "ios", "android"]).describe("The type of device to select"),
|
|
182
|
-
}, async ({ device, deviceType }) => {
|
|
183
|
-
switch (deviceType) {
|
|
184
|
-
case "simulator":
|
|
185
|
-
robot = simulatorManager.getSimulator(device);
|
|
186
|
-
break;
|
|
187
|
-
case "ios":
|
|
188
|
-
robot = new ios_1.IosRobot(device);
|
|
189
|
-
break;
|
|
190
|
-
case "android":
|
|
191
|
-
robot = new android_1.AndroidRobot(device);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
return `Selected device: ${device}`;
|
|
195
|
-
});
|
|
196
165
|
tool("mobile_list_apps", "List all the installed apps on the device", {
|
|
197
|
-
|
|
198
|
-
}, async ({}) => {
|
|
199
|
-
|
|
166
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
167
|
+
}, async ({ device }) => {
|
|
168
|
+
const robot = getRobotFromDevice(device);
|
|
200
169
|
const result = await robot.listApps();
|
|
201
170
|
return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
|
|
202
171
|
});
|
|
203
172
|
tool("mobile_launch_app", "Launch an app on mobile device. Use this to open a specific app. You can find the package name of the app by calling list_apps_on_device.", {
|
|
173
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
204
174
|
packageName: zod_1.z.string().describe("The package name of the app to launch"),
|
|
205
|
-
}, async ({ packageName }) => {
|
|
206
|
-
|
|
175
|
+
}, async ({ device, packageName }) => {
|
|
176
|
+
const robot = getRobotFromDevice(device);
|
|
207
177
|
await robot.launchApp(packageName);
|
|
208
178
|
return `Launched app ${packageName}`;
|
|
209
179
|
});
|
|
210
180
|
tool("mobile_terminate_app", "Stop and terminate an app on mobile device", {
|
|
181
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
211
182
|
packageName: zod_1.z.string().describe("The package name of the app to terminate"),
|
|
212
|
-
}, async ({ packageName }) => {
|
|
213
|
-
|
|
183
|
+
}, async ({ device, packageName }) => {
|
|
184
|
+
const robot = getRobotFromDevice(device);
|
|
214
185
|
await robot.terminateApp(packageName);
|
|
215
186
|
return `Terminated app ${packageName}`;
|
|
216
187
|
});
|
|
217
188
|
tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {
|
|
218
|
-
|
|
219
|
-
}, async ({}) => {
|
|
220
|
-
|
|
189
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
190
|
+
}, async ({ device }) => {
|
|
191
|
+
const robot = getRobotFromDevice(device);
|
|
221
192
|
const screenSize = await robot.getScreenSize();
|
|
222
193
|
return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
|
|
223
194
|
});
|
|
224
195
|
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.", {
|
|
196
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
225
197
|
x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
|
|
226
198
|
y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
|
|
227
|
-
}, async ({ x, y }) => {
|
|
228
|
-
|
|
199
|
+
}, async ({ device, x, y }) => {
|
|
200
|
+
const robot = getRobotFromDevice(device);
|
|
229
201
|
await robot.tap(x, y);
|
|
230
202
|
return `Clicked on screen at coordinates: ${x}, ${y}`;
|
|
231
203
|
});
|
|
232
204
|
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.", {
|
|
205
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
233
206
|
x: zod_1.z.number().describe("The x coordinate to long press on the screen, in pixels"),
|
|
234
207
|
y: zod_1.z.number().describe("The y coordinate to long press on the screen, in pixels"),
|
|
235
|
-
}, async ({ x, y }) => {
|
|
236
|
-
|
|
208
|
+
}, async ({ device, x, y }) => {
|
|
209
|
+
const robot = getRobotFromDevice(device);
|
|
237
210
|
await robot.longPress(x, y);
|
|
238
211
|
return `Long pressed on screen at coordinates: ${x}, ${y}`;
|
|
239
212
|
});
|
|
240
213
|
tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
|
|
241
|
-
|
|
242
|
-
}, async ({}) => {
|
|
243
|
-
|
|
214
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
215
|
+
}, async ({ device }) => {
|
|
216
|
+
const robot = getRobotFromDevice(device);
|
|
244
217
|
const elements = await robot.getElementsOnScreen();
|
|
245
218
|
const result = elements.map(element => {
|
|
246
219
|
const out = {
|
|
@@ -265,26 +238,29 @@ const createMcpServer = () => {
|
|
|
265
238
|
return `Found these elements on screen: ${JSON.stringify(result)}`;
|
|
266
239
|
});
|
|
267
240
|
tool("mobile_press_button", "Press a button on device", {
|
|
241
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
268
242
|
button: zod_1.z.string().describe("The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER, DPAD_CENTER (android tv only), DPAD_UP (android tv only), DPAD_DOWN (android tv only), DPAD_LEFT (android tv only), DPAD_RIGHT (android tv only)"),
|
|
269
|
-
}, async ({ button }) => {
|
|
270
|
-
|
|
243
|
+
}, async ({ device, button }) => {
|
|
244
|
+
const robot = getRobotFromDevice(device);
|
|
271
245
|
await robot.pressButton(button);
|
|
272
246
|
return `Pressed the button: ${button}`;
|
|
273
247
|
});
|
|
274
248
|
tool("mobile_open_url", "Open a URL in browser on device", {
|
|
249
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
275
250
|
url: zod_1.z.string().describe("The URL to open"),
|
|
276
|
-
}, async ({ url }) => {
|
|
277
|
-
|
|
251
|
+
}, async ({ device, url }) => {
|
|
252
|
+
const robot = getRobotFromDevice(device);
|
|
278
253
|
await robot.openUrl(url);
|
|
279
254
|
return `Opened URL: ${url}`;
|
|
280
255
|
});
|
|
281
|
-
tool("
|
|
256
|
+
tool("mobile_swipe_on_screen", "Swipe on the screen", {
|
|
257
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
282
258
|
direction: zod_1.z.enum(["up", "down", "left", "right"]).describe("The direction to swipe"),
|
|
283
259
|
x: zod_1.z.number().optional().describe("The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
284
260
|
y: zod_1.z.number().optional().describe("The y coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
|
|
285
261
|
distance: zod_1.z.number().optional().describe("The distance to swipe in pixels. Defaults to 400 pixels for iOS or 30% of screen dimension for Android"),
|
|
286
|
-
}, async ({ direction, x, y, distance }) => {
|
|
287
|
-
|
|
262
|
+
}, async ({ device, direction, x, y, distance }) => {
|
|
263
|
+
const robot = getRobotFromDevice(device);
|
|
288
264
|
if (x !== undefined && y !== undefined) {
|
|
289
265
|
// Use coordinate-based swipe
|
|
290
266
|
await robot.swipeFromCoordinate(x, y, direction, distance);
|
|
@@ -298,10 +274,11 @@ const createMcpServer = () => {
|
|
|
298
274
|
}
|
|
299
275
|
});
|
|
300
276
|
tool("mobile_type_keys", "Type text into the focused element", {
|
|
277
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
301
278
|
text: zod_1.z.string().describe("The text to type"),
|
|
302
279
|
submit: zod_1.z.boolean().describe("Whether to submit the text. If true, the text will be submitted as if the user pressed the enter key."),
|
|
303
|
-
}, async ({ text, submit }) => {
|
|
304
|
-
|
|
280
|
+
}, async ({ device, text, submit }) => {
|
|
281
|
+
const robot = getRobotFromDevice(device);
|
|
305
282
|
await robot.sendKeys(text);
|
|
306
283
|
if (submit) {
|
|
307
284
|
await robot.pressButton("ENTER");
|
|
@@ -309,18 +286,19 @@ const createMcpServer = () => {
|
|
|
309
286
|
return `Typed text: ${text}`;
|
|
310
287
|
});
|
|
311
288
|
tool("mobile_save_screenshot", "Save a screenshot of the mobile device to a file", {
|
|
289
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
312
290
|
saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
|
|
313
|
-
}, async ({ saveTo }) => {
|
|
314
|
-
|
|
291
|
+
}, async ({ device, saveTo }) => {
|
|
292
|
+
const robot = getRobotFromDevice(device);
|
|
315
293
|
const screenshot = await robot.getScreenshot();
|
|
316
294
|
node_fs_1.default.writeFileSync(saveTo, screenshot);
|
|
317
295
|
return `Screenshot saved to: ${saveTo}`;
|
|
318
296
|
});
|
|
319
297
|
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.", {
|
|
320
|
-
|
|
321
|
-
}, async ({}) => {
|
|
322
|
-
requireRobot();
|
|
298
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
299
|
+
}, async ({ device }) => {
|
|
323
300
|
try {
|
|
301
|
+
const robot = getRobotFromDevice(device);
|
|
324
302
|
const screenSize = await robot.getScreenSize();
|
|
325
303
|
let screenshot = await robot.getScreenshot();
|
|
326
304
|
let mimeType = "image/png";
|
|
@@ -356,38 +334,20 @@ const createMcpServer = () => {
|
|
|
356
334
|
}
|
|
357
335
|
});
|
|
358
336
|
tool("mobile_set_orientation", "Change the screen orientation of the device", {
|
|
337
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
|
|
359
338
|
orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
|
|
360
|
-
}, async ({ orientation }) => {
|
|
361
|
-
|
|
339
|
+
}, async ({ device, orientation }) => {
|
|
340
|
+
const robot = getRobotFromDevice(device);
|
|
362
341
|
await robot.setOrientation(orientation);
|
|
363
342
|
return `Changed device orientation to ${orientation}`;
|
|
364
343
|
});
|
|
365
344
|
tool("mobile_get_orientation", "Get the current screen orientation of the device", {
|
|
366
|
-
|
|
367
|
-
}, async () => {
|
|
368
|
-
|
|
345
|
+
device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
|
|
346
|
+
}, async ({ device }) => {
|
|
347
|
+
const robot = getRobotFromDevice(device);
|
|
369
348
|
const orientation = await robot.getOrientation();
|
|
370
349
|
return `Current device orientation is ${orientation}`;
|
|
371
350
|
});
|
|
372
|
-
/*
|
|
373
|
-
tool(
|
|
374
|
-
"mobile_get_logs",
|
|
375
|
-
"Get device logs",
|
|
376
|
-
{
|
|
377
|
-
timeWindow: z.string().optional().describe("Time window to look back (e.g., '5m' for 5 minutes, '1h' for 1 hour). Defaults to '1m'"),
|
|
378
|
-
filter: z.string().optional().describe("Filter logs containing this query (case-insensitive). For Android: supports 'package:mine <query>' (user apps only), 'package:com.app.bundle <query>' (specific app), or '<query>' (text search). For iOS: simple text search only."),
|
|
379
|
-
process: z.string().optional().describe("Filter logs to a specific process/app bundle ID")
|
|
380
|
-
},
|
|
381
|
-
async ({ timeWindow, filter, process }) => {
|
|
382
|
-
requireRobot();
|
|
383
|
-
const logs = await robot!.getDeviceLogs({ timeWindow, filter, process });
|
|
384
|
-
const filterText = filter ? ` (filtered by: ${filter})` : "";
|
|
385
|
-
const processText = process ? ` (process: ${process})` : "";
|
|
386
|
-
const timeText = timeWindow ? ` from last ${timeWindow}` : "";
|
|
387
|
-
return `Device logs${timeText}${filterText}${processText}:\n${logs}`;
|
|
388
|
-
}
|
|
389
|
-
);
|
|
390
|
-
*/
|
|
391
351
|
// async check for latest agent version
|
|
392
352
|
checkForLatestAgentVersion().then();
|
|
393
353
|
return server;
|
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.
|
|
4
|
+
"version": "0.0.29",
|
|
5
5
|
"description": "Mobile MCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"lib"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "
|
|
28
|
-
"commander": "
|
|
29
|
-
"express": "
|
|
30
|
-
"fast-xml-parser": "
|
|
31
|
-
"zod-to-json-schema": "
|
|
27
|
+
"@modelcontextprotocol/sdk": "1.13.2",
|
|
28
|
+
"commander": "14.0.0",
|
|
29
|
+
"express": "5.1.0",
|
|
30
|
+
"fast-xml-parser": "5.2.5",
|
|
31
|
+
"zod-to-json-schema": "3.24.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/eslintrc": "^3.2.0",
|