@ggboi360/mobile-dev-mcp 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -682,6 +682,245 @@ const coreTools = [
682
682
  required: ["query"],
683
683
  },
684
684
  },
685
+ // === NETWORK INSPECTION TOOLS ===
686
+ {
687
+ name: "get_network_requests",
688
+ description: "[PRO] Get recent network requests from app logs. Parses fetch/XHR requests from React Native logs.",
689
+ inputSchema: {
690
+ type: "object",
691
+ properties: {
692
+ lines: {
693
+ type: "number",
694
+ description: "Number of log lines to search through (default: 200)",
695
+ default: 200,
696
+ },
697
+ filter: {
698
+ type: "string",
699
+ description: "Filter by URL pattern or method (e.g., 'api', 'POST')",
700
+ },
701
+ device: {
702
+ type: "string",
703
+ description: "Specific device ID (optional)",
704
+ },
705
+ },
706
+ },
707
+ },
708
+ {
709
+ name: "start_network_monitoring",
710
+ description: "[PRO] Start real-time network request monitoring. Captures all HTTP/HTTPS traffic in background.",
711
+ inputSchema: {
712
+ type: "object",
713
+ properties: {
714
+ device: {
715
+ type: "string",
716
+ description: "Specific device ID (optional)",
717
+ },
718
+ },
719
+ },
720
+ },
721
+ {
722
+ name: "stop_network_monitoring",
723
+ description: "[PRO] Stop network monitoring and get summary of captured requests.",
724
+ inputSchema: {
725
+ type: "object",
726
+ properties: {},
727
+ },
728
+ },
729
+ {
730
+ name: "get_network_stats",
731
+ description: "[PRO] Get device network statistics including data usage, active connections, and WiFi info.",
732
+ inputSchema: {
733
+ type: "object",
734
+ properties: {
735
+ device: {
736
+ type: "string",
737
+ description: "Specific device ID (optional)",
738
+ },
739
+ },
740
+ },
741
+ },
742
+ {
743
+ name: "analyze_request",
744
+ description: "[PRO] Analyze a specific network request by index from captured requests. Shows headers, body, timing.",
745
+ inputSchema: {
746
+ type: "object",
747
+ properties: {
748
+ index: {
749
+ type: "number",
750
+ description: "Request index from get_network_requests or monitoring",
751
+ },
752
+ },
753
+ required: ["index"],
754
+ },
755
+ },
756
+ // === EXPO DEVTOOLS INTEGRATION ===
757
+ {
758
+ name: "check_expo_status",
759
+ description: "[PRO] Check Expo dev server status. Shows bundler status, dev client connection, and tunnel URLs.",
760
+ inputSchema: {
761
+ type: "object",
762
+ properties: {
763
+ port: {
764
+ type: "number",
765
+ description: "Expo bundler port (default: 8081 for Expo SDK 49+, 19000 for older)",
766
+ default: 8081,
767
+ },
768
+ },
769
+ },
770
+ },
771
+ {
772
+ name: "get_expo_config",
773
+ description: "[PRO] Get Expo project configuration from app.json or app.config.js. Shows app name, version, plugins, and more.",
774
+ inputSchema: {
775
+ type: "object",
776
+ properties: {
777
+ projectPath: {
778
+ type: "string",
779
+ description: "Path to Expo project directory (default: current directory)",
780
+ },
781
+ },
782
+ },
783
+ },
784
+ {
785
+ name: "expo_dev_menu",
786
+ description: "[PRO] Open the Expo developer menu on the connected device. Equivalent to shaking the device or pressing 'd' in terminal.",
787
+ inputSchema: {
788
+ type: "object",
789
+ properties: {
790
+ device: {
791
+ type: "string",
792
+ description: "Specific device ID (optional)",
793
+ },
794
+ },
795
+ },
796
+ },
797
+ {
798
+ name: "expo_reload",
799
+ description: "[PRO] Trigger a reload of the Expo app. Refreshes the JavaScript bundle without a full rebuild.",
800
+ inputSchema: {
801
+ type: "object",
802
+ properties: {
803
+ device: {
804
+ type: "string",
805
+ description: "Specific device ID (optional)",
806
+ },
807
+ },
808
+ },
809
+ },
810
+ {
811
+ name: "get_eas_builds",
812
+ description: "[PRO] Get recent EAS (Expo Application Services) build status. Shows build history for your project.",
813
+ inputSchema: {
814
+ type: "object",
815
+ properties: {
816
+ platform: {
817
+ type: "string",
818
+ enum: ["android", "ios", "all"],
819
+ description: "Platform to show builds for (default: all)",
820
+ default: "all",
821
+ },
822
+ limit: {
823
+ type: "number",
824
+ description: "Maximum number of builds to show (default: 5)",
825
+ default: 5,
826
+ },
827
+ },
828
+ },
829
+ },
830
+ // === PERFORMANCE METRICS TOOLS ===
831
+ {
832
+ name: "get_cpu_usage",
833
+ description: "[PRO] Get CPU usage for device or specific app. Shows per-core and per-process CPU consumption.",
834
+ inputSchema: {
835
+ type: "object",
836
+ properties: {
837
+ packageName: {
838
+ type: "string",
839
+ description: "App package name to filter (optional, shows all if not specified)",
840
+ },
841
+ device: {
842
+ type: "string",
843
+ description: "Specific device ID (optional)",
844
+ },
845
+ },
846
+ },
847
+ },
848
+ {
849
+ name: "get_memory_usage",
850
+ description: "[PRO] Get memory usage for a specific app. Shows heap, native, graphics, and total memory consumption.",
851
+ inputSchema: {
852
+ type: "object",
853
+ properties: {
854
+ packageName: {
855
+ type: "string",
856
+ description: "App package name (required)",
857
+ },
858
+ device: {
859
+ type: "string",
860
+ description: "Specific device ID (optional)",
861
+ },
862
+ },
863
+ required: ["packageName"],
864
+ },
865
+ },
866
+ {
867
+ name: "get_fps_stats",
868
+ description: "[PRO] Get frame rendering statistics (FPS). Shows jank frames, slow renders, and frame timing histogram.",
869
+ inputSchema: {
870
+ type: "object",
871
+ properties: {
872
+ packageName: {
873
+ type: "string",
874
+ description: "App package name (required)",
875
+ },
876
+ device: {
877
+ type: "string",
878
+ description: "Specific device ID (optional)",
879
+ },
880
+ reset: {
881
+ type: "boolean",
882
+ description: "Reset stats before measuring (default: false)",
883
+ default: false,
884
+ },
885
+ },
886
+ required: ["packageName"],
887
+ },
888
+ },
889
+ {
890
+ name: "get_battery_stats",
891
+ description: "[PRO] Get battery consumption statistics. Shows power usage by app and component.",
892
+ inputSchema: {
893
+ type: "object",
894
+ properties: {
895
+ packageName: {
896
+ type: "string",
897
+ description: "App package name to filter (optional)",
898
+ },
899
+ device: {
900
+ type: "string",
901
+ description: "Specific device ID (optional)",
902
+ },
903
+ },
904
+ },
905
+ },
906
+ {
907
+ name: "get_performance_snapshot",
908
+ description: "[PRO] Get a comprehensive performance snapshot including CPU, memory, FPS, and battery stats for an app.",
909
+ inputSchema: {
910
+ type: "object",
911
+ properties: {
912
+ packageName: {
913
+ type: "string",
914
+ description: "App package name (required)",
915
+ },
916
+ device: {
917
+ type: "string",
918
+ description: "Specific device ID (optional)",
919
+ },
920
+ },
921
+ required: ["packageName"],
922
+ },
923
+ },
685
924
  ];
686
925
  // Combine core tools with license tools
687
926
  const tools = [...coreTools, ...licenseTools];
@@ -2077,59 +2316,1112 @@ async function searchReactComponents(query, port = 8097) {
2077
2316
  }
2078
2317
  }
2079
2318
  // ============================================================================
2080
- // MCP SERVER SETUP
2319
+ // NETWORK INSPECTION
2081
2320
  // ============================================================================
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;
2321
+ // Network monitoring state
2322
+ let networkMonitorProcess = null;
2323
+ let networkRequestBuffer = [];
2324
+ const MAX_NETWORK_REQUESTS = 100;
2325
+ async function getNetworkRequests(lines = 200, filter, device) {
2326
+ const check = await requireAdvanced("get_network_requests");
2327
+ if (!check.allowed)
2328
+ return check.message;
2097
2329
  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 }] };
