mobile-debug-mcp 0.5.0 → 0.7.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.
@@ -0,0 +1,67 @@
1
+ import { AndroidInteract } from "../src/android/interact.js";
2
+ import { AndroidObserve } from "../src/android/observe.js";
3
+ // Configure ADB path and target via env vars or CLI args.
4
+ // Usage: node test/wait_for_element_real.js [deviceId] [appId]
5
+ // Priority: CLI args > environment variables
6
+ const args = process.argv.slice(2);
7
+ const DEVICE_ID = args[0] || process.env.DEVICE_ID;
8
+ const APP_ID = args[1] || process.env.APP_ID;
9
+
10
+ // Do not hard-code ADB_PATH here. If the user supplied ADB_PATH in the environment, leave it as-is.
11
+ // (process.env.ADB_PATH may be set externally before running this script.)
12
+
13
+ if (!DEVICE_ID || !APP_ID) {
14
+ console.error("Usage: node test/wait_for_element_real.js <deviceId> <appId> or set DEVICE_ID and APP_ID env vars");
15
+ process.exit(1);
16
+ }
17
+ async function runRealTest() {
18
+ console.log(`Connecting to device ${DEVICE_ID}...`);
19
+ const interact = new AndroidInteract();
20
+ const observe = new AndroidObserve();
21
+ try {
22
+ // 1. Start App
23
+ console.log(`\nStarting app ${APP_ID}...`);
24
+ await interact.startApp(APP_ID, DEVICE_ID);
25
+ // Give it a moment to render
26
+ console.log("Waiting 3s for app to render...");
27
+ await new Promise(r => setTimeout(r, 3000));
28
+ // 2. Get UI Tree to find a valid text target
29
+ console.log("\nFetching UI Tree to find a target text...");
30
+ const tree = await observe.getUITree(DEVICE_ID);
31
+ if (tree.error) {
32
+ console.error("Failed to get UI Tree:", tree.error);
33
+ return;
34
+ }
35
+ // Find first visible element with text
36
+ const targetElement = tree.elements.find(e => e.text && e.text.length > 0 && e.visible);
37
+ if (!targetElement || !targetElement.text) {
38
+ console.warn("No visible text elements found on screen to test with.");
39
+ console.log("Elements found:", tree.elements.length);
40
+ return;
41
+ }
42
+ const targetText = targetElement.text;
43
+ console.log(`Found target element: "${targetText}"`);
44
+ // 3. Test waitForElement (Success Case)
45
+ console.log(`\nTest 1: Waiting for existing element "${targetText}" (should succeed)...`);
46
+ const start1 = Date.now();
47
+ const result1 = await interact.waitForElement(targetText, 5000, DEVICE_ID);
48
+ const elapsed1 = Date.now() - start1;
49
+ console.log(`Result: ${result1.found ? "PASS" : "FAIL"}`);
50
+ console.log(`Found Element: ${result1.element?.text}`);
51
+ console.log(`Time taken: ${elapsed1}ms`);
52
+ // 4. Test waitForElement (Timeout Case)
53
+ const missingText = "THIS_TEXT_SHOULD_NOT_EXIST_XYZ_123";
54
+ console.log(`\nTest 2: Waiting for missing element "${missingText}" (should timeout)...`);
55
+ const start2 = Date.now();
56
+ // Use short timeout 2s
57
+ const result2 = await interact.waitForElement(missingText, 2000, DEVICE_ID);
58
+ const elapsed2 = Date.now() - start2;
59
+ console.log(`Result: ${!result2.found ? "PASS" : "FAIL"}`);
60
+ console.log(`Found: ${result2.found}`);
61
+ console.log(`Time taken: ${elapsed2}ms (expected ~2000ms)`);
62
+ }
63
+ catch (error) {
64
+ console.error("Test failed with error:", error);
65
+ }
66
+ }
67
+ runRealTest();
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Test script for verify UI Tree functionality.
3
+ *
4
+ * Usage:
5
+ * npx tsx test-ui-tree.ts [android|ios] [deviceId]
6
+ *
7
+ * Examples:
8
+ * npx tsx test-ui-tree.ts android
9
+ * npx tsx test-ui-tree.ts ios booted
10
+ */
11
+ import { AndroidObserve } from "./src/android/observe.js";
12
+ import { iOSObserve } from "./src/ios/observe.js";
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const platform = (args[0] || 'android').toLowerCase();
16
+ const deviceId = args[1];
17
+ console.log(`Starting UI Tree Test for ${platform}...`);
18
+ if (deviceId)
19
+ console.log(`Targeting device: ${deviceId}`);
20
+ try {
21
+ let result;
22
+ if (platform === 'ios') {
23
+ const observer = new iOSObserve();
24
+ result = await observer.getUITree(deviceId || 'booted');
25
+ }
26
+ else {
27
+ const observer = new AndroidObserve();
28
+ result = await observer.getUITree(deviceId);
29
+ }
30
+ console.log("\nUI Tree Result Summary:");
31
+ console.log("-----------------------");
32
+ if (result.error) {
33
+ console.error("❌ Error:", result.error);
34
+ process.exit(1);
35
+ }
36
+ console.log(`Device: ${result.device.platform} (${result.device.model || 'Unknown Model'})`);
37
+ console.log(`Resolution: ${result.resolution.width}x${result.resolution.height}`);
38
+ console.log(`Elements Found: ${result.elements.length}`);
39
+ if (result.elements.length === 0) {
40
+ console.warn("⚠️ Warning: No elements found. Is the screen empty or locked?");
41
+ }
42
+ else {
43
+ // Print sample element to verify structure
44
+ const first = result.elements[0];
45
+ console.log("\nSample Element (First):");
46
+ console.log(JSON.stringify(first, null, 2));
47
+ // Check for new fields
48
+ if (first.center && first.depth !== undefined) {
49
+ console.log("\n✅ Verified 'center' and 'depth' fields exist.");
50
+ }
51
+ else {
52
+ console.error("\n❌ 'center' or 'depth' fields missing!");
53
+ process.exit(1);
54
+ }
55
+ // Check for filtering
56
+ const interactive = result.elements.filter(e => e.clickable).length;
57
+ const withText = result.elements.filter(e => e.text).length;
58
+ console.log(`\nStats:`);
59
+ console.log(`- Interactive elements: ${interactive}`);
60
+ console.log(`- Elements with text: ${withText}`);
61
+ }
62
+ }
63
+ catch (error) {
64
+ console.error("\n❌ Test Failed:", error);
65
+ process.exit(1);
66
+ }
67
+ }
68
+ main();
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Test script for verify UI Tree functionality.
3
+ *
4
+ * Usage:
5
+ * npx tsx test-ui-tree.ts [android|ios] [deviceId]
6
+ *
7
+ * Examples:
8
+ * npx tsx test-ui-tree.ts android
9
+ * npx tsx test-ui-tree.ts ios booted
10
+ */
11
+
12
+ import { AndroidObserve } from "./src/android/observe.js";
13
+ import { iOSObserve } from "./src/ios/observe.js";
14
+
15
+ async function main() {
16
+ const args = process.argv.slice(2);
17
+ const platform = (args[0] || 'android').toLowerCase();
18
+ const deviceId = args[1];
19
+
20
+ console.log(`Starting UI Tree Test for ${platform}...`);
21
+ if (deviceId) console.log(`Targeting device: ${deviceId}`);
22
+
23
+ try {
24
+ let result;
25
+
26
+ if (platform === 'ios') {
27
+ const observer = new iOSObserve();
28
+ result = await observer.getUITree(deviceId || 'booted');
29
+ } else {
30
+ const observer = new AndroidObserve();
31
+ result = await observer.getUITree(deviceId);
32
+ }
33
+
34
+ console.log("\nUI Tree Result Summary:");
35
+ console.log("-----------------------");
36
+
37
+ if (result.error) {
38
+ console.error("❌ Error:", result.error);
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log(`Device: ${result.device.platform} (${result.device.model || 'Unknown Model'})`);
43
+ console.log(`Resolution: ${result.resolution.width}x${result.resolution.height}`);
44
+ console.log(`Elements Found: ${result.elements.length}`);
45
+
46
+ if (result.elements.length === 0) {
47
+ console.warn("⚠️ Warning: No elements found. Is the screen empty or locked?");
48
+ } else {
49
+ // Print sample element to verify structure
50
+ const first = result.elements[0];
51
+ console.log("\nSample Element (First):");
52
+ console.log(JSON.stringify(first, null, 2));
53
+
54
+ // Check for new fields
55
+ if (first.center && first.depth !== undefined) {
56
+ console.log("\n✅ Verified 'center' and 'depth' fields exist.");
57
+ } else {
58
+ console.error("\n❌ 'center' or 'depth' fields missing!");
59
+ process.exit(1);
60
+ }
61
+
62
+ // Check for filtering
63
+ const interactive = result.elements.filter(e => e.clickable).length;
64
+ const withText = result.elements.filter(e => e.text).length;
65
+ console.log(`\nStats:`);
66
+ console.log(`- Interactive elements: ${interactive}`);
67
+ console.log(`- Elements with text: ${withText}`);
68
+ }
69
+
70
+ } catch (error) {
71
+ console.error("\n❌ Test Failed:", error);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ main();
package/tsconfig.json CHANGED
@@ -6,7 +6,8 @@
6
6
  "rootDir": "src",
7
7
  "moduleResolution": "NodeNext",
8
8
  "strict": true,
9
+ "skipLibCheck": true,
9
10
  "esModuleInterop": true
10
11
  },
11
- "exclude": ["smoke-test.ts"]
12
+ "exclude": ["smoke-test.ts", "test-ui-tree.ts"]
12
13
  }
package/src/android.ts DELETED
@@ -1,222 +0,0 @@
1
- import { execFile, spawn } from "child_process"
2
- import { StartAppResponse, GetLogsResponse, CaptureAndroidScreenResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, DeviceInfo } from "./types.js"
3
-
4
- const ADB = process.env.ADB_PATH || "adb"
5
-
6
- // Helper to construct ADB args with optional device ID
7
- function getAdbArgs(args: string[], deviceId?: string): string[] {
8
- if (deviceId) {
9
- return ['-s', deviceId, ...args]
10
- }
11
- return args
12
- }
13
-
14
- function execAdb(args: string[], deviceId?: string, options: any = {}): Promise<string> {
15
- const adbArgs = getAdbArgs(args, deviceId)
16
- return new Promise((resolve, reject) => {
17
- // Use spawn instead of execFile for better stream control and to avoid potential buffering hangs
18
- const child = spawn(ADB, adbArgs, options)
19
-
20
- let stdout = ''
21
- let stderr = ''
22
-
23
- if (child.stdout) {
24
- child.stdout.on('data', (data) => {
25
- stdout += data.toString()
26
- })
27
- }
28
-
29
- if (child.stderr) {
30
- child.stderr.on('data', (data) => {
31
- stderr += data.toString()
32
- })
33
- }
34
-
35
- const timeoutMs = args.includes('logcat') ? 10000 : 2000 // Shorter timeout for metadata queries
36
- const timeout = setTimeout(() => {
37
- child.kill()
38
- reject(new Error(`ADB command timed out after ${timeoutMs}ms: ${args.join(' ')}`))
39
- }, timeoutMs)
40
-
41
- child.on('close', (code) => {
42
- clearTimeout(timeout)
43
- if (code !== 0) {
44
- // If there's an actual error (non-zero exit code), reject
45
- reject(new Error(stderr.trim() || `Command failed with code ${code}`))
46
- } else {
47
- // If exit code is 0, resolve with stdout
48
- resolve(stdout.trim())
49
- }
50
- })
51
-
52
- child.on('error', (err) => {
53
- clearTimeout(timeout)
54
- reject(err)
55
- })
56
- })
57
- }
58
-
59
- function getDeviceInfo(deviceId: string, metadata: Partial<DeviceInfo> = {}): DeviceInfo {
60
- return {
61
- platform: 'android',
62
- id: deviceId || 'default',
63
- osVersion: metadata.osVersion || '',
64
- model: metadata.model || '',
65
- simulator: metadata.simulator || false
66
- }
67
- }
68
-
69
- export async function getAndroidDeviceMetadata(appId: string, deviceId?: string): Promise<DeviceInfo> {
70
- try {
71
- // Run these in parallel to avoid sequential timeouts
72
- const [osVersion, model, simOutput] = await Promise.all([
73
- execAdb(['shell', 'getprop', 'ro.build.version.release'], deviceId).catch(() => ''),
74
- execAdb(['shell', 'getprop', 'ro.product.model'], deviceId).catch(() => ''),
75
- execAdb(['shell', 'getprop', 'ro.kernel.qemu'], deviceId).catch(() => '0')
76
- ])
77
-
78
- const simulator = simOutput === '1'
79
- return { platform: 'android', id: deviceId || 'default', osVersion, model, simulator }
80
- } catch (e) {
81
- return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false }
82
- }
83
- }
84
-
85
- export async function startAndroidApp(appId: string, deviceId?: string): Promise<StartAppResponse> {
86
- const metadata = await getAndroidDeviceMetadata(appId, deviceId)
87
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
88
-
89
- await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
90
-
91
- return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 }
92
- }
93
-
94
- export async function terminateAndroidApp(appId: string, deviceId?: string): Promise<TerminateAppResponse> {
95
- const metadata = await getAndroidDeviceMetadata(appId, deviceId)
96
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
97
-
98
- await execAdb(['shell', 'am', 'force-stop', appId], deviceId)
99
-
100
- return { device: deviceInfo, appTerminated: true }
101
- }
102
-
103
- export async function restartAndroidApp(appId: string, deviceId?: string): Promise<RestartAppResponse> {
104
- await terminateAndroidApp(appId, deviceId)
105
- const startResult = await startAndroidApp(appId, deviceId)
106
- return {
107
- device: startResult.device,
108
- appRestarted: startResult.appStarted,
109
- launchTimeMs: startResult.launchTimeMs
110
- }
111
- }
112
-
113
- export async function resetAndroidAppData(appId: string, deviceId?: string): Promise<ResetAppDataResponse> {
114
- const metadata = await getAndroidDeviceMetadata(appId, deviceId)
115
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
116
-
117
- const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId)
118
-
119
- return { device: deviceInfo, dataCleared: output === 'Success' }
120
- }
121
-
122
- export async function getAndroidLogs(appId?: string, lines = 200, deviceId?: string): Promise<GetLogsResponse> {
123
- const metadata = await getAndroidDeviceMetadata(appId || "", deviceId)
124
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
125
-
126
- try {
127
- // We'll skip PID lookup for now to avoid potential hangs with 'pidof' on some emulators
128
- // and rely on robust string matching against the log line.
129
-
130
- // Get logs
131
- const stdout = await execAdb(['logcat', '-d', '-t', lines.toString(), '-v', 'threadtime'], deviceId)
132
- const allLogs = stdout.split('\n')
133
-
134
- let filteredLogs = allLogs
135
- if (appId) {
136
- // Filter by checking if the line contains the appId string.
137
- const matchingLogs = allLogs.filter(line => line.includes(appId))
138
-
139
- if (matchingLogs.length > 0) {
140
- filteredLogs = matchingLogs
141
- } else {
142
- // Fallback: if no logs match the appId, return the raw logs (last N lines)
143
- // This matches the behavior of the "working" version provided by the user,
144
- // ensuring they at least see system activity if the app is silent or crashing early.
145
- filteredLogs = allLogs
146
- }
147
- }
148
-
149
- return { device: deviceInfo, logs: filteredLogs, logCount: filteredLogs.length }
150
- } catch (e) {
151
- console.error("Error fetching logs:", e)
152
- return { device: deviceInfo, logs: [], logCount: 0 }
153
- }
154
- }
155
-
156
- export async function captureAndroidScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
157
- const metadata = await getAndroidDeviceMetadata("", deviceId)
158
- const deviceInfo: DeviceInfo = getDeviceInfo(deviceId || 'default', metadata)
159
-
160
- return new Promise((resolve, reject) => {
161
- const adbArgs = getAdbArgs(['exec-out', 'screencap', '-p'], deviceId)
162
-
163
- // Using spawn for screencap as well to ensure consistent process handling
164
- const child = spawn(ADB, adbArgs)
165
-
166
- const chunks: Buffer[] = []
167
- let stderr = ''
168
-
169
- child.stdout.on('data', (chunk) => {
170
- chunks.push(Buffer.from(chunk))
171
- })
172
-
173
- child.stderr.on('data', (data) => {
174
- stderr += data.toString()
175
- })
176
-
177
- const timeout = setTimeout(() => {
178
- child.kill()
179
- reject(new Error(`ADB screencap timed out after 10s`))
180
- }, 10000)
181
-
182
- child.on('close', (code) => {
183
- clearTimeout(timeout)
184
- if (code !== 0) {
185
- reject(new Error(stderr.trim() || `Screencap failed with code ${code}`))
186
- return
187
- }
188
-
189
- const screenshotBuffer = Buffer.concat(chunks)
190
- const screenshotBase64 = screenshotBuffer.toString('base64')
191
-
192
- // Get resolution
193
- execAdb(['shell', 'wm', 'size'], deviceId)
194
- .then(sizeStdout => {
195
- let width = 0
196
- let height = 0
197
- const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/)
198
- if (match) {
199
- width = parseInt(match[1], 10)
200
- height = parseInt(match[2], 10)
201
- }
202
- resolve({
203
- device: deviceInfo,
204
- screenshot: screenshotBase64,
205
- resolution: { width, height }
206
- })
207
- })
208
- .catch(() => {
209
- resolve({
210
- device: deviceInfo,
211
- screenshot: screenshotBase64,
212
- resolution: { width: 0, height: 0 }
213
- })
214
- })
215
- })
216
-
217
- child.on('error', (err) => {
218
- clearTimeout(timeout)
219
- reject(err)
220
- })
221
- })
222
- }
package/src/ios.ts DELETED
@@ -1,243 +0,0 @@
1
- import { execFile, spawn } from "child_process"
2
- import { promises as fs } from "fs"
3
- import { pathToFileURL } from "url"
4
- import { StartAppResponse, GetLogsResponse, GetCrashResponse, CaptureIOSScreenshotResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, DeviceInfo } from "./types.js"
5
-
6
- const XCRUN = process.env.XCRUN_PATH || "xcrun"
7
-
8
- interface IOSResult {
9
- output: string
10
- device: DeviceInfo
11
- }
12
-
13
- // Validate bundle ID to prevent any potential injection or invalid characters
14
- function validateBundleId(bundleId: string) {
15
- if (!bundleId) return
16
- // Allow alphanumeric, dots, hyphens, and underscores.
17
- if (!/^[a-zA-Z0-9.\-_]+$/.test(bundleId)) {
18
- throw new Error(`Invalid Bundle ID: ${bundleId}. Must contain only alphanumeric characters, dots, hyphens, or underscores.`)
19
- }
20
- }
21
-
22
- function execCommand(args: string[], deviceId: string = "booted"): Promise<IOSResult> {
23
- return new Promise((resolve, reject) => {
24
- // Use spawn for better stream control and consistency with Android implementation
25
- const child = spawn(XCRUN, args)
26
-
27
- let stdout = ''
28
- let stderr = ''
29
-
30
- if (child.stdout) {
31
- child.stdout.on('data', (data) => {
32
- stdout += data.toString()
33
- })
34
- }
35
-
36
- if (child.stderr) {
37
- child.stderr.on('data', (data) => {
38
- stderr += data.toString()
39
- })
40
- }
41
-
42
- const timeoutMs = args.includes('log') ? 10000 : 5000 // 10s for logs, 5s for others
43
- const timeout = setTimeout(() => {
44
- child.kill()
45
- reject(new Error(`Command timed out after ${timeoutMs}ms: ${XCRUN} ${args.join(' ')}`))
46
- }, timeoutMs)
47
-
48
- child.on('close', (code) => {
49
- clearTimeout(timeout)
50
- if (code !== 0) {
51
- reject(new Error(stderr.trim() || `Command failed with code ${code}`))
52
- } else {
53
- resolve({ output: stdout.trim(), device: { platform: "ios", id: deviceId } as DeviceInfo })
54
- }
55
- })
56
-
57
- child.on('error', (err) => {
58
- clearTimeout(timeout)
59
- reject(err)
60
- })
61
- })
62
- }
63
-
64
- function parseRuntimeName(runtime: string): string {
65
- // Example: com.apple.CoreSimulator.SimRuntime.iOS-17-0 -> iOS 17.0
66
- try {
67
- const parts = runtime.split('.')
68
- const lastPart = parts[parts.length - 1]
69
- return lastPart.replace(/-/g, ' ').replace('iOS ', 'iOS ') // Keep iOS prefix
70
- } catch {
71
- return runtime
72
- }
73
- }
74
-
75
- export async function getIOSDeviceMetadata(deviceId: string = "booted"): Promise<DeviceInfo> {
76
- return new Promise((resolve) => {
77
- // If deviceId is provided (and not "booted"), we could try to list just that device.
78
- // But listing all booted devices is usually fine to find the one we want or just one.
79
- // Let's stick to listing all and filtering if needed, or just return basic info if we can't find it.
80
- execFile(XCRUN, ['simctl', 'list', 'devices', 'booted', '--json'], (err, stdout) => {
81
- // Default fallback
82
- const fallback: DeviceInfo = {
83
- platform: "ios",
84
- id: deviceId,
85
- osVersion: "Unknown",
86
- model: "Simulator",
87
- simulator: true,
88
- }
89
-
90
- if (err || !stdout) {
91
- resolve(fallback)
92
- return
93
- }
94
-
95
- try {
96
- const data = JSON.parse(stdout)
97
- const devicesMap = data.devices || {}
98
-
99
- // Find the device
100
- for (const runtime in devicesMap) {
101
- const devices = devicesMap[runtime]
102
- if (Array.isArray(devices)) {
103
- for (const device of devices) {
104
- if (deviceId === "booted" || device.udid === deviceId) {
105
- resolve({
106
- platform: "ios",
107
- id: device.udid,
108
- osVersion: parseRuntimeName(runtime),
109
- model: device.name,
110
- simulator: true,
111
- })
112
- return
113
- }
114
- }
115
- }
116
- }
117
- resolve(fallback)
118
- } catch (error) {
119
- resolve(fallback)
120
- }
121
- })
122
- })
123
- }
124
-
125
- export async function startIOSApp(bundleId: string, deviceId: string = "booted"): Promise<StartAppResponse> {
126
- validateBundleId(bundleId)
127
- const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
128
- const device = await getIOSDeviceMetadata(deviceId)
129
- // Simulate launch time and appStarted for demonstration
130
- return {
131
- device,
132
- appStarted: !!result.output,
133
- launchTimeMs: 1000,
134
- }
135
- }
136
-
137
- export async function terminateIOSApp(bundleId: string, deviceId: string = "booted"): Promise<TerminateAppResponse> {
138
- validateBundleId(bundleId)
139
- await execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId)
140
- const device = await getIOSDeviceMetadata(deviceId)
141
- return {
142
- device,
143
- appTerminated: true
144
- }
145
- }
146
-
147
- export async function restartIOSApp(bundleId: string, deviceId: string = "booted"): Promise<RestartAppResponse> {
148
- // terminateIOSApp already validates bundleId
149
- await terminateIOSApp(bundleId, deviceId)
150
- const startResult = await startIOSApp(bundleId, deviceId)
151
- return {
152
- device: startResult.device,
153
- appRestarted: startResult.appStarted,
154
- launchTimeMs: startResult.launchTimeMs
155
- }
156
- }
157
-
158
- export async function resetIOSAppData(bundleId: string, deviceId: string = "booted"): Promise<ResetAppDataResponse> {
159
- validateBundleId(bundleId)
160
- await terminateIOSApp(bundleId, deviceId)
161
- const device = await getIOSDeviceMetadata(deviceId)
162
-
163
- // Get data container path
164
- const containerResult = await execCommand(['simctl', 'get_app_container', deviceId, bundleId, 'data'], deviceId)
165
- const dataPath = containerResult.output.trim()
166
-
167
- if (!dataPath) {
168
- throw new Error(`Could not find data container for ${bundleId}`)
169
- }
170
-
171
- // Clear contents of Library and Documents
172
- try {
173
- const libraryPath = `${dataPath}/Library`
174
- const documentsPath = `${dataPath}/Documents`
175
- const tmpPath = `${dataPath}/tmp`
176
-
177
- await fs.rm(libraryPath, { recursive: true, force: true }).catch(() => {})
178
- await fs.rm(documentsPath, { recursive: true, force: true }).catch(() => {})
179
- await fs.rm(tmpPath, { recursive: true, force: true }).catch(() => {})
180
-
181
- // Re-create empty directories as they are expected by apps
182
- await fs.mkdir(libraryPath, { recursive: true }).catch(() => {})
183
- await fs.mkdir(documentsPath, { recursive: true }).catch(() => {})
184
- await fs.mkdir(tmpPath, { recursive: true }).catch(() => {})
185
-
186
- return {
187
- device,
188
- dataCleared: true
189
- }
190
- } catch (err) {
191
- throw new Error(`Failed to clear data for ${bundleId}: ${err instanceof Error ? err.message : String(err)}`)
192
- }
193
- }
194
-
195
- export async function getIOSLogs(appId?: string, deviceId: string = "booted"): Promise<GetLogsResponse> {
196
- // If appId is provided, use predicate filtering
197
- // Note: execFile passes args directly, so we don't need shell escaping for the predicate string itself,
198
- // but we do need to construct the predicate correctly for log show.
199
- const args = ['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--last', '1m']
200
- if (appId) {
201
- validateBundleId(appId)
202
- args.push('--predicate', `subsystem contains "${appId}" or process == "${appId}"`)
203
- }
204
-
205
- const result = await execCommand(args, deviceId)
206
- const device = await getIOSDeviceMetadata(deviceId)
207
- const logs = result.output ? result.output.split('\n') : []
208
- return {
209
- device,
210
- logs,
211
- logCount: logs.length,
212
- }
213
- }
214
-
215
-
216
- export async function captureIOSScreenshot(deviceId: string = "booted"): Promise<CaptureIOSScreenshotResponse> {
217
- const device = await getIOSDeviceMetadata(deviceId)
218
- const tmpFile = `/tmp/mcp-ios-screenshot-${Date.now()}.png`
219
-
220
- try {
221
- // 1. Capture screenshot to temp file
222
- await execCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId)
223
-
224
- // 2. Read file as base64
225
- const buffer = await fs.readFile(tmpFile)
226
- const base64 = buffer.toString('base64')
227
-
228
- // 3. Clean up
229
- await fs.rm(tmpFile).catch(() => {})
230
-
231
- return {
232
- device,
233
- screenshot: base64,
234
- // Default resolution since we can't easily parse it without extra libs
235
- // Clients will read the real dimensions from the PNG header anyway
236
- resolution: { width: 0, height: 0 },
237
- }
238
- } catch (err) {
239
- // Ensure cleanup happens even on error
240
- await fs.rm(tmpFile).catch(() => {})
241
- throw new Error(`Failed to capture screenshot: ${err instanceof Error ? err.message : String(err)}`)
242
- }
243
- }