@ggboi360/mobile-dev-mcp 0.2.0 → 0.3.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/LICENSE +19 -18
- package/LICENSE-ELASTIC +10 -22
- package/README.md +51 -172
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +714 -3394
- package/dist/index.js.map +1 -1
- package/dist/license.d.ts +11 -96
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +243 -570
- package/dist/license.js.map +1 -1
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +72 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +307 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -3
- package/dist/license.test.d.ts +0 -2
- package/dist/license.test.d.ts.map +0 -1
- package/dist/license.test.js +0 -198
- package/dist/license.test.js.map +0 -1
- package/dist/tools.test.d.ts +0 -2
- package/dist/tools.test.d.ts.map +0 -1
- package/dist/tools.test.js +0 -337
- package/dist/tools.test.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,3655 +1,976 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mobile Dev MCP - Read-Only Debugging Tools for Mobile Development
|
|
4
|
+
*
|
|
5
|
+
* A read-only MCP server for observing and debugging mobile apps:
|
|
6
|
+
* - Taking screenshots
|
|
7
|
+
* - Viewing device information
|
|
8
|
+
* - Reading logs
|
|
9
|
+
* - Inspecting UI hierarchy
|
|
10
|
+
* - Analyzing screen content
|
|
11
|
+
*
|
|
12
|
+
* License: Dual (MIT + Elastic License 2.0)
|
|
13
|
+
*/
|
|
2
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
16
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import WebSocket from "ws";
|
|
11
|
-
// License module
|
|
12
|
-
import { checkLicense, requireAdvanced, requireBasic, licenseTools, handleLicenseTool, loadConfig, TIER_LIMITS, } from "./license.js";
|
|
13
|
-
const execAsync = promisify(exec);
|
|
14
|
-
// Configuration (loaded from user config)
|
|
17
|
+
import { ADB, XCRUN, execAsync, sleep, parseUiTree, findElementInTree, captureAndroidScreenshot, captureIosScreenshot, listConnectedDevices, loadConfig, validateDeviceId, validatePackageName, validateUdid, validateLogFilter, validateLogLevel, } from "./utils.js";
|
|
18
|
+
import { checkLicense, canAccessTool, getMaxLogLines, getMaxDevices, getLicenseStatus, setLicenseKey, } from "./license.js";
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// CONFIG
|
|
21
|
+
// ============================================================================
|
|
15
22
|
const userConfig = loadConfig();
|
|
16
23
|
const CONFIG = {
|
|
17
24
|
metroPort: userConfig.metroPort || 8081,
|
|
18
|
-
logBufferSize: userConfig.logBufferSize || 100,
|
|
19
25
|
screenshotDir: process.env.TEMP || "/tmp",
|
|
20
26
|
};
|
|
21
|
-
// Log buffers
|
|
22
|
-
let metroLogBuffer = [];
|
|
23
|
-
let adbLogBuffer = [];
|
|
24
|
-
let metroProcess = null;
|
|
25
|
-
let adbLogProcess = null;
|
|
26
|
-
// Screenshot history for Pro users
|
|
27
|
-
let screenshotHistory = [];
|
|
28
|
-
const MAX_SCREENSHOT_HISTORY = 20;
|
|
29
27
|
// ============================================================================
|
|
30
|
-
// TOOL DEFINITIONS
|
|
28
|
+
// TOOL DEFINITIONS (21 tools)
|
|
31
29
|
// ============================================================================
|
|
32
|
-
const
|
|
33
|
-
// === FREE TIER
|
|
34
|
-
{
|
|
35
|
-
name: "get_metro_logs",
|
|
36
|
-
description: "Get recent logs from Metro bundler. Returns the last N lines of Metro output. Useful for seeing build errors, warnings, and bundle status.",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
type: "object",
|
|
39
|
-
properties: {
|
|
40
|
-
lines: {
|
|
41
|
-
type: "number",
|
|
42
|
-
description: "Number of log lines to retrieve (default: 50, max: 50 for free tier)",
|
|
43
|
-
default: 50,
|
|
44
|
-
},
|
|
45
|
-
filter: {
|
|
46
|
-
type: "string",
|
|
47
|
-
description: "Optional filter string to match (e.g., 'error', 'warning')",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: "get_adb_logs",
|
|
54
|
-
description: "Get logs from Android device/emulator via ADB logcat. Filters for React Native and JavaScript logs by default.",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: {
|
|
58
|
-
lines: {
|
|
59
|
-
type: "number",
|
|
60
|
-
description: "Number of log lines to retrieve (default: 50, max: 50 for free tier)",
|
|
61
|
-
default: 50,
|
|
62
|
-
},
|
|
63
|
-
filter: {
|
|
64
|
-
type: "string",
|
|
65
|
-
description: "Tag filter for logcat (default: 'ReactNativeJS'). Use '*' for all logs.",
|
|
66
|
-
default: "ReactNativeJS",
|
|
67
|
-
},
|
|
68
|
-
level: {
|
|
69
|
-
type: "string",
|
|
70
|
-
enum: ["V", "D", "I", "W", "E", "F"],
|
|
71
|
-
description: "Minimum log level: V(erbose), D(ebug), I(nfo), W(arn), E(rror), F(atal)",
|
|
72
|
-
default: "I",
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
30
|
+
const tools = [
|
|
31
|
+
// === FREE TIER - Screenshots (2 tools) ===
|
|
77
32
|
{
|
|
78
33
|
name: "screenshot_emulator",
|
|
79
|
-
description: "Capture a screenshot from the currently running Android emulator. Returns
|
|
34
|
+
description: "Capture a screenshot from the currently running Android emulator. Returns base64-encoded PNG image.",
|
|
80
35
|
inputSchema: {
|
|
81
36
|
type: "object",
|
|
82
37
|
properties: {
|
|
83
38
|
device: {
|
|
84
39
|
type: "string",
|
|
85
|
-
description: "Specific device ID
|
|
40
|
+
description: "Specific device ID. Leave empty for default device.",
|
|
86
41
|
},
|
|
87
42
|
},
|
|
88
43
|
},
|
|
89
44
|
},
|
|
90
45
|
{
|
|
91
|
-
name: "
|
|
92
|
-
description: "
|
|
93
|
-
inputSchema: {
|
|
94
|
-
type: "object",
|
|
95
|
-
properties: {},
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: "check_metro_status",
|
|
100
|
-
description: "Check if Metro bundler is running and get its current status. Returns bundle status and any pending builds.",
|
|
46
|
+
name: "screenshot_ios_simulator",
|
|
47
|
+
description: "Capture a screenshot from an iOS Simulator. Returns base64-encoded PNG image.",
|
|
101
48
|
inputSchema: {
|
|
102
49
|
type: "object",
|
|
103
50
|
properties: {
|
|
104
|
-
|
|
105
|
-
type: "
|
|
106
|
-
description: "
|
|
107
|
-
default: 8081,
|
|
51
|
+
udid: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Simulator UDID. Leave empty for booted simulator.",
|
|
108
54
|
},
|
|
109
55
|
},
|
|
110
56
|
},
|
|
111
57
|
},
|
|
58
|
+
// === FREE TIER - Device Info (5 tools) ===
|
|
112
59
|
{
|
|
113
|
-
name: "
|
|
114
|
-
description: "
|
|
115
|
-
inputSchema: {
|
|
116
|
-
type: "object",
|
|
117
|
-
properties: {
|
|
118
|
-
packageName: {
|
|
119
|
-
type: "string",
|
|
120
|
-
description: "The app package name (e.g., 'com.myapp' or 'host.exp.exponent')",
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
required: ["packageName"],
|
|
124
|
-
},
|
|
60
|
+
name: "list_devices",
|
|
61
|
+
description: "List all connected Android devices and emulators via ADB.",
|
|
62
|
+
inputSchema: { type: "object", properties: {} },
|
|
125
63
|
},
|
|
126
64
|
{
|
|
127
|
-
name: "
|
|
128
|
-
description: "
|
|
65
|
+
name: "list_ios_simulators",
|
|
66
|
+
description: "List all available iOS Simulators with state and iOS version.",
|
|
129
67
|
inputSchema: {
|
|
130
68
|
type: "object",
|
|
131
69
|
properties: {
|
|
132
|
-
|
|
133
|
-
type: "
|
|
134
|
-
description: "
|
|
70
|
+
onlyBooted: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Only show booted simulators (default: false)",
|
|
73
|
+
default: false,
|
|
135
74
|
},
|
|
136
75
|
},
|
|
137
|
-
required: ["packageName"],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "restart_adb",
|
|
142
|
-
description: "Restart the ADB server. Useful when ADB becomes unresponsive or devices are not detected.",
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: "object",
|
|
145
|
-
properties: {},
|
|
146
76
|
},
|
|
147
77
|
},
|
|
148
78
|
{
|
|
149
79
|
name: "get_device_info",
|
|
150
|
-
description: "Get detailed information about
|
|
80
|
+
description: "Get detailed information about connected Android device (OS version, screen size, memory).",
|
|
151
81
|
inputSchema: {
|
|
152
82
|
type: "object",
|
|
153
83
|
properties: {
|
|
154
84
|
device: {
|
|
155
85
|
type: "string",
|
|
156
|
-
description: "Specific device ID. Leave empty for default
|
|
86
|
+
description: "Specific device ID. Leave empty for default.",
|
|
157
87
|
},
|
|
158
88
|
},
|
|
159
89
|
},
|
|
160
90
|
},
|
|
161
91
|
{
|
|
162
|
-
name: "
|
|
163
|
-
description: "
|
|
92
|
+
name: "get_ios_simulator_info",
|
|
93
|
+
description: "Get detailed information about an iOS Simulator.",
|
|
164
94
|
inputSchema: {
|
|
165
95
|
type: "object",
|
|
166
96
|
properties: {
|
|
167
|
-
|
|
97
|
+
udid: {
|
|
168
98
|
type: "string",
|
|
169
|
-
description: "
|
|
99
|
+
description: "Simulator UDID. Leave empty for booted simulator.",
|
|
170
100
|
},
|
|
171
101
|
},
|
|
172
102
|
},
|
|
173
103
|
},
|
|
174
104
|
{
|
|
175
|
-
name: "
|
|
176
|
-
description: "
|
|
177
|
-
inputSchema: {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: {},
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
// === PRO TIER TOOLS ===
|
|
183
|
-
{
|
|
184
|
-
name: "stream_adb_realtime",
|
|
185
|
-
description: "[PRO] Start real-time ADB log streaming. Logs are continuously captured in the background.",
|
|
105
|
+
name: "get_app_info",
|
|
106
|
+
description: "Get information about an installed app on Android device.",
|
|
186
107
|
inputSchema: {
|
|
187
108
|
type: "object",
|
|
188
109
|
properties: {
|
|
189
|
-
|
|
110
|
+
packageName: {
|
|
190
111
|
type: "string",
|
|
191
|
-
description: "
|
|
192
|
-
default: "ReactNativeJS",
|
|
112
|
+
description: "The app package name (e.g., 'com.myapp')",
|
|
193
113
|
},
|
|
194
114
|
},
|
|
115
|
+
required: ["packageName"],
|
|
195
116
|
},
|
|
196
117
|
},
|
|
118
|
+
// === FREE TIER - Logs (4 tools) ===
|
|
197
119
|
{
|
|
198
|
-
name: "
|
|
199
|
-
description: "
|
|
200
|
-
inputSchema: {
|
|
201
|
-
type: "object",
|
|
202
|
-
properties: {},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
name: "screenshot_history",
|
|
207
|
-
description: "[PRO] Get previously captured screenshots. Stores up to 20 screenshots.",
|
|
120
|
+
name: "get_metro_logs",
|
|
121
|
+
description: "Get recent logs from Metro bundler. Useful for build errors and warnings.",
|
|
208
122
|
inputSchema: {
|
|
209
123
|
type: "object",
|
|
210
124
|
properties: {
|
|
211
|
-
|
|
125
|
+
lines: {
|
|
212
126
|
type: "number",
|
|
213
|
-
description: "Number of
|
|
214
|
-
default:
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: "watch_for_errors",
|
|
221
|
-
description: "[PRO] Start watching logs for specific error patterns. Returns when an error is detected.",
|
|
222
|
-
inputSchema: {
|
|
223
|
-
type: "object",
|
|
224
|
-
properties: {
|
|
225
|
-
patterns: {
|
|
226
|
-
type: "array",
|
|
227
|
-
items: { type: "string" },
|
|
228
|
-
description: "Error patterns to watch for (e.g., ['Error', 'Exception', 'crash'])",
|
|
127
|
+
description: "Number of lines (default: 50, limit varies by tier)",
|
|
128
|
+
default: 50,
|
|
229
129
|
},
|
|
230
|
-
|
|
231
|
-
type: "
|
|
232
|
-
description: "
|
|
233
|
-
default: 60,
|
|
130
|
+
filter: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Optional filter string (e.g., 'error', 'warning')",
|
|
234
133
|
},
|
|
235
134
|
},
|
|
236
135
|
},
|
|
237
136
|
},
|
|
238
137
|
{
|
|
239
|
-
name: "
|
|
240
|
-
description: "
|
|
138
|
+
name: "get_adb_logs",
|
|
139
|
+
description: "Get logs from Android device via ADB logcat. Filters for React Native by default.",
|
|
241
140
|
inputSchema: {
|
|
242
141
|
type: "object",
|
|
243
142
|
properties: {
|
|
244
|
-
devices: {
|
|
245
|
-
type: "array",
|
|
246
|
-
items: { type: "string" },
|
|
247
|
-
description: "Array of device IDs to get logs from",
|
|
248
|
-
},
|
|
249
143
|
lines: {
|
|
250
144
|
type: "number",
|
|
251
|
-
description: "Number of
|
|
252
|
-
default:
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
// === INTERACTION TOOLS (Advanced) ===
|
|
258
|
-
{
|
|
259
|
-
name: "tap_screen",
|
|
260
|
-
description: "[PRO] Tap on the screen at specific coordinates. Use this to interact with UI elements.",
|
|
261
|
-
inputSchema: {
|
|
262
|
-
type: "object",
|
|
263
|
-
properties: {
|
|
264
|
-
x: {
|
|
265
|
-
type: "number",
|
|
266
|
-
description: "X coordinate to tap",
|
|
267
|
-
},
|
|
268
|
-
y: {
|
|
269
|
-
type: "number",
|
|
270
|
-
description: "Y coordinate to tap",
|
|
271
|
-
},
|
|
272
|
-
device: {
|
|
273
|
-
type: "string",
|
|
274
|
-
description: "Specific device ID (optional)",
|
|
145
|
+
description: "Number of lines (default: 50, limit varies by tier)",
|
|
146
|
+
default: 50,
|
|
275
147
|
},
|
|
276
|
-
|
|
277
|
-
required: ["x", "y"],
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
name: "input_text",
|
|
282
|
-
description: "[PRO] Type text into the currently focused input field.",
|
|
283
|
-
inputSchema: {
|
|
284
|
-
type: "object",
|
|
285
|
-
properties: {
|
|
286
|
-
text: {
|
|
148
|
+
filter: {
|
|
287
149
|
type: "string",
|
|
288
|
-
description: "
|
|
150
|
+
description: "Tag filter (default: 'ReactNativeJS'). Use '*' for all.",
|
|
151
|
+
default: "ReactNativeJS",
|
|
289
152
|
},
|
|
290
|
-
|
|
153
|
+
level: {
|
|
291
154
|
type: "string",
|
|
292
|
-
|
|
155
|
+
enum: ["V", "D", "I", "W", "E", "F"],
|
|
156
|
+
description: "Minimum log level",
|
|
157
|
+
default: "I",
|
|
293
158
|
},
|
|
294
159
|
},
|
|
295
|
-
required: ["text"],
|
|
296
160
|
},
|
|
297
161
|
},
|
|
298
162
|
{
|
|
299
|
-
name: "
|
|
300
|
-
description: "
|
|
163
|
+
name: "get_ios_simulator_logs",
|
|
164
|
+
description: "Get recent logs from iOS Simulator.",
|
|
301
165
|
inputSchema: {
|
|
302
166
|
type: "object",
|
|
303
167
|
properties: {
|
|
304
|
-
|
|
168
|
+
lines: {
|
|
169
|
+
type: "number",
|
|
170
|
+
description: "Number of lines (default: 50)",
|
|
171
|
+
default: 50,
|
|
172
|
+
},
|
|
173
|
+
filter: {
|
|
305
174
|
type: "string",
|
|
306
|
-
|
|
307
|
-
description: "Button to press",
|
|
175
|
+
description: "Filter logs by subsystem or content",
|
|
308
176
|
},
|
|
309
|
-
|
|
177
|
+
udid: {
|
|
310
178
|
type: "string",
|
|
311
|
-
description: "
|
|
179
|
+
description: "Simulator UDID. Leave empty for booted simulator.",
|
|
312
180
|
},
|
|
313
181
|
},
|
|
314
|
-
required: ["button"],
|
|
315
182
|
},
|
|
316
183
|
},
|
|
317
184
|
{
|
|
318
|
-
name: "
|
|
319
|
-
description: "
|
|
185
|
+
name: "check_metro_status",
|
|
186
|
+
description: "Check if Metro bundler is running and get its status.",
|
|
320
187
|
inputSchema: {
|
|
321
188
|
type: "object",
|
|
322
189
|
properties: {
|
|
323
|
-
|
|
324
|
-
type: "number",
|
|
325
|
-
description: "Starting X coordinate",
|
|
326
|
-
},
|
|
327
|
-
startY: {
|
|
328
|
-
type: "number",
|
|
329
|
-
description: "Starting Y coordinate",
|
|
330
|
-
},
|
|
331
|
-
endX: {
|
|
332
|
-
type: "number",
|
|
333
|
-
description: "Ending X coordinate",
|
|
334
|
-
},
|
|
335
|
-
endY: {
|
|
336
|
-
type: "number",
|
|
337
|
-
description: "Ending Y coordinate",
|
|
338
|
-
},
|
|
339
|
-
duration: {
|
|
190
|
+
port: {
|
|
340
191
|
type: "number",
|
|
341
|
-
description: "
|
|
342
|
-
default:
|
|
343
|
-
},
|
|
344
|
-
device: {
|
|
345
|
-
type: "string",
|
|
346
|
-
description: "Specific device ID (optional)",
|
|
192
|
+
description: "Metro port (default: 8081)",
|
|
193
|
+
default: 8081,
|
|
347
194
|
},
|
|
348
195
|
},
|
|
349
|
-
required: ["startX", "startY", "endX", "endY"],
|
|
350
196
|
},
|
|
351
197
|
},
|
|
198
|
+
// === FREE TIER - License (1 tool) ===
|
|
352
199
|
{
|
|
353
|
-
name: "
|
|
354
|
-
description: "
|
|
355
|
-
inputSchema: {
|
|
356
|
-
type: "object",
|
|
357
|
-
properties: {
|
|
358
|
-
packageName: {
|
|
359
|
-
type: "string",
|
|
360
|
-
description: "Package name of the app (e.g., 'com.example.myapp')",
|
|
361
|
-
},
|
|
362
|
-
device: {
|
|
363
|
-
type: "string",
|
|
364
|
-
description: "Specific device ID (optional)",
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
required: ["packageName"],
|
|
368
|
-
},
|
|
200
|
+
name: "get_license_status",
|
|
201
|
+
description: "Get your current license tier and available features.",
|
|
202
|
+
inputSchema: { type: "object", properties: {} },
|
|
369
203
|
},
|
|
204
|
+
// === ADVANCED TIER - UI Inspection (5 tools) ===
|
|
370
205
|
{
|
|
371
|
-
name: "
|
|
372
|
-
description: "[
|
|
206
|
+
name: "get_ui_tree",
|
|
207
|
+
description: "[ADVANCED] Get the current UI hierarchy from Android device. Returns all visible elements with text, bounds, and properties.",
|
|
373
208
|
inputSchema: {
|
|
374
209
|
type: "object",
|
|
375
210
|
properties: {
|
|
376
|
-
apkPath: {
|
|
377
|
-
type: "string",
|
|
378
|
-
description: "Path to the APK file",
|
|
379
|
-
},
|
|
380
211
|
device: {
|
|
381
212
|
type: "string",
|
|
382
213
|
description: "Specific device ID (optional)",
|
|
383
214
|
},
|
|
384
|
-
|
|
385
|
-
required: ["apkPath"],
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
// === iOS SIMULATOR TOOLS ===
|
|
389
|
-
{
|
|
390
|
-
name: "list_ios_simulators",
|
|
391
|
-
description: "List all available iOS Simulators. Shows device name, UDID, state (Booted/Shutdown), and iOS version.",
|
|
392
|
-
inputSchema: {
|
|
393
|
-
type: "object",
|
|
394
|
-
properties: {
|
|
395
|
-
onlyBooted: {
|
|
215
|
+
compressed: {
|
|
396
216
|
type: "boolean",
|
|
397
|
-
description: "
|
|
398
|
-
default:
|
|
217
|
+
description: "Return only interactive elements (default: true)",
|
|
218
|
+
default: true,
|
|
399
219
|
},
|
|
400
220
|
},
|
|
401
221
|
},
|
|
402
222
|
},
|
|
403
223
|
{
|
|
404
|
-
name: "
|
|
405
|
-
description: "
|
|
224
|
+
name: "find_element",
|
|
225
|
+
description: "[ADVANCED] Find a UI element by text, resource ID, or content description.",
|
|
406
226
|
inputSchema: {
|
|
407
227
|
type: "object",
|
|
408
228
|
properties: {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
},
|
|
229
|
+
text: { type: "string", description: "Element text to find" },
|
|
230
|
+
resourceId: { type: "string", description: "Resource ID to find" },
|
|
231
|
+
contentDescription: { type: "string", description: "Accessibility label" },
|
|
232
|
+
device: { type: "string", description: "Specific device ID (optional)" },
|
|
413
233
|
},
|
|
414
234
|
},
|
|
415
235
|
},
|
|
416
236
|
{
|
|
417
|
-
name: "
|
|
418
|
-
description: "
|
|
237
|
+
name: "wait_for_element",
|
|
238
|
+
description: "[ADVANCED] Wait for a UI element to appear on screen.",
|
|
419
239
|
inputSchema: {
|
|
420
240
|
type: "object",
|
|
421
241
|
properties: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
filter: {
|
|
427
|
-
type: "string",
|
|
428
|
-
description: "Filter logs by subsystem or message content",
|
|
429
|
-
},
|
|
430
|
-
lines: {
|
|
242
|
+
text: { type: "string", description: "Element text to wait for" },
|
|
243
|
+
resourceId: { type: "string", description: "Resource ID to wait for" },
|
|
244
|
+
contentDescription: { type: "string", description: "Accessibility label" },
|
|
245
|
+
timeout: {
|
|
431
246
|
type: "number",
|
|
432
|
-
description: "
|
|
433
|
-
default:
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
name: "get_ios_simulator_info",
|
|
440
|
-
description: "Get detailed information about an iOS Simulator including device type, iOS version, and state.",
|
|
441
|
-
inputSchema: {
|
|
442
|
-
type: "object",
|
|
443
|
-
properties: {
|
|
444
|
-
udid: {
|
|
445
|
-
type: "string",
|
|
446
|
-
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
name: "boot_ios_simulator",
|
|
453
|
-
description: "[PRO] Boot an iOS Simulator by UDID or device name.",
|
|
454
|
-
inputSchema: {
|
|
455
|
-
type: "object",
|
|
456
|
-
properties: {
|
|
457
|
-
udid: {
|
|
458
|
-
type: "string",
|
|
459
|
-
description: "Simulator UDID or device name (e.g., 'iPhone 15 Pro')",
|
|
247
|
+
description: "Maximum wait time in ms (default: 5000)",
|
|
248
|
+
default: 5000,
|
|
460
249
|
},
|
|
250
|
+
device: { type: "string", description: "Specific device ID (optional)" },
|
|
461
251
|
},
|
|
462
|
-
required: ["udid"],
|
|
463
252
|
},
|
|
464
253
|
},
|
|
465
254
|
{
|
|
466
|
-
name: "
|
|
467
|
-
description: "[
|
|
255
|
+
name: "get_element_property",
|
|
256
|
+
description: "[ADVANCED] Get a specific property of a UI element.",
|
|
468
257
|
inputSchema: {
|
|
469
258
|
type: "object",
|
|
470
259
|
properties: {
|
|
471
|
-
|
|
260
|
+
text: { type: "string", description: "Element text to find" },
|
|
261
|
+
resourceId: { type: "string", description: "Resource ID to find" },
|
|
262
|
+
property: {
|
|
472
263
|
type: "string",
|
|
473
|
-
|
|
264
|
+
enum: ["text", "enabled", "checked", "selected", "focused", "clickable", "scrollable"],
|
|
265
|
+
description: "Property to retrieve",
|
|
474
266
|
},
|
|
267
|
+
device: { type: "string", description: "Specific device ID (optional)" },
|
|
475
268
|
},
|
|
476
|
-
required: ["
|
|
269
|
+
required: ["property"],
|
|
477
270
|
},
|
|
478
271
|
},
|
|
479
272
|
{
|
|
480
|
-
name: "
|
|
481
|
-
description: "[
|
|
273
|
+
name: "assert_element",
|
|
274
|
+
description: "[ADVANCED] Verify a UI element exists or has expected state.",
|
|
482
275
|
inputSchema: {
|
|
483
276
|
type: "object",
|
|
484
277
|
properties: {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
278
|
+
text: { type: "string", description: "Element text to verify" },
|
|
279
|
+
resourceId: { type: "string", description: "Resource ID to verify" },
|
|
280
|
+
shouldExist: {
|
|
281
|
+
type: "boolean",
|
|
282
|
+
description: "Whether element should exist (default: true)",
|
|
283
|
+
default: true,
|
|
492
284
|
},
|
|
285
|
+
isEnabled: { type: "boolean", description: "Expected enabled state" },
|
|
286
|
+
isChecked: { type: "boolean", description: "Expected checked state" },
|
|
287
|
+
device: { type: "string", description: "Specific device ID (optional)" },
|
|
493
288
|
},
|
|
494
|
-
required: ["appPath"],
|
|
495
289
|
},
|
|
496
290
|
},
|
|
291
|
+
// === ADVANCED TIER - Screen Analysis (3 tools) ===
|
|
497
292
|
{
|
|
498
|
-
name: "
|
|
499
|
-
description: "[
|
|
293
|
+
name: "suggest_action",
|
|
294
|
+
description: "[ADVANCED] Analyze the screen and suggest what action to take based on the current UI state. Returns suggestions without executing.",
|
|
500
295
|
inputSchema: {
|
|
501
296
|
type: "object",
|
|
502
297
|
properties: {
|
|
503
|
-
|
|
504
|
-
type: "string",
|
|
505
|
-
description: "App bundle identifier (e.g., 'com.example.myapp')",
|
|
506
|
-
},
|
|
507
|
-
udid: {
|
|
298
|
+
goal: {
|
|
508
299
|
type: "string",
|
|
509
|
-
description: "
|
|
300
|
+
description: "What you're trying to accomplish (e.g., 'login', 'send message', 'navigate to settings')",
|
|
510
301
|
},
|
|
302
|
+
device: { type: "string", description: "Specific device ID" },
|
|
511
303
|
},
|
|
512
|
-
required: ["
|
|
304
|
+
required: ["goal"],
|
|
513
305
|
},
|
|
514
306
|
},
|
|
515
307
|
{
|
|
516
|
-
name: "
|
|
517
|
-
description: "[
|
|
308
|
+
name: "analyze_screen",
|
|
309
|
+
description: "[ADVANCED] Get a detailed analysis of what's currently on the screen.",
|
|
518
310
|
inputSchema: {
|
|
519
311
|
type: "object",
|
|
520
312
|
properties: {
|
|
521
|
-
|
|
522
|
-
type: "string",
|
|
523
|
-
description: "App bundle identifier to terminate",
|
|
524
|
-
},
|
|
525
|
-
udid: {
|
|
526
|
-
type: "string",
|
|
527
|
-
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
528
|
-
},
|
|
313
|
+
device: { type: "string", description: "Specific device ID" },
|
|
529
314
|
},
|
|
530
|
-
required: ["bundleId"],
|
|
531
315
|
},
|
|
532
316
|
},
|
|
533
317
|
{
|
|
534
|
-
name: "
|
|
535
|
-
description: "[
|
|
318
|
+
name: "get_screen_text",
|
|
319
|
+
description: "[ADVANCED] Extract all visible text from the current screen.",
|
|
536
320
|
inputSchema: {
|
|
537
321
|
type: "object",
|
|
538
322
|
properties: {
|
|
539
|
-
|
|
540
|
-
type: "string",
|
|
541
|
-
description: "URL to open (e.g., 'myapp://screen' or 'https://example.com')",
|
|
542
|
-
},
|
|
543
|
-
udid: {
|
|
544
|
-
type: "string",
|
|
545
|
-
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
546
|
-
},
|
|
323
|
+
device: { type: "string", description: "Specific device ID" },
|
|
547
324
|
},
|
|
548
|
-
required: ["url"],
|
|
549
325
|
},
|
|
550
326
|
},
|
|
327
|
+
// === ADVANCED TIER - License (1 tool) ===
|
|
551
328
|
{
|
|
552
|
-
name: "
|
|
553
|
-
description: "
|
|
329
|
+
name: "set_license_key",
|
|
330
|
+
description: "Activate a license key to unlock premium features.",
|
|
554
331
|
inputSchema: {
|
|
555
332
|
type: "object",
|
|
556
333
|
properties: {
|
|
557
|
-
|
|
558
|
-
type: "string",
|
|
559
|
-
description: "App bundle identifier",
|
|
560
|
-
},
|
|
561
|
-
payload: {
|
|
562
|
-
type: "object",
|
|
563
|
-
description: "Push notification payload (APS format)",
|
|
564
|
-
},
|
|
565
|
-
udid: {
|
|
334
|
+
licenseKey: {
|
|
566
335
|
type: "string",
|
|
567
|
-
description: "
|
|
336
|
+
description: "Your license key from mobiledevmcp.dev",
|
|
568
337
|
},
|
|
569
338
|
},
|
|
570
|
-
required: ["
|
|
339
|
+
required: ["licenseKey"],
|
|
571
340
|
},
|
|
572
341
|
},
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
},
|
|
587
|
-
udid: {
|
|
588
|
-
type: "string",
|
|
589
|
-
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
required: ["latitude", "longitude"],
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
// === REACT DEVTOOLS TOOLS ===
|
|
596
|
-
{
|
|
597
|
-
name: "setup_react_devtools",
|
|
598
|
-
description: "[PRO] Set up React DevTools connection for debugging. Configures port forwarding and checks connectivity.",
|
|
599
|
-
inputSchema: {
|
|
600
|
-
type: "object",
|
|
601
|
-
properties: {
|
|
602
|
-
port: {
|
|
603
|
-
type: "number",
|
|
604
|
-
description: "DevTools port (default: 8097)",
|
|
605
|
-
default: 8097,
|
|
606
|
-
},
|
|
607
|
-
device: {
|
|
608
|
-
type: "string",
|
|
609
|
-
description: "Specific Android device ID (optional)",
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
},
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
name: "check_devtools_connection",
|
|
616
|
-
description: "[PRO] Check if React DevTools is connected and get connection status.",
|
|
617
|
-
inputSchema: {
|
|
618
|
-
type: "object",
|
|
619
|
-
properties: {
|
|
620
|
-
port: {
|
|
621
|
-
type: "number",
|
|
622
|
-
description: "DevTools port (default: 8097)",
|
|
623
|
-
default: 8097,
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
name: "get_react_component_tree",
|
|
630
|
-
description: "[PRO] Get the React component hierarchy from connected DevTools. Shows component names and structure.",
|
|
631
|
-
inputSchema: {
|
|
632
|
-
type: "object",
|
|
633
|
-
properties: {
|
|
634
|
-
port: {
|
|
635
|
-
type: "number",
|
|
636
|
-
description: "DevTools port (default: 8097)",
|
|
637
|
-
default: 8097,
|
|
638
|
-
},
|
|
639
|
-
depth: {
|
|
640
|
-
type: "number",
|
|
641
|
-
description: "Maximum depth to traverse (default: 5)",
|
|
642
|
-
default: 5,
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
name: "inspect_react_component",
|
|
649
|
-
description: "[PRO] Inspect a specific React component by ID. Returns props, state, and hooks.",
|
|
650
|
-
inputSchema: {
|
|
651
|
-
type: "object",
|
|
652
|
-
properties: {
|
|
653
|
-
componentId: {
|
|
654
|
-
type: "number",
|
|
655
|
-
description: "Component ID from the component tree",
|
|
656
|
-
},
|
|
657
|
-
port: {
|
|
658
|
-
type: "number",
|
|
659
|
-
description: "DevTools port (default: 8097)",
|
|
660
|
-
default: 8097,
|
|
661
|
-
},
|
|
662
|
-
},
|
|
663
|
-
required: ["componentId"],
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
name: "search_react_components",
|
|
668
|
-
description: "[PRO] Search for React components by name or pattern.",
|
|
669
|
-
inputSchema: {
|
|
670
|
-
type: "object",
|
|
671
|
-
properties: {
|
|
672
|
-
query: {
|
|
673
|
-
type: "string",
|
|
674
|
-
description: "Component name or pattern to search for",
|
|
675
|
-
},
|
|
676
|
-
port: {
|
|
677
|
-
type: "number",
|
|
678
|
-
description: "DevTools port (default: 8097)",
|
|
679
|
-
default: 8097,
|
|
680
|
-
},
|
|
681
|
-
},
|
|
682
|
-
required: ["query"],
|
|
683
|
-
},
|
|
684
|
-
},
|
|
685
|
-
// === NETWORK INSPECTION TOOLS ===
|
|
686
|
-
{
|
|
687
|
-
name: "get_network_requests",
|
|
688
|
-
description: "[PRO] Get recent network requests from app logs. Parses fetch/XHR requests from React Native logs.",
|
|
689
|
-
inputSchema: {
|
|
690
|
-
type: "object",
|
|
691
|
-
properties: {
|
|
692
|
-
lines: {
|
|
693
|
-
type: "number",
|
|
694
|
-
description: "Number of log lines to search through (default: 200)",
|
|
695
|
-
default: 200,
|
|
696
|
-
},
|
|
697
|
-
filter: {
|
|
698
|
-
type: "string",
|
|
699
|
-
description: "Filter by URL pattern or method (e.g., 'api', 'POST')",
|
|
700
|
-
},
|
|
701
|
-
device: {
|
|
702
|
-
type: "string",
|
|
703
|
-
description: "Specific device ID (optional)",
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
name: "start_network_monitoring",
|
|
710
|
-
description: "[PRO] Start real-time network request monitoring. Captures all HTTP/HTTPS traffic in background.",
|
|
711
|
-
inputSchema: {
|
|
712
|
-
type: "object",
|
|
713
|
-
properties: {
|
|
714
|
-
device: {
|
|
715
|
-
type: "string",
|
|
716
|
-
description: "Specific device ID (optional)",
|
|
717
|
-
},
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
name: "stop_network_monitoring",
|
|
723
|
-
description: "[PRO] Stop network monitoring and get summary of captured requests.",
|
|
724
|
-
inputSchema: {
|
|
725
|
-
type: "object",
|
|
726
|
-
properties: {},
|
|
727
|
-
},
|
|
728
|
-
},
|
|
729
|
-
{
|
|
730
|
-
name: "get_network_stats",
|
|
731
|
-
description: "[PRO] Get device network statistics including data usage, active connections, and WiFi info.",
|
|
732
|
-
inputSchema: {
|
|
733
|
-
type: "object",
|
|
734
|
-
properties: {
|
|
735
|
-
device: {
|
|
736
|
-
type: "string",
|
|
737
|
-
description: "Specific device ID (optional)",
|
|
738
|
-
},
|
|
739
|
-
},
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
name: "analyze_request",
|
|
744
|
-
description: "[PRO] Analyze a specific network request by index from captured requests. Shows headers, body, timing.",
|
|
745
|
-
inputSchema: {
|
|
746
|
-
type: "object",
|
|
747
|
-
properties: {
|
|
748
|
-
index: {
|
|
749
|
-
type: "number",
|
|
750
|
-
description: "Request index from get_network_requests or monitoring",
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
required: ["index"],
|
|
754
|
-
},
|
|
755
|
-
},
|
|
756
|
-
// === EXPO DEVTOOLS INTEGRATION ===
|
|
757
|
-
{
|
|
758
|
-
name: "check_expo_status",
|
|
759
|
-
description: "[PRO] Check Expo dev server status. Shows bundler status, dev client connection, and tunnel URLs.",
|
|
760
|
-
inputSchema: {
|
|
761
|
-
type: "object",
|
|
762
|
-
properties: {
|
|
763
|
-
port: {
|
|
764
|
-
type: "number",
|
|
765
|
-
description: "Expo bundler port (default: 8081 for Expo SDK 49+, 19000 for older)",
|
|
766
|
-
default: 8081,
|
|
767
|
-
},
|
|
768
|
-
},
|
|
769
|
-
},
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
name: "get_expo_config",
|
|
773
|
-
description: "[PRO] Get Expo project configuration from app.json or app.config.js. Shows app name, version, plugins, and more.",
|
|
774
|
-
inputSchema: {
|
|
775
|
-
type: "object",
|
|
776
|
-
properties: {
|
|
777
|
-
projectPath: {
|
|
778
|
-
type: "string",
|
|
779
|
-
description: "Path to Expo project directory (default: current directory)",
|
|
780
|
-
},
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
},
|
|
784
|
-
{
|
|
785
|
-
name: "expo_dev_menu",
|
|
786
|
-
description: "[PRO] Open the Expo developer menu on the connected device. Equivalent to shaking the device or pressing 'd' in terminal.",
|
|
787
|
-
inputSchema: {
|
|
788
|
-
type: "object",
|
|
789
|
-
properties: {
|
|
790
|
-
device: {
|
|
791
|
-
type: "string",
|
|
792
|
-
description: "Specific device ID (optional)",
|
|
793
|
-
},
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
|
-
},
|
|
797
|
-
{
|
|
798
|
-
name: "expo_reload",
|
|
799
|
-
description: "[PRO] Trigger a reload of the Expo app. Refreshes the JavaScript bundle without a full rebuild.",
|
|
800
|
-
inputSchema: {
|
|
801
|
-
type: "object",
|
|
802
|
-
properties: {
|
|
803
|
-
device: {
|
|
804
|
-
type: "string",
|
|
805
|
-
description: "Specific device ID (optional)",
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
},
|
|
809
|
-
},
|
|
810
|
-
{
|
|
811
|
-
name: "get_eas_builds",
|
|
812
|
-
description: "[PRO] Get recent EAS (Expo Application Services) build status. Shows build history for your project.",
|
|
813
|
-
inputSchema: {
|
|
814
|
-
type: "object",
|
|
815
|
-
properties: {
|
|
816
|
-
platform: {
|
|
817
|
-
type: "string",
|
|
818
|
-
enum: ["android", "ios", "all"],
|
|
819
|
-
description: "Platform to show builds for (default: all)",
|
|
820
|
-
default: "all",
|
|
821
|
-
},
|
|
822
|
-
limit: {
|
|
823
|
-
type: "number",
|
|
824
|
-
description: "Maximum number of builds to show (default: 5)",
|
|
825
|
-
default: 5,
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
},
|
|
829
|
-
},
|
|
830
|
-
// === PERFORMANCE METRICS TOOLS ===
|
|
831
|
-
{
|
|
832
|
-
name: "get_cpu_usage",
|
|
833
|
-
description: "[PRO] Get CPU usage for device or specific app. Shows per-core and per-process CPU consumption.",
|
|
834
|
-
inputSchema: {
|
|
835
|
-
type: "object",
|
|
836
|
-
properties: {
|
|
837
|
-
packageName: {
|
|
838
|
-
type: "string",
|
|
839
|
-
description: "App package name to filter (optional, shows all if not specified)",
|
|
840
|
-
},
|
|
841
|
-
device: {
|
|
842
|
-
type: "string",
|
|
843
|
-
description: "Specific device ID (optional)",
|
|
844
|
-
},
|
|
845
|
-
},
|
|
846
|
-
},
|
|
847
|
-
},
|
|
848
|
-
{
|
|
849
|
-
name: "get_memory_usage",
|
|
850
|
-
description: "[PRO] Get memory usage for a specific app. Shows heap, native, graphics, and total memory consumption.",
|
|
851
|
-
inputSchema: {
|
|
852
|
-
type: "object",
|
|
853
|
-
properties: {
|
|
854
|
-
packageName: {
|
|
855
|
-
type: "string",
|
|
856
|
-
description: "App package name (required)",
|
|
857
|
-
},
|
|
858
|
-
device: {
|
|
859
|
-
type: "string",
|
|
860
|
-
description: "Specific device ID (optional)",
|
|
861
|
-
},
|
|
862
|
-
},
|
|
863
|
-
required: ["packageName"],
|
|
864
|
-
},
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
name: "get_fps_stats",
|
|
868
|
-
description: "[PRO] Get frame rendering statistics (FPS). Shows jank frames, slow renders, and frame timing histogram.",
|
|
869
|
-
inputSchema: {
|
|
870
|
-
type: "object",
|
|
871
|
-
properties: {
|
|
872
|
-
packageName: {
|
|
873
|
-
type: "string",
|
|
874
|
-
description: "App package name (required)",
|
|
875
|
-
},
|
|
876
|
-
device: {
|
|
877
|
-
type: "string",
|
|
878
|
-
description: "Specific device ID (optional)",
|
|
879
|
-
},
|
|
880
|
-
reset: {
|
|
881
|
-
type: "boolean",
|
|
882
|
-
description: "Reset stats before measuring (default: false)",
|
|
883
|
-
default: false,
|
|
884
|
-
},
|
|
885
|
-
},
|
|
886
|
-
required: ["packageName"],
|
|
887
|
-
},
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
name: "get_battery_stats",
|
|
891
|
-
description: "[PRO] Get battery consumption statistics. Shows power usage by app and component.",
|
|
892
|
-
inputSchema: {
|
|
893
|
-
type: "object",
|
|
894
|
-
properties: {
|
|
895
|
-
packageName: {
|
|
896
|
-
type: "string",
|
|
897
|
-
description: "App package name to filter (optional)",
|
|
898
|
-
},
|
|
899
|
-
device: {
|
|
900
|
-
type: "string",
|
|
901
|
-
description: "Specific device ID (optional)",
|
|
902
|
-
},
|
|
903
|
-
},
|
|
904
|
-
},
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
name: "get_performance_snapshot",
|
|
908
|
-
description: "[PRO] Get a comprehensive performance snapshot including CPU, memory, FPS, and battery stats for an app.",
|
|
909
|
-
inputSchema: {
|
|
910
|
-
type: "object",
|
|
911
|
-
properties: {
|
|
912
|
-
packageName: {
|
|
913
|
-
type: "string",
|
|
914
|
-
description: "App package name (required)",
|
|
915
|
-
},
|
|
916
|
-
device: {
|
|
917
|
-
type: "string",
|
|
918
|
-
description: "Specific device ID (optional)",
|
|
919
|
-
},
|
|
920
|
-
},
|
|
921
|
-
required: ["packageName"],
|
|
922
|
-
},
|
|
923
|
-
},
|
|
924
|
-
];
|
|
925
|
-
// Combine core tools with license tools
|
|
926
|
-
const tools = [...coreTools, ...licenseTools];
|
|
927
|
-
// ============================================================================
|
|
928
|
-
// TOOL IMPLEMENTATIONS
|
|
929
|
-
// ============================================================================
|
|
930
|
-
async function getMetroLogs(lines = 50, filter) {
|
|
931
|
-
// Check license/trial status
|
|
932
|
-
const check = await requireBasic("get_metro_logs");
|
|
933
|
-
if (!check.allowed)
|
|
934
|
-
return check.message;
|
|
935
|
-
const license = await checkLicense();
|
|
936
|
-
const tierLimits = TIER_LIMITS[license.tier];
|
|
937
|
-
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
938
|
-
let logs = metroLogBuffer.slice(-maxLines);
|
|
939
|
-
if (filter) {
|
|
940
|
-
logs = logs.filter((line) => line.toLowerCase().includes(filter.toLowerCase()));
|
|
941
|
-
}
|
|
942
|
-
if (logs.length === 0) {
|
|
943
|
-
try {
|
|
944
|
-
const response = await fetchMetroStatus(CONFIG.metroPort);
|
|
945
|
-
let result = `Metro is running but no logs captured yet.\nMetro status: ${response}\n\nTip: Use 'start_metro_logging' with a log file path, or pipe Metro output:\n npx expo start 2>&1 | tee metro.log`;
|
|
946
|
-
if (check.message)
|
|
947
|
-
result += `\n\n${check.message}`;
|
|
948
|
-
return result;
|
|
949
|
-
}
|
|
950
|
-
catch {
|
|
951
|
-
let result = `No Metro logs available. Metro may not be running.\n\nTo capture logs:\n1. Start Metro with output to file: npx expo start 2>&1 | tee metro.log\n2. Use start_metro_logging tool with logFile parameter`;
|
|
952
|
-
if (check.message)
|
|
953
|
-
result += `\n\n${check.message}`;
|
|
954
|
-
return result;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
const header = license.valid
|
|
958
|
-
? `📋 Metro Logs (${logs.length} lines):`
|
|
959
|
-
: `📋 Metro Logs (${logs.length} lines, trial mode):`;
|
|
960
|
-
let result = `${header}\n${"─".repeat(50)}\n${logs.join("\n")}`;
|
|
961
|
-
if (check.message)
|
|
962
|
-
result += `\n\n${check.message}`;
|
|
963
|
-
return result;
|
|
964
|
-
}
|
|
965
|
-
async function getAdbLogs(lines = 50, filter = "ReactNativeJS", level = "I") {
|
|
966
|
-
// Check license/trial status
|
|
967
|
-
const check = await requireBasic("get_adb_logs");
|
|
968
|
-
if (!check.allowed)
|
|
969
|
-
return check.message;
|
|
970
|
-
const license = await checkLicense();
|
|
971
|
-
const tierLimits = TIER_LIMITS[license.tier];
|
|
972
|
-
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
973
|
-
try {
|
|
974
|
-
await execAsync("adb version");
|
|
975
|
-
let command;
|
|
976
|
-
if (filter === "*") {
|
|
977
|
-
command = `adb logcat -d -t ${maxLines} *:${level}`;
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
command = `adb logcat -d -t ${maxLines} ${filter}:${level} *:S`;
|
|
981
|
-
}
|
|
982
|
-
const { stdout, stderr } = await execAsync(command);
|
|
983
|
-
if (stderr && !stdout) {
|
|
984
|
-
return `ADB Error: ${stderr}`;
|
|
985
|
-
}
|
|
986
|
-
let result = stdout || "No logs found matching the filter.";
|
|
987
|
-
if (check.message)
|
|
988
|
-
result += `\n\n${check.message}`;
|
|
989
|
-
return result;
|
|
990
|
-
}
|
|
991
|
-
catch (error) {
|
|
992
|
-
if (error.message.includes("not recognized") || error.message.includes("not found")) {
|
|
993
|
-
return "ADB is not installed or not in PATH. Please install Android SDK Platform Tools.";
|
|
994
|
-
}
|
|
995
|
-
if (error.message.includes("no devices")) {
|
|
996
|
-
return "No Android devices/emulators connected. Start an emulator or connect a device.";
|
|
997
|
-
}
|
|
998
|
-
return `Error getting ADB logs: ${error.message}`;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
async function screenshotEmulator(device) {
|
|
1002
|
-
// Check license/trial status
|
|
1003
|
-
const check = await requireBasic("screenshot_emulator");
|
|
1004
|
-
if (!check.allowed) {
|
|
1005
|
-
return { success: false, error: check.message };
|
|
1006
|
-
}
|
|
1007
|
-
try {
|
|
1008
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1009
|
-
const screenshotPath = path.join(CONFIG.screenshotDir, `screenshot_${Date.now()}.png`);
|
|
1010
|
-
// Capture screenshot
|
|
1011
|
-
await execAsync(`adb ${deviceFlag} exec-out screencap -p > "${screenshotPath}"`);
|
|
1012
|
-
// Read and convert to base64
|
|
1013
|
-
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
1014
|
-
const base64Data = imageBuffer.toString("base64");
|
|
1015
|
-
// Clean up temp file
|
|
1016
|
-
fs.unlinkSync(screenshotPath);
|
|
1017
|
-
// Save to history for Advanced users
|
|
1018
|
-
const license = await checkLicense();
|
|
1019
|
-
if (license.valid && license.tier === "advanced") {
|
|
1020
|
-
screenshotHistory.unshift({
|
|
1021
|
-
timestamp: new Date().toISOString(),
|
|
1022
|
-
data: base64Data,
|
|
1023
|
-
});
|
|
1024
|
-
if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
|
|
1025
|
-
screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
return {
|
|
1029
|
-
success: true,
|
|
1030
|
-
data: base64Data,
|
|
1031
|
-
mimeType: "image/png",
|
|
1032
|
-
trialMessage: check.message,
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
catch (error) {
|
|
1036
|
-
return {
|
|
1037
|
-
success: false,
|
|
1038
|
-
error: `Failed to capture screenshot: ${error.message}`,
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
async function listDevices() {
|
|
1043
|
-
// Check license/trial status
|
|
1044
|
-
const check = await requireBasic("list_devices");
|
|
1045
|
-
if (!check.allowed)
|
|
1046
|
-
return check.message;
|
|
1047
|
-
try {
|
|
1048
|
-
const { stdout } = await execAsync("adb devices -l");
|
|
1049
|
-
const lines = stdout.trim().split("\n");
|
|
1050
|
-
if (lines.length <= 1) {
|
|
1051
|
-
let result = `No devices connected.\n\nTo connect:\n- Start an Android emulator (Android Studio, Genymotion)\n- Or connect a physical device with USB debugging enabled`;
|
|
1052
|
-
if (check.message)
|
|
1053
|
-
result += `\n\n${check.message}`;
|
|
1054
|
-
return result;
|
|
1055
|
-
}
|
|
1056
|
-
let result = stdout;
|
|
1057
|
-
if (check.message)
|
|
1058
|
-
result += `\n\n${check.message}`;
|
|
1059
|
-
return result;
|
|
1060
|
-
}
|
|
1061
|
-
catch (error) {
|
|
1062
|
-
return `Error listing devices: ${error.message}`;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
async function fetchMetroStatus(port) {
|
|
1066
|
-
return new Promise((resolve, reject) => {
|
|
1067
|
-
const req = http.get(`http://localhost:${port}/status`, (res) => {
|
|
1068
|
-
let data = "";
|
|
1069
|
-
res.on("data", (chunk) => (data += chunk));
|
|
1070
|
-
res.on("end", () => resolve(data || "Metro is running"));
|
|
1071
|
-
});
|
|
1072
|
-
req.on("error", (err) => reject(err));
|
|
1073
|
-
req.setTimeout(3000, () => {
|
|
1074
|
-
req.destroy();
|
|
1075
|
-
reject(new Error("Timeout"));
|
|
1076
|
-
});
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
async function checkMetroStatus(port = 8081) {
|
|
1080
|
-
// Check license/trial status
|
|
1081
|
-
const check = await requireBasic("check_metro_status");
|
|
1082
|
-
if (!check.allowed)
|
|
1083
|
-
return check.message;
|
|
1084
|
-
try {
|
|
1085
|
-
const status = await fetchMetroStatus(port);
|
|
1086
|
-
let result = `✅ Metro is running on port ${port}\nStatus: ${status}`;
|
|
1087
|
-
if (check.message)
|
|
1088
|
-
result += `\n\n${check.message}`;
|
|
1089
|
-
return result;
|
|
1090
|
-
}
|
|
1091
|
-
catch {
|
|
1092
|
-
let result = `❌ Metro does not appear to be running on port ${port}.\n\nTo start Metro:\n npx expo start\n # or\n npx react-native start`;
|
|
1093
|
-
if (check.message)
|
|
1094
|
-
result += `\n\n${check.message}`;
|
|
1095
|
-
return result;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
async function getAppInfo(packageName) {
|
|
1099
|
-
// Check license/trial status
|
|
1100
|
-
const check = await requireBasic("get_app_info");
|
|
1101
|
-
if (!check.allowed)
|
|
1102
|
-
return check.message;
|
|
1103
|
-
try {
|
|
1104
|
-
const { stdout } = await execAsync(`adb shell dumpsys package ${packageName}`);
|
|
1105
|
-
const lines = stdout.split("\n");
|
|
1106
|
-
const relevantInfo = [];
|
|
1107
|
-
for (const line of lines) {
|
|
1108
|
-
if (line.includes("versionName") ||
|
|
1109
|
-
line.includes("versionCode") ||
|
|
1110
|
-
line.includes("targetSdk") ||
|
|
1111
|
-
line.includes("dataDir") ||
|
|
1112
|
-
line.includes("firstInstallTime") ||
|
|
1113
|
-
line.includes("lastUpdateTime")) {
|
|
1114
|
-
relevantInfo.push(line.trim());
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
if (relevantInfo.length === 0) {
|
|
1118
|
-
let result = `Package ${packageName} not found on device.`;
|
|
1119
|
-
if (check.message)
|
|
1120
|
-
result += `\n\n${check.message}`;
|
|
1121
|
-
return result;
|
|
1122
|
-
}
|
|
1123
|
-
let result = `📱 App Info for ${packageName}:\n${relevantInfo.join("\n")}`;
|
|
1124
|
-
if (check.message)
|
|
1125
|
-
result += `\n\n${check.message}`;
|
|
1126
|
-
return result;
|
|
1127
|
-
}
|
|
1128
|
-
catch (error) {
|
|
1129
|
-
return `Error getting app info: ${error.message}`;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
async function clearAppData(packageName) {
|
|
1133
|
-
// Check license/trial status
|
|
1134
|
-
const check = await requireBasic("clear_app_data");
|
|
1135
|
-
if (!check.allowed)
|
|
1136
|
-
return check.message;
|
|
1137
|
-
try {
|
|
1138
|
-
await execAsync(`adb shell pm clear ${packageName}`);
|
|
1139
|
-
let result = `✅ Successfully cleared data for ${packageName}`;
|
|
1140
|
-
if (check.message)
|
|
1141
|
-
result += `\n\n${check.message}`;
|
|
1142
|
-
return result;
|
|
1143
|
-
}
|
|
1144
|
-
catch (error) {
|
|
1145
|
-
return `Error clearing app data: ${error.message}`;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
async function restartAdb() {
|
|
1149
|
-
// Check license/trial status
|
|
1150
|
-
const check = await requireBasic("restart_adb");
|
|
1151
|
-
if (!check.allowed)
|
|
1152
|
-
return check.message;
|
|
1153
|
-
try {
|
|
1154
|
-
await execAsync("adb kill-server");
|
|
1155
|
-
await execAsync("adb start-server");
|
|
1156
|
-
const { stdout } = await execAsync("adb devices");
|
|
1157
|
-
let result = `✅ ADB server restarted successfully.\n\nConnected devices:\n${stdout}`;
|
|
1158
|
-
if (check.message)
|
|
1159
|
-
result += `\n\n${check.message}`;
|
|
1160
|
-
return result;
|
|
1161
|
-
}
|
|
1162
|
-
catch (error) {
|
|
1163
|
-
return `Error restarting ADB: ${error.message}`;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
async function getDeviceInfo(device) {
|
|
1167
|
-
// Check license/trial status
|
|
1168
|
-
const check = await requireBasic("get_device_info");
|
|
1169
|
-
if (!check.allowed)
|
|
1170
|
-
return check.message;
|
|
1171
|
-
try {
|
|
1172
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1173
|
-
const commands = [
|
|
1174
|
-
`adb ${deviceFlag} shell getprop ro.build.version.release`,
|
|
1175
|
-
`adb ${deviceFlag} shell getprop ro.build.version.sdk`,
|
|
1176
|
-
`adb ${deviceFlag} shell getprop ro.product.model`,
|
|
1177
|
-
`adb ${deviceFlag} shell getprop ro.product.manufacturer`,
|
|
1178
|
-
`adb ${deviceFlag} shell wm size`,
|
|
1179
|
-
`adb ${deviceFlag} shell wm density`,
|
|
1180
|
-
];
|
|
1181
|
-
const results = await Promise.all(commands.map((cmd) => execAsync(cmd)
|
|
1182
|
-
.then(({ stdout }) => stdout.trim())
|
|
1183
|
-
.catch(() => "N/A")));
|
|
1184
|
-
let result = `📱 Device Information:
|
|
1185
|
-
─────────────────────────────
|
|
1186
|
-
Android Version: ${results[0]}
|
|
1187
|
-
SDK Level: ${results[1]}
|
|
1188
|
-
Model: ${results[3]} ${results[2]}
|
|
1189
|
-
Screen Size: ${results[4].replace("Physical size: ", "")}
|
|
1190
|
-
Screen Density: ${results[5].replace("Physical density: ", "")} dpi`;
|
|
1191
|
-
if (check.message)
|
|
1192
|
-
result += `\n\n${check.message}`;
|
|
1193
|
-
return result;
|
|
1194
|
-
}
|
|
1195
|
-
catch (error) {
|
|
1196
|
-
return `Error getting device info: ${error.message}`;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// ============================================================================
|
|
1200
|
-
// FIXED: Metro Logging Implementation
|
|
1201
|
-
// ============================================================================
|
|
1202
|
-
async function startMetroLogging(logFile) {
|
|
1203
|
-
// Check license/trial status
|
|
1204
|
-
const check = await requireBasic("start_metro_logging");
|
|
1205
|
-
if (!check.allowed)
|
|
1206
|
-
return check.message;
|
|
1207
|
-
if (metroProcess) {
|
|
1208
|
-
let result = "Metro logging is already running. Use 'stop_metro_logging' first.";
|
|
1209
|
-
if (check.message)
|
|
1210
|
-
result += `\n\n${check.message}`;
|
|
1211
|
-
return result;
|
|
1212
|
-
}
|
|
1213
|
-
metroLogBuffer = [];
|
|
1214
|
-
// If a log file is provided, tail it
|
|
1215
|
-
if (logFile) {
|
|
1216
|
-
if (!fs.existsSync(logFile)) {
|
|
1217
|
-
let result = `Log file not found: ${logFile}\n\nCreate it by running:\n npx expo start 2>&1 | tee ${logFile}`;
|
|
1218
|
-
if (check.message)
|
|
1219
|
-
result += `\n\n${check.message}`;
|
|
1220
|
-
return result;
|
|
1221
|
-
}
|
|
1222
|
-
// Use PowerShell's Get-Content -Wait on Windows, tail -f on Unix
|
|
1223
|
-
const isWindows = process.platform === "win32";
|
|
1224
|
-
if (isWindows) {
|
|
1225
|
-
metroProcess = spawn("powershell", [
|
|
1226
|
-
"-Command",
|
|
1227
|
-
`Get-Content -Path "${logFile}" -Wait -Tail 100`,
|
|
1228
|
-
]);
|
|
1229
|
-
}
|
|
1230
|
-
else {
|
|
1231
|
-
metroProcess = spawn("tail", ["-f", "-n", "100", logFile]);
|
|
1232
|
-
}
|
|
1233
|
-
metroProcess.stdout?.on("data", (data) => {
|
|
1234
|
-
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
1235
|
-
metroLogBuffer.push(...lines);
|
|
1236
|
-
if (metroLogBuffer.length > CONFIG.logBufferSize) {
|
|
1237
|
-
metroLogBuffer = metroLogBuffer.slice(-CONFIG.logBufferSize);
|
|
1238
|
-
}
|
|
1239
|
-
});
|
|
1240
|
-
metroProcess.stderr?.on("data", (data) => {
|
|
1241
|
-
metroLogBuffer.push(`[STDERR] ${data.toString().trim()}`);
|
|
1242
|
-
});
|
|
1243
|
-
metroProcess.on("error", (err) => {
|
|
1244
|
-
metroLogBuffer.push(`[ERROR] ${err.message}`);
|
|
1245
|
-
});
|
|
1246
|
-
let result = `✅ Metro log capture started!\nWatching: ${logFile}\n\nUse 'get_metro_logs' to retrieve captured logs.`;
|
|
1247
|
-
if (check.message)
|
|
1248
|
-
result += `\n\n${check.message}`;
|
|
1249
|
-
return result;
|
|
1250
|
-
}
|
|
1251
|
-
// No log file provided - give instructions
|
|
1252
|
-
let result = `📋 Metro Log Capture Setup
|
|
1253
|
-
─────────────────────────────
|
|
1254
|
-
|
|
1255
|
-
To capture Metro logs, you have two options:
|
|
1256
|
-
|
|
1257
|
-
Option 1: Pipe Metro to a file (Recommended)
|
|
1258
|
-
npx expo start 2>&1 | tee metro.log
|
|
1259
|
-
|
|
1260
|
-
Then run: start_metro_logging with logFile="metro.log"
|
|
1261
|
-
|
|
1262
|
-
Option 2: Check common log locations
|
|
1263
|
-
- Expo: .expo/logs/
|
|
1264
|
-
- React Native: Check Metro terminal output
|
|
1265
|
-
|
|
1266
|
-
Option 3: Use ADB logs instead
|
|
1267
|
-
For device-side JavaScript logs, use 'get_adb_logs'
|
|
1268
|
-
|
|
1269
|
-
─────────────────────────────
|
|
1270
|
-
Once you have a log file, call this tool again with the logFile parameter.`;
|
|
1271
|
-
if (check.message)
|
|
1272
|
-
result += `\n\n${check.message}`;
|
|
1273
|
-
return result;
|
|
1274
|
-
}
|
|
1275
|
-
async function stopMetroLogging() {
|
|
1276
|
-
// Check license/trial status
|
|
1277
|
-
const check = await requireBasic("stop_metro_logging");
|
|
1278
|
-
if (!check.allowed)
|
|
1279
|
-
return check.message;
|
|
1280
|
-
if (metroProcess) {
|
|
1281
|
-
metroProcess.kill();
|
|
1282
|
-
metroProcess = null;
|
|
1283
|
-
}
|
|
1284
|
-
const logCount = metroLogBuffer.length;
|
|
1285
|
-
let result = `✅ Metro logging stopped. ${logCount} log lines were captured.`;
|
|
1286
|
-
if (check.message)
|
|
1287
|
-
result += `\n\n${check.message}`;
|
|
1288
|
-
return result;
|
|
1289
|
-
}
|
|
1290
|
-
// ============================================================================
|
|
1291
|
-
// PRO FEATURE IMPLEMENTATIONS
|
|
1292
|
-
// ============================================================================
|
|
1293
|
-
async function streamAdbRealtime(filter = "ReactNativeJS") {
|
|
1294
|
-
const check = await requireAdvanced("stream_adb_realtime");
|
|
1295
|
-
if (!check.allowed)
|
|
1296
|
-
return check.message;
|
|
1297
|
-
if (adbLogProcess) {
|
|
1298
|
-
return "ADB streaming is already running. Use 'stop_adb_streaming' first.";
|
|
1299
|
-
}
|
|
1300
|
-
adbLogBuffer = [];
|
|
1301
|
-
adbLogProcess = spawn("adb", ["logcat", `${filter}:V`, "*:S"]);
|
|
1302
|
-
adbLogProcess.stdout?.on("data", (data) => {
|
|
1303
|
-
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
1304
|
-
adbLogBuffer.push(...lines);
|
|
1305
|
-
if (adbLogBuffer.length > 500) {
|
|
1306
|
-
adbLogBuffer = adbLogBuffer.slice(-500);
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
adbLogProcess.on("error", (err) => {
|
|
1310
|
-
adbLogBuffer.push(`[ERROR] ${err.message}`);
|
|
1311
|
-
});
|
|
1312
|
-
return `✅ [PRO] Real-time ADB streaming started!\nFilter: ${filter}\n\nUse 'get_adb_logs' to retrieve the live buffer.`;
|
|
1313
|
-
}
|
|
1314
|
-
function stopAdbStreaming() {
|
|
1315
|
-
if (adbLogProcess) {
|
|
1316
|
-
adbLogProcess.kill();
|
|
1317
|
-
adbLogProcess = null;
|
|
1318
|
-
}
|
|
1319
|
-
return `✅ ADB streaming stopped. Buffer contained ${adbLogBuffer.length} lines.`;
|
|
1320
|
-
}
|
|
1321
|
-
async function getScreenshotHistory(count = 5) {
|
|
1322
|
-
const check = await requireAdvanced("screenshot_history");
|
|
1323
|
-
if (!check.allowed)
|
|
1324
|
-
return check.message;
|
|
1325
|
-
if (screenshotHistory.length === 0) {
|
|
1326
|
-
return "No screenshots in history. Take screenshots using 'screenshot_emulator' first.";
|
|
1327
|
-
}
|
|
1328
|
-
const recent = screenshotHistory.slice(0, count);
|
|
1329
|
-
return `📸 [PRO] Screenshot History (${recent.length} of ${screenshotHistory.length}):\n\n${recent
|
|
1330
|
-
.map((s, i) => `${i + 1}. ${s.timestamp}`)
|
|
1331
|
-
.join("\n")}\n\nNote: Full image data available in tool response.`;
|
|
1332
|
-
}
|
|
1333
|
-
async function watchForErrors(patterns = ["Error", "Exception"], timeout = 60) {
|
|
1334
|
-
const check = await requireAdvanced("watch_for_errors");
|
|
1335
|
-
if (!check.allowed)
|
|
1336
|
-
return check.message;
|
|
1337
|
-
return new Promise((resolve) => {
|
|
1338
|
-
const startTime = Date.now();
|
|
1339
|
-
const checkInterval = setInterval(async () => {
|
|
1340
|
-
// Check ADB logs for patterns
|
|
1341
|
-
try {
|
|
1342
|
-
const { stdout } = await execAsync("adb logcat -d -t 50 *:E");
|
|
1343
|
-
for (const pattern of patterns) {
|
|
1344
|
-
if (stdout.toLowerCase().includes(pattern.toLowerCase())) {
|
|
1345
|
-
clearInterval(checkInterval);
|
|
1346
|
-
resolve(`🚨 [PRO] Error detected!\nPattern: "${pattern}"\n\nRelevant logs:\n${stdout}`);
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
catch { }
|
|
1352
|
-
// Check timeout
|
|
1353
|
-
if (Date.now() - startTime > timeout * 1000) {
|
|
1354
|
-
clearInterval(checkInterval);
|
|
1355
|
-
resolve(`✅ [PRO] No errors detected in ${timeout} seconds.`);
|
|
1356
|
-
}
|
|
1357
|
-
}, 2000);
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
async function multiDeviceLogs(devices, lines = 30) {
|
|
1361
|
-
const check = await requireAdvanced("multi_device_logs");
|
|
1362
|
-
if (!check.allowed)
|
|
1363
|
-
return check.message;
|
|
1364
|
-
if (!devices || devices.length === 0) {
|
|
1365
|
-
const { stdout } = await execAsync("adb devices");
|
|
1366
|
-
return `No devices specified. Available devices:\n${stdout}`;
|
|
1367
|
-
}
|
|
1368
|
-
const results = await Promise.all(devices.map(async (device) => {
|
|
1369
|
-
try {
|
|
1370
|
-
const { stdout } = await execAsync(`adb -s ${device} logcat -d -t ${lines} ReactNativeJS:V *:S`);
|
|
1371
|
-
return `📱 Device: ${device}\n${"─".repeat(30)}\n${stdout}`;
|
|
1372
|
-
}
|
|
1373
|
-
catch (error) {
|
|
1374
|
-
return `📱 Device: ${device}\n${"─".repeat(30)}\nError: ${error.message}`;
|
|
1375
|
-
}
|
|
1376
|
-
}));
|
|
1377
|
-
return `📋 [PRO] Multi-Device Logs\n${"═".repeat(50)}\n\n${results.join("\n\n")}`;
|
|
1378
|
-
}
|
|
1379
|
-
// ============================================================================
|
|
1380
|
-
// INTERACTION TOOLS (Advanced)
|
|
1381
|
-
// ============================================================================
|
|
1382
|
-
async function tapScreen(x, y, device) {
|
|
1383
|
-
const check = await requireAdvanced("tap_screen");
|
|
1384
|
-
if (!check.allowed)
|
|
1385
|
-
return check.message;
|
|
1386
|
-
try {
|
|
1387
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1388
|
-
await execAsync(`adb ${deviceFlag} shell input tap ${x} ${y}`);
|
|
1389
|
-
let result = `✅ Tapped at (${x}, ${y})`;
|
|
1390
|
-
if (check.message)
|
|
1391
|
-
result += `\n\n${check.message}`;
|
|
1392
|
-
return result;
|
|
1393
|
-
}
|
|
1394
|
-
catch (error) {
|
|
1395
|
-
return `Error tapping screen: ${error.message}`;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
async function inputText(text, device) {
|
|
1399
|
-
const check = await requireAdvanced("input_text");
|
|
1400
|
-
if (!check.allowed)
|
|
1401
|
-
return check.message;
|
|
1402
|
-
try {
|
|
1403
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1404
|
-
// Escape special characters for shell
|
|
1405
|
-
const escapedText = text.replace(/([\\'"$ `])/g, "\\$1").replace(/ /g, "%s");
|
|
1406
|
-
await execAsync(`adb ${deviceFlag} shell input text "${escapedText}"`);
|
|
1407
|
-
let result = `✅ Typed: "${text}"`;
|
|
1408
|
-
if (check.message)
|
|
1409
|
-
result += `\n\n${check.message}`;
|
|
1410
|
-
return result;
|
|
1411
|
-
}
|
|
1412
|
-
catch (error) {
|
|
1413
|
-
return `Error inputting text: ${error.message}`;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
async function pressButton(button, device) {
|
|
1417
|
-
const check = await requireAdvanced("press_button");
|
|
1418
|
-
if (!check.allowed)
|
|
1419
|
-
return check.message;
|
|
1420
|
-
const keyMap = {
|
|
1421
|
-
back: 4,
|
|
1422
|
-
home: 3,
|
|
1423
|
-
recent: 187,
|
|
1424
|
-
volume_up: 24,
|
|
1425
|
-
volume_down: 25,
|
|
1426
|
-
power: 26,
|
|
1427
|
-
enter: 66,
|
|
1428
|
-
};
|
|
1429
|
-
const keyCode = keyMap[button];
|
|
1430
|
-
if (!keyCode) {
|
|
1431
|
-
return `Unknown button: ${button}. Available: ${Object.keys(keyMap).join(", ")}`;
|
|
1432
|
-
}
|
|
1433
|
-
try {
|
|
1434
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1435
|
-
await execAsync(`adb ${deviceFlag} shell input keyevent ${keyCode}`);
|
|
1436
|
-
let result = `✅ Pressed: ${button.toUpperCase()}`;
|
|
1437
|
-
if (check.message)
|
|
1438
|
-
result += `\n\n${check.message}`;
|
|
1439
|
-
return result;
|
|
1440
|
-
}
|
|
1441
|
-
catch (error) {
|
|
1442
|
-
return `Error pressing button: ${error.message}`;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
async function swipeScreen(startX, startY, endX, endY, duration = 300, device) {
|
|
1446
|
-
const check = await requireAdvanced("swipe_screen");
|
|
1447
|
-
if (!check.allowed)
|
|
1448
|
-
return check.message;
|
|
1449
|
-
try {
|
|
1450
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1451
|
-
await execAsync(`adb ${deviceFlag} shell input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`);
|
|
1452
|
-
let result = `✅ Swiped from (${startX}, ${startY}) to (${endX}, ${endY})`;
|
|
1453
|
-
if (check.message)
|
|
1454
|
-
result += `\n\n${check.message}`;
|
|
1455
|
-
return result;
|
|
1456
|
-
}
|
|
1457
|
-
catch (error) {
|
|
1458
|
-
return `Error swiping: ${error.message}`;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
async function launchApp(packageName, device) {
|
|
1462
|
-
const check = await requireAdvanced("launch_app");
|
|
1463
|
-
if (!check.allowed)
|
|
1464
|
-
return check.message;
|
|
1465
|
-
try {
|
|
1466
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1467
|
-
// Get the main activity using monkey
|
|
1468
|
-
await execAsync(`adb ${deviceFlag} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`);
|
|
1469
|
-
let result = `✅ Launched: ${packageName}`;
|
|
1470
|
-
if (check.message)
|
|
1471
|
-
result += `\n\n${check.message}`;
|
|
1472
|
-
return result;
|
|
1473
|
-
}
|
|
1474
|
-
catch (error) {
|
|
1475
|
-
return `Error launching app: ${error.message}\n\nMake sure the package name is correct.`;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
async function installApk(apkPath, device) {
|
|
1479
|
-
const check = await requireAdvanced("install_apk");
|
|
1480
|
-
if (!check.allowed)
|
|
1481
|
-
return check.message;
|
|
1482
|
-
if (!fs.existsSync(apkPath)) {
|
|
1483
|
-
return `APK file not found: ${apkPath}`;
|
|
1484
|
-
}
|
|
1485
|
-
try {
|
|
1486
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1487
|
-
const { stdout } = await execAsync(`adb ${deviceFlag} install -r "${apkPath}"`);
|
|
1488
|
-
let result = `✅ APK installed successfully!\n\n${stdout}`;
|
|
1489
|
-
if (check.message)
|
|
1490
|
-
result += `\n\n${check.message}`;
|
|
1491
|
-
return result;
|
|
1492
|
-
}
|
|
1493
|
-
catch (error) {
|
|
1494
|
-
return `Error installing APK: ${error.message}`;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
async function checkXcodeInstalled() {
|
|
1498
|
-
try {
|
|
1499
|
-
await execAsync("xcrun simctl help");
|
|
1500
|
-
return true;
|
|
1501
|
-
}
|
|
1502
|
-
catch {
|
|
1503
|
-
return false;
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
async function listIosSimulators(onlyBooted = false) {
|
|
1507
|
-
const check = await requireBasic("list_ios_simulators");
|
|
1508
|
-
if (!check.allowed)
|
|
1509
|
-
return check.message;
|
|
1510
|
-
if (process.platform !== "darwin") {
|
|
1511
|
-
return "iOS Simulators are only available on macOS.";
|
|
1512
|
-
}
|
|
1513
|
-
try {
|
|
1514
|
-
if (!(await checkXcodeInstalled())) {
|
|
1515
|
-
return "Xcode Command Line Tools not installed. Run: xcode-select --install";
|
|
1516
|
-
}
|
|
1517
|
-
const { stdout } = await execAsync("xcrun simctl list devices -j");
|
|
1518
|
-
const data = JSON.parse(stdout);
|
|
1519
|
-
const results = [];
|
|
1520
|
-
results.push("📱 iOS Simulators\n" + "═".repeat(50));
|
|
1521
|
-
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
1522
|
-
const deviceList = devices;
|
|
1523
|
-
const filteredDevices = onlyBooted
|
|
1524
|
-
? deviceList.filter((d) => d.state === "Booted")
|
|
1525
|
-
: deviceList.filter((d) => d.isAvailable);
|
|
1526
|
-
if (filteredDevices.length > 0) {
|
|
1527
|
-
// Extract iOS version from runtime identifier
|
|
1528
|
-
const runtimeName = runtime.split(".").pop()?.replace(/-/g, " ") || runtime;
|
|
1529
|
-
results.push(`\n${runtimeName}:`);
|
|
1530
|
-
for (const device of filteredDevices) {
|
|
1531
|
-
const status = device.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown";
|
|
1532
|
-
results.push(` ${status} ${device.name}`);
|
|
1533
|
-
results.push(` UDID: ${device.udid}`);
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
if (results.length === 1) {
|
|
1538
|
-
let result = onlyBooted
|
|
1539
|
-
? "No booted simulators found. Boot one with 'boot_ios_simulator'."
|
|
1540
|
-
: "No iOS Simulators available. Open Xcode to download simulator runtimes.";
|
|
1541
|
-
if (check.message)
|
|
1542
|
-
result += `\n\n${check.message}`;
|
|
1543
|
-
return result;
|
|
1544
|
-
}
|
|
1545
|
-
let result = results.join("\n");
|
|
1546
|
-
if (check.message)
|
|
1547
|
-
result += `\n\n${check.message}`;
|
|
1548
|
-
return result;
|
|
1549
|
-
}
|
|
1550
|
-
catch (error) {
|
|
1551
|
-
return `Error listing iOS Simulators: ${error.message}`;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
async function screenshotIosSimulator(udid) {
|
|
1555
|
-
const check = await requireBasic("screenshot_ios_simulator");
|
|
1556
|
-
if (!check.allowed) {
|
|
1557
|
-
return { success: false, error: check.message };
|
|
1558
|
-
}
|
|
1559
|
-
if (process.platform !== "darwin") {
|
|
1560
|
-
return { success: false, error: "iOS Simulators are only available on macOS." };
|
|
1561
|
-
}
|
|
1562
|
-
try {
|
|
1563
|
-
const target = udid || "booted";
|
|
1564
|
-
const screenshotPath = path.join(CONFIG.screenshotDir, `ios_screenshot_${Date.now()}.png`);
|
|
1565
|
-
await execAsync(`xcrun simctl io ${target} screenshot "${screenshotPath}"`);
|
|
1566
|
-
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
1567
|
-
const base64Data = imageBuffer.toString("base64");
|
|
1568
|
-
fs.unlinkSync(screenshotPath);
|
|
1569
|
-
// Save to history for Advanced users
|
|
1570
|
-
const license = await checkLicense();
|
|
1571
|
-
if (license.valid && license.tier === "advanced") {
|
|
1572
|
-
screenshotHistory.unshift({
|
|
1573
|
-
timestamp: new Date().toISOString(),
|
|
1574
|
-
data: base64Data,
|
|
1575
|
-
});
|
|
1576
|
-
if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
|
|
1577
|
-
screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
return {
|
|
1581
|
-
success: true,
|
|
1582
|
-
data: base64Data,
|
|
1583
|
-
mimeType: "image/png",
|
|
1584
|
-
trialMessage: check.message,
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
catch (error) {
|
|
1588
|
-
if (error.message.includes("No devices are booted")) {
|
|
1589
|
-
return { success: false, error: "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'." };
|
|
1590
|
-
}
|
|
1591
|
-
return { success: false, error: `Failed to capture iOS screenshot: ${error.message}` };
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
async function getIosSimulatorLogs(udid, filter, lines = 50) {
|
|
1595
|
-
const check = await requireBasic("get_ios_simulator_logs");
|
|
1596
|
-
if (!check.allowed)
|
|
1597
|
-
return check.message;
|
|
1598
|
-
if (process.platform !== "darwin") {
|
|
1599
|
-
return "iOS Simulators are only available on macOS.";
|
|
1600
|
-
}
|
|
1601
|
-
const license = await checkLicense();
|
|
1602
|
-
const tierLimits = TIER_LIMITS[license.tier];
|
|
1603
|
-
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
1604
|
-
try {
|
|
1605
|
-
const target = udid || "booted";
|
|
1606
|
-
// Use predicate filter if provided
|
|
1607
|
-
let command = `xcrun simctl spawn ${target} log show --last 5m --style compact`;
|
|
1608
|
-
if (filter) {
|
|
1609
|
-
command += ` --predicate 'eventMessage CONTAINS "${filter}" OR subsystem CONTAINS "${filter}"'`;
|
|
1610
|
-
}
|
|
1611
|
-
const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 });
|
|
1612
|
-
if (stderr && !stdout) {
|
|
1613
|
-
return `Error: ${stderr}`;
|
|
1614
|
-
}
|
|
1615
|
-
const logLines = stdout.split("\n").slice(-maxLines);
|
|
1616
|
-
let result = `📋 iOS Simulator Logs (${logLines.length} lines):\n${"─".repeat(50)}\n${logLines.join("\n")}`;
|
|
1617
|
-
if (check.message)
|
|
1618
|
-
result += `\n\n${check.message}`;
|
|
1619
|
-
return result;
|
|
1620
|
-
}
|
|
1621
|
-
catch (error) {
|
|
1622
|
-
if (error.message.includes("No devices are booted")) {
|
|
1623
|
-
return "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'.";
|
|
1624
|
-
}
|
|
1625
|
-
return `Error getting iOS logs: ${error.message}`;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
async function getIosSimulatorInfo(udid) {
|
|
1629
|
-
const check = await requireBasic("get_ios_simulator_info");
|
|
1630
|
-
if (!check.allowed)
|
|
1631
|
-
return check.message;
|
|
1632
|
-
if (process.platform !== "darwin") {
|
|
1633
|
-
return "iOS Simulators are only available on macOS.";
|
|
1634
|
-
}
|
|
1635
|
-
try {
|
|
1636
|
-
const { stdout } = await execAsync("xcrun simctl list devices -j");
|
|
1637
|
-
const data = JSON.parse(stdout);
|
|
1638
|
-
// Find the target device
|
|
1639
|
-
let targetDevice = null;
|
|
1640
|
-
let targetRuntime = "";
|
|
1641
|
-
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
1642
|
-
const deviceList = devices;
|
|
1643
|
-
const found = deviceList.find((d) => udid ? d.udid === udid : d.state === "Booted");
|
|
1644
|
-
if (found) {
|
|
1645
|
-
targetDevice = found;
|
|
1646
|
-
targetRuntime = runtime;
|
|
1647
|
-
break;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (!targetDevice) {
|
|
1651
|
-
return udid
|
|
1652
|
-
? `Simulator with UDID ${udid} not found.`
|
|
1653
|
-
: "No booted simulator found. Boot one first or specify a UDID.";
|
|
1654
|
-
}
|
|
1655
|
-
const runtimeName = targetRuntime.split(".").pop()?.replace(/-/g, " ") || targetRuntime;
|
|
1656
|
-
let result = `📱 iOS Simulator Info
|
|
1657
|
-
${"─".repeat(40)}
|
|
1658
|
-
Name: ${targetDevice.name}
|
|
1659
|
-
UDID: ${targetDevice.udid}
|
|
1660
|
-
State: ${targetDevice.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown"}
|
|
1661
|
-
Runtime: ${runtimeName}
|
|
1662
|
-
Available: ${targetDevice.isAvailable ? "Yes" : "No"}`;
|
|
1663
|
-
if (check.message)
|
|
1664
|
-
result += `\n\n${check.message}`;
|
|
1665
|
-
return result;
|
|
1666
|
-
}
|
|
1667
|
-
catch (error) {
|
|
1668
|
-
return `Error getting simulator info: ${error.message}`;
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
async function bootIosSimulator(udid) {
|
|
1672
|
-
const check = await requireAdvanced("boot_ios_simulator");
|
|
1673
|
-
if (!check.allowed)
|
|
1674
|
-
return check.message;
|
|
1675
|
-
if (process.platform !== "darwin") {
|
|
1676
|
-
return "iOS Simulators are only available on macOS.";
|
|
1677
|
-
}
|
|
1678
|
-
try {
|
|
1679
|
-
await execAsync(`xcrun simctl boot "${udid}"`);
|
|
1680
|
-
let result = `✅ iOS Simulator booted: ${udid}\n\nOpening Simulator app...`;
|
|
1681
|
-
// Open Simulator app to show the booted device
|
|
1682
|
-
try {
|
|
1683
|
-
await execAsync("open -a Simulator");
|
|
1684
|
-
}
|
|
1685
|
-
catch { }
|
|
1686
|
-
if (check.message)
|
|
1687
|
-
result += `\n\n${check.message}`;
|
|
1688
|
-
return result;
|
|
1689
|
-
}
|
|
1690
|
-
catch (error) {
|
|
1691
|
-
if (error.message.includes("Unable to boot device in current state: Booted")) {
|
|
1692
|
-
return "Simulator is already booted.";
|
|
1693
|
-
}
|
|
1694
|
-
return `Error booting simulator: ${error.message}`;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
async function shutdownIosSimulator(udid) {
|
|
1698
|
-
const check = await requireAdvanced("shutdown_ios_simulator");
|
|
1699
|
-
if (!check.allowed)
|
|
1700
|
-
return check.message;
|
|
1701
|
-
if (process.platform !== "darwin") {
|
|
1702
|
-
return "iOS Simulators are only available on macOS.";
|
|
1703
|
-
}
|
|
1704
|
-
try {
|
|
1705
|
-
if (udid.toLowerCase() === "all") {
|
|
1706
|
-
await execAsync("xcrun simctl shutdown all");
|
|
1707
|
-
let result = "✅ All iOS Simulators have been shut down.";
|
|
1708
|
-
if (check.message)
|
|
1709
|
-
result += `\n\n${check.message}`;
|
|
1710
|
-
return result;
|
|
1711
|
-
}
|
|
1712
|
-
await execAsync(`xcrun simctl shutdown "${udid}"`);
|
|
1713
|
-
let result = `✅ iOS Simulator shut down: ${udid}`;
|
|
1714
|
-
if (check.message)
|
|
1715
|
-
result += `\n\n${check.message}`;
|
|
1716
|
-
return result;
|
|
1717
|
-
}
|
|
1718
|
-
catch (error) {
|
|
1719
|
-
if (error.message.includes("Unable to shutdown device in current state: Shutdown")) {
|
|
1720
|
-
return "Simulator is already shut down.";
|
|
1721
|
-
}
|
|
1722
|
-
return `Error shutting down simulator: ${error.message}`;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
async function installIosApp(appPath, udid) {
|
|
1726
|
-
const check = await requireAdvanced("install_ios_app");
|
|
1727
|
-
if (!check.allowed)
|
|
1728
|
-
return check.message;
|
|
1729
|
-
if (process.platform !== "darwin") {
|
|
1730
|
-
return "iOS Simulators are only available on macOS.";
|
|
1731
|
-
}
|
|
1732
|
-
if (!fs.existsSync(appPath)) {
|
|
1733
|
-
return `App not found: ${appPath}`;
|
|
1734
|
-
}
|
|
1735
|
-
try {
|
|
1736
|
-
const target = udid || "booted";
|
|
1737
|
-
await execAsync(`xcrun simctl install ${target} "${appPath}"`);
|
|
1738
|
-
let result = `✅ App installed successfully on iOS Simulator!`;
|
|
1739
|
-
if (check.message)
|
|
1740
|
-
result += `\n\n${check.message}`;
|
|
1741
|
-
return result;
|
|
1742
|
-
}
|
|
1743
|
-
catch (error) {
|
|
1744
|
-
return `Error installing app: ${error.message}`;
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
async function launchIosApp(bundleId, udid) {
|
|
1748
|
-
const check = await requireAdvanced("launch_ios_app");
|
|
1749
|
-
if (!check.allowed)
|
|
1750
|
-
return check.message;
|
|
1751
|
-
if (process.platform !== "darwin") {
|
|
1752
|
-
return "iOS Simulators are only available on macOS.";
|
|
1753
|
-
}
|
|
1754
|
-
try {
|
|
1755
|
-
const target = udid || "booted";
|
|
1756
|
-
await execAsync(`xcrun simctl launch ${target} "${bundleId}"`);
|
|
1757
|
-
let result = `✅ Launched: ${bundleId}`;
|
|
1758
|
-
if (check.message)
|
|
1759
|
-
result += `\n\n${check.message}`;
|
|
1760
|
-
return result;
|
|
1761
|
-
}
|
|
1762
|
-
catch (error) {
|
|
1763
|
-
return `Error launching app: ${error.message}\n\nMake sure the bundle ID is correct and the app is installed.`;
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
async function terminateIosApp(bundleId, udid) {
|
|
1767
|
-
const check = await requireAdvanced("terminate_ios_app");
|
|
1768
|
-
if (!check.allowed)
|
|
1769
|
-
return check.message;
|
|
1770
|
-
if (process.platform !== "darwin") {
|
|
1771
|
-
return "iOS Simulators are only available on macOS.";
|
|
1772
|
-
}
|
|
1773
|
-
try {
|
|
1774
|
-
const target = udid || "booted";
|
|
1775
|
-
await execAsync(`xcrun simctl terminate ${target} "${bundleId}"`);
|
|
1776
|
-
let result = `✅ Terminated: ${bundleId}`;
|
|
1777
|
-
if (check.message)
|
|
1778
|
-
result += `\n\n${check.message}`;
|
|
1779
|
-
return result;
|
|
1780
|
-
}
|
|
1781
|
-
catch (error) {
|
|
1782
|
-
return `Error terminating app: ${error.message}`;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
async function iosOpenUrl(url, udid) {
|
|
1786
|
-
const check = await requireAdvanced("ios_open_url");
|
|
1787
|
-
if (!check.allowed)
|
|
1788
|
-
return check.message;
|
|
1789
|
-
if (process.platform !== "darwin") {
|
|
1790
|
-
return "iOS Simulators are only available on macOS.";
|
|
1791
|
-
}
|
|
1792
|
-
try {
|
|
1793
|
-
const target = udid || "booted";
|
|
1794
|
-
await execAsync(`xcrun simctl openurl ${target} "${url}"`);
|
|
1795
|
-
let result = `✅ Opened URL: ${url}`;
|
|
1796
|
-
if (check.message)
|
|
1797
|
-
result += `\n\n${check.message}`;
|
|
1798
|
-
return result;
|
|
1799
|
-
}
|
|
1800
|
-
catch (error) {
|
|
1801
|
-
return `Error opening URL: ${error.message}`;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
async function iosPushNotification(bundleId, payload, udid) {
|
|
1805
|
-
const check = await requireAdvanced("ios_push_notification");
|
|
1806
|
-
if (!check.allowed)
|
|
1807
|
-
return check.message;
|
|
1808
|
-
if (process.platform !== "darwin") {
|
|
1809
|
-
return "iOS Simulators are only available on macOS.";
|
|
1810
|
-
}
|
|
1811
|
-
try {
|
|
1812
|
-
const target = udid || "booted";
|
|
1813
|
-
const payloadPath = path.join(CONFIG.screenshotDir, `push_${Date.now()}.json`);
|
|
1814
|
-
// Write payload to temp file
|
|
1815
|
-
fs.writeFileSync(payloadPath, JSON.stringify(payload));
|
|
1816
|
-
await execAsync(`xcrun simctl push ${target} "${bundleId}" "${payloadPath}"`);
|
|
1817
|
-
// Clean up
|
|
1818
|
-
fs.unlinkSync(payloadPath);
|
|
1819
|
-
let result = `✅ Push notification sent to ${bundleId}`;
|
|
1820
|
-
if (check.message)
|
|
1821
|
-
result += `\n\n${check.message}`;
|
|
1822
|
-
return result;
|
|
1823
|
-
}
|
|
1824
|
-
catch (error) {
|
|
1825
|
-
return `Error sending push notification: ${error.message}`;
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
async function iosSetLocation(latitude, longitude, udid) {
|
|
1829
|
-
const check = await requireAdvanced("ios_set_location");
|
|
1830
|
-
if (!check.allowed)
|
|
1831
|
-
return check.message;
|
|
1832
|
-
if (process.platform !== "darwin") {
|
|
1833
|
-
return "iOS Simulators are only available on macOS.";
|
|
1834
|
-
}
|
|
1835
|
-
try {
|
|
1836
|
-
const target = udid || "booted";
|
|
1837
|
-
await execAsync(`xcrun simctl location ${target} set ${latitude},${longitude}`);
|
|
1838
|
-
let result = `✅ Location set to: ${latitude}, ${longitude}`;
|
|
1839
|
-
if (check.message)
|
|
1840
|
-
result += `\n\n${check.message}`;
|
|
1841
|
-
return result;
|
|
1842
|
-
}
|
|
1843
|
-
catch (error) {
|
|
1844
|
-
return `Error setting location: ${error.message}`;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
// ============================================================================
|
|
1848
|
-
// REACT DEVTOOLS INTEGRATION
|
|
1849
|
-
// ============================================================================
|
|
1850
|
-
// Store for DevTools WebSocket connection
|
|
1851
|
-
let devToolsWs = null;
|
|
1852
|
-
let devToolsConnected = false;
|
|
1853
|
-
let componentTree = new Map();
|
|
1854
|
-
let pendingRequests = new Map();
|
|
1855
|
-
let requestId = 0;
|
|
1856
|
-
async function setupReactDevTools(port = 8097, device) {
|
|
1857
|
-
const check = await requireAdvanced("setup_react_devtools");
|
|
1858
|
-
if (!check.allowed)
|
|
1859
|
-
return check.message;
|
|
1860
|
-
const results = [];
|
|
1861
|
-
results.push("🔧 React DevTools Setup\n" + "═".repeat(50));
|
|
1862
|
-
// Step 1: Set up ADB port forwarding (for Android)
|
|
1863
|
-
try {
|
|
1864
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
1865
|
-
await execAsync(`adb ${deviceFlag} reverse tcp:${port} tcp:${port}`);
|
|
1866
|
-
results.push(`\n✅ ADB port forwarding configured: tcp:${port} -> tcp:${port}`);
|
|
1867
|
-
}
|
|
1868
|
-
catch (error) {
|
|
1869
|
-
results.push(`\n⚠️ ADB port forwarding failed: ${error.message}`);
|
|
1870
|
-
results.push(" (This is OK if using iOS Simulator or DevTools is on same machine)");
|
|
1871
|
-
}
|
|
1872
|
-
// Step 2: Check if DevTools server is available
|
|
1873
|
-
try {
|
|
1874
|
-
const isRunning = await checkDevToolsServer(port);
|
|
1875
|
-
if (isRunning) {
|
|
1876
|
-
results.push(`✅ React DevTools server detected on port ${port}`);
|
|
1877
|
-
}
|
|
1878
|
-
else {
|
|
1879
|
-
results.push(`⚠️ React DevTools server not detected on port ${port}`);
|
|
1880
|
-
results.push("\nTo start React DevTools:");
|
|
1881
|
-
results.push(" npx react-devtools");
|
|
1882
|
-
results.push(" # or install globally:");
|
|
1883
|
-
results.push(" npm install -g react-devtools && react-devtools");
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
catch (error) {
|
|
1887
|
-
results.push(`⚠️ Could not check DevTools server: ${error.message}`);
|
|
1888
|
-
}
|
|
1889
|
-
// Step 3: Connection instructions
|
|
1890
|
-
results.push("\n📋 Next Steps:");
|
|
1891
|
-
results.push("1. Make sure React DevTools standalone is running (npx react-devtools)");
|
|
1892
|
-
results.push("2. Your React Native app should connect automatically in dev mode");
|
|
1893
|
-
results.push("3. Use 'check_devtools_connection' to verify the connection");
|
|
1894
|
-
results.push("4. Use 'get_react_component_tree' to inspect components");
|
|
1895
|
-
let result = results.join("\n");
|
|
1896
|
-
if (check.message)
|
|
1897
|
-
result += `\n\n${check.message}`;
|
|
1898
|
-
return result;
|
|
1899
|
-
}
|
|
1900
|
-
async function checkDevToolsServer(port) {
|
|
1901
|
-
return new Promise((resolve) => {
|
|
1902
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1903
|
-
const timeout = setTimeout(() => {
|
|
1904
|
-
ws.close();
|
|
1905
|
-
resolve(false);
|
|
1906
|
-
}, 3000);
|
|
1907
|
-
ws.on("open", () => {
|
|
1908
|
-
clearTimeout(timeout);
|
|
1909
|
-
ws.close();
|
|
1910
|
-
resolve(true);
|
|
1911
|
-
});
|
|
1912
|
-
ws.on("error", () => {
|
|
1913
|
-
clearTimeout(timeout);
|
|
1914
|
-
resolve(false);
|
|
1915
|
-
});
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
async function checkDevToolsConnection(port = 8097) {
|
|
1919
|
-
const check = await requireAdvanced("check_devtools_connection");
|
|
1920
|
-
if (!check.allowed)
|
|
1921
|
-
return check.message;
|
|
1922
|
-
const results = [];
|
|
1923
|
-
results.push("🔍 React DevTools Connection Status\n" + "═".repeat(50));
|
|
1924
|
-
// Check if server is running
|
|
1925
|
-
const serverRunning = await checkDevToolsServer(port);
|
|
1926
|
-
if (!serverRunning) {
|
|
1927
|
-
results.push(`\n❌ DevTools server not found on port ${port}`);
|
|
1928
|
-
results.push("\nTo start React DevTools:");
|
|
1929
|
-
results.push(" npx react-devtools");
|
|
1930
|
-
let result = results.join("\n");
|
|
1931
|
-
if (check.message)
|
|
1932
|
-
result += `\n\n${check.message}`;
|
|
1933
|
-
return result;
|
|
1934
|
-
}
|
|
1935
|
-
results.push(`\n✅ DevTools server running on port ${port}`);
|
|
1936
|
-
// Try to connect and get basic info
|
|
1937
|
-
try {
|
|
1938
|
-
const connection = await connectToDevTools(port);
|
|
1939
|
-
if (connection.connected) {
|
|
1940
|
-
results.push("✅ Successfully connected to DevTools");
|
|
1941
|
-
if (connection.rendererCount > 0) {
|
|
1942
|
-
results.push(`✅ ${connection.rendererCount} React renderer(s) connected`);
|
|
1943
|
-
results.push("\n📱 App is connected and ready for inspection!");
|
|
1944
|
-
results.push(" Use 'get_react_component_tree' to view components");
|
|
1945
|
-
}
|
|
1946
|
-
else {
|
|
1947
|
-
results.push("⚠️ No React renderers connected");
|
|
1948
|
-
results.push("\nMake sure your React Native app is running in development mode.");
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
else {
|
|
1952
|
-
results.push("⚠️ Connected to server but no app detected");
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
catch (error) {
|
|
1956
|
-
results.push(`⚠️ Connection test failed: ${error.message}`);
|
|
1957
|
-
}
|
|
1958
|
-
let result = results.join("\n");
|
|
1959
|
-
if (check.message)
|
|
1960
|
-
result += `\n\n${check.message}`;
|
|
1961
|
-
return result;
|
|
1962
|
-
}
|
|
1963
|
-
async function connectToDevTools(port) {
|
|
1964
|
-
return new Promise((resolve, reject) => {
|
|
1965
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1966
|
-
let rendererCount = 0;
|
|
1967
|
-
let resolved = false;
|
|
1968
|
-
const timeout = setTimeout(() => {
|
|
1969
|
-
if (!resolved) {
|
|
1970
|
-
resolved = true;
|
|
1971
|
-
ws.close();
|
|
1972
|
-
resolve({ connected: true, rendererCount: 0 });
|
|
1973
|
-
}
|
|
1974
|
-
}, 5000);
|
|
1975
|
-
ws.on("open", () => {
|
|
1976
|
-
// DevTools protocol: request operations
|
|
1977
|
-
// The standalone DevTools uses a different protocol than we might expect
|
|
1978
|
-
// For now, we'll just confirm connection
|
|
1979
|
-
});
|
|
1980
|
-
ws.on("message", (data) => {
|
|
1981
|
-
try {
|
|
1982
|
-
const message = JSON.parse(data.toString());
|
|
1983
|
-
if (message.event === "operations" || message.event === "roots") {
|
|
1984
|
-
rendererCount++;
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
catch { }
|
|
1988
|
-
});
|
|
1989
|
-
ws.on("close", () => {
|
|
1990
|
-
if (!resolved) {
|
|
1991
|
-
resolved = true;
|
|
1992
|
-
clearTimeout(timeout);
|
|
1993
|
-
resolve({ connected: true, rendererCount });
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
1996
|
-
ws.on("error", (err) => {
|
|
1997
|
-
if (!resolved) {
|
|
1998
|
-
resolved = true;
|
|
1999
|
-
clearTimeout(timeout);
|
|
2000
|
-
reject(err);
|
|
2001
|
-
}
|
|
2002
|
-
});
|
|
2003
|
-
// Give it time to receive messages
|
|
2004
|
-
setTimeout(() => {
|
|
2005
|
-
if (!resolved) {
|
|
2006
|
-
resolved = true;
|
|
2007
|
-
clearTimeout(timeout);
|
|
2008
|
-
ws.close();
|
|
2009
|
-
resolve({ connected: true, rendererCount });
|
|
2010
|
-
}
|
|
2011
|
-
}, 2000);
|
|
2012
|
-
});
|
|
2013
|
-
}
|
|
2014
|
-
async function getReactComponentTree(port = 8097, depth = 5) {
|
|
2015
|
-
const check = await requireAdvanced("get_react_component_tree");
|
|
2016
|
-
if (!check.allowed)
|
|
2017
|
-
return check.message;
|
|
2018
|
-
try {
|
|
2019
|
-
const tree = await fetchComponentTree(port, depth);
|
|
2020
|
-
if (!tree || tree.length === 0) {
|
|
2021
|
-
let result = `📊 React Component Tree\n${"═".repeat(50)}\n\nNo components found.\n\nMake sure:\n1. React DevTools standalone is running (npx react-devtools)\n2. Your React Native app is running in development mode\n3. The app is connected to DevTools`;
|
|
2022
|
-
if (check.message)
|
|
2023
|
-
result += `\n\n${check.message}`;
|
|
2024
|
-
return result;
|
|
2025
|
-
}
|
|
2026
|
-
const lines = [];
|
|
2027
|
-
lines.push("📊 React Component Tree");
|
|
2028
|
-
lines.push("═".repeat(50));
|
|
2029
|
-
lines.push("");
|
|
2030
|
-
for (const node of tree) {
|
|
2031
|
-
const indent = " ".repeat(node.depth);
|
|
2032
|
-
const typeIcon = node.type === "function" ? "ƒ" : node.type === "class" ? "◆" : "○";
|
|
2033
|
-
lines.push(`${indent}${typeIcon} ${node.name} [id:${node.id}]`);
|
|
2034
|
-
if (node.key) {
|
|
2035
|
-
lines.push(`${indent} key: "${node.key}"`);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
lines.push("");
|
|
2039
|
-
lines.push(`Total: ${tree.length} components (depth: ${depth})`);
|
|
2040
|
-
lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
|
|
2041
|
-
let result = lines.join("\n");
|
|
2042
|
-
if (check.message)
|
|
2043
|
-
result += `\n\n${check.message}`;
|
|
2044
|
-
return result;
|
|
2045
|
-
}
|
|
2046
|
-
catch (error) {
|
|
2047
|
-
let result = `Error fetching component tree: ${error.message}\n\nMake sure React DevTools is running: npx react-devtools`;
|
|
2048
|
-
if (check.message)
|
|
2049
|
-
result += `\n\n${check.message}`;
|
|
2050
|
-
return result;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
async function fetchComponentTree(port, maxDepth) {
|
|
2054
|
-
return new Promise((resolve, reject) => {
|
|
2055
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
2056
|
-
const components = [];
|
|
2057
|
-
let resolved = false;
|
|
2058
|
-
const timeout = setTimeout(() => {
|
|
2059
|
-
if (!resolved) {
|
|
2060
|
-
resolved = true;
|
|
2061
|
-
ws.close();
|
|
2062
|
-
resolve(components);
|
|
2063
|
-
}
|
|
2064
|
-
}, 10000);
|
|
2065
|
-
ws.on("open", () => {
|
|
2066
|
-
// Send a request to get the tree
|
|
2067
|
-
// The DevTools protocol varies, so we listen for operations
|
|
2068
|
-
});
|
|
2069
|
-
ws.on("message", (data) => {
|
|
2070
|
-
try {
|
|
2071
|
-
const message = JSON.parse(data.toString());
|
|
2072
|
-
// Handle different message types from React DevTools
|
|
2073
|
-
if (message.event === "operations") {
|
|
2074
|
-
// Parse operations to build component tree
|
|
2075
|
-
const ops = message.payload;
|
|
2076
|
-
if (Array.isArray(ops)) {
|
|
2077
|
-
// Operations array contains component tree data
|
|
2078
|
-
parseOperations(ops, components, maxDepth);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
else if (message.event === "roots") {
|
|
2082
|
-
// Root components
|
|
2083
|
-
if (Array.isArray(message.payload)) {
|
|
2084
|
-
for (const root of message.payload) {
|
|
2085
|
-
components.push({
|
|
2086
|
-
id: root.id || components.length,
|
|
2087
|
-
name: root.displayName || root.name || "Root",
|
|
2088
|
-
type: root.type || "root",
|
|
2089
|
-
depth: 0,
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
catch { }
|
|
2096
|
-
});
|
|
2097
|
-
ws.on("error", (err) => {
|
|
2098
|
-
if (!resolved) {
|
|
2099
|
-
resolved = true;
|
|
2100
|
-
clearTimeout(timeout);
|
|
2101
|
-
// Return empty array on error, not reject
|
|
2102
|
-
resolve(components);
|
|
2103
|
-
}
|
|
2104
|
-
});
|
|
2105
|
-
ws.on("close", () => {
|
|
2106
|
-
if (!resolved) {
|
|
2107
|
-
resolved = true;
|
|
2108
|
-
clearTimeout(timeout);
|
|
2109
|
-
resolve(components);
|
|
2110
|
-
}
|
|
2111
|
-
});
|
|
2112
|
-
// Give time to receive data
|
|
2113
|
-
setTimeout(() => {
|
|
2114
|
-
if (!resolved) {
|
|
2115
|
-
resolved = true;
|
|
2116
|
-
clearTimeout(timeout);
|
|
2117
|
-
ws.close();
|
|
2118
|
-
resolve(components);
|
|
2119
|
-
}
|
|
2120
|
-
}, 3000);
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
function parseOperations(ops, components, maxDepth) {
|
|
2124
|
-
// React DevTools operations format varies by version
|
|
2125
|
-
// This is a simplified parser
|
|
2126
|
-
let depth = 0;
|
|
2127
|
-
for (let i = 0; i < ops.length && components.length < 100; i++) {
|
|
2128
|
-
const op = ops[i];
|
|
2129
|
-
if (typeof op === "object" && op.id !== undefined) {
|
|
2130
|
-
if (depth <= maxDepth) {
|
|
2131
|
-
components.push({
|
|
2132
|
-
id: op.id,
|
|
2133
|
-
name: op.displayName || op.name || `Component_${op.id}`,
|
|
2134
|
-
type: op.type === 1 ? "function" : op.type === 2 ? "class" : "other",
|
|
2135
|
-
depth: Math.min(op.depth || depth, maxDepth),
|
|
2136
|
-
key: op.key,
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
async function inspectReactComponent(componentId, port = 8097) {
|
|
2143
|
-
const check = await requireAdvanced("inspect_react_component");
|
|
2144
|
-
if (!check.allowed)
|
|
2145
|
-
return check.message;
|
|
2146
|
-
try {
|
|
2147
|
-
const inspection = await fetchComponentDetails(componentId, port);
|
|
2148
|
-
if (!inspection) {
|
|
2149
|
-
let result = `Component with ID ${componentId} not found.\n\nUse 'get_react_component_tree' to get valid component IDs.`;
|
|
2150
|
-
if (check.message)
|
|
2151
|
-
result += `\n\n${check.message}`;
|
|
2152
|
-
return result;
|
|
2153
|
-
}
|
|
2154
|
-
const lines = [];
|
|
2155
|
-
lines.push(`🔍 Component Inspection: ${inspection.name}`);
|
|
2156
|
-
lines.push("═".repeat(50));
|
|
2157
|
-
lines.push(`ID: ${componentId}`);
|
|
2158
|
-
lines.push(`Type: ${inspection.type}`);
|
|
2159
|
-
if (inspection.props && Object.keys(inspection.props).length > 0) {
|
|
2160
|
-
lines.push("\n📦 Props:");
|
|
2161
|
-
for (const [key, value] of Object.entries(inspection.props)) {
|
|
2162
|
-
const displayValue = formatValue(value);
|
|
2163
|
-
lines.push(` ${key}: ${displayValue}`);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
lines.push("\n📦 Props: (none)");
|
|
2168
|
-
}
|
|
2169
|
-
if (inspection.state && Object.keys(inspection.state).length > 0) {
|
|
2170
|
-
lines.push("\n💾 State:");
|
|
2171
|
-
for (const [key, value] of Object.entries(inspection.state)) {
|
|
2172
|
-
const displayValue = formatValue(value);
|
|
2173
|
-
lines.push(` ${key}: ${displayValue}`);
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
if (inspection.hooks && inspection.hooks.length > 0) {
|
|
2177
|
-
lines.push("\n🪝 Hooks:");
|
|
2178
|
-
for (const hook of inspection.hooks) {
|
|
2179
|
-
lines.push(` ${hook.name}: ${formatValue(hook.value)}`);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
let result = lines.join("\n");
|
|
2183
|
-
if (check.message)
|
|
2184
|
-
result += `\n\n${check.message}`;
|
|
2185
|
-
return result;
|
|
2186
|
-
}
|
|
2187
|
-
catch (error) {
|
|
2188
|
-
let result = `Error inspecting component: ${error.message}`;
|
|
2189
|
-
if (check.message)
|
|
2190
|
-
result += `\n\n${check.message}`;
|
|
2191
|
-
return result;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
function formatValue(value) {
|
|
2195
|
-
if (value === null)
|
|
2196
|
-
return "null";
|
|
2197
|
-
if (value === undefined)
|
|
2198
|
-
return "undefined";
|
|
2199
|
-
if (typeof value === "string")
|
|
2200
|
-
return `"${value.substring(0, 100)}${value.length > 100 ? "..." : ""}"`;
|
|
2201
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
2202
|
-
return String(value);
|
|
2203
|
-
if (Array.isArray(value))
|
|
2204
|
-
return `Array(${value.length})`;
|
|
2205
|
-
if (typeof value === "object") {
|
|
2206
|
-
const keys = Object.keys(value);
|
|
2207
|
-
if (keys.length === 0)
|
|
2208
|
-
return "{}";
|
|
2209
|
-
return `{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
|
|
2210
|
-
}
|
|
2211
|
-
if (typeof value === "function")
|
|
2212
|
-
return "ƒ()";
|
|
2213
|
-
return String(value).substring(0, 50);
|
|
2214
|
-
}
|
|
2215
|
-
async function fetchComponentDetails(componentId, port) {
|
|
2216
|
-
return new Promise((resolve) => {
|
|
2217
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
2218
|
-
let resolved = false;
|
|
2219
|
-
let details = null;
|
|
2220
|
-
const timeout = setTimeout(() => {
|
|
2221
|
-
if (!resolved) {
|
|
2222
|
-
resolved = true;
|
|
2223
|
-
ws.close();
|
|
2224
|
-
resolve(details);
|
|
2225
|
-
}
|
|
2226
|
-
}, 5000);
|
|
2227
|
-
ws.on("open", () => {
|
|
2228
|
-
// Request inspection of specific element
|
|
2229
|
-
const request = {
|
|
2230
|
-
event: "inspectElement",
|
|
2231
|
-
payload: {
|
|
2232
|
-
id: componentId,
|
|
2233
|
-
rendererID: 1,
|
|
2234
|
-
requestID: Date.now(),
|
|
2235
|
-
},
|
|
2236
|
-
};
|
|
2237
|
-
ws.send(JSON.stringify(request));
|
|
2238
|
-
});
|
|
2239
|
-
ws.on("message", (data) => {
|
|
2240
|
-
try {
|
|
2241
|
-
const message = JSON.parse(data.toString());
|
|
2242
|
-
if (message.event === "inspectedElement" && message.payload) {
|
|
2243
|
-
const p = message.payload;
|
|
2244
|
-
details = {
|
|
2245
|
-
name: p.displayName || p.name || `Component_${componentId}`,
|
|
2246
|
-
type: p.type || "unknown",
|
|
2247
|
-
props: p.props || {},
|
|
2248
|
-
state: p.state,
|
|
2249
|
-
hooks: p.hooks,
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
catch { }
|
|
2254
|
-
});
|
|
2255
|
-
ws.on("error", () => {
|
|
2256
|
-
if (!resolved) {
|
|
2257
|
-
resolved = true;
|
|
2258
|
-
clearTimeout(timeout);
|
|
2259
|
-
resolve(null);
|
|
2260
|
-
}
|
|
2261
|
-
});
|
|
2262
|
-
ws.on("close", () => {
|
|
2263
|
-
if (!resolved) {
|
|
2264
|
-
resolved = true;
|
|
2265
|
-
clearTimeout(timeout);
|
|
2266
|
-
resolve(details);
|
|
2267
|
-
}
|
|
2268
|
-
});
|
|
2269
|
-
// Give time to receive response
|
|
2270
|
-
setTimeout(() => {
|
|
2271
|
-
if (!resolved) {
|
|
2272
|
-
resolved = true;
|
|
2273
|
-
clearTimeout(timeout);
|
|
2274
|
-
ws.close();
|
|
2275
|
-
resolve(details);
|
|
2276
|
-
}
|
|
2277
|
-
}, 3000);
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
async function searchReactComponents(query, port = 8097) {
|
|
2281
|
-
const check = await requireAdvanced("search_react_components");
|
|
2282
|
-
if (!check.allowed)
|
|
2283
|
-
return check.message;
|
|
2284
|
-
try {
|
|
2285
|
-
const tree = await fetchComponentTree(port, 10);
|
|
2286
|
-
const queryLower = query.toLowerCase();
|
|
2287
|
-
const matches = tree.filter((component) => component.name.toLowerCase().includes(queryLower));
|
|
2288
|
-
if (matches.length === 0) {
|
|
2289
|
-
let result = `🔍 Search Results for "${query}"\n${"═".repeat(50)}\n\nNo components found matching "${query}".\n\nTip: Try a partial name or check if the app is connected to DevTools.`;
|
|
2290
|
-
if (check.message)
|
|
2291
|
-
result += `\n\n${check.message}`;
|
|
2292
|
-
return result;
|
|
2293
|
-
}
|
|
2294
|
-
const lines = [];
|
|
2295
|
-
lines.push(`🔍 Search Results for "${query}"`);
|
|
2296
|
-
lines.push("═".repeat(50));
|
|
2297
|
-
lines.push(`\nFound ${matches.length} matching component(s):\n`);
|
|
2298
|
-
for (const match of matches.slice(0, 20)) {
|
|
2299
|
-
const typeIcon = match.type === "function" ? "ƒ" : match.type === "class" ? "◆" : "○";
|
|
2300
|
-
lines.push(`${typeIcon} ${match.name} [id:${match.id}]`);
|
|
2301
|
-
}
|
|
2302
|
-
if (matches.length > 20) {
|
|
2303
|
-
lines.push(`\n... and ${matches.length - 20} more matches`);
|
|
2304
|
-
}
|
|
2305
|
-
lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
|
|
2306
|
-
let result = lines.join("\n");
|
|
2307
|
-
if (check.message)
|
|
2308
|
-
result += `\n\n${check.message}`;
|
|
2309
|
-
return result;
|
|
2310
|
-
}
|
|
2311
|
-
catch (error) {
|
|
2312
|
-
let result = `Error searching components: ${error.message}`;
|
|
2313
|
-
if (check.message)
|
|
2314
|
-
result += `\n\n${check.message}`;
|
|
2315
|
-
return result;
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
// ============================================================================
|
|
2319
|
-
// NETWORK INSPECTION
|
|
2320
|
-
// ============================================================================
|
|
2321
|
-
// Network monitoring state
|
|
2322
|
-
let networkMonitorProcess = null;
|
|
2323
|
-
let networkRequestBuffer = [];
|
|
2324
|
-
const MAX_NETWORK_REQUESTS = 100;
|
|
2325
|
-
async function getNetworkRequests(lines = 200, filter, device) {
|
|
2326
|
-
const check = await requireAdvanced("get_network_requests");
|
|
2327
|
-
if (!check.allowed)
|
|
2328
|
-
return check.message;
|
|
2329
|
-
try {
|
|
2330
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2331
|
-
// Get logs and filter for network-related entries
|
|
2332
|
-
// React Native logs network requests with various patterns
|
|
2333
|
-
const { stdout } = await execAsync(`adb ${deviceFlag} logcat -d -t ${lines} *:V`, { maxBuffer: 5 * 1024 * 1024 });
|
|
2334
|
-
const requests = [];
|
|
2335
|
-
const logLines = stdout.split("\n");
|
|
2336
|
-
// Patterns to match network requests
|
|
2337
|
-
const patterns = [
|
|
2338
|
-
// OkHttp pattern
|
|
2339
|
-
/OkHttp.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2340
|
-
// React Native fetch pattern
|
|
2341
|
-
/\[fetch\].*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2342
|
-
// Generic HTTP pattern
|
|
2343
|
-
/HTTP.*?(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i,
|
|
2344
|
-
// XMLHttpRequest pattern
|
|
2345
|
-
/XMLHttpRequest.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2346
|
-
// Network response pattern
|
|
2347
|
-
/(\d{3})\s+(https?:\/\/\S+).*?(\d+ms|\d+\.\d+s)/i,
|
|
2348
|
-
];
|
|
2349
|
-
let index = 0;
|
|
2350
|
-
for (const line of logLines) {
|
|
2351
|
-
for (const pattern of patterns) {
|
|
2352
|
-
const match = line.match(pattern);
|
|
2353
|
-
if (match) {
|
|
2354
|
-
const method = match[1]?.toUpperCase() || "GET";
|
|
2355
|
-
const url = match[2] || "";
|
|
2356
|
-
// Apply filter if provided
|
|
2357
|
-
if (filter) {
|
|
2358
|
-
const filterLower = filter.toLowerCase();
|
|
2359
|
-
if (!method.toLowerCase().includes(filterLower) &&
|
|
2360
|
-
!url.toLowerCase().includes(filterLower)) {
|
|
2361
|
-
continue;
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
requests.push({
|
|
2365
|
-
index: index++,
|
|
2366
|
-
timestamp: new Date().toISOString(),
|
|
2367
|
-
method,
|
|
2368
|
-
url: url.substring(0, 200),
|
|
2369
|
-
status: match[1]?.match(/^\d{3}$/) ? parseInt(match[1]) : undefined,
|
|
2370
|
-
});
|
|
2371
|
-
break;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
if (requests.length === 0) {
|
|
2376
|
-
let result = `📡 Network Requests\n${"═".repeat(50)}\n\nNo network requests found in recent logs.\n\nTips:\n- Make sure your app is making network requests\n- React Native logs network activity in development mode\n- Try increasing the lines parameter`;
|
|
2377
|
-
if (check.message)
|
|
2378
|
-
result += `\n\n${check.message}`;
|
|
2379
|
-
return result;
|
|
2380
|
-
}
|
|
2381
|
-
const resultLines = [];
|
|
2382
|
-
resultLines.push("📡 Network Requests");
|
|
2383
|
-
resultLines.push("═".repeat(50));
|
|
2384
|
-
resultLines.push(`\nFound ${requests.length} request(s):\n`);
|
|
2385
|
-
for (const req of requests.slice(0, 30)) {
|
|
2386
|
-
const statusIcon = req.status
|
|
2387
|
-
? (req.status >= 200 && req.status < 300 ? "✅" : "❌")
|
|
2388
|
-
: "⏳";
|
|
2389
|
-
resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 60)}${req.url.length > 60 ? "..." : ""}`);
|
|
2390
|
-
if (req.status) {
|
|
2391
|
-
resultLines.push(` Status: ${req.status}`);
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
if (requests.length > 30) {
|
|
2395
|
-
resultLines.push(`\n... and ${requests.length - 30} more requests`);
|
|
2396
|
-
}
|
|
2397
|
-
resultLines.push("\nUse 'start_network_monitoring' for real-time capture");
|
|
2398
|
-
let result = resultLines.join("\n");
|
|
2399
|
-
if (check.message)
|
|
2400
|
-
result += `\n\n${check.message}`;
|
|
2401
|
-
return result;
|
|
2402
|
-
}
|
|
2403
|
-
catch (error) {
|
|
2404
|
-
return `Error getting network requests: ${error.message}`;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
async function startNetworkMonitoring(device) {
|
|
2408
|
-
const check = await requireAdvanced("start_network_monitoring");
|
|
2409
|
-
if (!check.allowed)
|
|
2410
|
-
return check.message;
|
|
2411
|
-
if (networkMonitorProcess) {
|
|
2412
|
-
return "Network monitoring is already running. Use 'stop_network_monitoring' first.";
|
|
2413
|
-
}
|
|
2414
|
-
networkRequestBuffer = [];
|
|
2415
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2416
|
-
// Start logcat with network-related filters
|
|
2417
|
-
networkMonitorProcess = spawn("adb", [
|
|
2418
|
-
...deviceFlag.split(" ").filter(s => s),
|
|
2419
|
-
"logcat",
|
|
2420
|
-
"-v", "time",
|
|
2421
|
-
"OkHttp:V",
|
|
2422
|
-
"Retrofit:V",
|
|
2423
|
-
"ReactNativeJS:V",
|
|
2424
|
-
"*:S"
|
|
2425
|
-
]);
|
|
2426
|
-
let requestIndex = 0;
|
|
2427
|
-
networkMonitorProcess.stdout?.on("data", (data) => {
|
|
2428
|
-
const lines = data.toString().split("\n");
|
|
2429
|
-
for (const line of lines) {
|
|
2430
|
-
// Parse network request patterns
|
|
2431
|
-
const httpMatch = line.match(/(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i);
|
|
2432
|
-
if (httpMatch) {
|
|
2433
|
-
networkRequestBuffer.push({
|
|
2434
|
-
index: requestIndex++,
|
|
2435
|
-
timestamp: new Date().toISOString(),
|
|
2436
|
-
method: httpMatch[1].toUpperCase(),
|
|
2437
|
-
url: httpMatch[2],
|
|
2438
|
-
});
|
|
2439
|
-
if (networkRequestBuffer.length > MAX_NETWORK_REQUESTS) {
|
|
2440
|
-
networkRequestBuffer = networkRequestBuffer.slice(-MAX_NETWORK_REQUESTS);
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
// Parse response status
|
|
2444
|
-
const statusMatch = line.match(/<--\s*(\d{3})\s+(https?:\/\/\S+)/i);
|
|
2445
|
-
if (statusMatch) {
|
|
2446
|
-
const url = statusMatch[2];
|
|
2447
|
-
const req = networkRequestBuffer.find(r => r.url === url && !r.status);
|
|
2448
|
-
if (req) {
|
|
2449
|
-
req.status = parseInt(statusMatch[1]);
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
});
|
|
2454
|
-
networkMonitorProcess.on("error", (err) => {
|
|
2455
|
-
console.error("Network monitor error:", err);
|
|
2456
|
-
});
|
|
2457
|
-
let result = `📡 Network Monitoring Started\n${"═".repeat(50)}\n\nCapturing HTTP/HTTPS requests in background.\n\nUse 'stop_network_monitoring' to stop and see captured requests.\nUse 'get_network_requests' to see current buffer.`;
|
|
2458
|
-
if (check.message)
|
|
2459
|
-
result += `\n\n${check.message}`;
|
|
2460
|
-
return result;
|
|
2461
|
-
}
|
|
2462
|
-
async function stopNetworkMonitoring() {
|
|
2463
|
-
const check = await requireAdvanced("stop_network_monitoring");
|
|
2464
|
-
if (!check.allowed)
|
|
2465
|
-
return check.message;
|
|
2466
|
-
if (networkMonitorProcess) {
|
|
2467
|
-
networkMonitorProcess.kill();
|
|
2468
|
-
networkMonitorProcess = null;
|
|
2469
|
-
}
|
|
2470
|
-
const requests = networkRequestBuffer;
|
|
2471
|
-
const summary = {
|
|
2472
|
-
total: requests.length,
|
|
2473
|
-
successful: requests.filter(r => r.status && r.status >= 200 && r.status < 300).length,
|
|
2474
|
-
failed: requests.filter(r => r.status && (r.status < 200 || r.status >= 300)).length,
|
|
2475
|
-
pending: requests.filter(r => !r.status).length,
|
|
2476
|
-
};
|
|
2477
|
-
const resultLines = [];
|
|
2478
|
-
resultLines.push("📡 Network Monitoring Stopped");
|
|
2479
|
-
resultLines.push("═".repeat(50));
|
|
2480
|
-
resultLines.push(`\nSummary:`);
|
|
2481
|
-
resultLines.push(` Total requests: ${summary.total}`);
|
|
2482
|
-
resultLines.push(` Successful (2xx): ${summary.successful}`);
|
|
2483
|
-
resultLines.push(` Failed: ${summary.failed}`);
|
|
2484
|
-
resultLines.push(` Pending: ${summary.pending}`);
|
|
2485
|
-
if (requests.length > 0) {
|
|
2486
|
-
resultLines.push(`\nRecent requests:`);
|
|
2487
|
-
for (const req of requests.slice(-10)) {
|
|
2488
|
-
const statusIcon = req.status
|
|
2489
|
-
? (req.status >= 200 && req.status < 300 ? "✅" : "❌")
|
|
2490
|
-
: "⏳";
|
|
2491
|
-
resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 50)}...`);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
let result = resultLines.join("\n");
|
|
2495
|
-
if (check.message)
|
|
2496
|
-
result += `\n\n${check.message}`;
|
|
2497
|
-
return result;
|
|
2498
|
-
}
|
|
2499
|
-
async function getNetworkStats(device) {
|
|
2500
|
-
const check = await requireAdvanced("get_network_stats");
|
|
2501
|
-
if (!check.allowed)
|
|
2502
|
-
return check.message;
|
|
2503
|
-
try {
|
|
2504
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2505
|
-
// Get various network stats
|
|
2506
|
-
const [wifiInfo, netStats, activeConns] = await Promise.all([
|
|
2507
|
-
execAsync(`adb ${deviceFlag} shell dumpsys wifi | head -50`)
|
|
2508
|
-
.then(r => r.stdout)
|
|
2509
|
-
.catch(() => "N/A"),
|
|
2510
|
-
execAsync(`adb ${deviceFlag} shell cat /proc/net/dev`)
|
|
2511
|
-
.then(r => r.stdout)
|
|
2512
|
-
.catch(() => "N/A"),
|
|
2513
|
-
execAsync(`adb ${deviceFlag} shell netstat -an | head -30`)
|
|
2514
|
-
.then(r => r.stdout)
|
|
2515
|
-
.catch(() => "N/A"),
|
|
2516
|
-
]);
|
|
2517
|
-
const resultLines = [];
|
|
2518
|
-
resultLines.push("📊 Network Statistics");
|
|
2519
|
-
resultLines.push("═".repeat(50));
|
|
2520
|
-
// Parse WiFi info
|
|
2521
|
-
const ssidMatch = wifiInfo.match(/mWifiInfo.*?SSID:\s*([^,]+)/);
|
|
2522
|
-
const rssiMatch = wifiInfo.match(/RSSI:\s*(-?\d+)/);
|
|
2523
|
-
const linkSpeedMatch = wifiInfo.match(/Link speed:\s*(\d+)/);
|
|
2524
|
-
resultLines.push("\n📶 WiFi Info:");
|
|
2525
|
-
if (ssidMatch)
|
|
2526
|
-
resultLines.push(` SSID: ${ssidMatch[1].trim()}`);
|
|
2527
|
-
if (rssiMatch)
|
|
2528
|
-
resultLines.push(` Signal: ${rssiMatch[1]} dBm`);
|
|
2529
|
-
if (linkSpeedMatch)
|
|
2530
|
-
resultLines.push(` Speed: ${linkSpeedMatch[1]} Mbps`);
|
|
2531
|
-
// Parse network interface stats
|
|
2532
|
-
resultLines.push("\n📈 Interface Stats:");
|
|
2533
|
-
const devLines = netStats.split("\n").filter(l => l.includes("wlan") || l.includes("rmnet"));
|
|
2534
|
-
for (const line of devLines.slice(0, 5)) {
|
|
2535
|
-
const parts = line.trim().split(/\s+/);
|
|
2536
|
-
if (parts.length >= 10) {
|
|
2537
|
-
const iface = parts[0].replace(":", "");
|
|
2538
|
-
const rxBytes = parseInt(parts[1]) || 0;
|
|
2539
|
-
const txBytes = parseInt(parts[9]) || 0;
|
|
2540
|
-
resultLines.push(` ${iface}: RX ${formatBytes(rxBytes)}, TX ${formatBytes(txBytes)}`);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
// Parse active connections
|
|
2544
|
-
resultLines.push("\n🔗 Active Connections:");
|
|
2545
|
-
const connLines = activeConns.split("\n")
|
|
2546
|
-
.filter(l => l.includes("ESTABLISHED") || l.includes("TIME_WAIT"))
|
|
2547
|
-
.slice(0, 8);
|
|
2548
|
-
if (connLines.length > 0) {
|
|
2549
|
-
for (const line of connLines) {
|
|
2550
|
-
const parts = line.trim().split(/\s+/);
|
|
2551
|
-
if (parts.length >= 5) {
|
|
2552
|
-
resultLines.push(` ${parts[3]} -> ${parts[4]} (${parts[5] || ""})`);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
else {
|
|
2557
|
-
resultLines.push(" No active connections found");
|
|
2558
|
-
}
|
|
2559
|
-
let result = resultLines.join("\n");
|
|
2560
|
-
if (check.message)
|
|
2561
|
-
result += `\n\n${check.message}`;
|
|
2562
|
-
return result;
|
|
2563
|
-
}
|
|
2564
|
-
catch (error) {
|
|
2565
|
-
return `Error getting network stats: ${error.message}`;
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
function formatBytes(bytes) {
|
|
2569
|
-
if (bytes < 1024)
|
|
2570
|
-
return `${bytes} B`;
|
|
2571
|
-
if (bytes < 1024 * 1024)
|
|
2572
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2573
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
2574
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
2575
|
-
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
2576
|
-
}
|
|
2577
|
-
async function analyzeRequest(index) {
|
|
2578
|
-
const check = await requireAdvanced("analyze_request");
|
|
2579
|
-
if (!check.allowed)
|
|
2580
|
-
return check.message;
|
|
2581
|
-
const request = networkRequestBuffer.find(r => r.index === index);
|
|
2582
|
-
if (!request) {
|
|
2583
|
-
let result = `Request #${index} not found.\n\nUse 'get_network_requests' or 'start_network_monitoring' to capture requests first.`;
|
|
2584
|
-
if (check.message)
|
|
2585
|
-
result += `\n\n${check.message}`;
|
|
2586
|
-
return result;
|
|
2587
|
-
}
|
|
2588
|
-
const resultLines = [];
|
|
2589
|
-
resultLines.push(`🔍 Request Analysis #${index}`);
|
|
2590
|
-
resultLines.push("═".repeat(50));
|
|
2591
|
-
resultLines.push(`\nMethod: ${request.method}`);
|
|
2592
|
-
resultLines.push(`URL: ${request.url}`);
|
|
2593
|
-
resultLines.push(`Timestamp: ${request.timestamp}`);
|
|
2594
|
-
if (request.status) {
|
|
2595
|
-
const statusText = request.status >= 200 && request.status < 300 ? "Success" : "Failed";
|
|
2596
|
-
resultLines.push(`Status: ${request.status} (${statusText})`);
|
|
2597
|
-
}
|
|
2598
|
-
else {
|
|
2599
|
-
resultLines.push("Status: Pending/Unknown");
|
|
2600
|
-
}
|
|
2601
|
-
if (request.duration) {
|
|
2602
|
-
resultLines.push(`Duration: ${request.duration}ms`);
|
|
2603
|
-
}
|
|
2604
|
-
if (request.size) {
|
|
2605
|
-
resultLines.push(`Size: ${request.size}`);
|
|
2606
|
-
}
|
|
2607
|
-
if (request.headers && Object.keys(request.headers).length > 0) {
|
|
2608
|
-
resultLines.push("\n📋 Headers:");
|
|
2609
|
-
for (const [key, value] of Object.entries(request.headers)) {
|
|
2610
|
-
resultLines.push(` ${key}: ${value}`);
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
if (request.body) {
|
|
2614
|
-
resultLines.push("\n📤 Request Body:");
|
|
2615
|
-
resultLines.push(` ${request.body.substring(0, 500)}${request.body.length > 500 ? "..." : ""}`);
|
|
2616
|
-
}
|
|
2617
|
-
if (request.response) {
|
|
2618
|
-
resultLines.push("\n📥 Response:");
|
|
2619
|
-
resultLines.push(` ${request.response.substring(0, 500)}${request.response.length > 500 ? "..." : ""}`);
|
|
2620
|
-
}
|
|
2621
|
-
let result = resultLines.join("\n");
|
|
2622
|
-
if (check.message)
|
|
2623
|
-
result += `\n\n${check.message}`;
|
|
2624
|
-
return result;
|
|
2625
|
-
}
|
|
2626
|
-
// ============================================================================
|
|
2627
|
-
// EXPO DEVTOOLS INTEGRATION
|
|
2628
|
-
// ============================================================================
|
|
2629
|
-
async function checkExpoStatus(port = 8081) {
|
|
2630
|
-
const check = await requireAdvanced("check_expo_status");
|
|
2631
|
-
if (!check.allowed)
|
|
2632
|
-
return check.message;
|
|
2633
|
-
const resultLines = [];
|
|
2634
|
-
resultLines.push("📱 Expo Dev Server Status");
|
|
2635
|
-
resultLines.push("═".repeat(50));
|
|
2636
|
-
// Check multiple ports that Expo might use
|
|
2637
|
-
const portsToCheck = [
|
|
2638
|
-
{ port, name: "Metro Bundler" },
|
|
2639
|
-
{ port: 19000, name: "Legacy Bundler" },
|
|
2640
|
-
{ port: 19001, name: "WebSocket" },
|
|
2641
|
-
{ port: 19002, name: "DevTools UI" },
|
|
2642
|
-
];
|
|
2643
|
-
let anyRunning = false;
|
|
2644
|
-
for (const { port: p, name } of portsToCheck) {
|
|
2645
|
-
try {
|
|
2646
|
-
const response = await new Promise((resolve, reject) => {
|
|
2647
|
-
const req = http.request({ hostname: "localhost", port: p, path: "/status", method: "GET", timeout: 2000 }, (res) => {
|
|
2648
|
-
let data = "";
|
|
2649
|
-
res.on("data", (chunk) => (data += chunk));
|
|
2650
|
-
res.on("end", () => resolve(data));
|
|
2651
|
-
});
|
|
2652
|
-
req.on("error", reject);
|
|
2653
|
-
req.on("timeout", () => reject(new Error("timeout")));
|
|
2654
|
-
req.end();
|
|
2655
|
-
});
|
|
2656
|
-
resultLines.push(`\n✅ ${name} (port ${p}): Running`);
|
|
2657
|
-
if (response && response.length > 0 && response.length < 200) {
|
|
2658
|
-
resultLines.push(` Status: ${response.trim()}`);
|
|
2659
|
-
}
|
|
2660
|
-
anyRunning = true;
|
|
2661
|
-
}
|
|
2662
|
-
catch {
|
|
2663
|
-
// Port not responding, check if it's a different status endpoint
|
|
2664
|
-
try {
|
|
2665
|
-
// Try the root endpoint for bundler status
|
|
2666
|
-
const response = await new Promise((resolve, reject) => {
|
|
2667
|
-
const req = http.request({ hostname: "localhost", port: p, path: "/", method: "GET", timeout: 1000 }, (res) => {
|
|
2668
|
-
let data = "";
|
|
2669
|
-
res.on("data", (chunk) => (data += chunk));
|
|
2670
|
-
res.on("end", () => resolve(`HTTP ${res.statusCode}`));
|
|
2671
|
-
});
|
|
2672
|
-
req.on("error", reject);
|
|
2673
|
-
req.on("timeout", () => reject(new Error("timeout")));
|
|
2674
|
-
req.end();
|
|
2675
|
-
});
|
|
2676
|
-
resultLines.push(`\n✅ ${name} (port ${p}): Running (${response})`);
|
|
2677
|
-
anyRunning = true;
|
|
2678
|
-
}
|
|
2679
|
-
catch {
|
|
2680
|
-
resultLines.push(`\n⚪ ${name} (port ${p}): Not running`);
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
// Get Expo CLI version if available
|
|
2685
|
-
try {
|
|
2686
|
-
const { stdout: expoVersion } = await execAsync("npx expo --version", { timeout: 5000 });
|
|
2687
|
-
resultLines.push(`\n📦 Expo CLI: v${expoVersion.trim()}`);
|
|
2688
|
-
}
|
|
2689
|
-
catch {
|
|
2690
|
-
resultLines.push("\n📦 Expo CLI: Not detected");
|
|
2691
|
-
}
|
|
2692
|
-
// Check if any device is connected
|
|
2693
|
-
try {
|
|
2694
|
-
const { stdout: devices } = await execAsync("adb devices");
|
|
2695
|
-
const deviceLines = devices.split("\n").filter(line => line.includes("\tdevice"));
|
|
2696
|
-
if (deviceLines.length > 0) {
|
|
2697
|
-
resultLines.push(`\n📱 Connected devices: ${deviceLines.length}`);
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
catch {
|
|
2701
|
-
// ADB not available
|
|
2702
|
-
}
|
|
2703
|
-
if (!anyRunning) {
|
|
2704
|
-
resultLines.push("\n⚠️ No Expo dev server detected. Start with:");
|
|
2705
|
-
resultLines.push(" npx expo start");
|
|
2706
|
-
}
|
|
2707
|
-
let result = resultLines.join("\n");
|
|
2708
|
-
if (check.message)
|
|
2709
|
-
result += `\n\n${check.message}`;
|
|
2710
|
-
return result;
|
|
2711
|
-
}
|
|
2712
|
-
async function getExpoConfig(projectPath) {
|
|
2713
|
-
const check = await requireAdvanced("get_expo_config");
|
|
2714
|
-
if (!check.allowed)
|
|
2715
|
-
return check.message;
|
|
2716
|
-
const cwd = projectPath || process.cwd();
|
|
2717
|
-
const resultLines = [];
|
|
2718
|
-
resultLines.push("📋 Expo Project Configuration");
|
|
2719
|
-
resultLines.push("═".repeat(50));
|
|
2720
|
-
// Try to read app.json
|
|
2721
|
-
const appJsonPath = path.join(cwd, "app.json");
|
|
2722
|
-
const appConfigJsPath = path.join(cwd, "app.config.js");
|
|
2723
|
-
const appConfigTsPath = path.join(cwd, "app.config.ts");
|
|
2724
|
-
let config = null;
|
|
2725
|
-
if (fs.existsSync(appJsonPath)) {
|
|
2726
|
-
try {
|
|
2727
|
-
const content = fs.readFileSync(appJsonPath, "utf-8");
|
|
2728
|
-
config = JSON.parse(content);
|
|
2729
|
-
resultLines.push(`\nSource: app.json`);
|
|
2730
|
-
}
|
|
2731
|
-
catch (err) {
|
|
2732
|
-
resultLines.push(`\n❌ Error reading app.json: ${err}`);
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
else if (fs.existsSync(appConfigJsPath)) {
|
|
2736
|
-
resultLines.push(`\nSource: app.config.js (dynamic config)`);
|
|
2737
|
-
// Can't easily evaluate JS config, suggest using expo config
|
|
2738
|
-
try {
|
|
2739
|
-
const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
|
|
2740
|
-
config = JSON.parse(stdout);
|
|
2741
|
-
}
|
|
2742
|
-
catch {
|
|
2743
|
-
resultLines.push("⚠️ Run 'npx expo config' to see resolved config");
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
else if (fs.existsSync(appConfigTsPath)) {
|
|
2747
|
-
resultLines.push(`\nSource: app.config.ts (TypeScript config)`);
|
|
2748
|
-
try {
|
|
2749
|
-
const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
|
|
2750
|
-
config = JSON.parse(stdout);
|
|
2751
|
-
}
|
|
2752
|
-
catch {
|
|
2753
|
-
resultLines.push("⚠️ Run 'npx expo config' to see resolved config");
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
else {
|
|
2757
|
-
resultLines.push(`\n❌ No Expo config found in: ${cwd}`);
|
|
2758
|
-
resultLines.push("\nLooking for: app.json, app.config.js, or app.config.ts");
|
|
2759
|
-
let result = resultLines.join("\n");
|
|
2760
|
-
if (check.message)
|
|
2761
|
-
result += `\n\n${check.message}`;
|
|
2762
|
-
return result;
|
|
2763
|
-
}
|
|
2764
|
-
if (config) {
|
|
2765
|
-
const expo = config.expo || config;
|
|
2766
|
-
resultLines.push(`\n📱 App Name: ${expo.name || "N/A"}`);
|
|
2767
|
-
resultLines.push(`📌 Slug: ${expo.slug || "N/A"}`);
|
|
2768
|
-
resultLines.push(`📦 Version: ${expo.version || "N/A"}`);
|
|
2769
|
-
resultLines.push(`🔧 SDK Version: ${expo.sdkVersion || "N/A"}`);
|
|
2770
|
-
if (expo.ios) {
|
|
2771
|
-
resultLines.push(`\n🍎 iOS:`);
|
|
2772
|
-
resultLines.push(` Bundle ID: ${expo.ios.bundleIdentifier || "N/A"}`);
|
|
2773
|
-
if (expo.ios.buildNumber)
|
|
2774
|
-
resultLines.push(` Build: ${expo.ios.buildNumber}`);
|
|
2775
|
-
}
|
|
2776
|
-
if (expo.android) {
|
|
2777
|
-
resultLines.push(`\n🤖 Android:`);
|
|
2778
|
-
resultLines.push(` Package: ${expo.android.package || "N/A"}`);
|
|
2779
|
-
if (expo.android.versionCode)
|
|
2780
|
-
resultLines.push(` Version Code: ${expo.android.versionCode}`);
|
|
2781
|
-
}
|
|
2782
|
-
if (expo.plugins && expo.plugins.length > 0) {
|
|
2783
|
-
resultLines.push(`\n🔌 Plugins (${expo.plugins.length}):`);
|
|
2784
|
-
for (const plugin of expo.plugins.slice(0, 10)) {
|
|
2785
|
-
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
2786
|
-
resultLines.push(` - ${pluginName}`);
|
|
2787
|
-
}
|
|
2788
|
-
if (expo.plugins.length > 10) {
|
|
2789
|
-
resultLines.push(` ... and ${expo.plugins.length - 10} more`);
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
if (expo.extra) {
|
|
2793
|
-
resultLines.push(`\n📎 Extra Config: ${JSON.stringify(expo.extra).substring(0, 100)}...`);
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
let result = resultLines.join("\n");
|
|
2797
|
-
if (check.message)
|
|
2798
|
-
result += `\n\n${check.message}`;
|
|
2799
|
-
return result;
|
|
2800
|
-
}
|
|
2801
|
-
async function expoDevMenu(device) {
|
|
2802
|
-
const check = await requireAdvanced("expo_dev_menu");
|
|
2803
|
-
if (!check.allowed)
|
|
2804
|
-
return check.message;
|
|
2805
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2806
|
-
try {
|
|
2807
|
-
// The developer menu in React Native/Expo is triggered by shaking the device
|
|
2808
|
-
// We can simulate this with a combination of keyboard events or the menu keycode
|
|
2809
|
-
// KEYCODE_MENU = 82 opens the dev menu on most RN/Expo apps
|
|
2810
|
-
await execAsync(`adb ${deviceFlag} shell input keyevent 82`);
|
|
2811
|
-
let result = `✅ Developer menu triggered
|
|
2812
|
-
|
|
2813
|
-
The Expo/React Native developer menu should now be visible.
|
|
2814
|
-
|
|
2815
|
-
Available options typically include:
|
|
2816
|
-
- Reload
|
|
2817
|
-
- Go to Expo Home
|
|
2818
|
-
- Toggle Inspector
|
|
2819
|
-
- Toggle Performance Monitor
|
|
2820
|
-
- Show Element Inspector
|
|
2821
|
-
- Open JS Debugger
|
|
2822
|
-
- Fast Refresh settings`;
|
|
2823
|
-
if (check.message)
|
|
2824
|
-
result += `\n\n${check.message}`;
|
|
2825
|
-
return result;
|
|
2826
|
-
}
|
|
2827
|
-
catch (error) {
|
|
2828
|
-
let result = `Error opening dev menu: ${error.message}\n\nMake sure a device is connected: adb devices`;
|
|
2829
|
-
if (check.message)
|
|
2830
|
-
result += `\n\n${check.message}`;
|
|
2831
|
-
return result;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
async function expoReload(device) {
|
|
2835
|
-
const check = await requireAdvanced("expo_reload");
|
|
2836
|
-
if (!check.allowed)
|
|
2837
|
-
return check.message;
|
|
2838
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2839
|
-
try {
|
|
2840
|
-
// Method 1: Send reload broadcast to Expo/RN
|
|
2841
|
-
// Double-tap R in dev menu typically reloads
|
|
2842
|
-
// KEYCODE_R = 46, but we need to simulate the reload command
|
|
2843
|
-
// First try to use the reload command via adb
|
|
2844
|
-
// Many Expo apps respond to the "RR" double-tap
|
|
2845
|
-
await execAsync(`adb ${deviceFlag} shell input keyevent 82`); // Open menu
|
|
2846
|
-
await new Promise(resolve => setTimeout(resolve, 300)); // Wait for menu
|
|
2847
|
-
await execAsync(`adb ${deviceFlag} shell input text r`); // Press 'r' to reload
|
|
2848
|
-
// Alternative: Send the reload intent directly if app supports it
|
|
2849
|
-
try {
|
|
2850
|
-
// Try sending a reload broadcast (works on some Expo setups)
|
|
2851
|
-
await execAsync(`adb ${deviceFlag} shell am broadcast -a "com.facebook.react.reload"`);
|
|
2852
|
-
}
|
|
2853
|
-
catch {
|
|
2854
|
-
// Not all apps support this broadcast
|
|
2855
|
-
}
|
|
2856
|
-
let result = `🔄 Reload triggered!
|
|
2857
|
-
|
|
2858
|
-
The app should now be reloading its JavaScript bundle.
|
|
2859
|
-
|
|
2860
|
-
If the app didn't reload, try:
|
|
2861
|
-
1. Press 'r' twice quickly in the Metro terminal
|
|
2862
|
-
2. Use expo_dev_menu and select "Reload"
|
|
2863
|
-
3. Shake the device to open dev menu`;
|
|
2864
|
-
if (check.message)
|
|
2865
|
-
result += `\n\n${check.message}`;
|
|
2866
|
-
return result;
|
|
2867
|
-
}
|
|
2868
|
-
catch (error) {
|
|
2869
|
-
let result = `Error triggering reload: ${error.message}\n\nMake sure a device is connected: adb devices`;
|
|
2870
|
-
if (check.message)
|
|
2871
|
-
result += `\n\n${check.message}`;
|
|
2872
|
-
return result;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
async function getEasBuilds(platform = "all", limit = 5) {
|
|
2876
|
-
const check = await requireAdvanced("get_eas_builds");
|
|
2877
|
-
if (!check.allowed)
|
|
2878
|
-
return check.message;
|
|
2879
|
-
const resultLines = [];
|
|
2880
|
-
resultLines.push("🏗️ EAS Build Status");
|
|
2881
|
-
resultLines.push("═".repeat(50));
|
|
2882
|
-
try {
|
|
2883
|
-
// Check if eas-cli is available
|
|
2884
|
-
await execAsync("npx eas-cli --version", { timeout: 5000 });
|
|
2885
|
-
// Get build list
|
|
2886
|
-
const platformFlag = platform !== "all" ? `--platform ${platform}` : "";
|
|
2887
|
-
const { stdout } = await execAsync(`npx eas-cli build:list ${platformFlag} --limit ${limit} --json --non-interactive`, { timeout: 30000 });
|
|
2888
|
-
let builds = [];
|
|
2889
|
-
try {
|
|
2890
|
-
builds = JSON.parse(stdout);
|
|
2891
|
-
}
|
|
2892
|
-
catch {
|
|
2893
|
-
// Sometimes eas-cli outputs extra text before JSON
|
|
2894
|
-
const jsonMatch = stdout.match(/\[[\s\S]*\]/);
|
|
2895
|
-
if (jsonMatch) {
|
|
2896
|
-
builds = JSON.parse(jsonMatch[0]);
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
if (builds.length === 0) {
|
|
2900
|
-
resultLines.push("\nNo builds found.");
|
|
2901
|
-
resultLines.push("\nTo create a build:");
|
|
2902
|
-
resultLines.push(" npx eas-cli build --platform android");
|
|
2903
|
-
resultLines.push(" npx eas-cli build --platform ios");
|
|
2904
|
-
}
|
|
2905
|
-
else {
|
|
2906
|
-
resultLines.push(`\nShowing ${builds.length} most recent builds:\n`);
|
|
2907
|
-
for (const build of builds) {
|
|
2908
|
-
const platformEmoji = build.platform === "ANDROID" ? "🤖" : "🍎";
|
|
2909
|
-
const statusEmoji = build.status === "FINISHED" ? "✅" :
|
|
2910
|
-
build.status === "IN_PROGRESS" ? "🔄" :
|
|
2911
|
-
build.status === "ERRORED" ? "❌" : "⏳";
|
|
2912
|
-
resultLines.push(`${statusEmoji} ${platformEmoji} ${build.platform}`);
|
|
2913
|
-
resultLines.push(` Build ID: ${build.id}`);
|
|
2914
|
-
resultLines.push(` Status: ${build.status}`);
|
|
2915
|
-
resultLines.push(` Profile: ${build.buildProfile || "N/A"}`);
|
|
2916
|
-
resultLines.push(` Started: ${new Date(build.createdAt).toLocaleString()}`);
|
|
2917
|
-
if (build.status === "FINISHED" && build.artifacts?.buildUrl) {
|
|
2918
|
-
resultLines.push(` 📥 Download: ${build.artifacts.buildUrl}`);
|
|
2919
|
-
}
|
|
2920
|
-
if (build.error) {
|
|
2921
|
-
resultLines.push(` ❌ Error: ${build.error.message || build.error}`);
|
|
2922
|
-
}
|
|
2923
|
-
resultLines.push("");
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
catch (error) {
|
|
2928
|
-
if (error.message?.includes("not found") || error.message?.includes("ENOENT")) {
|
|
2929
|
-
resultLines.push("\n❌ EAS CLI not found");
|
|
2930
|
-
resultLines.push("\nInstall with: npm install -g eas-cli");
|
|
2931
|
-
resultLines.push("Then login: npx eas-cli login");
|
|
2932
|
-
}
|
|
2933
|
-
else if (error.message?.includes("not logged in") || error.message?.includes("AUTH")) {
|
|
2934
|
-
resultLines.push("\n❌ Not logged in to EAS");
|
|
2935
|
-
resultLines.push("\nLogin with: npx eas-cli login");
|
|
2936
|
-
}
|
|
2937
|
-
else {
|
|
2938
|
-
resultLines.push(`\n❌ Error: ${error.message}`);
|
|
2939
|
-
resultLines.push("\nMake sure you're in an Expo project directory with eas.json");
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
let result = resultLines.join("\n");
|
|
2943
|
-
if (check.message)
|
|
2944
|
-
result += `\n\n${check.message}`;
|
|
2945
|
-
return result;
|
|
2946
|
-
}
|
|
2947
|
-
// ============================================================================
|
|
2948
|
-
// PERFORMANCE METRICS
|
|
2949
|
-
// ============================================================================
|
|
2950
|
-
async function getCpuUsage(packageName, device) {
|
|
2951
|
-
const check = await requireAdvanced("get_cpu_usage");
|
|
2952
|
-
if (!check.allowed)
|
|
2953
|
-
return check.message;
|
|
2954
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
2955
|
-
try {
|
|
2956
|
-
const resultLines = [];
|
|
2957
|
-
resultLines.push("📊 CPU Usage");
|
|
2958
|
-
resultLines.push("═".repeat(50));
|
|
2959
|
-
// Get overall CPU info
|
|
2960
|
-
const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo`, { maxBuffer: 2 * 1024 * 1024 });
|
|
2961
|
-
// Parse CPU usage
|
|
2962
|
-
const lines = cpuInfo.split("\n");
|
|
2963
|
-
const totalMatch = lines.find(line => line.includes("TOTAL"));
|
|
2964
|
-
if (totalMatch) {
|
|
2965
|
-
resultLines.push(`\n${totalMatch.trim()}`);
|
|
2966
|
-
}
|
|
2967
|
-
// If package specified, filter for that app
|
|
2968
|
-
if (packageName) {
|
|
2969
|
-
resultLines.push(`\n📱 App: ${packageName}`);
|
|
2970
|
-
const appLines = lines.filter(line => line.toLowerCase().includes(packageName.toLowerCase()));
|
|
2971
|
-
if (appLines.length > 0) {
|
|
2972
|
-
appLines.forEach(line => {
|
|
2973
|
-
resultLines.push(` ${line.trim()}`);
|
|
2974
|
-
});
|
|
2975
|
-
}
|
|
2976
|
-
else {
|
|
2977
|
-
resultLines.push(" App not found in CPU stats (may not be running)");
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
else {
|
|
2981
|
-
// Show top processes
|
|
2982
|
-
resultLines.push("\n🔝 Top Processes:");
|
|
2983
|
-
const processLines = lines
|
|
2984
|
-
.filter(line => line.includes("%") && !line.includes("TOTAL"))
|
|
2985
|
-
.slice(0, 10);
|
|
2986
|
-
processLines.forEach(line => {
|
|
2987
|
-
resultLines.push(` ${line.trim()}`);
|
|
2988
|
-
});
|
|
2989
|
-
}
|
|
2990
|
-
// Get per-core usage
|
|
2991
|
-
try {
|
|
2992
|
-
const { stdout: coreInfo } = await execAsync(`adb ${deviceFlag} shell cat /proc/stat`, { timeout: 5000 });
|
|
2993
|
-
const coreLines = coreInfo.split("\n").filter(line => line.startsWith("cpu"));
|
|
2994
|
-
if (coreLines.length > 1) {
|
|
2995
|
-
resultLines.push(`\n💻 CPU Cores: ${coreLines.length - 1}`);
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
catch {
|
|
2999
|
-
// Core info not available
|
|
3000
|
-
}
|
|
3001
|
-
let result = resultLines.join("\n");
|
|
3002
|
-
if (check.message)
|
|
3003
|
-
result += `\n\n${check.message}`;
|
|
3004
|
-
return result;
|
|
3005
|
-
}
|
|
3006
|
-
catch (error) {
|
|
3007
|
-
let result = `Error getting CPU usage: ${error.message}`;
|
|
3008
|
-
if (check.message)
|
|
3009
|
-
result += `\n\n${check.message}`;
|
|
3010
|
-
return result;
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
async function getMemoryUsage(packageName, device) {
|
|
3014
|
-
const check = await requireAdvanced("get_memory_usage");
|
|
3015
|
-
if (!check.allowed)
|
|
3016
|
-
return check.message;
|
|
3017
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
3018
|
-
try {
|
|
3019
|
-
const resultLines = [];
|
|
3020
|
-
resultLines.push("🧠 Memory Usage");
|
|
3021
|
-
resultLines.push("═".repeat(50));
|
|
3022
|
-
resultLines.push(`\n📱 App: ${packageName}\n`);
|
|
3023
|
-
// Get detailed memory info for the app
|
|
3024
|
-
const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
|
|
3025
|
-
// Parse key memory metrics
|
|
3026
|
-
const lines = memInfo.split("\n");
|
|
3027
|
-
// Find summary section
|
|
3028
|
-
const summaryStart = lines.findIndex(line => line.includes("App Summary"));
|
|
3029
|
-
if (summaryStart !== -1) {
|
|
3030
|
-
resultLines.push("📋 App Summary:");
|
|
3031
|
-
for (let i = summaryStart + 1; i < Math.min(summaryStart + 10, lines.length); i++) {
|
|
3032
|
-
const line = lines[i].trim();
|
|
3033
|
-
if (line && !line.startsWith("--")) {
|
|
3034
|
-
resultLines.push(` ${line}`);
|
|
3035
|
-
}
|
|
3036
|
-
if (line.includes("TOTAL"))
|
|
3037
|
-
break;
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
// Find PSS and heap info
|
|
3041
|
-
const pssMatch = memInfo.match(/TOTAL PSS:\s+(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
|
|
3042
|
-
const heapMatch = memInfo.match(/Native Heap:\s+(\d+)/);
|
|
3043
|
-
const javaHeapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
|
|
3044
|
-
if (pssMatch || heapMatch) {
|
|
3045
|
-
resultLines.push("\n📊 Key Metrics:");
|
|
3046
|
-
if (pssMatch) {
|
|
3047
|
-
const pssKb = parseInt(pssMatch[1]);
|
|
3048
|
-
resultLines.push(` Total PSS: ${(pssKb / 1024).toFixed(1)} MB`);
|
|
3049
|
-
}
|
|
3050
|
-
if (javaHeapMatch) {
|
|
3051
|
-
const heapKb = parseInt(javaHeapMatch[1]);
|
|
3052
|
-
resultLines.push(` Java Heap: ${(heapKb / 1024).toFixed(1)} MB`);
|
|
3053
|
-
}
|
|
3054
|
-
if (heapMatch) {
|
|
3055
|
-
const nativeKb = parseInt(heapMatch[1]);
|
|
3056
|
-
resultLines.push(` Native Heap: ${(nativeKb / 1024).toFixed(1)} MB`);
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
// Memory warnings
|
|
3060
|
-
if (memInfo.includes("Low memory")) {
|
|
3061
|
-
resultLines.push("\n⚠️ Warning: Device is in low memory state");
|
|
3062
|
-
}
|
|
3063
|
-
let result = resultLines.join("\n");
|
|
3064
|
-
if (check.message)
|
|
3065
|
-
result += `\n\n${check.message}`;
|
|
3066
|
-
return result;
|
|
342
|
+
];
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// TOOL IMPLEMENTATIONS
|
|
345
|
+
// ============================================================================
|
|
346
|
+
async function handleTool(name, args, tier) {
|
|
347
|
+
// Check tool access
|
|
348
|
+
if (!canAccessTool(name, tier)) {
|
|
349
|
+
return {
|
|
350
|
+
content: [{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: `This tool requires ADVANCED tier ($18/mo). Your tier: ${tier.toUpperCase()}. Upgrade at https://mobiledevmcp.dev/pricing`,
|
|
353
|
+
}],
|
|
354
|
+
};
|
|
3067
355
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
}
|
|
3075
|
-
let result = `Error getting memory usage: ${error.message}`;
|
|
3076
|
-
if (check.message)
|
|
3077
|
-
result += `\n\n${check.message}`;
|
|
3078
|
-
return result;
|
|
356
|
+
const device = args.device;
|
|
357
|
+
// Validate device ID if provided (security: prevent shell injection)
|
|
358
|
+
if (device && !validateDeviceId(device)) {
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text: "Invalid device ID format. Device IDs should be alphanumeric with dashes, colons, or periods." }],
|
|
361
|
+
};
|
|
3079
362
|
}
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
if (reset) {
|
|
3093
|
-
await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName} reset`);
|
|
3094
|
-
resultLines.push("📊 Stats reset. Interact with the app, then run this tool again.\n");
|
|
3095
|
-
}
|
|
3096
|
-
// Get graphics info
|
|
3097
|
-
const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
|
|
3098
|
-
const lines = gfxInfo.split("\n");
|
|
3099
|
-
// Parse total frames
|
|
3100
|
-
const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
|
|
3101
|
-
const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
|
|
3102
|
-
const percentile50 = gfxInfo.match(/50th percentile:\s*(\d+)ms/);
|
|
3103
|
-
const percentile90 = gfxInfo.match(/90th percentile:\s*(\d+)ms/);
|
|
3104
|
-
const percentile95 = gfxInfo.match(/95th percentile:\s*(\d+)ms/);
|
|
3105
|
-
const percentile99 = gfxInfo.match(/99th percentile:\s*(\d+)ms/);
|
|
3106
|
-
if (totalMatch) {
|
|
3107
|
-
const totalFrames = parseInt(totalMatch[1]);
|
|
3108
|
-
resultLines.push(`📈 Total Frames: ${totalFrames}`);
|
|
3109
|
-
if (jankyMatch) {
|
|
3110
|
-
const jankyFrames = parseInt(jankyMatch[1]);
|
|
3111
|
-
const jankyPercent = jankyMatch[2];
|
|
3112
|
-
resultLines.push(`🐌 Janky Frames: ${jankyFrames} (${jankyPercent})`);
|
|
3113
|
-
// Calculate smooth FPS estimate
|
|
3114
|
-
const smoothFrames = totalFrames - jankyFrames;
|
|
3115
|
-
const smoothPercent = ((smoothFrames / totalFrames) * 100).toFixed(1);
|
|
3116
|
-
resultLines.push(`✨ Smooth Frames: ${smoothFrames} (${smoothPercent}%)`);
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
// Frame timing percentiles
|
|
3120
|
-
if (percentile50 || percentile90) {
|
|
3121
|
-
resultLines.push("\n⏱️ Frame Time Percentiles:");
|
|
3122
|
-
if (percentile50)
|
|
3123
|
-
resultLines.push(` 50th: ${percentile50[1]}ms`);
|
|
3124
|
-
if (percentile90)
|
|
3125
|
-
resultLines.push(` 90th: ${percentile90[1]}ms`);
|
|
3126
|
-
if (percentile95)
|
|
3127
|
-
resultLines.push(` 95th: ${percentile95[1]}ms`);
|
|
3128
|
-
if (percentile99)
|
|
3129
|
-
resultLines.push(` 99th: ${percentile99[1]}ms`);
|
|
3130
|
-
}
|
|
3131
|
-
// Look for slow frames breakdown
|
|
3132
|
-
const slowUIMatch = gfxInfo.match(/Number Slow UI thread:\s*(\d+)/);
|
|
3133
|
-
const slowBitmapMatch = gfxInfo.match(/Number Slow bitmap uploads:\s*(\d+)/);
|
|
3134
|
-
const slowDrawMatch = gfxInfo.match(/Number Slow issue draw commands:\s*(\d+)/);
|
|
3135
|
-
if (slowUIMatch || slowBitmapMatch || slowDrawMatch) {
|
|
3136
|
-
resultLines.push("\n🔍 Slow Frame Analysis:");
|
|
3137
|
-
if (slowUIMatch && parseInt(slowUIMatch[1]) > 0) {
|
|
3138
|
-
resultLines.push(` Slow UI thread: ${slowUIMatch[1]}`);
|
|
3139
|
-
}
|
|
3140
|
-
if (slowBitmapMatch && parseInt(slowBitmapMatch[1]) > 0) {
|
|
3141
|
-
resultLines.push(` Slow bitmap uploads: ${slowBitmapMatch[1]}`);
|
|
363
|
+
const deviceArg = device ? `-s ${device}` : "";
|
|
364
|
+
switch (name) {
|
|
365
|
+
// === SCREENSHOTS ===
|
|
366
|
+
case "screenshot_emulator": {
|
|
367
|
+
try {
|
|
368
|
+
const base64 = await captureAndroidScreenshot(device);
|
|
369
|
+
return {
|
|
370
|
+
content: [
|
|
371
|
+
{ type: "text", text: "Screenshot captured successfully" },
|
|
372
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
373
|
+
],
|
|
374
|
+
};
|
|
3142
375
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
376
|
+
catch (error) {
|
|
377
|
+
return { content: [{ type: "text", text: `Failed to capture screenshot: ${error.message}` }] };
|
|
3145
378
|
}
|
|
3146
379
|
}
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
const
|
|
3150
|
-
if (
|
|
3151
|
-
|
|
3152
|
-
|
|
380
|
+
case "screenshot_ios_simulator": {
|
|
381
|
+
// Validate UDID if provided (defense in depth - also validated in captureIosScreenshot)
|
|
382
|
+
const iosUdid = args.udid;
|
|
383
|
+
if (iosUdid && !validateUdid(iosUdid)) {
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: "Invalid iOS Simulator UDID format. Must be UUID format or 'booted'." }],
|
|
386
|
+
};
|
|
3153
387
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
388
|
+
try {
|
|
389
|
+
const base64 = await captureIosScreenshot(iosUdid);
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{ type: "text", text: "iOS screenshot captured successfully" },
|
|
393
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
394
|
+
],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
return { content: [{ type: "text", text: `Failed to capture iOS screenshot: ${error.message}` }] };
|
|
3156
399
|
}
|
|
3157
400
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
resultLines.push("Make sure the app is visible and interact with it first.");
|
|
3161
|
-
}
|
|
3162
|
-
let result = resultLines.join("\n");
|
|
3163
|
-
if (check.message)
|
|
3164
|
-
result += `\n\n${check.message}`;
|
|
3165
|
-
return result;
|
|
3166
|
-
}
|
|
3167
|
-
catch (error) {
|
|
3168
|
-
let result = `Error getting FPS stats: ${error.message}`;
|
|
3169
|
-
if (check.message)
|
|
3170
|
-
result += `\n\n${check.message}`;
|
|
3171
|
-
return result;
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
async function getBatteryStats(packageName, device) {
|
|
3175
|
-
const check = await requireAdvanced("get_battery_stats");
|
|
3176
|
-
if (!check.allowed)
|
|
3177
|
-
return check.message;
|
|
3178
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
3179
|
-
try {
|
|
3180
|
-
const resultLines = [];
|
|
3181
|
-
resultLines.push("🔋 Battery Statistics");
|
|
3182
|
-
resultLines.push("═".repeat(50));
|
|
3183
|
-
// Get current battery level and status
|
|
3184
|
-
const { stdout: batteryInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys battery`, { timeout: 5000 });
|
|
3185
|
-
const levelMatch = batteryInfo.match(/level:\s*(\d+)/);
|
|
3186
|
-
const statusMatch = batteryInfo.match(/status:\s*(\d+)/);
|
|
3187
|
-
const healthMatch = batteryInfo.match(/health:\s*(\d+)/);
|
|
3188
|
-
const tempMatch = batteryInfo.match(/temperature:\s*(\d+)/);
|
|
3189
|
-
resultLines.push("\n📊 Current Status:");
|
|
3190
|
-
if (levelMatch) {
|
|
3191
|
-
resultLines.push(` Battery Level: ${levelMatch[1]}%`);
|
|
3192
|
-
}
|
|
3193
|
-
if (statusMatch) {
|
|
3194
|
-
const statuses = ["Unknown", "Charging", "Discharging", "Not charging", "Full"];
|
|
3195
|
-
const status = statuses[parseInt(statusMatch[1])] || "Unknown";
|
|
3196
|
-
resultLines.push(` Status: ${status}`);
|
|
3197
|
-
}
|
|
3198
|
-
if (tempMatch) {
|
|
3199
|
-
const temp = parseInt(tempMatch[1]) / 10;
|
|
3200
|
-
resultLines.push(` Temperature: ${temp}°C`);
|
|
3201
|
-
}
|
|
3202
|
-
// Get battery stats (power consumption)
|
|
3203
|
-
if (packageName) {
|
|
3204
|
-
resultLines.push(`\n📱 App: ${packageName}`);
|
|
401
|
+
// === DEVICE INFO ===
|
|
402
|
+
case "list_devices": {
|
|
3205
403
|
try {
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
resultLines.push(` Power Used: ${powerMatch[1]} mAh`);
|
|
3211
|
-
}
|
|
3212
|
-
// CPU time
|
|
3213
|
-
const cpuMatch = appStats.match(/Total cpu time:\s*u=(\d+)ms\s*s=(\d+)ms/);
|
|
3214
|
-
if (cpuMatch) {
|
|
3215
|
-
const userMs = parseInt(cpuMatch[1]);
|
|
3216
|
-
const sysMs = parseInt(cpuMatch[2]);
|
|
3217
|
-
resultLines.push(` CPU Time: ${((userMs + sysMs) / 1000).toFixed(1)}s (user: ${(userMs / 1000).toFixed(1)}s, sys: ${(sysMs / 1000).toFixed(1)}s)`);
|
|
404
|
+
const devices = await listConnectedDevices();
|
|
405
|
+
const maxDevices = getMaxDevices(tier);
|
|
406
|
+
if (devices.length === 0) {
|
|
407
|
+
return { content: [{ type: "text", text: "No devices connected. Start an emulator or connect a device." }] };
|
|
3218
408
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
// Wakelock time
|
|
3225
|
-
const wakelockMatch = appStats.match(/Wake lock\s+\S+\s+.*?realtime=(\d+)/);
|
|
3226
|
-
if (wakelockMatch) {
|
|
3227
|
-
const wakeSec = parseInt(wakelockMatch[1]) / 1000;
|
|
3228
|
-
if (wakeSec > 0) {
|
|
3229
|
-
resultLines.push(` Wake Lock: ${wakeSec.toFixed(1)}s`);
|
|
3230
|
-
}
|
|
409
|
+
const limited = devices.slice(0, maxDevices);
|
|
410
|
+
let result = `Connected devices (showing ${limited.length}/${devices.length}):\n`;
|
|
411
|
+
result += limited.map((d) => ` ${d.id} - ${d.status}`).join("\n");
|
|
412
|
+
if (devices.length > maxDevices) {
|
|
413
|
+
result += `\n\n[Upgrade to ADVANCED to see all ${devices.length} devices]`;
|
|
3231
414
|
}
|
|
415
|
+
return { content: [{ type: "text", text: result }] };
|
|
3232
416
|
}
|
|
3233
|
-
catch {
|
|
3234
|
-
|
|
417
|
+
catch (error) {
|
|
418
|
+
return { content: [{ type: "text", text: `Failed to list devices: ${error.message}` }] };
|
|
3235
419
|
}
|
|
3236
420
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
421
|
+
case "list_ios_simulators": {
|
|
422
|
+
if (process.platform !== "darwin") {
|
|
423
|
+
return { content: [{ type: "text", text: "iOS Simulators are only available on macOS" }] };
|
|
424
|
+
}
|
|
3240
425
|
try {
|
|
3241
|
-
const { stdout
|
|
3242
|
-
|
|
3243
|
-
const
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
426
|
+
const { stdout } = await execAsync(`${XCRUN} simctl list devices --json`);
|
|
427
|
+
const data = JSON.parse(stdout);
|
|
428
|
+
const onlyBooted = args.onlyBooted;
|
|
429
|
+
let result = "iOS Simulators:\n";
|
|
430
|
+
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
431
|
+
const filteredDevices = onlyBooted
|
|
432
|
+
? devices.filter((d) => d.state === "Booted")
|
|
433
|
+
: devices;
|
|
434
|
+
if (filteredDevices.length > 0) {
|
|
435
|
+
result += `\n${runtime}:\n`;
|
|
436
|
+
for (const device of filteredDevices) {
|
|
437
|
+
result += ` ${device.name} (${device.udid}) - ${device.state}\n`;
|
|
3250
438
|
}
|
|
3251
|
-
}
|
|
439
|
+
}
|
|
3252
440
|
}
|
|
441
|
+
return { content: [{ type: "text", text: result }] };
|
|
3253
442
|
}
|
|
3254
|
-
catch {
|
|
3255
|
-
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
let result = resultLines.join("\n");
|
|
3259
|
-
if (check.message)
|
|
3260
|
-
result += `\n\n${check.message}`;
|
|
3261
|
-
return result;
|
|
3262
|
-
}
|
|
3263
|
-
catch (error) {
|
|
3264
|
-
let result = `Error getting battery stats: ${error.message}`;
|
|
3265
|
-
if (check.message)
|
|
3266
|
-
result += `\n\n${check.message}`;
|
|
3267
|
-
return result;
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
async function getPerformanceSnapshot(packageName, device) {
|
|
3271
|
-
const check = await requireAdvanced("get_performance_snapshot");
|
|
3272
|
-
if (!check.allowed)
|
|
3273
|
-
return check.message;
|
|
3274
|
-
const deviceFlag = device ? `-s ${device}` : "";
|
|
3275
|
-
try {
|
|
3276
|
-
const resultLines = [];
|
|
3277
|
-
resultLines.push("📊 Performance Snapshot");
|
|
3278
|
-
resultLines.push("═".repeat(50));
|
|
3279
|
-
resultLines.push(`\n📱 App: ${packageName}`);
|
|
3280
|
-
resultLines.push(`⏰ Time: ${new Date().toLocaleString()}\n`);
|
|
3281
|
-
// CPU Usage
|
|
3282
|
-
try {
|
|
3283
|
-
const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo | grep -i "${packageName}"`, { timeout: 5000 });
|
|
3284
|
-
if (cpuInfo.trim()) {
|
|
3285
|
-
resultLines.push("💻 CPU:");
|
|
3286
|
-
cpuInfo.split("\n").slice(0, 3).forEach(line => {
|
|
3287
|
-
if (line.trim())
|
|
3288
|
-
resultLines.push(` ${line.trim()}`);
|
|
3289
|
-
});
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
catch {
|
|
3293
|
-
resultLines.push("💻 CPU: N/A");
|
|
3294
|
-
}
|
|
3295
|
-
// Memory Usage
|
|
3296
|
-
try {
|
|
3297
|
-
const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024, timeout: 5000 });
|
|
3298
|
-
const pssMatch = memInfo.match(/TOTAL PSS:\s*(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
|
|
3299
|
-
if (pssMatch) {
|
|
3300
|
-
const pssMb = (parseInt(pssMatch[1]) / 1024).toFixed(1);
|
|
3301
|
-
resultLines.push(`\n🧠 Memory: ${pssMb} MB (PSS)`);
|
|
3302
|
-
}
|
|
3303
|
-
const heapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
|
|
3304
|
-
const nativeMatch = memInfo.match(/Native Heap:\s+(\d+)/);
|
|
3305
|
-
if (heapMatch) {
|
|
3306
|
-
resultLines.push(` Java Heap: ${(parseInt(heapMatch[1]) / 1024).toFixed(1)} MB`);
|
|
3307
|
-
}
|
|
3308
|
-
if (nativeMatch) {
|
|
3309
|
-
resultLines.push(` Native: ${(parseInt(nativeMatch[1]) / 1024).toFixed(1)} MB`);
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
catch {
|
|
3313
|
-
resultLines.push("\n🧠 Memory: App not running or no data");
|
|
3314
|
-
}
|
|
3315
|
-
// FPS Stats
|
|
3316
|
-
try {
|
|
3317
|
-
const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 1024 * 1024, timeout: 5000 });
|
|
3318
|
-
const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
|
|
3319
|
-
const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
|
|
3320
|
-
if (totalMatch) {
|
|
3321
|
-
resultLines.push(`\n🎮 Frames: ${totalMatch[1]} rendered`);
|
|
3322
|
-
if (jankyMatch) {
|
|
3323
|
-
resultLines.push(` Janky: ${jankyMatch[1]} (${jankyMatch[2]})`);
|
|
3324
|
-
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
return { content: [{ type: "text", text: `Failed to list simulators: ${error.message}` }] };
|
|
3325
445
|
}
|
|
3326
446
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
447
|
+
case "get_device_info": {
|
|
448
|
+
try {
|
|
449
|
+
const props = [
|
|
450
|
+
"ro.product.model",
|
|
451
|
+
"ro.build.version.release",
|
|
452
|
+
"ro.build.version.sdk",
|
|
453
|
+
"ro.product.manufacturer",
|
|
454
|
+
];
|
|
455
|
+
let result = "Device Information:\n";
|
|
456
|
+
for (const prop of props) {
|
|
457
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} shell getprop ${prop}`);
|
|
458
|
+
result += ` ${prop}: ${stdout.trim()}\n`;
|
|
3339
459
|
}
|
|
460
|
+
// Screen size
|
|
461
|
+
const { stdout: screenSize } = await execAsync(`${ADB} ${deviceArg} shell wm size`);
|
|
462
|
+
result += ` Screen: ${screenSize.trim()}\n`;
|
|
463
|
+
// Memory (avoid shell piping - process in Node.js)
|
|
464
|
+
const { stdout: memInfo } = await execAsync(`${ADB} ${deviceArg} shell cat /proc/meminfo`);
|
|
465
|
+
const memLines = memInfo.split("\n").slice(0, 3); // Limit to first 3 lines in Node.js
|
|
466
|
+
result += ` Memory:\n${memLines.map((l) => ` ${l}`).join("\n")}`;
|
|
467
|
+
return { content: [{ type: "text", text: result }] };
|
|
3340
468
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
resultLines.push("\n🔋 Battery: N/A");
|
|
3344
|
-
}
|
|
3345
|
-
// Network usage from app
|
|
3346
|
-
try {
|
|
3347
|
-
const { stdout: netStats } = await execAsync(`adb ${deviceFlag} shell cat /proc/net/xt_qtaguid/stats`, { timeout: 3000 });
|
|
3348
|
-
// Just note that network is available
|
|
3349
|
-
if (netStats.includes(packageName)) {
|
|
3350
|
-
resultLines.push("\n📡 Network: Active");
|
|
469
|
+
catch (error) {
|
|
470
|
+
return { content: [{ type: "text", text: `Failed to get device info: ${error.message}` }] };
|
|
3351
471
|
}
|
|
3352
472
|
}
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
resultLines.push("\n" + "─".repeat(50));
|
|
3357
|
-
resultLines.push("Use individual tools for detailed metrics:");
|
|
3358
|
-
resultLines.push(" get_cpu_usage, get_memory_usage, get_fps_stats, get_battery_stats");
|
|
3359
|
-
let result = resultLines.join("\n");
|
|
3360
|
-
if (check.message)
|
|
3361
|
-
result += `\n\n${check.message}`;
|
|
3362
|
-
return result;
|
|
3363
|
-
}
|
|
3364
|
-
catch (error) {
|
|
3365
|
-
let result = `Error getting performance snapshot: ${error.message}`;
|
|
3366
|
-
if (check.message)
|
|
3367
|
-
result += `\n\n${check.message}`;
|
|
3368
|
-
return result;
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
// ============================================================================
|
|
3372
|
-
// MCP SERVER SETUP
|
|
3373
|
-
// ============================================================================
|
|
3374
|
-
const server = new Server({
|
|
3375
|
-
name: "claude-mobile-dev-mcp",
|
|
3376
|
-
version: "0.1.0",
|
|
3377
|
-
}, {
|
|
3378
|
-
capabilities: {
|
|
3379
|
-
tools: {},
|
|
3380
|
-
},
|
|
3381
|
-
});
|
|
3382
|
-
// Handle tool listing
|
|
3383
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3384
|
-
tools,
|
|
3385
|
-
}));
|
|
3386
|
-
// Handle tool execution
|
|
3387
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3388
|
-
const { name, arguments: args } = request.params;
|
|
3389
|
-
try {
|
|
3390
|
-
// License tools
|
|
3391
|
-
if (name === "get_license_status" || name === "set_license_key") {
|
|
3392
|
-
const result = await handleLicenseTool(name, args || {});
|
|
3393
|
-
return { content: [{ type: "text", text: result }] };
|
|
3394
|
-
}
|
|
3395
|
-
// Core tools
|
|
3396
|
-
switch (name) {
|
|
3397
|
-
case "get_metro_logs": {
|
|
3398
|
-
const result = await getMetroLogs(args?.lines, args?.filter);
|
|
3399
|
-
return { content: [{ type: "text", text: result }] };
|
|
473
|
+
case "get_ios_simulator_info": {
|
|
474
|
+
if (process.platform !== "darwin") {
|
|
475
|
+
return { content: [{ type: "text", text: "iOS Simulators only available on macOS" }] };
|
|
3400
476
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
477
|
+
// Validate UDID if provided
|
|
478
|
+
const simUdid = args.udid || "booted";
|
|
479
|
+
if (simUdid !== "booted" && !validateUdid(simUdid)) {
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text: "Invalid iOS Simulator UDID format. Must be UUID format or 'booted'." }],
|
|
482
|
+
};
|
|
3404
483
|
}
|
|
3405
|
-
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
484
|
+
try {
|
|
485
|
+
const udid = simUdid;
|
|
486
|
+
const { stdout } = await execAsync(`${XCRUN} simctl list devices --json`);
|
|
487
|
+
const data = JSON.parse(stdout);
|
|
488
|
+
for (const devices of Object.values(data.devices)) {
|
|
489
|
+
for (const device of devices) {
|
|
490
|
+
if (device.udid === udid || (udid === "booted" && device.state === "Booted")) {
|
|
491
|
+
return {
|
|
492
|
+
content: [{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: JSON.stringify(device, null, 2),
|
|
495
|
+
}],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
3418
498
|
}
|
|
3419
|
-
return { content };
|
|
3420
499
|
}
|
|
3421
|
-
return { content: [{ type: "text", text:
|
|
3422
|
-
}
|
|
3423
|
-
case "list_devices": {
|
|
3424
|
-
const result = await listDevices();
|
|
3425
|
-
return { content: [{ type: "text", text: result }] };
|
|
500
|
+
return { content: [{ type: "text", text: "Simulator not found" }] };
|
|
3426
501
|
}
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
return { content: [{ type: "text", text: result }] };
|
|
502
|
+
catch (error) {
|
|
503
|
+
return { content: [{ type: "text", text: `Failed to get simulator info: ${error.message}` }] };
|
|
3430
504
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
505
|
+
}
|
|
506
|
+
case "get_app_info": {
|
|
507
|
+
const packageName = args.packageName;
|
|
508
|
+
// Validate package name (security: prevent shell injection)
|
|
509
|
+
if (!validatePackageName(packageName)) {
|
|
510
|
+
return {
|
|
511
|
+
content: [{ type: "text", text: "Invalid package name format. Package names should be like 'com.example.app'." }],
|
|
512
|
+
};
|
|
3434
513
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
514
|
+
try {
|
|
515
|
+
// Quote package name and avoid shell piping - process in Node.js
|
|
516
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} shell dumpsys package "${packageName}"`);
|
|
517
|
+
const outputLines = stdout.split("\n").slice(0, 50).join("\n"); // Limit to 50 lines in Node.js
|
|
518
|
+
return { content: [{ type: "text", text: outputLines }] };
|
|
3438
519
|
}
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
return { content: [{ type: "text", text: result }] };
|
|
520
|
+
catch (error) {
|
|
521
|
+
return { content: [{ type: "text", text: `Failed to get app info: ${error.message}` }] };
|
|
3442
522
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
523
|
+
}
|
|
524
|
+
// === LOGS ===
|
|
525
|
+
case "get_metro_logs": {
|
|
526
|
+
const requestedLines = args.lines || 50;
|
|
527
|
+
const maxLines = getMaxLogLines(tier);
|
|
528
|
+
// Ensure lines is a positive integer
|
|
529
|
+
const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
|
|
530
|
+
try {
|
|
531
|
+
const port = CONFIG.metroPort;
|
|
532
|
+
const { stdout } = await execAsync(`curl -s http://localhost:${port}/status`, { timeout: 2000 });
|
|
533
|
+
let result = `Metro Status (port ${port}): ${stdout}\n`;
|
|
534
|
+
result += `\n[Metro logs require 'start_metro_logging' to capture output]`;
|
|
535
|
+
if (lines < requestedLines) {
|
|
536
|
+
result += `\n[Showing ${lines} lines, upgrade to ADVANCED for more]`;
|
|
537
|
+
}
|
|
3445
538
|
return { content: [{ type: "text", text: result }] };
|
|
3446
539
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
540
|
+
catch {
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: "text",
|
|
544
|
+
text: `Metro not responding on port ${CONFIG.metroPort}. Is it running?`,
|
|
545
|
+
}],
|
|
546
|
+
};
|
|
3450
547
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
548
|
+
}
|
|
549
|
+
case "get_adb_logs": {
|
|
550
|
+
const requestedLines = args.lines || 50;
|
|
551
|
+
const maxLines = getMaxLogLines(tier);
|
|
552
|
+
// Ensure lines is a positive integer (security: prevent injection via negative/float values)
|
|
553
|
+
const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
|
|
554
|
+
const filter = args.filter || "ReactNativeJS";
|
|
555
|
+
const level = args.level || "I";
|
|
556
|
+
// Validate filter (security: prevent shell injection)
|
|
557
|
+
if (!validateLogFilter(filter)) {
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: "text", text: "Invalid log filter format. Filters should be alphanumeric with underscores (e.g., 'ReactNativeJS', '*')." }],
|
|
560
|
+
};
|
|
3454
561
|
}
|
|
3455
|
-
//
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
562
|
+
// Validate level
|
|
563
|
+
if (!validateLogLevel(level)) {
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: "Invalid log level. Use one of: V, D, I, W, E, F." }],
|
|
566
|
+
};
|
|
3459
567
|
}
|
|
3460
|
-
|
|
3461
|
-
const
|
|
568
|
+
try {
|
|
569
|
+
const filterArg = filter === "*" ? "" : `-s ${filter}:${level}`;
|
|
570
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} logcat -d ${filterArg} -t ${lines}`, { timeout: 5000 });
|
|
571
|
+
let result = stdout || "No logs found";
|
|
572
|
+
if (lines < requestedLines) {
|
|
573
|
+
result += `\n\n[Showing ${lines} lines, upgrade to ADVANCED for more]`;
|
|
574
|
+
}
|
|
3462
575
|
return { content: [{ type: "text", text: result }] };
|
|
3463
576
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
return { content: [{ type: "text", text: result }] };
|
|
577
|
+
catch (error) {
|
|
578
|
+
return { content: [{ type: "text", text: `Failed to get logs: ${error.message}` }] };
|
|
3467
579
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
580
|
+
}
|
|
581
|
+
case "get_ios_simulator_logs": {
|
|
582
|
+
if (process.platform !== "darwin") {
|
|
583
|
+
return { content: [{ type: "text", text: "iOS Simulator logs only available on macOS" }] };
|
|
3471
584
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
585
|
+
const requestedLines = args.lines || 50;
|
|
586
|
+
const maxLines = getMaxLogLines(tier);
|
|
587
|
+
// Ensure lines is a safe positive integer (security: prevent tail injection)
|
|
588
|
+
const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
|
|
589
|
+
try {
|
|
590
|
+
// Avoid shell piping - process in Node.js
|
|
591
|
+
const { stdout } = await execAsync(`log show --predicate 'subsystem CONTAINS "com.apple.CoreSimulator"' --last 5m --style compact`, { timeout: 10000 });
|
|
592
|
+
// Limit output lines in Node.js instead of shell tail
|
|
593
|
+
const outputLines = stdout.split("\n").slice(-lines).join("\n");
|
|
594
|
+
return { content: [{ type: "text", text: outputLines || "No recent logs found" }] };
|
|
3475
595
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
const result = await tapScreen(args?.x, args?.y, args?.device);
|
|
3479
|
-
return { content: [{ type: "text", text: result }] };
|
|
596
|
+
catch (error) {
|
|
597
|
+
return { content: [{ type: "text", text: `Failed to get iOS logs: ${error.message}` }] };
|
|
3480
598
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
599
|
+
}
|
|
600
|
+
case "check_metro_status": {
|
|
601
|
+
const port = args.port || CONFIG.metroPort;
|
|
602
|
+
// Validate port (security: prevent injection via port number)
|
|
603
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: "Invalid port number. Port must be between 1 and 65535." }],
|
|
606
|
+
};
|
|
3484
607
|
}
|
|
3485
|
-
|
|
3486
|
-
const
|
|
3487
|
-
return {
|
|
608
|
+
try {
|
|
609
|
+
const { stdout } = await execAsync(`curl -s http://localhost:${port}/status`, { timeout: 2000 });
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: `Metro bundler is running on port ${port}\nStatus: ${stdout}`,
|
|
614
|
+
}],
|
|
615
|
+
};
|
|
3488
616
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
617
|
+
catch {
|
|
618
|
+
return {
|
|
619
|
+
content: [{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: `Metro bundler is NOT running on port ${port}`,
|
|
622
|
+
}],
|
|
623
|
+
};
|
|
3492
624
|
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
625
|
+
}
|
|
626
|
+
// === UI TREE (ADVANCED) ===
|
|
627
|
+
case "get_ui_tree": {
|
|
628
|
+
try {
|
|
629
|
+
const compressed = args.compressed !== false;
|
|
630
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
631
|
+
const elements = parseUiTree(stdout);
|
|
632
|
+
const filtered = compressed
|
|
633
|
+
? elements.filter((el) => el.clickable || el.text || el.contentDescription)
|
|
634
|
+
: elements;
|
|
635
|
+
return {
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: JSON.stringify({
|
|
639
|
+
elementCount: filtered.length,
|
|
640
|
+
elements: filtered.map((el) => ({
|
|
641
|
+
text: el.text,
|
|
642
|
+
resourceId: el.resourceId,
|
|
643
|
+
className: el.className.split(".").pop(),
|
|
644
|
+
contentDescription: el.contentDescription,
|
|
645
|
+
bounds: el.bounds,
|
|
646
|
+
clickable: el.clickable,
|
|
647
|
+
center: el.centerX && el.centerY ? { x: el.centerX, y: el.centerY } : undefined,
|
|
648
|
+
})),
|
|
649
|
+
}, null, 2),
|
|
650
|
+
}],
|
|
651
|
+
};
|
|
3496
652
|
}
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
return { content: [{ type: "text", text: result }] };
|
|
653
|
+
catch (error) {
|
|
654
|
+
return { content: [{ type: "text", text: `Failed to get UI tree: ${error.message}` }] };
|
|
3500
655
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
656
|
+
}
|
|
657
|
+
case "find_element": {
|
|
658
|
+
// Validate search parameters have reasonable length (prevent DoS)
|
|
659
|
+
const MAX_SEARCH_LEN = 500;
|
|
660
|
+
const searchText = args.text;
|
|
661
|
+
const searchResourceId = args.resourceId;
|
|
662
|
+
const searchContentDesc = args.contentDescription;
|
|
663
|
+
if ((searchText && searchText.length > MAX_SEARCH_LEN) ||
|
|
664
|
+
(searchResourceId && searchResourceId.length > MAX_SEARCH_LEN) ||
|
|
665
|
+
(searchContentDesc && searchContentDesc.length > MAX_SEARCH_LEN)) {
|
|
666
|
+
return { content: [{ type: "text", text: "Search parameters too long (max 500 chars)" }] };
|
|
3505
667
|
}
|
|
3506
|
-
|
|
3507
|
-
const
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
668
|
+
try {
|
|
669
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
670
|
+
const elements = parseUiTree(stdout);
|
|
671
|
+
const found = findElementInTree(elements, {
|
|
672
|
+
text: searchText,
|
|
673
|
+
resourceId: searchResourceId,
|
|
674
|
+
contentDescription: searchContentDesc,
|
|
675
|
+
});
|
|
676
|
+
if (found) {
|
|
677
|
+
return {
|
|
678
|
+
content: [{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: JSON.stringify({
|
|
681
|
+
found: true,
|
|
682
|
+
element: {
|
|
683
|
+
text: found.text,
|
|
684
|
+
resourceId: found.resourceId,
|
|
685
|
+
className: found.className,
|
|
686
|
+
contentDescription: found.contentDescription,
|
|
687
|
+
bounds: found.bounds,
|
|
688
|
+
center: { x: found.centerX, y: found.centerY },
|
|
689
|
+
clickable: found.clickable,
|
|
690
|
+
enabled: found.enabled,
|
|
691
|
+
},
|
|
692
|
+
}, null, 2),
|
|
693
|
+
}],
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return { content: [{ type: "text", text: JSON.stringify({ found: false }) }] };
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
return { content: [{ type: "text", text: `Failed to find element: ${error.message}` }] };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
case "wait_for_element": {
|
|
703
|
+
// Cap timeout at 60 seconds to prevent indefinite blocking
|
|
704
|
+
const MAX_TIMEOUT = 60000;
|
|
705
|
+
const requestedTimeout = args.timeout || 5000;
|
|
706
|
+
const timeout = Math.max(1000, Math.min(requestedTimeout, MAX_TIMEOUT));
|
|
707
|
+
const pollInterval = 500;
|
|
708
|
+
const startTime = Date.now();
|
|
709
|
+
// Validate search parameters have reasonable length (prevent DoS)
|
|
710
|
+
const MAX_SEARCH_LEN = 500;
|
|
711
|
+
const searchText = args.text;
|
|
712
|
+
const searchResourceId = args.resourceId;
|
|
713
|
+
const searchContentDesc = args.contentDescription;
|
|
714
|
+
if ((searchText && searchText.length > MAX_SEARCH_LEN) ||
|
|
715
|
+
(searchResourceId && searchResourceId.length > MAX_SEARCH_LEN) ||
|
|
716
|
+
(searchContentDesc && searchContentDesc.length > MAX_SEARCH_LEN)) {
|
|
717
|
+
return { content: [{ type: "text", text: "Search parameters too long (max 500 chars)" }] };
|
|
718
|
+
}
|
|
719
|
+
while (Date.now() - startTime < timeout) {
|
|
720
|
+
try {
|
|
721
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 5000 });
|
|
722
|
+
const elements = parseUiTree(stdout);
|
|
723
|
+
const found = findElementInTree(elements, {
|
|
724
|
+
text: searchText,
|
|
725
|
+
resourceId: searchResourceId,
|
|
726
|
+
contentDescription: searchContentDesc,
|
|
727
|
+
});
|
|
728
|
+
if (found) {
|
|
729
|
+
return {
|
|
730
|
+
content: [{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: JSON.stringify({
|
|
733
|
+
found: true,
|
|
734
|
+
waitTime: Date.now() - startTime,
|
|
735
|
+
element: {
|
|
736
|
+
text: found.text,
|
|
737
|
+
center: { x: found.centerX, y: found.centerY },
|
|
738
|
+
},
|
|
739
|
+
}, null, 2),
|
|
740
|
+
}],
|
|
741
|
+
};
|
|
3518
742
|
}
|
|
3519
|
-
return { content };
|
|
3520
743
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
return { content: [{ type: "text", text: result }] };
|
|
3526
|
-
}
|
|
3527
|
-
case "get_ios_simulator_info": {
|
|
3528
|
-
const result = await getIosSimulatorInfo(args?.udid);
|
|
3529
|
-
return { content: [{ type: "text", text: result }] };
|
|
3530
|
-
}
|
|
3531
|
-
case "boot_ios_simulator": {
|
|
3532
|
-
const result = await bootIosSimulator(args?.udid);
|
|
3533
|
-
return { content: [{ type: "text", text: result }] };
|
|
3534
|
-
}
|
|
3535
|
-
case "shutdown_ios_simulator": {
|
|
3536
|
-
const result = await shutdownIosSimulator(args?.udid);
|
|
3537
|
-
return { content: [{ type: "text", text: result }] };
|
|
3538
|
-
}
|
|
3539
|
-
case "install_ios_app": {
|
|
3540
|
-
const result = await installIosApp(args?.appPath, args?.udid);
|
|
3541
|
-
return { content: [{ type: "text", text: result }] };
|
|
3542
|
-
}
|
|
3543
|
-
case "launch_ios_app": {
|
|
3544
|
-
const result = await launchIosApp(args?.bundleId, args?.udid);
|
|
3545
|
-
return { content: [{ type: "text", text: result }] };
|
|
3546
|
-
}
|
|
3547
|
-
case "terminate_ios_app": {
|
|
3548
|
-
const result = await terminateIosApp(args?.bundleId, args?.udid);
|
|
3549
|
-
return { content: [{ type: "text", text: result }] };
|
|
3550
|
-
}
|
|
3551
|
-
case "ios_open_url": {
|
|
3552
|
-
const result = await iosOpenUrl(args?.url, args?.udid);
|
|
3553
|
-
return { content: [{ type: "text", text: result }] };
|
|
3554
|
-
}
|
|
3555
|
-
case "ios_push_notification": {
|
|
3556
|
-
const result = await iosPushNotification(args?.bundleId, args?.payload, args?.udid);
|
|
3557
|
-
return { content: [{ type: "text", text: result }] };
|
|
3558
|
-
}
|
|
3559
|
-
case "ios_set_location": {
|
|
3560
|
-
const result = await iosSetLocation(args?.latitude, args?.longitude, args?.udid);
|
|
3561
|
-
return { content: [{ type: "text", text: result }] };
|
|
3562
|
-
}
|
|
3563
|
-
// REACT DEVTOOLS TOOLS
|
|
3564
|
-
case "setup_react_devtools": {
|
|
3565
|
-
const result = await setupReactDevTools(args?.port, args?.device);
|
|
3566
|
-
return { content: [{ type: "text", text: result }] };
|
|
3567
|
-
}
|
|
3568
|
-
case "check_devtools_connection": {
|
|
3569
|
-
const result = await checkDevToolsConnection(args?.port);
|
|
3570
|
-
return { content: [{ type: "text", text: result }] };
|
|
3571
|
-
}
|
|
3572
|
-
case "get_react_component_tree": {
|
|
3573
|
-
const result = await getReactComponentTree(args?.port, args?.depth);
|
|
3574
|
-
return { content: [{ type: "text", text: result }] };
|
|
3575
|
-
}
|
|
3576
|
-
case "inspect_react_component": {
|
|
3577
|
-
const result = await inspectReactComponent(args?.componentId, args?.port);
|
|
3578
|
-
return { content: [{ type: "text", text: result }] };
|
|
3579
|
-
}
|
|
3580
|
-
case "search_react_components": {
|
|
3581
|
-
const result = await searchReactComponents(args?.query, args?.port);
|
|
3582
|
-
return { content: [{ type: "text", text: result }] };
|
|
3583
|
-
}
|
|
3584
|
-
// NETWORK INSPECTION TOOLS
|
|
3585
|
-
case "get_network_requests": {
|
|
3586
|
-
const result = await getNetworkRequests(args?.lines, args?.filter, args?.device);
|
|
3587
|
-
return { content: [{ type: "text", text: result }] };
|
|
744
|
+
catch {
|
|
745
|
+
// Ignore errors during polling
|
|
746
|
+
}
|
|
747
|
+
await sleep(pollInterval);
|
|
3588
748
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
749
|
+
return {
|
|
750
|
+
content: [{
|
|
751
|
+
type: "text",
|
|
752
|
+
text: JSON.stringify({ found: false, timeout: true, waitTime: timeout }),
|
|
753
|
+
}],
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
case "get_element_property":
|
|
757
|
+
case "assert_element": {
|
|
758
|
+
try {
|
|
759
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
760
|
+
const elements = parseUiTree(stdout);
|
|
761
|
+
const found = findElementInTree(elements, {
|
|
762
|
+
text: args.text,
|
|
763
|
+
resourceId: args.resourceId,
|
|
764
|
+
contentDescription: args.contentDescription,
|
|
765
|
+
});
|
|
766
|
+
if (name === "get_element_property") {
|
|
767
|
+
const property = args.property;
|
|
768
|
+
if (found) {
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: "text",
|
|
772
|
+
text: JSON.stringify({ [property]: found[property] }),
|
|
773
|
+
}],
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Element not found" }) }] };
|
|
777
|
+
}
|
|
778
|
+
// assert_element
|
|
779
|
+
const shouldExist = args.shouldExist !== false;
|
|
780
|
+
const exists = !!found;
|
|
781
|
+
const passed = shouldExist === exists;
|
|
782
|
+
return {
|
|
783
|
+
content: [{
|
|
784
|
+
type: "text",
|
|
785
|
+
text: JSON.stringify({
|
|
786
|
+
passed,
|
|
787
|
+
exists,
|
|
788
|
+
shouldExist,
|
|
789
|
+
element: found ? { text: found.text, enabled: found.enabled } : null,
|
|
790
|
+
}, null, 2),
|
|
791
|
+
}],
|
|
792
|
+
};
|
|
3592
793
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
return { content: [{ type: "text", text: result }] };
|
|
794
|
+
catch (error) {
|
|
795
|
+
return { content: [{ type: "text", text: `Failed: ${error.message}` }] };
|
|
3596
796
|
}
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
797
|
+
}
|
|
798
|
+
// === SCREEN ANALYSIS (ADVANCED) ===
|
|
799
|
+
case "suggest_action": {
|
|
800
|
+
const goal = args.goal;
|
|
801
|
+
// Type guard and validation
|
|
802
|
+
if (!goal || typeof goal !== "string") {
|
|
803
|
+
return { content: [{ type: "text", text: "Goal parameter is required and must be a string" }] };
|
|
3600
804
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
805
|
+
// Validate goal has reasonable length (prevent DoS via long strings)
|
|
806
|
+
const MAX_GOAL_LEN = 1000;
|
|
807
|
+
if (goal.length > MAX_GOAL_LEN) {
|
|
808
|
+
return { content: [{ type: "text", text: `Goal too long (max ${MAX_GOAL_LEN} chars)` }] };
|
|
3604
809
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
const
|
|
3608
|
-
|
|
810
|
+
try {
|
|
811
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
812
|
+
const elements = parseUiTree(stdout);
|
|
813
|
+
const clickableElements = elements.filter((el) => el.clickable && (el.text || el.contentDescription));
|
|
814
|
+
const suggestions = [];
|
|
815
|
+
const goalLower = goal.toLowerCase();
|
|
816
|
+
for (const el of clickableElements) {
|
|
817
|
+
const text = (el.text || el.contentDescription || "").toLowerCase();
|
|
818
|
+
if (goalLower.includes("login") && (text.includes("login") || text.includes("sign in"))) {
|
|
819
|
+
suggestions.push({
|
|
820
|
+
action: "tap",
|
|
821
|
+
target: el.text || el.contentDescription,
|
|
822
|
+
reasoning: "This button appears to initiate login",
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (goalLower.includes("search") && (text.includes("search") || el.className.includes("EditText"))) {
|
|
826
|
+
suggestions.push({
|
|
827
|
+
action: el.className.includes("EditText") ? "input" : "tap",
|
|
828
|
+
target: el.text || el.contentDescription || "search field",
|
|
829
|
+
reasoning: "This appears to be a search input",
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (goalLower.includes("settings") && text.includes("setting")) {
|
|
833
|
+
suggestions.push({
|
|
834
|
+
action: "tap",
|
|
835
|
+
target: el.text || el.contentDescription,
|
|
836
|
+
reasoning: "This navigates to settings",
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (goalLower.includes("back") && text.includes("back")) {
|
|
840
|
+
suggestions.push({
|
|
841
|
+
action: "tap",
|
|
842
|
+
target: el.text || el.contentDescription,
|
|
843
|
+
reasoning: "This goes back",
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (suggestions.length === 0 && clickableElements.length > 0) {
|
|
848
|
+
suggestions.push({
|
|
849
|
+
action: "analyze",
|
|
850
|
+
target: "screen",
|
|
851
|
+
reasoning: `No direct match for "${goal}". ${clickableElements.length} clickable elements found. Use analyze_screen for details.`,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
content: [{
|
|
856
|
+
type: "text",
|
|
857
|
+
text: JSON.stringify({
|
|
858
|
+
goal,
|
|
859
|
+
suggestions,
|
|
860
|
+
clickableElementCount: clickableElements.length,
|
|
861
|
+
note: "These are SUGGESTIONS only. MobileDevMCP is read-only and does not perform actions.",
|
|
862
|
+
}, null, 2),
|
|
863
|
+
}],
|
|
864
|
+
};
|
|
3609
865
|
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
return { content: [{ type: "text", text: result }] };
|
|
866
|
+
catch (error) {
|
|
867
|
+
return { content: [{ type: "text", text: `Failed to analyze screen: ${error.message}` }] };
|
|
3613
868
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
869
|
+
}
|
|
870
|
+
case "analyze_screen": {
|
|
871
|
+
try {
|
|
872
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
873
|
+
const elements = parseUiTree(stdout);
|
|
874
|
+
const analysis = {
|
|
875
|
+
totalElements: elements.length,
|
|
876
|
+
clickableElements: elements.filter((el) => el.clickable).length,
|
|
877
|
+
textElements: elements.filter((el) => el.text).length,
|
|
878
|
+
inputFields: elements.filter((el) => el.className.includes("EditText")).length,
|
|
879
|
+
buttons: elements.filter((el) => el.className.includes("Button")).length,
|
|
880
|
+
visibleText: elements
|
|
881
|
+
.filter((el) => el.text)
|
|
882
|
+
.map((el) => el.text)
|
|
883
|
+
.slice(0, 20),
|
|
884
|
+
interactiveElements: elements
|
|
885
|
+
.filter((el) => el.clickable && (el.text || el.contentDescription))
|
|
886
|
+
.map((el) => ({
|
|
887
|
+
text: el.text || el.contentDescription,
|
|
888
|
+
type: el.className.split(".").pop(),
|
|
889
|
+
bounds: el.bounds,
|
|
890
|
+
}))
|
|
891
|
+
.slice(0, 15),
|
|
892
|
+
};
|
|
893
|
+
return {
|
|
894
|
+
content: [{
|
|
895
|
+
type: "text",
|
|
896
|
+
text: JSON.stringify(analysis, null, 2),
|
|
897
|
+
}],
|
|
898
|
+
};
|
|
3617
899
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
return { content: [{ type: "text", text: result }] };
|
|
900
|
+
catch (error) {
|
|
901
|
+
return { content: [{ type: "text", text: `Failed to analyze screen: ${error.message}` }] };
|
|
3621
902
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
903
|
+
}
|
|
904
|
+
case "get_screen_text": {
|
|
905
|
+
try {
|
|
906
|
+
const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
|
|
907
|
+
const elements = parseUiTree(stdout);
|
|
908
|
+
const allText = elements
|
|
909
|
+
.filter((el) => el.text || el.contentDescription)
|
|
910
|
+
.map((el) => el.text || el.contentDescription)
|
|
911
|
+
.filter((text, index, arr) => arr.indexOf(text) === index);
|
|
912
|
+
return {
|
|
913
|
+
content: [{
|
|
914
|
+
type: "text",
|
|
915
|
+
text: JSON.stringify({
|
|
916
|
+
textCount: allText.length,
|
|
917
|
+
text: allText,
|
|
918
|
+
}, null, 2),
|
|
919
|
+
}],
|
|
920
|
+
};
|
|
3625
921
|
}
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
const result = await getCpuUsage(args?.packageName, args?.device);
|
|
3629
|
-
return { content: [{ type: "text", text: result }] };
|
|
922
|
+
catch (error) {
|
|
923
|
+
return { content: [{ type: "text", text: `Failed to get screen text: ${error.message}` }] };
|
|
3630
924
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
925
|
+
}
|
|
926
|
+
// === LICENSE ===
|
|
927
|
+
case "get_license_status": {
|
|
928
|
+
try {
|
|
929
|
+
const status = await getLicenseStatus();
|
|
930
|
+
return { content: [{ type: "text", text: status }] };
|
|
3634
931
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
return { content: [{ type: "text", text: result }] };
|
|
932
|
+
catch (error) {
|
|
933
|
+
return { content: [{ type: "text", text: `Failed to get license status: ${error.message}` }] };
|
|
3638
934
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
935
|
+
}
|
|
936
|
+
case "set_license_key": {
|
|
937
|
+
try {
|
|
938
|
+
const result = await setLicenseKey(args.licenseKey);
|
|
3641
939
|
return { content: [{ type: "text", text: result }] };
|
|
3642
940
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
return { content: [{ type: "text", text: result }] };
|
|
941
|
+
catch (error) {
|
|
942
|
+
return { content: [{ type: "text", text: `Failed to set license key: ${error.message}` }] };
|
|
3646
943
|
}
|
|
3647
|
-
default:
|
|
3648
|
-
return {
|
|
3649
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
3650
|
-
isError: true,
|
|
3651
|
-
};
|
|
3652
944
|
}
|
|
945
|
+
default:
|
|
946
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// ============================================================================
|
|
950
|
+
// MCP SERVER SETUP
|
|
951
|
+
// ============================================================================
|
|
952
|
+
const server = new Server({
|
|
953
|
+
name: "mobile-dev-mcp",
|
|
954
|
+
version: "1.0.0",
|
|
955
|
+
}, {
|
|
956
|
+
capabilities: {
|
|
957
|
+
tools: {},
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
// List available tools
|
|
961
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
962
|
+
const license = await checkLicense();
|
|
963
|
+
// Filter tools based on tier
|
|
964
|
+
const availableTools = tools.filter((tool) => canAccessTool(tool.name, license.tier));
|
|
965
|
+
return { tools: availableTools };
|
|
966
|
+
});
|
|
967
|
+
// Handle tool calls
|
|
968
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
969
|
+
const { name, arguments: args } = request.params;
|
|
970
|
+
try {
|
|
971
|
+
const license = await checkLicense();
|
|
972
|
+
const result = await handleTool(name, args || {}, license.tier);
|
|
973
|
+
return result;
|
|
3653
974
|
}
|
|
3654
975
|
catch (error) {
|
|
3655
976
|
return {
|
|
@@ -3658,14 +979,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3658
979
|
};
|
|
3659
980
|
}
|
|
3660
981
|
});
|
|
3661
|
-
// Start
|
|
982
|
+
// Start server
|
|
3662
983
|
async function main() {
|
|
3663
984
|
const transport = new StdioServerTransport();
|
|
3664
985
|
await server.connect(transport);
|
|
3665
|
-
// Log startup info to stderr (won't interfere with MCP protocol)
|
|
3666
986
|
const license = await checkLicense();
|
|
3667
|
-
console.error(`Mobile Dev MCP
|
|
3668
|
-
console.error(`License: ${license.tier.toUpperCase()}`);
|
|
987
|
+
console.error(`Mobile Dev MCP v1.0.0 - Read-Only Debugging`);
|
|
988
|
+
console.error(`License: ${license.tier.toUpperCase()} (${license.tier === "free" ? 8 : 21} tools)`);
|
|
3669
989
|
console.error(`Ready for connections...`);
|
|
3670
990
|
}
|
|
3671
991
|
main().catch((error) => {
|