2330
+ const deviceFlag = device ? `-s ${device}` : "";
2331
+ // Get logs and filter for network-related entries
2332
+ // React Native logs network requests with various patterns
2333
+ const { stdout } = await execAsync(`adb ${deviceFlag} logcat -d -t ${lines} *:V`, { maxBuffer: 5 * 1024 * 1024 });
2334
+ const requests = [];
2335
+ const logLines = stdout.split("\n");
2336
+ // Patterns to match network requests
2337
+ const patterns = [
2338
+ // OkHttp pattern
2339
+ /OkHttp.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
2340
+ // React Native fetch pattern
2341
+ /\[fetch\].*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
2342
+ // Generic HTTP pattern
2343
+ /HTTP.*?(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i,
2344
+ // XMLHttpRequest pattern
2345
+ /XMLHttpRequest.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
2346
+ // Network response pattern
2347
+ /(\d{3})\s+(https?:\/\/\S+).*?(\d+ms|\d+\.\d+s)/i,
2348
+ ];
2349
+ let index = 0;
2350
+ for (const line of logLines) {
2351
+ for (const pattern of patterns) {
2352
+ const match = line.match(pattern);
2353
+ if (match) {
2354
+ const method = match[1]?.toUpperCase() || "GET";
2355
+ const url = match[2] || "";
2356
+ // Apply filter if provided
2357
+ if (filter) {
2358
+ const filterLower = filter.toLowerCase();
2359
+ if (!method.toLowerCase().includes(filterLower) &&
2360
+ !url.toLowerCase().includes(filterLower)) {
2361
+ continue;
2362
+ }
2363
+ }
2364
+ requests.push({
2365
+ index: index++,
2366
+ timestamp: new Date().toISOString(),
2367
+ method,
2368
+ url: url.substring(0, 200),
2369
+ status: match[1]?.match(/^\d{3}$/) ? parseInt(match[1]) : undefined,
2370
+ });
2371
+ break;
2372
+ }
2373
+ }
2102
2374
  }
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 }] };
2375
+ if (requests.length === 0) {
2376
+ let result = `šŸ“” Network Requests\n${"═".repeat(50)}\n\nNo network requests found in recent logs.\n\nTips:\n- Make sure your app is making network requests\n- React Native logs network activity in development mode\n- Try increasing the lines parameter`;
2377
+ if (check.message)
2378
+ result += `\n\n${check.message}`;
2379
+ return result;
2380
+ }
2381
+ const resultLines = [];
2382
+ resultLines.push("šŸ“” Network Requests");
2383
+ resultLines.push("═".repeat(50));
2384
+ resultLines.push(`\nFound ${requests.length} request(s):\n`);
2385
+ for (const req of requests.slice(0, 30)) {
2386
+ const statusIcon = req.status
2387
+ ? (req.status >= 200 && req.status < 300 ? "āœ…" : "āŒ")
2388
+ : "ā³";
2389
+ resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 60)}${req.url.length > 60 ? "..." : ""}`);
2390
+ if (req.status) {
2391
+ resultLines.push(` Status: ${req.status}`);
2108
2392
  }
2109
- case "get_adb_logs": {
2110
- const result = await getAdbLogs(args?.lines, args?.filter, args?.level);
2111
- return { content: [{ type: "text", text: result }] };
2393
+ }
2394
+ if (requests.length > 30) {
2395
+ resultLines.push(`\n... and ${requests.length - 30} more requests`);
2396
+ }
2397
+ resultLines.push("\nUse 'start_network_monitoring' for real-time capture");
2398
+ let result = resultLines.join("\n");
2399
+ if (check.message)
2400
+ result += `\n\n${check.message}`;
2401
+ return result;
2402
+ }
2403
+ catch (error) {
2404
+ return `Error getting network requests: ${error.message}`;
2405
+ }
2406
+ }
2407
+ async function startNetworkMonitoring(device) {
2408
+ const check = await requireAdvanced("start_network_monitoring");
2409
+ if (!check.allowed)
2410
+ return check.message;
2411
+ if (networkMonitorProcess) {
2412
+ return "Network monitoring is already running. Use 'stop_network_monitoring' first.";
2413
+ }
2414
+ networkRequestBuffer = [];
2415
+ const deviceFlag = device ? `-s ${device}` : "";
2416
+ // Start logcat with network-related filters
2417
+ networkMonitorProcess = spawn("adb", [
2418
+ ...deviceFlag.split(" ").filter(s => s),
2419
+ "logcat",
2420
+ "-v", "time",
2421
+ "OkHttp:V",
2422
+ "Retrofit:V",
2423
+ "ReactNativeJS:V",
2424
+ "*:S"
2425
+ ]);
2426
+ let requestIndex = 0;
2427
+ networkMonitorProcess.stdout?.on("data", (data) => {
2428
+ const lines = data.toString().split("\n");
2429
+ for (const line of lines) {
2430
+ // Parse network request patterns
2431
+ const httpMatch = line.match(/(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i);
2432
+ if (httpMatch) {
2433
+ networkRequestBuffer.push({
2434
+ index: requestIndex++,
2435
+ timestamp: new Date().toISOString(),
2436
+ method: httpMatch[1].toUpperCase(),
2437
+ url: httpMatch[2],
2438
+ });
2439
+ if (networkRequestBuffer.length > MAX_NETWORK_REQUESTS) {
2440
+ networkRequestBuffer = networkRequestBuffer.slice(-MAX_NETWORK_REQUESTS);
2441
+ }
2112
2442
  }
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 };
2443
+ // Parse response status
2444
+ const statusMatch = line.match(/<--\s*(\d{3})\s+(https?:\/\/\S+)/i);
2445
+ if (statusMatch) {
2446
+ const url = statusMatch[2];
2447
+ const req = networkRequestBuffer.find(r => r.url === url && !r.status);
2448
+ if (req) {
2449
+ req.status = parseInt(statusMatch[1]);
2128
2450
  }
2129
- return { content: [{ type: "text", text: result.error }] };
2130
2451
  }
2131
- case "list_devices": {
2132
- const result = await listDevices();
2452
+ }
2453
+ });
2454
+ networkMonitorProcess.on("error", (err) => {
2455
+ console.error("Network monitor error:", err);
2456
+ });
2457
+ let result = `šŸ“” Network Monitoring Started\n${"═".repeat(50)}\n\nCapturing HTTP/HTTPS requests in background.\n\nUse 'stop_network_monitoring' to stop and see captured requests.\nUse 'get_network_requests' to see current buffer.`;
2458
+ if (check.message)
2459
+ result += `\n\n${check.message}`;
2460
+ return result;
2461
+ }
2462
+ async function stopNetworkMonitoring() {
2463
+ const check = await requireAdvanced("stop_network_monitoring");
2464
+ if (!check.allowed)
2465
+ return check.message;
2466
+ if (networkMonitorProcess) {
2467
+ networkMonitorProcess.kill();
2468
+ networkMonitorProcess = null;
2469
+ }
2470
+ const requests = networkRequestBuffer;
2471
+ const summary = {
2472
+ total: requests.length,
2473
+ successful: requests.filter(r => r.status && r.status >= 200 && r.status < 300).length,
2474
+ failed: requests.filter(r => r.status && (r.status < 200 || r.status >= 300)).length,
2475
+ pending: requests.filter(r => !r.status).length,
2476
+ };
2477
+ const resultLines = [];
2478
+ resultLines.push("šŸ“” Network Monitoring Stopped");
2479
+ resultLines.push("═".repeat(50));
2480
+ resultLines.push(`\nSummary:`);
2481
+ resultLines.push(` Total requests: ${summary.total}`);
2482
+ resultLines.push(` Successful (2xx): ${summary.successful}`);
2483
+ resultLines.push(` Failed: ${summary.failed}`);
2484
+ resultLines.push(` Pending: ${summary.pending}`);
2485
+ if (requests.length > 0) {
2486
+ resultLines.push(`\nRecent requests:`);
2487
+ for (const req of requests.slice(-10)) {
2488
+ const statusIcon = req.status
2489
+ ? (req.status >= 200 && req.status < 300 ? "āœ…" : "āŒ")
2490
+ : "ā³";
2491
+ resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 50)}...`);
2492
+ }
2493
+ }
2494
+ let result = resultLines.join("\n");
2495
+ if (check.message)
2496
+ result += `\n\n${check.message}`;
2497
+ return result;
2498
+ }
2499
+ async function getNetworkStats(device) {
2500
+ const check = await requireAdvanced("get_network_stats");
2501
+ if (!check.allowed)
2502
+ return check.message;
2503
+ try {
2504
+ const deviceFlag = device ? `-s ${device}` : "";
2505
+ // Get various network stats
2506
+ const [wifiInfo, netStats, activeConns] = await Promise.all([
2507
+ execAsync(`adb ${deviceFlag} shell dumpsys wifi | head -50`)
2508
+ .then(r => r.stdout)
2509
+ .catch(() => "N/A"),
2510
+ execAsync(`adb ${deviceFlag} shell cat /proc/net/dev`)
2511
+ .then(r => r.stdout)
2512
+ .catch(() => "N/A"),
2513
+ execAsync(`adb ${deviceFlag} shell netstat -an | head -30`)
2514
+ .then(r => r.stdout)
2515
+ .catch(() => "N/A"),
2516
+ ]);
2517
+ const resultLines = [];
2518
+ resultLines.push("šŸ“Š Network Statistics");
2519
+ resultLines.push("═".repeat(50));
2520
+ // Parse WiFi info
2521
+ const ssidMatch = wifiInfo.match(/mWifiInfo.*?SSID:\s*([^,]+)/);
2522
+ const rssiMatch = wifiInfo.match(/RSSI:\s*(-?\d+)/);
2523
+ const linkSpeedMatch = wifiInfo.match(/Link speed:\s*(\d+)/);
2524
+ resultLines.push("\nšŸ“¶ WiFi Info:");
2525
+ if (ssidMatch)
2526
+ resultLines.push(` SSID: ${ssidMatch[1].trim()}`);
2527
+ if (rssiMatch)
2528
+ resultLines.push(` Signal: ${rssiMatch[1]} dBm`);
2529
+ if (linkSpeedMatch)
2530
+ resultLines.push(` Speed: ${linkSpeedMatch[1]} Mbps`);
2531
+ // Parse network interface stats
2532
+ resultLines.push("\nšŸ“ˆ Interface Stats:");
2533
+ const devLines = netStats.split("\n").filter(l => l.includes("wlan") || l.includes("rmnet"));
2534
+ for (const line of devLines.slice(0, 5)) {
2535
+ const parts = line.trim().split(/\s+/);
2536
+ if (parts.length >= 10) {
2537
+ const iface = parts[0].replace(":", "");
2538
+ const rxBytes = parseInt(parts[1]) || 0;
2539
+ const txBytes = parseInt(parts[9]) || 0;
2540
+ resultLines.push(` ${iface}: RX ${formatBytes(rxBytes)}, TX ${formatBytes(txBytes)}`);
2541
+ }
2542
+ }
2543
+ // Parse active connections
2544
+ resultLines.push("\nšŸ”— Active Connections:");
2545
+ const connLines = activeConns.split("\n")
2546
+ .filter(l => l.includes("ESTABLISHED") || l.includes("TIME_WAIT"))
2547
+ .slice(0, 8);
2548
+ if (connLines.length > 0) {
2549
+ for (const line of connLines) {
2550
+ const parts = line.trim().split(/\s+/);
2551
+ if (parts.length >= 5) {
2552
+ resultLines.push(` ${parts[3]} -> ${parts[4]} (${parts[5] || ""})`);
2553
+ }
2554
+ }
2555
+ }
2556
+ else {
2557
+ resultLines.push(" No active connections found");
2558
+ }
2559
+ let result = resultLines.join("\n");
2560
+ if (check.message)
2561
+ result += `\n\n${check.message}`;
2562
+ return result;
2563
+ }
2564
+ catch (error) {
2565
+ return `Error getting network stats: ${error.message}`;
2566
+ }
2567
+ }
2568
+ function formatBytes(bytes) {
2569
+ if (bytes < 1024)
2570
+ return `${bytes} B`;
2571
+ if (bytes < 1024 * 1024)
2572
+ return `${(bytes / 1024).toFixed(1)} KB`;
2573
+ if (bytes < 1024 * 1024 * 1024)
2574
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
2575
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
2576
+ }
2577
+ async function analyzeRequest(index) {
2578
+ const check = await requireAdvanced("analyze_request");
2579
+ if (!check.allowed)
2580
+ return check.message;
2581
+ const request = networkRequestBuffer.find(r => r.index === index);
2582
+ if (!request) {
2583
+ let result = `Request #${index} not found.\n\nUse 'get_network_requests' or 'start_network_monitoring' to capture requests first.`;
2584
+ if (check.message)
2585
+ result += `\n\n${check.message}`;
2586
+ return result;
2587
+ }
2588
+ const resultLines = [];
2589
+ resultLines.push(`šŸ” Request Analysis #${index}`);
2590
+ resultLines.push("═".repeat(50));
2591
+ resultLines.push(`\nMethod: ${request.method}`);
2592
+ resultLines.push(`URL: ${request.url}`);
2593
+ resultLines.push(`Timestamp: ${request.timestamp}`);
2594
+ if (request.status) {
2595
+ const statusText = request.status >= 200 && request.status < 300 ? "Success" : "Failed";
2596
+ resultLines.push(`Status: ${request.status} (${statusText})`);
2597
+ }
2598
+ else {
2599
+ resultLines.push("Status: Pending/Unknown");
2600
+ }
2601
+ if (request.duration) {
2602
+ resultLines.push(`Duration: ${request.duration}ms`);
2603
+ }
2604
+ if (request.size) {
2605
+ resultLines.push(`Size: ${request.size}`);
2606
+ }
2607
+ if (request.headers && Object.keys(request.headers).length > 0) {
2608
+ resultLines.push("\nšŸ“‹ Headers:");
2609
+ for (const [key, value] of Object.entries(request.headers)) {
2610
+ resultLines.push(` ${key}: ${value}`);
2611
+ }
2612
+ }
2613
+ if (request.body) {
2614
+ resultLines.push("\nšŸ“¤ Request Body:");
2615
+ resultLines.push(` ${request.body.substring(0, 500)}${request.body.length > 500 ? "..." : ""}`);
2616
+ }
2617
+ if (request.response) {
2618
+ resultLines.push("\nšŸ“„ Response:");
2619
+ resultLines.push(` ${request.response.substring(0, 500)}${request.response.length > 500 ? "..." : ""}`);
2620
+ }
2621
+ let result = resultLines.join("\n");
2622
+ if (check.message)
2623
+ result += `\n\n${check.message}`;
2624
+ return result;
2625
+ }
2626
+ // ============================================================================
2627
+ // EXPO DEVTOOLS INTEGRATION
2628
+ // ============================================================================
2629
+ async function checkExpoStatus(port = 8081) {
2630
+ const check = await requireAdvanced("check_expo_status");
2631
+ if (!check.allowed)
2632
+ return check.message;
2633
+ const resultLines = [];
2634
+ resultLines.push("šŸ“± Expo Dev Server Status");
2635
+ resultLines.push("═".repeat(50));
2636
+ // Check multiple ports that Expo might use
2637
+ const portsToCheck = [
2638
+ { port, name: "Metro Bundler" },
2639
+ { port: 19000, name: "Legacy Bundler" },
2640
+ { port: 19001, name: "WebSocket" },
2641
+ { port: 19002, name: "DevTools UI" },
2642
+ ];
2643
+ let anyRunning = false;
2644
+ for (const { port: p, name } of portsToCheck) {
2645
+ try {
2646
+ const response = await new Promise((resolve, reject) => {
2647
+ const req = http.request({ hostname: "localhost", port: p, path: "/status", method: "GET", timeout: 2000 }, (res) => {
2648
+ let data = "";
2649
+ res.on("data", (chunk) => (data += chunk));
2650
+ res.on("end", () => resolve(data));
2651
+ });
2652
+ req.on("error", reject);
2653
+ req.on("timeout", () => reject(new Error("timeout")));
2654
+ req.end();
2655
+ });
2656
+ resultLines.push(`\nāœ… ${name} (port ${p}): Running`);
2657
+ if (response && response.length > 0 && response.length < 200) {
2658
+ resultLines.push(` Status: ${response.trim()}`);
2659
+ }
2660
+ anyRunning = true;
2661
+ }
2662
+ catch {
2663
+ // Port not responding, check if it's a different status endpoint
2664
+ try {
2665
+ // Try the root endpoint for bundler status
2666
+ const response = await new Promise((resolve, reject) => {
2667
+ const req = http.request({ hostname: "localhost", port: p, path: "/", method: "GET", timeout: 1000 }, (res) => {
2668
+ let data = "";
2669
+ res.on("data", (chunk) => (data += chunk));
2670
+ res.on("end", () => resolve(`HTTP ${res.statusCode}`));
2671
+ });
2672
+ req.on("error", reject);
2673
+ req.on("timeout", () => reject(new Error("timeout")));
2674
+ req.end();
2675
+ });
2676
+ resultLines.push(`\nāœ… ${name} (port ${p}): Running (${response})`);
2677
+ anyRunning = true;
2678
+ }
2679
+ catch {
2680
+ resultLines.push(`\n⚪ ${name} (port ${p}): Not running`);
2681
+ }
2682
+ }
2683
+ }
2684
+ // Get Expo CLI version if available
2685
+ try {
2686
+ const { stdout: expoVersion } = await execAsync("npx expo --version", { timeout: 5000 });
2687
+ resultLines.push(`\nšŸ“¦ Expo CLI: v${expoVersion.trim()}`);
2688
+ }
2689
+ catch {
2690
+ resultLines.push("\nšŸ“¦ Expo CLI: Not detected");
2691
+ }
2692
+ // Check if any device is connected
2693
+ try {
2694
+ const { stdout: devices } = await execAsync("adb devices");
2695
+ const deviceLines = devices.split("\n").filter(line => line.includes("\tdevice"));
2696
+ if (deviceLines.length > 0) {
2697
+ resultLines.push(`\nšŸ“± Connected devices: ${deviceLines.length}`);
2698
+ }
2699
+ }
2700
+ catch {
2701
+ // ADB not available
2702
+ }
2703
+ if (!anyRunning) {
2704
+ resultLines.push("\nāš ļø No Expo dev server detected. Start with:");
2705
+ resultLines.push(" npx expo start");
2706
+ }
2707
+ let result = resultLines.join("\n");
2708
+ if (check.message)
2709
+ result += `\n\n${check.message}`;
2710
+ return result;
2711
+ }
2712
+ async function getExpoConfig(projectPath) {
2713
+ const check = await requireAdvanced("get_expo_config");
2714
+ if (!check.allowed)
2715
+ return check.message;
2716
+ const cwd = projectPath || process.cwd();
2717
+ const resultLines = [];
2718
+ resultLines.push("šŸ“‹ Expo Project Configuration");
2719
+ resultLines.push("═".repeat(50));
2720
+ // Try to read app.json
2721
+ const appJsonPath = path.join(cwd, "app.json");
2722
+ const appConfigJsPath = path.join(cwd, "app.config.js");
2723
+ const appConfigTsPath = path.join(cwd, "app.config.ts");
2724
+ let config = null;
2725
+ if (fs.existsSync(appJsonPath)) {
2726
+ try {
2727
+ const content = fs.readFileSync(appJsonPath, "utf-8");
2728
+ config = JSON.parse(content);
2729
+ resultLines.push(`\nSource: app.json`);
2730
+ }
2731
+ catch (err) {
2732
+ resultLines.push(`\nāŒ Error reading app.json: ${err}`);
2733
+ }
2734
+ }
2735
+ else if (fs.existsSync(appConfigJsPath)) {
2736
+ resultLines.push(`\nSource: app.config.js (dynamic config)`);
2737
+ // Can't easily evaluate JS config, suggest using expo config
2738
+ try {
2739
+ const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
2740
+ config = JSON.parse(stdout);
2741
+ }
2742
+ catch {
2743
+ resultLines.push("āš ļø Run 'npx expo config' to see resolved config");
2744
+ }
2745
+ }
2746
+ else if (fs.existsSync(appConfigTsPath)) {
2747
+ resultLines.push(`\nSource: app.config.ts (TypeScript config)`);
2748
+ try {
2749
+ const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
2750
+ config = JSON.parse(stdout);
2751
+ }
2752
+ catch {
2753
+ resultLines.push("āš ļø Run 'npx expo config' to see resolved config");
2754
+ }
2755
+ }
2756
+ else {
2757
+ resultLines.push(`\nāŒ No Expo config found in: ${cwd}`);
2758
+ resultLines.push("\nLooking for: app.json, app.config.js, or app.config.ts");
2759
+ let result = resultLines.join("\n");
2760
+ if (check.message)
2761
+ result += `\n\n${check.message}`;
2762
+ return result;
2763
+ }
2764
+ if (config) {
2765
+ const expo = config.expo || config;
2766
+ resultLines.push(`\nšŸ“± App Name: ${expo.name || "N/A"}`);
2767
+ resultLines.push(`šŸ“Œ Slug: ${expo.slug || "N/A"}`);
2768
+ resultLines.push(`šŸ“¦ Version: ${expo.version || "N/A"}`);
2769
+ resultLines.push(`šŸ”§ SDK Version: ${expo.sdkVersion || "N/A"}`);
2770
+ if (expo.ios) {
2771
+ resultLines.push(`\nšŸŽ iOS:`);
2772
+ resultLines.push(` Bundle ID: ${expo.ios.bundleIdentifier || "N/A"}`);
2773
+ if (expo.ios.buildNumber)
2774
+ resultLines.push(` Build: ${expo.ios.buildNumber}`);
2775
+ }
2776
+ if (expo.android) {
2777
+ resultLines.push(`\nšŸ¤– Android:`);
2778
+ resultLines.push(` Package: ${expo.android.package || "N/A"}`);
2779
+ if (expo.android.versionCode)
2780
+ resultLines.push(` Version Code: ${expo.android.versionCode}`);
2781
+ }
2782
+ if (expo.plugins && expo.plugins.length > 0) {
2783
+ resultLines.push(`\nšŸ”Œ Plugins (${expo.plugins.length}):`);
2784
+ for (const plugin of expo.plugins.slice(0, 10)) {
2785
+ const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
2786
+ resultLines.push(` - ${pluginName}`);
2787
+ }
2788
+ if (expo.plugins.length > 10) {
2789
+ resultLines.push(` ... and ${expo.plugins.length - 10} more`);
2790
+ }
2791
+ }
2792
+ if (expo.extra) {
2793
+ resultLines.push(`\nšŸ“Ž Extra Config: ${JSON.stringify(expo.extra).substring(0, 100)}...`);
2794
+ }
2795
+ }
2796
+ let result = resultLines.join("\n");
2797
+ if (check.message)
2798
+ result += `\n\n${check.message}`;
2799
+ return result;
2800
+ }
2801
+ async function expoDevMenu(device) {
2802
+ const check = await requireAdvanced("expo_dev_menu");
2803
+ if (!check.allowed)
2804
+ return check.message;
2805
+ const deviceFlag = device ? `-s ${device}` : "";
2806
+ try {
2807
+ // The developer menu in React Native/Expo is triggered by shaking the device
2808
+ // We can simulate this with a combination of keyboard events or the menu keycode
2809
+ // KEYCODE_MENU = 82 opens the dev menu on most RN/Expo apps
2810
+ await execAsync(`adb ${deviceFlag} shell input keyevent 82`);
2811
+ let result = `āœ… Developer menu triggered
2812
+
2813
+ The Expo/React Native developer menu should now be visible.
2814
+
2815
+ Available options typically include:
2816
+ - Reload
2817
+ - Go to Expo Home
2818
+ - Toggle Inspector
2819
+ - Toggle Performance Monitor
2820
+ - Show Element Inspector
2821
+ - Open JS Debugger
2822
+ - Fast Refresh settings`;
2823
+ if (check.message)
2824
+ result += `\n\n${check.message}`;
2825
+ return result;
2826
+ }
2827
+ catch (error) {
2828
+ let result = `Error opening dev menu: ${error.message}\n\nMake sure a device is connected: adb devices`;
2829
+ if (check.message)
2830
+ result += `\n\n${check.message}`;
2831
+ return result;
2832
+ }
2833
+ }
2834
+ async function expoReload(device) {
2835
+ const check = await requireAdvanced("expo_reload");
2836
+ if (!check.allowed)
2837
+ return check.message;
2838
+ const deviceFlag = device ? `-s ${device}` : "";
2839
+ try {
2840
+ // Method 1: Send reload broadcast to Expo/RN
2841
+ // Double-tap R in dev menu typically reloads
2842
+ // KEYCODE_R = 46, but we need to simulate the reload command
2843
+ // First try to use the reload command via adb
2844
+ // Many Expo apps respond to the "RR" double-tap
2845
+ await execAsync(`adb ${deviceFlag} shell input keyevent 82`); // Open menu
2846
+ await new Promise(resolve => setTimeout(resolve, 300)); // Wait for menu
2847
+ await execAsync(`adb ${deviceFlag} shell input text r`); // Press 'r' to reload
2848
+ // Alternative: Send the reload intent directly if app supports it
2849
+ try {
2850
+ // Try sending a reload broadcast (works on some Expo setups)
2851
+ await execAsync(`adb ${deviceFlag} shell am broadcast -a "com.facebook.react.reload"`);
2852
+ }
2853
+ catch {
2854
+ // Not all apps support this broadcast
2855
+ }
2856
+ let result = `šŸ”„ Reload triggered!
2857
+
2858
+ The app should now be reloading its JavaScript bundle.
2859
+
2860
+ If the app didn't reload, try:
2861
+ 1. Press 'r' twice quickly in the Metro terminal
2862
+ 2. Use expo_dev_menu and select "Reload"
2863
+ 3. Shake the device to open dev menu`;
2864
+ if (check.message)
2865
+ result += `\n\n${check.message}`;
2866
+ return result;
2867
+ }
2868
+ catch (error) {
2869
+ let result = `Error triggering reload: ${error.message}\n\nMake sure a device is connected: adb devices`;
2870
+ if (check.message)
2871
+ result += `\n\n${check.message}`;
2872
+ return result;
2873
+ }
2874
+ }
2875
+ async function getEasBuilds(platform = "all", limit = 5) {
2876
+ const check = await requireAdvanced("get_eas_builds");
2877
+ if (!check.allowed)
2878
+ return check.message;
2879
+ const resultLines = [];
2880
+ resultLines.push("šŸ—ļø EAS Build Status");
2881
+ resultLines.push("═".repeat(50));
2882
+ try {
2883
+ // Check if eas-cli is available
2884
+ await execAsync("npx eas-cli --version", { timeout: 5000 });
2885
+ // Get build list
2886
+ const platformFlag = platform !== "all" ? `--platform ${platform}` : "";
2887
+ const { stdout } = await execAsync(`npx eas-cli build:list ${platformFlag} --limit ${limit} --json --non-interactive`, { timeout: 30000 });
2888
+ let builds = [];
2889
+ try {
2890
+ builds = JSON.parse(stdout);
2891
+ }
2892
+ catch {
2893
+ // Sometimes eas-cli outputs extra text before JSON
2894
+ const jsonMatch = stdout.match(/\[[\s\S]*\]/);
2895
+ if (jsonMatch) {
2896
+ builds = JSON.parse(jsonMatch[0]);
2897
+ }
2898
+ }
2899
+ if (builds.length === 0) {
2900
+ resultLines.push("\nNo builds found.");
2901
+ resultLines.push("\nTo create a build:");
2902
+ resultLines.push(" npx eas-cli build --platform android");
2903
+ resultLines.push(" npx eas-cli build --platform ios");
2904
+ }
2905
+ else {
2906
+ resultLines.push(`\nShowing ${builds.length} most recent builds:\n`);
2907
+ for (const build of builds) {
2908
+ const platformEmoji = build.platform === "ANDROID" ? "šŸ¤–" : "šŸŽ";
2909
+ const statusEmoji = build.status === "FINISHED" ? "āœ…" :
2910
+ build.status === "IN_PROGRESS" ? "šŸ”„" :
2911
+ build.status === "ERRORED" ? "āŒ" : "ā³";
2912
+ resultLines.push(`${statusEmoji} ${platformEmoji} ${build.platform}`);
2913
+ resultLines.push(` Build ID: ${build.id}`);
2914
+ resultLines.push(` Status: ${build.status}`);
2915
+ resultLines.push(` Profile: ${build.buildProfile || "N/A"}`);
2916
+ resultLines.push(` Started: ${new Date(build.createdAt).toLocaleString()}`);
2917
+ if (build.status === "FINISHED" && build.artifacts?.buildUrl) {
2918
+ resultLines.push(` šŸ“„ Download: ${build.artifacts.buildUrl}`);
2919
+ }
2920
+ if (build.error) {
2921
+ resultLines.push(` āŒ Error: ${build.error.message || build.error}`);
2922
+ }
2923
+ resultLines.push("");
2924
+ }
2925
+ }
2926
+ }
2927
+ catch (error) {
2928
+ if (error.message?.includes("not found") || error.message?.includes("ENOENT")) {
2929
+ resultLines.push("\nāŒ EAS CLI not found");
2930
+ resultLines.push("\nInstall with: npm install -g eas-cli");
2931
+ resultLines.push("Then login: npx eas-cli login");
2932
+ }
2933
+ else if (error.message?.includes("not logged in") || error.message?.includes("AUTH")) {
2934
+ resultLines.push("\nāŒ Not logged in to EAS");
2935
+ resultLines.push("\nLogin with: npx eas-cli login");
2936
+ }
2937
+ else {
2938
+ resultLines.push(`\nāŒ Error: ${error.message}`);
2939
+ resultLines.push("\nMake sure you're in an Expo project directory with eas.json");
2940
+ }
2941
+ }
2942
+ let result = resultLines.join("\n");
2943
+ if (check.message)
2944
+ result += `\n\n${check.message}`;
2945
+ return result;
2946
+ }
2947
+ // ============================================================================
2948
+ // PERFORMANCE METRICS
2949
+ // ============================================================================
2950
+ async function getCpuUsage(packageName, device) {
2951
+ const check = await requireAdvanced("get_cpu_usage");
2952
+ if (!check.allowed)
2953
+ return check.message;
2954
+ const deviceFlag = device ? `-s ${device}` : "";
2955
+ try {
2956
+ const resultLines = [];
2957
+ resultLines.push("šŸ“Š CPU Usage");
2958
+ resultLines.push("═".repeat(50));
2959
+ // Get overall CPU info
2960
+ const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo`, { maxBuffer: 2 * 1024 * 1024 });
2961
+ // Parse CPU usage
2962
+ const lines = cpuInfo.split("\n");
2963
+ const totalMatch = lines.find(line => line.includes("TOTAL"));
2964
+ if (totalMatch) {
2965
+ resultLines.push(`\n${totalMatch.trim()}`);
2966
+ }
2967
+ // If package specified, filter for that app
2968
+ if (packageName) {
2969
+ resultLines.push(`\nšŸ“± App: ${packageName}`);
2970
+ const appLines = lines.filter(line => line.toLowerCase().includes(packageName.toLowerCase()));
2971
+ if (appLines.length > 0) {
2972
+ appLines.forEach(line => {
2973
+ resultLines.push(` ${line.trim()}`);
2974
+ });
2975
+ }
2976
+ else {
2977
+ resultLines.push(" App not found in CPU stats (may not be running)");
2978
+ }
2979
+ }
2980
+ else {
2981
+ // Show top processes
2982
+ resultLines.push("\nšŸ” Top Processes:");
2983
+ const processLines = lines
2984
+ .filter(line => line.includes("%") && !line.includes("TOTAL"))
2985
+ .slice(0, 10);
2986
+ processLines.forEach(line => {
2987
+ resultLines.push(` ${line.trim()}`);
2988
+ });
2989
+ }
2990
+ // Get per-core usage
2991
+ try {
2992
+ const { stdout: coreInfo } = await execAsync(`adb ${deviceFlag} shell cat /proc/stat`, { timeout: 5000 });
2993
+ const coreLines = coreInfo.split("\n").filter(line => line.startsWith("cpu"));
2994
+ if (coreLines.length > 1) {
2995
+ resultLines.push(`\nšŸ’» CPU Cores: ${coreLines.length - 1}`);
2996
+ }
2997
+ }
2998
+ catch {
2999
+ // Core info not available
3000
+ }
3001
+ let result = resultLines.join("\n");
3002
+ if (check.message)
3003
+ result += `\n\n${check.message}`;
3004
+ return result;
3005
+ }
3006
+ catch (error) {
3007
+ let result = `Error getting CPU usage: ${error.message}`;
3008
+ if (check.message)
3009
+ result += `\n\n${check.message}`;
3010
+ return result;
3011
+ }
3012
+ }
3013
+ async function getMemoryUsage(packageName, device) {
3014
+ const check = await requireAdvanced("get_memory_usage");
3015
+ if (!check.allowed)
3016
+ return check.message;
3017
+ const deviceFlag = device ? `-s ${device}` : "";
3018
+ try {
3019
+ const resultLines = [];
3020
+ resultLines.push("🧠 Memory Usage");
3021
+ resultLines.push("═".repeat(50));
3022
+ resultLines.push(`\nšŸ“± App: ${packageName}\n`);
3023
+ // Get detailed memory info for the app
3024
+ const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
3025
+ // Parse key memory metrics
3026
+ const lines = memInfo.split("\n");
3027
+ // Find summary section
3028
+ const summaryStart = lines.findIndex(line => line.includes("App Summary"));
3029
+ if (summaryStart !== -1) {
3030
+ resultLines.push("šŸ“‹ App Summary:");
3031
+ for (let i = summaryStart + 1; i < Math.min(summaryStart + 10, lines.length); i++) {
3032
+ const line = lines[i].trim();
3033
+ if (line && !line.startsWith("--")) {
3034
+ resultLines.push(` ${line}`);
3035
+ }
3036
+ if (line.includes("TOTAL"))
3037
+ break;
3038
+ }
3039
+ }
3040
+ // Find PSS and heap info
3041
+ const pssMatch = memInfo.match(/TOTAL PSS:\s+(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
3042
+ const heapMatch = memInfo.match(/Native Heap:\s+(\d+)/);
3043
+ const javaHeapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
3044
+ if (pssMatch || heapMatch) {
3045
+ resultLines.push("\nšŸ“Š Key Metrics:");
3046
+ if (pssMatch) {
3047
+ const pssKb = parseInt(pssMatch[1]);
3048
+ resultLines.push(` Total PSS: ${(pssKb / 1024).toFixed(1)} MB`);
3049
+ }
3050
+ if (javaHeapMatch) {
3051
+ const heapKb = parseInt(javaHeapMatch[1]);
3052
+ resultLines.push(` Java Heap: ${(heapKb / 1024).toFixed(1)} MB`);
3053
+ }
3054
+ if (heapMatch) {
3055
+ const nativeKb = parseInt(heapMatch[1]);
3056
+ resultLines.push(` Native Heap: ${(nativeKb / 1024).toFixed(1)} MB`);
3057
+ }
3058
+ }
3059
+ // Memory warnings
3060
+ if (memInfo.includes("Low memory")) {
3061
+ resultLines.push("\nāš ļø Warning: Device is in low memory state");
3062
+ }
3063
+ let result = resultLines.join("\n");
3064
+ if (check.message)
3065
+ result += `\n\n${check.message}`;
3066
+ return result;
3067
+ }
3068
+ catch (error) {
3069
+ if (error.message?.includes("No process found")) {
3070
+ let result = `App '${packageName}' is not running.\n\nStart the app first, then check memory usage.`;
3071
+ if (check.message)
3072
+ result += `\n\n${check.message}`;
3073
+ return result;
3074
+ }
3075
+ let result = `Error getting memory usage: ${error.message}`;
3076
+ if (check.message)
3077
+ result += `\n\n${check.message}`;
3078
+ return result;
3079
+ }
3080
+ }
3081
+ async function getFpsStats(packageName, device, reset = false) {
3082
+ const check = await requireAdvanced("get_fps_stats");
3083
+ if (!check.allowed)
3084
+ return check.message;
3085
+ const deviceFlag = device ? `-s ${device}` : "";
3086
+ try {
3087
+ const resultLines = [];
3088
+ resultLines.push("šŸŽ® Frame Rendering Stats (FPS)");
3089
+ resultLines.push("═".repeat(50));
3090
+ resultLines.push(`\nšŸ“± App: ${packageName}\n`);
3091
+ // Reset stats if requested
3092
+ if (reset) {
3093
+ await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName} reset`);
3094
+ resultLines.push("šŸ“Š Stats reset. Interact with the app, then run this tool again.\n");
3095
+ }
3096
+ // Get graphics info
3097
+ const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
3098
+ const lines = gfxInfo.split("\n");
3099
+ // Parse total frames
3100
+ const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
3101
+ const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
3102
+ const percentile50 = gfxInfo.match(/50th percentile:\s*(\d+)ms/);
3103
+ const percentile90 = gfxInfo.match(/90th percentile:\s*(\d+)ms/);
3104
+ const percentile95 = gfxInfo.match(/95th percentile:\s*(\d+)ms/);
3105
+ const percentile99 = gfxInfo.match(/99th percentile:\s*(\d+)ms/);
3106
+ if (totalMatch) {
3107
+ const totalFrames = parseInt(totalMatch[1]);
3108
+ resultLines.push(`šŸ“ˆ Total Frames: ${totalFrames}`);
3109
+ if (jankyMatch) {
3110
+ const jankyFrames = parseInt(jankyMatch[1]);
3111
+ const jankyPercent = jankyMatch[2];
3112
+ resultLines.push(`🐌 Janky Frames: ${jankyFrames} (${jankyPercent})`);
3113
+ // Calculate smooth FPS estimate
3114
+ const smoothFrames = totalFrames - jankyFrames;
3115
+ const smoothPercent = ((smoothFrames / totalFrames) * 100).toFixed(1);
3116
+ resultLines.push(`✨ Smooth Frames: ${smoothFrames} (${smoothPercent}%)`);
3117
+ }
3118
+ }
3119
+ // Frame timing percentiles
3120
+ if (percentile50 || percentile90) {
3121
+ resultLines.push("\nā±ļø Frame Time Percentiles:");
3122
+ if (percentile50)
3123
+ resultLines.push(` 50th: ${percentile50[1]}ms`);
3124
+ if (percentile90)
3125
+ resultLines.push(` 90th: ${percentile90[1]}ms`);
3126
+ if (percentile95)
3127
+ resultLines.push(` 95th: ${percentile95[1]}ms`);
3128
+ if (percentile99)
3129
+ resultLines.push(` 99th: ${percentile99[1]}ms`);
3130
+ }
3131
+ // Look for slow frames breakdown
3132
+ const slowUIMatch = gfxInfo.match(/Number Slow UI thread:\s*(\d+)/);
3133
+ const slowBitmapMatch = gfxInfo.match(/Number Slow bitmap uploads:\s*(\d+)/);
3134
+ const slowDrawMatch = gfxInfo.match(/Number Slow issue draw commands:\s*(\d+)/);
3135
+ if (slowUIMatch || slowBitmapMatch || slowDrawMatch) {
3136
+ resultLines.push("\nšŸ” Slow Frame Analysis:");
3137
+ if (slowUIMatch && parseInt(slowUIMatch[1]) > 0) {
3138
+ resultLines.push(` Slow UI thread: ${slowUIMatch[1]}`);
3139
+ }
3140
+ if (slowBitmapMatch && parseInt(slowBitmapMatch[1]) > 0) {
3141
+ resultLines.push(` Slow bitmap uploads: ${slowBitmapMatch[1]}`);
3142
+ }
3143
+ if (slowDrawMatch && parseInt(slowDrawMatch[1]) > 0) {
3144
+ resultLines.push(` Slow draw commands: ${slowDrawMatch[1]}`);
3145
+ }
3146
+ }
3147
+ // Performance recommendation
3148
+ if (jankyMatch) {
3149
+ const jankyPercent = parseFloat(jankyMatch[2]);
3150
+ if (jankyPercent > 10) {
3151
+ resultLines.push("\nāš ļø Performance Issue: High jank percentage");
3152
+ resultLines.push(" Consider optimizing heavy UI operations");
3153
+ }
3154
+ else if (jankyPercent < 5) {
3155
+ resultLines.push("\nāœ… Good Performance: Low jank percentage");
3156
+ }
3157
+ }
3158
+ if (!totalMatch) {
3159
+ resultLines.push("No frame data available.");
3160
+ resultLines.push("Make sure the app is visible and interact with it first.");
3161
+ }
3162
+ let result = resultLines.join("\n");
3163
+ if (check.message)
3164
+ result += `\n\n${check.message}`;
3165
+ return result;
3166
+ }
3167
+ catch (error) {
3168
+ let result = `Error getting FPS stats: ${error.message}`;
3169
+ if (check.message)
3170
+ result += `\n\n${check.message}`;
3171
+ return result;
3172
+ }
3173
+ }
3174
+ async function getBatteryStats(packageName, device) {
3175
+ const check = await requireAdvanced("get_battery_stats");
3176
+ if (!check.allowed)
3177
+ return check.message;
3178
+ const deviceFlag = device ? `-s ${device}` : "";
3179
+ try {
3180
+ const resultLines = [];
3181
+ resultLines.push("šŸ”‹ Battery Statistics");
3182
+ resultLines.push("═".repeat(50));
3183
+ // Get current battery level and status
3184
+ const { stdout: batteryInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys battery`, { timeout: 5000 });
3185
+ const levelMatch = batteryInfo.match(/level:\s*(\d+)/);
3186
+ const statusMatch = batteryInfo.match(/status:\s*(\d+)/);
3187
+ const healthMatch = batteryInfo.match(/health:\s*(\d+)/);
3188
+ const tempMatch = batteryInfo.match(/temperature:\s*(\d+)/);
3189
+ resultLines.push("\nšŸ“Š Current Status:");
3190
+ if (levelMatch) {
3191
+ resultLines.push(` Battery Level: ${levelMatch[1]}%`);
3192
+ }
3193
+ if (statusMatch) {
3194
+ const statuses = ["Unknown", "Charging", "Discharging", "Not charging", "Full"];
3195
+ const status = statuses[parseInt(statusMatch[1])] || "Unknown";
3196
+ resultLines.push(` Status: ${status}`);
3197
+ }
3198
+ if (tempMatch) {
3199
+ const temp = parseInt(tempMatch[1]) / 10;
3200
+ resultLines.push(` Temperature: ${temp}°C`);
3201
+ }
3202
+ // Get battery stats (power consumption)
3203
+ if (packageName) {
3204
+ resultLines.push(`\nšŸ“± App: ${packageName}`);
3205
+ try {
3206
+ const { stdout: appStats } = await execAsync(`adb ${deviceFlag} shell dumpsys batterystats ${packageName}`, { maxBuffer: 5 * 1024 * 1024, timeout: 10000 });
3207
+ // Parse power usage
3208
+ const powerMatch = appStats.match(/Estimated power use \(mAh\)[\s\S]*?Uid \S+:\s*([\d.]+)/);
3209
+ if (powerMatch) {
3210
+ resultLines.push(` Power Used: ${powerMatch[1]} mAh`);
3211
+ }
3212
+ // CPU time
3213
+ const cpuMatch = appStats.match(/Total cpu time:\s*u=(\d+)ms\s*s=(\d+)ms/);
3214
+ if (cpuMatch) {
3215
+ const userMs = parseInt(cpuMatch[1]);
3216
+ const sysMs = parseInt(cpuMatch[2]);
3217
+ resultLines.push(` CPU Time: ${((userMs + sysMs) / 1000).toFixed(1)}s (user: ${(userMs / 1000).toFixed(1)}s, sys: ${(sysMs / 1000).toFixed(1)}s)`);
3218
+ }
3219
+ // Network usage
3220
+ const networkMatch = appStats.match(/Network:\s*([\d.]+)\s*MB\s*received,\s*([\d.]+)\s*MB\s*transmitted/i);
3221
+ if (networkMatch) {
3222
+ resultLines.push(` Network: ${networkMatch[1]} MB ↓, ${networkMatch[2]} MB ↑`);
3223
+ }
3224
+ // Wakelock time
3225
+ const wakelockMatch = appStats.match(/Wake lock\s+\S+\s+.*?realtime=(\d+)/);
3226
+ if (wakelockMatch) {
3227
+ const wakeSec = parseInt(wakelockMatch[1]) / 1000;
3228
+ if (wakeSec > 0) {
3229
+ resultLines.push(` Wake Lock: ${wakeSec.toFixed(1)}s`);
3230
+ }
3231
+ }
3232
+ }
3233
+ catch {
3234
+ resultLines.push(" Detailed stats not available for this app");
3235
+ }
3236
+ }
3237
+ else {
3238
+ // Show top battery consumers
3239
+ resultLines.push("\nšŸ” Top Power Consumers:");
3240
+ try {
3241
+ const { stdout: stats } = await execAsync(`adb ${deviceFlag} shell dumpsys batterystats`, { maxBuffer: 10 * 1024 * 1024, timeout: 15000 });
3242
+ // Parse estimated power section
3243
+ const powerSection = stats.match(/Estimated power use[\s\S]*?(?=\n\n|\nStatistics)/);
3244
+ if (powerSection) {
3245
+ const powerLines = powerSection[0].split("\n").slice(1, 8);
3246
+ powerLines.forEach(line => {
3247
+ const trimmed = line.trim();
3248
+ if (trimmed && !trimmed.startsWith("Estimated")) {
3249
+ resultLines.push(` ${trimmed}`);
3250
+ }
3251
+ });
3252
+ }
3253
+ }
3254
+ catch {
3255
+ resultLines.push(" Could not retrieve detailed battery stats");
3256
+ }
3257
+ }
3258
+ let result = resultLines.join("\n");
3259
+ if (check.message)
3260
+ result += `\n\n${check.message}`;
3261
+ return result;
3262
+ }
3263
+ catch (error) {
3264
+ let result = `Error getting battery stats: ${error.message}`;
3265
+ if (check.message)
3266
+ result += `\n\n${check.message}`;
3267
+ return result;
3268
+ }
3269
+ }
3270
+ async function getPerformanceSnapshot(packageName, device) {
3271
+ const check = await requireAdvanced("get_performance_snapshot");
3272
+ if (!check.allowed)
3273
+ return check.message;
3274
+ const deviceFlag = device ? `-s ${device}` : "";
3275
+ try {
3276
+ const resultLines = [];
3277
+ resultLines.push("šŸ“Š Performance Snapshot");
3278
+ resultLines.push("═".repeat(50));
3279
+ resultLines.push(`\nšŸ“± App: ${packageName}`);
3280
+ resultLines.push(`ā° Time: ${new Date().toLocaleString()}\n`);
3281
+ // CPU Usage
3282
+ try {
3283
+ const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo | grep -i "${packageName}"`, { timeout: 5000 });
3284
+ if (cpuInfo.trim()) {
3285
+ resultLines.push("šŸ’» CPU:");
3286
+ cpuInfo.split("\n").slice(0, 3).forEach(line => {
3287
+ if (line.trim())
3288
+ resultLines.push(` ${line.trim()}`);
3289
+ });
3290
+ }
3291
+ }
3292
+ catch {
3293
+ resultLines.push("šŸ’» CPU: N/A");
3294
+ }
3295
+ // Memory Usage
3296
+ try {
3297
+ const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024, timeout: 5000 });
3298
+ const pssMatch = memInfo.match(/TOTAL PSS:\s*(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
3299
+ if (pssMatch) {
3300
+ const pssMb = (parseInt(pssMatch[1]) / 1024).toFixed(1);
3301
+ resultLines.push(`\n🧠 Memory: ${pssMb} MB (PSS)`);
3302
+ }
3303
+ const heapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
3304
+ const nativeMatch = memInfo.match(/Native Heap:\s+(\d+)/);
3305
+ if (heapMatch) {
3306
+ resultLines.push(` Java Heap: ${(parseInt(heapMatch[1]) / 1024).toFixed(1)} MB`);
3307
+ }
3308
+ if (nativeMatch) {
3309
+ resultLines.push(` Native: ${(parseInt(nativeMatch[1]) / 1024).toFixed(1)} MB`);
3310
+ }
3311
+ }
3312
+ catch {
3313
+ resultLines.push("\n🧠 Memory: App not running or no data");
3314
+ }
3315
+ // FPS Stats
3316
+ try {
3317
+ const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 1024 * 1024, timeout: 5000 });
3318
+ const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
3319
+ const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
3320
+ if (totalMatch) {
3321
+ resultLines.push(`\nšŸŽ® Frames: ${totalMatch[1]} rendered`);
3322
+ if (jankyMatch) {
3323
+ resultLines.push(` Janky: ${jankyMatch[1]} (${jankyMatch[2]})`);
3324
+ }
3325
+ }
3326
+ }
3327
+ catch {
3328
+ resultLines.push("\nšŸŽ® Frames: No data");
3329
+ }
3330
+ // Battery
3331
+ try {
3332
+ const { stdout: batteryInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys battery`, { timeout: 3000 });
3333
+ const levelMatch = batteryInfo.match(/level:\s*(\d+)/);
3334
+ const tempMatch = batteryInfo.match(/temperature:\s*(\d+)/);
3335
+ if (levelMatch) {
3336
+ resultLines.push(`\nšŸ”‹ Battery: ${levelMatch[1]}%`);
3337
+ if (tempMatch) {
3338
+ resultLines.push(` Temperature: ${parseInt(tempMatch[1]) / 10}°C`);
3339
+ }
3340
+ }
3341
+ }
3342
+ catch {
3343
+ resultLines.push("\nšŸ”‹ Battery: N/A");
3344
+ }
3345
+ // Network usage from app
3346
+ try {
3347
+ const { stdout: netStats } = await execAsync(`adb ${deviceFlag} shell cat /proc/net/xt_qtaguid/stats`, { timeout: 3000 });
3348
+ // Just note that network is available
3349
+ if (netStats.includes(packageName)) {
3350
+ resultLines.push("\nšŸ“” Network: Active");
3351
+ }
3352
+ }
3353
+ catch {
3354
+ // Network stats not available
3355
+ }
3356
+ resultLines.push("\n" + "─".repeat(50));
3357
+ resultLines.push("Use individual tools for detailed metrics:");
3358
+ resultLines.push(" get_cpu_usage, get_memory_usage, get_fps_stats, get_battery_stats");
3359
+ let result = resultLines.join("\n");
3360
+ if (check.message)
3361
+ result += `\n\n${check.message}`;
3362
+ return result;
3363
+ }
3364
+ catch (error) {
3365
+ let result = `Error getting performance snapshot: ${error.message}`;
3366
+ if (check.message)
3367
+ result += `\n\n${check.message}`;
3368
+ return result;
3369
+ }
3370
+ }
3371
+ // ============================================================================
3372
+ // MCP SERVER SETUP
3373
+ // ============================================================================
3374
+ const server = new Server({
3375
+ name: "claude-mobile-dev-mcp",
3376
+ version: "0.1.0",
3377
+ }, {
3378
+ capabilities: {
3379
+ tools: {},
3380
+ },
3381
+ });
3382
+ // Handle tool listing
3383
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3384
+ tools,
3385
+ }));
3386
+ // Handle tool execution
3387
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3388
+ const { name, arguments: args } = request.params;
3389
+ try {
3390
+ // License tools
3391
+ if (name === "get_license_status" || name === "set_license_key") {
3392
+ const result = await handleLicenseTool(name, args || {});
3393
+ return { content: [{ type: "text", text: result }] };
3394
+ }
3395
+ // Core tools
3396
+ switch (name) {
3397
+ case "get_metro_logs": {
3398
+ const result = await getMetroLogs(args?.lines, args?.filter);
3399
+ return { content: [{ type: "text", text: result }] };
3400
+ }
3401
+ case "get_adb_logs": {
3402
+ const result = await getAdbLogs(args?.lines, args?.filter, args?.level);
3403
+ return { content: [{ type: "text", text: result }] };
3404
+ }
3405
+ case "screenshot_emulator": {
3406
+ const result = await screenshotEmulator(args?.device);
3407
+ if (result.success && result.data) {
3408
+ const content = [
3409
+ {
3410
+ type: "image",
3411
+ data: result.data,
3412
+ mimeType: result.mimeType,
3413
+ },
3414
+ ];
3415
+ // Add trial warning if present
3416
+ if (result.trialMessage) {
3417
+ content.push({ type: "text", text: result.trialMessage });
3418
+ }
3419
+ return { content };
3420
+ }
3421
+ return { content: [{ type: "text", text: result.error }] };
3422
+ }
3423
+ case "list_devices": {
3424
+ const result = await listDevices();
2133
3425
  return { content: [{ type: "text", text: result }] };
2134
3426
  }
2135
3427
  case "check_metro_status": {
@@ -2289,6 +3581,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2289
3581
  const result = await searchReactComponents(args?.query, args?.port);
2290
3582
  return { content: [{ type: "text", text: result }] };
2291
3583
  }
3584
+ // NETWORK INSPECTION TOOLS
3585
+ case "get_network_requests": {
3586
+ const result = await getNetworkRequests(args?.lines, args?.filter, args?.device);
3587
+ return { content: [{ type: "text", text: result }] };
3588
+ }
3589
+ case "start_network_monitoring": {
3590
+ const result = await startNetworkMonitoring(args?.device);
3591
+ return { content: [{ type: "text", text: result }] };
3592
+ }
3593
+ case "stop_network_monitoring": {
3594
+ const result = await stopNetworkMonitoring();
3595
+ return { content: [{ type: "text", text: result }] };
3596
+ }
3597
+ case "get_network_stats": {
3598
+ const result = await getNetworkStats(args?.device);
3599
+ return { content: [{ type: "text", text: result }] };
3600
+ }
3601
+ case "analyze_request": {
3602
+ const result = await analyzeRequest(args?.index);
3603
+ return { content: [{ type: "text", text: result }] };
3604
+ }
3605
+ // EXPO DEVTOOLS TOOLS
3606
+ case "check_expo_status": {
3607
+ const result = await checkExpoStatus(args?.port);
3608
+ return { content: [{ type: "text", text: result }] };
3609
+ }
3610
+ case "get_expo_config": {
3611
+ const result = await getExpoConfig(args?.projectPath);
3612
+ return { content: [{ type: "text", text: result }] };
3613
+ }
3614
+ case "expo_dev_menu": {
3615
+ const result = await expoDevMenu(args?.device);
3616
+ return { content: [{ type: "text", text: result }] };
3617
+ }
3618
+ case "expo_reload": {
3619
+ const result = await expoReload(args?.device);
3620
+ return { content: [{ type: "text", text: result }] };
3621
+ }
3622
+ case "get_eas_builds": {
3623
+ const result = await getEasBuilds(args?.platform, args?.limit);
3624
+ return { content: [{ type: "text", text: result }] };
3625
+ }
3626
+ // PERFORMANCE METRICS TOOLS
3627
+ case "get_cpu_usage": {
3628
+ const result = await getCpuUsage(args?.packageName, args?.device);
3629
+ return { content: [{ type: "text", text: result }] };
3630
+ }
3631
+ case "get_memory_usage": {
3632
+ const result = await getMemoryUsage(args?.packageName, args?.device);
3633
+ return { content: [{ type: "text", text: result }] };
3634
+ }
3635
+ case "get_fps_stats": {
3636
+ const result = await getFpsStats(args?.packageName, args?.device, args?.reset);
3637
+ return { content: [{ type: "text", text: result }] };
3638
+ }
3639
+ case "get_battery_stats": {
3640
+ const result = await getBatteryStats(args?.packageName, args?.device);
3641
+ return { content: [{ type: "text", text: result }] };
3642
+ }
3643
+ case "get_performance_snapshot": {
3644
+ const result = await getPerformanceSnapshot(args?.packageName, args?.device);
3645
+ return { content: [{ type: "text", text: result }] };
3646
+ }
2292
3647
  default:
2293
3648
  return {
2294
3649
  content: [{ type: "text", text: `Unknown tool: ${name}` }],