mobile-debug-mcp 0.13.0 → 0.15.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 +2 -2
- package/dist/android/interact.js +13 -1
- package/dist/android/observe.js +13 -0
- package/dist/cli/ios/run-ios-smoke.js +2 -2
- package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
- package/dist/interact/android.js +91 -0
- package/dist/interact/index.js +37 -0
- package/dist/interact/ios.js +120 -0
- package/dist/interact/shared/fingerprint.js +72 -0
- package/dist/interact/shared/scroll_to_element.js +98 -0
- package/dist/ios/interact.js +52 -1
- package/dist/ios/observe.js +12 -0
- package/dist/manage/android.js +162 -0
- package/dist/manage/index.js +364 -0
- package/dist/manage/ios.js +353 -0
- package/dist/observe/android.js +351 -0
- package/dist/observe/fingerprint.js +1 -0
- package/dist/observe/index.js +85 -0
- package/dist/observe/ios.js +320 -0
- package/dist/observe/test/device/logstream-real.js +34 -0
- package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
- package/dist/observe/test/device/run-scroll-test-android.js +22 -0
- package/dist/observe/test/device/test-ui-tree.js +67 -0
- package/dist/observe/test/device/wait_for_element_real.js +69 -0
- package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
- package/dist/observe/test/unit/logparse.test.js +39 -0
- package/dist/observe/test/unit/logstream.test.js +41 -0
- package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
- package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
- package/dist/server.js +54 -9
- package/dist/shared/fingerprint.js +72 -0
- package/dist/shared/scroll_to_element.js +98 -0
- package/dist/tools/interact.js +19 -22
- package/dist/tools/manage.js +2 -2
- package/dist/tools/observe.js +45 -43
- package/dist/tools/scroll_to_element.js +98 -0
- package/dist/utils/android/utils.js +429 -0
- package/dist/utils/cli/idb/check-idb.js +84 -0
- package/dist/utils/cli/idb/idb-helper.js +91 -0
- package/dist/utils/cli/idb/install-idb.js +82 -0
- package/dist/utils/cli/ios/preflight-ios.js +155 -0
- package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
- package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/utils/diagnostics.js +1 -1
- package/dist/utils/ios/utils.js +301 -0
- package/dist/utils/resolve-device.js +2 -2
- package/docs/CHANGELOG.md +11 -0
- package/docs/tools/TOOLS.md +3 -3
- package/docs/tools/interact.md +31 -0
- package/docs/tools/observe.md +24 -0
- package/package.json +1 -1
- package/src/{android/interact.ts → interact/android.ts} +15 -2
- package/src/interact/index.ts +47 -0
- package/src/{ios/interact.ts → interact/ios.ts} +58 -3
- package/src/interact/shared/fingerprint.ts +73 -0
- package/src/interact/shared/scroll_to_element.ts +110 -0
- package/src/{android/manage.ts → manage/android.ts} +2 -2
- package/src/{tools/manage.ts → manage/index.ts} +7 -4
- package/src/{ios/manage.ts → manage/ios.ts} +1 -1
- package/src/{android/observe.ts → observe/android.ts} +14 -26
- package/src/observe/index.ts +92 -0
- package/src/{ios/observe.ts → observe/ios.ts} +17 -35
- package/src/server.ts +57 -10
- package/src/{android → utils/android}/utils.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
- package/src/utils/diagnostics.ts +1 -1
- package/src/{ios → utils/ios}/utils.ts +2 -2
- package/src/utils/resolve-device.ts +2 -2
- package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
- package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
- package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
- package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
- package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
- package/test/observe/device/run-screen-fingerprint.ts +36 -0
- package/test/observe/device/run-scroll-test-android.ts +24 -0
- package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
- package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
- package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
- package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
- package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
- package/test/observe/unit/scroll_to_element.test.ts +129 -0
- package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +3 -3
- package/test/unit/index.ts +12 -11
- package/src/tools/interact.ts +0 -45
- package/src/tools/observe.ts +0 -82
- package/test/device/README.md +0 -49
- package/test/device/index.ts +0 -27
- package/test/device/utils/test-dist.ts +0 -41
- package/test/unit/utils/detect-java.test.ts +0 -22
- /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
- /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
- /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
- /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
- /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
- /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
- /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { UIElement, GetUITreeResponse, SwipeResponse } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
export interface ScrollSelector { text?: string; resourceId?: string; contentDesc?: string; className?: string }
|
|
4
|
+
|
|
5
|
+
export async function scrollToElementShared(opts: {
|
|
6
|
+
selector: ScrollSelector,
|
|
7
|
+
direction?: 'down' | 'up',
|
|
8
|
+
maxScrolls?: number,
|
|
9
|
+
scrollAmount?: number,
|
|
10
|
+
deviceId?: string,
|
|
11
|
+
fetchTree: () => Promise<GetUITreeResponse>,
|
|
12
|
+
swipe: (x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string) => Promise<SwipeResponse>,
|
|
13
|
+
stabilizationDelayMs?: number
|
|
14
|
+
}): Promise<{ success: boolean; reason?: string; element?: Partial<UIElement>; scrollsPerformed: number }> {
|
|
15
|
+
const { selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId, fetchTree, swipe, stabilizationDelayMs = 350 } = opts
|
|
16
|
+
|
|
17
|
+
const matchElement = (el?: UIElement) => {
|
|
18
|
+
if (!el) return false
|
|
19
|
+
if (selector.text !== undefined && selector.text !== el.text) return false
|
|
20
|
+
if (selector.resourceId !== undefined && selector.resourceId !== el.resourceId) return false
|
|
21
|
+
if (selector.contentDesc !== undefined && selector.contentDesc !== el.contentDescription) return false
|
|
22
|
+
if (selector.className !== undefined && selector.className !== el.type) return false
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isVisible = (el?: UIElement, resolution?: GetUITreeResponse['resolution']) => {
|
|
27
|
+
if (!el) return false
|
|
28
|
+
if (el.visible === false) return false
|
|
29
|
+
if (!el.bounds || !resolution || !resolution.width || !resolution.height) return (el.visible === undefined ? true : !!el.visible)
|
|
30
|
+
const [left, top, right, bottom] = el.bounds
|
|
31
|
+
const withinY = bottom > 0 && top < resolution.height
|
|
32
|
+
const withinX = right > 0 && left < resolution.width
|
|
33
|
+
return withinX && withinY
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const findVisibleMatch = (elements?: UIElement[], resolution?: GetUITreeResponse['resolution']) => {
|
|
37
|
+
if (!Array.isArray(elements)) return null
|
|
38
|
+
for (const e of elements) {
|
|
39
|
+
if (matchElement(e) && isVisible(e, resolution)) return e
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Initial check
|
|
45
|
+
let tree = await fetchTree()
|
|
46
|
+
if (tree.error) return { success: false, reason: tree.error, scrollsPerformed: 0 }
|
|
47
|
+
|
|
48
|
+
let found = findVisibleMatch(tree.elements, tree.resolution)
|
|
49
|
+
if (found) {
|
|
50
|
+
return { success: true, element: { text: found.text, resourceId: found.resourceId, bounds: found.bounds }, scrollsPerformed: 0 }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fingerprintOf = (t: GetUITreeResponse) => {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify((t.elements || []).map((e: UIElement) => ({ text: e.text, resourceId: e.resourceId, bounds: e.bounds })))
|
|
56
|
+
} catch {
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let prevFingerprint = fingerprintOf(tree)
|
|
62
|
+
|
|
63
|
+
const width = (tree.resolution && tree.resolution.width) ? tree.resolution.width : 0
|
|
64
|
+
const height = (tree.resolution && tree.resolution.height) ? tree.resolution.height : 0
|
|
65
|
+
const centerX = Math.round(width / 2) || 50
|
|
66
|
+
|
|
67
|
+
const clampPct = (v: number) => Math.max(0.05, Math.min(0.95, v))
|
|
68
|
+
const computeCoords = () => {
|
|
69
|
+
const defaultStart = direction === 'down' ? 0.8 : 0.2
|
|
70
|
+
const startPct = clampPct(defaultStart)
|
|
71
|
+
const endPct = clampPct(defaultStart + (direction === 'down' ? -scrollAmount : scrollAmount))
|
|
72
|
+
const x1 = centerX
|
|
73
|
+
const x2 = centerX
|
|
74
|
+
const y1 = Math.round((height || 100) * startPct)
|
|
75
|
+
const y2 = Math.round((height || 100) * endPct)
|
|
76
|
+
return { x1, y1, x2, y2 }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const duration = 300
|
|
80
|
+
let scrollsPerformed = 0
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
83
|
+
const { x1, y1, x2, y2 } = computeCoords()
|
|
84
|
+
try {
|
|
85
|
+
await swipe(x1, y1, x2, y2, duration, deviceId)
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Log swipe failures to aid debugging but don't fail the overall flow
|
|
88
|
+
try { console.warn(`scrollToElement swipe failed: ${e instanceof Error ? e.message : String(e)}`) } catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
scrollsPerformed++
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, stabilizationDelayMs))
|
|
93
|
+
|
|
94
|
+
tree = await fetchTree()
|
|
95
|
+
if (tree.error) return { success: false, reason: tree.error, scrollsPerformed: scrollsPerformed }
|
|
96
|
+
|
|
97
|
+
found = findVisibleMatch(tree.elements, tree.resolution)
|
|
98
|
+
if (found) {
|
|
99
|
+
return { success: true, element: { text: found.text, resourceId: found.resourceId, bounds: found.bounds }, scrollsPerformed }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fp = fingerprintOf(tree)
|
|
103
|
+
if (fp === prevFingerprint) {
|
|
104
|
+
return { success: false, reason: 'UI unchanged after scroll; likely end of list', scrollsPerformed: scrollsPerformed }
|
|
105
|
+
}
|
|
106
|
+
prevFingerprint = fp
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { success: false, reason: 'Element not found after scrolling', scrollsPerformed: scrollsPerformed }
|
|
110
|
+
}
|
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs'
|
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
|
-
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '
|
|
5
|
+
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js'
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js'
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js'
|
|
8
8
|
import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
@@ -12,7 +12,7 @@ export class AndroidManage {
|
|
|
12
12
|
void _variant
|
|
13
13
|
try {
|
|
14
14
|
// Always use the shared prepareGradle utility for consistent env/setup
|
|
15
|
-
const { execCmd, gradleArgs, spawnOpts } = await (await import('
|
|
15
|
+
const { execCmd, gradleArgs, spawnOpts } = await (await import('../utils/android/utils.js')).prepareGradle(projectPath)
|
|
16
16
|
await new Promise<void>((resolve, reject) => {
|
|
17
17
|
const proc = spawn(execCmd, gradleArgs, spawnOpts)
|
|
18
18
|
let stderr = ''
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { promises as fs } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js'
|
|
4
|
-
import { AndroidManage } from '
|
|
5
|
-
import { iOSManage } from '
|
|
6
|
-
import { findApk } from '../android/utils.js'
|
|
7
|
-
import { findAppBundle } from '../ios/utils.js'
|
|
4
|
+
import { AndroidManage } from './android.js'
|
|
5
|
+
import { iOSManage } from './ios.js'
|
|
6
|
+
import { findApk } from '../utils/android/utils.js'
|
|
7
|
+
import { findAppBundle } from '../utils/ios/utils.js'
|
|
8
8
|
import { execSync } from 'child_process'
|
|
9
9
|
import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
10
10
|
|
|
11
|
+
export { AndroidManage } from './android.js';
|
|
12
|
+
export { iOSManage } from './ios.js';
|
|
13
|
+
|
|
11
14
|
export async function detectProjectPlatform(projectPath: string): Promise<'ios'|'android'|'ambiguous'|'unknown'> {
|
|
12
15
|
// Recursively scan up to a limited depth for platform markers to avoid mis-detection
|
|
13
16
|
async function scan(dir: string, depth = 3): Promise<{ ios: boolean, android: boolean }>{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import { spawn, spawnSync } from "child_process"
|
|
3
3
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, InstallAppResponse } from "../types.js"
|
|
4
|
-
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "
|
|
4
|
+
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js"
|
|
5
5
|
import path from "path"
|
|
6
6
|
|
|
7
7
|
export class iOSManage {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { spawn } from "child_process"
|
|
2
2
|
import { XMLParser } from "fast-xml-parser"
|
|
3
3
|
import { GetLogsResponse, CaptureAndroidScreenResponse, GetUITreeResponse, GetCurrentScreenResponse, UIElement, DeviceInfo } from "../types.js"
|
|
4
|
-
import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "
|
|
4
|
+
import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "../utils/android/utils.js"
|
|
5
5
|
import { createWriteStream } from "fs"
|
|
6
6
|
import { promises as fsPromises } from "fs"
|
|
7
7
|
import path from "path"
|
|
8
|
+
import { computeScreenFingerprint } from "../interact/shared/fingerprint.js"
|
|
8
9
|
|
|
9
10
|
const activeLogStreams: Map<string, { proc: any, file: string }> = new Map()
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
12
|
export class AndroidObserve {
|
|
14
13
|
async getDeviceMetadata(appId: string, deviceId?: string): Promise<DeviceInfo> {
|
|
15
14
|
return getAndroidDeviceMetadata(appId, deviceId);
|
|
@@ -98,24 +97,16 @@ export class AndroidObserve {
|
|
|
98
97
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
99
98
|
|
|
100
99
|
try {
|
|
101
|
-
// We'll skip PID lookup for now to avoid potential hangs with 'pidof' on some emulators
|
|
102
|
-
// and rely on robust string matching against the log line.
|
|
103
|
-
|
|
104
|
-
// Get logs
|
|
105
100
|
const stdout = await execAdb(['logcat', '-d', '-t', lines.toString(), '-v', 'threadtime'], deviceId)
|
|
106
101
|
const allLogs = stdout.split('\n')
|
|
107
102
|
|
|
108
103
|
let filteredLogs = allLogs
|
|
109
104
|
if (appId) {
|
|
110
|
-
// Filter by checking if the line contains the appId string.
|
|
111
105
|
const matchingLogs = allLogs.filter(line => line.includes(appId))
|
|
112
106
|
|
|
113
107
|
if (matchingLogs.length > 0) {
|
|
114
108
|
filteredLogs = matchingLogs
|
|
115
109
|
} else {
|
|
116
|
-
// Fallback: if no logs match the appId, return the raw logs (last N lines)
|
|
117
|
-
// This matches the behavior of the "working" version provided by the user,
|
|
118
|
-
// ensuring they at least see system activity if the app is silent or crashing early.
|
|
119
110
|
filteredLogs = allLogs
|
|
120
111
|
}
|
|
121
112
|
}
|
|
@@ -132,10 +123,7 @@ export class AndroidObserve {
|
|
|
132
123
|
const deviceInfo: DeviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
133
124
|
|
|
134
125
|
return new Promise((resolve, reject) => {
|
|
135
|
-
// Need to construct ADB args manually since spawn handles it
|
|
136
126
|
const args = deviceId ? ['-s', deviceId, 'exec-out', 'screencap', '-p'] : ['exec-out', 'screencap', '-p'];
|
|
137
|
-
|
|
138
|
-
// Using spawn for screencap as well to ensure consistent process handling
|
|
139
127
|
const child = spawn(getAdbCmd(), args)
|
|
140
128
|
|
|
141
129
|
const chunks: Buffer[] = []
|
|
@@ -164,7 +152,6 @@ export class AndroidObserve {
|
|
|
164
152
|
const screenshotBuffer = Buffer.concat(chunks)
|
|
165
153
|
const screenshotBase64 = screenshotBuffer.toString('base64')
|
|
166
154
|
|
|
167
|
-
// Get resolution
|
|
168
155
|
execAdb(['shell', 'wm', 'size'], deviceId)
|
|
169
156
|
.then(sizeStdout => {
|
|
170
157
|
let width = 0
|
|
@@ -201,13 +188,8 @@ export class AndroidObserve {
|
|
|
201
188
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
202
189
|
|
|
203
190
|
try {
|
|
204
|
-
// Dumpsys activity can be slow on some devices, so we increase timeout to 10s
|
|
205
191
|
const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 })
|
|
206
|
-
|
|
207
|
-
// Find the line with mResumedActivity or ResumedActivity (some versions might differ)
|
|
208
192
|
const lines = output.split('\n');
|
|
209
|
-
// Prioritize mResumedActivity, then ResumedActivity.
|
|
210
|
-
// Use strict regex match to ensure it starts with the key, avoiding false positives like 'mLastResumedActivity'.
|
|
211
193
|
let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
|
|
212
194
|
|
|
213
195
|
if (!resumedLine) {
|
|
@@ -224,17 +206,12 @@ export class AndroidObserve {
|
|
|
224
206
|
}
|
|
225
207
|
}
|
|
226
208
|
|
|
227
|
-
// Regex to parse the line: ActivityRecord{... package/activity ...}
|
|
228
|
-
// Matches: ActivityRecord{<hex> <user> <package>/<activity> ...}
|
|
229
|
-
// We want to capture the component "package/activity" which is separated by space from other tokens.
|
|
230
|
-
// We use greedy match ([^ \{}]+) for activity to ensure we get the full name until a space or closing brace.
|
|
231
209
|
const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
|
|
232
210
|
|
|
233
211
|
if (match) {
|
|
234
212
|
const packageName = match[1];
|
|
235
213
|
let activityName = match[2];
|
|
236
214
|
|
|
237
|
-
// Handle relative activity names (e.g. .LoginActivity)
|
|
238
215
|
if (activityName.startsWith('.')) {
|
|
239
216
|
activityName = packageName + activityName;
|
|
240
217
|
}
|
|
@@ -268,6 +245,18 @@ export class AndroidObserve {
|
|
|
268
245
|
}
|
|
269
246
|
}
|
|
270
247
|
|
|
248
|
+
async getScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string }> {
|
|
249
|
+
try {
|
|
250
|
+
const tree = await this.getUITree(deviceId)
|
|
251
|
+
if (!tree || (tree as any).error) return { fingerprint: null, error: (tree as any).error }
|
|
252
|
+
|
|
253
|
+
const current = await this.getCurrentScreen(deviceId).catch(() => null)
|
|
254
|
+
return computeScreenFingerprint(tree, current, 'android', 50)
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
271
260
|
async startLogStream(packageName: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId?: string, sessionId: string = 'default') {
|
|
272
261
|
try {
|
|
273
262
|
const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '')
|
|
@@ -328,7 +317,6 @@ export class AndroidObserve {
|
|
|
328
317
|
}
|
|
329
318
|
|
|
330
319
|
async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
|
|
331
|
-
// Prefer active stream if present, otherwise fall back to a well-known NDJSON file for the session
|
|
332
320
|
const entry = activeLogStreams.get(sessionId)
|
|
333
321
|
let file: string | undefined
|
|
334
322
|
if (entry && entry.file) file = entry.file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolveTargetDevice } from '../utils/resolve-device.js'
|
|
2
|
+
import { AndroidObserve } from './android.js'
|
|
3
|
+
import { iOSObserve } from './ios.js'
|
|
4
|
+
|
|
5
|
+
export { AndroidObserve } from './android.js'
|
|
6
|
+
export { iOSObserve } from './ios.js'
|
|
7
|
+
|
|
8
|
+
export class ToolsObserve {
|
|
9
|
+
// Resolve a target device and return the appropriate observe instance and resolved info.
|
|
10
|
+
private static async resolveObserve(platform?: 'android' | 'ios', deviceId?: string, appId?: string) {
|
|
11
|
+
if (platform === 'android') {
|
|
12
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId, appId })
|
|
13
|
+
return { observe: new AndroidObserve(), resolved }
|
|
14
|
+
}
|
|
15
|
+
if (platform === 'ios') {
|
|
16
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId, appId })
|
|
17
|
+
return { observe: new iOSObserve(), resolved }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// No platform specified: try android then ios
|
|
21
|
+
try {
|
|
22
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId, appId })
|
|
23
|
+
return { observe: new AndroidObserve(), resolved }
|
|
24
|
+
} catch {
|
|
25
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId, appId })
|
|
26
|
+
return { observe: new iOSObserve(), resolved }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async getUITreeHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string }) {
|
|
31
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId)
|
|
32
|
+
return await observe.getUITree(resolved.id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static async getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
|
|
36
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve('android', deviceId)
|
|
37
|
+
// getCurrentScreen is Android-specific
|
|
38
|
+
return await (observe as AndroidObserve).getCurrentScreen(resolved.id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static async getLogsHandler({ platform, appId, deviceId, lines }: { platform?: 'android' | 'ios', appId?: string, deviceId?: string, lines?: number }) {
|
|
42
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId, appId)
|
|
43
|
+
if (observe instanceof AndroidObserve) {
|
|
44
|
+
const response = await observe.getLogs(appId, lines ?? 200, resolved.id)
|
|
45
|
+
const logs = Array.isArray(response.logs) ? response.logs : []
|
|
46
|
+
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
|
|
47
|
+
return { device: response.device, logs, crashLines }
|
|
48
|
+
} else {
|
|
49
|
+
const resp = await (observe as iOSObserve).getLogs(appId, resolved.id)
|
|
50
|
+
const logs = Array.isArray(resp.logs) ? resp.logs : []
|
|
51
|
+
const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'))
|
|
52
|
+
return { device: resp.device, logs, crashLines }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static async startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }: { platform?: 'android' | 'ios', packageName: string, level?: 'error' | 'warn' | 'info' | 'debug', sessionId?: string, deviceId?: string }) {
|
|
57
|
+
const sid = sessionId || 'default'
|
|
58
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId, packageName)
|
|
59
|
+
if (observe instanceof AndroidObserve) {
|
|
60
|
+
return await observe.startLogStream(packageName, level || 'error', resolved.id, sid)
|
|
61
|
+
} else {
|
|
62
|
+
return await (observe as iOSObserve).startLogStream(packageName, resolved.id, sid)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static async readLogStreamHandler({ platform, sessionId, limit, since }: { platform?: 'android' | 'ios', sessionId?: string, limit?: number, since?: string }) {
|
|
67
|
+
const sid = sessionId || 'default'
|
|
68
|
+
const { observe } = await ToolsObserve.resolveObserve(platform)
|
|
69
|
+
return await (observe as any).readLogStream(sid, limit ?? 100, since)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static async stopLogStreamHandler({ platform, sessionId }: { platform?: 'android' | 'ios', sessionId?: string }) {
|
|
73
|
+
const sid = sessionId || 'default'
|
|
74
|
+
const { observe } = await ToolsObserve.resolveObserve(platform)
|
|
75
|
+
return await (observe as any).stopLogStream(sid)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static async captureScreenshotHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string }) {
|
|
79
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId)
|
|
80
|
+
if (observe instanceof AndroidObserve) {
|
|
81
|
+
return await observe.captureScreen(resolved.id)
|
|
82
|
+
} else {
|
|
83
|
+
return await (observe as iOSObserve).captureScreenshot(resolved.id)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static async getScreenFingerprintHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string } = {}) {
|
|
88
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId)
|
|
89
|
+
// Both observes implement getScreenFingerprint
|
|
90
|
+
return await (observe as any).getScreenFingerprint(resolved.id)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { spawn } from "child_process"
|
|
2
2
|
import { promises as fs } from "fs"
|
|
3
3
|
import { GetLogsResponse, CaptureIOSScreenshotResponse, GetUITreeResponse, UIElement, DeviceInfo } from "../types.js"
|
|
4
|
-
import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcrunCmd, isIDBInstalled } from "
|
|
4
|
+
import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcrunCmd, isIDBInstalled } from "../utils/ios/utils.js"
|
|
5
5
|
import { createWriteStream, promises as fsPromises } from 'fs'
|
|
6
6
|
import path from 'path'
|
|
7
|
-
import { parseLogLine } from '../android/utils.js'
|
|
8
|
-
|
|
9
|
-
// --- Helper Functions Specific to Observe ---
|
|
7
|
+
import { parseLogLine } from '../utils/android/utils.js'
|
|
8
|
+
import { computeScreenFingerprint } from '../interact/shared/fingerprint.js'
|
|
10
9
|
|
|
11
10
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
12
11
|
|
|
@@ -25,7 +24,6 @@ interface IDBElement {
|
|
|
25
24
|
|
|
26
25
|
function parseIDBFrame(frame: any): [number, number, number, number] {
|
|
27
26
|
if (!frame) return [0, 0, 0, 0];
|
|
28
|
-
// Handle string frames like "{{0, 0}, {402, 874}}"
|
|
29
27
|
if (typeof frame === 'string') {
|
|
30
28
|
const nums = frame.match(/-?\d+(?:\.\d+)?/g);
|
|
31
29
|
if (!nums || nums.length < 4) return [0, 0, 0, 0];
|
|
@@ -53,29 +51,25 @@ function traverseIDBNode(node: IDBElement, elements: UIElement[], parentIndex: n
|
|
|
53
51
|
|
|
54
52
|
let currentIndex = -1;
|
|
55
53
|
|
|
56
|
-
// Prefer standard keys, fallback to alternatives
|
|
57
54
|
const type = node.AXElementType || node.type || "unknown";
|
|
58
55
|
const label = node.AXLabel || node.label || null;
|
|
59
56
|
const value = node.AXValue || null;
|
|
60
57
|
const frame = node.AXFrame || node.frame;
|
|
61
58
|
const traits = node.AXTraits || [];
|
|
62
59
|
|
|
63
|
-
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
60
|
+
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
64
61
|
|
|
65
|
-
// Filtering Logic:
|
|
66
|
-
// Keep if clickable OR has visible text/label OR has value
|
|
67
|
-
// Also keep 'Window' or 'Application' types as they define the root structure often, though usually depth 0
|
|
68
62
|
const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
|
|
69
63
|
|
|
70
64
|
if (isUseful) {
|
|
71
65
|
const bounds = parseIDBFrame(frame);
|
|
72
66
|
const element: UIElement = {
|
|
73
67
|
text: label,
|
|
74
|
-
contentDescription: value,
|
|
68
|
+
contentDescription: value,
|
|
75
69
|
type: type,
|
|
76
70
|
resourceId: node.AXUniqueId || null,
|
|
77
71
|
clickable: clickable,
|
|
78
|
-
enabled: true,
|
|
72
|
+
enabled: true,
|
|
79
73
|
visible: true,
|
|
80
74
|
bounds: bounds,
|
|
81
75
|
center: getCenter(bounds),
|
|
@@ -90,8 +84,6 @@ function traverseIDBNode(node: IDBElement, elements: UIElement[], parentIndex: n
|
|
|
90
84
|
currentIndex = elements.length - 1;
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
// If current node was skipped, children inherit parentIndex and depth (flattening)
|
|
94
|
-
// If current node was added, children use currentIndex and depth + 1
|
|
95
87
|
const nextParentIndex = currentIndex !== -1 ? currentIndex : parentIndex;
|
|
96
88
|
const nextDepth = currentIndex !== -1 ? depth + 1 : depth;
|
|
97
89
|
|
|
@@ -113,12 +105,8 @@ function traverseIDBNode(node: IDBElement, elements: UIElement[], parentIndex: n
|
|
|
113
105
|
return currentIndex;
|
|
114
106
|
}
|
|
115
107
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// iOS live log stream support (moved from ios/utils to observe)
|
|
119
108
|
const iosActiveLogStreams: Map<string, { proc: ReturnType<typeof import('child_process').spawn>, file: string }> = new Map()
|
|
120
109
|
|
|
121
|
-
// Test helpers
|
|
122
110
|
export function _setIOSActiveLogStream(sessionId: string, file: string) {
|
|
123
111
|
iosActiveLogStreams.set(sessionId, { proc: {} as any, file })
|
|
124
112
|
}
|
|
@@ -133,9 +121,6 @@ export class iOSObserve {
|
|
|
133
121
|
}
|
|
134
122
|
|
|
135
123
|
async getLogs(appId?: string, deviceId: string = "booted"): Promise<GetLogsResponse> {
|
|
136
|
-
// If appId is provided, use predicate filtering
|
|
137
|
-
// Note: execFile passes args directly, so we don't need shell escaping for the predicate string itself,
|
|
138
|
-
// but we do need to construct the predicate correctly for log show.
|
|
139
124
|
const args = ['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--last', '1m']
|
|
140
125
|
if (appId) {
|
|
141
126
|
validateBundleId(appId)
|
|
@@ -157,25 +142,19 @@ export class iOSObserve {
|
|
|
157
142
|
const tmpFile = `/tmp/mcp-ios-screenshot-${Date.now()}.png`
|
|
158
143
|
|
|
159
144
|
try {
|
|
160
|
-
// 1. Capture screenshot to temp file
|
|
161
145
|
await execCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId)
|
|
162
146
|
|
|
163
|
-
// 2. Read file as base64
|
|
164
147
|
const buffer = await fs.readFile(tmpFile)
|
|
165
148
|
const base64 = buffer.toString('base64')
|
|
166
149
|
|
|
167
|
-
// 3. Clean up
|
|
168
150
|
await fs.rm(tmpFile).catch(() => {})
|
|
169
151
|
|
|
170
152
|
return {
|
|
171
153
|
device,
|
|
172
154
|
screenshot: base64,
|
|
173
|
-
// Default resolution since we can't easily parse it without extra libs
|
|
174
|
-
// Clients will read the real dimensions from the PNG header anyway
|
|
175
155
|
resolution: { width: 0, height: 0 },
|
|
176
156
|
}
|
|
177
157
|
} catch (e) {
|
|
178
|
-
// Ensure cleanup happens even on error
|
|
179
158
|
await fs.rm(tmpFile).catch(() => {})
|
|
180
159
|
throw new Error(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`)
|
|
181
160
|
}
|
|
@@ -184,7 +163,6 @@ export class iOSObserve {
|
|
|
184
163
|
async getUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> {
|
|
185
164
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
186
165
|
|
|
187
|
-
// idb is required
|
|
188
166
|
const idbExists = await isIDBInstalled();
|
|
189
167
|
if (!idbExists) {
|
|
190
168
|
return {
|
|
@@ -196,10 +174,8 @@ export class iOSObserve {
|
|
|
196
174
|
};
|
|
197
175
|
}
|
|
198
176
|
|
|
199
|
-
// Resolve UDID if needed
|
|
200
177
|
const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
|
|
201
178
|
|
|
202
|
-
// Retry Logic
|
|
203
179
|
let jsonContent: any = null;
|
|
204
180
|
let attempts = 0;
|
|
205
181
|
const maxAttempts = 3;
|
|
@@ -207,7 +183,6 @@ export class iOSObserve {
|
|
|
207
183
|
while (attempts < maxAttempts) {
|
|
208
184
|
attempts++;
|
|
209
185
|
try {
|
|
210
|
-
// Stabilization delay
|
|
211
186
|
await delay(300 + (attempts * 100));
|
|
212
187
|
|
|
213
188
|
const args = ['ui', 'describe-all', '--json'];
|
|
@@ -255,7 +230,6 @@ export class iOSObserve {
|
|
|
255
230
|
|
|
256
231
|
try {
|
|
257
232
|
const elements: UIElement[] = [];
|
|
258
|
-
// idb describe-all returns either a root object or an array of root nodes
|
|
259
233
|
if (Array.isArray(jsonContent)) {
|
|
260
234
|
for (const node of jsonContent) {
|
|
261
235
|
traverseIDBNode(node, elements);
|
|
@@ -264,7 +238,6 @@ export class iOSObserve {
|
|
|
264
238
|
traverseIDBNode(jsonContent, elements);
|
|
265
239
|
}
|
|
266
240
|
|
|
267
|
-
// Infer resolution from root element if possible (usually the Window/Application frame)
|
|
268
241
|
let width = 0;
|
|
269
242
|
let height = 0;
|
|
270
243
|
if (elements.length > 0) {
|
|
@@ -290,7 +263,17 @@ export class iOSObserve {
|
|
|
290
263
|
}
|
|
291
264
|
}
|
|
292
265
|
|
|
293
|
-
|
|
266
|
+
async getScreenFingerprint(deviceId: string = 'booted'): Promise<{ fingerprint: string | null; activity?: string; error?: string }> {
|
|
267
|
+
try {
|
|
268
|
+
const tree = await this.getUITree(deviceId)
|
|
269
|
+
if (!tree || tree.error) return { fingerprint: null, error: tree && (tree as any).error }
|
|
270
|
+
|
|
271
|
+
return computeScreenFingerprint(tree, null, 'ios', 50)
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
294
277
|
async startLogStream(bundleId: string, deviceId: string = 'booted', sessionId: string = 'default') : Promise<{ success: boolean; stream_started?: boolean; error?: string }> {
|
|
295
278
|
try {
|
|
296
279
|
const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`
|
|
@@ -303,7 +286,6 @@ export class iOSObserve {
|
|
|
303
286
|
const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate]
|
|
304
287
|
const proc = spawn(getXcrunCmd(), args)
|
|
305
288
|
|
|
306
|
-
// Prepare output file
|
|
307
289
|
const tmpDir = process.env.TMPDIR || '/tmp'
|
|
308
290
|
const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`)
|
|
309
291
|
const stream = createWriteStream(file, { flags: 'a' })
|
package/src/server.ts
CHANGED
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
InstallAppResponse
|
|
15
15
|
} from "./types.js"
|
|
16
16
|
|
|
17
|
-
import { ToolsManage } from './
|
|
18
|
-
import { ToolsInteract } from './
|
|
19
|
-
import { ToolsObserve } from './
|
|
20
|
-
import { AndroidManage } from './
|
|
21
|
-
import { iOSManage } from './
|
|
17
|
+
import { ToolsManage } from './manage/index.js'
|
|
18
|
+
import { ToolsInteract } from './interact/index.js'
|
|
19
|
+
import { ToolsObserve } from './observe/index.js'
|
|
20
|
+
import { AndroidManage } from './manage/index.js'
|
|
21
|
+
import { iOSManage } from './manage/index.js'
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
const server = new Server(
|
|
@@ -283,6 +283,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
},
|
|
286
|
+
{
|
|
287
|
+
name: "get_screen_fingerprint",
|
|
288
|
+
description: "Generate a stable fingerprint representing the current visible screen (activity + visible UI elements).",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override (android|ios)" },
|
|
293
|
+
deviceId: { type: "string", description: "Optional device id/udid to target" }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
286
297
|
{
|
|
287
298
|
name: "wait_for_element",
|
|
288
299
|
description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
|
|
@@ -346,8 +357,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
346
357
|
properties: {
|
|
347
358
|
platform: {
|
|
348
359
|
type: "string",
|
|
349
|
-
enum: ["android"],
|
|
350
|
-
description: "Platform to swipe on (
|
|
360
|
+
enum: ["android","ios"],
|
|
361
|
+
description: "Platform to swipe on (android or ios)"
|
|
351
362
|
},
|
|
352
363
|
x1: { type: "number", description: "Start X coordinate" },
|
|
353
364
|
y1: { type: "number", description: "Start Y coordinate" },
|
|
@@ -362,6 +373,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
362
373
|
required: ["x1", "y1", "x2", "y2", "duration"]
|
|
363
374
|
}
|
|
364
375
|
},
|
|
376
|
+
{
|
|
377
|
+
name: "scroll_to_element",
|
|
378
|
+
description: "Scroll the current screen until a target UI element becomes visible, then return its details.",
|
|
379
|
+
inputSchema: {
|
|
380
|
+
type: "object",
|
|
381
|
+
properties: {
|
|
382
|
+
platform: { type: "string", enum: ["android", "ios"], description: "Platform to operate on (required)" },
|
|
383
|
+
selector: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
text: { type: "string" },
|
|
387
|
+
resourceId: { type: "string" },
|
|
388
|
+
contentDesc: { type: "string" },
|
|
389
|
+
className: { type: "string" }
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
direction: { type: "string", enum: ["down", "up"], default: "down" },
|
|
393
|
+
maxScrolls: { type: "number", default: 10 },
|
|
394
|
+
scrollAmount: { type: "number", default: 0.7 },
|
|
395
|
+
deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
|
|
396
|
+
},
|
|
397
|
+
required: ["platform", "selector"]
|
|
398
|
+
}
|
|
399
|
+
},
|
|
365
400
|
{
|
|
366
401
|
name: "type_text",
|
|
367
402
|
description: "Type text into the currently focused input field on an Android device.",
|
|
@@ -555,6 +590,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
555
590
|
return wrapResponse(res)
|
|
556
591
|
}
|
|
557
592
|
|
|
593
|
+
if (name === "get_screen_fingerprint") {
|
|
594
|
+
const { platform, deviceId } = (args || {}) as any
|
|
595
|
+
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
|
|
596
|
+
return wrapResponse(res)
|
|
597
|
+
}
|
|
598
|
+
|
|
558
599
|
if (name === "wait_for_element") {
|
|
559
600
|
const { platform, text, timeout, deviceId } = (args || {}) as any
|
|
560
601
|
const res = await ToolsInteract.waitForElementHandler({ platform, text, timeout, deviceId })
|
|
@@ -568,8 +609,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
568
609
|
}
|
|
569
610
|
|
|
570
611
|
if (name === "swipe") {
|
|
571
|
-
const { x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
|
|
572
|
-
const res = await ToolsInteract.swipeHandler({ x1, y1, x2, y2, duration, deviceId })
|
|
612
|
+
const { platform = 'android', x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
|
|
613
|
+
const res = await ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId })
|
|
614
|
+
return wrapResponse(res)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (name === "scroll_to_element") {
|
|
618
|
+
const { platform, selector, direction, maxScrolls, scrollAmount, deviceId } = (args || {}) as any
|
|
619
|
+
const res = await ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId })
|
|
573
620
|
return wrapResponse(res)
|
|
574
621
|
}
|
|
575
622
|
|
|
@@ -614,7 +661,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
614
661
|
const transport = new StdioServerTransport()
|
|
615
662
|
|
|
616
663
|
async function main() {
|
|
617
|
-
await server.connect(transport)
|
|
664
|
+
await (server as any).connect(transport)
|
|
618
665
|
}
|
|
619
666
|
|
|
620
667
|
main().catch((error) => {
|