mobile-debug-mcp 0.7.0 → 0.9.0
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 +18 -443
- package/dist/android/interact.js +96 -1
- package/dist/android/utils.js +404 -12
- package/dist/ios/interact.js +105 -0
- package/dist/ios/utils.js +154 -0
- package/dist/resolve-device.js +70 -0
- package/dist/server.js +126 -194
- package/dist/tools/app.js +45 -0
- package/dist/tools/devices.js +5 -0
- package/dist/tools/install.js +47 -0
- package/dist/tools/logs.js +62 -0
- package/dist/tools/screenshot.js +17 -0
- package/dist/tools/ui.js +57 -0
- package/docs/CHANGELOG.md +19 -0
- package/docs/TOOLS.md +272 -0
- package/package.json +6 -2
- package/src/android/interact.ts +100 -1
- package/src/android/utils.ts +395 -10
- package/src/ios/interact.ts +102 -0
- package/src/ios/utils.ts +157 -0
- package/src/resolve-device.ts +80 -0
- package/src/server.ts +149 -276
- package/src/tools/app.ts +46 -0
- package/src/tools/devices.ts +6 -0
- package/src/tools/install.ts +43 -0
- package/src/tools/logs.ts +62 -0
- package/src/tools/screenshot.ts +18 -0
- package/src/tools/ui.ts +62 -0
- package/src/types.ts +7 -0
- package/test/integration/index.ts +8 -0
- package/test/integration/install.integration.ts +64 -0
- package/test/integration/logstream-real.ts +35 -0
- package/test/integration/run-install-android.ts +21 -0
- package/test/integration/run-install-ios.ts +21 -0
- package/test/integration/run-real-test.ts +19 -0
- package/{smoke-test.ts → test/integration/smoke-test.ts} +17 -25
- package/test/integration/test-dist.mjs +41 -0
- package/test/integration/test-dist.ts +41 -0
- package/{test-ui-tree.ts → test/integration/test-ui-tree.ts} +2 -2
- package/test/integration/wait_for_element_real.ts +80 -0
- package/test/unit/index.ts +7 -0
- package/test/unit/install.test.ts +82 -0
- package/test/unit/logparse.test.ts +41 -0
- package/test/unit/logstream.test.ts +46 -0
- package/test/unit/wait_for_element_mock.ts +104 -0
- package/tsconfig.json +1 -1
- package/smoke-test.js +0 -102
- package/test/run-real-test.js +0 -24
- package/test/wait_for_element_mock.js +0 -113
- package/test/wait_for_element_real.js +0 -67
- package/test-ui-tree.js +0 -68
package/dist/ios/utils.js
CHANGED
|
@@ -112,3 +112,157 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
|
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
+
export async function listIOSDevices(appId) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
execFile(XCRUN, ['simctl', 'list', 'devices', '--json'], (err, stdout) => {
|
|
118
|
+
if (err || !stdout)
|
|
119
|
+
return resolve([]);
|
|
120
|
+
try {
|
|
121
|
+
const data = JSON.parse(stdout);
|
|
122
|
+
const devicesMap = data.devices || {};
|
|
123
|
+
const out = [];
|
|
124
|
+
const checks = [];
|
|
125
|
+
for (const runtime in devicesMap) {
|
|
126
|
+
const devices = devicesMap[runtime];
|
|
127
|
+
if (Array.isArray(devices)) {
|
|
128
|
+
for (const device of devices) {
|
|
129
|
+
const info = {
|
|
130
|
+
platform: 'ios',
|
|
131
|
+
id: device.udid,
|
|
132
|
+
osVersion: parseRuntimeName(runtime),
|
|
133
|
+
model: device.name,
|
|
134
|
+
simulator: true
|
|
135
|
+
};
|
|
136
|
+
if (appId) {
|
|
137
|
+
// check if installed
|
|
138
|
+
const p = execCommand(['simctl', 'get_app_container', device.udid, appId, 'data'], device.udid)
|
|
139
|
+
.then(() => { info.appInstalled = true; })
|
|
140
|
+
.catch(() => { info.appInstalled = false; })
|
|
141
|
+
.then(() => { out.push(info); });
|
|
142
|
+
checks.push(p);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
out.push(info);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out));
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
resolve([]);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// --- iOS live log stream support ---
|
|
159
|
+
import { createWriteStream, promises as fsPromises } from 'fs';
|
|
160
|
+
import path from 'path';
|
|
161
|
+
import { parseLogLine } from '../android/utils.js';
|
|
162
|
+
const iosActiveLogStreams = new Map();
|
|
163
|
+
// Test helpers
|
|
164
|
+
export function _setIOSActiveLogStream(sessionId, file) {
|
|
165
|
+
iosActiveLogStreams.set(sessionId, { proc: {}, file });
|
|
166
|
+
}
|
|
167
|
+
export function _clearIOSActiveLogStream(sessionId) {
|
|
168
|
+
iosActiveLogStreams.delete(sessionId);
|
|
169
|
+
}
|
|
170
|
+
export async function startIOSLogStream(bundleId, level = 'error', deviceId = 'booted', sessionId = 'default') {
|
|
171
|
+
try {
|
|
172
|
+
// Build predicate to filter by process or subsystem
|
|
173
|
+
const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`;
|
|
174
|
+
// Prevent multiple streams per session
|
|
175
|
+
if (iosActiveLogStreams.has(sessionId)) {
|
|
176
|
+
try {
|
|
177
|
+
iosActiveLogStreams.get(sessionId).proc.kill();
|
|
178
|
+
}
|
|
179
|
+
catch (e) { }
|
|
180
|
+
iosActiveLogStreams.delete(sessionId);
|
|
181
|
+
}
|
|
182
|
+
// Start simctl log stream: xcrun simctl spawn <device> log stream --style syslog --predicate '<predicate>'
|
|
183
|
+
const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate];
|
|
184
|
+
const proc = spawn(XCRUN, args);
|
|
185
|
+
// Prepare output file
|
|
186
|
+
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
187
|
+
const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`);
|
|
188
|
+
const stream = createWriteStream(file, { flags: 'a' });
|
|
189
|
+
proc.stdout.on('data', (chunk) => {
|
|
190
|
+
const text = chunk.toString();
|
|
191
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
192
|
+
for (const l of lines) {
|
|
193
|
+
// Try to parse with shared parser; parser may be optimized for Android but extracts exceptions and message
|
|
194
|
+
const entry = parseLogLine(l);
|
|
195
|
+
stream.write(JSON.stringify(entry) + '\n');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
proc.stderr.on('data', (chunk) => {
|
|
199
|
+
const text = chunk.toString();
|
|
200
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
201
|
+
for (const l of lines) {
|
|
202
|
+
const entry = { timestamp: '', level: 'E', tag: 'xcrun', message: l };
|
|
203
|
+
stream.write(JSON.stringify(entry) + '\n');
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
proc.on('close', (code) => {
|
|
207
|
+
stream.end();
|
|
208
|
+
iosActiveLogStreams.delete(sessionId);
|
|
209
|
+
});
|
|
210
|
+
iosActiveLogStreams.set(sessionId, { proc, file });
|
|
211
|
+
return { success: true, stream_started: true };
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
return { success: false, error: 'log_stream_start_failed' };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export async function stopIOSLogStream(sessionId = 'default') {
|
|
218
|
+
const entry = iosActiveLogStreams.get(sessionId);
|
|
219
|
+
if (!entry)
|
|
220
|
+
return { success: true };
|
|
221
|
+
try {
|
|
222
|
+
entry.proc.kill();
|
|
223
|
+
}
|
|
224
|
+
catch (e) { }
|
|
225
|
+
iosActiveLogStreams.delete(sessionId);
|
|
226
|
+
return { success: true };
|
|
227
|
+
}
|
|
228
|
+
export async function readIOSLogStreamLines(sessionId = 'default', limit = 100, since) {
|
|
229
|
+
const entry = iosActiveLogStreams.get(sessionId);
|
|
230
|
+
if (!entry)
|
|
231
|
+
return { entries: [] };
|
|
232
|
+
try {
|
|
233
|
+
const data = await fsPromises.readFile(entry.file, 'utf8').catch(() => '');
|
|
234
|
+
if (!data)
|
|
235
|
+
return { entries: [], crash_summary: { crash_detected: false } };
|
|
236
|
+
const lines = data.split(/\r?\n/).filter(Boolean);
|
|
237
|
+
const parsed = lines.map(l => {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(l);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return { message: l, _iso: null, crash: false };
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Minimal since filtering if provided
|
|
246
|
+
let filtered = parsed;
|
|
247
|
+
if (since) {
|
|
248
|
+
let sinceMs = null;
|
|
249
|
+
if (/^\d+$/.test(since))
|
|
250
|
+
sinceMs = Number(since);
|
|
251
|
+
else {
|
|
252
|
+
const sDate = new Date(since);
|
|
253
|
+
if (!isNaN(sDate.getTime()))
|
|
254
|
+
sinceMs = sDate.getTime();
|
|
255
|
+
}
|
|
256
|
+
if (sinceMs !== null) {
|
|
257
|
+
filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const entries = filtered.slice(-Math.max(0, limit));
|
|
261
|
+
const crashEntry = entries.find(e => e.crash);
|
|
262
|
+
const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
|
|
263
|
+
return { entries, crash_summary };
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
return { entries: [], crash_summary: { crash_detected: false } };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { listAndroidDevices } from "./android/utils.js";
|
|
2
|
+
import { listIOSDevices } from "./ios/utils.js";
|
|
3
|
+
function parseNumericVersion(v) {
|
|
4
|
+
if (!v)
|
|
5
|
+
return 0;
|
|
6
|
+
// extract first number groups like 17.0 -> 17.0 or Android 12 -> 12
|
|
7
|
+
const m = v.match(/(\d+)(?:[\.\-](\d+))?/);
|
|
8
|
+
if (!m)
|
|
9
|
+
return 0;
|
|
10
|
+
const major = parseInt(m[1], 10) || 0;
|
|
11
|
+
const minor = parseInt(m[2] || "0", 10) || 0;
|
|
12
|
+
return major + minor / 100;
|
|
13
|
+
}
|
|
14
|
+
export async function listDevices(platform, appId) {
|
|
15
|
+
if (!platform || platform === "android") {
|
|
16
|
+
const android = await listAndroidDevices(appId);
|
|
17
|
+
if (platform === "android")
|
|
18
|
+
return android;
|
|
19
|
+
// if no platform specified, merge with ios below
|
|
20
|
+
const ios = await listIOSDevices(appId);
|
|
21
|
+
return [...android, ...ios];
|
|
22
|
+
}
|
|
23
|
+
return listIOSDevices(appId);
|
|
24
|
+
}
|
|
25
|
+
export async function resolveTargetDevice(opts) {
|
|
26
|
+
const { platform, appId, prefer, deviceId } = opts;
|
|
27
|
+
const devices = await listDevices(platform, appId);
|
|
28
|
+
if (deviceId) {
|
|
29
|
+
const found = devices.find(d => d.id === deviceId);
|
|
30
|
+
if (!found)
|
|
31
|
+
throw new Error(`Device '${deviceId}' not found for platform ${platform}`);
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
let candidates = devices.slice();
|
|
35
|
+
// Apply prefer filter
|
|
36
|
+
if (prefer === "physical")
|
|
37
|
+
candidates = candidates.filter(d => !d.simulator);
|
|
38
|
+
if (prefer === "emulator")
|
|
39
|
+
candidates = candidates.filter(d => d.simulator);
|
|
40
|
+
// If appId provided, prefer devices with appInstalled
|
|
41
|
+
if (appId) {
|
|
42
|
+
const installed = candidates.filter(d => d.appInstalled);
|
|
43
|
+
if (installed.length > 0)
|
|
44
|
+
candidates = installed;
|
|
45
|
+
}
|
|
46
|
+
if (candidates.length === 1)
|
|
47
|
+
return candidates[0];
|
|
48
|
+
if (candidates.length > 1) {
|
|
49
|
+
// Prefer physical over emulator unless prefer=emulator
|
|
50
|
+
if (!prefer) {
|
|
51
|
+
const physical = candidates.filter(d => !d.simulator);
|
|
52
|
+
if (physical.length === 1)
|
|
53
|
+
return physical[0];
|
|
54
|
+
if (physical.length > 1)
|
|
55
|
+
candidates = physical;
|
|
56
|
+
}
|
|
57
|
+
// Pick highest OS version
|
|
58
|
+
candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion));
|
|
59
|
+
// If top is unique (numeric differs), return it
|
|
60
|
+
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
61
|
+
return candidates[0];
|
|
62
|
+
}
|
|
63
|
+
// Ambiguous: throw an error with candidate list so caller (agent) can present choices
|
|
64
|
+
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: d.appInstalled }));
|
|
65
|
+
const err = new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`);
|
|
66
|
+
err.devices = list;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`No devices found for platform ${platform}`);
|
|
70
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -6,6 +6,12 @@ import { AndroidObserve } from "./android/observe.js";
|
|
|
6
6
|
import { AndroidInteract } from "./android/interact.js";
|
|
7
7
|
import { iOSObserve } from "./ios/observe.js";
|
|
8
8
|
import { iOSInteract } from "./ios/interact.js";
|
|
9
|
+
import { installAppHandler } from './tools/install.js';
|
|
10
|
+
import { startAppHandler, terminateAppHandler, restartAppHandler, resetAppDataHandler } from './tools/app.js';
|
|
11
|
+
import { getLogsHandler, startLogStreamHandler, readLogStreamHandler, stopLogStreamHandler } from './tools/logs.js';
|
|
12
|
+
import { listDevicesHandler } from './tools/devices.js';
|
|
13
|
+
import { captureScreenshotHandler } from './tools/screenshot.js';
|
|
14
|
+
import { getUITreeHandler, getCurrentScreenHandler, waitForElementHandler, tapHandler, swipeHandler, typeTextHandler, pressBackHandler } from './tools/ui.js';
|
|
9
15
|
const androidObserve = new AndroidObserve();
|
|
10
16
|
const androidInteract = new AndroidInteract();
|
|
11
17
|
const iosObserve = new iOSObserve();
|
|
@@ -116,6 +122,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
116
122
|
required: ["platform", "appId"]
|
|
117
123
|
}
|
|
118
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: "install_app",
|
|
127
|
+
description: "Install an app on Android or iOS. Accepts a built binary (apk/.ipa/.app) or a project directory to build then install.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from appPath/project files." },
|
|
132
|
+
appPath: { type: "string", description: "Path to APK, .app, .ipa, or project directory" },
|
|
133
|
+
deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
|
|
134
|
+
},
|
|
135
|
+
required: ["appPath"]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
119
138
|
{
|
|
120
139
|
name: "get_logs",
|
|
121
140
|
description: "Get recent logs from Android or iOS simulator. Returns device metadata and the log output.",
|
|
@@ -142,6 +161,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
142
161
|
required: ["platform"]
|
|
143
162
|
}
|
|
144
163
|
},
|
|
164
|
+
{
|
|
165
|
+
name: "list_devices",
|
|
166
|
+
description: "List connected devices and their metadata (android + ios).",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
platform: { type: "string", enum: ["android", "ios"] }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
145
174
|
{
|
|
146
175
|
name: "capture_screenshot",
|
|
147
176
|
description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
|
|
@@ -160,6 +189,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
160
189
|
required: ["platform"]
|
|
161
190
|
}
|
|
162
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: "start_log_stream",
|
|
194
|
+
description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
platform: { type: "string", enum: ["android", "ios"], default: "android" },
|
|
199
|
+
packageName: { type: "string", description: "Android package name or iOS bundle id" },
|
|
200
|
+
level: { type: "string", enum: ["error", "warn", "info", "debug"], default: "error" },
|
|
201
|
+
deviceId: { type: "string", description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device." },
|
|
202
|
+
sessionId: { type: "string", description: "Session identifier for the log stream" }
|
|
203
|
+
},
|
|
204
|
+
required: ["packageName"]
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "read_log_stream",
|
|
209
|
+
description: "Read accumulated log stream entries for the active session.",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
sessionId: { type: "string" }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "stop_log_stream",
|
|
219
|
+
description: "Stop an active log stream for the session.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
sessionId: { type: "string" }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
163
227
|
{
|
|
164
228
|
name: "get_ui_tree",
|
|
165
229
|
description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
|
|
@@ -319,249 +383,117 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
319
383
|
try {
|
|
320
384
|
if (name === "start_app") {
|
|
321
385
|
const { platform, appId, deviceId } = args;
|
|
322
|
-
|
|
323
|
-
let launchTimeMs;
|
|
324
|
-
let deviceInfo;
|
|
325
|
-
if (platform === "android") {
|
|
326
|
-
const result = await androidInteract.startApp(appId, deviceId);
|
|
327
|
-
appStarted = result.appStarted;
|
|
328
|
-
launchTimeMs = result.launchTimeMs;
|
|
329
|
-
deviceInfo = result.device;
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
const result = await iosInteract.startApp(appId, deviceId);
|
|
333
|
-
appStarted = result.appStarted;
|
|
334
|
-
launchTimeMs = result.launchTimeMs;
|
|
335
|
-
deviceInfo = result.device;
|
|
336
|
-
}
|
|
386
|
+
const result = await startAppHandler({ platform, appId, deviceId });
|
|
337
387
|
const response = {
|
|
338
|
-
device:
|
|
339
|
-
appStarted,
|
|
340
|
-
launchTimeMs
|
|
388
|
+
device: result.device,
|
|
389
|
+
appStarted: result.appStarted,
|
|
390
|
+
launchTimeMs: result.launchTimeMs
|
|
341
391
|
};
|
|
342
392
|
return wrapResponse(response);
|
|
343
393
|
}
|
|
344
394
|
if (name === "terminate_app") {
|
|
345
395
|
const { platform, appId, deviceId } = args;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (platform === "android") {
|
|
349
|
-
const result = await androidInteract.terminateApp(appId, deviceId);
|
|
350
|
-
appTerminated = result.appTerminated;
|
|
351
|
-
deviceInfo = result.device;
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
const result = await iosInteract.terminateApp(appId, deviceId);
|
|
355
|
-
appTerminated = result.appTerminated;
|
|
356
|
-
deviceInfo = result.device;
|
|
357
|
-
}
|
|
358
|
-
const response = {
|
|
359
|
-
device: deviceInfo,
|
|
360
|
-
appTerminated
|
|
361
|
-
};
|
|
396
|
+
const result = await terminateAppHandler({ platform, appId, deviceId });
|
|
397
|
+
const response = { device: result.device, appTerminated: result.appTerminated };
|
|
362
398
|
return wrapResponse(response);
|
|
363
399
|
}
|
|
364
400
|
if (name === "restart_app") {
|
|
365
401
|
const { platform, appId, deviceId } = args;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
let deviceInfo;
|
|
369
|
-
if (platform === "android") {
|
|
370
|
-
const result = await androidInteract.restartApp(appId, deviceId);
|
|
371
|
-
appRestarted = result.appRestarted;
|
|
372
|
-
launchTimeMs = result.launchTimeMs;
|
|
373
|
-
deviceInfo = result.device;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
const result = await iosInteract.restartApp(appId, deviceId);
|
|
377
|
-
appRestarted = result.appRestarted;
|
|
378
|
-
launchTimeMs = result.launchTimeMs;
|
|
379
|
-
deviceInfo = result.device;
|
|
380
|
-
}
|
|
381
|
-
const response = {
|
|
382
|
-
device: deviceInfo,
|
|
383
|
-
appRestarted,
|
|
384
|
-
launchTimeMs
|
|
385
|
-
};
|
|
402
|
+
const result = await restartAppHandler({ platform, appId, deviceId });
|
|
403
|
+
const response = { device: result.device, appRestarted: result.appRestarted, launchTimeMs: result.launchTimeMs };
|
|
386
404
|
return wrapResponse(response);
|
|
387
405
|
}
|
|
388
406
|
if (name === "reset_app_data") {
|
|
389
407
|
const { platform, appId, deviceId } = args;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
const result = await iosInteract.resetAppData(appId, deviceId);
|
|
399
|
-
dataCleared = result.dataCleared;
|
|
400
|
-
deviceInfo = result.device;
|
|
401
|
-
}
|
|
408
|
+
const result = await resetAppDataHandler({ platform, appId, deviceId });
|
|
409
|
+
const response = { device: result.device, dataCleared: result.dataCleared };
|
|
410
|
+
return wrapResponse(response);
|
|
411
|
+
}
|
|
412
|
+
if (name === "install_app") {
|
|
413
|
+
const { platform, appPath, deviceId } = args;
|
|
414
|
+
const result = await installAppHandler({ platform, appPath, deviceId });
|
|
402
415
|
const response = {
|
|
403
|
-
device:
|
|
404
|
-
|
|
416
|
+
device: result.device,
|
|
417
|
+
installed: result.installed,
|
|
418
|
+
output: result.output,
|
|
419
|
+
error: result.error
|
|
405
420
|
};
|
|
406
421
|
return wrapResponse(response);
|
|
407
422
|
}
|
|
408
423
|
if (name === "get_logs") {
|
|
409
424
|
const { platform, appId, deviceId, lines } = args;
|
|
410
|
-
|
|
411
|
-
let deviceInfo;
|
|
412
|
-
if (platform === "android") {
|
|
413
|
-
deviceInfo = await androidObserve.getDeviceMetadata(appId || "", deviceId);
|
|
414
|
-
const response = await androidObserve.getLogs(appId, lines ?? 200, deviceId);
|
|
415
|
-
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
|
|
419
|
-
const response = await iosObserve.getLogs(appId, deviceId);
|
|
420
|
-
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
421
|
-
}
|
|
422
|
-
// Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
|
|
423
|
-
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'));
|
|
424
|
-
// Return device metadata plus logs
|
|
425
|
+
const res = await getLogsHandler({ platform, appId, deviceId, lines });
|
|
425
426
|
return {
|
|
426
427
|
content: [
|
|
427
|
-
{
|
|
428
|
-
|
|
429
|
-
text: JSON.stringify({
|
|
430
|
-
device: deviceInfo,
|
|
431
|
-
result: {
|
|
432
|
-
lines: logs.length,
|
|
433
|
-
crashLines: crashLines.length > 0 ? crashLines : undefined
|
|
434
|
-
}
|
|
435
|
-
}, null, 2)
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
type: "text",
|
|
439
|
-
text: logs.join("\n")
|
|
440
|
-
}
|
|
428
|
+
{ type: 'text', text: JSON.stringify({ device: res.device, result: { lines: res.logs.length, crashLines: res.crashLines.length > 0 ? res.crashLines : undefined } }, null, 2) },
|
|
429
|
+
{ type: 'text', text: (res.logs || []).join('\n') }
|
|
441
430
|
]
|
|
442
431
|
};
|
|
443
432
|
}
|
|
433
|
+
if (name === "list_devices") {
|
|
434
|
+
const { platform, appId } = (args || {});
|
|
435
|
+
const res = await listDevicesHandler({ platform, appId });
|
|
436
|
+
return wrapResponse(res);
|
|
437
|
+
}
|
|
444
438
|
if (name === "capture_screenshot") {
|
|
445
439
|
const { platform, deviceId } = args;
|
|
446
|
-
|
|
447
|
-
let resolution;
|
|
448
|
-
let deviceInfo;
|
|
449
|
-
if (platform === "android") {
|
|
450
|
-
deviceInfo = await androidObserve.getDeviceMetadata("", deviceId);
|
|
451
|
-
const result = await androidObserve.captureScreen(deviceId);
|
|
452
|
-
screenshot = result.screenshot;
|
|
453
|
-
resolution = result.resolution;
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
|
|
457
|
-
const result = await iosObserve.captureScreenshot(deviceId);
|
|
458
|
-
screenshot = result.screenshot;
|
|
459
|
-
resolution = result.resolution;
|
|
460
|
-
}
|
|
440
|
+
const res = await captureScreenshotHandler({ platform, deviceId });
|
|
461
441
|
return {
|
|
462
442
|
content: [
|
|
463
|
-
{
|
|
464
|
-
|
|
465
|
-
text: JSON.stringify({
|
|
466
|
-
device: deviceInfo,
|
|
467
|
-
result: {
|
|
468
|
-
resolution
|
|
469
|
-
}
|
|
470
|
-
}, null, 2)
|
|
471
|
-
},
|
|
472
|
-
{
|
|
473
|
-
type: "image",
|
|
474
|
-
data: screenshot,
|
|
475
|
-
mimeType: "image/png"
|
|
476
|
-
}
|
|
443
|
+
{ type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution } }, null, 2) },
|
|
444
|
+
{ type: 'image', data: res.screenshot, mimeType: 'image/png' }
|
|
477
445
|
]
|
|
478
446
|
};
|
|
479
447
|
}
|
|
480
448
|
if (name === "get_ui_tree") {
|
|
481
449
|
const { platform, deviceId } = args;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
result = await androidObserve.getUITree(deviceId);
|
|
485
|
-
}
|
|
486
|
-
else if (platform === "ios") {
|
|
487
|
-
result = await iosObserve.getUITree(deviceId);
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
throw new Error(`Platform ${platform} not supported for get_ui_tree`);
|
|
491
|
-
}
|
|
492
|
-
return wrapResponse(result);
|
|
450
|
+
const res = await getUITreeHandler({ platform, deviceId });
|
|
451
|
+
return wrapResponse(res);
|
|
493
452
|
}
|
|
494
453
|
if (name === "get_current_screen") {
|
|
495
454
|
const { deviceId } = (args || {});
|
|
496
|
-
const
|
|
497
|
-
return wrapResponse(
|
|
455
|
+
const res = await getCurrentScreenHandler({ deviceId });
|
|
456
|
+
return wrapResponse(res);
|
|
498
457
|
}
|
|
499
458
|
if (name === "wait_for_element") {
|
|
500
459
|
const { platform, text, timeout, deviceId } = (args || {});
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
if (platform === "android") {
|
|
504
|
-
result = await androidInteract.waitForElement(text, effectiveTimeout, deviceId);
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
result = await iosInteract.waitForElement(text, effectiveTimeout, deviceId);
|
|
508
|
-
}
|
|
509
|
-
return wrapResponse(result);
|
|
460
|
+
const res = await waitForElementHandler({ platform, text, timeout, deviceId });
|
|
461
|
+
return wrapResponse(res);
|
|
510
462
|
}
|
|
511
463
|
if (name === "tap") {
|
|
512
464
|
const { platform, x, y, deviceId } = (args || {});
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
516
|
-
throw new Error("x and y coordinates are required and must be numbers");
|
|
517
|
-
}
|
|
518
|
-
let result;
|
|
519
|
-
if (effectivePlatform === "android") {
|
|
520
|
-
result = await androidInteract.tap(x, y, deviceId);
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
result = await iosInteract.tap(x, y, deviceId);
|
|
524
|
-
}
|
|
525
|
-
return wrapResponse(result);
|
|
465
|
+
const res = await tapHandler({ platform, x, y, deviceId });
|
|
466
|
+
return wrapResponse(res);
|
|
526
467
|
}
|
|
527
468
|
if (name === "swipe") {
|
|
528
|
-
const {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
|
|
532
|
-
}
|
|
533
|
-
let result;
|
|
534
|
-
if (effectivePlatform === "android") {
|
|
535
|
-
result = await androidInteract.swipe(x1, y1, x2, y2, duration, deviceId);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
throw new Error(`Platform ${effectivePlatform} not supported for swipe`);
|
|
539
|
-
}
|
|
540
|
-
return wrapResponse(result);
|
|
469
|
+
const { x1, y1, x2, y2, duration, deviceId } = (args || {});
|
|
470
|
+
const res = await swipeHandler({ x1, y1, x2, y2, duration, deviceId });
|
|
471
|
+
return wrapResponse(res);
|
|
541
472
|
}
|
|
542
473
|
if (name === "type_text") {
|
|
543
|
-
const {
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
throw new Error("text is required and must be a string");
|
|
547
|
-
}
|
|
548
|
-
let result;
|
|
549
|
-
if (effectivePlatform === "android") {
|
|
550
|
-
result = await androidInteract.typeText(text, deviceId);
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
throw new Error(`Platform ${effectivePlatform} not supported for type_text`);
|
|
554
|
-
}
|
|
555
|
-
return wrapResponse(result);
|
|
474
|
+
const { text, deviceId } = (args || {});
|
|
475
|
+
const res = await typeTextHandler({ text, deviceId });
|
|
476
|
+
return wrapResponse(res);
|
|
556
477
|
}
|
|
557
478
|
if (name === "press_back") {
|
|
558
|
-
const {
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
479
|
+
const { deviceId } = (args || {});
|
|
480
|
+
const res = await pressBackHandler({ deviceId });
|
|
481
|
+
return wrapResponse(res);
|
|
482
|
+
}
|
|
483
|
+
if (name === 'start_log_stream') {
|
|
484
|
+
const { platform, packageName, level, sessionId, deviceId } = args;
|
|
485
|
+
const res = await startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
486
|
+
return wrapResponse(res);
|
|
487
|
+
}
|
|
488
|
+
if (name === 'read_log_stream') {
|
|
489
|
+
const { platform, sessionId, limit, since } = args;
|
|
490
|
+
const res = await readLogStreamHandler({ platform, sessionId, limit, since });
|
|
491
|
+
return wrapResponse(res);
|
|
492
|
+
}
|
|
493
|
+
if (name === 'stop_log_stream') {
|
|
494
|
+
const { platform, sessionId } = (args || {});
|
|
495
|
+
const res = await stopLogStreamHandler({ platform, sessionId });
|
|
496
|
+
return wrapResponse(res);
|
|
565
497
|
}
|
|
566
498
|
}
|
|
567
499
|
catch (error) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resolveTargetDevice } from '../resolve-device.js';
|
|
2
|
+
import { AndroidInteract } from '../android/interact.js';
|
|
3
|
+
import { iOSInteract } from '../ios/interact.js';
|
|
4
|
+
const androidInteract = new AndroidInteract();
|
|
5
|
+
const iosInteract = new iOSInteract();
|
|
6
|
+
export async function startAppHandler({ platform, appId, deviceId }) {
|
|
7
|
+
if (platform === 'android') {
|
|
8
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
9
|
+
return await androidInteract.startApp(appId, resolved.id);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
13
|
+
return await iosInteract.startApp(appId, resolved.id);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function terminateAppHandler({ platform, appId, deviceId }) {
|
|
17
|
+
if (platform === 'android') {
|
|
18
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
19
|
+
return await androidInteract.terminateApp(appId, resolved.id);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
23
|
+
return await iosInteract.terminateApp(appId, resolved.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function restartAppHandler({ platform, appId, deviceId }) {
|
|
27
|
+
if (platform === 'android') {
|
|
28
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
29
|
+
return await androidInteract.restartApp(appId, resolved.id);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
33
|
+
return await iosInteract.restartApp(appId, resolved.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function resetAppDataHandler({ platform, appId, deviceId }) {
|
|
37
|
+
if (platform === 'android') {
|
|
38
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
39
|
+
return await androidInteract.resetAppData(appId, resolved.id);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
43
|
+
return await iosInteract.resetAppData(appId, resolved.id);
|
|
44
|
+
}
|
|
45
|
+
}
|