react-native-ai-debugger 1.1.6 → 1.4.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +386 -45
- package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
- package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
- package/build/__tests__/helpers/tap-test-helpers.js +130 -0
- package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
- package/build/__tests__/integration/connection-health.test.js +12 -6
- package/build/__tests__/integration/connection-health.test.js.map +1 -1
- package/build/__tests__/integration/execute-in-app.test.js +2 -1
- package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
- package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
- package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
- package/build/__tests__/integration/tap-strategies.test.js +329 -0
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
- package/build/__tests__/integration/tools.test.js +9 -10
- package/build/__tests__/integration/tools.test.js.map +1 -1
- package/build/__tests__/unit/burst.test.d.ts +2 -0
- package/build/__tests__/unit/burst.test.d.ts.map +1 -0
- package/build/__tests__/unit/burst.test.js +50 -0
- package/build/__tests__/unit/burst.test.js.map +1 -0
- package/build/__tests__/unit/connection-health.test.js +26 -16
- package/build/__tests__/unit/connection-health.test.js.map +1 -1
- package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
- package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
- package/build/__tests__/unit/fiberMatch.test.js +114 -0
- package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
- package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
- package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
- package/build/__tests__/unit/fingerprint.test.js +89 -0
- package/build/__tests__/unit/fingerprint.test.js.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.js +128 -0
- package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
- package/build/__tests__/unit/logs.test.js +9 -0
- package/build/__tests__/unit/logs.test.js.map +1 -1
- package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
- package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
- package/build/__tests__/unit/networkInterceptor.test.js +104 -0
- package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.js +97 -0
- package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.js +542 -25
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +104 -113
- package/build/core/android.js.map +1 -1
- package/build/core/appDetection.d.ts +8 -0
- package/build/core/appDetection.d.ts.map +1 -0
- package/build/core/appDetection.js +115 -0
- package/build/core/appDetection.js.map +1 -0
- package/build/core/bundle.d.ts.map +1 -1
- package/build/core/bundle.js +15 -23
- package/build/core/bundle.js.map +1 -1
- package/build/core/config.d.ts +8 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +27 -0
- package/build/core/config.js.map +1 -0
- package/build/core/connection.d.ts +47 -3
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +519 -124
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +82 -14
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +1869 -418
- package/build/core/executor.js.map +1 -1
- package/build/core/feedback.d.ts +18 -0
- package/build/core/feedback.d.ts.map +1 -0
- package/build/core/feedback.js +73 -0
- package/build/core/feedback.js.map +1 -0
- package/build/core/fiberMatch.d.ts +25 -0
- package/build/core/fiberMatch.d.ts.map +1 -0
- package/build/core/fiberMatch.js +35 -0
- package/build/core/fiberMatch.js.map +1 -0
- package/build/core/fingerprint.d.ts +4 -0
- package/build/core/fingerprint.d.ts.map +1 -0
- package/build/core/fingerprint.js +47 -0
- package/build/core/fingerprint.js.map +1 -0
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +66 -3
- package/build/core/guides.js.map +1 -1
- package/build/core/imageBuffer.d.ts +37 -0
- package/build/core/imageBuffer.d.ts.map +1 -0
- package/build/core/imageBuffer.js +58 -0
- package/build/core/imageBuffer.js.map +1 -0
- package/build/core/index.d.ts +22 -17
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +23 -19
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +36 -7
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +561 -326
- package/build/core/ios.js.map +1 -1
- package/build/core/license.d.ts +38 -0
- package/build/core/license.d.ts.map +1 -0
- package/build/core/license.js +243 -0
- package/build/core/license.js.map +1 -0
- package/build/core/logbox.d.ts +57 -0
- package/build/core/logbox.d.ts.map +1 -0
- package/build/core/logbox.js +282 -0
- package/build/core/logbox.js.map +1 -0
- package/build/core/logs.d.ts +1 -0
- package/build/core/logs.d.ts.map +1 -1
- package/build/core/logs.js +5 -0
- package/build/core/logs.js.map +1 -1
- package/build/core/metro.d.ts +8 -0
- package/build/core/metro.d.ts.map +1 -1
- package/build/core/metro.js +28 -0
- package/build/core/metro.js.map +1 -1
- package/build/core/networkInterceptor.d.ts +29 -2
- package/build/core/networkInterceptor.d.ts.map +1 -1
- package/build/core/networkInterceptor.js +195 -191
- package/build/core/networkInterceptor.js.map +1 -1
- package/build/core/ocr.d.ts +8 -3
- package/build/core/ocr.d.ts.map +1 -1
- package/build/core/ocr.js +116 -15
- package/build/core/ocr.js.map +1 -1
- package/build/core/posthog.d.ts +5 -0
- package/build/core/posthog.d.ts.map +1 -0
- package/build/core/posthog.js +46 -0
- package/build/core/posthog.js.map +1 -0
- package/build/core/sdkBridge.d.ts +67 -0
- package/build/core/sdkBridge.d.ts.map +1 -0
- package/build/core/sdkBridge.js +148 -0
- package/build/core/sdkBridge.js.map +1 -0
- package/build/core/state.d.ts +25 -5
- package/build/core/state.d.ts.map +1 -1
- package/build/core/state.js +79 -10
- package/build/core/state.js.map +1 -1
- package/build/core/telemetry.d.ts +21 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +166 -10
- package/build/core/telemetry.js.map +1 -1
- package/build/core/types.d.ts +11 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/index.js +1490 -324
- package/build/index.js.map +1 -1
- package/build/pro/screenshot-diff.d.ts +10 -0
- package/build/pro/screenshot-diff.d.ts.map +1 -0
- package/build/pro/screenshot-diff.js +38 -0
- package/build/pro/screenshot-diff.js.map +1 -0
- package/build/pro/tap.d.ts +84 -2
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +835 -129
- package/build/pro/tap.js.map +1 -1
- package/build/tools/accountTools.d.ts +44 -0
- package/build/tools/accountTools.d.ts.map +1 -0
- package/build/tools/accountTools.js +217 -0
- package/build/tools/accountTools.js.map +1 -0
- package/package.json +8 -7
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js +0 -2459
- package/build/core/httpServer.js.map +0 -1
- package/build/core/httpServerProcess.d.ts +0 -25
- package/build/core/httpServerProcess.d.ts.map +0 -1
- package/build/core/httpServerProcess.js +0 -153
- package/build/core/httpServerProcess.js.map +0 -1
- package/build/core/tap.d.ts +0 -91
- package/build/core/tap.d.ts.map +0 -1
- package/build/core/tap.js +0 -542
- package/build/core/tap.js.map +0 -1
- package/build/httpServerStandalone.d.ts +0 -7
- package/build/httpServerStandalone.d.ts.map +0 -1
- package/build/httpServerStandalone.js +0 -31
- package/build/httpServerStandalone.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -3,14 +3,21 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
5
|
import { createServer as createHttpServer } from "node:http";
|
|
6
|
+
import { existsSync, unlinkSync } from "fs";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { getGuideOverview, getGuideByTopic, getAvailableTopics } from "./core/guides.js";
|
|
9
|
+
import { getLicenseStatus, getDashboardUrl, getUsageInfo } from "./core/license.js";
|
|
10
|
+
import { API_BASE_URL } from "./core/config.js";
|
|
11
|
+
import { getPostHogClient, identifyIfDevMode, shutdownPostHog } from "./core/posthog.js";
|
|
12
|
+
import { getInstallationId, getServerVersion, isDevMode, TELEMETRY_JSONL_PATH, categorizeError } from "./core/telemetry.js";
|
|
13
|
+
import { isSDKInstalled, querySDKNetwork, getSDKNetworkEntry, getSDKNetworkStats, clearSDKNetwork, querySDKConsole, getSDKConsoleStats, clearSDKConsole } from "./core/sdkBridge.js";
|
|
8
14
|
import { tap } from "./pro/tap.js";
|
|
9
|
-
import {
|
|
15
|
+
import { getActivateLicenseConfig, handleActivateLicense, getDeleteAccountConfig, handleDeleteAccount, } from "./tools/accountTools.js";
|
|
16
|
+
import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, LogBuffer, NetworkBuffer, bundleErrorBuffer, imageBuffer, connectedApps, scanMetroPorts, fetchDevices, filterDebuggableDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
|
|
10
17
|
// React Component Inspection
|
|
11
|
-
getComponentTree, getScreenLayout, inspectComponent, findComponents, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
|
|
18
|
+
getComponentTree, getScreenLayout, getPressableElements, inspectComponent, findComponents, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
|
|
12
19
|
// Connection state
|
|
13
|
-
getAllConnectionStates, getAllConnectionMetadata, getRecentGaps, formatDuration, cancelAllReconnectionTimers, clearAllConnectionState, suppressReconnection, clearReconnectionSuppression,
|
|
20
|
+
getAllConnectionStates, getAllConnectionMetadata, getRecentGaps, formatDuration, cancelAllReconnectionTimers, cancelReconnectionTimer, clearAllConnectionState, clearAllCDPMessageTimes, suppressReconnection, suppressReconnectionForKey, clearReconnectionSuppression, purgeStaleConnectionsForPorts,
|
|
14
21
|
// Context health tracking
|
|
15
22
|
getContextHealth,
|
|
16
23
|
// Connection resilience
|
|
@@ -20,7 +27,7 @@ connectMetroBuildEvents, disconnectMetroBuildEvents, getBundleErrors, getBundleS
|
|
|
20
27
|
// Error screen parsing (OCR fallback)
|
|
21
28
|
parseErrorScreenText, formatParsedError,
|
|
22
29
|
// OCR
|
|
23
|
-
recognizeText, inferIOSDevicePixelRatio,
|
|
30
|
+
recognizeText, inferIOSDevicePixelRatio, enrichScreenshotWithLayout,
|
|
24
31
|
// Android
|
|
25
32
|
listAndroidDevices, androidScreenshot, androidInstallApp, androidLaunchApp, androidListPackages,
|
|
26
33
|
// Android UI Input (Phase 2)
|
|
@@ -31,19 +38,55 @@ androidDescribeAll, androidDescribePoint,
|
|
|
31
38
|
androidFindElement, androidWaitForElement,
|
|
32
39
|
// iOS
|
|
33
40
|
listIOSSimulators, iosScreenshot, iosInstallApp, iosLaunchApp, iosOpenUrl, iosTerminateApp, iosBootSimulator,
|
|
34
|
-
// iOS
|
|
35
|
-
iosTap, iosSwipe, iosInputText, iosButton, iosKeyEvent, iosKeySequence, iosDescribeAll, iosDescribePoint, IOS_BUTTON_TYPES,
|
|
41
|
+
// iOS UI driver tools
|
|
42
|
+
isUiDriverAvailable, iosTap, iosSwipe, iosInputText, iosButton, iosKeyEvent, iosKeySequence, iosDescribeAll, iosDescribePoint, IOS_BUTTON_TYPES,
|
|
36
43
|
// iOS Element Finding (no screenshots)
|
|
37
44
|
iosFindElement, iosWaitForElement,
|
|
38
|
-
// Debug HTTP Server
|
|
39
|
-
startDebugHttpServer, getDebugServerPort,
|
|
40
45
|
// Telemetry
|
|
41
46
|
initTelemetry, trackToolInvocation, getTargetPlatform,
|
|
42
47
|
// Format utilities (TONL)
|
|
43
|
-
formatLogsAsTonl, formatNetworkAsTonl
|
|
48
|
+
formatLogsAsTonl, formatNetworkAsTonl,
|
|
49
|
+
// LogBox detection & control
|
|
50
|
+
detectLogBox, formatLogBoxWarning, dismissLogBox, formatDismissedEntries, pushLogBox, addLogBoxIgnorePatterns, verifyLogPipeline, formatIssueBody, buildGitHubUrl, shouldShowFeedbackHint, markFeedbackHintShown, } from "./core/index.js";
|
|
51
|
+
// Helper: resolve log buffer for a device (or create a merged buffer from all devices)
|
|
52
|
+
function resolveLogBuffer(device) {
|
|
53
|
+
if (device) {
|
|
54
|
+
const app = getConnectedAppByDevice(device);
|
|
55
|
+
if (!app)
|
|
56
|
+
throw new Error(`No connected device matches "${device}"`);
|
|
57
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
58
|
+
return getLogBuffer(deviceName);
|
|
59
|
+
}
|
|
60
|
+
// Merge all logs into a temporary buffer for read operations
|
|
61
|
+
const merged = new LogBuffer(5000);
|
|
62
|
+
for (const buffer of logBuffers.values()) {
|
|
63
|
+
for (const entry of buffer.getAll()) {
|
|
64
|
+
merged.add(entry);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
// Helper: resolve network buffer for a device (or create a merged buffer from all devices)
|
|
70
|
+
function resolveNetworkBuffer(device) {
|
|
71
|
+
if (device) {
|
|
72
|
+
const app = getConnectedAppByDevice(device);
|
|
73
|
+
if (!app)
|
|
74
|
+
throw new Error(`No connected device matches "${device}"`);
|
|
75
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
76
|
+
return getNetworkBuffer(deviceName);
|
|
77
|
+
}
|
|
78
|
+
// Merge all network requests into a temporary buffer for read operations
|
|
79
|
+
const merged = new NetworkBuffer(5000);
|
|
80
|
+
for (const buffer of networkBuffers.values()) {
|
|
81
|
+
for (const req of buffer.getAll({})) {
|
|
82
|
+
merged.set(`${Math.random()}`, req);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
|
44
87
|
// Create MCP server
|
|
45
88
|
const server = new McpServer({
|
|
46
|
-
name: "react-native-ai-
|
|
89
|
+
name: "react-native-ai-devtools",
|
|
47
90
|
version: "1.0.0"
|
|
48
91
|
}, {
|
|
49
92
|
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 → tap(text=\"Submit\") or tap(x, y) (interact with UI)."
|
|
@@ -112,18 +155,56 @@ function estimateImageTokens(base64Data) {
|
|
|
112
155
|
return Math.ceil(base64Data.length / 4); // fallback
|
|
113
156
|
}
|
|
114
157
|
}
|
|
158
|
+
// Tools that do NOT require an active Metro connection — excluded from feedback hint trigger
|
|
159
|
+
const NON_METRO_TOOLS = new Set([
|
|
160
|
+
"scan_metro",
|
|
161
|
+
"connect_metro",
|
|
162
|
+
"disconnect_metro",
|
|
163
|
+
"ensure_connection",
|
|
164
|
+
"get_connection_status",
|
|
165
|
+
"get_license_status",
|
|
166
|
+
"activate_license",
|
|
167
|
+
"delete_account",
|
|
168
|
+
"get_usage_guide",
|
|
169
|
+
"get_apps",
|
|
170
|
+
"list_ios_simulators",
|
|
171
|
+
"list_android_devices",
|
|
172
|
+
"ios_boot_simulator",
|
|
173
|
+
"ios_launch_app",
|
|
174
|
+
"android_launch_app",
|
|
175
|
+
"ios_install_app",
|
|
176
|
+
"android_install_app",
|
|
177
|
+
"send_feedback"
|
|
178
|
+
]);
|
|
115
179
|
// Registry for dev meta-tool — stores handlers and configs for dynamic dispatch
|
|
116
180
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
117
181
|
const toolRegistry = new Map();
|
|
118
|
-
function registerToolWithTelemetry(toolName, config, handler) {
|
|
182
|
+
function registerToolWithTelemetry(toolName, config, handler, emptyResultDetector) {
|
|
119
183
|
toolRegistry.set(toolName, { config, handler });
|
|
120
184
|
server.registerTool(toolName, config, async (args) => {
|
|
185
|
+
// --- Usage limit check (runs on every tool call) ---
|
|
186
|
+
const usageInfo = getUsageInfo();
|
|
187
|
+
if (usageInfo && !usageInfo.canUse) {
|
|
188
|
+
const isCredits = usageInfo.creditsRemaining !== null;
|
|
189
|
+
const dashboardUrl = `${API_BASE_URL}/dashboard/usage`;
|
|
190
|
+
const message = isCredits
|
|
191
|
+
? `Credits depleted. Purchase more at ${dashboardUrl}`
|
|
192
|
+
: `Monthly free limit reached (${usageInfo.used}/${usageInfo.limit}). Purchase a credits pack for unlimited usage at ${dashboardUrl}`;
|
|
193
|
+
return { content: [{ type: "text", text: message }] };
|
|
194
|
+
}
|
|
121
195
|
const startTime = Date.now();
|
|
122
196
|
let success = true;
|
|
123
197
|
let errorMessage;
|
|
124
198
|
let errorContext;
|
|
125
199
|
let inputTokens;
|
|
126
200
|
let outputTokens;
|
|
201
|
+
let emptyResult;
|
|
202
|
+
let meaningful;
|
|
203
|
+
let changeRate;
|
|
204
|
+
let tapStrategy;
|
|
205
|
+
let iosDriver;
|
|
206
|
+
let responsePreview;
|
|
207
|
+
let emptyReason;
|
|
127
208
|
try {
|
|
128
209
|
inputTokens = Math.ceil(JSON.stringify(args).length / 4);
|
|
129
210
|
}
|
|
@@ -135,10 +216,31 @@ function registerToolWithTelemetry(toolName, config, handler) {
|
|
|
135
216
|
// Check if result indicates an error
|
|
136
217
|
if (result?.isError) {
|
|
137
218
|
success = false;
|
|
138
|
-
|
|
219
|
+
// Prefer concise _errorMessage over full response text (which may be large JSON)
|
|
220
|
+
errorMessage = result._errorMessage || result.content?.[0]?.text || "Unknown error";
|
|
139
221
|
// Extract error context if provided (e.g., the expression that caused a syntax error)
|
|
140
222
|
errorContext = result._errorContext;
|
|
141
223
|
}
|
|
224
|
+
// Check for empty result (only on success, only if detector provided)
|
|
225
|
+
if (success && emptyResultDetector) {
|
|
226
|
+
try {
|
|
227
|
+
emptyResult = emptyResultDetector(result);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Detector failure should never affect tool execution
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Extract meaningfulness data if provided (tap tool verification)
|
|
234
|
+
if (result?._meaningful !== undefined)
|
|
235
|
+
meaningful = result._meaningful;
|
|
236
|
+
if (result?._changeRate !== undefined)
|
|
237
|
+
changeRate = result._changeRate;
|
|
238
|
+
if (result?._tapStrategy)
|
|
239
|
+
tapStrategy = result._tapStrategy;
|
|
240
|
+
if (result?._iosDriver)
|
|
241
|
+
iosDriver = result._iosDriver;
|
|
242
|
+
if (result?._emptyReason)
|
|
243
|
+
emptyReason = result._emptyReason;
|
|
142
244
|
if (Array.isArray(result?.content)) {
|
|
143
245
|
let totalTokens = 0;
|
|
144
246
|
for (const item of result.content) {
|
|
@@ -152,16 +254,52 @@ function registerToolWithTelemetry(toolName, config, handler) {
|
|
|
152
254
|
if (totalTokens > 0)
|
|
153
255
|
outputTokens = totalTokens;
|
|
154
256
|
}
|
|
257
|
+
// Capture response text preview for local dev dashboard
|
|
258
|
+
if (Array.isArray(result?.content)) {
|
|
259
|
+
const textParts = result.content
|
|
260
|
+
.filter((item) => item.type === "text")
|
|
261
|
+
.map((item) => item.text);
|
|
262
|
+
if (textParts.length > 0) {
|
|
263
|
+
responsePreview = textParts.join("\n").substring(0, 2000);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// First-install feedback hint — fires once on first successful Metro-connected tool
|
|
267
|
+
if (!NON_METRO_TOOLS.has(toolName) && shouldShowFeedbackHint()) {
|
|
268
|
+
markFeedbackHintShown();
|
|
269
|
+
// Fire-and-forget — don't block the tool response
|
|
270
|
+
pushLogBox("Congratulations on your first tool call! If you encounter any issues or have ideas for improvement, ask your AI assistant to call send_feedback. Your feedback helps me make this product better for everyone. Best regards, rn-ai-devtools developer.", "warning", true, "logbox").catch(() => {
|
|
271
|
+
// Non-fatal — hint delivery failure should not affect tool execution
|
|
272
|
+
});
|
|
273
|
+
}
|
|
155
274
|
return result;
|
|
156
275
|
}
|
|
157
276
|
catch (error) {
|
|
158
277
|
success = false;
|
|
159
278
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
279
|
+
getPostHogClient()?.captureException(error, getInstallationId(), { tool: toolName, server_version: getServerVersion() });
|
|
160
280
|
throw error;
|
|
161
281
|
}
|
|
162
282
|
finally {
|
|
163
283
|
const duration = Date.now() - startTime;
|
|
164
|
-
trackToolInvocation(toolName, success, duration, errorMessage, errorContext, inputTokens, outputTokens, getTargetPlatform());
|
|
284
|
+
trackToolInvocation(toolName, success, duration, errorMessage, errorContext, inputTokens, outputTokens, getTargetPlatform(), emptyResult, meaningful, changeRate, tapStrategy, iosDriver, responsePreview, emptyReason);
|
|
285
|
+
getPostHogClient()?.capture({
|
|
286
|
+
distinctId: getInstallationId(),
|
|
287
|
+
event: toolName,
|
|
288
|
+
properties: {
|
|
289
|
+
success,
|
|
290
|
+
duration,
|
|
291
|
+
server_version: getServerVersion(),
|
|
292
|
+
...(errorMessage && { error: errorMessage.substring(0, 200) }),
|
|
293
|
+
...(errorMessage && { error_category: categorizeError(errorMessage) }),
|
|
294
|
+
...(getTargetPlatform() && { platform: getTargetPlatform() }),
|
|
295
|
+
...(tapStrategy && { tap_strategy: tapStrategy }),
|
|
296
|
+
...(meaningful !== undefined && { meaningful }),
|
|
297
|
+
...(changeRate !== undefined && { change_rate: changeRate }),
|
|
298
|
+
...(iosDriver && { ios_driver: iosDriver }),
|
|
299
|
+
...(emptyResult !== undefined && { empty_result: emptyResult }),
|
|
300
|
+
...(emptyReason && { empty_reason: emptyReason }),
|
|
301
|
+
},
|
|
302
|
+
});
|
|
165
303
|
}
|
|
166
304
|
});
|
|
167
305
|
}
|
|
@@ -219,33 +357,105 @@ registerToolWithTelemetry("scan_metro", {
|
|
|
219
357
|
]
|
|
220
358
|
};
|
|
221
359
|
}
|
|
222
|
-
// Fetch devices from
|
|
223
|
-
const
|
|
360
|
+
// Phase 1: Fetch devices from all ports first
|
|
361
|
+
const portDevices = new Map();
|
|
224
362
|
for (const port of openPorts) {
|
|
225
363
|
const devices = await fetchDevices(port);
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
const debuggable = filterDebuggableDevices(devices);
|
|
365
|
+
if (debuggable.length > 0) {
|
|
366
|
+
portDevices.set(port, debuggable);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Phase 1.5: Purge stale connections for scanned ports
|
|
370
|
+
// When Metro restarts with a different app, device IDs change.
|
|
371
|
+
// Old connections remain in connectedApps — remove them before connecting new devices.
|
|
372
|
+
const purged = purgeStaleConnectionsForPorts(portDevices);
|
|
373
|
+
// Phase 2: Assign each device to the best port
|
|
374
|
+
// If a device appears on multiple ports, prefer the port where it has
|
|
375
|
+
// the fewest OTHER unique devices (i.e. its dedicated Metro server)
|
|
376
|
+
const devicePortAssignment = new Map(); // deviceName -> best port
|
|
377
|
+
const allDeviceNames = new Map();
|
|
378
|
+
for (const [port, devices] of portDevices) {
|
|
379
|
+
for (const device of devices) {
|
|
380
|
+
const name = device.deviceName || device.title;
|
|
381
|
+
const entry = allDeviceNames.get(name);
|
|
382
|
+
if (entry) {
|
|
383
|
+
entry.ports.push(port);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
allDeviceNames.set(name, { device, ports: [port] });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const [name, { ports }] of allDeviceNames) {
|
|
391
|
+
if (ports.length === 1) {
|
|
392
|
+
devicePortAssignment.set(name, ports[0]);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Prefer the port with fewer OTHER unique devices (the dedicated Metro)
|
|
396
|
+
let bestPort = ports[0];
|
|
397
|
+
let fewestOthers = Infinity;
|
|
398
|
+
for (const port of ports) {
|
|
399
|
+
const othersOnPort = (portDevices.get(port) || [])
|
|
400
|
+
.filter(d => (d.deviceName || d.title) !== name).length;
|
|
401
|
+
if (othersOnPort < fewestOthers) {
|
|
402
|
+
fewestOthers = othersOnPort;
|
|
403
|
+
bestPort = port;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
devicePortAssignment.set(name, bestPort);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Phase 3: Connect devices to their assigned ports
|
|
410
|
+
const results = [];
|
|
411
|
+
if (purged.length > 0) {
|
|
412
|
+
results.push(`Purged ${purged.length} stale connection(s): ${purged.join(", ")}`);
|
|
413
|
+
}
|
|
414
|
+
for (const port of openPorts) {
|
|
415
|
+
const devices = portDevices.get(port);
|
|
416
|
+
if (!devices) {
|
|
417
|
+
results.push(`Port ${port}: No debuggable devices found`);
|
|
228
418
|
continue;
|
|
229
419
|
}
|
|
230
420
|
results.push(`Port ${port}: Found ${devices.length} device(s)`);
|
|
231
|
-
const
|
|
232
|
-
|
|
421
|
+
for (const device of devices) {
|
|
422
|
+
const name = device.deviceName || device.title;
|
|
423
|
+
const assignedPort = devicePortAssignment.get(name);
|
|
424
|
+
if (assignedPort !== port) {
|
|
425
|
+
results.push(` - ${name}: Skipped (assigned to port ${assignedPort})`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
233
428
|
try {
|
|
234
|
-
const connectionResult = await connectToDevice(
|
|
429
|
+
const connectionResult = await connectToDevice(device, port);
|
|
235
430
|
results.push(` - ${connectionResult}`);
|
|
236
|
-
// Also connect to Metro build events for this port
|
|
237
|
-
try {
|
|
238
|
-
await connectMetroBuildEvents(port);
|
|
239
|
-
results.push(` - Connected to Metro build events`);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
// Build events connection is optional, don't fail the scan
|
|
243
|
-
}
|
|
244
431
|
}
|
|
245
432
|
catch (error) {
|
|
246
|
-
results.push(` -
|
|
433
|
+
results.push(` - ${name}: Failed - ${error}`);
|
|
247
434
|
}
|
|
248
435
|
}
|
|
436
|
+
// Connect to Metro build events for this port
|
|
437
|
+
try {
|
|
438
|
+
await connectMetroBuildEvents(port);
|
|
439
|
+
results.push(` - Connected to Metro build events`);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Build events connection is optional
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Proactive check: warn if iOS UI driver is missing
|
|
446
|
+
const hasIosDevice = Array.from(allDeviceNames.values()).some(({ device }) => {
|
|
447
|
+
const title = (device.title || "").toLowerCase();
|
|
448
|
+
return title.includes("iphone") || title.includes("ipad") || title.includes("ios");
|
|
449
|
+
});
|
|
450
|
+
if (hasIosDevice) {
|
|
451
|
+
const uiDriverOk = await isUiDriverAvailable();
|
|
452
|
+
if (!uiDriverOk) {
|
|
453
|
+
results.push("");
|
|
454
|
+
results.push("⚠️ WARNING: iOS UI driver is NOT installed. Tools like tap, ios_swipe, ios_input_text, ios_describe_all, and ios_find_element will fail.");
|
|
455
|
+
results.push(" Install the recommended driver: brew install cameroncooke/axe/axe");
|
|
456
|
+
results.push(" Then set IOS_DRIVER=axe in your MCP server environment.");
|
|
457
|
+
results.push(" Alternative: brew install idb-companion (used by default)");
|
|
458
|
+
}
|
|
249
459
|
}
|
|
250
460
|
return {
|
|
251
461
|
content: [
|
|
@@ -261,33 +471,41 @@ registerToolWithTelemetry("get_apps", {
|
|
|
261
471
|
description: "List currently connected React Native apps and their connection status. If no apps are connected, run scan_metro first to establish a connection.",
|
|
262
472
|
inputSchema: {}
|
|
263
473
|
}, async () => {
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
474
|
+
const apps = getConnectedApps();
|
|
475
|
+
if (apps.length === 0) {
|
|
266
476
|
return {
|
|
267
477
|
content: [
|
|
268
478
|
{
|
|
269
479
|
type: "text",
|
|
270
|
-
text:
|
|
480
|
+
text: "No connected devices. Run scan_metro to discover and connect to Metro servers."
|
|
271
481
|
}
|
|
272
482
|
]
|
|
273
483
|
};
|
|
274
484
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
485
|
+
const deviceLines = apps
|
|
486
|
+
.filter(({ isConnected }) => isConnected)
|
|
487
|
+
.map(({ app }, i) => {
|
|
488
|
+
const name = app.deviceInfo.deviceName || app.deviceInfo.title;
|
|
489
|
+
const appId = app.deviceInfo.appId || app.deviceInfo.title.split(" (")[0] || "unknown";
|
|
490
|
+
const lines = [` ${i + 1}. ${name} — ${appId} (${app.platform}, port ${app.port})`];
|
|
491
|
+
if (app.appDetection) {
|
|
492
|
+
const d = app.appDetection;
|
|
493
|
+
const version = d.reactNativeVersion !== "unknown" ? `RN ${d.reactNativeVersion}` : "RN unknown";
|
|
494
|
+
const expo = d.expoSdkVersion ? `, Expo SDK ${d.expoSdkVersion}` : "";
|
|
495
|
+
lines.push(` Environment: ${version}, ${d.architecture} arch, ${d.jsEngine}, ${app.platform} ${d.osVersion}${expo}`);
|
|
496
|
+
}
|
|
497
|
+
return lines.join("\n");
|
|
278
498
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
499
|
+
const text = [
|
|
500
|
+
`Connected devices:`,
|
|
501
|
+
...deviceLines,
|
|
502
|
+
``,
|
|
503
|
+
`Use device="${apps[0].app.deviceInfo.deviceName}" to target a specific device.`,
|
|
504
|
+
``,
|
|
505
|
+
`Total logs in buffer: ${getTotalLogCount()}`
|
|
506
|
+
].join("\n");
|
|
284
507
|
return {
|
|
285
|
-
content: [
|
|
286
|
-
{
|
|
287
|
-
type: "text",
|
|
288
|
-
text: `Connected apps:\n${status.join("\n")}${simulatorInfo}\n\nTotal logs in buffer: ${logBuffer.size}`
|
|
289
|
-
}
|
|
290
|
-
]
|
|
508
|
+
content: [{ type: "text", text }]
|
|
291
509
|
};
|
|
292
510
|
});
|
|
293
511
|
// Tool: Get connection status (detailed health and gap tracking)
|
|
@@ -370,6 +588,99 @@ registerToolWithTelemetry("get_connection_status", {
|
|
|
370
588
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
371
589
|
};
|
|
372
590
|
});
|
|
591
|
+
// Tool: License status
|
|
592
|
+
registerToolWithTelemetry("get_license_status", {
|
|
593
|
+
description: "Get your installation ID and license status. Shows your unique Installation ID (needed to activate Pro in the dashboard), current license tier, and cache validity.",
|
|
594
|
+
inputSchema: {},
|
|
595
|
+
}, async () => {
|
|
596
|
+
const status = getLicenseStatus();
|
|
597
|
+
const lines = [];
|
|
598
|
+
lines.push(`Installation ID: ${status.installationId}`);
|
|
599
|
+
lines.push(`License: ${status.tier.charAt(0).toUpperCase() + status.tier.slice(1)}`);
|
|
600
|
+
if (status.plan) {
|
|
601
|
+
lines.push(`Plan expires: ${status.plan.expiresAt}`);
|
|
602
|
+
}
|
|
603
|
+
lines.push(`Cache valid until: ${status.cacheExpiresAt}`);
|
|
604
|
+
// Usage limits
|
|
605
|
+
const usage = getUsageInfo();
|
|
606
|
+
if (usage) {
|
|
607
|
+
lines.push("");
|
|
608
|
+
lines.push("--- Usage ---");
|
|
609
|
+
if (usage.creditsRemaining !== null) {
|
|
610
|
+
lines.push(`Credits remaining: ${usage.creditsRemaining.toLocaleString()}`);
|
|
611
|
+
lines.push(`Used this month: ${usage.used.toLocaleString()}`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
lines.push(`Monthly usage: ${usage.used} / ${usage.limit}`);
|
|
615
|
+
}
|
|
616
|
+
lines.push(`Month: ${usage.monthKey}`);
|
|
617
|
+
if (usage.promotionalPeriod) {
|
|
618
|
+
const endsAt = usage.promotionalPeriodEndsAt
|
|
619
|
+
? new Date(usage.promotionalPeriodEndsAt).toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" })
|
|
620
|
+
: "unknown";
|
|
621
|
+
lines.push(`Status: Active (promotional period — no limits until ${endsAt})`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
lines.push(`Status: ${usage.canUse ? "Active" : "Limit reached"}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (status.tier === "free") {
|
|
628
|
+
const dashboardUrl = getDashboardUrl();
|
|
629
|
+
lines.push("");
|
|
630
|
+
if (dashboardUrl) {
|
|
631
|
+
lines.push(`Link your account: ${dashboardUrl}/link?id=${status.installationId}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
// Tool: Send feedback / bug report / feature request
|
|
639
|
+
registerToolWithTelemetry("send_feedback", {
|
|
640
|
+
description: "Create a feedback report (bug, feature request, or general feedback) for the react-native-ai-devtools team. " +
|
|
641
|
+
"Auto-collects environment info. Returns a pre-filled GitHub issue URL and formatted issue body. " +
|
|
642
|
+
"Ask the user to open the URL and paste the body to submit.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: z.enum(["feedback", "feature_request", "bug"]).describe('Type of feedback: "feedback", "feature_request", or "bug"'),
|
|
645
|
+
title: z.string().describe("Short summary (becomes the GitHub issue title)"),
|
|
646
|
+
description: z.string().describe("Detailed description of the feedback, feature request, or bug"),
|
|
647
|
+
workflow_context: z.string().optional().describe("What the user was trying to accomplish when they decided to give feedback")
|
|
648
|
+
}
|
|
649
|
+
}, async ({ type, title, description, workflow_context }) => {
|
|
650
|
+
// Collect environment info
|
|
651
|
+
const serverVersion = getServerVersion();
|
|
652
|
+
const platform = process.platform;
|
|
653
|
+
const deviceType = getTargetPlatform();
|
|
654
|
+
const licenseStatus = getLicenseStatus();
|
|
655
|
+
const env = {
|
|
656
|
+
serverVersion,
|
|
657
|
+
platform,
|
|
658
|
+
deviceType,
|
|
659
|
+
licenseTier: licenseStatus.tier
|
|
660
|
+
};
|
|
661
|
+
const input = { type, title, description, workflowContext: workflow_context };
|
|
662
|
+
const issueBody = formatIssueBody(input, env);
|
|
663
|
+
const githubUrl = buildGitHubUrl(title, type);
|
|
664
|
+
const output = [
|
|
665
|
+
"## Feedback Report Ready",
|
|
666
|
+
"",
|
|
667
|
+
`**GitHub Issue URL:** ${githubUrl}`,
|
|
668
|
+
"",
|
|
669
|
+
"**Issue body to paste:**",
|
|
670
|
+
"",
|
|
671
|
+
"```markdown",
|
|
672
|
+
issueBody,
|
|
673
|
+
"```",
|
|
674
|
+
"",
|
|
675
|
+
"Please ask the user to:",
|
|
676
|
+
"1. Open the GitHub URL above",
|
|
677
|
+
"2. Paste the issue body into the description field",
|
|
678
|
+
"3. Review and submit the issue"
|
|
679
|
+
].join("\n");
|
|
680
|
+
return {
|
|
681
|
+
content: [{ type: "text", text: output }]
|
|
682
|
+
};
|
|
683
|
+
});
|
|
373
684
|
// Tool: Ensure connection health
|
|
374
685
|
registerToolWithTelemetry("ensure_connection", {
|
|
375
686
|
description: "Verify or establish a healthy connection to a React Native app. Use before running commands if connection may be stale, or after navigation/reload. This tool runs a health check and will auto-reconnect if needed.",
|
|
@@ -393,7 +704,7 @@ registerToolWithTelemetry("ensure_connection", {
|
|
|
393
704
|
content: [
|
|
394
705
|
{
|
|
395
706
|
type: "text",
|
|
396
|
-
text: result.error || "Connection failed:
|
|
707
|
+
text: result.error || "Connection failed: No error details available. Try running scan_metro to check if Metro is running, then ensure_connection with forceRefresh=true."
|
|
397
708
|
}
|
|
398
709
|
],
|
|
399
710
|
isError: true
|
|
@@ -455,16 +766,71 @@ registerToolWithTelemetry("get_logs", {
|
|
|
455
766
|
.boolean()
|
|
456
767
|
.optional()
|
|
457
768
|
.default(false)
|
|
458
|
-
.describe("Return summary statistics instead of full logs (count by level + last 5 messages). Use for quick overview.")
|
|
769
|
+
.describe("Return summary statistics instead of full logs (count by level + last 5 messages). Use for quick overview."),
|
|
770
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
771
|
+
}
|
|
772
|
+
}, async ({ maxLogs, level, startFromText, maxMessageLength, verbose, format, summary, device }) => {
|
|
773
|
+
// Check if SDK is installed — prefer SDK data for richer logs
|
|
774
|
+
const sdkAvailable = await isSDKInstalled();
|
|
775
|
+
if (sdkAvailable) {
|
|
776
|
+
if (summary) {
|
|
777
|
+
const sdkStats = await getSDKConsoleStats();
|
|
778
|
+
if (sdkStats.success) {
|
|
779
|
+
const s = sdkStats.data;
|
|
780
|
+
const lines = [];
|
|
781
|
+
lines.push(`Total logs: ${s.total}`);
|
|
782
|
+
if (s.byLevel && Object.keys(s.byLevel).length > 0) {
|
|
783
|
+
lines.push("\nBy Level:");
|
|
784
|
+
for (const [lvl, cnt] of Object.entries(s.byLevel))
|
|
785
|
+
lines.push(` ${lvl}: ${cnt}`);
|
|
786
|
+
}
|
|
787
|
+
return { content: [{ type: "text", text: `Log Summary (SDK):\n\n${lines.join("\n")}` }] };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const sdkResult = await querySDKConsole({ count: maxLogs, level, text: startFromText });
|
|
791
|
+
if (sdkResult.success) {
|
|
792
|
+
const entries = sdkResult.data;
|
|
793
|
+
if (entries.length === 0) {
|
|
794
|
+
return { content: [{ type: "text", text: "No console logs captured yet." }] };
|
|
795
|
+
}
|
|
796
|
+
if (format === "tonl") {
|
|
797
|
+
const tonlLines = entries.map((e) => {
|
|
798
|
+
const time = new Date(e.timestamp).toLocaleTimeString();
|
|
799
|
+
let msg = e.message;
|
|
800
|
+
if (!verbose && maxMessageLength > 0 && msg.length > maxMessageLength) {
|
|
801
|
+
msg = msg.slice(0, maxMessageLength) + "...";
|
|
802
|
+
}
|
|
803
|
+
return `${time} [${e.level}] ${msg}`;
|
|
804
|
+
});
|
|
805
|
+
return { content: [{ type: "text", text: `Console Logs (${entries.length} entries, SDK):\n\n${tonlLines.join("\n")}` }] };
|
|
806
|
+
}
|
|
807
|
+
const lines = entries.map((e) => {
|
|
808
|
+
const time = new Date(e.timestamp).toLocaleTimeString();
|
|
809
|
+
let msg = e.message;
|
|
810
|
+
if (!verbose && maxMessageLength > 0 && msg.length > maxMessageLength) {
|
|
811
|
+
msg = msg.slice(0, maxMessageLength) + "...";
|
|
812
|
+
}
|
|
813
|
+
return `[${time}] [${e.level.toUpperCase()}] ${msg}`;
|
|
814
|
+
});
|
|
815
|
+
return { content: [{ type: "text", text: `Console Logs (${entries.length} entries, SDK):\n\n${lines.join("\n")}` }] };
|
|
816
|
+
}
|
|
459
817
|
}
|
|
460
|
-
}, async ({ maxLogs, level, startFromText, maxMessageLength, verbose, format, summary }) => {
|
|
461
818
|
// Return summary if requested
|
|
462
819
|
if (summary) {
|
|
463
|
-
const summaryText = getLogSummary(
|
|
820
|
+
const summaryText = getLogSummary(resolveLogBuffer(device), { lastN: 5, maxMessageLength: 100 });
|
|
464
821
|
let connectionWarning = "";
|
|
465
|
-
if (
|
|
466
|
-
const status = await checkAndEnsureConnection();
|
|
822
|
+
if (getTotalLogCount() === 0) {
|
|
823
|
+
const status = await checkAndEnsureConnection(device);
|
|
467
824
|
connectionWarning = status.message ? `\n\n${status.message}` : "";
|
|
825
|
+
if (status.connected) {
|
|
826
|
+
const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
|
|
827
|
+
if (targetApp) {
|
|
828
|
+
const pipeline = await verifyLogPipeline(targetApp);
|
|
829
|
+
if (pipeline.message) {
|
|
830
|
+
connectionWarning += `\n\n${pipeline.message}`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
468
834
|
}
|
|
469
835
|
return {
|
|
470
836
|
content: [
|
|
@@ -475,7 +841,7 @@ registerToolWithTelemetry("get_logs", {
|
|
|
475
841
|
]
|
|
476
842
|
};
|
|
477
843
|
}
|
|
478
|
-
const { logs, count, formatted } = getLogs(
|
|
844
|
+
const { logs, count, formatted } = getLogs(resolveLogBuffer(device), {
|
|
479
845
|
maxLogs,
|
|
480
846
|
level,
|
|
481
847
|
startFromText,
|
|
@@ -484,9 +850,60 @@ registerToolWithTelemetry("get_logs", {
|
|
|
484
850
|
});
|
|
485
851
|
// Check connection health
|
|
486
852
|
let connectionWarning = "";
|
|
853
|
+
let emptyReason;
|
|
487
854
|
if (count === 0) {
|
|
488
|
-
const status = await checkAndEnsureConnection();
|
|
855
|
+
const status = await checkAndEnsureConnection(device);
|
|
489
856
|
connectionWarning = status.message ? `\n\n${status.message}` : "";
|
|
857
|
+
// Track empty reason for telemetry
|
|
858
|
+
emptyReason = "no_logs";
|
|
859
|
+
if (!status.connected) {
|
|
860
|
+
emptyReason = "disconnected";
|
|
861
|
+
}
|
|
862
|
+
else if (status.wasReconnected) {
|
|
863
|
+
emptyReason = "post_reconnect";
|
|
864
|
+
}
|
|
865
|
+
// End-to-end log pipeline verification (with automatic recovery)
|
|
866
|
+
if (status.connected) {
|
|
867
|
+
const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
|
|
868
|
+
if (targetApp) {
|
|
869
|
+
const pipeline = await verifyLogPipeline(targetApp);
|
|
870
|
+
if (pipeline.message) {
|
|
871
|
+
connectionWarning += `\n\n${pipeline.message}`;
|
|
872
|
+
}
|
|
873
|
+
if (!pipeline.ok) {
|
|
874
|
+
emptyReason = "pipeline_failed";
|
|
875
|
+
}
|
|
876
|
+
else if (pipeline.recovered) {
|
|
877
|
+
emptyReason = "pipeline_recovered";
|
|
878
|
+
}
|
|
879
|
+
// If pipeline recovered, re-read the buffer — new logs may have arrived
|
|
880
|
+
if (pipeline.recovered && pipeline.ok) {
|
|
881
|
+
const retryResult = getLogs(resolveLogBuffer(device), {
|
|
882
|
+
maxLogs, level, startFromText, maxMessageLength, verbose
|
|
883
|
+
});
|
|
884
|
+
if (retryResult.count > 0) {
|
|
885
|
+
// Return the recovered logs instead of empty
|
|
886
|
+
return {
|
|
887
|
+
_emptyReason: "pipeline_recovered",
|
|
888
|
+
content: [{
|
|
889
|
+
type: "text",
|
|
890
|
+
text: `React Native Console Logs (${retryResult.count} entries):\n\n${retryResult.formatted}${connectionWarning}`
|
|
891
|
+
}]
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Add diagnostic metadata for empty results (captured by telemetry via responsePreview)
|
|
898
|
+
if (count === 0) {
|
|
899
|
+
const diagParts = [
|
|
900
|
+
`empty_reason=${status?.wasReconnected ? "post_reconnect" : "no_logs"}`,
|
|
901
|
+
`connection=${getPassiveConnectionStatus().reason}`,
|
|
902
|
+
`device_count=${connectedApps.size}`,
|
|
903
|
+
`buffer_sizes=${JSON.stringify(Object.fromEntries([...logBuffers.entries()].map(([k, v]) => [k, v.size])))}`,
|
|
904
|
+
];
|
|
905
|
+
connectionWarning += `\n\n[DIAG] ${diagParts.join(", ")}`;
|
|
906
|
+
}
|
|
490
907
|
}
|
|
491
908
|
else {
|
|
492
909
|
const passive = getPassiveConnectionStatus();
|
|
@@ -514,6 +931,7 @@ registerToolWithTelemetry("get_logs", {
|
|
|
514
931
|
if (format === "tonl") {
|
|
515
932
|
const tonlOutput = formatLogsAsTonl(logs, { maxMessageLength: verbose ? 0 : maxMessageLength });
|
|
516
933
|
return {
|
|
934
|
+
...(emptyReason && { _emptyReason: emptyReason }),
|
|
517
935
|
content: [
|
|
518
936
|
{
|
|
519
937
|
type: "text",
|
|
@@ -523,6 +941,7 @@ registerToolWithTelemetry("get_logs", {
|
|
|
523
941
|
};
|
|
524
942
|
}
|
|
525
943
|
return {
|
|
944
|
+
...(emptyReason && { _emptyReason: emptyReason }),
|
|
526
945
|
content: [
|
|
527
946
|
{
|
|
528
947
|
type: "text",
|
|
@@ -530,7 +949,9 @@ registerToolWithTelemetry("get_logs", {
|
|
|
530
949
|
}
|
|
531
950
|
]
|
|
532
951
|
};
|
|
533
|
-
}
|
|
952
|
+
},
|
|
953
|
+
// Empty result detector: buffer has no entries at all
|
|
954
|
+
() => getTotalLogCount() === 0);
|
|
534
955
|
// Tool: Search logs
|
|
535
956
|
registerToolWithTelemetry("search_logs", {
|
|
536
957
|
description: "Search console logs for text (case-insensitive)",
|
|
@@ -551,14 +972,46 @@ registerToolWithTelemetry("search_logs", {
|
|
|
551
972
|
.enum(["text", "tonl"])
|
|
552
973
|
.optional()
|
|
553
974
|
.default("tonl")
|
|
554
|
-
.describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)")
|
|
975
|
+
.describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)"),
|
|
976
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
977
|
+
}
|
|
978
|
+
}, async ({ text, maxResults, maxMessageLength, verbose, format, device }) => {
|
|
979
|
+
// Check if SDK is installed — prefer SDK data
|
|
980
|
+
const sdkAvailable = await isSDKInstalled();
|
|
981
|
+
if (sdkAvailable) {
|
|
982
|
+
const sdkResult = await querySDKConsole({ count: maxResults, text });
|
|
983
|
+
if (sdkResult.success) {
|
|
984
|
+
const entries = sdkResult.data;
|
|
985
|
+
if (entries.length === 0) {
|
|
986
|
+
return { content: [{ type: "text", text: `No logs matching "${text}" found.` }] };
|
|
987
|
+
}
|
|
988
|
+
if (format === "tonl") {
|
|
989
|
+
const tonlLines = entries.map((e) => {
|
|
990
|
+
const time = new Date(e.timestamp).toLocaleTimeString();
|
|
991
|
+
let msg = e.message;
|
|
992
|
+
if (!verbose && maxMessageLength > 0 && msg.length > maxMessageLength) {
|
|
993
|
+
msg = msg.slice(0, maxMessageLength) + "...";
|
|
994
|
+
}
|
|
995
|
+
return `${time} [${e.level}] ${msg}`;
|
|
996
|
+
});
|
|
997
|
+
return { content: [{ type: "text", text: `Search results for "${text}" (${entries.length} matches, SDK):\n\n${tonlLines.join("\n")}` }] };
|
|
998
|
+
}
|
|
999
|
+
const lines = entries.map((e) => {
|
|
1000
|
+
const time = new Date(e.timestamp).toLocaleTimeString();
|
|
1001
|
+
let msg = e.message;
|
|
1002
|
+
if (!verbose && maxMessageLength > 0 && msg.length > maxMessageLength) {
|
|
1003
|
+
msg = msg.slice(0, maxMessageLength) + "...";
|
|
1004
|
+
}
|
|
1005
|
+
return `[${time}] [${e.level.toUpperCase()}] ${msg}`;
|
|
1006
|
+
});
|
|
1007
|
+
return { content: [{ type: "text", text: `Search results for "${text}" (${entries.length} matches, SDK):\n\n${lines.join("\n")}` }] };
|
|
1008
|
+
}
|
|
555
1009
|
}
|
|
556
|
-
|
|
557
|
-
const { logs, count, formatted } = searchLogs(logBuffer, text, { maxResults, maxMessageLength, verbose });
|
|
1010
|
+
const { logs, count, formatted } = searchLogs(resolveLogBuffer(device), text, { maxResults, maxMessageLength, verbose });
|
|
558
1011
|
// Check connection health
|
|
559
1012
|
let connectionWarning = "";
|
|
560
1013
|
if (count === 0) {
|
|
561
|
-
const status = await checkAndEnsureConnection();
|
|
1014
|
+
const status = await checkAndEnsureConnection(device);
|
|
562
1015
|
connectionWarning = status.message ? `\n\n${status.message}` : "";
|
|
563
1016
|
}
|
|
564
1017
|
else {
|
|
@@ -591,17 +1044,32 @@ registerToolWithTelemetry("search_logs", {
|
|
|
591
1044
|
// Tool: Clear logs
|
|
592
1045
|
registerToolWithTelemetry("clear_logs", {
|
|
593
1046
|
description: "Clear the log buffer",
|
|
594
|
-
inputSchema: {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1047
|
+
inputSchema: {
|
|
1048
|
+
device: z.string().optional().describe("Target device name (substring match). Omit to clear all devices. Run get_apps to see connected devices.")
|
|
1049
|
+
}
|
|
1050
|
+
}, async ({ device }) => {
|
|
1051
|
+
if (device) {
|
|
1052
|
+
const app = getConnectedAppByDevice(device);
|
|
1053
|
+
if (!app)
|
|
1054
|
+
throw new Error(`No connected device matches "${device}"`);
|
|
1055
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
1056
|
+
const count = getLogBuffer(deviceName).clear();
|
|
1057
|
+
return { content: [{ type: "text", text: `Cleared ${count} log entries from ${deviceName}.` }] };
|
|
1058
|
+
}
|
|
1059
|
+
// Clear all
|
|
1060
|
+
let total = 0;
|
|
1061
|
+
for (const buffer of logBuffers.values()) {
|
|
1062
|
+
total += buffer.clear();
|
|
1063
|
+
}
|
|
1064
|
+
// Also clear SDK buffer if available
|
|
1065
|
+
const sdkAvailable = await isSDKInstalled();
|
|
1066
|
+
if (sdkAvailable) {
|
|
1067
|
+
const sdkResult = await clearSDKConsole();
|
|
1068
|
+
if (sdkResult.success && sdkResult.count) {
|
|
1069
|
+
total += sdkResult.count;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return { content: [{ type: "text", text: `Cleared ${total} log entries from all devices.` }] };
|
|
605
1073
|
});
|
|
606
1074
|
// Tool: Connect to specific Metro port
|
|
607
1075
|
registerToolWithTelemetry("connect_metro", {
|
|
@@ -662,9 +1130,11 @@ registerToolWithTelemetry("connect_metro", {
|
|
|
662
1130
|
});
|
|
663
1131
|
// Tool: Disconnect from Metro
|
|
664
1132
|
registerToolWithTelemetry("disconnect_metro", {
|
|
665
|
-
description: "Disconnect from
|
|
666
|
-
inputSchema: {
|
|
667
|
-
|
|
1133
|
+
description: "Disconnect from Metro servers and stop auto-reconnection. Without device param: disconnects ALL devices. With device param: disconnects only the matching device. Use this to remove stale connections or free the CDP slot for the built-in debugger. Log and network buffers are preserved. Reconnect later with scan_metro.",
|
|
1134
|
+
inputSchema: {
|
|
1135
|
+
device: z.string().optional().describe("Target device name (substring match) to disconnect. Omit to disconnect all devices. Run get_apps to see connected devices.")
|
|
1136
|
+
}
|
|
1137
|
+
}, async ({ device }) => {
|
|
668
1138
|
const connections = getConnectedApps();
|
|
669
1139
|
if (connections.length === 0) {
|
|
670
1140
|
return {
|
|
@@ -676,6 +1146,36 @@ registerToolWithTelemetry("disconnect_metro", {
|
|
|
676
1146
|
]
|
|
677
1147
|
};
|
|
678
1148
|
}
|
|
1149
|
+
// Targeted disconnect: only disconnect a specific device
|
|
1150
|
+
if (device) {
|
|
1151
|
+
const app = getConnectedAppByDevice(device);
|
|
1152
|
+
if (!app) {
|
|
1153
|
+
return {
|
|
1154
|
+
content: [{ type: "text", text: `No connected device matches "${device}". Run get_apps to see connected devices.` }],
|
|
1155
|
+
isError: true
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
// Find and close the matching connection
|
|
1159
|
+
for (const [key, connectedApp] of connectedApps.entries()) {
|
|
1160
|
+
if (connectedApp.ws === app.ws) {
|
|
1161
|
+
// Suppress reconnection for this specific device
|
|
1162
|
+
suppressReconnectionForKey(key);
|
|
1163
|
+
cancelReconnectionTimer(key);
|
|
1164
|
+
try {
|
|
1165
|
+
connectedApp.ws.close();
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
// Ignore close errors
|
|
1169
|
+
}
|
|
1170
|
+
connectedApps.delete(key);
|
|
1171
|
+
const name = connectedApp.deviceInfo.deviceName || connectedApp.deviceInfo.title;
|
|
1172
|
+
return {
|
|
1173
|
+
content: [{ type: "text", text: `Disconnected from ${name} (port ${connectedApp.port}). Buffers preserved. Use scan_metro to reconnect.` }]
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Disconnect all
|
|
679
1179
|
const disconnected = [];
|
|
680
1180
|
// Suppress reconnection BEFORE closing sockets
|
|
681
1181
|
// (close handlers fire async and would re-schedule reconnection)
|
|
@@ -696,6 +1196,7 @@ registerToolWithTelemetry("disconnect_metro", {
|
|
|
696
1196
|
disconnectMetroBuildEvents();
|
|
697
1197
|
// Clear connection state (but NOT log/network buffers)
|
|
698
1198
|
clearAllConnectionState();
|
|
1199
|
+
clearAllCDPMessageTimes();
|
|
699
1200
|
const lines = [
|
|
700
1201
|
`Disconnected from ${disconnected.length} app(s):`,
|
|
701
1202
|
...disconnected.map((d) => ` - ${d}`),
|
|
@@ -737,10 +1238,11 @@ registerToolWithTelemetry("execute_in_app", {
|
|
|
737
1238
|
.boolean()
|
|
738
1239
|
.optional()
|
|
739
1240
|
.default(false)
|
|
740
|
-
.describe("Disable result truncation. Tip: Be cautious - Redux stores or large state can return 10KB+.")
|
|
1241
|
+
.describe("Disable result truncation. Tip: Be cautious - Redux stores or large state can return 10KB+."),
|
|
1242
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
741
1243
|
}
|
|
742
|
-
}, async ({ expression, awaitPromise, maxResultLength, verbose }) => {
|
|
743
|
-
const result = await executeInApp(expression, awaitPromise);
|
|
1244
|
+
}, async ({ expression, awaitPromise, maxResultLength, verbose, device }) => {
|
|
1245
|
+
const result = await executeInApp(expression, awaitPromise, {}, device);
|
|
744
1246
|
if (!result.success) {
|
|
745
1247
|
let errorText = `Error: ${result.error}`;
|
|
746
1248
|
// If the error is a ReferenceError (accessing a global that doesn't exist),
|
|
@@ -775,13 +1277,22 @@ registerToolWithTelemetry("execute_in_app", {
|
|
|
775
1277
|
}
|
|
776
1278
|
]
|
|
777
1279
|
};
|
|
1280
|
+
},
|
|
1281
|
+
// Empty result detector: successful execution but no meaningful output
|
|
1282
|
+
(result) => {
|
|
1283
|
+
if (result?.isError)
|
|
1284
|
+
return false;
|
|
1285
|
+
const text = result?.content?.[0]?.text;
|
|
1286
|
+
return text === undefined || text === "" || text === "undefined" || text === "null";
|
|
778
1287
|
});
|
|
779
1288
|
// Tool: List debug globals available in the app
|
|
780
1289
|
registerToolWithTelemetry("list_debug_globals", {
|
|
781
1290
|
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.",
|
|
782
|
-
inputSchema: {
|
|
783
|
-
|
|
784
|
-
|
|
1291
|
+
inputSchema: {
|
|
1292
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1293
|
+
}
|
|
1294
|
+
}, async ({ device }) => {
|
|
1295
|
+
const result = await listDebugGlobals(device);
|
|
785
1296
|
if (!result.success) {
|
|
786
1297
|
return {
|
|
787
1298
|
content: [
|
|
@@ -808,10 +1319,11 @@ registerToolWithTelemetry("inspect_global", {
|
|
|
808
1319
|
inputSchema: {
|
|
809
1320
|
objectName: z
|
|
810
1321
|
.string()
|
|
811
|
-
.describe("Name of the global object to inspect (e.g., '__EXPO_ROUTER__', '__APOLLO_CLIENT__')")
|
|
1322
|
+
.describe("Name of the global object to inspect (e.g., '__EXPO_ROUTER__', '__APOLLO_CLIENT__')"),
|
|
1323
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
812
1324
|
}
|
|
813
|
-
}, async ({ objectName }) => {
|
|
814
|
-
const result = await inspectGlobal(objectName);
|
|
1325
|
+
}, async ({ objectName, device }) => {
|
|
1326
|
+
const result = await inspectGlobal(objectName, device);
|
|
815
1327
|
if (!result.success) {
|
|
816
1328
|
let errorText = `Error: ${result.error}`;
|
|
817
1329
|
// If the error is a ReferenceError (accessing a global that doesn't exist),
|
|
@@ -843,13 +1355,8 @@ registerToolWithTelemetry("inspect_global", {
|
|
|
843
1355
|
// ============================================================================
|
|
844
1356
|
// Tool: Get the React component tree
|
|
845
1357
|
registerToolWithTelemetry("get_component_tree", {
|
|
846
|
-
description: "Get the React component tree from the running app.
|
|
1358
|
+
description: "Get the full React component tree from the running app. Shows the complete fiber hierarchy including providers, navigation wrappers, and internal components. For a screen overview with positions and text, use get_screen_layout instead. Use structureOnly=true for compact names-only output.",
|
|
847
1359
|
inputSchema: {
|
|
848
|
-
focusedOnly: z
|
|
849
|
-
.boolean()
|
|
850
|
-
.optional()
|
|
851
|
-
.default(false)
|
|
852
|
-
.describe("Return only the focused/active screen subtree, skipping navigation wrappers and overlays. Dramatically reduces output size. (Recommended: true)"),
|
|
853
1360
|
structureOnly: z
|
|
854
1361
|
.boolean()
|
|
855
1362
|
.optional()
|
|
@@ -858,7 +1365,7 @@ registerToolWithTelemetry("get_component_tree", {
|
|
|
858
1365
|
maxDepth: z
|
|
859
1366
|
.number()
|
|
860
1367
|
.optional()
|
|
861
|
-
.describe("Maximum tree depth (default:
|
|
1368
|
+
.describe("Maximum tree depth (default: 5000)"),
|
|
862
1369
|
includeProps: z
|
|
863
1370
|
.boolean()
|
|
864
1371
|
.optional()
|
|
@@ -878,17 +1385,18 @@ registerToolWithTelemetry("get_component_tree", {
|
|
|
878
1385
|
.enum(["json", "tonl"])
|
|
879
1386
|
.optional()
|
|
880
1387
|
.default("tonl")
|
|
881
|
-
.describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true.")
|
|
1388
|
+
.describe("Output format: 'json' or 'tonl' (default, compact indented tree). Ignored if structureOnly=true."),
|
|
1389
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
882
1390
|
}
|
|
883
|
-
}, async ({
|
|
1391
|
+
}, async ({ structureOnly, maxDepth, includeProps, includeStyles, hideInternals, format, device }) => {
|
|
884
1392
|
const result = await getComponentTree({
|
|
885
|
-
focusedOnly,
|
|
886
1393
|
structureOnly,
|
|
887
1394
|
maxDepth,
|
|
888
1395
|
includeProps,
|
|
889
1396
|
includeStyles,
|
|
890
1397
|
hideInternals,
|
|
891
|
-
format
|
|
1398
|
+
format,
|
|
1399
|
+
device
|
|
892
1400
|
});
|
|
893
1401
|
if (!result.success) {
|
|
894
1402
|
return {
|
|
@@ -912,36 +1420,22 @@ registerToolWithTelemetry("get_component_tree", {
|
|
|
912
1420
|
});
|
|
913
1421
|
// Tool: Get full screen layout (all components with layout styles)
|
|
914
1422
|
registerToolWithTelemetry("get_screen_layout", {
|
|
915
|
-
description: "Get
|
|
1423
|
+
description: "Get a screen map showing visible components as an indented tree with actual screen positions. Uses measureInWindow for real coordinates and filters out off-screen components. Returns meaningful component names with text content and frame data (x,y width x height). Coordinates are in **points** (iOS) or **dp** (Android) — NOT screenshot pixels. Use tap(text=...) or tap(testID=...) to interact with discovered components. Use extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.).",
|
|
916
1424
|
inputSchema: {
|
|
917
|
-
|
|
918
|
-
.number()
|
|
919
|
-
.optional()
|
|
920
|
-
.default(65)
|
|
921
|
-
.describe("Maximum tree depth to traverse (default: 65, balanced for most screens)"),
|
|
922
|
-
componentsOnly: z
|
|
1425
|
+
extended: z
|
|
923
1426
|
.boolean()
|
|
924
1427
|
.optional()
|
|
925
1428
|
.default(false)
|
|
926
|
-
.describe("
|
|
927
|
-
shortPath: z
|
|
928
|
-
.boolean()
|
|
929
|
-
.optional()
|
|
930
|
-
.default(true)
|
|
931
|
-
.describe("Show only last 3 path segments instead of full path (default: true)"),
|
|
1429
|
+
.describe("Include layout styles (padding, margin, flex, backgroundColor, borderRadius, etc.) for each component. Default: false for compact output."),
|
|
932
1430
|
summary: z
|
|
933
1431
|
.boolean()
|
|
934
1432
|
.optional()
|
|
935
1433
|
.default(false)
|
|
936
|
-
.describe("Return only component counts by name instead of full
|
|
937
|
-
|
|
938
|
-
.enum(["json", "tonl"])
|
|
939
|
-
.optional()
|
|
940
|
-
.default("tonl")
|
|
941
|
-
.describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)")
|
|
1434
|
+
.describe("Return only component counts by name instead of full tree (default: false)"),
|
|
1435
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
942
1436
|
}
|
|
943
|
-
}, async ({
|
|
944
|
-
const result = await getScreenLayout({
|
|
1437
|
+
}, async ({ extended, summary, device }) => {
|
|
1438
|
+
const result = await getScreenLayout({ extended, summary, device });
|
|
945
1439
|
if (!result.success) {
|
|
946
1440
|
return {
|
|
947
1441
|
content: [
|
|
@@ -962,9 +1456,37 @@ registerToolWithTelemetry("get_screen_layout", {
|
|
|
962
1456
|
]
|
|
963
1457
|
};
|
|
964
1458
|
});
|
|
1459
|
+
// Tool: Get all pressable elements on screen
|
|
1460
|
+
registerToolWithTelemetry("get_pressable_elements", {
|
|
1461
|
+
description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, tap-ready center coordinates (in points/dp), text labels, testID, and accessibilityLabel. Useful when you need to tap an icon or button but can't identify it from a screenshot alone. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).",
|
|
1462
|
+
inputSchema: {
|
|
1463
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1464
|
+
}
|
|
1465
|
+
}, async ({ device }) => {
|
|
1466
|
+
const result = await getPressableElements({ device });
|
|
1467
|
+
if (!result.success) {
|
|
1468
|
+
return {
|
|
1469
|
+
content: [
|
|
1470
|
+
{
|
|
1471
|
+
type: "text",
|
|
1472
|
+
text: `Error: ${result.error}`
|
|
1473
|
+
}
|
|
1474
|
+
],
|
|
1475
|
+
isError: true
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
content: [
|
|
1480
|
+
{
|
|
1481
|
+
type: "text",
|
|
1482
|
+
text: result.result || "No pressable elements found."
|
|
1483
|
+
}
|
|
1484
|
+
]
|
|
1485
|
+
};
|
|
1486
|
+
});
|
|
965
1487
|
// Tool: Inspect a specific component by name
|
|
966
1488
|
registerToolWithTelemetry("inspect_component", {
|
|
967
|
-
description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after
|
|
1489
|
+
description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go.",
|
|
968
1490
|
inputSchema: {
|
|
969
1491
|
componentName: z
|
|
970
1492
|
.string()
|
|
@@ -990,16 +1512,18 @@ registerToolWithTelemetry("inspect_component", {
|
|
|
990
1512
|
.boolean()
|
|
991
1513
|
.optional()
|
|
992
1514
|
.default(true)
|
|
993
|
-
.describe("Simplify hooks output by hiding effects and reducing depth (default: true)")
|
|
1515
|
+
.describe("Simplify hooks output by hiding effects and reducing depth (default: true)"),
|
|
1516
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
994
1517
|
}
|
|
995
|
-
}, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks }) => {
|
|
1518
|
+
}, async ({ componentName, index, includeState, includeChildren, childrenDepth, shortPath, simplifyHooks, device }) => {
|
|
996
1519
|
const result = await inspectComponent(componentName, {
|
|
997
1520
|
index,
|
|
998
1521
|
includeState,
|
|
999
1522
|
includeChildren,
|
|
1000
1523
|
childrenDepth,
|
|
1001
1524
|
shortPath,
|
|
1002
|
-
simplifyHooks
|
|
1525
|
+
simplifyHooks,
|
|
1526
|
+
device
|
|
1003
1527
|
});
|
|
1004
1528
|
if (!result.success) {
|
|
1005
1529
|
return {
|
|
@@ -1023,7 +1547,7 @@ registerToolWithTelemetry("inspect_component", {
|
|
|
1023
1547
|
});
|
|
1024
1548
|
// Tool: Find components matching a pattern
|
|
1025
1549
|
registerToolWithTelemetry("find_components", {
|
|
1026
|
-
description: "Find components matching a name pattern. **TARGETED SEARCH**: Use after get_component_tree(structureOnly=true) to find specific components by pattern
|
|
1550
|
+
description: "Find components matching a name pattern. **TARGETED SEARCH**: Use after get_screen_layout or get_component_tree(structureOnly=true) to find specific components by pattern. Use includeLayout=true to get padding/margin/flex styles.",
|
|
1027
1551
|
inputSchema: {
|
|
1028
1552
|
pattern: z
|
|
1029
1553
|
.string()
|
|
@@ -1044,10 +1568,11 @@ registerToolWithTelemetry("find_components", {
|
|
|
1044
1568
|
.enum(["json", "tonl"])
|
|
1045
1569
|
.optional()
|
|
1046
1570
|
.default("tonl")
|
|
1047
|
-
.describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)")
|
|
1571
|
+
.describe("Output format: 'json' or 'tonl' (default, pipe-delimited rows, ~40% smaller)"),
|
|
1572
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1048
1573
|
}
|
|
1049
|
-
}, async ({ pattern, maxResults, includeLayout, shortPath, summary, format }) => {
|
|
1050
|
-
const result = await findComponents(pattern, { maxResults, includeLayout, shortPath, summary, format });
|
|
1574
|
+
}, async ({ pattern, maxResults, includeLayout, shortPath, summary, format, device }) => {
|
|
1575
|
+
const result = await findComponents(pattern, { maxResults, includeLayout, shortPath, summary, format, device });
|
|
1051
1576
|
if (!result.success) {
|
|
1052
1577
|
return {
|
|
1053
1578
|
content: [
|
|
@@ -1071,13 +1596,18 @@ registerToolWithTelemetry("find_components", {
|
|
|
1071
1596
|
// Tool: Unified tap — tries fiber, accessibility, OCR, coordinate strategies
|
|
1072
1597
|
registerToolWithTelemetry("tap", {
|
|
1073
1598
|
description: "Tap a UI element. Automatically tries multiple strategies: fiber tree (React), accessibility tree (native), and OCR (visual). " +
|
|
1074
|
-
"Auto-detects platform (iOS/Android). For coordinates,
|
|
1599
|
+
"Auto-detects platform (iOS/Android). For coordinates, pass raw pixel values from screenshot — tap handles all coordinate conversion internally (iOS pixel-to-point, scale factor adjustments).\n\n" +
|
|
1600
|
+
"PREREQUISITE: For iOS, an iOS UI driver must be installed — AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion). Without it, accessibility and coordinate strategies will fail.\n\n" +
|
|
1601
|
+
"IMPORTANT: When both iOS and Android devices are connected, specify platform explicitly to avoid tapping on the wrong device.\n\n" +
|
|
1075
1602
|
"Examples:\n" +
|
|
1076
1603
|
"- tap(text=\"Submit\") — finds and taps element with matching text\n" +
|
|
1077
1604
|
"- tap(testID=\"login-btn\") — finds by testID\n" +
|
|
1078
1605
|
"- tap(component=\"HamburgerIcon\") — finds by React component name\n" +
|
|
1079
|
-
"- tap(x=300, y=600) — taps at pixel coordinates from
|
|
1080
|
-
"- tap(text=\"Menu\", strategy=\"ocr\") — forces OCR strategy only"
|
|
1606
|
+
"- tap(x=300, y=600) — taps at pixel coordinates; use coordinates from the pressable elements list returned by ios_screenshot/android_screenshot for reliable icon and ambiguous-element taps\n" +
|
|
1607
|
+
"- tap(text=\"Menu\", strategy=\"ocr\") — forces OCR strategy only\n" +
|
|
1608
|
+
"- tap(x=300, y=600, native=true, platform=\"android\") — taps directly via ADB/simctl without React Native connection\n\n" +
|
|
1609
|
+
"Returns a post-tap screenshot by default (set screenshot=false to disable). " +
|
|
1610
|
+
"For coordinate/accessibility/OCR taps, automatically verifies the tap had a visual effect via before/after screenshot diff (set verify=false to disable).",
|
|
1081
1611
|
inputSchema: {
|
|
1082
1612
|
text: z
|
|
1083
1613
|
.string()
|
|
@@ -1113,6 +1643,35 @@ registerToolWithTelemetry("tap", {
|
|
|
1113
1643
|
.optional()
|
|
1114
1644
|
.describe("Max parent levels to traverse when searching by component name (default: 15). " +
|
|
1115
1645
|
"Increase if your component is deeply wrapped (e.g. inside multiple HOCs/animation wrappers)."),
|
|
1646
|
+
native: z
|
|
1647
|
+
.boolean()
|
|
1648
|
+
.optional()
|
|
1649
|
+
.default(false)
|
|
1650
|
+
.describe("When true, tap coordinates directly via ADB/simctl without requiring a React Native connection. " +
|
|
1651
|
+
"Useful for interacting with native UI, system dialogs, or non-RN apps. Requires x/y coordinates."),
|
|
1652
|
+
platform: z
|
|
1653
|
+
.enum(["ios", "android"])
|
|
1654
|
+
.optional()
|
|
1655
|
+
.describe("Target platform. Required when both iOS and Android devices are connected. Auto-detected if only one platform is available."),
|
|
1656
|
+
screenshot: z
|
|
1657
|
+
.boolean()
|
|
1658
|
+
.optional()
|
|
1659
|
+
.default(true)
|
|
1660
|
+
.describe("Capture and return a post-tap screenshot. Enabled by default. Set to false for fastest execution (no screenshots)."),
|
|
1661
|
+
verify: z
|
|
1662
|
+
.boolean()
|
|
1663
|
+
.optional()
|
|
1664
|
+
.describe("Run before/after screenshot diff to detect if the tap had a meaningful visual effect. " +
|
|
1665
|
+
"Default: true for coordinate/accessibility/ocr strategies, false for fiber. Requires screenshot=true."),
|
|
1666
|
+
burst: z
|
|
1667
|
+
.boolean()
|
|
1668
|
+
.optional()
|
|
1669
|
+
.default(false)
|
|
1670
|
+
.describe("Enable burst screenshot capture for enhanced verification. " +
|
|
1671
|
+
"Captures 4 rapid screenshots (~150ms intervals) after the tap to detect transient visual feedback " +
|
|
1672
|
+
"(press animations, highlights, ripples) that may settle before a standard after-screenshot. " +
|
|
1673
|
+
"Results are stored in the image buffer (use get_images to inspect individual frames). " +
|
|
1674
|
+
"Default: false."),
|
|
1116
1675
|
},
|
|
1117
1676
|
}, async (args) => {
|
|
1118
1677
|
const result = await tap({
|
|
@@ -1124,19 +1683,51 @@ registerToolWithTelemetry("tap", {
|
|
|
1124
1683
|
y: args.y,
|
|
1125
1684
|
strategy: args.strategy,
|
|
1126
1685
|
maxTraversalDepth: args.maxTraversalDepth,
|
|
1686
|
+
native: args.native,
|
|
1687
|
+
platform: args.platform,
|
|
1688
|
+
screenshot: args.screenshot,
|
|
1689
|
+
verify: args.verify,
|
|
1690
|
+
burst: args.burst,
|
|
1127
1691
|
});
|
|
1128
|
-
const
|
|
1692
|
+
const { screenshot: screenshotData, ...resultWithoutScreenshot } = result;
|
|
1693
|
+
const text = JSON.stringify(resultWithoutScreenshot, null, 2);
|
|
1694
|
+
// Pack strategy mode + attempted strategies into errorContext for telemetry
|
|
1695
|
+
// e.g. "s=ocr|fiber:no_pressable|ocr:no_match" or "s=auto|fiber:no_pressable|accessibility:not_found|ocr:no_match"
|
|
1696
|
+
const stratPrefix = args.strategy && args.strategy !== "auto" ? `s=${args.strategy}|` : "";
|
|
1697
|
+
const errorContext = result.attempted?.length
|
|
1698
|
+
? stratPrefix + result.attempted.map(a => `${a.strategy}:${a.reason.slice(0, 40)}`).join("|")
|
|
1699
|
+
: undefined;
|
|
1700
|
+
const content = [
|
|
1701
|
+
{ type: "text", text },
|
|
1702
|
+
];
|
|
1703
|
+
if (screenshotData) {
|
|
1704
|
+
content.push({
|
|
1705
|
+
type: "image",
|
|
1706
|
+
data: screenshotData.image,
|
|
1707
|
+
mimeType: "image/jpeg",
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1129
1710
|
return {
|
|
1130
|
-
content
|
|
1711
|
+
content,
|
|
1131
1712
|
isError: !result.success,
|
|
1713
|
+
_errorMessage: !result.success
|
|
1714
|
+
? `${JSON.stringify(result.query)}|${result.error || ""}`
|
|
1715
|
+
: undefined,
|
|
1716
|
+
_errorContext: errorContext,
|
|
1717
|
+
_meaningful: result.verification?.meaningful,
|
|
1718
|
+
_changeRate: result.verification?.changeRate,
|
|
1719
|
+
_tapStrategy: result.method,
|
|
1720
|
+
_iosDriver: result.platform === "ios" ? (process.env.IOS_DRIVER?.toLowerCase() || "idb") : undefined,
|
|
1132
1721
|
};
|
|
1133
1722
|
});
|
|
1134
1723
|
// Tool: Toggle Element Inspector programmatically
|
|
1135
1724
|
registerToolWithTelemetry("toggle_element_inspector", {
|
|
1136
1725
|
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.",
|
|
1137
|
-
inputSchema: {
|
|
1138
|
-
|
|
1139
|
-
|
|
1726
|
+
inputSchema: {
|
|
1727
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1728
|
+
}
|
|
1729
|
+
}, async ({ device }) => {
|
|
1730
|
+
const result = await toggleElementInspector(device);
|
|
1140
1731
|
if (!result.success) {
|
|
1141
1732
|
return {
|
|
1142
1733
|
content: [
|
|
@@ -1192,21 +1783,22 @@ registerToolWithTelemetry("get_inspector_selection", {
|
|
|
1192
1783
|
y: z
|
|
1193
1784
|
.number()
|
|
1194
1785
|
.optional()
|
|
1195
|
-
.describe("Y coordinate (in points). If provided with x, auto-taps at this location.")
|
|
1786
|
+
.describe("Y coordinate (in points). If provided with x, auto-taps at this location."),
|
|
1787
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1196
1788
|
}
|
|
1197
|
-
}, async ({ x, y }) => {
|
|
1789
|
+
}, async ({ x, y, device }) => {
|
|
1198
1790
|
// If coordinates provided, do the full flow: enable inspector -> tap -> read
|
|
1199
1791
|
if (x !== undefined && y !== undefined) {
|
|
1200
1792
|
// Check if inspector is active
|
|
1201
|
-
const inspectorActive = await isInspectorActive();
|
|
1793
|
+
const inspectorActive = await isInspectorActive(device);
|
|
1202
1794
|
// Enable inspector if not active
|
|
1203
1795
|
if (!inspectorActive) {
|
|
1204
|
-
await toggleElementInspector();
|
|
1796
|
+
await toggleElementInspector(device);
|
|
1205
1797
|
// Wait for inspector to initialize
|
|
1206
1798
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1207
1799
|
}
|
|
1208
1800
|
// Detect platform from connected app
|
|
1209
|
-
const app = getFirstConnectedApp();
|
|
1801
|
+
const app = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
|
|
1210
1802
|
if (!app) {
|
|
1211
1803
|
return {
|
|
1212
1804
|
content: [{ type: "text", text: "No app connected. Run scan_metro first." }],
|
|
@@ -1241,7 +1833,7 @@ registerToolWithTelemetry("get_inspector_selection", {
|
|
|
1241
1833
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1242
1834
|
}
|
|
1243
1835
|
// Read the current selection
|
|
1244
|
-
const result = await getInspectorSelection();
|
|
1836
|
+
const result = await getInspectorSelection(device);
|
|
1245
1837
|
if (!result.success) {
|
|
1246
1838
|
return {
|
|
1247
1839
|
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
@@ -1295,10 +1887,11 @@ registerToolWithTelemetry("inspect_at_point", {
|
|
|
1295
1887
|
.boolean()
|
|
1296
1888
|
.optional()
|
|
1297
1889
|
.default(true)
|
|
1298
|
-
.describe("Include position/dimensions (frame) in the output (default: true)")
|
|
1890
|
+
.describe("Include position/dimensions (frame) in the output (default: true)"),
|
|
1891
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
1299
1892
|
}
|
|
1300
|
-
}, async ({ x, y, includeProps, includeFrame }) => {
|
|
1301
|
-
const result = await inspectAtPoint(x, y, { includeProps, includeFrame });
|
|
1893
|
+
}, async ({ x, y, includeProps, includeFrame, device }) => {
|
|
1894
|
+
const result = await inspectAtPoint(x, y, { includeProps, includeFrame, device });
|
|
1302
1895
|
if (!result.success) {
|
|
1303
1896
|
return {
|
|
1304
1897
|
content: [
|
|
@@ -1343,7 +1936,7 @@ registerToolWithTelemetry("inspect_at_point", {
|
|
|
1343
1936
|
});
|
|
1344
1937
|
// Tool: Get network requests
|
|
1345
1938
|
registerToolWithTelemetry("get_network_requests", {
|
|
1346
|
-
description: "Retrieve captured network requests from connected React Native app. Shows URL, method, status, and timing.
|
|
1939
|
+
description: "Retrieve captured network requests from connected React Native app. Shows URL, method, status, and timing. Note: On Bridgeless targets (Expo SDK 52+) without the SDK, capture may miss early startup requests. Install react-native-ai-devtools-sdk for full capture with headers and response bodies. Tip: Use summary=true first for stats overview.",
|
|
1347
1940
|
inputSchema: {
|
|
1348
1941
|
maxRequests: z
|
|
1349
1942
|
.number()
|
|
@@ -1362,16 +1955,67 @@ registerToolWithTelemetry("get_network_requests", {
|
|
|
1362
1955
|
.boolean()
|
|
1363
1956
|
.optional()
|
|
1364
1957
|
.default(false)
|
|
1365
|
-
.describe("Return statistics only (count, methods, domains, status codes). Use for quick overview.")
|
|
1958
|
+
.describe("Return statistics only (count, methods, domains, status codes). Use for quick overview."),
|
|
1959
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
1960
|
+
}
|
|
1961
|
+
}, async ({ maxRequests, method, urlPattern, status, format, summary, device }) => {
|
|
1962
|
+
// Check if SDK is installed — prefer SDK data over CDP/interceptor buffer
|
|
1963
|
+
const sdkAvailable = await isSDKInstalled();
|
|
1964
|
+
if (sdkAvailable) {
|
|
1965
|
+
if (summary) {
|
|
1966
|
+
const sdkStats = await getSDKNetworkStats();
|
|
1967
|
+
if (sdkStats.success) {
|
|
1968
|
+
const s = sdkStats.data;
|
|
1969
|
+
const lines = [];
|
|
1970
|
+
lines.push(`Total requests: ${s.total}`);
|
|
1971
|
+
lines.push(`Completed: ${s.completed}`);
|
|
1972
|
+
lines.push(`Errors: ${s.errors}`);
|
|
1973
|
+
if (s.avgDuration != null)
|
|
1974
|
+
lines.push(`Avg duration: ${s.avgDuration}ms`);
|
|
1975
|
+
if (s.byMethod && Object.keys(s.byMethod).length > 0) {
|
|
1976
|
+
lines.push("\nBy Method:");
|
|
1977
|
+
for (const [m, c] of Object.entries(s.byMethod))
|
|
1978
|
+
lines.push(` ${m}: ${c}`);
|
|
1979
|
+
}
|
|
1980
|
+
if (s.byStatus && Object.keys(s.byStatus).length > 0) {
|
|
1981
|
+
lines.push("\nBy Status:");
|
|
1982
|
+
for (const [st, c] of Object.entries(s.byStatus))
|
|
1983
|
+
lines.push(` ${st}: ${c}`);
|
|
1984
|
+
}
|
|
1985
|
+
if (s.byDomain && Object.keys(s.byDomain).length > 0) {
|
|
1986
|
+
lines.push("\nBy Domain:");
|
|
1987
|
+
for (const [d, c] of Object.entries(s.byDomain).sort((a, b) => b[1] - a[1]).slice(0, 10))
|
|
1988
|
+
lines.push(` ${d}: ${c}`);
|
|
1989
|
+
}
|
|
1990
|
+
return { content: [{ type: "text", text: `Network Summary (SDK):\n\n${lines.join("\n")}` }] };
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const sdkResult = await querySDKNetwork({ count: maxRequests, method, urlPattern, status });
|
|
1994
|
+
if (sdkResult.success) {
|
|
1995
|
+
const entries = sdkResult.data;
|
|
1996
|
+
if (entries.length === 0) {
|
|
1997
|
+
return { content: [{ type: "text", text: "No network requests captured yet." }] };
|
|
1998
|
+
}
|
|
1999
|
+
const lines = entries.map((r) => {
|
|
2000
|
+
const time = new Date(r.timestamp).toLocaleTimeString();
|
|
2001
|
+
const st = r.status ?? "pending";
|
|
2002
|
+
const dur = r.duration != null ? `${r.duration}ms` : "-";
|
|
2003
|
+
return `[${r.id}] ${time} ${r.method} ${st} ${dur} ${r.url}`;
|
|
2004
|
+
});
|
|
2005
|
+
return { content: [{ type: "text", text: `Network Requests (${entries.length} entries, SDK):\n\n${lines.join("\n")}` }] };
|
|
2006
|
+
}
|
|
1366
2007
|
}
|
|
1367
|
-
|
|
2008
|
+
// Fallback: read from in-process buffer (CDP/interceptor)
|
|
1368
2009
|
// Return summary if requested
|
|
1369
2010
|
if (summary) {
|
|
1370
|
-
const stats = getNetworkStats(
|
|
2011
|
+
const stats = getNetworkStats(resolveNetworkBuffer(device));
|
|
1371
2012
|
let connectionWarning = "";
|
|
1372
|
-
if (
|
|
1373
|
-
const connStatus = await checkAndEnsureConnection();
|
|
2013
|
+
if (resolveNetworkBuffer(device).size === 0) {
|
|
2014
|
+
const connStatus = await checkAndEnsureConnection(device);
|
|
1374
2015
|
connectionWarning = connStatus.message ? `\n\n${connStatus.message}` : "";
|
|
2016
|
+
if (!sdkAvailable) {
|
|
2017
|
+
connectionWarning += "\n\n[TIP] For full network capture including startup requests and response bodies, install the SDK: npm install react-native-ai-devtools-sdk";
|
|
2018
|
+
}
|
|
1375
2019
|
}
|
|
1376
2020
|
return {
|
|
1377
2021
|
content: [
|
|
@@ -1382,7 +2026,7 @@ registerToolWithTelemetry("get_network_requests", {
|
|
|
1382
2026
|
]
|
|
1383
2027
|
};
|
|
1384
2028
|
}
|
|
1385
|
-
const { requests, count, formatted } = getNetworkRequests(
|
|
2029
|
+
const { requests, count, formatted } = getNetworkRequests(resolveNetworkBuffer(device), {
|
|
1386
2030
|
maxRequests,
|
|
1387
2031
|
method,
|
|
1388
2032
|
urlPattern,
|
|
@@ -1391,8 +2035,11 @@ registerToolWithTelemetry("get_network_requests", {
|
|
|
1391
2035
|
// Check connection health
|
|
1392
2036
|
let connectionWarning = "";
|
|
1393
2037
|
if (count === 0) {
|
|
1394
|
-
const connStatus = await checkAndEnsureConnection();
|
|
2038
|
+
const connStatus = await checkAndEnsureConnection(device);
|
|
1395
2039
|
connectionWarning = connStatus.message ? `\n\n${connStatus.message}` : "";
|
|
2040
|
+
if (!sdkAvailable) {
|
|
2041
|
+
connectionWarning += "\n\n[TIP] For full network capture including startup requests and response bodies, install the SDK: npm install react-native-ai-devtools-sdk";
|
|
2042
|
+
}
|
|
1396
2043
|
}
|
|
1397
2044
|
else {
|
|
1398
2045
|
const passive = getPassiveConnectionStatus();
|
|
@@ -1435,7 +2082,10 @@ registerToolWithTelemetry("get_network_requests", {
|
|
|
1435
2082
|
}
|
|
1436
2083
|
]
|
|
1437
2084
|
};
|
|
1438
|
-
}
|
|
2085
|
+
},
|
|
2086
|
+
// Empty result detector: buffer has no entries at all
|
|
2087
|
+
() => { let total = 0; for (const b of networkBuffers.values())
|
|
2088
|
+
total += b.size; return total === 0; });
|
|
1439
2089
|
// Tool: Search network requests
|
|
1440
2090
|
registerToolWithTelemetry("search_network", {
|
|
1441
2091
|
description: "Search network requests by URL pattern (case-insensitive)",
|
|
@@ -1446,14 +2096,36 @@ registerToolWithTelemetry("search_network", {
|
|
|
1446
2096
|
.enum(["text", "tonl"])
|
|
1447
2097
|
.optional()
|
|
1448
2098
|
.default("tonl")
|
|
1449
|
-
.describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)")
|
|
2099
|
+
.describe("Output format: 'text' or 'tonl' (default, compact token-optimized format)"),
|
|
2100
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
2101
|
+
}
|
|
2102
|
+
}, async ({ urlPattern, maxResults, format, device }) => {
|
|
2103
|
+
// Check if SDK is installed — prefer SDK data
|
|
2104
|
+
const sdkAvailable = await isSDKInstalled();
|
|
2105
|
+
if (sdkAvailable) {
|
|
2106
|
+
const sdkResult = await querySDKNetwork({ count: maxResults, urlPattern });
|
|
2107
|
+
if (sdkResult.success) {
|
|
2108
|
+
const entries = sdkResult.data;
|
|
2109
|
+
if (entries.length === 0) {
|
|
2110
|
+
return { content: [{ type: "text", text: `No network requests matching "${urlPattern}" found.` }] };
|
|
2111
|
+
}
|
|
2112
|
+
const lines = entries.map((r) => {
|
|
2113
|
+
const time = new Date(r.timestamp).toLocaleTimeString();
|
|
2114
|
+
const st = r.status ?? "pending";
|
|
2115
|
+
const dur = r.duration != null ? `${r.duration}ms` : "-";
|
|
2116
|
+
return `[${r.id}] ${time} ${r.method} ${st} ${dur} ${r.url}`;
|
|
2117
|
+
});
|
|
2118
|
+
if (format === "tonl") {
|
|
2119
|
+
return { content: [{ type: "text", text: `Network search results for "${urlPattern}" (${entries.length} matches, SDK):\n\n${lines.join("\n")}` }] };
|
|
2120
|
+
}
|
|
2121
|
+
return { content: [{ type: "text", text: `Network search results for "${urlPattern}" (${entries.length} matches, SDK):\n\n${lines.join("\n")}` }] };
|
|
2122
|
+
}
|
|
1450
2123
|
}
|
|
1451
|
-
|
|
1452
|
-
const { requests, count, formatted } = searchNetworkRequests(networkBuffer, urlPattern, maxResults);
|
|
2124
|
+
const { requests, count, formatted } = searchNetworkRequests(resolveNetworkBuffer(device), urlPattern, maxResults);
|
|
1453
2125
|
// Check connection health
|
|
1454
2126
|
let connectionWarning = "";
|
|
1455
2127
|
if (count === 0) {
|
|
1456
|
-
const status = await checkAndEnsureConnection();
|
|
2128
|
+
const status = await checkAndEnsureConnection(device);
|
|
1457
2129
|
connectionWarning = status.message ? `\n\n${status.message}` : "";
|
|
1458
2130
|
}
|
|
1459
2131
|
else {
|
|
@@ -1485,7 +2157,7 @@ registerToolWithTelemetry("search_network", {
|
|
|
1485
2157
|
});
|
|
1486
2158
|
// Tool: Get request details
|
|
1487
2159
|
registerToolWithTelemetry("get_request_details", {
|
|
1488
|
-
description: "Get full details of a specific network request including headers, body, and timing. Use get_network_requests first to find the request ID.",
|
|
2160
|
+
description: "Get full details of a specific network request including headers, body, and timing. With the SDK installed, includes full request/response bodies. Without SDK, bodies are not available on most targets. Use get_network_requests first to find the request ID.",
|
|
1489
2161
|
inputSchema: {
|
|
1490
2162
|
requestId: z.string().describe("The request ID to get details for"),
|
|
1491
2163
|
maxBodyLength: z.coerce
|
|
@@ -1497,12 +2169,60 @@ registerToolWithTelemetry("get_request_details", {
|
|
|
1497
2169
|
.boolean()
|
|
1498
2170
|
.optional()
|
|
1499
2171
|
.default(false)
|
|
1500
|
-
.describe("Disable body truncation. Tip: Use when you need to inspect full JSON payloads.")
|
|
2172
|
+
.describe("Disable body truncation. Tip: Use when you need to inspect full JSON payloads."),
|
|
2173
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
2174
|
+
}
|
|
2175
|
+
}, async ({ requestId, maxBodyLength, verbose, device }) => {
|
|
2176
|
+
// Check SDK first — it has full headers and body
|
|
2177
|
+
const sdkAvailable = await isSDKInstalled();
|
|
2178
|
+
if (sdkAvailable) {
|
|
2179
|
+
const sdkResult = await getSDKNetworkEntry(requestId);
|
|
2180
|
+
if (sdkResult.success && sdkResult.data) {
|
|
2181
|
+
const r = sdkResult.data;
|
|
2182
|
+
const lines = [];
|
|
2183
|
+
lines.push(`=== ${r.method} ${r.url} ===`);
|
|
2184
|
+
lines.push(`Request ID: ${r.id}`);
|
|
2185
|
+
lines.push(`Time: ${new Date(r.timestamp).toISOString()}`);
|
|
2186
|
+
lines.push(`Status: ${r.status ?? "pending"} ${r.statusText ?? ""}`);
|
|
2187
|
+
if (r.duration != null)
|
|
2188
|
+
lines.push(`Duration: ${r.duration}ms`);
|
|
2189
|
+
if (r.mimeType)
|
|
2190
|
+
lines.push(`Content-Type: ${r.mimeType}`);
|
|
2191
|
+
if (r.error)
|
|
2192
|
+
lines.push(`Error: ${r.error}`);
|
|
2193
|
+
if (r.requestHeaders && Object.keys(r.requestHeaders).length > 0) {
|
|
2194
|
+
lines.push("\n--- Request Headers ---");
|
|
2195
|
+
for (const [k, v] of Object.entries(r.requestHeaders))
|
|
2196
|
+
lines.push(`${k}: ${v}`);
|
|
2197
|
+
}
|
|
2198
|
+
if (r.requestBody) {
|
|
2199
|
+
lines.push("\n--- Request Body ---");
|
|
2200
|
+
let body = r.requestBody;
|
|
2201
|
+
if (!verbose && maxBodyLength > 0 && body.length > maxBodyLength) {
|
|
2202
|
+
body = body.slice(0, maxBodyLength) + `... [truncated: ${r.requestBody.length} chars]`;
|
|
2203
|
+
}
|
|
2204
|
+
lines.push(body);
|
|
2205
|
+
}
|
|
2206
|
+
if (r.responseHeaders && Object.keys(r.responseHeaders).length > 0) {
|
|
2207
|
+
lines.push("\n--- Response Headers ---");
|
|
2208
|
+
for (const [k, v] of Object.entries(r.responseHeaders))
|
|
2209
|
+
lines.push(`${k}: ${v}`);
|
|
2210
|
+
}
|
|
2211
|
+
if (r.responseBody) {
|
|
2212
|
+
lines.push("\n--- Response Body ---");
|
|
2213
|
+
let body = r.responseBody;
|
|
2214
|
+
if (!verbose && maxBodyLength > 0 && body.length > maxBodyLength) {
|
|
2215
|
+
body = body.slice(0, maxBodyLength) + `... [truncated: ${r.responseBody.length} chars]`;
|
|
2216
|
+
}
|
|
2217
|
+
lines.push(body);
|
|
2218
|
+
}
|
|
2219
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2220
|
+
}
|
|
1501
2221
|
}
|
|
1502
|
-
|
|
1503
|
-
const request =
|
|
2222
|
+
// Fallback: read from in-process buffer
|
|
2223
|
+
const request = resolveNetworkBuffer(device).get(requestId);
|
|
1504
2224
|
if (!request) {
|
|
1505
|
-
const status = await checkAndEnsureConnection();
|
|
2225
|
+
const status = await checkAndEnsureConnection(device);
|
|
1506
2226
|
const connectionNote = status.message ? `\n\n${status.message}` : "";
|
|
1507
2227
|
return {
|
|
1508
2228
|
content: [
|
|
@@ -1526,13 +2246,45 @@ registerToolWithTelemetry("get_request_details", {
|
|
|
1526
2246
|
// Tool: Get network stats
|
|
1527
2247
|
registerToolWithTelemetry("get_network_stats", {
|
|
1528
2248
|
description: "Get statistics about captured network requests: counts by method, status code, and domain.",
|
|
1529
|
-
inputSchema: {
|
|
1530
|
-
|
|
1531
|
-
|
|
2249
|
+
inputSchema: {
|
|
2250
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for all devices. Run get_apps to see connected devices.")
|
|
2251
|
+
}
|
|
2252
|
+
}, async ({ device }) => {
|
|
2253
|
+
// Check if SDK is installed — prefer SDK data
|
|
2254
|
+
const sdkAvailable = await isSDKInstalled();
|
|
2255
|
+
if (sdkAvailable) {
|
|
2256
|
+
const sdkStats = await getSDKNetworkStats();
|
|
2257
|
+
if (sdkStats.success) {
|
|
2258
|
+
const s = sdkStats.data;
|
|
2259
|
+
const lines = [];
|
|
2260
|
+
lines.push(`Total requests: ${s.total}`);
|
|
2261
|
+
lines.push(`Completed: ${s.completed}`);
|
|
2262
|
+
lines.push(`Errors: ${s.errors}`);
|
|
2263
|
+
if (s.avgDuration != null)
|
|
2264
|
+
lines.push(`Avg duration: ${s.avgDuration}ms`);
|
|
2265
|
+
if (s.byMethod && Object.keys(s.byMethod).length > 0) {
|
|
2266
|
+
lines.push("\nBy Method:");
|
|
2267
|
+
for (const [m, c] of Object.entries(s.byMethod))
|
|
2268
|
+
lines.push(` ${m}: ${c}`);
|
|
2269
|
+
}
|
|
2270
|
+
if (s.byStatus && Object.keys(s.byStatus).length > 0) {
|
|
2271
|
+
lines.push("\nBy Status:");
|
|
2272
|
+
for (const [st, c] of Object.entries(s.byStatus))
|
|
2273
|
+
lines.push(` ${st}: ${c}`);
|
|
2274
|
+
}
|
|
2275
|
+
if (s.byDomain && Object.keys(s.byDomain).length > 0) {
|
|
2276
|
+
lines.push("\nBy Domain:");
|
|
2277
|
+
for (const [d, c] of Object.entries(s.byDomain).sort((a, b) => b[1] - a[1]).slice(0, 10))
|
|
2278
|
+
lines.push(` ${d}: ${c}`);
|
|
2279
|
+
}
|
|
2280
|
+
return { content: [{ type: "text", text: `Network Statistics (SDK):\n\n${lines.join("\n")}` }] };
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
const stats = getNetworkStats(resolveNetworkBuffer(device));
|
|
1532
2284
|
// Check connection health
|
|
1533
2285
|
let connectionWarning = "";
|
|
1534
|
-
if (
|
|
1535
|
-
const status = await checkAndEnsureConnection();
|
|
2286
|
+
if (resolveNetworkBuffer(device).size === 0) {
|
|
2287
|
+
const status = await checkAndEnsureConnection(device);
|
|
1536
2288
|
connectionWarning = status.message ? `\n\n${status.message}` : "";
|
|
1537
2289
|
}
|
|
1538
2290
|
else {
|
|
@@ -1549,37 +2301,126 @@ registerToolWithTelemetry("get_network_stats", {
|
|
|
1549
2301
|
}
|
|
1550
2302
|
]
|
|
1551
2303
|
};
|
|
1552
|
-
}
|
|
2304
|
+
},
|
|
2305
|
+
// Empty result detector: buffer has no entries at all
|
|
2306
|
+
() => { let total = 0; for (const b of networkBuffers.values())
|
|
2307
|
+
total += b.size; return total === 0; });
|
|
1553
2308
|
// Tool: Clear network requests
|
|
1554
2309
|
registerToolWithTelemetry("clear_network", {
|
|
1555
2310
|
description: "Clear the network request buffer",
|
|
1556
|
-
inputSchema: {
|
|
1557
|
-
|
|
1558
|
-
|
|
2311
|
+
inputSchema: {
|
|
2312
|
+
device: z.string().optional().describe("Target device name (substring match). Omit to clear all devices. Run get_apps to see connected devices.")
|
|
2313
|
+
}
|
|
2314
|
+
}, async ({ device }) => {
|
|
2315
|
+
let totalCleared = 0;
|
|
2316
|
+
if (device) {
|
|
2317
|
+
const app = getConnectedAppByDevice(device);
|
|
2318
|
+
if (!app)
|
|
2319
|
+
throw new Error(`No connected device matches "${device}"`);
|
|
2320
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
2321
|
+
totalCleared = getNetworkBuffer(deviceName).clear();
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
for (const buffer of networkBuffers.values()) {
|
|
2325
|
+
totalCleared += buffer.clear();
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// Also clear SDK buffer if available
|
|
2329
|
+
const sdkAvailable = await isSDKInstalled();
|
|
2330
|
+
if (sdkAvailable) {
|
|
2331
|
+
const sdkResult = await clearSDKNetwork();
|
|
2332
|
+
if (sdkResult.success && sdkResult.count) {
|
|
2333
|
+
totalCleared += sdkResult.count;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
1559
2336
|
return {
|
|
1560
2337
|
content: [
|
|
1561
2338
|
{
|
|
1562
2339
|
type: "text",
|
|
1563
|
-
text: `Cleared ${
|
|
2340
|
+
text: `Cleared ${totalCleared} network requests from buffer.`
|
|
1564
2341
|
}
|
|
1565
2342
|
]
|
|
1566
2343
|
};
|
|
1567
2344
|
});
|
|
1568
|
-
// Tool:
|
|
1569
|
-
registerToolWithTelemetry("
|
|
1570
|
-
description: "
|
|
1571
|
-
inputSchema: {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2345
|
+
// Tool: Get images from shared image buffer
|
|
2346
|
+
registerToolWithTelemetry("get_images", {
|
|
2347
|
+
description: "Access the shared image buffer containing screenshots from all tools (ios_screenshot, android_screenshot, ocr_screenshot, tap verification). Returns metadata only by default — use id or groupId+frameIndex to retrieve actual image data. Tap burst verification stores frame groups here when burst=true is used.",
|
|
2348
|
+
inputSchema: {
|
|
2349
|
+
list: z.boolean().optional().describe("List all entries and groups (metadata only, no image data)"),
|
|
2350
|
+
id: z.string().optional().describe("Retrieve a specific image by ID (returns image data)"),
|
|
2351
|
+
groupId: z.string().optional().describe("List frames in a group (metadata only), or combine with frameIndex to retrieve a specific frame"),
|
|
2352
|
+
frameIndex: z.coerce.number().optional().describe("Retrieve a specific frame from a group (requires groupId)"),
|
|
2353
|
+
last: z.coerce.number().optional().describe("Return the N most recent entries (metadata only)"),
|
|
2354
|
+
source: z.string().optional().describe("Filter entries by source"),
|
|
2355
|
+
clear: z.boolean().optional().describe("Clear the buffer")
|
|
2356
|
+
}
|
|
2357
|
+
}, async ({ id, groupId, frameIndex, last, source, clear }) => {
|
|
2358
|
+
if (clear) {
|
|
2359
|
+
const count = imageBuffer.clear();
|
|
1575
2360
|
return {
|
|
1576
|
-
content: [
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2361
|
+
content: [{ type: "text", text: `Cleared ${count} images from buffer.` }]
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
if (id) {
|
|
2365
|
+
const entry = imageBuffer.getById(id);
|
|
2366
|
+
if (!entry) {
|
|
2367
|
+
return {
|
|
2368
|
+
content: [{ type: "text", text: `No image found with id "${id}".` }],
|
|
2369
|
+
isError: true
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
const { image, ...meta } = entry;
|
|
2373
|
+
return {
|
|
2374
|
+
content: [
|
|
2375
|
+
{ type: "text", text: JSON.stringify(meta, null, 2) },
|
|
2376
|
+
{ type: "image", data: image.toString("base64"), mimeType: "image/png" }
|
|
2377
|
+
]
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
if (groupId !== undefined && frameIndex !== undefined) {
|
|
2381
|
+
const entry = imageBuffer.getByGroupFrame(groupId, frameIndex);
|
|
2382
|
+
if (!entry) {
|
|
2383
|
+
return {
|
|
2384
|
+
content: [{ type: "text", text: `No frame ${frameIndex} found in group "${groupId}".` }],
|
|
2385
|
+
isError: true
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
const { image, ...meta } = entry;
|
|
2389
|
+
return {
|
|
2390
|
+
content: [
|
|
2391
|
+
{ type: "text", text: JSON.stringify(meta, null, 2) },
|
|
2392
|
+
{ type: "image", data: image.toString("base64"), mimeType: "image/png" }
|
|
2393
|
+
]
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
const entries = imageBuffer.listEntries({ source, groupId, last });
|
|
2397
|
+
const groups = imageBuffer.listGroups();
|
|
2398
|
+
return {
|
|
2399
|
+
content: [
|
|
2400
|
+
{
|
|
2401
|
+
type: "text",
|
|
2402
|
+
text: JSON.stringify({ entries, groups, total: imageBuffer.size }, null, 2)
|
|
2403
|
+
}
|
|
2404
|
+
]
|
|
2405
|
+
};
|
|
2406
|
+
});
|
|
2407
|
+
// Tool: Reload the app
|
|
2408
|
+
registerToolWithTelemetry("reload_app", {
|
|
2409
|
+
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.).",
|
|
2410
|
+
inputSchema: {
|
|
2411
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
2412
|
+
}
|
|
2413
|
+
}, async ({ device }) => {
|
|
2414
|
+
const result = await reloadApp(device);
|
|
2415
|
+
if (!result.success) {
|
|
2416
|
+
return {
|
|
2417
|
+
content: [
|
|
2418
|
+
{
|
|
2419
|
+
type: "text",
|
|
2420
|
+
text: `Error: ${result.error}`
|
|
2421
|
+
}
|
|
2422
|
+
],
|
|
2423
|
+
isError: true
|
|
1583
2424
|
};
|
|
1584
2425
|
}
|
|
1585
2426
|
return {
|
|
@@ -1591,6 +2432,170 @@ registerToolWithTelemetry("reload_app", {
|
|
|
1591
2432
|
]
|
|
1592
2433
|
};
|
|
1593
2434
|
});
|
|
2435
|
+
// Tool: LogBox control (replaces dismiss_logbox)
|
|
2436
|
+
registerToolWithTelemetry("logbox", {
|
|
2437
|
+
description: "Interact with React Native's LogBox overlay (dev mode only). " +
|
|
2438
|
+
'Actions: "dismiss" clears all entries and returns their content. ' +
|
|
2439
|
+
'"push" displays a message in the LogBox error banner (visible to the developer watching the device). ' +
|
|
2440
|
+
'"ignore" adds patterns to suppress future LogBox entries for this session. ' +
|
|
2441
|
+
'"detect" reads current LogBox state without modifying it. ' +
|
|
2442
|
+
"Only works in __DEV__ mode — LogBox does not exist in production builds.",
|
|
2443
|
+
inputSchema: {
|
|
2444
|
+
action: z.enum(["dismiss", "push", "ignore", "detect"]).describe('Action to perform: "dismiss", "push", "ignore", or "detect"'),
|
|
2445
|
+
message: z.string().optional().describe('Message to push into LogBox (required when action="push")'),
|
|
2446
|
+
level: z.enum(["error", "warning"]).optional().describe('LogBox level for push (default: "error"). Only "error" shows a visible bottom banner; "warning" is stored but not visually shown unless LogBox is already open'),
|
|
2447
|
+
expanded: z.boolean().optional().describe('When true, opens the full-screen LogBox detail view instead of the minimized bottom banner. Useful for important messages with clickable URLs (default: false)'),
|
|
2448
|
+
subtitle: z.string().optional().describe('Additional info shown in the call stack area when expanded=true (default: "MCP Server"). Use for context like "License Check", "Usage Limit", etc.'),
|
|
2449
|
+
target: z.enum(["logbox", "metro"]).optional().describe('Where to push the message (default: "logbox"). "logbox" shows on device screen, "metro" outputs to Metro terminal via console.log'),
|
|
2450
|
+
patterns: z.array(z.string()).optional().describe('Patterns to ignore (required when action="ignore"), e.g. ["[APOLLO]", "deprecated"]'),
|
|
2451
|
+
device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
|
|
2452
|
+
}
|
|
2453
|
+
}, async ({ action, message, level, expanded, subtitle, target, patterns, device }) => {
|
|
2454
|
+
if (action === "dismiss") {
|
|
2455
|
+
const result = await dismissLogBox(device);
|
|
2456
|
+
if (result === null) {
|
|
2457
|
+
return {
|
|
2458
|
+
content: [
|
|
2459
|
+
{
|
|
2460
|
+
type: "text",
|
|
2461
|
+
text: "LogBox not available. This can happen if: (1) the app is a production build (__DEV__ is false), (2) no React Native app is connected, or (3) the Metro module registry is not accessible."
|
|
2462
|
+
}
|
|
2463
|
+
]
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
if (result.totalDismissed === 0) {
|
|
2467
|
+
return {
|
|
2468
|
+
content: [
|
|
2469
|
+
{
|
|
2470
|
+
type: "text",
|
|
2471
|
+
text: "No LogBox entries to dismiss."
|
|
2472
|
+
}
|
|
2473
|
+
]
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
return {
|
|
2477
|
+
content: [
|
|
2478
|
+
{
|
|
2479
|
+
type: "text",
|
|
2480
|
+
text: formatDismissedEntries(result)
|
|
2481
|
+
}
|
|
2482
|
+
]
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
if (action === "push") {
|
|
2486
|
+
if (!message) {
|
|
2487
|
+
return {
|
|
2488
|
+
content: [
|
|
2489
|
+
{
|
|
2490
|
+
type: "text",
|
|
2491
|
+
text: 'Error: "message" parameter is required when action is "push".'
|
|
2492
|
+
}
|
|
2493
|
+
],
|
|
2494
|
+
isError: true
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
const success = await pushLogBox(message, level || "error", expanded || false, target || "logbox", subtitle, device);
|
|
2498
|
+
if (!success) {
|
|
2499
|
+
return {
|
|
2500
|
+
content: [
|
|
2501
|
+
{
|
|
2502
|
+
type: "text",
|
|
2503
|
+
text: "Failed to push message to LogBox. This can happen if: (1) the app is a production build, (2) no React Native app is connected, or (3) the Metro module registry is not accessible."
|
|
2504
|
+
}
|
|
2505
|
+
]
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
return {
|
|
2509
|
+
content: [
|
|
2510
|
+
{
|
|
2511
|
+
type: "text",
|
|
2512
|
+
text: target === "metro"
|
|
2513
|
+
? `Message pushed to Metro terminal: "${message}"`
|
|
2514
|
+
: `Message pushed to LogBox as ${level || "error"}: "${message}"`
|
|
2515
|
+
}
|
|
2516
|
+
]
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
if (action === "ignore") {
|
|
2520
|
+
if (!patterns || patterns.length === 0) {
|
|
2521
|
+
return {
|
|
2522
|
+
content: [
|
|
2523
|
+
{
|
|
2524
|
+
type: "text",
|
|
2525
|
+
text: 'Error: "patterns" parameter is required when action is "ignore" (non-empty array of strings).'
|
|
2526
|
+
}
|
|
2527
|
+
],
|
|
2528
|
+
isError: true
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
const activePatterns = await addLogBoxIgnorePatterns(patterns, device);
|
|
2532
|
+
if (activePatterns === null) {
|
|
2533
|
+
return {
|
|
2534
|
+
content: [
|
|
2535
|
+
{
|
|
2536
|
+
type: "text",
|
|
2537
|
+
text: "Failed to add ignore patterns. This can happen if: (1) the app is a production build, (2) no React Native app is connected, or (3) the Metro module registry is not accessible."
|
|
2538
|
+
}
|
|
2539
|
+
]
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
return {
|
|
2543
|
+
content: [
|
|
2544
|
+
{
|
|
2545
|
+
type: "text",
|
|
2546
|
+
text: `Added ignore patterns: ${patterns.map((p) => `"${p}"`).join(", ")}\n\nAll active ignore patterns: ${activePatterns.map((p) => `"${p}"`).join(", ")}`
|
|
2547
|
+
}
|
|
2548
|
+
]
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
if (action === "detect") {
|
|
2552
|
+
const state = await detectLogBox(device);
|
|
2553
|
+
if (state === null) {
|
|
2554
|
+
return {
|
|
2555
|
+
content: [
|
|
2556
|
+
{
|
|
2557
|
+
type: "text",
|
|
2558
|
+
text: "LogBox not available. This can happen if: (1) the app is a production build (__DEV__ is false), (2) no React Native app is connected, or (3) the Metro module registry is not accessible."
|
|
2559
|
+
}
|
|
2560
|
+
]
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
if (state.total === 0) {
|
|
2564
|
+
return {
|
|
2565
|
+
content: [
|
|
2566
|
+
{
|
|
2567
|
+
type: "text",
|
|
2568
|
+
text: "No LogBox entries detected."
|
|
2569
|
+
}
|
|
2570
|
+
]
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
const MAX_MSG_LENGTH = 150;
|
|
2574
|
+
let output = `LogBox state: ${state.total} entr${state.total === 1 ? "y" : "ies"} (${state.errors} error${state.errors !== 1 ? "s" : ""}, ${state.warnings} warning${state.warnings !== 1 ? "s" : ""}, ${state.fatals} fatal${state.fatals !== 1 ? "s" : ""})\n`;
|
|
2575
|
+
for (const entry of state.entries) {
|
|
2576
|
+
const truncated = entry.message.length > MAX_MSG_LENGTH ? entry.message.substring(0, MAX_MSG_LENGTH) + "..." : entry.message;
|
|
2577
|
+
const countStr = entry.count > 1 ? ` (x${entry.count})` : "";
|
|
2578
|
+
output += `\n[${entry.level}] ${truncated}${countStr}`;
|
|
2579
|
+
}
|
|
2580
|
+
return {
|
|
2581
|
+
content: [
|
|
2582
|
+
{
|
|
2583
|
+
type: "text",
|
|
2584
|
+
text: output
|
|
2585
|
+
}
|
|
2586
|
+
]
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
return {
|
|
2590
|
+
content: [
|
|
2591
|
+
{
|
|
2592
|
+
type: "text",
|
|
2593
|
+
text: `Unknown action: "${action}". Use "dismiss", "push", "ignore", or "detect".`
|
|
2594
|
+
}
|
|
2595
|
+
],
|
|
2596
|
+
isError: true
|
|
2597
|
+
};
|
|
2598
|
+
});
|
|
1594
2599
|
// ============================================================================
|
|
1595
2600
|
// Bundle/Build Error Tools
|
|
1596
2601
|
// ============================================================================
|
|
@@ -1776,7 +2781,7 @@ registerToolWithTelemetry("list_android_devices", {
|
|
|
1776
2781
|
});
|
|
1777
2782
|
// Tool: Android screenshot
|
|
1778
2783
|
registerToolWithTelemetry("android_screenshot", {
|
|
1779
|
-
description: "Take a screenshot from an Android device/emulator. Returns the image
|
|
2784
|
+
description: "Take a screenshot from an Android device/emulator. Returns the image plus two lists: (1) pressable elements — the tappable components on screen with ready-to-use pixel tap coordinates, testIDs, and labels; (2) screen layout — all visible React components for context. Prefer tap(text=\"...\") when text is exact and unique; otherwise use tap(x, y) with coordinates from the pressables list — this is the most reliable way to tap icons or visually-identified elements. Use component names from the layout for inspect_component/find_components.",
|
|
1780
2785
|
inputSchema: {
|
|
1781
2786
|
outputPath: z
|
|
1782
2787
|
.string()
|
|
@@ -1833,16 +2838,90 @@ registerToolWithTelemetry("android_screenshot", {
|
|
|
1833
2838
|
catch {
|
|
1834
2839
|
// Use defaults
|
|
1835
2840
|
}
|
|
1836
|
-
|
|
2841
|
+
// Enrich with screen layout data (component names + tap coordinates)
|
|
2842
|
+
// Android: density ratio = densityDpi / 160 (160dpi = 1dp = 1px baseline)
|
|
2843
|
+
const androidPixelRatio = densityDpi / 160;
|
|
2844
|
+
let layoutText = null;
|
|
2845
|
+
try {
|
|
2846
|
+
layoutText = await enrichScreenshotWithLayout(androidPixelRatio, result.scaleFactor || 1);
|
|
2847
|
+
}
|
|
2848
|
+
catch {
|
|
2849
|
+
// Non-fatal: screenshot works without layout enrichment
|
|
2850
|
+
}
|
|
2851
|
+
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
2852
|
+
let pressablesText = null;
|
|
2853
|
+
try {
|
|
2854
|
+
const pressables = await getPressableElements({});
|
|
2855
|
+
if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
|
|
2856
|
+
const screenshotScale = result.scaleFactor || 1;
|
|
2857
|
+
pressablesText = pressables.parsedElements.map((el) => {
|
|
2858
|
+
const px = Math.round((el.center.x * androidPixelRatio) / screenshotScale);
|
|
2859
|
+
const py = Math.round((el.center.y * androidPixelRatio) / screenshotScale);
|
|
2860
|
+
const label = el.accessibilityLabel || el.text || el.testID || el.component;
|
|
2861
|
+
const idPart = el.testID ? ` testID="${el.testID}"` : "";
|
|
2862
|
+
const kindPart = el.isInput ? " [input]" : "";
|
|
2863
|
+
const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
|
|
2864
|
+
return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
|
|
2865
|
+
}).join("\n");
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
catch {
|
|
2869
|
+
// Non-fatal: screenshot works without pressables enrichment
|
|
2870
|
+
}
|
|
2871
|
+
infoText += `\n📱 Android uses PIXELS for all coordinates`;
|
|
1837
2872
|
if (result.scaleFactor && result.scaleFactor > 1) {
|
|
1838
|
-
infoText += `\n
|
|
1839
|
-
infoText += `\n📐
|
|
2873
|
+
infoText += `\n🖼️ Image was scaled down to fit API limits (scale: ${result.scaleFactor.toFixed(3)})`;
|
|
2874
|
+
infoText += `\n📐 tap() handles coordinate conversion automatically — pass pixel coords from this image directly`;
|
|
1840
2875
|
}
|
|
1841
2876
|
else {
|
|
1842
2877
|
infoText += `\n📐 Screenshot coords = tap coords (no conversion needed)`;
|
|
1843
2878
|
}
|
|
1844
2879
|
infoText += `\n⚠️ Status bar: ${statusBarPixels}px (${statusBarDp}dp) from top - app content starts below this`;
|
|
1845
2880
|
infoText += `\n📊 Display density: ${densityDpi}dpi`;
|
|
2881
|
+
if (pressablesText) {
|
|
2882
|
+
infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
|
|
2883
|
+
infoText += `\n${pressablesText}`;
|
|
2884
|
+
}
|
|
2885
|
+
if (layoutText) {
|
|
2886
|
+
infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
|
|
2887
|
+
infoText += `\n${layoutText}`;
|
|
2888
|
+
}
|
|
2889
|
+
if (pressablesText || layoutText) {
|
|
2890
|
+
infoText += `\n\n💡 Next steps:`;
|
|
2891
|
+
infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
|
|
2892
|
+
infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
|
|
2893
|
+
infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
|
|
2894
|
+
infoText += `\n • inspect_component("ComponentName") — inspect a component from the layout`;
|
|
2895
|
+
}
|
|
2896
|
+
else {
|
|
2897
|
+
infoText += `\n\n💡 Next steps:`;
|
|
2898
|
+
infoText += `\n • tap(text="Button Label") — tap element by visible text`;
|
|
2899
|
+
infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
|
|
2900
|
+
infoText += `\n • android_describe_all — get full UI tree with exact tap coordinates`;
|
|
2901
|
+
infoText += `\n • android_find_element(text="...") — find element coordinates without tapping`;
|
|
2902
|
+
}
|
|
2903
|
+
// Check for LogBox overlay (uses default CDP device — native deviceId cannot be mapped to CDP device name)
|
|
2904
|
+
try {
|
|
2905
|
+
const logBoxState = await detectLogBox();
|
|
2906
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
2907
|
+
infoText += formatLogBoxWarning(logBoxState);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
catch {
|
|
2911
|
+
// Non-fatal: LogBox detection failure should not break screenshot
|
|
2912
|
+
}
|
|
2913
|
+
imageBuffer.add({
|
|
2914
|
+
id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2915
|
+
image: result.data,
|
|
2916
|
+
timestamp: Date.now(),
|
|
2917
|
+
source: "android_screenshot",
|
|
2918
|
+
metadata: {
|
|
2919
|
+
width: result.originalWidth || 0,
|
|
2920
|
+
height: result.originalHeight || 0,
|
|
2921
|
+
scaleFactor: result.scaleFactor || 1,
|
|
2922
|
+
platform: "android",
|
|
2923
|
+
},
|
|
2924
|
+
});
|
|
1846
2925
|
return {
|
|
1847
2926
|
content: [
|
|
1848
2927
|
{
|
|
@@ -2090,11 +3169,24 @@ server.registerTool("android_describe_all", {
|
|
|
2090
3169
|
}
|
|
2091
3170
|
}, async ({ deviceId }) => {
|
|
2092
3171
|
const result = await androidDescribeAll(deviceId);
|
|
3172
|
+
let text = result.success ? result.formatted : `Error: ${result.error}`;
|
|
3173
|
+
// Check for LogBox overlay
|
|
3174
|
+
if (result.success) {
|
|
3175
|
+
try {
|
|
3176
|
+
const logBoxState = await detectLogBox();
|
|
3177
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3178
|
+
text += formatLogBoxWarning(logBoxState);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
catch {
|
|
3182
|
+
// Non-fatal: LogBox detection failure should not break describe_all
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
2093
3185
|
return {
|
|
2094
3186
|
content: [
|
|
2095
3187
|
{
|
|
2096
3188
|
type: "text",
|
|
2097
|
-
text
|
|
3189
|
+
text
|
|
2098
3190
|
}
|
|
2099
3191
|
],
|
|
2100
3192
|
isError: !result.success
|
|
@@ -2277,7 +3369,7 @@ registerToolWithTelemetry("list_ios_simulators", {
|
|
|
2277
3369
|
});
|
|
2278
3370
|
// Tool: iOS screenshot
|
|
2279
3371
|
registerToolWithTelemetry("ios_screenshot", {
|
|
2280
|
-
description: "Take a screenshot from an iOS simulator. Returns the image
|
|
3372
|
+
description: "Take a screenshot from an iOS simulator. Returns the image plus two lists: (1) pressable elements — the tappable components on screen with ready-to-use pixel tap coordinates, testIDs, and labels; (2) screen layout — all visible React components for context. Prefer tap(text=\"...\") when text is exact and unique; otherwise use tap(x, y) with coordinates from the pressables list — this is the most reliable way to tap icons or visually-identified elements. Use component names from the layout for inspect_component/find_components.",
|
|
2281
3373
|
inputSchema: {
|
|
2282
3374
|
outputPath: z
|
|
2283
3375
|
.string()
|
|
@@ -2361,16 +3453,85 @@ registerToolWithTelemetry("ios_screenshot", {
|
|
|
2361
3453
|
pointHeight = Math.round(pixelHeight / scaleFactor);
|
|
2362
3454
|
}
|
|
2363
3455
|
const safeAreaOffsetPixels = safeAreaTop * scaleFactor;
|
|
3456
|
+
// Enrich with screen layout data (component names + tap coordinates)
|
|
3457
|
+
let layoutText = null;
|
|
3458
|
+
try {
|
|
3459
|
+
layoutText = await enrichScreenshotWithLayout(scaleFactor, result.scaleFactor || 1);
|
|
3460
|
+
}
|
|
3461
|
+
catch {
|
|
3462
|
+
// Non-fatal: screenshot works without layout enrichment
|
|
3463
|
+
}
|
|
3464
|
+
// Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
|
|
3465
|
+
let pressablesText = null;
|
|
3466
|
+
try {
|
|
3467
|
+
const pressables = await getPressableElements({});
|
|
3468
|
+
if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
|
|
3469
|
+
const screenshotScale = result.scaleFactor || 1;
|
|
3470
|
+
pressablesText = pressables.parsedElements.map((el) => {
|
|
3471
|
+
const px = Math.round((el.center.x * scaleFactor) / screenshotScale);
|
|
3472
|
+
const py = Math.round((el.center.y * scaleFactor) / screenshotScale);
|
|
3473
|
+
const label = el.accessibilityLabel || el.text || el.testID || el.component;
|
|
3474
|
+
const idPart = el.testID ? ` testID="${el.testID}"` : "";
|
|
3475
|
+
const kindPart = el.isInput ? " [input]" : "";
|
|
3476
|
+
const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
|
|
3477
|
+
return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
|
|
3478
|
+
}).join("\n");
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
catch {
|
|
3482
|
+
// Non-fatal: screenshot works without pressables enrichment
|
|
3483
|
+
}
|
|
2364
3484
|
let infoText = `Screenshot captured (${pixelWidth}x${pixelHeight} pixels)`;
|
|
2365
|
-
infoText += `\n📱 iOS
|
|
2366
|
-
infoText += `\n📐
|
|
2367
|
-
infoText += `\n tap_x = pixel_x / ${scaleFactor}`;
|
|
2368
|
-
infoText += `\n tap_y = pixel_y / ${scaleFactor}`;
|
|
3485
|
+
infoText += `\n📱 iOS screen: ${pointWidth}x${pointHeight} points (${scaleFactor}x scale)`;
|
|
3486
|
+
infoText += `\n📐 tap() handles pixel-to-point conversion automatically — pass pixel coords from this image directly`;
|
|
2369
3487
|
infoText += `\n⚠️ Status bar + safe area: ${safeAreaTop} points (${safeAreaOffsetPixels} pixels) from top`;
|
|
2370
3488
|
if (result.scaleFactor && result.scaleFactor > 1) {
|
|
2371
3489
|
infoText += `\n🖼️ Image was scaled down to fit API limits (scale: ${result.scaleFactor.toFixed(3)})`;
|
|
2372
3490
|
}
|
|
2373
|
-
|
|
3491
|
+
if (pressablesText) {
|
|
3492
|
+
infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
|
|
3493
|
+
infoText += `\n${pressablesText}`;
|
|
3494
|
+
}
|
|
3495
|
+
if (layoutText) {
|
|
3496
|
+
infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
|
|
3497
|
+
infoText += `\n${layoutText}`;
|
|
3498
|
+
}
|
|
3499
|
+
if (pressablesText || layoutText) {
|
|
3500
|
+
infoText += `\n\n💡 Next steps:`;
|
|
3501
|
+
infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
|
|
3502
|
+
infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
|
|
3503
|
+
infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
|
|
3504
|
+
infoText += `\n • inspect_component("ComponentName") — inspect a component from the layout`;
|
|
3505
|
+
}
|
|
3506
|
+
else {
|
|
3507
|
+
infoText += `\n\n💡 Next steps:`;
|
|
3508
|
+
infoText += `\n • tap(text="Button Label") — tap element by visible text`;
|
|
3509
|
+
infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
|
|
3510
|
+
infoText += `\n • ios_describe_all — get full UI tree with exact tap coordinates`;
|
|
3511
|
+
infoText += `\n • ios_find_element(label="...") — find element coordinates without tapping`;
|
|
3512
|
+
}
|
|
3513
|
+
// Check for LogBox overlay (uses default CDP device — native UDID cannot be mapped to CDP device name)
|
|
3514
|
+
try {
|
|
3515
|
+
const logBoxState = await detectLogBox();
|
|
3516
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3517
|
+
infoText += formatLogBoxWarning(logBoxState);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
catch {
|
|
3521
|
+
// Non-fatal: LogBox detection failure should not break screenshot
|
|
3522
|
+
}
|
|
3523
|
+
imageBuffer.add({
|
|
3524
|
+
id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3525
|
+
image: result.data,
|
|
3526
|
+
timestamp: Date.now(),
|
|
3527
|
+
source: "ios_screenshot",
|
|
3528
|
+
metadata: {
|
|
3529
|
+
width: result.originalWidth || 0,
|
|
3530
|
+
height: result.originalHeight || 0,
|
|
3531
|
+
scaleFactor: result.scaleFactor || 1,
|
|
3532
|
+
platform: "ios",
|
|
3533
|
+
},
|
|
3534
|
+
});
|
|
2374
3535
|
return {
|
|
2375
3536
|
content: [
|
|
2376
3537
|
{
|
|
@@ -2407,32 +3568,39 @@ registerToolWithTelemetry("ocr_screenshot", {
|
|
|
2407
3568
|
.describe("Optional device ID (Android) or UDID (iOS). Uses first available device if not specified.")
|
|
2408
3569
|
}
|
|
2409
3570
|
}, async ({ platform, deviceId }) => {
|
|
2410
|
-
// Call the HTTP endpoint for OCR (allows hot-reload without session restart)
|
|
2411
|
-
// Prefer child process port, fall back to in-process port
|
|
2412
|
-
const port = getDebugServerPort();
|
|
2413
|
-
if (!port) {
|
|
2414
|
-
return {
|
|
2415
|
-
content: [
|
|
2416
|
-
{
|
|
2417
|
-
type: "text",
|
|
2418
|
-
text: "Debug HTTP server not running"
|
|
2419
|
-
}
|
|
2420
|
-
],
|
|
2421
|
-
isError: true
|
|
2422
|
-
};
|
|
2423
|
-
}
|
|
2424
3571
|
try {
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3572
|
+
// Take screenshot
|
|
3573
|
+
const screenshotResult = platform === "android"
|
|
3574
|
+
? await androidScreenshot(undefined, deviceId)
|
|
3575
|
+
: await iosScreenshot(undefined, deviceId);
|
|
3576
|
+
if (!screenshotResult.success || !screenshotResult.data) {
|
|
3577
|
+
return {
|
|
3578
|
+
content: [
|
|
3579
|
+
{
|
|
3580
|
+
type: "text",
|
|
3581
|
+
text: `Screenshot failed: ${screenshotResult.error || "No image data"}`
|
|
3582
|
+
}
|
|
3583
|
+
],
|
|
3584
|
+
isError: true
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3587
|
+
// Calculate device pixel ratio for iOS
|
|
3588
|
+
const devicePixelRatio = platform === "ios" && screenshotResult.originalWidth && screenshotResult.originalHeight
|
|
3589
|
+
? inferIOSDevicePixelRatio(screenshotResult.originalWidth, screenshotResult.originalHeight)
|
|
3590
|
+
: 1;
|
|
3591
|
+
// Run OCR on the screenshot
|
|
3592
|
+
const scaleFactor = screenshotResult.scaleFactor || 1;
|
|
3593
|
+
const ocrResult = await recognizeText(screenshotResult.data, {
|
|
3594
|
+
scaleFactor,
|
|
3595
|
+
platform,
|
|
3596
|
+
devicePixelRatio
|
|
3597
|
+
});
|
|
2430
3598
|
if (!ocrResult.success) {
|
|
2431
3599
|
return {
|
|
2432
3600
|
content: [
|
|
2433
3601
|
{
|
|
2434
3602
|
type: "text",
|
|
2435
|
-
text: `OCR failed:
|
|
3603
|
+
text: `OCR failed: no text recognized`
|
|
2436
3604
|
}
|
|
2437
3605
|
],
|
|
2438
3606
|
isError: true
|
|
@@ -2455,8 +3623,36 @@ registerToolWithTelemetry("ocr_screenshot", {
|
|
|
2455
3623
|
confidence: Math.round(ocrResult.confidence || 0),
|
|
2456
3624
|
elementCount: elements.length,
|
|
2457
3625
|
elements,
|
|
2458
|
-
note: "tapX/tapY are
|
|
3626
|
+
note: "tapX/tapY are in device pixels — pass directly to tap(x, y) for automatic platform conversion"
|
|
2459
3627
|
};
|
|
3628
|
+
// Check for LogBox overlay (uses default CDP device — native deviceId cannot be mapped to CDP device name)
|
|
3629
|
+
try {
|
|
3630
|
+
const logBoxState = await detectLogBox();
|
|
3631
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3632
|
+
result.logBoxWarning = formatLogBoxWarning(logBoxState).trim();
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
catch {
|
|
3636
|
+
// Non-fatal: LogBox detection failure should not break OCR
|
|
3637
|
+
}
|
|
3638
|
+
// Store screenshot in image buffer
|
|
3639
|
+
try {
|
|
3640
|
+
imageBuffer.add({
|
|
3641
|
+
id: `ocr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3642
|
+
image: screenshotResult.data,
|
|
3643
|
+
timestamp: Date.now(),
|
|
3644
|
+
source: "ocr_screenshot",
|
|
3645
|
+
metadata: {
|
|
3646
|
+
width: screenshotResult.originalWidth || 0,
|
|
3647
|
+
height: screenshotResult.originalHeight || 0,
|
|
3648
|
+
scaleFactor,
|
|
3649
|
+
platform,
|
|
3650
|
+
},
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
catch {
|
|
3654
|
+
// Non-fatal: image buffer write failure should not break OCR response
|
|
3655
|
+
}
|
|
2460
3656
|
return {
|
|
2461
3657
|
content: [
|
|
2462
3658
|
{
|
|
@@ -2471,7 +3667,7 @@ registerToolWithTelemetry("ocr_screenshot", {
|
|
|
2471
3667
|
content: [
|
|
2472
3668
|
{
|
|
2473
3669
|
type: "text",
|
|
2474
|
-
text: `OCR
|
|
3670
|
+
text: `OCR failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2475
3671
|
}
|
|
2476
3672
|
],
|
|
2477
3673
|
isError: true
|
|
@@ -2573,12 +3769,13 @@ registerToolWithTelemetry("ios_boot_simulator", {
|
|
|
2573
3769
|
};
|
|
2574
3770
|
});
|
|
2575
3771
|
// ============================================================================
|
|
2576
|
-
// iOS
|
|
2577
|
-
//
|
|
3772
|
+
// iOS UI Interaction Tools (require an iOS UI driver)
|
|
3773
|
+
// Recommended: AXe — brew install cameroncooke/axe/axe (set IOS_DRIVER=axe)
|
|
3774
|
+
// Alternative: IDB — brew install idb-companion (default)
|
|
2578
3775
|
// ============================================================================
|
|
2579
3776
|
// Tool: iOS swipe
|
|
2580
3777
|
server.registerTool("ios_swipe", {
|
|
2581
|
-
description: "Swipe gesture on an iOS simulator screen. Requires
|
|
3778
|
+
description: "Swipe gesture on an iOS simulator screen. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2582
3779
|
inputSchema: {
|
|
2583
3780
|
startX: z.coerce.number().describe("Starting X coordinate in pixels"),
|
|
2584
3781
|
startY: z.coerce.number().describe("Starting Y coordinate in pixels"),
|
|
@@ -2602,7 +3799,7 @@ server.registerTool("ios_swipe", {
|
|
|
2602
3799
|
});
|
|
2603
3800
|
// Tool: iOS input text
|
|
2604
3801
|
server.registerTool("ios_input_text", {
|
|
2605
|
-
description: "Type text into the active input field on an iOS simulator. Requires
|
|
3802
|
+
description: "Type text into the active input field on an iOS simulator. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2606
3803
|
inputSchema: {
|
|
2607
3804
|
text: z.string().describe("Text to type into the active input field"),
|
|
2608
3805
|
udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
@@ -2621,7 +3818,7 @@ server.registerTool("ios_input_text", {
|
|
|
2621
3818
|
});
|
|
2622
3819
|
// Tool: iOS button
|
|
2623
3820
|
server.registerTool("ios_button", {
|
|
2624
|
-
description: "Press a hardware button on an iOS simulator. Requires
|
|
3821
|
+
description: "Press a hardware button on an iOS simulator. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2625
3822
|
inputSchema: {
|
|
2626
3823
|
button: z
|
|
2627
3824
|
.enum(IOS_BUTTON_TYPES)
|
|
@@ -2643,7 +3840,7 @@ server.registerTool("ios_button", {
|
|
|
2643
3840
|
});
|
|
2644
3841
|
// Tool: iOS key event
|
|
2645
3842
|
server.registerTool("ios_key_event", {
|
|
2646
|
-
description: "Send a key event to an iOS simulator by keycode. Requires
|
|
3843
|
+
description: "Send a key event to an iOS simulator by keycode. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2647
3844
|
inputSchema: {
|
|
2648
3845
|
keycode: z.coerce.number().describe("iOS keycode to send"),
|
|
2649
3846
|
duration: z.coerce.number().optional().describe("Optional key press duration in seconds"),
|
|
@@ -2663,7 +3860,7 @@ server.registerTool("ios_key_event", {
|
|
|
2663
3860
|
});
|
|
2664
3861
|
// Tool: iOS key sequence
|
|
2665
3862
|
server.registerTool("ios_key_sequence", {
|
|
2666
|
-
description: "Send a sequence of key events to an iOS simulator. Requires
|
|
3863
|
+
description: "Send a sequence of key events to an iOS simulator. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2667
3864
|
inputSchema: {
|
|
2668
3865
|
keycodes: z.array(z.coerce.number()).describe("Array of iOS keycodes to send in sequence"),
|
|
2669
3866
|
udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
@@ -2682,17 +3879,30 @@ server.registerTool("ios_key_sequence", {
|
|
|
2682
3879
|
});
|
|
2683
3880
|
// Tool: iOS describe all (accessibility tree)
|
|
2684
3881
|
server.registerTool("ios_describe_all", {
|
|
2685
|
-
description: "Get accessibility information for the entire iOS simulator screen. Returns a nested tree of UI elements with labels, values, and frames. Requires
|
|
3882
|
+
description: "Get accessibility information for the entire iOS simulator screen. Returns a nested tree of UI elements with labels, values, and frames. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2686
3883
|
inputSchema: {
|
|
2687
3884
|
udid: z.string().optional().describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
2688
3885
|
}
|
|
2689
3886
|
}, async ({ udid }) => {
|
|
2690
3887
|
const result = await iosDescribeAll(udid);
|
|
3888
|
+
let text = result.success ? result.result : `Error: ${result.error}`;
|
|
3889
|
+
// Check for LogBox overlay
|
|
3890
|
+
if (result.success) {
|
|
3891
|
+
try {
|
|
3892
|
+
const logBoxState = await detectLogBox();
|
|
3893
|
+
if (logBoxState && logBoxState.total > 0) {
|
|
3894
|
+
text += formatLogBoxWarning(logBoxState);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
catch {
|
|
3898
|
+
// Non-fatal: LogBox detection failure should not break describe_all
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
2691
3901
|
return {
|
|
2692
3902
|
content: [
|
|
2693
3903
|
{
|
|
2694
3904
|
type: "text",
|
|
2695
|
-
text
|
|
3905
|
+
text
|
|
2696
3906
|
}
|
|
2697
3907
|
],
|
|
2698
3908
|
isError: !result.success
|
|
@@ -2700,7 +3910,7 @@ server.registerTool("ios_describe_all", {
|
|
|
2700
3910
|
});
|
|
2701
3911
|
// Tool: iOS describe point
|
|
2702
3912
|
server.registerTool("ios_describe_point", {
|
|
2703
|
-
description: "Get accessibility information for the UI element at a specific point on the iOS simulator screen. Requires
|
|
3913
|
+
description: "Get accessibility information for the UI element at a specific point on the iOS simulator screen. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).",
|
|
2704
3914
|
inputSchema: {
|
|
2705
3915
|
x: z.coerce.number().describe("X coordinate in pixels"),
|
|
2706
3916
|
y: z.coerce.number().describe("Y coordinate in pixels"),
|
|
@@ -2720,7 +3930,7 @@ server.registerTool("ios_describe_point", {
|
|
|
2720
3930
|
});
|
|
2721
3931
|
// Tool: iOS find element (no screenshot needed)
|
|
2722
3932
|
server.registerTool("ios_find_element", {
|
|
2723
|
-
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.",
|
|
3933
|
+
description: "Find a UI element on iOS simulator by accessibility label or value. Returns element details including tap coordinates. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or 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.",
|
|
2724
3934
|
inputSchema: {
|
|
2725
3935
|
label: z.string().optional().describe("Exact accessibility label match"),
|
|
2726
3936
|
labelContains: z.string().optional().describe("Partial label match (case-insensitive)"),
|
|
@@ -2758,7 +3968,7 @@ server.registerTool("ios_find_element", {
|
|
|
2758
3968
|
` Value: "${el.value}"`,
|
|
2759
3969
|
` Type: ${el.type}`,
|
|
2760
3970
|
` Frame: {x: ${el.frame.x}, y: ${el.frame.y}, w: ${el.frame.width}, h: ${el.frame.height}}`,
|
|
2761
|
-
` Center (tap coords): (${el.center.x}, ${el.center.y})
|
|
3971
|
+
` Center (tap coords): (${el.center.x}, ${el.center.y}) — iOS points. Use tap(text="...") or tap(testID="...") for reliable targeting.`,
|
|
2762
3972
|
` Enabled: ${el.enabled}`
|
|
2763
3973
|
].join("\n");
|
|
2764
3974
|
return {
|
|
@@ -2767,7 +3977,7 @@ server.registerTool("ios_find_element", {
|
|
|
2767
3977
|
});
|
|
2768
3978
|
// Tool: iOS wait for element
|
|
2769
3979
|
server.registerTool("ios_wait_for_element", {
|
|
2770
|
-
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.",
|
|
3980
|
+
description: "Wait for a UI element to appear on iOS simulator. Polls until found or timeout. Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion). Use this FIRST after navigation to ensure screen is ready, then use find_element + tap.",
|
|
2771
3981
|
inputSchema: {
|
|
2772
3982
|
label: z.string().optional().describe("Exact accessibility label match"),
|
|
2773
3983
|
labelContains: z.string().optional().describe("Partial label match (case-insensitive)"),
|
|
@@ -2825,109 +4035,43 @@ server.registerTool("ios_wait_for_element", {
|
|
|
2825
4035
|
` Label: "${el.label}"`,
|
|
2826
4036
|
` Value: "${el.value}"`,
|
|
2827
4037
|
` Type: ${el.type}`,
|
|
2828
|
-
` Center (tap coords): (${el.center.x}, ${el.center.y})
|
|
4038
|
+
` Center (tap coords): (${el.center.x}, ${el.center.y}) — iOS points. Use tap(text="...") or tap(testID="...") for reliable targeting.`,
|
|
2829
4039
|
` Enabled: ${el.enabled}`
|
|
2830
4040
|
].join("\n");
|
|
2831
4041
|
return {
|
|
2832
4042
|
content: [{ type: "text", text: info }]
|
|
2833
4043
|
};
|
|
2834
4044
|
});
|
|
2835
|
-
// Tool:
|
|
2836
|
-
registerToolWithTelemetry("
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
4045
|
+
// Tool: Activate Pro license
|
|
4046
|
+
registerToolWithTelemetry("activate_license", getActivateLicenseConfig(), handleActivateLicense);
|
|
4047
|
+
// Tool: Delete account
|
|
4048
|
+
registerToolWithTelemetry("delete_account", getDeleteAccountConfig(), handleDeleteAccount);
|
|
4049
|
+
// Dev-only tool: reset local telemetry data
|
|
4050
|
+
if (isDevMode()) {
|
|
4051
|
+
registerToolWithTelemetry("reset_telemetry", {
|
|
4052
|
+
description: "Clear local telemetry data file (/tmp/rn-devtools-telemetry.jsonl). Only available in development mode.",
|
|
4053
|
+
inputSchema: {},
|
|
4054
|
+
}, async () => {
|
|
4055
|
+
if (existsSync(TELEMETRY_JSONL_PATH)) {
|
|
4056
|
+
unlinkSync(TELEMETRY_JSONL_PATH);
|
|
4057
|
+
return {
|
|
4058
|
+
content: [{ type: "text", text: "Local telemetry data cleared." }],
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
2842
4061
|
return {
|
|
2843
|
-
content: [
|
|
2844
|
-
{
|
|
2845
|
-
type: "text",
|
|
2846
|
-
text: "Debug HTTP server is not running."
|
|
2847
|
-
}
|
|
2848
|
-
],
|
|
2849
|
-
isError: true
|
|
4062
|
+
content: [{ type: "text", text: "No local telemetry data file found." }],
|
|
2850
4063
|
};
|
|
2851
|
-
}
|
|
2852
|
-
const info = {
|
|
2853
|
-
url: `http://localhost:${port}`,
|
|
2854
|
-
endpoints: {
|
|
2855
|
-
status: `http://localhost:${port}/api/status`,
|
|
2856
|
-
logs: `http://localhost:${port}/api/logs`,
|
|
2857
|
-
network: `http://localhost:${port}/api/network`,
|
|
2858
|
-
bundleErrors: `http://localhost:${port}/api/bundle-errors`,
|
|
2859
|
-
apps: `http://localhost:${port}/api/apps`
|
|
2860
|
-
}
|
|
2861
|
-
};
|
|
2862
|
-
return {
|
|
2863
|
-
content: [
|
|
2864
|
-
{
|
|
2865
|
-
type: "text",
|
|
2866
|
-
text: `Debug HTTP server running at:\n\n${JSON.stringify(info, null, 2)}`
|
|
2867
|
-
}
|
|
2868
|
-
]
|
|
2869
|
-
};
|
|
2870
|
-
});
|
|
2871
|
-
// Tool: Restart HTTP server (hot-reload)
|
|
2872
|
-
registerToolWithTelemetry("restart_http_server", {
|
|
2873
|
-
description: "Note: HTTP server now runs in-process to share state. To apply code changes, restart the MCP session.",
|
|
2874
|
-
inputSchema: {}
|
|
2875
|
-
}, async () => {
|
|
2876
|
-
const port = getDebugServerPort();
|
|
2877
|
-
return {
|
|
2878
|
-
content: [
|
|
2879
|
-
{
|
|
2880
|
-
type: "text",
|
|
2881
|
-
text: `HTTP server is running in-process on port ${port}. To apply code changes, rebuild with 'npm run build' and restart the MCP session. The in-process mode is required for the dashboard to show logs, network requests, and connected apps.`
|
|
2882
|
-
}
|
|
2883
|
-
]
|
|
2884
|
-
};
|
|
2885
|
-
});
|
|
2886
|
-
/**
|
|
2887
|
-
* Auto-connect to Metro bundler on startup
|
|
2888
|
-
* Scans common ports and connects to any running Metro servers
|
|
2889
|
-
*/
|
|
2890
|
-
async function autoConnectToMetro() {
|
|
2891
|
-
console.error("[rn-ai-debugger] Auto-scanning for Metro servers...");
|
|
2892
|
-
try {
|
|
2893
|
-
const openPorts = await scanMetroPorts();
|
|
2894
|
-
if (openPorts.length === 0) {
|
|
2895
|
-
console.error("[rn-ai-debugger] No Metro servers found on startup. Use scan_metro to connect later.");
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
for (const port of openPorts) {
|
|
2899
|
-
try {
|
|
2900
|
-
const devices = await fetchDevices(port);
|
|
2901
|
-
const mainDevice = selectMainDevice(devices);
|
|
2902
|
-
if (mainDevice) {
|
|
2903
|
-
await connectToDevice(mainDevice, port);
|
|
2904
|
-
console.error(`[rn-ai-debugger] Auto-connected to ${mainDevice.title} on port ${port}`);
|
|
2905
|
-
// Also connect to Metro build events
|
|
2906
|
-
try {
|
|
2907
|
-
await connectMetroBuildEvents(port);
|
|
2908
|
-
}
|
|
2909
|
-
catch {
|
|
2910
|
-
// Build events connection is optional
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
catch (error) {
|
|
2915
|
-
console.error(`[rn-ai-debugger] Failed to auto-connect on port ${port}: ${error}`);
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
catch (error) {
|
|
2920
|
-
console.error(`[rn-ai-debugger] Auto-connect error: ${error}`);
|
|
2921
|
-
}
|
|
4064
|
+
});
|
|
2922
4065
|
}
|
|
2923
4066
|
// Main function
|
|
2924
4067
|
async function main() {
|
|
2925
4068
|
// Initialize telemetry (checks opt-out env var, loads/creates installation ID)
|
|
4069
|
+
// License validation is lazy — runs on first tool use via ensureLicense()
|
|
2926
4070
|
initTelemetry();
|
|
2927
|
-
|
|
2928
|
-
//
|
|
2929
|
-
await
|
|
2930
|
-
|
|
4071
|
+
identifyIfDevMode(getInstallationId());
|
|
4072
|
+
// --- Eager usage check (pre-loads usage cache for tool-level gate) ---
|
|
4073
|
+
const { ensureLicense } = await import("./core/license.js");
|
|
4074
|
+
await ensureLicense();
|
|
2931
4075
|
const useHttp = process.argv.includes("--http");
|
|
2932
4076
|
const httpPort = parseInt(process.env.MCP_HTTP_PORT || "8600", 10);
|
|
2933
4077
|
if (useHttp) {
|
|
@@ -2997,16 +4141,38 @@ async function main() {
|
|
|
2997
4141
|
await server.connect(transport);
|
|
2998
4142
|
console.error("[rn-ai-debugger] Server started on stdio");
|
|
2999
4143
|
}
|
|
3000
|
-
// Auto-connect to Metro in background (non-blocking)
|
|
3001
|
-
// Use setImmediate to ensure MCP server is fully ready first
|
|
3002
|
-
setImmediate(() => {
|
|
3003
|
-
autoConnectToMetro().catch((err) => {
|
|
3004
|
-
console.error("[rn-ai-debugger] Auto-connect failed:", err);
|
|
3005
|
-
});
|
|
3006
|
-
});
|
|
3007
4144
|
}
|
|
3008
4145
|
main().catch((error) => {
|
|
3009
4146
|
console.error("[rn-ai-debugger] Fatal error:", error);
|
|
3010
4147
|
process.exit(1);
|
|
3011
4148
|
});
|
|
4149
|
+
// Graceful shutdown: close CDP connections so the slot is freed for other sessions
|
|
4150
|
+
function gracefulShutdown() {
|
|
4151
|
+
suppressReconnection();
|
|
4152
|
+
cancelAllReconnectionTimers();
|
|
4153
|
+
for (const [key, app] of connectedApps.entries()) {
|
|
4154
|
+
try {
|
|
4155
|
+
app.ws.close();
|
|
4156
|
+
}
|
|
4157
|
+
catch {
|
|
4158
|
+
// Ignore close errors during shutdown
|
|
4159
|
+
}
|
|
4160
|
+
connectedApps.delete(key);
|
|
4161
|
+
}
|
|
4162
|
+
disconnectMetroBuildEvents();
|
|
4163
|
+
clearAllConnectionState();
|
|
4164
|
+
clearAllCDPMessageTimes();
|
|
4165
|
+
shutdownPostHog().catch(() => { });
|
|
4166
|
+
}
|
|
4167
|
+
process.on("beforeExit", () => {
|
|
4168
|
+
shutdownPostHog().catch(() => { });
|
|
4169
|
+
});
|
|
4170
|
+
process.on("SIGINT", () => {
|
|
4171
|
+
gracefulShutdown();
|
|
4172
|
+
process.exit(0);
|
|
4173
|
+
});
|
|
4174
|
+
process.on("SIGTERM", () => {
|
|
4175
|
+
gracefulShutdown();
|
|
4176
|
+
process.exit(0);
|
|
4177
|
+
});
|
|
3012
4178
|
//# sourceMappingURL=index.js.map
|