react-native-ai-devtools 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -8,7 +8,7 @@ import { getGuideOverview, getGuideByTopic, getAvailableTopics } from "./core/gu
8
8
  import { getLicenseStatus, getDashboardUrl } from "./core/license.js";
9
9
  import { isSDKInstalled, readSDKNetworkRequests, readSDKNetworkRequest, readSDKNetworkStats, clearSDKNetwork } from "./core/sdkBridge.js";
10
10
  import { tap } from "./pro/tap.js";
11
- import { logBuffer, networkBuffer, bundleErrorBuffer, connectedApps, getActiveSimulatorUdid, scanMetroPorts, fetchDevices, selectMainDevice, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
11
+ import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, LogBuffer, NetworkBuffer, bundleErrorBuffer, connectedApps, scanMetroPorts, fetchDevices, selectMainDevice, filterBridgelessDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
12
12
  // React Component Inspection
13
13
  getComponentTree, getScreenLayout, inspectComponent, findComponents, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
14
14
  // Connection state
@@ -43,6 +43,42 @@ startDebugHttpServer, getDebugServerPort,
43
43
  initTelemetry, trackToolInvocation, getTargetPlatform,
44
44
  // Format utilities (TONL)
45
45
  formatLogsAsTonl, formatNetworkAsTonl } from "./core/index.js";
46
+ // Helper: resolve log buffer for a device (or create a merged buffer from all devices)
47
+ function resolveLogBuffer(device) {
48
+ if (device) {
49
+ const app = getConnectedAppByDevice(device);
50
+ if (!app)
51
+ throw new Error(`No connected device matches "${device}"`);
52
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
53
+ return getLogBuffer(deviceName);
54
+ }
55
+ // Merge all logs into a temporary buffer for read operations
56
+ const merged = new LogBuffer(5000);
57
+ for (const buffer of logBuffers.values()) {
58
+ for (const entry of buffer.getAll()) {
59
+ merged.add(entry);
60
+ }
61
+ }
62
+ return merged;
63
+ }
64
+ // Helper: resolve network buffer for a device (or create a merged buffer from all devices)
65
+ function resolveNetworkBuffer(device) {
66
+ if (device) {
67
+ const app = getConnectedAppByDevice(device);
68
+ if (!app)
69
+ throw new Error(`No connected device matches "${device}"`);
70
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
71
+ return getNetworkBuffer(deviceName);
72
+ }
73
+ // Merge all network requests into a temporary buffer for read operations
74
+ const merged = new NetworkBuffer(5000);
75
+ for (const buffer of networkBuffers.values()) {
76
+ for (const req of buffer.getAll({})) {
77
+ merged.set(`${Math.random()}`, req);
78
+ }
79
+ }
80
+ return merged;
81
+ }
46
82
  // Create MCP server
