@ggboi360/mobile-dev-mcp 0.1.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 +53 -0
- package/README.md +193 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2320 -0
- package/dist/index.js.map +1 -0
- package/dist/license.d.ts +103 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +606 -0
- package/dist/license.js.map +1 -0
- package/dist/license.test.d.ts +2 -0
- package/dist/license.test.d.ts.map +1 -0
- package/dist/license.test.js +198 -0
- package/dist/license.test.js.map +1 -0
- package/dist/tools.test.d.ts +2 -0
- package/dist/tools.test.d.ts.map +1 -0
- package/dist/tools.test.js +337 -0
- package/dist/tools.test.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { exec, spawn } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as http from "http";
|
|
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)
|
|
15
|
+
const userConfig = loadConfig();
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
metroPort: userConfig.metroPort || 8081,
|
|
18
|
+
logBufferSize: userConfig.logBufferSize || 100,
|
|
19
|
+
screenshotDir: process.env.TEMP || "/tmp",
|
|
20
|
+
};
|
|
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
|
+
// ============================================================================
|
|
30
|
+
// TOOL DEFINITIONS
|
|
31
|
+
// ============================================================================
|
|
32
|
+
const coreTools = [
|
|
33
|
+
// === FREE TIER TOOLS ===
|
|
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
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "screenshot_emulator",
|
|
79
|
+
description: "Capture a screenshot from the currently running Android emulator. Returns the screenshot as a base64-encoded image.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
device: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Specific device ID (from 'adb devices'). Leave empty for default device.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "list_devices",
|
|
92
|
+
description: "List all connected Android devices and emulators via ADB. Shows device IDs and status.",
|
|
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.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
port: {
|
|
105
|
+
type: "number",
|
|
106
|
+
description: "Metro port (default: 8081)",
|
|
107
|
+
default: 8081,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "get_app_info",
|
|
114
|
+
description: "Get information about an installed app on the Android device, including version and permissions.",
|
|
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
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "clear_app_data",
|
|
128
|
+
description: "Clear app data and cache on Android device. Useful for testing fresh installs.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
packageName: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "The app package name to clear data for",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
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
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "get_device_info",
|
|
150
|
+
description: "Get detailed information about the connected Android device including OS version, screen size, and available memory.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
device: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "Specific device ID. Leave empty for default device.",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "start_metro_logging",
|
|
163
|
+
description: "Start capturing Metro bundler logs by watching a log file. Point this at your Metro output file or pipe Metro to a file.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
logFile: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "Path to Metro log file to watch. If not provided, will try common locations.",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "stop_metro_logging",
|
|
176
|
+
description: "Stop the background Metro log capture.",
|
|
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.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
filter: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "Tag filter for logcat (default: 'ReactNativeJS')",
|
|
192
|
+
default: "ReactNativeJS",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "stop_adb_streaming",
|
|
199
|
+
description: "[PRO] Stop real-time ADB log streaming.",
|
|
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.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
count: {
|
|
212
|
+
type: "number",
|
|
213
|
+
description: "Number of recent screenshots to retrieve (default: 5)",
|
|
214
|
+
default: 5,
|
|
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'])",
|
|
229
|
+
},
|
|
230
|
+
timeout: {
|
|
231
|
+
type: "number",
|
|
232
|
+
description: "Timeout in seconds (default: 60)",
|
|
233
|
+
default: 60,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "multi_device_logs",
|
|
240
|
+
description: "[PRO] Get logs from multiple devices simultaneously.",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
devices: {
|
|
245
|
+
type: "array",
|
|
246
|
+
items: { type: "string" },
|
|
247
|
+
description: "Array of device IDs to get logs from",
|
|
248
|
+
},
|
|
249
|
+
lines: {
|
|
250
|
+
type: "number",
|
|
251
|
+
description: "Number of log lines per device",
|
|
252
|
+
default: 30,
|
|
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)",
|
|
275
|
+
},
|
|
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: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: "Text to type",
|
|
289
|
+
},
|
|
290
|
+
device: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description: "Specific device ID (optional)",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ["text"],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "press_button",
|
|
300
|
+
description: "[PRO] Press a hardware button (back, home, recent apps, volume, power).",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
button: {
|
|
305
|
+
type: "string",
|
|
306
|
+
enum: ["back", "home", "recent", "volume_up", "volume_down", "power", "enter"],
|
|
307
|
+
description: "Button to press",
|
|
308
|
+
},
|
|
309
|
+
device: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Specific device ID (optional)",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
required: ["button"],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "swipe_screen",
|
|
319
|
+
description: "[PRO] Swipe on the screen from one point to another. Use for scrolling or gestures.",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
startX: {
|
|
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: {
|
|
340
|
+
type: "number",
|
|
341
|
+
description: "Swipe duration in milliseconds (default: 300)",
|
|
342
|
+
default: 300,
|
|
343
|
+
},
|
|
344
|
+
device: {
|
|
345
|
+
type: "string",
|
|
346
|
+
description: "Specific device ID (optional)",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
required: ["startX", "startY", "endX", "endY"],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "launch_app",
|
|
354
|
+
description: "[PRO] Launch an app by its package name.",
|
|
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
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "install_apk",
|
|
372
|
+
description: "[PRO] Install an APK file to the device.",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
apkPath: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Path to the APK file",
|
|
379
|
+
},
|
|
380
|
+
device: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description: "Specific device ID (optional)",
|
|
383
|
+
},
|
|
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: {
|
|
396
|
+
type: "boolean",
|
|
397
|
+
description: "Only show booted simulators (default: false)",
|
|
398
|
+
default: false,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "screenshot_ios_simulator",
|
|
405
|
+
description: "Capture a screenshot from an iOS Simulator. Returns the screenshot as a base64-encoded image.",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: "object",
|
|
408
|
+
properties: {
|
|
409
|
+
udid: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "get_ios_simulator_logs",
|
|
418
|
+
description: "Get recent logs from an iOS Simulator. Useful for debugging React Native iOS apps.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
udid: {
|
|
423
|
+
type: "string",
|
|
424
|
+
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
425
|
+
},
|
|
426
|
+
filter: {
|
|
427
|
+
type: "string",
|
|
428
|
+
description: "Filter logs by subsystem or message content",
|
|
429
|
+
},
|
|
430
|
+
lines: {
|
|
431
|
+
type: "number",
|
|
432
|
+
description: "Number of recent log lines (default: 50)",
|
|
433
|
+
default: 50,
|
|
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')",
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
required: ["udid"],
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "shutdown_ios_simulator",
|
|
467
|
+
description: "[PRO] Shutdown an iOS Simulator.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
udid: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "Simulator UDID. Use 'all' to shutdown all simulators.",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
required: ["udid"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "install_ios_app",
|
|
481
|
+
description: "[PRO] Install an app (.app bundle) on an iOS Simulator.",
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
appPath: {
|
|
486
|
+
type: "string",
|
|
487
|
+
description: "Path to the .app bundle",
|
|
488
|
+
},
|
|
489
|
+
udid: {
|
|
490
|
+
type: "string",
|
|
491
|
+
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
required: ["appPath"],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "launch_ios_app",
|
|
499
|
+
description: "[PRO] Launch an app on an iOS Simulator by bundle identifier.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
bundleId: {
|
|
504
|
+
type: "string",
|
|
505
|
+
description: "App bundle identifier (e.g., 'com.example.myapp')",
|
|
506
|
+
},
|
|
507
|
+
udid: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
required: ["bundleId"],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "terminate_ios_app",
|
|
517
|
+
description: "[PRO] Terminate (force quit) an app on an iOS Simulator.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
bundleId: {
|
|
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
|
+
},
|
|
529
|
+
},
|
|
530
|
+
required: ["bundleId"],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "ios_open_url",
|
|
535
|
+
description: "[PRO] Open a URL in the iOS Simulator (deep links, universal links).",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
url: {
|
|
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
|
+
},
|
|
547
|
+
},
|
|
548
|
+
required: ["url"],
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "ios_push_notification",
|
|
553
|
+
description: "[PRO] Send a push notification to an iOS Simulator.",
|
|
554
|
+
inputSchema: {
|
|
555
|
+
type: "object",
|
|
556
|
+
properties: {
|
|
557
|
+
bundleId: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "App bundle identifier",
|
|
560
|
+
},
|
|
561
|
+
payload: {
|
|
562
|
+
type: "object",
|
|
563
|
+
description: "Push notification payload (APS format)",
|
|
564
|
+
},
|
|
565
|
+
udid: {
|
|
566
|
+
type: "string",
|
|
567
|
+
description: "Simulator UDID. Leave empty for the booted simulator.",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
required: ["bundleId", "payload"],
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "ios_set_location",
|
|
575
|
+
description: "[PRO] Set the simulated GPS location on an iOS Simulator.",
|
|
576
|
+
inputSchema: {
|
|
577
|
+
type: "object",
|
|
578
|
+
properties: {
|
|
579
|
+
latitude: {
|
|
580
|
+
type: "number",
|
|
581
|
+
description: "Latitude coordinate",
|
|
582
|
+
},
|
|
583
|
+
longitude: {
|
|
584
|
+
type: "number",
|
|
585
|
+
description: "Longitude coordinate",
|
|
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
|
+
];
|
|
686
|
+
// Combine core tools with license tools
|
|
687
|
+
const tools = [...coreTools, ...licenseTools];
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// TOOL IMPLEMENTATIONS
|
|
690
|
+
// ============================================================================
|
|
691
|
+
async function getMetroLogs(lines = 50, filter) {
|
|
692
|
+
// Check license/trial status
|
|
693
|
+
const check = await requireBasic("get_metro_logs");
|
|
694
|
+
if (!check.allowed)
|
|
695
|
+
return check.message;
|
|
696
|
+
const license = await checkLicense();
|
|
697
|
+
const tierLimits = TIER_LIMITS[license.tier];
|
|
698
|
+
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
699
|
+
let logs = metroLogBuffer.slice(-maxLines);
|
|
700
|
+
if (filter) {
|
|
701
|
+
logs = logs.filter((line) => line.toLowerCase().includes(filter.toLowerCase()));
|
|
702
|
+
}
|
|
703
|
+
if (logs.length === 0) {
|
|
704
|
+
try {
|
|
705
|
+
const response = await fetchMetroStatus(CONFIG.metroPort);
|
|
706
|
+
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`;
|
|
707
|
+
if (check.message)
|
|
708
|
+
result += `\n\n${check.message}`;
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
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`;
|
|
713
|
+
if (check.message)
|
|
714
|
+
result += `\n\n${check.message}`;
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const header = license.valid
|
|
719
|
+
? `📋 Metro Logs (${logs.length} lines):`
|
|
720
|
+
: `📋 Metro Logs (${logs.length} lines, trial mode):`;
|
|
721
|
+
let result = `${header}\n${"─".repeat(50)}\n${logs.join("\n")}`;
|
|
722
|
+
if (check.message)
|
|
723
|
+
result += `\n\n${check.message}`;
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
async function getAdbLogs(lines = 50, filter = "ReactNativeJS", level = "I") {
|
|
727
|
+
// Check license/trial status
|
|
728
|
+
const check = await requireBasic("get_adb_logs");
|
|
729
|
+
if (!check.allowed)
|
|
730
|
+
return check.message;
|
|
731
|
+
const license = await checkLicense();
|
|
732
|
+
const tierLimits = TIER_LIMITS[license.tier];
|
|
733
|
+
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
734
|
+
try {
|
|
735
|
+
await execAsync("adb version");
|
|
736
|
+
let command;
|
|
737
|
+
if (filter === "*") {
|
|
738
|
+
command = `adb logcat -d -t ${maxLines} *:${level}`;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
command = `adb logcat -d -t ${maxLines} ${filter}:${level} *:S`;
|
|
742
|
+
}
|
|
743
|
+
const { stdout, stderr } = await execAsync(command);
|
|
744
|
+
if (stderr && !stdout) {
|
|
745
|
+
return `ADB Error: ${stderr}`;
|
|
746
|
+
}
|
|
747
|
+
let result = stdout || "No logs found matching the filter.";
|
|
748
|
+
if (check.message)
|
|
749
|
+
result += `\n\n${check.message}`;
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (error.message.includes("not recognized") || error.message.includes("not found")) {
|
|
754
|
+
return "ADB is not installed or not in PATH. Please install Android SDK Platform Tools.";
|
|
755
|
+
}
|
|
756
|
+
if (error.message.includes("no devices")) {
|
|
757
|
+
return "No Android devices/emulators connected. Start an emulator or connect a device.";
|
|
758
|
+
}
|
|
759
|
+
return `Error getting ADB logs: ${error.message}`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function screenshotEmulator(device) {
|
|
763
|
+
// Check license/trial status
|
|
764
|
+
const check = await requireBasic("screenshot_emulator");
|
|
765
|
+
if (!check.allowed) {
|
|
766
|
+
return { success: false, error: check.message };
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
770
|
+
const screenshotPath = path.join(CONFIG.screenshotDir, `screenshot_${Date.now()}.png`);
|
|
771
|
+
// Capture screenshot
|
|
772
|
+
await execAsync(`adb ${deviceFlag} exec-out screencap -p > "${screenshotPath}"`);
|
|
773
|
+
// Read and convert to base64
|
|
774
|
+
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
775
|
+
const base64Data = imageBuffer.toString("base64");
|
|
776
|
+
// Clean up temp file
|
|
777
|
+
fs.unlinkSync(screenshotPath);
|
|
778
|
+
// Save to history for Advanced users
|
|
779
|
+
const license = await checkLicense();
|
|
780
|
+
if (license.valid && license.tier === "advanced") {
|
|
781
|
+
screenshotHistory.unshift({
|
|
782
|
+
timestamp: new Date().toISOString(),
|
|
783
|
+
data: base64Data,
|
|
784
|
+
});
|
|
785
|
+
if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
|
|
786
|
+
screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
success: true,
|
|
791
|
+
data: base64Data,
|
|
792
|
+
mimeType: "image/png",
|
|
793
|
+
trialMessage: check.message,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
return {
|
|
798
|
+
success: false,
|
|
799
|
+
error: `Failed to capture screenshot: ${error.message}`,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function listDevices() {
|
|
804
|
+
// Check license/trial status
|
|
805
|
+
const check = await requireBasic("list_devices");
|
|
806
|
+
if (!check.allowed)
|
|
807
|
+
return check.message;
|
|
808
|
+
try {
|
|
809
|
+
const { stdout } = await execAsync("adb devices -l");
|
|
810
|
+
const lines = stdout.trim().split("\n");
|
|
811
|
+
if (lines.length <= 1) {
|
|
812
|
+
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`;
|
|
813
|
+
if (check.message)
|
|
814
|
+
result += `\n\n${check.message}`;
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
let result = stdout;
|
|
818
|
+
if (check.message)
|
|
819
|
+
result += `\n\n${check.message}`;
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
return `Error listing devices: ${error.message}`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async function fetchMetroStatus(port) {
|
|
827
|
+
return new Promise((resolve, reject) => {
|
|
828
|
+
const req = http.get(`http://localhost:${port}/status`, (res) => {
|
|
829
|
+
let data = "";
|
|
830
|
+
res.on("data", (chunk) => (data += chunk));
|
|
831
|
+
res.on("end", () => resolve(data || "Metro is running"));
|
|
832
|
+
});
|
|
833
|
+
req.on("error", (err) => reject(err));
|
|
834
|
+
req.setTimeout(3000, () => {
|
|
835
|
+
req.destroy();
|
|
836
|
+
reject(new Error("Timeout"));
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
async function checkMetroStatus(port = 8081) {
|
|
841
|
+
// Check license/trial status
|
|
842
|
+
const check = await requireBasic("check_metro_status");
|
|
843
|
+
if (!check.allowed)
|
|
844
|
+
return check.message;
|
|
845
|
+
try {
|
|
846
|
+
const status = await fetchMetroStatus(port);
|
|
847
|
+
let result = `✅ Metro is running on port ${port}\nStatus: ${status}`;
|
|
848
|
+
if (check.message)
|
|
849
|
+
result += `\n\n${check.message}`;
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
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`;
|
|
854
|
+
if (check.message)
|
|
855
|
+
result += `\n\n${check.message}`;
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function getAppInfo(packageName) {
|
|
860
|
+
// Check license/trial status
|
|
861
|
+
const check = await requireBasic("get_app_info");
|
|
862
|
+
if (!check.allowed)
|
|
863
|
+
return check.message;
|
|
864
|
+
try {
|
|
865
|
+
const { stdout } = await execAsync(`adb shell dumpsys package ${packageName}`);
|
|
866
|
+
const lines = stdout.split("\n");
|
|
867
|
+
const relevantInfo = [];
|
|
868
|
+
for (const line of lines) {
|
|
869
|
+
if (line.includes("versionName") ||
|
|
870
|
+
line.includes("versionCode") ||
|
|
871
|
+
line.includes("targetSdk") ||
|
|
872
|
+
line.includes("dataDir") ||
|
|
873
|
+
line.includes("firstInstallTime") ||
|
|
874
|
+
line.includes("lastUpdateTime")) {
|
|
875
|
+
relevantInfo.push(line.trim());
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (relevantInfo.length === 0) {
|
|
879
|
+
let result = `Package ${packageName} not found on device.`;
|
|
880
|
+
if (check.message)
|
|
881
|
+
result += `\n\n${check.message}`;
|
|
882
|
+
return result;
|
|
883
|
+
}
|
|
884
|
+
let result = `📱 App Info for ${packageName}:\n${relevantInfo.join("\n")}`;
|
|
885
|
+
if (check.message)
|
|
886
|
+
result += `\n\n${check.message}`;
|
|
887
|
+
return result;
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
return `Error getting app info: ${error.message}`;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function clearAppData(packageName) {
|
|
894
|
+
// Check license/trial status
|
|
895
|
+
const check = await requireBasic("clear_app_data");
|
|
896
|
+
if (!check.allowed)
|
|
897
|
+
return check.message;
|
|
898
|
+
try {
|
|
899
|
+
await execAsync(`adb shell pm clear ${packageName}`);
|
|
900
|
+
let result = `✅ Successfully cleared data for ${packageName}`;
|
|
901
|
+
if (check.message)
|
|
902
|
+
result += `\n\n${check.message}`;
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
return `Error clearing app data: ${error.message}`;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async function restartAdb() {
|
|
910
|
+
// Check license/trial status
|
|
911
|
+
const check = await requireBasic("restart_adb");
|
|
912
|
+
if (!check.allowed)
|
|
913
|
+
return check.message;
|
|
914
|
+
try {
|
|
915
|
+
await execAsync("adb kill-server");
|
|
916
|
+
await execAsync("adb start-server");
|
|
917
|
+
const { stdout } = await execAsync("adb devices");
|
|
918
|
+
let result = `✅ ADB server restarted successfully.\n\nConnected devices:\n${stdout}`;
|
|
919
|
+
if (check.message)
|
|
920
|
+
result += `\n\n${check.message}`;
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
return `Error restarting ADB: ${error.message}`;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function getDeviceInfo(device) {
|
|
928
|
+
// Check license/trial status
|
|
929
|
+
const check = await requireBasic("get_device_info");
|
|
930
|
+
if (!check.allowed)
|
|
931
|
+
return check.message;
|
|
932
|
+
try {
|
|
933
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
934
|
+
const commands = [
|
|
935
|
+
`adb ${deviceFlag} shell getprop ro.build.version.release`,
|
|
936
|
+
`adb ${deviceFlag} shell getprop ro.build.version.sdk`,
|
|
937
|
+
`adb ${deviceFlag} shell getprop ro.product.model`,
|
|
938
|
+
`adb ${deviceFlag} shell getprop ro.product.manufacturer`,
|
|
939
|
+
`adb ${deviceFlag} shell wm size`,
|
|
940
|
+
`adb ${deviceFlag} shell wm density`,
|
|
941
|
+
];
|
|
942
|
+
const results = await Promise.all(commands.map((cmd) => execAsync(cmd)
|
|
943
|
+
.then(({ stdout }) => stdout.trim())
|
|
944
|
+
.catch(() => "N/A")));
|
|
945
|
+
let result = `📱 Device Information:
|
|
946
|
+
─────────────────────────────
|
|
947
|
+
Android Version: ${results[0]}
|
|
948
|
+
SDK Level: ${results[1]}
|
|
949
|
+
Model: ${results[3]} ${results[2]}
|
|
950
|
+
Screen Size: ${results[4].replace("Physical size: ", "")}
|
|
951
|
+
Screen Density: ${results[5].replace("Physical density: ", "")} dpi`;
|
|
952
|
+
if (check.message)
|
|
953
|
+
result += `\n\n${check.message}`;
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
return `Error getting device info: ${error.message}`;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// ============================================================================
|
|
961
|
+
// FIXED: Metro Logging Implementation
|
|
962
|
+
// ============================================================================
|
|
963
|
+
async function startMetroLogging(logFile) {
|
|
964
|
+
// Check license/trial status
|
|
965
|
+
const check = await requireBasic("start_metro_logging");
|
|
966
|
+
if (!check.allowed)
|
|
967
|
+
return check.message;
|
|
968
|
+
if (metroProcess) {
|
|
969
|
+
let result = "Metro logging is already running. Use 'stop_metro_logging' first.";
|
|
970
|
+
if (check.message)
|
|
971
|
+
result += `\n\n${check.message}`;
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
metroLogBuffer = [];
|
|
975
|
+
// If a log file is provided, tail it
|
|
976
|
+
if (logFile) {
|
|
977
|
+
if (!fs.existsSync(logFile)) {
|
|
978
|
+
let result = `Log file not found: ${logFile}\n\nCreate it by running:\n npx expo start 2>&1 | tee ${logFile}`;
|
|
979
|
+
if (check.message)
|
|
980
|
+
result += `\n\n${check.message}`;
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
// Use PowerShell's Get-Content -Wait on Windows, tail -f on Unix
|
|
984
|
+
const isWindows = process.platform === "win32";
|
|
985
|
+
if (isWindows) {
|
|
986
|
+
metroProcess = spawn("powershell", [
|
|
987
|
+
"-Command",
|
|
988
|
+
`Get-Content -Path "${logFile}" -Wait -Tail 100`,
|
|
989
|
+
]);
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
metroProcess = spawn("tail", ["-f", "-n", "100", logFile]);
|
|
993
|
+
}
|
|
994
|
+
metroProcess.stdout?.on("data", (data) => {
|
|
995
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
996
|
+
metroLogBuffer.push(...lines);
|
|
997
|
+
if (metroLogBuffer.length > CONFIG.logBufferSize) {
|
|
998
|
+
metroLogBuffer = metroLogBuffer.slice(-CONFIG.logBufferSize);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
metroProcess.stderr?.on("data", (data) => {
|
|
1002
|
+
metroLogBuffer.push(`[STDERR] ${data.toString().trim()}`);
|
|
1003
|
+
});
|
|
1004
|
+
metroProcess.on("error", (err) => {
|
|
1005
|
+
metroLogBuffer.push(`[ERROR] ${err.message}`);
|
|
1006
|
+
});
|
|
1007
|
+
let result = `✅ Metro log capture started!\nWatching: ${logFile}\n\nUse 'get_metro_logs' to retrieve captured logs.`;
|
|
1008
|
+
if (check.message)
|
|
1009
|
+
result += `\n\n${check.message}`;
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
// No log file provided - give instructions
|
|
1013
|
+
let result = `📋 Metro Log Capture Setup
|
|
1014
|
+
─────────────────────────────
|
|
1015
|
+
|
|
1016
|
+
To capture Metro logs, you have two options:
|
|
1017
|
+
|
|
1018
|
+
Option 1: Pipe Metro to a file (Recommended)
|
|
1019
|
+
npx expo start 2>&1 | tee metro.log
|
|
1020
|
+
|
|
1021
|
+
Then run: start_metro_logging with logFile="metro.log"
|
|
1022
|
+
|
|
1023
|
+
Option 2: Check common log locations
|
|
1024
|
+
- Expo: .expo/logs/
|
|
1025
|
+
- React Native: Check Metro terminal output
|
|
1026
|
+
|
|
1027
|
+
Option 3: Use ADB logs instead
|
|
1028
|
+
For device-side JavaScript logs, use 'get_adb_logs'
|
|
1029
|
+
|
|
1030
|
+
─────────────────────────────
|
|
1031
|
+
Once you have a log file, call this tool again with the logFile parameter.`;
|
|
1032
|
+
if (check.message)
|
|
1033
|
+
result += `\n\n${check.message}`;
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
1036
|
+
async function stopMetroLogging() {
|
|
1037
|
+
// Check license/trial status
|
|
1038
|
+
const check = await requireBasic("stop_metro_logging");
|
|
1039
|
+
if (!check.allowed)
|
|
1040
|
+
return check.message;
|
|
1041
|
+
if (metroProcess) {
|
|
1042
|
+
metroProcess.kill();
|
|
1043
|
+
metroProcess = null;
|
|
1044
|
+
}
|
|
1045
|
+
const logCount = metroLogBuffer.length;
|
|
1046
|
+
let result = `✅ Metro logging stopped. ${logCount} log lines were captured.`;
|
|
1047
|
+
if (check.message)
|
|
1048
|
+
result += `\n\n${check.message}`;
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
// ============================================================================
|
|
1052
|
+
// PRO FEATURE IMPLEMENTATIONS
|
|
1053
|
+
// ============================================================================
|
|
1054
|
+
async function streamAdbRealtime(filter = "ReactNativeJS") {
|
|
1055
|
+
const check = await requireAdvanced("stream_adb_realtime");
|
|
1056
|
+
if (!check.allowed)
|
|
1057
|
+
return check.message;
|
|
1058
|
+
if (adbLogProcess) {
|
|
1059
|
+
return "ADB streaming is already running. Use 'stop_adb_streaming' first.";
|
|
1060
|
+
}
|
|
1061
|
+
adbLogBuffer = [];
|
|
1062
|
+
adbLogProcess = spawn("adb", ["logcat", `${filter}:V`, "*:S"]);
|
|
1063
|
+
adbLogProcess.stdout?.on("data", (data) => {
|
|
1064
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
1065
|
+
adbLogBuffer.push(...lines);
|
|
1066
|
+
if (adbLogBuffer.length > 500) {
|
|
1067
|
+
adbLogBuffer = adbLogBuffer.slice(-500);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
adbLogProcess.on("error", (err) => {
|
|
1071
|
+
adbLogBuffer.push(`[ERROR] ${err.message}`);
|
|
1072
|
+
});
|
|
1073
|
+
return `✅ [PRO] Real-time ADB streaming started!\nFilter: ${filter}\n\nUse 'get_adb_logs' to retrieve the live buffer.`;
|
|
1074
|
+
}
|
|
1075
|
+
function stopAdbStreaming() {
|
|
1076
|
+
if (adbLogProcess) {
|
|
1077
|
+
adbLogProcess.kill();
|
|
1078
|
+
adbLogProcess = null;
|
|
1079
|
+
}
|
|
1080
|
+
return `✅ ADB streaming stopped. Buffer contained ${adbLogBuffer.length} lines.`;
|
|
1081
|
+
}
|
|
1082
|
+
async function getScreenshotHistory(count = 5) {
|
|
1083
|
+
const check = await requireAdvanced("screenshot_history");
|
|
1084
|
+
if (!check.allowed)
|
|
1085
|
+
return check.message;
|
|
1086
|
+
if (screenshotHistory.length === 0) {
|
|
1087
|
+
return "No screenshots in history. Take screenshots using 'screenshot_emulator' first.";
|
|
1088
|
+
}
|
|
1089
|
+
const recent = screenshotHistory.slice(0, count);
|
|
1090
|
+
return `📸 [PRO] Screenshot History (${recent.length} of ${screenshotHistory.length}):\n\n${recent
|
|
1091
|
+
.map((s, i) => `${i + 1}. ${s.timestamp}`)
|
|
1092
|
+
.join("\n")}\n\nNote: Full image data available in tool response.`;
|
|
1093
|
+
}
|
|
1094
|
+
async function watchForErrors(patterns = ["Error", "Exception"], timeout = 60) {
|
|
1095
|
+
const check = await requireAdvanced("watch_for_errors");
|
|
1096
|
+
if (!check.allowed)
|
|
1097
|
+
return check.message;
|
|
1098
|
+
return new Promise((resolve) => {
|
|
1099
|
+
const startTime = Date.now();
|
|
1100
|
+
const checkInterval = setInterval(async () => {
|
|
1101
|
+
// Check ADB logs for patterns
|
|
1102
|
+
try {
|
|
1103
|
+
const { stdout } = await execAsync("adb logcat -d -t 50 *:E");
|
|
1104
|
+
for (const pattern of patterns) {
|
|
1105
|
+
if (stdout.toLowerCase().includes(pattern.toLowerCase())) {
|
|
1106
|
+
clearInterval(checkInterval);
|
|
1107
|
+
resolve(`🚨 [PRO] Error detected!\nPattern: "${pattern}"\n\nRelevant logs:\n${stdout}`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch { }
|
|
1113
|
+
// Check timeout
|
|
1114
|
+
if (Date.now() - startTime > timeout * 1000) {
|
|
1115
|
+
clearInterval(checkInterval);
|
|
1116
|
+
resolve(`✅ [PRO] No errors detected in ${timeout} seconds.`);
|
|
1117
|
+
}
|
|
1118
|
+
}, 2000);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
async function multiDeviceLogs(devices, lines = 30) {
|
|
1122
|
+
const check = await requireAdvanced("multi_device_logs");
|
|
1123
|
+
if (!check.allowed)
|
|
1124
|
+
return check.message;
|
|
1125
|
+
if (!devices || devices.length === 0) {
|
|
1126
|
+
const { stdout } = await execAsync("adb devices");
|
|
1127
|
+
return `No devices specified. Available devices:\n${stdout}`;
|
|
1128
|
+
}
|
|
1129
|
+
const results = await Promise.all(devices.map(async (device) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const { stdout } = await execAsync(`adb -s ${device} logcat -d -t ${lines} ReactNativeJS:V *:S`);
|
|
1132
|
+
return `📱 Device: ${device}\n${"─".repeat(30)}\n${stdout}`;
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
return `📱 Device: ${device}\n${"─".repeat(30)}\nError: ${error.message}`;
|
|
1136
|
+
}
|
|
1137
|
+
}));
|
|
1138
|
+
return `📋 [PRO] Multi-Device Logs\n${"═".repeat(50)}\n\n${results.join("\n\n")}`;
|
|
1139
|
+
}
|
|
1140
|
+
// ============================================================================
|
|
1141
|
+
// INTERACTION TOOLS (Advanced)
|
|
1142
|
+
// ============================================================================
|
|
1143
|
+
async function tapScreen(x, y, device) {
|
|
1144
|
+
const check = await requireAdvanced("tap_screen");
|
|
1145
|
+
if (!check.allowed)
|
|
1146
|
+
return check.message;
|
|
1147
|
+
try {
|
|
1148
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1149
|
+
await execAsync(`adb ${deviceFlag} shell input tap ${x} ${y}`);
|
|
1150
|
+
let result = `✅ Tapped at (${x}, ${y})`;
|
|
1151
|
+
if (check.message)
|
|
1152
|
+
result += `\n\n${check.message}`;
|
|
1153
|
+
return result;
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
return `Error tapping screen: ${error.message}`;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async function inputText(text, device) {
|
|
1160
|
+
const check = await requireAdvanced("input_text");
|
|
1161
|
+
if (!check.allowed)
|
|
1162
|
+
return check.message;
|
|
1163
|
+
try {
|
|
1164
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1165
|
+
// Escape special characters for shell
|
|
1166
|
+
const escapedText = text.replace(/([\\'"$ `])/g, "\\$1").replace(/ /g, "%s");
|
|
1167
|
+
await execAsync(`adb ${deviceFlag} shell input text "${escapedText}"`);
|
|
1168
|
+
let result = `✅ Typed: "${text}"`;
|
|
1169
|
+
if (check.message)
|
|
1170
|
+
result += `\n\n${check.message}`;
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
return `Error inputting text: ${error.message}`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function pressButton(button, device) {
|
|
1178
|
+
const check = await requireAdvanced("press_button");
|
|
1179
|
+
if (!check.allowed)
|
|
1180
|
+
return check.message;
|
|
1181
|
+
const keyMap = {
|
|
1182
|
+
back: 4,
|
|
1183
|
+
home: 3,
|
|
1184
|
+
recent: 187,
|
|
1185
|
+
volume_up: 24,
|
|
1186
|
+
volume_down: 25,
|
|
1187
|
+
power: 26,
|
|
1188
|
+
enter: 66,
|
|
1189
|
+
};
|
|
1190
|
+
const keyCode = keyMap[button];
|
|
1191
|
+
if (!keyCode) {
|
|
1192
|
+
return `Unknown button: ${button}. Available: ${Object.keys(keyMap).join(", ")}`;
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1196
|
+
await execAsync(`adb ${deviceFlag} shell input keyevent ${keyCode}`);
|
|
1197
|
+
let result = `✅ Pressed: ${button.toUpperCase()}`;
|
|
1198
|
+
if (check.message)
|
|
1199
|
+
result += `\n\n${check.message}`;
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
return `Error pressing button: ${error.message}`;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async function swipeScreen(startX, startY, endX, endY, duration = 300, device) {
|
|
1207
|
+
const check = await requireAdvanced("swipe_screen");
|
|
1208
|
+
if (!check.allowed)
|
|
1209
|
+
return check.message;
|
|
1210
|
+
try {
|
|
1211
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1212
|
+
await execAsync(`adb ${deviceFlag} shell input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`);
|
|
1213
|
+
let result = `✅ Swiped from (${startX}, ${startY}) to (${endX}, ${endY})`;
|
|
1214
|
+
if (check.message)
|
|
1215
|
+
result += `\n\n${check.message}`;
|
|
1216
|
+
return result;
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
return `Error swiping: ${error.message}`;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function launchApp(packageName, device) {
|
|
1223
|
+
const check = await requireAdvanced("launch_app");
|
|
1224
|
+
if (!check.allowed)
|
|
1225
|
+
return check.message;
|
|
1226
|
+
try {
|
|
1227
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1228
|
+
// Get the main activity using monkey
|
|
1229
|
+
await execAsync(`adb ${deviceFlag} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`);
|
|
1230
|
+
let result = `✅ Launched: ${packageName}`;
|
|
1231
|
+
if (check.message)
|
|
1232
|
+
result += `\n\n${check.message}`;
|
|
1233
|
+
return result;
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
return `Error launching app: ${error.message}\n\nMake sure the package name is correct.`;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async function installApk(apkPath, device) {
|
|
1240
|
+
const check = await requireAdvanced("install_apk");
|
|
1241
|
+
if (!check.allowed)
|
|
1242
|
+
return check.message;
|
|
1243
|
+
if (!fs.existsSync(apkPath)) {
|
|
1244
|
+
return `APK file not found: ${apkPath}`;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1248
|
+
const { stdout } = await execAsync(`adb ${deviceFlag} install -r "${apkPath}"`);
|
|
1249
|
+
let result = `✅ APK installed successfully!\n\n${stdout}`;
|
|
1250
|
+
if (check.message)
|
|
1251
|
+
result += `\n\n${check.message}`;
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
catch (error) {
|
|
1255
|
+
return `Error installing APK: ${error.message}`;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async function checkXcodeInstalled() {
|
|
1259
|
+
try {
|
|
1260
|
+
await execAsync("xcrun simctl help");
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async function listIosSimulators(onlyBooted = false) {
|
|
1268
|
+
const check = await requireBasic("list_ios_simulators");
|
|
1269
|
+
if (!check.allowed)
|
|
1270
|
+
return check.message;
|
|
1271
|
+
if (process.platform !== "darwin") {
|
|
1272
|
+
return "iOS Simulators are only available on macOS.";
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
if (!(await checkXcodeInstalled())) {
|
|
1276
|
+
return "Xcode Command Line Tools not installed. Run: xcode-select --install";
|
|
1277
|
+
}
|
|
1278
|
+
const { stdout } = await execAsync("xcrun simctl list devices -j");
|
|
1279
|
+
const data = JSON.parse(stdout);
|
|
1280
|
+
const results = [];
|
|
1281
|
+
results.push("📱 iOS Simulators\n" + "═".repeat(50));
|
|
1282
|
+
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
1283
|
+
const deviceList = devices;
|
|
1284
|
+
const filteredDevices = onlyBooted
|
|
1285
|
+
? deviceList.filter((d) => d.state === "Booted")
|
|
1286
|
+
: deviceList.filter((d) => d.isAvailable);
|
|
1287
|
+
if (filteredDevices.length > 0) {
|
|
1288
|
+
// Extract iOS version from runtime identifier
|
|
1289
|
+
const runtimeName = runtime.split(".").pop()?.replace(/-/g, " ") || runtime;
|
|
1290
|
+
results.push(`\n${runtimeName}:`);
|
|
1291
|
+
for (const device of filteredDevices) {
|
|
1292
|
+
const status = device.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown";
|
|
1293
|
+
results.push(` ${status} ${device.name}`);
|
|
1294
|
+
results.push(` UDID: ${device.udid}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (results.length === 1) {
|
|
1299
|
+
let result = onlyBooted
|
|
1300
|
+
? "No booted simulators found. Boot one with 'boot_ios_simulator'."
|
|
1301
|
+
: "No iOS Simulators available. Open Xcode to download simulator runtimes.";
|
|
1302
|
+
if (check.message)
|
|
1303
|
+
result += `\n\n${check.message}`;
|
|
1304
|
+
return result;
|
|
1305
|
+
}
|
|
1306
|
+
let result = results.join("\n");
|
|
1307
|
+
if (check.message)
|
|
1308
|
+
result += `\n\n${check.message}`;
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
return `Error listing iOS Simulators: ${error.message}`;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function screenshotIosSimulator(udid) {
|
|
1316
|
+
const check = await requireBasic("screenshot_ios_simulator");
|
|
1317
|
+
if (!check.allowed) {
|
|
1318
|
+
return { success: false, error: check.message };
|
|
1319
|
+
}
|
|
1320
|
+
if (process.platform !== "darwin") {
|
|
1321
|
+
return { success: false, error: "iOS Simulators are only available on macOS." };
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
const target = udid || "booted";
|
|
1325
|
+
const screenshotPath = path.join(CONFIG.screenshotDir, `ios_screenshot_${Date.now()}.png`);
|
|
1326
|
+
await execAsync(`xcrun simctl io ${target} screenshot "${screenshotPath}"`);
|
|
1327
|
+
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
1328
|
+
const base64Data = imageBuffer.toString("base64");
|
|
1329
|
+
fs.unlinkSync(screenshotPath);
|
|
1330
|
+
// Save to history for Advanced users
|
|
1331
|
+
const license = await checkLicense();
|
|
1332
|
+
if (license.valid && license.tier === "advanced") {
|
|
1333
|
+
screenshotHistory.unshift({
|
|
1334
|
+
timestamp: new Date().toISOString(),
|
|
1335
|
+
data: base64Data,
|
|
1336
|
+
});
|
|
1337
|
+
if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
|
|
1338
|
+
screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
success: true,
|
|
1343
|
+
data: base64Data,
|
|
1344
|
+
mimeType: "image/png",
|
|
1345
|
+
trialMessage: check.message,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
catch (error) {
|
|
1349
|
+
if (error.message.includes("No devices are booted")) {
|
|
1350
|
+
return { success: false, error: "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'." };
|
|
1351
|
+
}
|
|
1352
|
+
return { success: false, error: `Failed to capture iOS screenshot: ${error.message}` };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function getIosSimulatorLogs(udid, filter, lines = 50) {
|
|
1356
|
+
const check = await requireBasic("get_ios_simulator_logs");
|
|
1357
|
+
if (!check.allowed)
|
|
1358
|
+
return check.message;
|
|
1359
|
+
if (process.platform !== "darwin") {
|
|
1360
|
+
return "iOS Simulators are only available on macOS.";
|
|
1361
|
+
}
|
|
1362
|
+
const license = await checkLicense();
|
|
1363
|
+
const tierLimits = TIER_LIMITS[license.tier];
|
|
1364
|
+
const maxLines = Math.min(lines, tierLimits.maxLogLines);
|
|
1365
|
+
try {
|
|
1366
|
+
const target = udid || "booted";
|
|
1367
|
+
// Use predicate filter if provided
|
|
1368
|
+
let command = `xcrun simctl spawn ${target} log show --last 5m --style compact`;
|
|
1369
|
+
if (filter) {
|
|
1370
|
+
command += ` --predicate 'eventMessage CONTAINS "${filter}" OR subsystem CONTAINS "${filter}"'`;
|
|
1371
|
+
}
|
|
1372
|
+
const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 });
|
|
1373
|
+
if (stderr && !stdout) {
|
|
1374
|
+
return `Error: ${stderr}`;
|
|
1375
|
+
}
|
|
1376
|
+
const logLines = stdout.split("\n").slice(-maxLines);
|
|
1377
|
+
let result = `📋 iOS Simulator Logs (${logLines.length} lines):\n${"─".repeat(50)}\n${logLines.join("\n")}`;
|
|
1378
|
+
if (check.message)
|
|
1379
|
+
result += `\n\n${check.message}`;
|
|
1380
|
+
return result;
|
|
1381
|
+
}
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
if (error.message.includes("No devices are booted")) {
|
|
1384
|
+
return "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'.";
|
|
1385
|
+
}
|
|
1386
|
+
return `Error getting iOS logs: ${error.message}`;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function getIosSimulatorInfo(udid) {
|
|
1390
|
+
const check = await requireBasic("get_ios_simulator_info");
|
|
1391
|
+
if (!check.allowed)
|
|
1392
|
+
return check.message;
|
|
1393
|
+
if (process.platform !== "darwin") {
|
|
1394
|
+
return "iOS Simulators are only available on macOS.";
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
const { stdout } = await execAsync("xcrun simctl list devices -j");
|
|
1398
|
+
const data = JSON.parse(stdout);
|
|
1399
|
+
// Find the target device
|
|
1400
|
+
let targetDevice = null;
|
|
1401
|
+
let targetRuntime = "";
|
|
1402
|
+
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
1403
|
+
const deviceList = devices;
|
|
1404
|
+
const found = deviceList.find((d) => udid ? d.udid === udid : d.state === "Booted");
|
|
1405
|
+
if (found) {
|
|
1406
|
+
targetDevice = found;
|
|
1407
|
+
targetRuntime = runtime;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (!targetDevice) {
|
|
1412
|
+
return udid
|
|
1413
|
+
? `Simulator with UDID ${udid} not found.`
|
|
1414
|
+
: "No booted simulator found. Boot one first or specify a UDID.";
|
|
1415
|
+
}
|
|
1416
|
+
const runtimeName = targetRuntime.split(".").pop()?.replace(/-/g, " ") || targetRuntime;
|
|
1417
|
+
let result = `📱 iOS Simulator Info
|
|
1418
|
+
${"─".repeat(40)}
|
|
1419
|
+
Name: ${targetDevice.name}
|
|
1420
|
+
UDID: ${targetDevice.udid}
|
|
1421
|
+
State: ${targetDevice.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown"}
|
|
1422
|
+
Runtime: ${runtimeName}
|
|
1423
|
+
Available: ${targetDevice.isAvailable ? "Yes" : "No"}`;
|
|
1424
|
+
if (check.message)
|
|
1425
|
+
result += `\n\n${check.message}`;
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
return `Error getting simulator info: ${error.message}`;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async function bootIosSimulator(udid) {
|
|
1433
|
+
const check = await requireAdvanced("boot_ios_simulator");
|
|
1434
|
+
if (!check.allowed)
|
|
1435
|
+
return check.message;
|
|
1436
|
+
if (process.platform !== "darwin") {
|
|
1437
|
+
return "iOS Simulators are only available on macOS.";
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
await execAsync(`xcrun simctl boot "${udid}"`);
|
|
1441
|
+
let result = `✅ iOS Simulator booted: ${udid}\n\nOpening Simulator app...`;
|
|
1442
|
+
// Open Simulator app to show the booted device
|
|
1443
|
+
try {
|
|
1444
|
+
await execAsync("open -a Simulator");
|
|
1445
|
+
}
|
|
1446
|
+
catch { }
|
|
1447
|
+
if (check.message)
|
|
1448
|
+
result += `\n\n${check.message}`;
|
|
1449
|
+
return result;
|
|
1450
|
+
}
|
|
1451
|
+
catch (error) {
|
|
1452
|
+
if (error.message.includes("Unable to boot device in current state: Booted")) {
|
|
1453
|
+
return "Simulator is already booted.";
|
|
1454
|
+
}
|
|
1455
|
+
return `Error booting simulator: ${error.message}`;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async function shutdownIosSimulator(udid) {
|
|
1459
|
+
const check = await requireAdvanced("shutdown_ios_simulator");
|
|
1460
|
+
if (!check.allowed)
|
|
1461
|
+
return check.message;
|
|
1462
|
+
if (process.platform !== "darwin") {
|
|
1463
|
+
return "iOS Simulators are only available on macOS.";
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
if (udid.toLowerCase() === "all") {
|
|
1467
|
+
await execAsync("xcrun simctl shutdown all");
|
|
1468
|
+
let result = "✅ All iOS Simulators have been shut down.";
|
|
1469
|
+
if (check.message)
|
|
1470
|
+
result += `\n\n${check.message}`;
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
await execAsync(`xcrun simctl shutdown "${udid}"`);
|
|
1474
|
+
let result = `✅ iOS Simulator shut down: ${udid}`;
|
|
1475
|
+
if (check.message)
|
|
1476
|
+
result += `\n\n${check.message}`;
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
if (error.message.includes("Unable to shutdown device in current state: Shutdown")) {
|
|
1481
|
+
return "Simulator is already shut down.";
|
|
1482
|
+
}
|
|
1483
|
+
return `Error shutting down simulator: ${error.message}`;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
async function installIosApp(appPath, udid) {
|
|
1487
|
+
const check = await requireAdvanced("install_ios_app");
|
|
1488
|
+
if (!check.allowed)
|
|
1489
|
+
return check.message;
|
|
1490
|
+
if (process.platform !== "darwin") {
|
|
1491
|
+
return "iOS Simulators are only available on macOS.";
|
|
1492
|
+
}
|
|
1493
|
+
if (!fs.existsSync(appPath)) {
|
|
1494
|
+
return `App not found: ${appPath}`;
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const target = udid || "booted";
|
|
1498
|
+
await execAsync(`xcrun simctl install ${target} "${appPath}"`);
|
|
1499
|
+
let result = `✅ App installed successfully on iOS Simulator!`;
|
|
1500
|
+
if (check.message)
|
|
1501
|
+
result += `\n\n${check.message}`;
|
|
1502
|
+
return result;
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
return `Error installing app: ${error.message}`;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function launchIosApp(bundleId, udid) {
|
|
1509
|
+
const check = await requireAdvanced("launch_ios_app");
|
|
1510
|
+
if (!check.allowed)
|
|
1511
|
+
return check.message;
|
|
1512
|
+
if (process.platform !== "darwin") {
|
|
1513
|
+
return "iOS Simulators are only available on macOS.";
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const target = udid || "booted";
|
|
1517
|
+
await execAsync(`xcrun simctl launch ${target} "${bundleId}"`);
|
|
1518
|
+
let result = `✅ Launched: ${bundleId}`;
|
|
1519
|
+
if (check.message)
|
|
1520
|
+
result += `\n\n${check.message}`;
|
|
1521
|
+
return result;
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
return `Error launching app: ${error.message}\n\nMake sure the bundle ID is correct and the app is installed.`;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function terminateIosApp(bundleId, udid) {
|
|
1528
|
+
const check = await requireAdvanced("terminate_ios_app");
|
|
1529
|
+
if (!check.allowed)
|
|
1530
|
+
return check.message;
|
|
1531
|
+
if (process.platform !== "darwin") {
|
|
1532
|
+
return "iOS Simulators are only available on macOS.";
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
const target = udid || "booted";
|
|
1536
|
+
await execAsync(`xcrun simctl terminate ${target} "${bundleId}"`);
|
|
1537
|
+
let result = `✅ Terminated: ${bundleId}`;
|
|
1538
|
+
if (check.message)
|
|
1539
|
+
result += `\n\n${check.message}`;
|
|
1540
|
+
return result;
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
return `Error terminating app: ${error.message}`;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async function iosOpenUrl(url, udid) {
|
|
1547
|
+
const check = await requireAdvanced("ios_open_url");
|
|
1548
|
+
if (!check.allowed)
|
|
1549
|
+
return check.message;
|
|
1550
|
+
if (process.platform !== "darwin") {
|
|
1551
|
+
return "iOS Simulators are only available on macOS.";
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const target = udid || "booted";
|
|
1555
|
+
await execAsync(`xcrun simctl openurl ${target} "${url}"`);
|
|
1556
|
+
let result = `✅ Opened URL: ${url}`;
|
|
1557
|
+
if (check.message)
|
|
1558
|
+
result += `\n\n${check.message}`;
|
|
1559
|
+
return result;
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
return `Error opening URL: ${error.message}`;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function iosPushNotification(bundleId, payload, udid) {
|
|
1566
|
+
const check = await requireAdvanced("ios_push_notification");
|
|
1567
|
+
if (!check.allowed)
|
|
1568
|
+
return check.message;
|
|
1569
|
+
if (process.platform !== "darwin") {
|
|
1570
|
+
return "iOS Simulators are only available on macOS.";
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
const target = udid || "booted";
|
|
1574
|
+
const payloadPath = path.join(CONFIG.screenshotDir, `push_${Date.now()}.json`);
|
|
1575
|
+
// Write payload to temp file
|
|
1576
|
+
fs.writeFileSync(payloadPath, JSON.stringify(payload));
|
|
1577
|
+
await execAsync(`xcrun simctl push ${target} "${bundleId}" "${payloadPath}"`);
|
|
1578
|
+
// Clean up
|
|
1579
|
+
fs.unlinkSync(payloadPath);
|
|
1580
|
+
let result = `✅ Push notification sent to ${bundleId}`;
|
|
1581
|
+
if (check.message)
|
|
1582
|
+
result += `\n\n${check.message}`;
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
return `Error sending push notification: ${error.message}`;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
async function iosSetLocation(latitude, longitude, udid) {
|
|
1590
|
+
const check = await requireAdvanced("ios_set_location");
|
|
1591
|
+
if (!check.allowed)
|
|
1592
|
+
return check.message;
|
|
1593
|
+
if (process.platform !== "darwin") {
|
|
1594
|
+
return "iOS Simulators are only available on macOS.";
|
|
1595
|
+
}
|
|
1596
|
+
try {
|
|
1597
|
+
const target = udid || "booted";
|
|
1598
|
+
await execAsync(`xcrun simctl location ${target} set ${latitude},${longitude}`);
|
|
1599
|
+
let result = `✅ Location set to: ${latitude}, ${longitude}`;
|
|
1600
|
+
if (check.message)
|
|
1601
|
+
result += `\n\n${check.message}`;
|
|
1602
|
+
return result;
|
|
1603
|
+
}
|
|
1604
|
+
catch (error) {
|
|
1605
|
+
return `Error setting location: ${error.message}`;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// ============================================================================
|
|
1609
|
+
// REACT DEVTOOLS INTEGRATION
|
|
1610
|
+
// ============================================================================
|
|
1611
|
+
// Store for DevTools WebSocket connection
|
|
1612
|
+
let devToolsWs = null;
|
|
1613
|
+
let devToolsConnected = false;
|
|
1614
|
+
let componentTree = new Map();
|
|
1615
|
+
let pendingRequests = new Map();
|
|
1616
|
+
let requestId = 0;
|
|
1617
|
+
async function setupReactDevTools(port = 8097, device) {
|
|
1618
|
+
const check = await requireAdvanced("setup_react_devtools");
|
|
1619
|
+
if (!check.allowed)
|
|
1620
|
+
return check.message;
|
|
1621
|
+
const results = [];
|
|
1622
|
+
results.push("🔧 React DevTools Setup\n" + "═".repeat(50));
|
|
1623
|
+
// Step 1: Set up ADB port forwarding (for Android)
|
|
1624
|
+
try {
|
|
1625
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
1626
|
+
await execAsync(`adb ${deviceFlag} reverse tcp:${port} tcp:${port}`);
|
|
1627
|
+
results.push(`\n✅ ADB port forwarding configured: tcp:${port} -> tcp:${port}`);
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
results.push(`\n⚠️ ADB port forwarding failed: ${error.message}`);
|
|
1631
|
+
results.push(" (This is OK if using iOS Simulator or DevTools is on same machine)");
|
|
1632
|
+
}
|
|
1633
|
+
// Step 2: Check if DevTools server is available
|
|
1634
|
+
try {
|
|
1635
|
+
const isRunning = await checkDevToolsServer(port);
|
|
1636
|
+
if (isRunning) {
|
|
1637
|
+
results.push(`✅ React DevTools server detected on port ${port}`);
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
results.push(`⚠️ React DevTools server not detected on port ${port}`);
|
|
1641
|
+
results.push("\nTo start React DevTools:");
|
|
1642
|
+
results.push(" npx react-devtools");
|
|
1643
|
+
results.push(" # or install globally:");
|
|
1644
|
+
results.push(" npm install -g react-devtools && react-devtools");
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
catch (error) {
|
|
1648
|
+
results.push(`⚠️ Could not check DevTools server: ${error.message}`);
|
|
1649
|
+
}
|
|
1650
|
+
// Step 3: Connection instructions
|
|
1651
|
+
results.push("\n📋 Next Steps:");
|
|
1652
|
+
results.push("1. Make sure React DevTools standalone is running (npx react-devtools)");
|
|
1653
|
+
results.push("2. Your React Native app should connect automatically in dev mode");
|
|
1654
|
+
results.push("3. Use 'check_devtools_connection' to verify the connection");
|
|
1655
|
+
results.push("4. Use 'get_react_component_tree' to inspect components");
|
|
1656
|
+
let result = results.join("\n");
|
|
1657
|
+
if (check.message)
|
|
1658
|
+
result += `\n\n${check.message}`;
|
|
1659
|
+
return result;
|
|
1660
|
+
}
|
|
1661
|
+
async function checkDevToolsServer(port) {
|
|
1662
|
+
return new Promise((resolve) => {
|
|
1663
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1664
|
+
const timeout = setTimeout(() => {
|
|
1665
|
+
ws.close();
|
|
1666
|
+
resolve(false);
|
|
1667
|
+
}, 3000);
|
|
1668
|
+
ws.on("open", () => {
|
|
1669
|
+
clearTimeout(timeout);
|
|
1670
|
+
ws.close();
|
|
1671
|
+
resolve(true);
|
|
1672
|
+
});
|
|
1673
|
+
ws.on("error", () => {
|
|
1674
|
+
clearTimeout(timeout);
|
|
1675
|
+
resolve(false);
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
async function checkDevToolsConnection(port = 8097) {
|
|
1680
|
+
const check = await requireAdvanced("check_devtools_connection");
|
|
1681
|
+
if (!check.allowed)
|
|
1682
|
+
return check.message;
|
|
1683
|
+
const results = [];
|
|
1684
|
+
results.push("🔍 React DevTools Connection Status\n" + "═".repeat(50));
|
|
1685
|
+
// Check if server is running
|
|
1686
|
+
const serverRunning = await checkDevToolsServer(port);
|
|
1687
|
+
if (!serverRunning) {
|
|
1688
|
+
results.push(`\n❌ DevTools server not found on port ${port}`);
|
|
1689
|
+
results.push("\nTo start React DevTools:");
|
|
1690
|
+
results.push(" npx react-devtools");
|
|
1691
|
+
let result = results.join("\n");
|
|
1692
|
+
if (check.message)
|
|
1693
|
+
result += `\n\n${check.message}`;
|
|
1694
|
+
return result;
|
|
1695
|
+
}
|
|
1696
|
+
results.push(`\n✅ DevTools server running on port ${port}`);
|
|
1697
|
+
// Try to connect and get basic info
|
|
1698
|
+
try {
|
|
1699
|
+
const connection = await connectToDevTools(port);
|
|
1700
|
+
if (connection.connected) {
|
|
1701
|
+
results.push("✅ Successfully connected to DevTools");
|
|
1702
|
+
if (connection.rendererCount > 0) {
|
|
1703
|
+
results.push(`✅ ${connection.rendererCount} React renderer(s) connected`);
|
|
1704
|
+
results.push("\n📱 App is connected and ready for inspection!");
|
|
1705
|
+
results.push(" Use 'get_react_component_tree' to view components");
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
results.push("⚠️ No React renderers connected");
|
|
1709
|
+
results.push("\nMake sure your React Native app is running in development mode.");
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
results.push("⚠️ Connected to server but no app detected");
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
catch (error) {
|
|
1717
|
+
results.push(`⚠️ Connection test failed: ${error.message}`);
|
|
1718
|
+
}
|
|
1719
|
+
let result = results.join("\n");
|
|
1720
|
+
if (check.message)
|
|
1721
|
+
result += `\n\n${check.message}`;
|
|
1722
|
+
return result;
|
|
1723
|
+
}
|
|
1724
|
+
async function connectToDevTools(port) {
|
|
1725
|
+
return new Promise((resolve, reject) => {
|
|
1726
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1727
|
+
let rendererCount = 0;
|
|
1728
|
+
let resolved = false;
|
|
1729
|
+
const timeout = setTimeout(() => {
|
|
1730
|
+
if (!resolved) {
|
|
1731
|
+
resolved = true;
|
|
1732
|
+
ws.close();
|
|
1733
|
+
resolve({ connected: true, rendererCount: 0 });
|
|
1734
|
+
}
|
|
1735
|
+
}, 5000);
|
|
1736
|
+
ws.on("open", () => {
|
|
1737
|
+
// DevTools protocol: request operations
|
|
1738
|
+
// The standalone DevTools uses a different protocol than we might expect
|
|
1739
|
+
// For now, we'll just confirm connection
|
|
1740
|
+
});
|
|
1741
|
+
ws.on("message", (data) => {
|
|
1742
|
+
try {
|
|
1743
|
+
const message = JSON.parse(data.toString());
|
|
1744
|
+
if (message.event === "operations" || message.event === "roots") {
|
|
1745
|
+
rendererCount++;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
catch { }
|
|
1749
|
+
});
|
|
1750
|
+
ws.on("close", () => {
|
|
1751
|
+
if (!resolved) {
|
|
1752
|
+
resolved = true;
|
|
1753
|
+
clearTimeout(timeout);
|
|
1754
|
+
resolve({ connected: true, rendererCount });
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
ws.on("error", (err) => {
|
|
1758
|
+
if (!resolved) {
|
|
1759
|
+
resolved = true;
|
|
1760
|
+
clearTimeout(timeout);
|
|
1761
|
+
reject(err);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
// Give it time to receive messages
|
|
1765
|
+
setTimeout(() => {
|
|
1766
|
+
if (!resolved) {
|
|
1767
|
+
resolved = true;
|
|
1768
|
+
clearTimeout(timeout);
|
|
1769
|
+
ws.close();
|
|
1770
|
+
resolve({ connected: true, rendererCount });
|
|
1771
|
+
}
|
|
1772
|
+
}, 2000);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
async function getReactComponentTree(port = 8097, depth = 5) {
|
|
1776
|
+
const check = await requireAdvanced("get_react_component_tree");
|
|
1777
|
+
if (!check.allowed)
|
|
1778
|
+
return check.message;
|
|
1779
|
+
try {
|
|
1780
|
+
const tree = await fetchComponentTree(port, depth);
|
|
1781
|
+
if (!tree || tree.length === 0) {
|
|
1782
|
+
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`;
|
|
1783
|
+
if (check.message)
|
|
1784
|
+
result += `\n\n${check.message}`;
|
|
1785
|
+
return result;
|
|
1786
|
+
}
|
|
1787
|
+
const lines = [];
|
|
1788
|
+
lines.push("📊 React Component Tree");
|
|
1789
|
+
lines.push("═".repeat(50));
|
|
1790
|
+
lines.push("");
|
|
1791
|
+
for (const node of tree) {
|
|
1792
|
+
const indent = " ".repeat(node.depth);
|
|
1793
|
+
const typeIcon = node.type === "function" ? "ƒ" : node.type === "class" ? "◆" : "○";
|
|
1794
|
+
lines.push(`${indent}${typeIcon} ${node.name} [id:${node.id}]`);
|
|
1795
|
+
if (node.key) {
|
|
1796
|
+
lines.push(`${indent} key: "${node.key}"`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
lines.push("");
|
|
1800
|
+
lines.push(`Total: ${tree.length} components (depth: ${depth})`);
|
|
1801
|
+
lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
|
|
1802
|
+
let result = lines.join("\n");
|
|
1803
|
+
if (check.message)
|
|
1804
|
+
result += `\n\n${check.message}`;
|
|
1805
|
+
return result;
|
|
1806
|
+
}
|
|
1807
|
+
catch (error) {
|
|
1808
|
+
let result = `Error fetching component tree: ${error.message}\n\nMake sure React DevTools is running: npx react-devtools`;
|
|
1809
|
+
if (check.message)
|
|
1810
|
+
result += `\n\n${check.message}`;
|
|
1811
|
+
return result;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
async function fetchComponentTree(port, maxDepth) {
|
|
1815
|
+
return new Promise((resolve, reject) => {
|
|
1816
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1817
|
+
const components = [];
|
|
1818
|
+
let resolved = false;
|
|
1819
|
+
const timeout = setTimeout(() => {
|
|
1820
|
+
if (!resolved) {
|
|
1821
|
+
resolved = true;
|
|
1822
|
+
ws.close();
|
|
1823
|
+
resolve(components);
|
|
1824
|
+
}
|
|
1825
|
+
}, 10000);
|
|
1826
|
+
ws.on("open", () => {
|
|
1827
|
+
// Send a request to get the tree
|
|
1828
|
+
// The DevTools protocol varies, so we listen for operations
|
|
1829
|
+
});
|
|
1830
|
+
ws.on("message", (data) => {
|
|
1831
|
+
try {
|
|
1832
|
+
const message = JSON.parse(data.toString());
|
|
1833
|
+
// Handle different message types from React DevTools
|
|
1834
|
+
if (message.event === "operations") {
|
|
1835
|
+
// Parse operations to build component tree
|
|
1836
|
+
const ops = message.payload;
|
|
1837
|
+
if (Array.isArray(ops)) {
|
|
1838
|
+
// Operations array contains component tree data
|
|
1839
|
+
parseOperations(ops, components, maxDepth);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
else if (message.event === "roots") {
|
|
1843
|
+
// Root components
|
|
1844
|
+
if (Array.isArray(message.payload)) {
|
|
1845
|
+
for (const root of message.payload) {
|
|
1846
|
+
components.push({
|
|
1847
|
+
id: root.id || components.length,
|
|
1848
|
+
name: root.displayName || root.name || "Root",
|
|
1849
|
+
type: root.type || "root",
|
|
1850
|
+
depth: 0,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
catch { }
|
|
1857
|
+
});
|
|
1858
|
+
ws.on("error", (err) => {
|
|
1859
|
+
if (!resolved) {
|
|
1860
|
+
resolved = true;
|
|
1861
|
+
clearTimeout(timeout);
|
|
1862
|
+
// Return empty array on error, not reject
|
|
1863
|
+
resolve(components);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
ws.on("close", () => {
|
|
1867
|
+
if (!resolved) {
|
|
1868
|
+
resolved = true;
|
|
1869
|
+
clearTimeout(timeout);
|
|
1870
|
+
resolve(components);
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
// Give time to receive data
|
|
1874
|
+
setTimeout(() => {
|
|
1875
|
+
if (!resolved) {
|
|
1876
|
+
resolved = true;
|
|
1877
|
+
clearTimeout(timeout);
|
|
1878
|
+
ws.close();
|
|
1879
|
+
resolve(components);
|
|
1880
|
+
}
|
|
1881
|
+
}, 3000);
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
function parseOperations(ops, components, maxDepth) {
|
|
1885
|
+
// React DevTools operations format varies by version
|
|
1886
|
+
// This is a simplified parser
|
|
1887
|
+
let depth = 0;
|
|
1888
|
+
for (let i = 0; i < ops.length && components.length < 100; i++) {
|
|
1889
|
+
const op = ops[i];
|
|
1890
|
+
if (typeof op === "object" && op.id !== undefined) {
|
|
1891
|
+
if (depth <= maxDepth) {
|
|
1892
|
+
components.push({
|
|
1893
|
+
id: op.id,
|
|
1894
|
+
name: op.displayName || op.name || `Component_${op.id}`,
|
|
1895
|
+
type: op.type === 1 ? "function" : op.type === 2 ? "class" : "other",
|
|
1896
|
+
depth: Math.min(op.depth || depth, maxDepth),
|
|
1897
|
+
key: op.key,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async function inspectReactComponent(componentId, port = 8097) {
|
|
1904
|
+
const check = await requireAdvanced("inspect_react_component");
|
|
1905
|
+
if (!check.allowed)
|
|
1906
|
+
return check.message;
|
|
1907
|
+
try {
|
|
1908
|
+
const inspection = await fetchComponentDetails(componentId, port);
|
|
1909
|
+
if (!inspection) {
|
|
1910
|
+
let result = `Component with ID ${componentId} not found.\n\nUse 'get_react_component_tree' to get valid component IDs.`;
|
|
1911
|
+
if (check.message)
|
|
1912
|
+
result += `\n\n${check.message}`;
|
|
1913
|
+
return result;
|
|
1914
|
+
}
|
|
1915
|
+
const lines = [];
|
|
1916
|
+
lines.push(`🔍 Component Inspection: ${inspection.name}`);
|
|
1917
|
+
lines.push("═".repeat(50));
|
|
1918
|
+
lines.push(`ID: ${componentId}`);
|
|
1919
|
+
lines.push(`Type: ${inspection.type}`);
|
|
1920
|
+
if (inspection.props && Object.keys(inspection.props).length > 0) {
|
|
1921
|
+
lines.push("\n📦 Props:");
|
|
1922
|
+
for (const [key, value] of Object.entries(inspection.props)) {
|
|
1923
|
+
const displayValue = formatValue(value);
|
|
1924
|
+
lines.push(` ${key}: ${displayValue}`);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
lines.push("\n📦 Props: (none)");
|
|
1929
|
+
}
|
|
1930
|
+
if (inspection.state && Object.keys(inspection.state).length > 0) {
|
|
1931
|
+
lines.push("\n💾 State:");
|
|
1932
|
+
for (const [key, value] of Object.entries(inspection.state)) {
|
|
1933
|
+
const displayValue = formatValue(value);
|
|
1934
|
+
lines.push(` ${key}: ${displayValue}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (inspection.hooks && inspection.hooks.length > 0) {
|
|
1938
|
+
lines.push("\n🪝 Hooks:");
|
|
1939
|
+
for (const hook of inspection.hooks) {
|
|
1940
|
+
lines.push(` ${hook.name}: ${formatValue(hook.value)}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
let result = lines.join("\n");
|
|
1944
|
+
if (check.message)
|
|
1945
|
+
result += `\n\n${check.message}`;
|
|
1946
|
+
return result;
|
|
1947
|
+
}
|
|
1948
|
+
catch (error) {
|
|
1949
|
+
let result = `Error inspecting component: ${error.message}`;
|
|
1950
|
+
if (check.message)
|
|
1951
|
+
result += `\n\n${check.message}`;
|
|
1952
|
+
return result;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function formatValue(value) {
|
|
1956
|
+
if (value === null)
|
|
1957
|
+
return "null";
|
|
1958
|
+
if (value === undefined)
|
|
1959
|
+
return "undefined";
|
|
1960
|
+
if (typeof value === "string")
|
|
1961
|
+
return `"${value.substring(0, 100)}${value.length > 100 ? "..." : ""}"`;
|
|
1962
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1963
|
+
return String(value);
|
|
1964
|
+
if (Array.isArray(value))
|
|
1965
|
+
return `Array(${value.length})`;
|
|
1966
|
+
if (typeof value === "object") {
|
|
1967
|
+
const keys = Object.keys(value);
|
|
1968
|
+
if (keys.length === 0)
|
|
1969
|
+
return "{}";
|
|
1970
|
+
return `{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof value === "function")
|
|
1973
|
+
return "ƒ()";
|
|
1974
|
+
return String(value).substring(0, 50);
|
|
1975
|
+
}
|
|
1976
|
+
async function fetchComponentDetails(componentId, port) {
|
|
1977
|
+
return new Promise((resolve) => {
|
|
1978
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
1979
|
+
let resolved = false;
|
|
1980
|
+
let details = null;
|
|
1981
|
+
const timeout = setTimeout(() => {
|
|
1982
|
+
if (!resolved) {
|
|
1983
|
+
resolved = true;
|
|
1984
|
+
ws.close();
|
|
1985
|
+
resolve(details);
|
|
1986
|
+
}
|
|
1987
|
+
}, 5000);
|
|
1988
|
+
ws.on("open", () => {
|
|
1989
|
+
// Request inspection of specific element
|
|
1990
|
+
const request = {
|
|
1991
|
+
event: "inspectElement",
|
|
1992
|
+
payload: {
|
|
1993
|
+
id: componentId,
|
|
1994
|
+
rendererID: 1,
|
|
1995
|
+
requestID: Date.now(),
|
|
1996
|
+
},
|
|
1997
|
+
};
|
|
1998
|
+
ws.send(JSON.stringify(request));
|
|
1999
|
+
});
|
|
2000
|
+
ws.on("message", (data) => {
|
|
2001
|
+
try {
|
|
2002
|
+
const message = JSON.parse(data.toString());
|
|
2003
|
+
if (message.event === "inspectedElement" && message.payload) {
|
|
2004
|
+
const p = message.payload;
|
|
2005
|
+
details = {
|
|
2006
|
+
name: p.displayName || p.name || `Component_${componentId}`,
|
|
2007
|
+
type: p.type || "unknown",
|
|
2008
|
+
props: p.props || {},
|
|
2009
|
+
state: p.state,
|
|
2010
|
+
hooks: p.hooks,
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
catch { }
|
|
2015
|
+
});
|
|
2016
|
+
ws.on("error", () => {
|
|
2017
|
+
if (!resolved) {
|
|
2018
|
+
resolved = true;
|
|
2019
|
+
clearTimeout(timeout);
|
|
2020
|
+
resolve(null);
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
ws.on("close", () => {
|
|
2024
|
+
if (!resolved) {
|
|
2025
|
+
resolved = true;
|
|
2026
|
+
clearTimeout(timeout);
|
|
2027
|
+
resolve(details);
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
// Give time to receive response
|
|
2031
|
+
setTimeout(() => {
|
|
2032
|
+
if (!resolved) {
|
|
2033
|
+
resolved = true;
|
|
2034
|
+
clearTimeout(timeout);
|
|
2035
|
+
ws.close();
|
|
2036
|
+
resolve(details);
|
|
2037
|
+
}
|
|
2038
|
+
}, 3000);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
async function searchReactComponents(query, port = 8097) {
|
|
2042
|
+
const check = await requireAdvanced("search_react_components");
|
|
2043
|
+
if (!check.allowed)
|
|
2044
|
+
return check.message;
|
|
2045
|
+
try {
|
|
2046
|
+
const tree = await fetchComponentTree(port, 10);
|
|
2047
|
+
const queryLower = query.toLowerCase();
|
|
2048
|
+
const matches = tree.filter((component) => component.name.toLowerCase().includes(queryLower));
|
|
2049
|
+
if (matches.length === 0) {
|
|
2050
|
+
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.`;
|
|
2051
|
+
if (check.message)
|
|
2052
|
+
result += `\n\n${check.message}`;
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
const lines = [];
|
|
2056
|
+
lines.push(`🔍 Search Results for "${query}"`);
|
|
2057
|
+
lines.push("═".repeat(50));
|
|
2058
|
+
lines.push(`\nFound ${matches.length} matching component(s):\n`);
|
|
2059
|
+
for (const match of matches.slice(0, 20)) {
|
|
2060
|
+
const typeIcon = match.type === "function" ? "ƒ" : match.type === "class" ? "◆" : "○";
|
|
2061
|
+
lines.push(`${typeIcon} ${match.name} [id:${match.id}]`);
|
|
2062
|
+
}
|
|
2063
|
+
if (matches.length > 20) {
|
|
2064
|
+
lines.push(`\n... and ${matches.length - 20} more matches`);
|
|
2065
|
+
}
|
|
2066
|
+
lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
|
|
2067
|
+
let result = lines.join("\n");
|
|
2068
|
+
if (check.message)
|
|
2069
|
+
result += `\n\n${check.message}`;
|
|
2070
|
+
return result;
|
|
2071
|
+
}
|
|
2072
|
+
catch (error) {
|
|
2073
|
+
let result = `Error searching components: ${error.message}`;
|
|
2074
|
+
if (check.message)
|
|
2075
|
+
result += `\n\n${check.message}`;
|
|
2076
|
+
return result;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
// ============================================================================
|
|
2080
|
+
// MCP SERVER SETUP
|
|
2081
|
+
// ============================================================================
|
|
2082
|
+
const server = new Server({
|
|
2083
|
+
name: "claude-mobile-dev-mcp",
|
|
2084
|
+
version: "0.1.0",
|
|
2085
|
+
}, {
|
|
2086
|
+
capabilities: {
|
|
2087
|
+
tools: {},
|
|
2088
|
+
},
|
|
2089
|
+
});
|
|
2090
|
+
// Handle tool listing
|
|
2091
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2092
|
+
tools,
|
|
2093
|
+
}));
|
|
2094
|
+
// Handle tool execution
|
|
2095
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2096
|
+
const { name, arguments: args } = request.params;
|
|
2097
|
+
try {
|
|
2098
|
+
// License tools
|
|
2099
|
+
if (name === "get_license_status" || name === "set_license_key") {
|
|
2100
|
+
const result = await handleLicenseTool(name, args || {});
|
|
2101
|
+
return { content: [{ type: "text", text: result }] };
|
|
2102
|
+
}
|
|
2103
|
+
// Core tools
|
|
2104
|
+
switch (name) {
|
|
2105
|
+
case "get_metro_logs": {
|
|
2106
|
+
const result = await getMetroLogs(args?.lines, args?.filter);
|
|
2107
|
+
return { content: [{ type: "text", text: result }] };
|
|
2108
|
+
}
|
|
2109
|
+
case "get_adb_logs": {
|
|
2110
|
+
const result = await getAdbLogs(args?.lines, args?.filter, args?.level);
|
|
2111
|
+
return { content: [{ type: "text", text: result }] };
|
|
2112
|
+
}
|
|
2113
|
+
case "screenshot_emulator": {
|
|
2114
|
+
const result = await screenshotEmulator(args?.device);
|
|
2115
|
+
if (result.success && result.data) {
|
|
2116
|
+
const content = [
|
|
2117
|
+
{
|
|
2118
|
+
type: "image",
|
|
2119
|
+
data: result.data,
|
|
2120
|
+
mimeType: result.mimeType,
|
|
2121
|
+
},
|
|
2122
|
+
];
|
|
2123
|
+
// Add trial warning if present
|
|
2124
|
+
if (result.trialMessage) {
|
|
2125
|
+
content.push({ type: "text", text: result.trialMessage });
|
|
2126
|
+
}
|
|
2127
|
+
return { content };
|
|
2128
|
+
}
|
|
2129
|
+
return { content: [{ type: "text", text: result.error }] };
|
|
2130
|
+
}
|
|
2131
|
+
case "list_devices": {
|
|
2132
|
+
const result = await listDevices();
|
|
2133
|
+
return { content: [{ type: "text", text: result }] };
|
|
2134
|
+
}
|
|
2135
|
+
case "check_metro_status": {
|
|
2136
|
+
const result = await checkMetroStatus(args?.port);
|
|
2137
|
+
return { content: [{ type: "text", text: result }] };
|
|
2138
|
+
}
|
|
2139
|
+
case "get_app_info": {
|
|
2140
|
+
const result = await getAppInfo(args?.packageName);
|
|
2141
|
+
return { content: [{ type: "text", text: result }] };
|
|
2142
|
+
}
|
|
2143
|
+
case "clear_app_data": {
|
|
2144
|
+
const result = await clearAppData(args?.packageName);
|
|
2145
|
+
return { content: [{ type: "text", text: result }] };
|
|
2146
|
+
}
|
|
2147
|
+
case "restart_adb": {
|
|
2148
|
+
const result = await restartAdb();
|
|
2149
|
+
return { content: [{ type: "text", text: result }] };
|
|
2150
|
+
}
|
|
2151
|
+
case "get_device_info": {
|
|
2152
|
+
const result = await getDeviceInfo(args?.device);
|
|
2153
|
+
return { content: [{ type: "text", text: result }] };
|
|
2154
|
+
}
|
|
2155
|
+
case "start_metro_logging": {
|
|
2156
|
+
const result = await startMetroLogging(args?.logFile);
|
|
2157
|
+
return { content: [{ type: "text", text: result }] };
|
|
2158
|
+
}
|
|
2159
|
+
case "stop_metro_logging": {
|
|
2160
|
+
const result = await stopMetroLogging();
|
|
2161
|
+
return { content: [{ type: "text", text: result }] };
|
|
2162
|
+
}
|
|
2163
|
+
// PRO FEATURES
|
|
2164
|
+
case "stream_adb_realtime": {
|
|
2165
|
+
const result = await streamAdbRealtime(args?.filter);
|
|
2166
|
+
return { content: [{ type: "text", text: result }] };
|
|
2167
|
+
}
|
|
2168
|
+
case "stop_adb_streaming": {
|
|
2169
|
+
const result = stopAdbStreaming();
|
|
2170
|
+
return { content: [{ type: "text", text: result }] };
|
|
2171
|
+
}
|
|
2172
|
+
case "screenshot_history": {
|
|
2173
|
+
const result = await getScreenshotHistory(args?.count);
|
|
2174
|
+
return { content: [{ type: "text", text: result }] };
|
|
2175
|
+
}
|
|
2176
|
+
case "watch_for_errors": {
|
|
2177
|
+
const result = await watchForErrors(args?.patterns, args?.timeout);
|
|
2178
|
+
return { content: [{ type: "text", text: result }] };
|
|
2179
|
+
}
|
|
2180
|
+
case "multi_device_logs": {
|
|
2181
|
+
const result = await multiDeviceLogs(args?.devices, args?.lines);
|
|
2182
|
+
return { content: [{ type: "text", text: result }] };
|
|
2183
|
+
}
|
|
2184
|
+
// INTERACTION TOOLS
|
|
2185
|
+
case "tap_screen": {
|
|
2186
|
+
const result = await tapScreen(args?.x, args?.y, args?.device);
|
|
2187
|
+
return { content: [{ type: "text", text: result }] };
|
|
2188
|
+
}
|
|
2189
|
+
case "input_text": {
|
|
2190
|
+
const result = await inputText(args?.text, args?.device);
|
|
2191
|
+
return { content: [{ type: "text", text: result }] };
|
|
2192
|
+
}
|
|
2193
|
+
case "press_button": {
|
|
2194
|
+
const result = await pressButton(args?.button, args?.device);
|
|
2195
|
+
return { content: [{ type: "text", text: result }] };
|
|
2196
|
+
}
|
|
2197
|
+
case "swipe_screen": {
|
|
2198
|
+
const result = await swipeScreen(args?.startX, args?.startY, args?.endX, args?.endY, args?.duration, args?.device);
|
|
2199
|
+
return { content: [{ type: "text", text: result }] };
|
|
2200
|
+
}
|
|
2201
|
+
case "launch_app": {
|
|
2202
|
+
const result = await launchApp(args?.packageName, args?.device);
|
|
2203
|
+
return { content: [{ type: "text", text: result }] };
|
|
2204
|
+
}
|
|
2205
|
+
case "install_apk": {
|
|
2206
|
+
const result = await installApk(args?.apkPath, args?.device);
|
|
2207
|
+
return { content: [{ type: "text", text: result }] };
|
|
2208
|
+
}
|
|
2209
|
+
// iOS SIMULATOR TOOLS
|
|
2210
|
+
case "list_ios_simulators": {
|
|
2211
|
+
const result = await listIosSimulators(args?.onlyBooted);
|
|
2212
|
+
return { content: [{ type: "text", text: result }] };
|
|
2213
|
+
}
|
|
2214
|
+
case "screenshot_ios_simulator": {
|
|
2215
|
+
const result = await screenshotIosSimulator(args?.udid);
|
|
2216
|
+
if (result.success && result.data) {
|
|
2217
|
+
const content = [
|
|
2218
|
+
{
|
|
2219
|
+
type: "image",
|
|
2220
|
+
data: result.data,
|
|
2221
|
+
mimeType: result.mimeType,
|
|
2222
|
+
},
|
|
2223
|
+
];
|
|
2224
|
+
if (result.trialMessage) {
|
|
2225
|
+
content.push({ type: "text", text: result.trialMessage });
|
|
2226
|
+
}
|
|
2227
|
+
return { content };
|
|
2228
|
+
}
|
|
2229
|
+
return { content: [{ type: "text", text: result.error }] };
|
|
2230
|
+
}
|
|
2231
|
+
case "get_ios_simulator_logs": {
|
|
2232
|
+
const result = await getIosSimulatorLogs(args?.udid, args?.filter, args?.lines);
|
|
2233
|
+
return { content: [{ type: "text", text: result }] };
|
|
2234
|
+
}
|
|
2235
|
+
case "get_ios_simulator_info": {
|
|
2236
|
+
const result = await getIosSimulatorInfo(args?.udid);
|
|
2237
|
+
return { content: [{ type: "text", text: result }] };
|
|
2238
|
+
}
|
|
2239
|
+
case "boot_ios_simulator": {
|
|
2240
|
+
const result = await bootIosSimulator(args?.udid);
|
|
2241
|
+
return { content: [{ type: "text", text: result }] };
|
|
2242
|
+
}
|
|
2243
|
+
case "shutdown_ios_simulator": {
|
|
2244
|
+
const result = await shutdownIosSimulator(args?.udid);
|
|
2245
|
+
return { content: [{ type: "text", text: result }] };
|
|
2246
|
+
}
|
|
2247
|
+
case "install_ios_app": {
|
|
2248
|
+
const result = await installIosApp(args?.appPath, args?.udid);
|
|
2249
|
+
return { content: [{ type: "text", text: result }] };
|
|
2250
|
+
}
|
|
2251
|
+
case "launch_ios_app": {
|
|
2252
|
+
const result = await launchIosApp(args?.bundleId, args?.udid);
|
|
2253
|
+
return { content: [{ type: "text", text: result }] };
|
|
2254
|
+
}
|
|
2255
|
+
case "terminate_ios_app": {
|
|
2256
|
+
const result = await terminateIosApp(args?.bundleId, args?.udid);
|
|
2257
|
+
return { content: [{ type: "text", text: result }] };
|
|
2258
|
+
}
|
|
2259
|
+
case "ios_open_url": {
|
|
2260
|
+
const result = await iosOpenUrl(args?.url, args?.udid);
|
|
2261
|
+
return { content: [{ type: "text", text: result }] };
|
|
2262
|
+
}
|
|
2263
|
+
case "ios_push_notification": {
|
|
2264
|
+
const result = await iosPushNotification(args?.bundleId, args?.payload, args?.udid);
|
|
2265
|
+
return { content: [{ type: "text", text: result }] };
|
|
2266
|
+
}
|
|
2267
|
+
case "ios_set_location": {
|
|
2268
|
+
const result = await iosSetLocation(args?.latitude, args?.longitude, args?.udid);
|
|
2269
|
+
return { content: [{ type: "text", text: result }] };
|
|
2270
|
+
}
|
|
2271
|
+
// REACT DEVTOOLS TOOLS
|
|
2272
|
+
case "setup_react_devtools": {
|
|
2273
|
+
const result = await setupReactDevTools(args?.port, args?.device);
|
|
2274
|
+
return { content: [{ type: "text", text: result }] };
|
|
2275
|
+
}
|
|
2276
|
+
case "check_devtools_connection": {
|
|
2277
|
+
const result = await checkDevToolsConnection(args?.port);
|
|
2278
|
+
return { content: [{ type: "text", text: result }] };
|
|
2279
|
+
}
|
|
2280
|
+
case "get_react_component_tree": {
|
|
2281
|
+
const result = await getReactComponentTree(args?.port, args?.depth);
|
|
2282
|
+
return { content: [{ type: "text", text: result }] };
|
|
2283
|
+
}
|
|
2284
|
+
case "inspect_react_component": {
|
|
2285
|
+
const result = await inspectReactComponent(args?.componentId, args?.port);
|
|
2286
|
+
return { content: [{ type: "text", text: result }] };
|
|
2287
|
+
}
|
|
2288
|
+
case "search_react_components": {
|
|
2289
|
+
const result = await searchReactComponents(args?.query, args?.port);
|
|
2290
|
+
return { content: [{ type: "text", text: result }] };
|
|
2291
|
+
}
|
|
2292
|
+
default:
|
|
2293
|
+
return {
|
|
2294
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
2295
|
+
isError: true,
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
catch (error) {
|
|
2300
|
+
return {
|
|
2301
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
2302
|
+
isError: true,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
// Start the server
|
|
2307
|
+
async function main() {
|
|
2308
|
+
const transport = new StdioServerTransport();
|
|
2309
|
+
await server.connect(transport);
|
|
2310
|
+
// Log startup info to stderr (won't interfere with MCP protocol)
|
|
2311
|
+
const license = await checkLicense();
|
|
2312
|
+
console.error(`Mobile Dev MCP Server v0.1.0`);
|
|
2313
|
+
console.error(`License: ${license.tier.toUpperCase()}`);
|
|
2314
|
+
console.error(`Ready for connections...`);
|
|
2315
|
+
}
|
|
2316
|
+
main().catch((error) => {
|
|
2317
|
+
console.error("Fatal error:", error);
|
|
2318
|
+
process.exit(1);
|
|
2319
|
+
});
|
|
2320
|
+
//# sourceMappingURL=index.js.map
|