mobile-debug-mcp 0.4.0 → 0.5.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/.github/copilot-instructions.md +33 -0
- package/README.md +154 -23
- package/dist/android.js +174 -32
- package/dist/ios.js +203 -14
- package/dist/server.js +263 -20
- package/docs/CHANGELOG.md +23 -0
- package/package.json +2 -2
- package/smoke-test.js +102 -0
- package/smoke-test.ts +115 -0
- package/src/android.ts +205 -31
- package/src/ios.ts +234 -16
- package/src/server.ts +305 -24
- package/src/types.ts +58 -0
- package/tsconfig.json +2 -1
package/src/ios.ts
CHANGED
|
@@ -1,25 +1,243 @@
|
|
|
1
|
-
import {
|
|
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"
|
|
2
5
|
|
|
3
|
-
|
|
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> {
|
|
4
23
|
return new Promise((resolve, reject) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 })
|
|
10
54
|
}
|
|
11
|
-
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
clearTimeout(timeout)
|
|
59
|
+
reject(err)
|
|
60
|
+
})
|
|
12
61
|
})
|
|
13
62
|
}
|
|
14
63
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
|
22
120
|
}
|
|
23
|
-
)
|
|
121
|
+
})
|
|
24
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
|
+
}
|
|
25
243
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
3
4
|
import {
|
|
@@ -5,13 +6,21 @@ import {
|
|
|
5
6
|
CallToolRequestSchema
|
|
6
7
|
} from "@modelcontextprotocol/sdk/types.js"
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
StartAppResponse,
|
|
11
|
+
DeviceInfo,
|
|
12
|
+
TerminateAppResponse,
|
|
13
|
+
RestartAppResponse,
|
|
14
|
+
ResetAppDataResponse
|
|
15
|
+
} from "./types.js"
|
|
16
|
+
|
|
17
|
+
import { startAndroidApp, getAndroidLogs, captureAndroidScreen, getAndroidDeviceMetadata, terminateAndroidApp, restartAndroidApp, resetAndroidAppData } from "./android.js"
|
|
18
|
+
import { startIOSApp, getIOSLogs, captureIOSScreenshot, getIOSDeviceMetadata, terminateIOSApp, restartIOSApp, resetIOSAppData } from "./ios.js"
|
|
10
19
|
|
|
11
20
|
const server = new Server(
|
|
12
21
|
{
|
|
13
22
|
name: "mobile-debug-mcp",
|
|
14
|
-
version: "0.
|
|
23
|
+
version: "0.4.0"
|
|
15
24
|
},
|
|
16
25
|
{
|
|
17
26
|
capabilities: {
|
|
@@ -20,6 +29,15 @@ const server = new Server(
|
|
|
20
29
|
}
|
|
21
30
|
)
|
|
22
31
|
|
|
32
|
+
function wrapResponse<T>(data: T) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{
|
|
35
|
+
type: "text" as const,
|
|
36
|
+
text: JSON.stringify(data, null, 2)
|
|
37
|
+
}]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
24
42
|
tools: [
|
|
25
43
|
{
|
|
@@ -32,17 +50,65 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
32
50
|
type: "string",
|
|
33
51
|
enum: ["android", "ios"]
|
|
34
52
|
},
|
|
35
|
-
|
|
53
|
+
appId: {
|
|
36
54
|
type: "string",
|
|
37
55
|
description: "Android package name or iOS bundle id"
|
|
56
|
+
},
|
|
57
|
+
deviceId: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
38
60
|
}
|
|
39
61
|
},
|
|
40
|
-
required: ["platform", "
|
|
62
|
+
required: ["platform", "appId"]
|
|
41
63
|
}
|
|
42
64
|
},
|
|
43
65
|
{
|
|
44
|
-
name: "
|
|
45
|
-
description: "
|
|
66
|
+
name: "terminate_app",
|
|
67
|
+
description: "Terminate a mobile app on Android or iOS simulator",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
platform: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["android", "ios"]
|
|
74
|
+
},
|
|
75
|
+
appId: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Android package name or iOS bundle id"
|
|
78
|
+
},
|
|
79
|
+
deviceId: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: ["platform", "appId"]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "restart_app",
|
|
89
|
+
description: "Restart a mobile app on Android or iOS simulator",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
platform: {
|
|
94
|
+
type: "string",
|
|
95
|
+
enum: ["android", "ios"]
|
|
96
|
+
},
|
|
97
|
+
appId: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Android package name or iOS bundle id"
|
|
100
|
+
},
|
|
101
|
+
deviceId: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
required: ["platform", "appId"]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "reset_app_data",
|
|
111
|
+
description: "Reset app data (clear storage) for a mobile app on Android or iOS simulator",
|
|
46
112
|
inputSchema: {
|
|
47
113
|
type: "object",
|
|
48
114
|
properties: {
|
|
@@ -50,16 +116,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
50
116
|
type: "string",
|
|
51
117
|
enum: ["android", "ios"]
|
|
52
118
|
},
|
|
53
|
-
|
|
119
|
+
appId: {
|
|
54
120
|
type: "string",
|
|
55
121
|
description: "Android package name or iOS bundle id"
|
|
56
122
|
},
|
|
123
|
+
deviceId: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: ["platform", "appId"]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "get_logs",
|
|
133
|
+
description: "Get recent logs from Android or iOS simulator. Returns device metadata and the log output.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
platform: {
|
|
138
|
+
type: "string",
|
|
139
|
+
enum: ["android", "ios"]
|
|
140
|
+
},
|
|
141
|
+
appId: {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "Filter by Android package name or iOS bundle id"
|
|
144
|
+
},
|
|
145
|
+
deviceId: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
148
|
+
},
|
|
57
149
|
lines: {
|
|
58
150
|
type: "number",
|
|
59
151
|
description: "Number of log lines (android only)"
|
|
60
152
|
}
|
|
61
153
|
},
|
|
62
|
-
required: ["platform"
|
|
154
|
+
required: ["platform"]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "capture_screenshot",
|
|
159
|
+
description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
platform: {
|
|
164
|
+
type: "string",
|
|
165
|
+
enum: ["android", "ios"]
|
|
166
|
+
},
|
|
167
|
+
deviceId: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ["platform"]
|
|
63
173
|
}
|
|
64
174
|
}
|
|
65
175
|
]
|
|
@@ -70,35 +180,206 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
70
180
|
|
|
71
181
|
try {
|
|
72
182
|
if (name === "start_app") {
|
|
73
|
-
const { platform,
|
|
183
|
+
const { platform, appId, deviceId } = args as {
|
|
74
184
|
platform: "android" | "ios"
|
|
75
|
-
|
|
185
|
+
appId: string
|
|
186
|
+
deviceId?: string
|
|
76
187
|
}
|
|
77
188
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
: await startIOSApp(id)
|
|
189
|
+
let appStarted: boolean
|
|
190
|
+
let launchTimeMs: number
|
|
191
|
+
let deviceInfo: DeviceInfo
|
|
82
192
|
|
|
83
|
-
|
|
84
|
-
|
|
193
|
+
if (platform === "android") {
|
|
194
|
+
const result = await startAndroidApp(appId, deviceId)
|
|
195
|
+
appStarted = result.appStarted
|
|
196
|
+
launchTimeMs = result.launchTimeMs
|
|
197
|
+
deviceInfo = await getAndroidDeviceMetadata(appId, deviceId)
|
|
198
|
+
} else {
|
|
199
|
+
const result = await startIOSApp(appId, deviceId)
|
|
200
|
+
appStarted = result.appStarted
|
|
201
|
+
launchTimeMs = result.launchTimeMs
|
|
202
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const response: StartAppResponse = {
|
|
206
|
+
device: deviceInfo,
|
|
207
|
+
appStarted,
|
|
208
|
+
launchTimeMs
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return wrapResponse(response)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (name === "terminate_app") {
|
|
215
|
+
const { platform, appId, deviceId } = args as {
|
|
216
|
+
platform: "android" | "ios"
|
|
217
|
+
appId: string
|
|
218
|
+
deviceId?: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let appTerminated: boolean
|
|
222
|
+
let deviceInfo: DeviceInfo
|
|
223
|
+
|
|
224
|
+
if (platform === "android") {
|
|
225
|
+
const result = await terminateAndroidApp(appId, deviceId)
|
|
226
|
+
appTerminated = result.appTerminated
|
|
227
|
+
deviceInfo = await getAndroidDeviceMetadata(appId, deviceId)
|
|
228
|
+
} else {
|
|
229
|
+
const result = await terminateIOSApp(appId, deviceId)
|
|
230
|
+
appTerminated = result.appTerminated
|
|
231
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const response: TerminateAppResponse = {
|
|
235
|
+
device: deviceInfo,
|
|
236
|
+
appTerminated
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return wrapResponse(response)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (name === "restart_app") {
|
|
243
|
+
const { platform, appId, deviceId } = args as {
|
|
244
|
+
platform: "android" | "ios"
|
|
245
|
+
appId: string
|
|
246
|
+
deviceId?: string
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let appRestarted: boolean
|
|
250
|
+
let launchTimeMs: number
|
|
251
|
+
let deviceInfo: DeviceInfo
|
|
252
|
+
|
|
253
|
+
if (platform === "android") {
|
|
254
|
+
const result = await restartAndroidApp(appId, deviceId)
|
|
255
|
+
appRestarted = result.appRestarted
|
|
256
|
+
launchTimeMs = result.launchTimeMs
|
|
257
|
+
deviceInfo = await getAndroidDeviceMetadata(appId, deviceId)
|
|
258
|
+
} else {
|
|
259
|
+
const result = await restartIOSApp(appId, deviceId)
|
|
260
|
+
appRestarted = result.appRestarted
|
|
261
|
+
launchTimeMs = result.launchTimeMs
|
|
262
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const response: RestartAppResponse = {
|
|
266
|
+
device: deviceInfo,
|
|
267
|
+
appRestarted,
|
|
268
|
+
launchTimeMs
|
|
85
269
|
}
|
|
270
|
+
|
|
271
|
+
return wrapResponse(response)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (name === "reset_app_data") {
|
|
275
|
+
const { platform, appId, deviceId } = args as {
|
|
276
|
+
platform: "android" | "ios"
|
|
277
|
+
appId: string
|
|
278
|
+
deviceId?: string
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let dataCleared: boolean
|
|
282
|
+
let deviceInfo: DeviceInfo
|
|
283
|
+
|
|
284
|
+
if (platform === "android") {
|
|
285
|
+
const result = await resetAndroidAppData(appId, deviceId)
|
|
286
|
+
dataCleared = result.dataCleared
|
|
287
|
+
deviceInfo = await getAndroidDeviceMetadata(appId, deviceId)
|
|
288
|
+
} else {
|
|
289
|
+
const result = await resetIOSAppData(appId, deviceId)
|
|
290
|
+
dataCleared = result.dataCleared
|
|
291
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const response: ResetAppDataResponse = {
|
|
295
|
+
device: deviceInfo,
|
|
296
|
+
dataCleared
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return wrapResponse(response)
|
|
86
300
|
}
|
|
87
301
|
|
|
88
302
|
if (name === "get_logs") {
|
|
89
|
-
const { platform,
|
|
303
|
+
const { platform, appId, deviceId, lines } = args as {
|
|
90
304
|
platform: "android" | "ios"
|
|
91
|
-
|
|
305
|
+
appId?: string
|
|
306
|
+
deviceId?: string
|
|
92
307
|
lines?: number
|
|
93
308
|
}
|
|
94
309
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
? await getAndroidLogs(id, lines ?? 200)
|
|
98
|
-
: await getIOSLogs()
|
|
310
|
+
let logs: string[]
|
|
311
|
+
let deviceInfo: DeviceInfo
|
|
99
312
|
|
|
313
|
+
if (platform === "android") {
|
|
314
|
+
deviceInfo = await getAndroidDeviceMetadata(appId || "", deviceId)
|
|
315
|
+
const response = await getAndroidLogs(appId, lines ?? 200, deviceId)
|
|
316
|
+
logs = Array.isArray(response.logs) ? response.logs : []
|
|
317
|
+
} else {
|
|
318
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
319
|
+
const response = await getIOSLogs(appId, deviceId)
|
|
320
|
+
logs = Array.isArray(response.logs) ? response.logs : []
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
|
|
324
|
+
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
|
|
325
|
+
|
|
326
|
+
// Return device metadata plus logs
|
|
100
327
|
return {
|
|
101
|
-
content: [
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: JSON.stringify({
|
|
332
|
+
device: deviceInfo,
|
|
333
|
+
result: {
|
|
334
|
+
lines: logs.length,
|
|
335
|
+
crashLines: crashLines.length > 0 ? crashLines : undefined
|
|
336
|
+
}
|
|
337
|
+
}, null, 2)
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: logs.join("\n")
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (name === "capture_screenshot") {
|
|
348
|
+
const { platform, deviceId } = args as { platform: "android" | "ios"; deviceId?: string }
|
|
349
|
+
|
|
350
|
+
let screenshot: string
|
|
351
|
+
let resolution: { width: number; height: number }
|
|
352
|
+
let deviceInfo: DeviceInfo
|
|
353
|
+
|
|
354
|
+
if (platform === "android") {
|
|
355
|
+
deviceInfo = await getAndroidDeviceMetadata("", deviceId)
|
|
356
|
+
const result = await captureAndroidScreen(deviceId)
|
|
357
|
+
screenshot = result.screenshot
|
|
358
|
+
resolution = result.resolution
|
|
359
|
+
} else {
|
|
360
|
+
deviceInfo = await getIOSDeviceMetadata(deviceId)
|
|
361
|
+
const result = await captureIOSScreenshot(deviceId)
|
|
362
|
+
screenshot = result.screenshot
|
|
363
|
+
resolution = result.resolution
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: JSON.stringify({
|
|
371
|
+
device: deviceInfo,
|
|
372
|
+
result: {
|
|
373
|
+
resolution
|
|
374
|
+
}
|
|
375
|
+
}, null, 2)
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
type: "image",
|
|
379
|
+
data: screenshot,
|
|
380
|
+
mimeType: "image/png"
|
|
381
|
+
}
|
|
382
|
+
]
|
|
102
383
|
}
|
|
103
384
|
}
|
|
104
385
|
} catch (error) {
|