47
83
  const server = new McpServer({
48
84
  name: "react-native-ai-debugger",
@@ -239,25 +275,29 @@ registerToolWithTelemetry("scan_metro", {
239
275
  results.push(`Port ${port}: No devices found`);
240
276
  continue;
241
277
  }
242
- results.push(`Port ${port}: Found ${devices.length} device(s)`);
243
- const mainDevice = selectMainDevice(devices);
244
- if (mainDevice) {
278
+ const bridgelessDevices = filterBridgelessDevices(devices);
279
+ if (bridgelessDevices.length === 0) {
280
+ results.push(`Port ${port}: No debuggable devices found`);
281
+ continue;
282
+ }
283
+ results.push(`Port ${port}: Found ${bridgelessDevices.length} device(s)`);
284
+ for (const device of bridgelessDevices) {
245
285
  try {
246
- const connectionResult = await connectToDevice(mainDevice, port);
286
+ const connectionResult = await connectToDevice(device, port);
247
287
  results.push(` - ${connectionResult}`);
248
- // Also connect to Metro build events for this port
249
- try {
250
- await connectMetroBuildEvents(port);
251
- results.push(` - Connected to Metro build events`);
252
- }
253
- catch {
254
- // Build events connection is optional, don't fail the scan
255
- }
256
288
  }
257
289
  catch (error) {
258
- results.push(` - Failed: ${error}`);
290
+ results.push(` - ${device.deviceName || device.title}: Failed - ${error}`);
259
291
  }
260
292
  }
293
+ // Connect to Metro build events for this port
294
+ try {
295
+ await connectMetroBuildEvents(port);
296
+ results.push(` - Connected to Metro build events`);
297
+ }
298
+ catch {
299
+ // Build events connection is optional
300
+ }
261
301
  }
262
302
  return {
263
303
  content: [
@@ -273,33 +313,34 @@ registerToolWithTelemetry("get_apps", {
273
313
  description: "List currently connected React Native apps and their connection status. If no apps are connected, run scan_metro first to establish a connection.",
274
314
  inputSchema: {}
275
315
  }, async () => {
276
- const connections = getConnectedApps();
277
- if (connections.length === 0) {
316
+ const apps = getConnectedApps();
317
+ if (apps.length === 0) {
278
318
  return {
279
319
  content: [
280
320
  {
281
321
  type: "text",
282
- text: 'No apps connected. Run "scan_metro" first to discover and connect to running apps.'
322
+ text: "No connected devices. Run scan_metro to discover and connect to Metro servers."
283
323
  }
284
324
  ]
285
325
  };
286
326
  }
287
- const status = connections.map(({ app, isConnected }) => {
288
- const state = isConnected ? "Connected" : "Disconnected";
289
- return `${app.deviceInfo.title} (${app.deviceInfo.deviceName}): ${state}`;
327
+ const deviceLines = apps
328
+ .filter(({ isConnected }) => isConnected)
329
+ .map(({ app }, i) => {
330
+ const name = app.deviceInfo.deviceName || app.deviceInfo.title;
331
+ const appId = app.deviceInfo.appId || app.deviceInfo.title.split(" (")[0] || "unknown";
332
+ return ` ${i + 1}. ${name} — ${appId} (${app.platform}, port ${app.port})`;
290
333
  });
291
- // Include active iOS simulator info if available
292
- const activeSimulatorUdid = getActiveSimulatorUdid();
293
- const simulatorInfo = activeSimulatorUdid
294
- ? `\nActive iOS Simulator (auto-scoped): ${activeSimulatorUdid}`
295
- : "\nNo iOS simulator linked (iOS tools will use first booted simulator)";
334
+ const text = [
335
+ `Connected devices:`,
336
+ ...deviceLines,
337
+ ``,
338
+ `Use device="${apps[0].app.deviceInfo.deviceName}" to target a specific device.`,
339
+ ``,
340
+ `Total logs in buffer: ${getTotalLogCount()}`
341
+ ].join("\n");
296
342
  return {
297
- content: [
298
- {
299
- type: "text",
300
- text: `Connected apps:\n${status.join("\n")}${simulatorInfo}\n\nTotal logs in buffer: ${logBuffer.size}`
301
- }
302
- ]
343
+ content: [{ type: "text", text }]
303
344
  };
304
345
  });
305
346
  // Tool: Get connection status (detailed health and gap tracking)
@@ -492,14 +533,15 @@ registerToolWithTelemetry("get_logs", {
492
533
  .boolean()
493
534
  .optional()
494
535
  .default(false)
495
- .describe("Return summary statistics instead of full logs (count by level + last 5 messages). Use for quick overview.")
536
+ .describe("Return summary statistics instead of full logs (count by level + last 5 messages). Use for quick overview."),
537
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
496
538
  }
497
- }, async ({ maxLogs, level, startFromText, maxMessageLength, verbose, format, summary }) => {
539
+ }, async ({ maxLogs, level, startFromText, maxMessageLength, verbose, format, summary, device }) => {
498
540
  // Return summary if requested
499
541
  if (summary) {
500
- const summaryText = getLogSummary(logBuffer, { lastN: 5, maxMessageLength: 100 });
542
+ const summaryText = getLogSummary(resolveLogBuffer(device), { lastN: 5, maxMessageLength: 100 });
501
543
  let connectionWarning = "";
502
- if (logBuffer.size === 0) {
544
+ if (getTotalLogCount() === 0) {
503
545
  const status = await checkAndEnsureConnection();
504
546
  connectionWarning = status.message ? `\n\n${status.message}` : "";
505
547
  }
@@ -512,7 +554,7 @@ registerToolWithTelemetry("get_logs", {
512
554
  ]
513
555
  };
514
556
  }
515
- const { logs, count, formatted } = getLogs(logBuffer, {
557
+ const { logs, count, formatted } = getLogs(resolveLogBuffer(device), {
516
558
  maxLogs,
517
559
  level,
518
560
  startFromText,
@@ -569,7 +611,7 @@ registerToolWithTelemetry("get_logs", {
569
611
  };
570
612
  },
571
613
  // Empty result detector: buffer has no entries at all
572
- () => logBuffer.size === 0);
614
+ () => getTotalLogCount() === 0);
573
615
  // Tool: Search logs
574
616
  registerToolWithTelemetry("search_logs", {
575
617
  description: "Search console logs for text (case-insensitive)",
@@ -590,10 +632,11 @@ registerToolWithTelemetry("search_logs", {
590
632
  .enum(["text", "tonl"])
591
633
  .optional()
592
634
  .default("tonl")
593
- .describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)")
635
+ .describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)"),
636
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
594
637
  }
595
- }, async ({ text, maxResults, maxMessageLength, verbose, format }) => {
596
- const { logs, count, formatted } = searchLogs(logBuffer, text, { maxResults, maxMessageLength, verbose });
638
+ }, async ({ text, maxResults, maxMessageLength, verbose, format, device }) => {
639
+ const { logs, count, formatted } = searchLogs(resolveLogBuffer(device), text, { maxResults, maxMessageLength, verbose });
597
640
  // Check connection health
598
641
  let connectionWarning = "";
599
642
  if (count === 0) {
@@ -630,17 +673,24 @@ registerToolWithTelemetry("search_logs", {
630
673
  // Tool: Clear logs
631
674
  registerToolWithTelemetry("clear_logs", {
632
675
  description: "Clear the log buffer",
633
- inputSchema: {}
634
- }, async () => {
635
- const count = logBuffer.clear();
636
- return {
637
- content: [
638
- {
639
- type: "text",
640
- text: `Cleared ${count} log entries from buffer.`
641
- }
642
- ]
643
- };
676
+ inputSchema: {
677
+ device: z.string().optional().describe("Target device name (substring match). Omit to clear all devices. Run get_apps to see connected devices.")
678
+ }
679
+ }, async ({ device }) => {
680
+ if (device) {
681
+ const app = getConnectedAppByDevice(device);
682
+ if (!app)
683
+ throw new Error(`No connected device matches "${device}"`);
684
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
685
+ const count = getLogBuffer(deviceName).clear();
686
+ return { content: [{ type: "text", text: `Cleared ${count} log entries from ${deviceName}.` }] };
687
+ }
688
+ // Clear all
689
+ let total = 0;
690
+ for (const buffer of logBuffers.values()) {
691
+ total += buffer.clear();
692
+ }
693
+ return { content: [{ type: "text", text: `Cleared ${total} log entries from all devices.` }] };
644
694
  });
645
695
  // Tool: Connect to specific Metro port
646
696
  registerToolWithTelemetry("connect_metro", {
@@ -776,10 +826,11 @@ registerToolWithTelemetry("execute_in_app", {
776
826
  .boolean()
777
827
  .optional()
778
828
  .default(false)
779
- .describe("Disable result truncation. Tip: Be cautious - Redux stores or large state can return 10KB+.")
829
+ .describe("Disable result truncation. Tip: Be cautious - Redux stores or large state can return 10KB+."),
830
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
780
831
  }
781
- }, async ({ expression, awaitPromise, maxResultLength, verbose }) => {
782
- const result = await executeInApp(expression, awaitPromise);
832
+ }, async ({ expression, awaitPromise, maxResultLength, verbose, device }) => {
833
+ const result = await executeInApp(expression, awaitPromise, {}, device);
783
834
  if (!result.success) {
784
835
  let errorText = `Error: ${result.error}`;
785
836
  // If the error is a ReferenceError (accessing a global that doesn't exist),
@@ -825,9 +876,11 @@ registerToolWithTelemetry("execute_in_app", {
825
876
  // Tool: List debug globals available in the app
826
877
  registerToolWithTelemetry("list_debug_globals", {
827
878
  description: "List globally available debugging objects in the connected React Native app (Apollo Client, Redux store, React DevTools, etc.). Use this to discover what state management and debugging tools are available.",
828
- inputSchema: {}
829
- }, async () => {
830
- const result = await listDebugGlobals();
879
+ inputSchema: {
880
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
881
+ }
882
+ }, async ({ device }) => {
883
+ const result = await listDebugGlobals(device);
831
884
  if (!result.success) {
832
885
  return {
833
886
  content: [
@@ -854,10 +907,11 @@ registerToolWithTelemetry("inspect_global", {
854
907
  inputSchema: {
855
908
  objectName: z
856
909
  .string()
857
- .describe("Name of the global object to inspect (e.g., '__EXPO_ROUTER__', '__APOLLO_CLIENT__')")
910
+ .describe("Name of the global object to inspect (e.g., '__EXPO_ROUTER__', '__APOLLO_CLIENT__')"),
911
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
858
912
  }
859
- }, async ({ objectName }) => {
860
- const result = await inspectGlobal(objectName);
913
+ }, async ({ objectName, device }) => {
914
+ const result = await inspectGlobal(objectName, device);
861
915
  if (!result.success) {
862
916
  let errorText = `Error: ${result.error}`;
863
917
  // If the error is a ReferenceError (accessing a global that doesn't exist),
@@ -924,9 +978,10 @@ registerToolWithTelemetry("get_component_tree", {
924
978
  .enum(["json", "tonl"])
925
979
  .optional()
926
980
  .default("tonl")
927
- .describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true.")
981
+ .describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true."),
982
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
928
983
  }
929
- }, async ({ focusedOnly, structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format }) => {
984
+ }, async ({ focusedOnly, structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format, device }) => {
930
985
  const result = await getComponentTree({
931
986
  focusedOnly,
932
987
  structureOnly,
@@ -934,7 +989,8 @@ registerToolWithTelemetry("get_component_tree", {
934
989
  includeProps,
935
990
  includeStyles,
936
991
  hideInternals,
937
- format
992
+ format,
993
+ device
938
994
  });
939
995
  if (!result.success) {
940
996
  return {
@@ -984,10 +1040,11 @@ registerToolWithTelemetry("get_screen_layout", {
984
1040
  .enum(["json", "tonl"])
985
1041
  .optional()
986
1042
  .default("tonl")
987
- .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)")
1043
+ .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)"),
1044
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
988
1045
  }
989
- }, async ({ maxDepth, componentsOnly, shortPath, summary, format }) => {
990
- const result = await getScreenLayout({ maxDepth, componentsOnly, shortPath, summary, format });
1046
+ }, async ({ maxDepth, componentsOnly, shortPath, summary, format, device }) => {
1047
+ const result = await getScreenLayout({ maxDepth, componentsOnly, shortPath, summary, format, device });
991
1048
  if (!result.success) {
992
1049
  return {
993
1050
  content: [
@@ -1036,16 +1093,18 @@ registerToolWithTelemetry("inspect_component", {
1036
1093
  .boolean()
1037
1094
  .optional()
1038
1095
  .default(true)
1039
- .describe("Simplify hooks output by hiding effects and reducing depth (default: true)")
1096
+ .describe("Simplify hooks output by hiding effects and reducing depth (default: true)"),
1097
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1040
1098
  }
1041
- }, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks }) => {
1099
+ }, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks, device }) => {
1042
1100
  const result = await inspectComponent(componentName, {
1043
1101
  index,
1044
1102
  includeState,
1045
1103
  includeChildren,
1046
1104
  childrenDepth,
1047
1105
  shortPath,
1048
- simplifyHooks
1106
+ simplifyHooks,
1107
+ device
1049
1108
  });
1050
1109
  if (!result.success) {
1051
1110
  return {
@@ -1090,10 +1149,11 @@ registerToolWithTelemetry("find_components", {
1090
1149
  .enum(["json", "tonl"])
1091
1150
  .optional()
1092
1151
  .default("tonl")
1093
- .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)")
1152
+ .describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)"),
1153
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1094
1154
  }
1095
- }, async ({ pattern, maxResults, includeLayout, shortPath, summary, format }) => {
1096
- const result = await findComponents(pattern, { maxResults, includeLayout, shortPath, summary, format });
1155
+ }, async ({ pattern, maxResults, includeLayout, shortPath, summary, format, device }) => {
1156
+ const result = await findComponents(pattern, { maxResults, includeLayout, shortPath, summary, format, device });
1097
1157
  if (!result.success) {
1098
1158
  return {
1099
1159
  content: [
@@ -1194,9 +1254,11 @@ registerToolWithTelemetry("tap", {
1194
1254
  // Tool: Toggle Element Inspector programmatically
1195
1255
  registerToolWithTelemetry("toggle_element_inspector", {
1196
1256
  description: "Toggle React Native's Element Inspector overlay on/off. Rarely needed directly — get_inspector_selection auto-enables the inspector when called with coordinates. Use this only when you need manual control over the overlay visibility.",
1197
- inputSchema: {}
1198
- }, async () => {
1199
- const result = await toggleElementInspector();
1257
+ inputSchema: {
1258
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1259
+ }
1260
+ }, async ({ device }) => {
1261
+ const result = await toggleElementInspector(device);
1200
1262
  if (!result.success) {
1201
1263
  return {
1202
1264
  content: [
@@ -1252,21 +1314,22 @@ registerToolWithTelemetry("get_inspector_selection", {
1252
1314
  y: z
1253
1315
  .number()
1254
1316
  .optional()
1255
- .describe("Y coordinate (in points). If provided with x, auto-taps at this location.")
1317
+ .describe("Y coordinate (in points). If provided with x, auto-taps at this location."),
1318
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1256
1319
  }
1257
- }, async ({ x, y }) => {
1320
+ }, async ({ x, y, device }) => {
1258
1321
  // If coordinates provided, do the full flow: enable inspector -> tap -> read
1259
1322
  if (x !== undefined && y !== undefined) {
1260
1323
  // Check if inspector is active
1261
- const inspectorActive = await isInspectorActive();
1324
+ const inspectorActive = await isInspectorActive(device);
1262
1325
  // Enable inspector if not active
1263
1326
  if (!inspectorActive) {
1264
- await toggleElementInspector();
1327
+ await toggleElementInspector(device);
1265
1328
  // Wait for inspector to initialize
1266
1329
  await new Promise((resolve) => setTimeout(resolve, 300));
1267
1330
  }
1268
1331
  // Detect platform from connected app
1269
- const app = getFirstConnectedApp();
1332
+ const app = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
1270
1333
  if (!app) {
1271
1334
  return {
1272
1335
  content: [{ type: "text", text: "No app connected. Run scan_metro first." }],
@@ -1301,7 +1364,7 @@ registerToolWithTelemetry("get_inspector_selection", {
1301
1364
  await new Promise((resolve) => setTimeout(resolve, 200));
1302
1365
  }
1303
1366
  // Read the current selection
1304
- const result = await getInspectorSelection();
1367
+ const result = await getInspectorSelection(device);
1305
1368
  if (!result.success) {
1306
1369
  return {
1307
1370
  content: [{ type: "text", text: `Error: ${result.error}` }],
@@ -1355,10 +1418,11 @@ registerToolWithTelemetry("inspect_at_point", {
1355
1418
  .boolean()
1356
1419
  .optional()
1357
1420
  .default(true)
1358
- .describe("Include position/dimensions (frame) in the output (default: true)")
1421
+ .describe("Include position/dimensions (frame) in the output (default: true)"),
1422
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1359
1423
  }
1360
- }, async ({ x, y, includeProps, includeFrame }) => {
1361
- const result = await inspectAtPoint(x, y, { includeProps, includeFrame });
1424
+ }, async ({ x, y, includeProps, includeFrame, device }) => {
1425
+ const result = await inspectAtPoint(x, y, { includeProps, includeFrame, device });
1362
1426
  if (!result.success) {
1363
1427
  return {
1364
1428
  content: [
@@ -1422,9 +1486,10 @@ registerToolWithTelemetry("get_network_requests", {
1422
1486
  .boolean()
1423
1487
  .optional()
1424
1488
  .default(false)
1425
- .describe("Return statistics only (count, methods, domains, status codes). Use for quick overview.")
1489
+ .describe("Return statistics only (count, methods, domains, status codes). Use for quick overview."),
1490
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
1426
1491
  }
1427
- }, async ({ maxRequests, method, urlPattern, status, format, summary }) => {
1492
+ }, async ({ maxRequests, method, urlPattern, status, format, summary, device }) => {
1428
1493
  // Check if SDK is installed — prefer SDK data over CDP/interceptor buffer
1429
1494
  const sdkAvailable = await isSDKInstalled();
1430
1495
  if (sdkAvailable) {
@@ -1474,9 +1539,9 @@ registerToolWithTelemetry("get_network_requests", {
1474
1539
  // Fallback: read from in-process buffer (CDP/interceptor)
1475
1540
  // Return summary if requested
1476
1541
  if (summary) {
1477
- const stats = getNetworkStats(networkBuffer);
1542
+ const stats = getNetworkStats(resolveNetworkBuffer(device));
1478
1543
  let connectionWarning = "";
1479
- if (networkBuffer.size === 0) {
1544
+ if (resolveNetworkBuffer(device).size === 0) {
1480
1545
  const connStatus = await checkAndEnsureConnection();
1481
1546
  connectionWarning = connStatus.message ? `\n\n${connStatus.message}` : "";
1482
1547
  if (!sdkAvailable) {
@@ -1492,7 +1557,7 @@ registerToolWithTelemetry("get_network_requests", {
1492
1557
  ]
1493
1558
  };
1494
1559
  }
1495
- const { requests, count, formatted } = getNetworkRequests(networkBuffer, {
1560
+ const { requests, count, formatted } = getNetworkRequests(resolveNetworkBuffer(device), {
1496
1561
  maxRequests,
1497
1562
  method,
1498
1563
  urlPattern,
@@ -1550,7 +1615,8 @@ registerToolWithTelemetry("get_network_requests", {
1550
1615
  };
1551
1616
  },
1552
1617
  // Empty result detector: buffer has no entries at all
1553
- () => networkBuffer.size === 0);
1618
+ () => { let total = 0; for (const b of networkBuffers.values())
1619
+ total += b.size; return total === 0; });
1554
1620
  // Tool: Search network requests
1555
1621
  registerToolWithTelemetry("search_network", {
1556
1622
  description: "Search network requests by URL pattern (case-insensitive)",
@@ -1561,10 +1627,11 @@ registerToolWithTelemetry("search_network", {
1561
1627
  .enum(["text", "tonl"])
1562
1628
  .optional()
1563
1629
  .default("tonl")
1564
- .describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)")
1630
+ .describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)"),
1631
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
1565
1632
  }
1566
- }, async ({ urlPattern, maxResults, format }) => {
1567
- const { requests, count, formatted } = searchNetworkRequests(networkBuffer, urlPattern, maxResults);
1633
+ }, async ({ urlPattern, maxResults, format, device }) => {
1634
+ const { requests, count, formatted } = searchNetworkRequests(resolveNetworkBuffer(device), urlPattern, maxResults);
1568
1635
  // Check connection health
1569
1636
  let connectionWarning = "";
1570
1637
  if (count === 0) {
@@ -1612,9 +1679,10 @@ registerToolWithTelemetry("get_request_details", {
1612
1679
  .boolean()
1613
1680
  .optional()
1614
1681
  .default(false)
1615
- .describe("Disable body truncation. Tip: Use when you need to inspect full JSON payloads.")
1682
+ .describe("Disable body truncation. Tip: Use when you need to inspect full JSON payloads."),
1683
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
1616
1684
  }
1617
- }, async ({ requestId, maxBodyLength, verbose }) => {
1685
+ }, async ({ requestId, maxBodyLength, verbose, device }) => {
1618
1686
  // Check SDK first — it has full headers and body
1619
1687
  const sdkAvailable = await isSDKInstalled();
1620
1688
  if (sdkAvailable) {
@@ -1662,7 +1730,7 @@ registerToolWithTelemetry("get_request_details", {
1662
1730
  }
1663
1731
  }
1664
1732
  // Fallback: read from in-process buffer
1665
- const request = networkBuffer.get(requestId);
1733
+ const request = resolveNetworkBuffer(device).get(requestId);
1666
1734
  if (!request) {
1667
1735
  const status = await checkAndEnsureConnection();
1668
1736
  const connectionNote = status.message ? `\n\n${status.message}` : "";
@@ -1688,12 +1756,14 @@ registerToolWithTelemetry("get_request_details", {
1688
1756
  // Tool: Get network stats
1689
1757
  registerToolWithTelemetry("get_network_stats", {
1690
1758
  description: "Get statistics about captured network requests: counts by method, status code, and domain.",
1691
- inputSchema: {}
1692
- }, async () => {
1693
- const stats = getNetworkStats(networkBuffer);
1759
+ inputSchema: {
1760
+ device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
1761
+ }
1762
+ }, async ({ device }) => {
1763
+ const stats = getNetworkStats(resolveNetworkBuffer(device));
1694
1764
  // Check connection health
1695
1765
  let connectionWarning = "";
1696
- if (networkBuffer.size === 0) {
1766
+ if (resolveNetworkBuffer(device).size === 0) {
1697
1767
  const status = await checkAndEnsureConnection();
1698
1768
  connectionWarning = status.message ? `\n\n${status.message}` : "";
1699
1769
  }
@@ -1713,13 +1783,28 @@ registerToolWithTelemetry("get_network_stats", {
1713
1783
  };
1714
1784
  },
1715
1785
  // Empty result detector: buffer has no entries at all
1716
- () => networkBuffer.size === 0);
1786
+ () => { let total = 0; for (const b of networkBuffers.values())
1787
+ total += b.size; return total === 0; });
1717
1788
  // Tool: Clear network requests
1718
1789
  registerToolWithTelemetry("clear_network", {
1719
1790
  description: "Clear the network request buffer",
1720
- inputSchema: {}
1721
- }, async () => {
1722
- let totalCleared = networkBuffer.clear();
1791
+ inputSchema: {
1792
+ device: z.string().optional().describe("Target device name (substring match). Omit to clear all devices. Run get_apps to see connected devices.")
1793
+ }
1794
+ }, async ({ device }) => {
1795
+ let totalCleared = 0;
1796
+ if (device) {
1797
+ const app = getConnectedAppByDevice(device);
1798
+ if (!app)
1799
+ throw new Error(`No connected device matches "${device}"`);
1800
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
1801
+ totalCleared = getNetworkBuffer(deviceName).clear();
1802
+ }
1803
+ else {
1804
+ for (const buffer of networkBuffers.values()) {
1805
+ totalCleared += buffer.clear();
1806
+ }
1807
+ }
1723
1808
  // Also clear SDK buffer if available
1724
1809
  const sdkAvailable = await isSDKInstalled();
1725
1810
  if (sdkAvailable) {
@@ -1740,9 +1825,11 @@ registerToolWithTelemetry("clear_network", {
1740
1825
  // Tool: Reload the app
1741
1826
  registerToolWithTelemetry("reload_app", {
1742
1827
  description: "Reload the React Native app (triggers JavaScript bundle reload like pressing 'r' in Metro). Will auto-connect to Metro if no connection exists. Note: After reload, the app may take a few seconds to fully restart and become responsive — wait before running other tools. IMPORTANT: React Native has Fast Refresh enabled by default - code changes are automatically applied without needing reload. Only use when: (1) logs/behavior don't reflect code changes after a few seconds, (2) app is in broken/error state, or (3) need to reset app state completely (navigation stack, context, etc.).",
1743
- inputSchema: {}
1744
- }, async () => {
1745
- const result = await reloadApp();
1828
+ inputSchema: {
1829
+ device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1830
+ }
1831
+ }, async ({ device }) => {
1832
+ const result = await reloadApp(device);
1746
1833
  if (!result.success) {
1747
1834
  return {
1748
1835
  content: [