react-native-ai-debugger 1.0.45 → 1.0.47

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
@@ -2,6 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ import { getGuideOverview, getGuideByTopic, getAvailableTopics } from "./core/guides.js";
5
6
  import { logBuffer, networkBuffer, bundleErrorBuffer, connectedApps, getActiveSimulatorUdid, scanMetroPorts, fetchDevices, selectMainDevice, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
6
7
  // React Component Inspection
7
8
  getComponentTree, getScreenLayout, inspectComponent, findComponents, pressElement, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
@@ -41,6 +42,8 @@ formatLogsAsTonl, formatNetworkAsTonl } from "./core/index.js";
41
42
  const server = new McpServer({
42
43
  name: "react-native-ai-debugger",
43
44
  version: "1.0.0"
45
+ }, {
46
+ instructions: "React Native debugging MCP server. Call get_usage_guide to learn recommended workflows for all tools. Quick start: scan_metro → get_logs / search_logs (console debugging) → ios_screenshot → get_inspector_selection(x, y) (identify components)."
44
47
  });
45
48
  // ============================================================================
46
49
  // Telemetry Wrapper
@@ -50,15 +53,15 @@ const server = new McpServer({
50
53
  * Only needs the first ~2KB of the image to find dimensions.
51
54
  */
52
55
  function getJpegDimensions(buffer) {
53
- if (buffer.length < 4 || buffer[0] !== 0xFF || buffer[1] !== 0xD8)
56
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
54
57
  return null;
55
58
  let offset = 2;
56
59
  while (offset < buffer.length - 9) {
57
- if (buffer[offset] !== 0xFF)
60
+ if (buffer[offset] !== 0xff)
58
61
  return null;
59
62
  const marker = buffer[offset + 1];
60
63
  // SOF markers: C0-CF except C4 (DHT) and CC (DAC)
61
- if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xCC) {
64
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xcc) {
62
65
  const height = buffer.readUInt16BE(offset + 5);
63
66
  const width = buffer.readUInt16BE(offset + 7);
64
67
  return { width, height };
@@ -118,13 +121,15 @@ function registerToolWithTelemetry(toolName, config, handler) {
118
121
  try {
119
122
  inputTokens = Math.ceil(JSON.stringify(args).length / 4);
120
123
  }
121
- catch { /* circular refs — leave undefined */ }
124
+ catch {
125
+ /* circular refs — leave undefined */
126
+ }
122
127
  try {
123
128
  const result = await handler(args);
124
129
  // Check if result indicates an error
125
130
  if (result?.isError) {
126
131
  success = false;
127
- errorMessage = result.content?.[0]?.text || 'Unknown error';
132
+ errorMessage = result.content?.[0]?.text || "Unknown error";
128
133
  // Extract error context if provided (e.g., the expression that caused a syntax error)
129
134
  errorContext = result._errorContext;
130
135
  }
@@ -155,6 +160,38 @@ function registerToolWithTelemetry(toolName, config, handler) {
155
160
  });
156
161
  }
157
162
  /* eslint-enable @typescript-eslint/no-explicit-any */
163
+ // Tool: Usage guide for agents
164
+ registerToolWithTelemetry("get_usage_guide", {
165
+ description: "Get recommended workflows and best practices for using the debugging tools. Call without parameters to see all available topics with short descriptions. Call with a topic parameter to get the full guide for that topic.",
166
+ inputSchema: {
167
+ topic: z
168
+ .string()
169
+ .optional()
170
+ .describe("Topic to get the full guide for. Available topics: setup, inspect, layout, interact, logs, network, state, bundle. Omit to see the overview of all topics.")
171
+ }
172
+ }, async ({ topic }) => {
173
+ if (!topic) {
174
+ return {
175
+ content: [{ type: "text", text: getGuideOverview() }]
176
+ };
177
+ }
178
+ const guide = getGuideByTopic(topic);
179
+ if (!guide) {
180
+ const available = getAvailableTopics().join(", ");
181
+ return {
182
+ content: [
183
+ {
184
+ type: "text",
185
+ text: `Unknown topic: "${topic}". Available topics: ${available}`
186
+ }
187
+ ],
188
+ isError: true
189
+ };
190
+ }
191
+ return {
192
+ content: [{ type: "text", text: guide }]
193
+ };
194
+ });
158
195
  // Tool: Scan for Metro servers
159
196
  registerToolWithTelemetry("scan_metro", {
160
197
  description: "Scan for running Metro bundler servers and automatically connect to any found React Native apps. This is typically the FIRST tool to call when starting a debugging session - it establishes the connection needed for other tools like get_logs, list_debug_globals, execute_in_app, and reload_app.",
@@ -380,15 +417,19 @@ registerToolWithTelemetry("ensure_connection", {
380
417
  registerToolWithTelemetry("get_logs", {
381
418
  description: "Retrieve console logs from connected React Native app. Tip: Use summary=true first for a quick overview (counts by level + last 5 messages), then fetch specific logs as needed.",
382
419
  inputSchema: {
383
- maxLogs: z.coerce.number().optional().default(50).describe("Maximum number of logs to return (default: 50)"),
420
+ maxLogs: z.coerce
421
+ .number()
422
+ .optional()
423
+ .default(50)
424
+ .describe("Maximum number of logs to return (default: 50)"),
384
425
  level: z
385
426
  .enum(["all", "log", "warn", "error", "info", "debug"])
386
427
  .optional()
387
428
  .default("all")
388
429
  .describe("Filter by log level (default: all)"),
389
430
  startFromText: z.string().optional().describe("Start from the first log line containing this text"),
390
- maxMessageLength: z
391
- .coerce.number()
431
+ maxMessageLength: z.coerce
432
+ .number()
392
433
  .optional()
393
434
  .default(500)
394
435
  .describe("Max characters per message (default: 500, set to 0 for unlimited). Tip: Use lower values for overview, higher when debugging specific data structures."),
@@ -426,7 +467,13 @@ registerToolWithTelemetry("get_logs", {
426
467
  ]
427
468
  };
428
469
  }
429
- const { logs, count, formatted } = getLogs(logBuffer, { maxLogs, level, startFromText, maxMessageLength, verbose });
470
+ const { logs, count, formatted } = getLogs(logBuffer, {
471
+ maxLogs,
472
+ level,
473
+ startFromText,
474
+ maxMessageLength,
475
+ verbose
476
+ });
430
477
  // Check connection health
431
478
  let connectionWarning = "";
432
479
  if (count === 0) {
@@ -445,7 +492,7 @@ registerToolWithTelemetry("get_logs", {
445
492
  let gapWarning = "";
446
493
  if (recentGaps.length > 0) {
447
494
  const latestGap = recentGaps[recentGaps.length - 1];
448
- const gapDuration = latestGap.durationMs || (Date.now() - latestGap.disconnectedAt.getTime());
495
+ const gapDuration = latestGap.durationMs || Date.now() - latestGap.disconnectedAt.getTime();
449
496
  if (latestGap.reconnectedAt) {
450
497
  const secAgo = Math.round((Date.now() - latestGap.reconnectedAt.getTime()) / 1000);
451
498
  gapWarning = `\n\n[WARNING] Connection was restored ${secAgo}s ago. Some logs may have been missed during the ${formatDuration(gapDuration)} gap.`;
@@ -481,17 +528,17 @@ registerToolWithTelemetry("search_logs", {
481
528
  description: "Search console logs for text (case-insensitive)",
482
529
  inputSchema: {
483
530
  text: z.string().describe("Text to search for in log messages"),
484
- maxResults: z.coerce.number().optional().default(50).describe("Maximum number of results to return (default: 50)"),
485
- maxMessageLength: z
486
- .coerce.number()
531
+ maxResults: z.coerce
532
+ .number()
533
+ .optional()
534
+ .default(50)
535
+ .describe("Maximum number of results to return (default: 50)"),
536
+ maxMessageLength: z.coerce
537
+ .number()
487
538
  .optional()
488
539
  .default(500)
489
540
  .describe("Max characters per message (default: 500, set to 0 for unlimited)"),
490
- verbose: z
491
- .boolean()
492
- .optional()
493
- .default(false)
494
- .describe("Disable all truncation and return full messages"),
541
+ verbose: z.boolean().optional().default(false).describe("Disable all truncation and return full messages"),
495
542
  format: z
496
543
  .enum(["text", "tonl"])
497
544
  .optional()
@@ -617,10 +664,16 @@ registerToolWithTelemetry("execute_in_app", {
617
664
  "GOOD examples: `__DEV__`, `__APOLLO_CLIENT__.cache.extract()`, `__EXPO_ROUTER__.navigate('/settings')`\n" +
618
665
  "BAD examples: `async () => { await fetch(...) }`, `require('react-native')`, `console.log('\\u{1F600}')`",
619
666
  inputSchema: {
620
- expression: z.string().describe("JavaScript expression to execute. Must be valid Hermes syntax — no require(), no async/await, no emoji/non-ASCII in strings. Use globals discovered via list_debug_globals."),
621
- awaitPromise: z.coerce.boolean().optional().default(true).describe("Whether to await promises (default: true)"),
622
- maxResultLength: z
623
- .coerce.number()
667
+ expression: z
668
+ .string()
669
+ .describe("JavaScript expression to execute. Must be valid Hermes syntax — no require(), no async/await, no emoji/non-ASCII in strings. Use globals discovered via list_debug_globals."),
670
+ awaitPromise: z.coerce
671
+ .boolean()
672
+ .optional()
673
+ .default(true)
674
+ .describe("Whether to await promises (default: true)"),
675
+ maxResultLength: z.coerce
676
+ .number()
624
677
  .optional()
625
678
  .default(2000)
626
679
  .describe("Max characters in result (default: 2000, set to 0 for unlimited). Tip: For large objects like Redux stores, use inspect_global instead or set higher limit."),
@@ -637,7 +690,8 @@ registerToolWithTelemetry("execute_in_app", {
637
690
  // If the error is a ReferenceError (accessing a global that doesn't exist),
638
691
  // guide the agent to expose the variable as a global first
639
692
  if (result.error?.includes("ReferenceError")) {
640
- errorText += "\n\nNOTE: This variable is not exposed as a global. To access it, first assign it to a global variable in your app code (e.g., `globalThis.__MY_VAR__ = myVar;`), then use execute_in_app to read `__MY_VAR__`. You can also use list_debug_globals to see what globals ARE currently available.";
693
+ errorText +=
694
+ "\n\nNOTE: This variable is not exposed as a global. To access it, first assign it to a global variable in your app code (e.g., `globalThis.__MY_VAR__ = myVar;`), then use execute_in_app to read `__MY_VAR__`. You can also use list_debug_globals to see what globals ARE currently available.";
641
695
  }
642
696
  return {
643
697
  content: [
@@ -654,7 +708,8 @@ registerToolWithTelemetry("execute_in_app", {
654
708
  let resultText = result.result ?? "undefined";
655
709
  // Apply truncation unless verbose or unlimited
656
710
  if (!verbose && maxResultLength > 0 && resultText.length > maxResultLength) {
657
- resultText = resultText.slice(0, maxResultLength) + `... [truncated: ${result.result?.length ?? 0} chars total]`;
711
+ resultText =
712
+ resultText.slice(0, maxResultLength) + `... [truncated: ${result.result?.length ?? 0} chars total]`;
658
713
  }
659
714
  return {
660
715
  content: [
@@ -706,7 +761,7 @@ registerToolWithTelemetry("inspect_global", {
706
761
  // If the error is a ReferenceError (accessing a global that doesn't exist),
707
762
  // guide the agent to expose the variable as a global first
708
763
  if (result.error?.includes("ReferenceError")) {
709
- errorText += `\n\nNOTE: '${objectName}' is not exposed as a global variable. To inspect it, first assign it to a global in your app code (e.g., \`globalThis.${objectName} = ${objectName.replace(/^__/, '').replace(/__$/, '')};\`), then call inspect_global again. Use list_debug_globals to see what globals ARE currently available.`;
764
+ errorText += `\n\nNOTE: '${objectName}' is not exposed as a global variable. To inspect it, first assign it to a global in your app code (e.g., \`globalThis.${objectName} = ${objectName.replace(/^__/, "").replace(/__$/, "")};\`), then call inspect_global again. Use list_debug_globals to see what globals ARE currently available.`;
710
765
  }
711
766
  return {
712
767
  content: [
@@ -770,7 +825,15 @@ registerToolWithTelemetry("get_component_tree", {
770
825
  .describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true.")
771
826
  }
772
827
  }, async ({ focusedOnly, structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format }) => {
773
- const result = await getComponentTree({ focusedOnly, structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format });
828
+ const result = await getComponentTree({
829
+ focusedOnly,
830
+ structureOnly,
831
+ maxDepth,
832
+ includeProps,
833
+ includeStyles,
834
+ hideInternals,
835
+ format
836
+ });
774
837
  if (!result.success) {
775
838
  return {
776
839
  content: [
@@ -860,21 +923,13 @@ registerToolWithTelemetry("inspect_component", {
860
923
  .optional()
861
924
  .default(true)
862
925
  .describe("Include component state/hooks (default: true)"),
863
- includeChildren: z
864
- .boolean()
865
- .optional()
866
- .default(false)
867
- .describe("Include children component tree"),
926
+ includeChildren: z.boolean().optional().default(false).describe("Include children component tree"),
868
927
  childrenDepth: z
869
928
  .number()
870
929
  .optional()
871
930
  .default(1)
872
931
  .describe("How many levels deep to show children (default: 1 = direct children only, 2+ = nested tree)"),
873
- shortPath: z
874
- .boolean()
875
- .optional()
876
- .default(true)
877
- .describe("Show only last 3 path segments (default: true)"),
932
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
878
933
  simplifyHooks: z
879
934
  .boolean()
880
935
  .optional()
@@ -882,7 +937,14 @@ registerToolWithTelemetry("inspect_component", {
882
937
  .describe("Simplify hooks output by hiding effects and reducing depth (default: true)")
883
938
  }
884
939
  }, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks }) => {
885
- const result = await inspectComponent(componentName, { index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks });
940
+ const result = await inspectComponent(componentName, {
941
+ index,
942
+ includeState,
943
+ includeChildren,
944
+ childrenDepth,
945
+ shortPath,
946
+ simplifyHooks
947
+ });
886
948
  if (!result.success) {
887
949
  return {
888
950
  content: [
@@ -910,21 +972,13 @@ registerToolWithTelemetry("find_components", {
910
972
  pattern: z
911
973
  .string()
912
974
  .describe("Regex pattern to match component names (case-insensitive). Examples: 'Button', 'Screen$', 'List.*Item'"),
913
- maxResults: z
914
- .number()
915
- .optional()
916
- .default(20)
917
- .describe("Maximum number of results to return (default: 20)"),
975
+ maxResults: z.number().optional().default(20).describe("Maximum number of results to return (default: 20)"),
918
976
  includeLayout: z
919
977
  .boolean()
920
978
  .optional()
921
979
  .default(false)
922
980
  .describe("Include layout styles (padding, margin, flex) for each matched component"),
923
- shortPath: z
924
- .boolean()
925
- .optional()
926
- .default(true)
927
- .describe("Show only last 3 path segments (default: true)"),
981
+ shortPath: z.boolean().optional().default(true).describe("Show only last 3 path segments (default: true)"),
928
982
  summary: z
929
983
  .boolean()
930
984
  .optional()
@@ -970,16 +1024,13 @@ registerToolWithTelemetry("press_element", {
970
1024
  .string()
971
1025
  .optional()
972
1026
  .describe("Case-insensitive partial match on the element's text content (e.g., 'Submit', 'Log in'). ASCII only — non-Latin characters (Cyrillic, CJK, etc.) cause Hermes parse errors. Use testID or component for localized UIs."),
973
- testID: z
974
- .string()
975
- .optional()
976
- .describe("Exact match on the element's testID prop"),
1027
+ testID: z.string().optional().describe("Exact match on the element's testID prop"),
977
1028
  component: z
978
1029
  .string()
979
1030
  .optional()
980
1031
  .describe("Case-insensitive partial match on the component's displayName or name (e.g., 'Button', 'MenuItem')"),
981
- index: z
982
- .coerce.number()
1032
+ index: z.coerce
1033
+ .number()
983
1034
  .optional()
984
1035
  .default(0)
985
1036
  .describe("Zero-based index when multiple elements match (default: 0). If unsure, omit to press the first match.")
@@ -1078,7 +1129,7 @@ registerToolWithTelemetry("get_inspector_selection", {
1078
1129
  if (!inspectorActive) {
1079
1130
  await toggleElementInspector();
1080
1131
  // Wait for inspector to initialize
1081
- await new Promise(resolve => setTimeout(resolve, 300));
1132
+ await new Promise((resolve) => setTimeout(resolve, 300));
1082
1133
  }
1083
1134
  // Detect platform from connected app
1084
1135
  const app = getFirstConnectedApp();
@@ -1088,10 +1139,10 @@ registerToolWithTelemetry("get_inspector_selection", {
1088
1139
  isError: true
1089
1140
  };
1090
1141
  }
1091
- const isIOS = app.deviceInfo.title?.toLowerCase().includes('iphone') ||
1092
- app.deviceInfo.title?.toLowerCase().includes('ipad') ||
1093
- app.deviceInfo.deviceName?.toLowerCase().includes('simulator') ||
1094
- app.deviceInfo.description?.toLowerCase().includes('ios');
1142
+ const isIOS = app.deviceInfo.title?.toLowerCase().includes("iphone") ||
1143
+ app.deviceInfo.title?.toLowerCase().includes("ipad") ||
1144
+ app.deviceInfo.deviceName?.toLowerCase().includes("simulator") ||
1145
+ app.deviceInfo.description?.toLowerCase().includes("ios");
1095
1146
  // Tap at coordinates
1096
1147
  try {
1097
1148
  if (isIOS) {
@@ -1103,15 +1154,17 @@ registerToolWithTelemetry("get_inspector_selection", {
1103
1154
  }
1104
1155
  catch (tapError) {
1105
1156
  return {
1106
- content: [{
1157
+ content: [
1158
+ {
1107
1159
  type: "text",
1108
1160
  text: `Failed to tap at (${x}, ${y}): ${tapError instanceof Error ? tapError.message : String(tapError)}`
1109
- }],
1161
+ }
1162
+ ],
1110
1163
  isError: true
1111
1164
  };
1112
1165
  }
1113
1166
  // Wait for selection to update
1114
- await new Promise(resolve => setTimeout(resolve, 200));
1167
+ await new Promise((resolve) => setTimeout(resolve, 200));
1115
1168
  }
1116
1169
  // Read the current selection
1117
1170
  const result = await getInspectorSelection();
@@ -1188,7 +1241,9 @@ registerToolWithTelemetry("inspect_at_point", {
1188
1241
  const parsed = JSON.parse(result.result || "{}");
1189
1242
  if (parsed.error) {
1190
1243
  const hint = parsed.hint ? `\n\n${parsed.hint}` : "";
1191
- const alternatives = parsed.alternatives ? `\n\nAlternatives:\n${parsed.alternatives.map((a) => ` - ${a}`).join('\n')}` : "";
1244
+ const alternatives = parsed.alternatives
1245
+ ? `\n\nAlternatives:\n${parsed.alternatives.map((a) => ` - ${a}`).join("\n")}`
1246
+ : "";
1192
1247
  return {
1193
1248
  content: [
1194
1249
  {
@@ -1221,18 +1276,9 @@ registerToolWithTelemetry("get_network_requests", {
1221
1276
  .optional()
1222
1277
  .default(50)
1223
1278
  .describe("Maximum number of requests to return (default: 50)"),
1224
- method: z
1225
- .string()
1226
- .optional()
1227
- .describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
1228
- urlPattern: z
1229
- .string()
1230
- .optional()
1231
- .describe("Filter by URL pattern (case-insensitive substring match)"),
1232
- status: z
1233
- .number()
1234
- .optional()
1235
- .describe("Filter by HTTP status code (e.g., 200, 401, 500)"),
1279
+ method: z.string().optional().describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
1280
+ urlPattern: z.string().optional().describe("Filter by URL pattern (case-insensitive substring match)"),
1281
+ status: z.number().optional().describe("Filter by HTTP status code (e.g., 200, 401, 500)"),
1236
1282
  format: z
1237
1283
  .enum(["text", "tonl"])
1238
1284
  .optional()
@@ -1286,7 +1332,7 @@ registerToolWithTelemetry("get_network_requests", {
1286
1332
  let gapWarning = "";
1287
1333
  if (recentGaps.length > 0) {
1288
1334
  const latestGap = recentGaps[recentGaps.length - 1];
1289
- const gapDuration = latestGap.durationMs || (Date.now() - latestGap.disconnectedAt.getTime());
1335
+ const gapDuration = latestGap.durationMs || Date.now() - latestGap.disconnectedAt.getTime();
1290
1336
  if (latestGap.reconnectedAt) {
1291
1337
  const secAgo = Math.round((Date.now() - latestGap.reconnectedAt.getTime()) / 1000);
1292
1338
  gapWarning = `\n\n[WARNING] Connection was restored ${secAgo}s ago. Some requests may have been missed during the ${formatDuration(gapDuration)} gap.`;
@@ -1321,11 +1367,7 @@ registerToolWithTelemetry("search_network", {
1321
1367
  description: "Search network requests by URL pattern (case-insensitive)",
1322
1368
  inputSchema: {
1323
1369
  urlPattern: z.string().describe("URL pattern to search for"),
1324
- maxResults: z
1325
- .number()
1326
- .optional()
1327
- .default(50)
1328
- .describe("Maximum number of results to return (default: 50)"),
1370
+ maxResults: z.number().optional().default(50).describe("Maximum number of results to return (default: 50)"),
1329
1371
  format: z
1330
1372
  .enum(["text", "tonl"])
1331
1373
  .optional()
@@ -1372,8 +1414,8 @@ registerToolWithTelemetry("get_request_details", {
1372
1414
  description: "Get full details of a specific network request including headers, body, and timing. Use get_network_requests first to find the request ID.",
1373
1415
  inputSchema: {
1374
1416
  requestId: z.string().describe("The request ID to get details for"),
1375
- maxBodyLength: z
1376
- .coerce.number()
1417
+ maxBodyLength: z.coerce
1418
+ .number()
1377
1419
  .optional()
1378
1420
  .default(500)
1379
1421
  .describe("Max characters for request body (default: 500, set to 0 for unlimited). Tip: Large POST bodies (file uploads, base64) can be 10KB+."),
@@ -1500,11 +1542,7 @@ registerToolWithTelemetry("get_bundle_status", {
1500
1542
  registerToolWithTelemetry("get_bundle_errors", {
1501
1543
  description: "Retrieve captured Metro bundling/compilation errors. These are errors that occur during the bundle build process (import resolution, syntax errors, transform errors) that prevent the app from loading. If no errors are captured but Metro is running without connected apps, automatically falls back to screenshot+OCR to capture the error from the device screen.",
1502
1544
  inputSchema: {
1503
- maxErrors: z
1504
- .number()
1505
- .optional()
1506
- .default(10)
1507
- .describe("Maximum number of errors to return (default: 10)"),
1545
+ maxErrors: z.number().optional().default(10).describe("Maximum number of errors to return (default: 10)"),
1508
1546
  platform: z
1509
1547
  .enum(["ios", "android"])
1510
1548
  .optional()
@@ -1811,10 +1849,7 @@ registerToolWithTelemetry("android_list_packages", {
1811
1849
  .string()
1812
1850
  .optional()
1813
1851
  .describe("Optional device ID. Uses first available device if not specified."),
1814
- filter: z
1815
- .string()
1816
- .optional()
1817
- .describe("Optional filter to search packages by name (case-insensitive)")
1852
+ filter: z.string().optional().describe("Optional filter to search packages by name (case-insensitive)")
1818
1853
  }
1819
1854
  }, async ({ deviceId, filter }) => {
1820
1855
  const result = await androidListPackages(deviceId, filter);
@@ -1860,11 +1895,7 @@ registerToolWithTelemetry("android_long_press", {
1860
1895
  inputSchema: {
1861
1896
  x: z.coerce.number().describe("X coordinate in pixels"),
1862
1897
  y: z.coerce.number().describe("Y coordinate in pixels"),
1863
- durationMs: z
1864
- .number()
1865
- .optional()
1866
- .default(1000)
1867
- .describe("Press duration in milliseconds (default: 1000)"),
1898
+ durationMs: z.number().optional().default(1000).describe("Press duration in milliseconds (default: 1000)"),
1868
1899
  deviceId: z
1869
1900
  .string()
1870
1901
  .optional()
@@ -1890,11 +1921,7 @@ registerToolWithTelemetry("android_swipe", {
1890
1921
  startY: z.coerce.number().describe("Starting Y coordinate in pixels"),
1891
1922
  endX: z.coerce.number().describe("Ending X coordinate in pixels"),
1892
1923
  endY: z.coerce.number().describe("Ending Y coordinate in pixels"),
1893
- durationMs: z
1894
- .number()
1895
- .optional()
1896
- .default(300)
1897
- .describe("Swipe duration in milliseconds (default: 300)"),
1924
+ durationMs: z.number().optional().default(300).describe("Swipe duration in milliseconds (default: 300)"),
1898
1925
  deviceId: z
1899
1926
  .string()
1900
1927
  .optional()
@@ -1938,9 +1965,7 @@ registerToolWithTelemetry("android_input_text", {
1938
1965
  registerToolWithTelemetry("android_key_event", {
1939
1966
  description: `Send a key event to an Android device/emulator. Common keys: ${Object.keys(ANDROID_KEY_EVENTS).join(", ")}`,
1940
1967
  inputSchema: {
1941
- key: z
1942
- .string()
1943
- .describe(`Key name (${Object.keys(ANDROID_KEY_EVENTS).join(", ")}) or numeric keycode`),
1968
+ key: z.string().describe(`Key name (${Object.keys(ANDROID_KEY_EVENTS).join(", ")}) or numeric keycode`),
1944
1969
  deviceId: z
1945
1970
  .string()
1946
1971
  .optional()
@@ -1948,9 +1973,7 @@ registerToolWithTelemetry("android_key_event", {
1948
1973
  }
1949
1974
  }, async ({ key, deviceId }) => {
1950
1975
  // Try to parse as number first, otherwise treat as key name
1951
- const keyCode = /^\d+$/.test(key)
1952
- ? parseInt(key, 10)
1953
- : key.toUpperCase();
1976
+ const keyCode = /^\d+$/.test(key) ? parseInt(key, 10) : key.toUpperCase();
1954
1977
  const result = await androidKeyEvent(keyCode, deviceId);
1955
1978
  return {
1956
1979
  content: [
@@ -2044,22 +2067,10 @@ server.registerTool("android_describe_point", {
2044
2067
  server.registerTool("android_tap_element", {
2045
2068
  description: "Tap an element by its text, content-description, or resource-id using uiautomator. TIP: Consider using ocr_screenshot first - it returns ready-to-use tap coordinates for all visible text and works more reliably across different apps.",
2046
2069
  inputSchema: {
2047
- text: z
2048
- .string()
2049
- .optional()
2050
- .describe("Exact text match for the element"),
2051
- textContains: z
2052
- .string()
2053
- .optional()
2054
- .describe("Partial text match (case-insensitive)"),
2055
- contentDesc: z
2056
- .string()
2057
- .optional()
2058
- .describe("Exact content-description match"),
2059
- contentDescContains: z
2060
- .string()
2061
- .optional()
2062
- .describe("Partial content-description match (case-insensitive)"),
2070
+ text: z.string().optional().describe("Exact text match for the element"),
2071
+ textContains: z.string().optional().describe("Partial text match (case-insensitive)"),
2072
+ contentDesc: z.string().optional().describe("Exact content-description match"),
2073
+ contentDescContains: z.string().optional().describe("Partial content-description match (case-insensitive)"),
2063
2074
  resourceId: z
2064
2075
  .string()
2065
2076
  .optional()
@@ -2098,15 +2109,9 @@ server.registerTool("android_find_element", {
2098
2109
  description: "Find a UI element on Android screen by text, content description, or resource ID. Returns element details including tap coordinates. Use this to check if an element exists without tapping it. Workflow: 1) wait_for_element, 2) find_element, 3) tap with returned coordinates. Prefer this over screenshots for button taps.",
2099
2110
  inputSchema: {
2100
2111
  text: z.string().optional().describe("Exact text match for the element"),
2101
- textContains: z
2102
- .string()
2103
- .optional()
2104
- .describe("Partial text match (case-insensitive)"),
2112
+ textContains: z.string().optional().describe("Partial text match (case-insensitive)"),
2105
2113
  contentDesc: z.string().optional().describe("Exact content-description match"),
2106
- contentDescContains: z
2107
- .string()
2108
- .optional()
2109
- .describe("Partial content-description match (case-insensitive)"),
2114
+ contentDescContains: z.string().optional().describe("Partial content-description match (case-insensitive)"),
2110
2115
  resourceId: z
2111
2116
  .string()
2112
2117
  .optional()
@@ -2158,15 +2163,9 @@ server.registerTool("android_wait_for_element", {
2158
2163
  description: "Wait for a UI element to appear on Android screen. Polls the accessibility tree until the element is found or timeout is reached. Use this FIRST after navigation to ensure screen is ready, then use find_element + tap.",
2159
2164
  inputSchema: {
2160
2165
  text: z.string().optional().describe("Exact text match for the element"),
2161
- textContains: z
2162
- .string()
2163
- .optional()
2164
- .describe("Partial text match (case-insensitive)"),
2166
+ textContains: z.string().optional().describe("Partial text match (case-insensitive)"),
2165
2167
  contentDesc: z.string().optional().describe("Exact content-description match"),
2166
- contentDescContains: z
2167
- .string()
2168
- .optional()
2169
- .describe("Partial content-description match (case-insensitive)"),
2168
+ contentDescContains: z.string().optional().describe("Partial content-description match (case-insensitive)"),
2170
2169
  resourceId: z
2171
2170
  .string()
2172
2171
  .optional()
@@ -2456,10 +2455,7 @@ registerToolWithTelemetry("ios_install_app", {
2456
2455
  description: "Install an app bundle (.app) on an iOS simulator",
2457
2456
  inputSchema: {
2458
2457
  appPath: z.string().describe("Path to the .app bundle to install"),
2459
- udid: z
2460
- .string()
2461
- .optional()
2462
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2458
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2463
2459
  }
2464
2460
  }, async ({ appPath, udid }) => {
2465
2461
  const result = await iosInstallApp(appPath, udid);
@@ -2478,10 +2474,7 @@ registerToolWithTelemetry("ios_launch_app", {
2478
2474
  description: "Launch an app on an iOS simulator by bundle ID",
2479
2475
  inputSchema: {
2480
2476
  bundleId: z.string().describe("Bundle ID of the app (e.g., com.example.myapp)"),
2481
- udid: z
2482
- .string()
2483
- .optional()
2484
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2477
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2485
2478
  }
2486
2479
  }, async ({ bundleId, udid }) => {
2487
2480
  const result = await iosLaunchApp(bundleId, udid);
@@ -2500,10 +2493,7 @@ registerToolWithTelemetry("ios_open_url", {
2500
2493
  description: "Open a URL in the iOS simulator (opens in default handler or Safari)",
2501
2494
  inputSchema: {
2502
2495
  url: z.string().describe("URL to open (e.g., https://example.com or myapp://path)"),
2503
- udid: z
2504
- .string()
2505
- .optional()
2506
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2496
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2507
2497
  }
2508
2498
  }, async ({ url, udid }) => {
2509
2499
  const result = await iosOpenUrl(url, udid);
@@ -2522,10 +2512,7 @@ registerToolWithTelemetry("ios_terminate_app", {
2522
2512
  description: "Terminate a running app on an iOS simulator",
2523
2513
  inputSchema: {
2524
2514
  bundleId: z.string().describe("Bundle ID of the app to terminate"),
2525
- udid: z
2526
- .string()
2527
- .optional()
2528
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2515
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2529
2516
  }
2530
2517
  }, async ({ bundleId, udid }) => {
2531
2518
  const result = await iosTerminateApp(bundleId, udid);
@@ -2567,14 +2554,8 @@ server.registerTool("ios_tap", {
2567
2554
  inputSchema: {
2568
2555
  x: z.coerce.number().describe("X coordinate in pixels"),
2569
2556
  y: z.coerce.number().describe("Y coordinate in pixels"),
2570
- duration: z
2571
- .number()
2572
- .optional()
2573
- .describe("Optional tap duration in seconds (for long press)"),
2574
- udid: z
2575
- .string()
2576
- .optional()
2577
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2557
+ duration: z.number().optional().describe("Optional tap duration in seconds (for long press)"),
2558
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2578
2559
  }
2579
2560
  }, async ({ x, y, duration, udid }) => {
2580
2561
  const result = await iosTap(x, y, { duration, udid });
@@ -2592,10 +2573,7 @@ server.registerTool("ios_tap", {
2592
2573
  server.registerTool("ios_tap_element", {
2593
2574
  description: "Tap an element by its accessibility label. Requires IDB (brew install idb-companion). TIP: Consider using ocr_screenshot first - it returns ready-to-use tap coordinates for all visible text and works without requiring accessibility labels.",
2594
2575
  inputSchema: {
2595
- label: z
2596
- .string()
2597
- .optional()
2598
- .describe("Exact accessibility label to match (e.g., 'Home', 'Settings')"),
2576
+ label: z.string().optional().describe("Exact accessibility label to match (e.g., 'Home', 'Settings')"),
2599
2577
  labelContains: z
2600
2578
  .string()
2601
2579
  .optional()
@@ -2604,14 +2582,8 @@ server.registerTool("ios_tap_element", {
2604
2582
  .number()
2605
2583
  .optional()
2606
2584
  .describe("If multiple elements match, tap the nth one (0-indexed, default: 0)"),
2607
- duration: z
2608
- .number()
2609
- .optional()
2610
- .describe("Optional tap duration in seconds (for long press)"),
2611
- udid: z
2612
- .string()
2613
- .optional()
2614
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2585
+ duration: z.number().optional().describe("Optional tap duration in seconds (for long press)"),
2586
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2615
2587
  }
2616
2588
  }, async ({ label, labelContains, index, duration, udid }) => {
2617
2589
  const result = await iosTapElement({ label, labelContains, index, duration, udid });
@@ -2635,10 +2607,7 @@ server.registerTool("ios_swipe", {
2635
2607
  endY: z.coerce.number().describe("Ending Y coordinate in pixels"),
2636
2608
  duration: z.coerce.number().optional().describe("Optional swipe duration in seconds"),
2637
2609
  delta: z.coerce.number().optional().describe("Optional delta between touch events (step size)"),
2638
- udid: z
2639
- .string()
2640
- .optional()
2641
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2610
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2642
2611
  }
2643
2612
  }, async ({ startX, startY, endX, endY, duration, delta, udid }) => {
2644
2613
  const result = await iosSwipe(startX, startY, endX, endY, { duration, delta, udid });
@@ -2657,10 +2626,7 @@ server.registerTool("ios_input_text", {
2657
2626
  description: "Type text into the active input field on an iOS simulator. Requires IDB to be installed (brew install idb-companion).",
2658
2627
  inputSchema: {
2659
2628
  text: z.string().describe("Text to type into the active input field"),
2660
- udid: z
2661
- .string()
2662
- .optional()
2663
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2629
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2664
2630
  }
2665
2631
  }, async ({ text, udid }) => {
2666
2632
  const result = await iosInputText(text, udid);
@@ -2682,10 +2648,7 @@ server.registerTool("ios_button", {
2682
2648
  .enum(IOS_BUTTON_TYPES)
2683
2649
  .describe("Hardware button to press: HOME, LOCK, SIDE_BUTTON, SIRI, or APPLE_PAY"),
2684
2650
  duration: z.coerce.number().optional().describe("Optional button press duration in seconds"),
2685
- udid: z
2686
- .string()
2687
- .optional()
2688
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2651
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2689
2652
  }
2690
2653
  }, async ({ button, duration, udid }) => {
2691
2654
  const result = await iosButton(button, { duration, udid });
@@ -2705,10 +2668,7 @@ server.registerTool("ios_key_event", {
2705
2668
  inputSchema: {
2706
2669
  keycode: z.coerce.number().describe("iOS keycode to send"),
2707
2670
  duration: z.coerce.number().optional().describe("Optional key press duration in seconds"),
2708
- udid: z
2709
- .string()
2710
- .optional()
2711
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2671
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2712
2672
  }
2713
2673
  }, async ({ keycode, duration, udid }) => {
2714
2674
  const result = await iosKeyEvent(keycode, { duration, udid });
@@ -2727,10 +2687,7 @@ server.registerTool("ios_key_sequence", {
2727
2687
  description: "Send a sequence of key events to an iOS simulator. Requires IDB to be installed (brew install idb-companion).",
2728
2688
  inputSchema: {
2729
2689
  keycodes: z.array(z.coerce.number()).describe("Array of iOS keycodes to send in sequence"),
2730
- udid: z
2731
- .string()
2732
- .optional()
2733
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2690
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2734
2691
  }
2735
2692
  }, async ({ keycodes, udid }) => {
2736
2693
  const result = await iosKeySequence(keycodes, udid);
@@ -2748,10 +2705,7 @@ server.registerTool("ios_key_sequence", {
2748
2705
  server.registerTool("ios_describe_all", {
2749
2706
  description: "Get accessibility information for the entire iOS simulator screen. Returns a nested tree of UI elements with labels, values, and frames. Requires IDB to be installed (brew install idb-companion).",
2750
2707
  inputSchema: {
2751
- udid: z
2752
- .string()
2753
- .optional()
2754
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2708
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2755
2709
  }
2756
2710
  }, async ({ udid }) => {
2757
2711
  const result = await iosDescribeAll(udid);
@@ -2771,10 +2725,7 @@ server.registerTool("ios_describe_point", {
2771
2725
  inputSchema: {
2772
2726
  x: z.coerce.number().describe("X coordinate in pixels"),
2773
2727
  y: z.coerce.number().describe("Y coordinate in pixels"),
2774
- udid: z
2775
- .string()
2776
- .optional()
2777
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2728
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2778
2729
  }
2779
2730
  }, async ({ x, y, udid }) => {
2780
2731
  const result = await iosDescribePoint(x, y, udid);
@@ -2793,27 +2744,15 @@ server.registerTool("ios_find_element", {
2793
2744
  description: "Find a UI element on iOS simulator by accessibility label or value. Returns element details including tap coordinates. Requires IDB (brew install idb-companion). Workflow: 1) wait_for_element, 2) find_element, 3) tap with returned coordinates. Prefer this over screenshots for button taps.",
2794
2745
  inputSchema: {
2795
2746
  label: z.string().optional().describe("Exact accessibility label match"),
2796
- labelContains: z
2797
- .string()
2798
- .optional()
2799
- .describe("Partial label match (case-insensitive)"),
2747
+ labelContains: z.string().optional().describe("Partial label match (case-insensitive)"),
2800
2748
  value: z.string().optional().describe("Exact accessibility value match"),
2801
- valueContains: z
2802
- .string()
2803
- .optional()
2804
- .describe("Partial value match (case-insensitive)"),
2805
- type: z
2806
- .string()
2807
- .optional()
2808
- .describe("Element type to match (e.g., 'Button', 'TextField')"),
2749
+ valueContains: z.string().optional().describe("Partial value match (case-insensitive)"),
2750
+ type: z.string().optional().describe("Element type to match (e.g., 'Button', 'TextField')"),
2809
2751
  index: z
2810
2752
  .number()
2811
2753
  .optional()
2812
2754
  .describe("If multiple elements match, select the nth one (0-indexed, default: 0)"),
2813
- udid: z
2814
- .string()
2815
- .optional()
2816
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2755
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2817
2756
  }
2818
2757
  }, async ({ label, labelContains, value, valueContains, type, index, udid }) => {
2819
2758
  const result = await iosFindElement({ label, labelContains, value, valueContains, type, index }, udid);
@@ -2852,19 +2791,10 @@ server.registerTool("ios_wait_for_element", {
2852
2791
  description: "Wait for a UI element to appear on iOS simulator. Polls until found or timeout. Requires IDB (brew install idb-companion). Use this FIRST after navigation to ensure screen is ready, then use find_element + tap.",
2853
2792
  inputSchema: {
2854
2793
  label: z.string().optional().describe("Exact accessibility label match"),
2855
- labelContains: z
2856
- .string()
2857
- .optional()
2858
- .describe("Partial label match (case-insensitive)"),
2794
+ labelContains: z.string().optional().describe("Partial label match (case-insensitive)"),
2859
2795
  value: z.string().optional().describe("Exact accessibility value match"),
2860
- valueContains: z
2861
- .string()
2862
- .optional()
2863
- .describe("Partial value match (case-insensitive)"),
2864
- type: z
2865
- .string()
2866
- .optional()
2867
- .describe("Element type to match (e.g., 'Button', 'TextField')"),
2796
+ valueContains: z.string().optional().describe("Partial value match (case-insensitive)"),
2797
+ type: z.string().optional().describe("Element type to match (e.g., 'Button', 'TextField')"),
2868
2798
  index: z
2869
2799
  .number()
2870
2800
  .optional()
@@ -2879,10 +2809,7 @@ server.registerTool("ios_wait_for_element", {
2879
2809
  .optional()
2880
2810
  .default(500)
2881
2811
  .describe("Time between polls in milliseconds (default: 500)"),
2882
- udid: z
2883
- .string()
2884
- .optional()
2885
- .describe("Optional simulator UDID. Uses booted simulator if not specified.")
2812
+ udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
2886
2813
  }
2887
2814
  }, async ({ label, labelContains, value, valueContains, type, index, timeoutMs, pollIntervalMs, udid }) => {
2888
2815
  const result = await iosWaitForElement({