@mobilenext/mobile-mcp 0.0.26 → 0.0.27
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/lib/android.js +116 -0
- package/lib/image-utils.js +93 -2
- package/lib/ios.js +36 -0
- package/lib/iphone-simulator.js +58 -0
- package/lib/server.js +21 -2
- package/package.json +1 -1
package/lib/android.js
CHANGED
|
@@ -336,6 +336,122 @@ 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
|
+
}
|
|
339
455
|
async getUiAutomatorDump() {
|
|
340
456
|
for (let tries = 0; tries < 10; tries++) {
|
|
341
457
|
const dump = this.adb("exec-out", "uiautomator", "dump", "/dev/tty").toString();
|
package/lib/image-utils.js
CHANGED
|
@@ -1,7 +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
|
-
exports.isImageMagickInstalled = exports.Image = exports.ImageTransformer = void 0;
|
|
6
|
+
exports.isScalingAvailable = exports.isImageMagickInstalled = exports.isSipsInstalled = exports.Image = exports.ImageTransformer = void 0;
|
|
4
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const logger_1 = require("./logger");
|
|
5
12
|
const DEFAULT_JPEG_QUALITY = 75;
|
|
6
13
|
class ImageTransformer {
|
|
7
14
|
buffer;
|
|
@@ -25,7 +32,71 @@ class ImageTransformer {
|
|
|
25
32
|
return this;
|
|
26
33
|
}
|
|
27
34
|
toBuffer() {
|
|
28
|
-
|
|
35
|
+
if ((0, exports.isSipsInstalled)()) {
|
|
36
|
+
try {
|
|
37
|
+
return this.toBufferWithSips();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
(0, logger_1.trace)(`Sips failed, falling back to ImageMagick: ${error}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return this.toBufferWithImageMagick();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
(0, logger_1.trace)(`ImageMagick failed: ${error}`);
|
|
48
|
+
throw new Error("Image scaling unavailable (requires Sips or ImageMagick).");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
qualityToSips(q) {
|
|
52
|
+
if (q >= 90) {
|
|
53
|
+
return "best";
|
|
54
|
+
}
|
|
55
|
+
if (q >= 75) {
|
|
56
|
+
return "high";
|
|
57
|
+
}
|
|
58
|
+
if (q >= 50) {
|
|
59
|
+
return "normal";
|
|
60
|
+
}
|
|
61
|
+
return "low";
|
|
62
|
+
}
|
|
63
|
+
toBufferWithSips() {
|
|
64
|
+
const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "image-"));
|
|
65
|
+
const inputFile = node_path_1.default.join(tempDir, "input");
|
|
66
|
+
const outputFile = node_path_1.default.join(tempDir, `output.${this.newFormat === "jpg" ? "jpg" : "png"}`);
|
|
67
|
+
try {
|
|
68
|
+
node_fs_1.default.writeFileSync(inputFile, this.buffer);
|
|
69
|
+
const args = ["-s", "format", this.newFormat === "jpg" ? "jpeg" : "png"];
|
|
70
|
+
if (this.newFormat === "jpg") {
|
|
71
|
+
args.push("-s", "formatOptions", this.qualityToSips(this.jpegOptions.quality));
|
|
72
|
+
}
|
|
73
|
+
args.push("-Z", `${this.newWidth}`);
|
|
74
|
+
args.push("--out", outputFile);
|
|
75
|
+
args.push(inputFile);
|
|
76
|
+
(0, logger_1.trace)(`Running sips command: /usr/bin/sips ${args.join(" ")}`);
|
|
77
|
+
const proc = (0, child_process_1.spawnSync)("/usr/bin/sips", args, {
|
|
78
|
+
maxBuffer: 8 * 1024 * 1024
|
|
79
|
+
});
|
|
80
|
+
if (proc.status !== 0) {
|
|
81
|
+
throw new Error(`Sips failed with status ${proc.status}`);
|
|
82
|
+
}
|
|
83
|
+
const outputBuffer = node_fs_1.default.readFileSync(outputFile);
|
|
84
|
+
(0, logger_1.trace)("Sips returned buffer of size: " + outputBuffer.length);
|
|
85
|
+
return outputBuffer;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
try {
|
|
89
|
+
node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Ignore cleanup errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
toBufferWithImageMagick() {
|
|
97
|
+
const magickArgs = ["-", "-resize", `${this.newWidth}x`, "-quality", `${this.jpegOptions.quality}`, `${this.newFormat}:-`];
|
|
98
|
+
(0, logger_1.trace)(`Running magick command: magick ${magickArgs.join(" ")}`);
|
|
99
|
+
const proc = (0, child_process_1.spawnSync)("magick", magickArgs, {
|
|
29
100
|
maxBuffer: 8 * 1024 * 1024,
|
|
30
101
|
input: this.buffer
|
|
31
102
|
});
|
|
@@ -49,6 +120,22 @@ class Image {
|
|
|
49
120
|
}
|
|
50
121
|
}
|
|
51
122
|
exports.Image = Image;
|
|
123
|
+
const isDarwin = () => {
|
|
124
|
+
return node_os_1.default.platform() === "darwin";
|
|
125
|
+
};
|
|
126
|
+
const isSipsInstalled = () => {
|
|
127
|
+
if (!isDarwin()) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
(0, child_process_1.execFileSync)("/usr/bin/sips", ["--version"]);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
exports.isSipsInstalled = isSipsInstalled;
|
|
52
139
|
const isImageMagickInstalled = () => {
|
|
53
140
|
try {
|
|
54
141
|
return (0, child_process_1.execFileSync)("magick", ["--version"])
|
|
@@ -62,3 +149,7 @@ const isImageMagickInstalled = () => {
|
|
|
62
149
|
}
|
|
63
150
|
};
|
|
64
151
|
exports.isImageMagickInstalled = isImageMagickInstalled;
|
|
152
|
+
const isScalingAvailable = () => {
|
|
153
|
+
return (0, exports.isImageMagickInstalled)() || (0, exports.isSipsInstalled)();
|
|
154
|
+
};
|
|
155
|
+
exports.isScalingAvailable = isScalingAvailable;
|
package/lib/ios.js
CHANGED
|
@@ -145,6 +145,42 @@ 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
|
+
}
|
|
148
184
|
}
|
|
149
185
|
exports.IosRobot = IosRobot;
|
|
150
186
|
class IosManager {
|
package/lib/iphone-simulator.js
CHANGED
|
@@ -124,6 +124,64 @@ 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
|
+
}
|
|
127
185
|
}
|
|
128
186
|
exports.Simctl = Simctl;
|
|
129
187
|
class SimctlManager {
|
package/lib/server.js
CHANGED
|
@@ -330,8 +330,8 @@ const createMcpServer = () => {
|
|
|
330
330
|
if (pngSize.width <= 0 || pngSize.height <= 0) {
|
|
331
331
|
throw new robot_1.ActionableError("Screenshot is invalid. Please try again.");
|
|
332
332
|
}
|
|
333
|
-
if ((0, image_utils_1.
|
|
334
|
-
(0, logger_1.trace)("
|
|
333
|
+
if ((0, image_utils_1.isScalingAvailable)()) {
|
|
334
|
+
(0, logger_1.trace)("Image scaling is available, resizing screenshot");
|
|
335
335
|
const image = image_utils_1.Image.fromBuffer(screenshot);
|
|
336
336
|
const beforeSize = screenshot.length;
|
|
337
337
|
screenshot = image.resize(Math.floor(pngSize.width / screenSize.scale))
|
|
@@ -369,6 +369,25 @@ const createMcpServer = () => {
|
|
|
369
369
|
const orientation = await robot.getOrientation();
|
|
370
370
|
return `Current device orientation is ${orientation}`;
|
|
371
371
|
});
|
|
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
|
+
*/
|
|
372
391
|
// async check for latest agent version
|
|
373
392
|
checkForLatestAgentVersion().then();
|
|
374
393
|
return server;
|