react-native-ai-debugger 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/build/core/android.d.ts +108 -0
- package/build/core/android.d.ts.map +1 -0
- package/build/core/android.js +686 -0
- package/build/core/android.js.map +1 -0
- package/build/core/connection.d.ts +12 -0
- package/build/core/connection.d.ts.map +1 -0
- package/build/core/connection.js +242 -0
- package/build/core/connection.js.map +1 -0
- package/build/core/executor.d.ts +6 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +112 -0
- package/build/core/executor.js.map +1 -0
- package/build/core/index.d.ts +10 -0
- package/build/core/index.d.ts.map +1 -0
- package/build/core/index.js +21 -0
- package/build/core/index.js.map +1 -0
- package/build/core/ios.d.ts +54 -0
- package/build/core/ios.d.ts.map +1 -0
- package/build/core/ios.js +393 -0
- package/build/core/ios.js.map +1 -0
- package/build/core/logs.d.ts +27 -0
- package/build/core/logs.d.ts.map +1 -0
- package/build/core/logs.js +102 -0
- package/build/core/logs.js.map +1 -0
- package/build/core/metro.d.ts +8 -0
- package/build/core/metro.d.ts.map +1 -0
- package/build/core/metro.js +79 -0
- package/build/core/metro.js.map +1 -0
- package/build/core/network.d.ts +37 -0
- package/build/core/network.d.ts.map +1 -0
- package/build/core/network.js +210 -0
- package/build/core/network.js.map +1 -0
- package/build/core/state.d.ts +9 -0
- package/build/core/state.d.ts.map +1 -0
- package/build/core/state.js +16 -0
- package/build/core/state.js.map +1 -0
- package/build/core/types.d.ts +68 -0
- package/build/core/types.d.ts.map +1 -0
- package/build/core/types.js +2 -0
- package/build/core/types.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +951 -0
- package/build/index.js.map +1 -0
- package/package.json +50 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { logBuffer, networkBuffer, scanMetroPorts, fetchDevices, selectMainDevice, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp, getLogs, searchLogs, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
|
|
6
|
+
// Android
|
|
7
|
+
listAndroidDevices, androidScreenshot, androidInstallApp, androidLaunchApp, androidListPackages,
|
|
8
|
+
// Android UI Input (Phase 2)
|
|
9
|
+
ANDROID_KEY_EVENTS, androidTap, androidLongPress, androidSwipe, androidInputText, androidKeyEvent, androidGetScreenSize,
|
|
10
|
+
// iOS
|
|
11
|
+
listIOSSimulators, iosScreenshot, iosInstallApp, iosLaunchApp, iosOpenUrl, iosTerminateApp, iosBootSimulator } from "./core/index.js";
|
|
12
|
+
// Create MCP server
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "react-native-ai-debugger",
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
});
|
|
17
|
+
// Tool: Scan for Metro servers
|
|
18
|
+
server.registerTool("scan_metro", {
|
|
19
|
+
description: "Scan for running Metro bundler servers on common ports",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
startPort: z.number().optional().default(8081).describe("Start port for scanning (default: 8081)"),
|
|
22
|
+
endPort: z.number().optional().default(19002).describe("End port for scanning (default: 19002)")
|
|
23
|
+
}
|
|
24
|
+
}, async ({ startPort, endPort }) => {
|
|
25
|
+
const openPorts = await scanMetroPorts(startPort, endPort);
|
|
26
|
+
if (openPorts.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: "No Metro servers found. Make sure Metro bundler is running (npm start or expo start)."
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Fetch devices from each port and connect
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const port of openPorts) {
|
|
39
|
+
const devices = await fetchDevices(port);
|
|
40
|
+
if (devices.length === 0) {
|
|
41
|
+
results.push(`Port ${port}: No devices found`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
results.push(`Port ${port}: Found ${devices.length} device(s)`);
|
|
45
|
+
const mainDevice = selectMainDevice(devices);
|
|
46
|
+
if (mainDevice) {
|
|
47
|
+
try {
|
|
48
|
+
const connectionResult = await connectToDevice(mainDevice, port);
|
|
49
|
+
results.push(` - ${connectionResult}`);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
results.push(` - Failed: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `Metro scan results:\n${results.join("\n")}`
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
// Tool: Get connected apps
|
|
66
|
+
server.registerTool("get_apps", {
|
|
67
|
+
description: "List connected React Native apps and Metro server status",
|
|
68
|
+
inputSchema: {}
|
|
69
|
+
}, async () => {
|
|
70
|
+
const connections = getConnectedApps();
|
|
71
|
+
if (connections.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: 'No apps connected. Run "scan_metro" first to discover and connect to running apps.'
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const status = connections.map(({ app, isConnected }) => {
|
|
82
|
+
const state = isConnected ? "Connected" : "Disconnected";
|
|
83
|
+
return `${app.deviceInfo.title} (${app.deviceInfo.deviceName}): ${state}`;
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Connected apps:\n${status.join("\n")}\n\nTotal logs in buffer: ${logBuffer.size}`
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
// Tool: Get console logs
|
|
95
|
+
server.registerTool("get_logs", {
|
|
96
|
+
description: "Retrieve console logs from connected React Native app",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
maxLogs: z.number().optional().default(50).describe("Maximum number of logs to return (default: 50)"),
|
|
99
|
+
level: z
|
|
100
|
+
.enum(["all", "log", "warn", "error", "info", "debug"])
|
|
101
|
+
.optional()
|
|
102
|
+
.default("all")
|
|
103
|
+
.describe("Filter by log level (default: all)"),
|
|
104
|
+
startFromText: z.string().optional().describe("Start from the first log line containing this text")
|
|
105
|
+
}
|
|
106
|
+
}, async ({ maxLogs, level, startFromText }) => {
|
|
107
|
+
const { logs, formatted } = getLogs(logBuffer, { maxLogs, level, startFromText });
|
|
108
|
+
const startNote = startFromText ? ` (starting from "${startFromText}")` : "";
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: `React Native Console Logs (${logs.length} entries)${startNote}:\n\n${formatted}`
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Tool: Search logs
|
|
119
|
+
server.registerTool("search_logs", {
|
|
120
|
+
description: "Search console logs for text (case-insensitive)",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
text: z.string().describe("Text to search for in log messages"),
|
|
123
|
+
maxResults: z.number().optional().default(50).describe("Maximum number of results to return (default: 50)")
|
|
124
|
+
}
|
|
125
|
+
}, async ({ text, maxResults }) => {
|
|
126
|
+
const { logs, formatted } = searchLogs(logBuffer, text, maxResults);
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: `Search results for "${text}" (${logs.length} matches):\n\n${formatted}`
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
// Tool: Clear logs
|
|
137
|
+
server.registerTool("clear_logs", {
|
|
138
|
+
description: "Clear the log buffer",
|
|
139
|
+
inputSchema: {}
|
|
140
|
+
}, async () => {
|
|
141
|
+
const count = logBuffer.clear();
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: `Cleared ${count} log entries from buffer.`
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
// Tool: Connect to specific Metro port
|
|
152
|
+
server.registerTool("connect_metro", {
|
|
153
|
+
description: "Connect to a specific Metro server port",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
port: z.number().default(8081).describe("Metro server port (default: 8081)")
|
|
156
|
+
}
|
|
157
|
+
}, async ({ port }) => {
|
|
158
|
+
try {
|
|
159
|
+
const devices = await fetchDevices(port);
|
|
160
|
+
if (devices.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `No devices found on port ${port}. Make sure the app is running.`
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const results = [`Found ${devices.length} device(s) on port ${port}:`];
|
|
171
|
+
for (const device of devices) {
|
|
172
|
+
try {
|
|
173
|
+
const result = await connectToDevice(device, port);
|
|
174
|
+
results.push(` - ${result}`);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
results.push(` - ${device.title}: Failed - ${error}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: results.join("\n")
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: `Failed to connect: ${error}`
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// Tool: Execute JavaScript in app
|
|
201
|
+
server.registerTool("execute_in_app", {
|
|
202
|
+
description: "Execute JavaScript code in the connected React Native app and return the result. Use this for REPL-style interactions, inspecting app state, or running diagnostic code.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
expression: z.string().describe("JavaScript expression to execute in the app"),
|
|
205
|
+
awaitPromise: z.boolean().optional().default(true).describe("Whether to await promises (default: true)")
|
|
206
|
+
}
|
|
207
|
+
}, async ({ expression, awaitPromise }) => {
|
|
208
|
+
const result = await executeInApp(expression, awaitPromise);
|
|
209
|
+
if (!result.success) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `Error: ${result.error}`
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
isError: true
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: result.result ?? "undefined"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
// Tool: List debug globals available in the app
|
|
230
|
+
server.registerTool("list_debug_globals", {
|
|
231
|
+
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.",
|
|
232
|
+
inputSchema: {}
|
|
233
|
+
}, async () => {
|
|
234
|
+
const result = await listDebugGlobals();
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
return {
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: "text",
|
|
240
|
+
text: `Error: ${result.error}`
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
isError: true
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: `Available debug globals in the app:\n\n${result.result}`
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
// Tool: Inspect a global object to see its properties and types
|
|
256
|
+
server.registerTool("inspect_global", {
|
|
257
|
+
description: "Inspect a global object to see its properties, types, and whether they are callable functions. Use this BEFORE calling methods on unfamiliar objects to avoid errors.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
objectName: z
|
|
260
|
+
.string()
|
|
261
|
+
.describe("Name of the global object to inspect (e.g., '__EXPO_ROUTER__', '__APOLLO_CLIENT__')")
|
|
262
|
+
}
|
|
263
|
+
}, async ({ objectName }) => {
|
|
264
|
+
const result = await inspectGlobal(objectName);
|
|
265
|
+
if (!result.success) {
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `Error: ${result.error}`
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
isError: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: `Properties of ${objectName}:\n\n${result.result}`
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
// Tool: Get network requests
|
|
286
|
+
server.registerTool("get_network_requests", {
|
|
287
|
+
description: "Retrieve captured network requests from connected React Native app. Shows URL, method, status, and timing.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
maxRequests: z
|
|
290
|
+
.number()
|
|
291
|
+
.optional()
|
|
292
|
+
.default(50)
|
|
293
|
+
.describe("Maximum number of requests to return (default: 50)"),
|
|
294
|
+
method: z
|
|
295
|
+
.string()
|
|
296
|
+
.optional()
|
|
297
|
+
.describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
|
|
298
|
+
urlPattern: z
|
|
299
|
+
.string()
|
|
300
|
+
.optional()
|
|
301
|
+
.describe("Filter by URL pattern (case-insensitive substring match)"),
|
|
302
|
+
status: z
|
|
303
|
+
.number()
|
|
304
|
+
.optional()
|
|
305
|
+
.describe("Filter by HTTP status code (e.g., 200, 401, 500)")
|
|
306
|
+
}
|
|
307
|
+
}, async ({ maxRequests, method, urlPattern, status }) => {
|
|
308
|
+
const { requests, formatted } = getNetworkRequests(networkBuffer, {
|
|
309
|
+
maxRequests,
|
|
310
|
+
method,
|
|
311
|
+
urlPattern,
|
|
312
|
+
status
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: `Network Requests (${requests.length} entries):\n\n${formatted}`
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
// Tool: Search network requests
|
|
324
|
+
server.registerTool("search_network", {
|
|
325
|
+
description: "Search network requests by URL pattern (case-insensitive)",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
urlPattern: z.string().describe("URL pattern to search for"),
|
|
328
|
+
maxResults: z
|
|
329
|
+
.number()
|
|
330
|
+
.optional()
|
|
331
|
+
.default(50)
|
|
332
|
+
.describe("Maximum number of results to return (default: 50)")
|
|
333
|
+
}
|
|
334
|
+
}, async ({ urlPattern, maxResults }) => {
|
|
335
|
+
const { requests, formatted } = searchNetworkRequests(networkBuffer, urlPattern, maxResults);
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `Network search results for "${urlPattern}" (${requests.length} matches):\n\n${formatted}`
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
// Tool: Get request details
|
|
346
|
+
server.registerTool("get_request_details", {
|
|
347
|
+
description: "Get full details of a specific network request including headers, body, and timing. Use get_network_requests first to find the request ID.",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
requestId: z.string().describe("The request ID to get details for")
|
|
350
|
+
}
|
|
351
|
+
}, async ({ requestId }) => {
|
|
352
|
+
const request = networkBuffer.get(requestId);
|
|
353
|
+
if (!request) {
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `Request not found: ${requestId}`
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
isError: true
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
content: [
|
|
366
|
+
{
|
|
367
|
+
type: "text",
|
|
368
|
+
text: formatRequestDetails(request)
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
// Tool: Get network stats
|
|
374
|
+
server.registerTool("get_network_stats", {
|
|
375
|
+
description: "Get statistics about captured network requests: counts by method, status code, and domain.",
|
|
376
|
+
inputSchema: {}
|
|
377
|
+
}, async () => {
|
|
378
|
+
const stats = getNetworkStats(networkBuffer);
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: "text",
|
|
383
|
+
text: `Network Statistics:\n\n${stats}`
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
// Tool: Clear network requests
|
|
389
|
+
server.registerTool("clear_network", {
|
|
390
|
+
description: "Clear the network request buffer",
|
|
391
|
+
inputSchema: {}
|
|
392
|
+
}, async () => {
|
|
393
|
+
const count = networkBuffer.clear();
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: `Cleared ${count} network requests from buffer.`
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
// Tool: Reload the app
|
|
404
|
+
server.registerTool("reload_app", {
|
|
405
|
+
description: "Reload the connected React Native app. Triggers a JavaScript bundle reload (like pressing 'r' in Metro or shaking the device).",
|
|
406
|
+
inputSchema: {}
|
|
407
|
+
}, async () => {
|
|
408
|
+
const result = await reloadApp();
|
|
409
|
+
if (!result.success) {
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: "text",
|
|
414
|
+
text: `Error: ${result.error}`
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
isError: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: "text",
|
|
424
|
+
text: result.result ?? "App reload triggered"
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// Android Tools
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// Tool: List Android devices
|
|
433
|
+
server.registerTool("list_android_devices", {
|
|
434
|
+
description: "List connected Android devices and emulators via ADB",
|
|
435
|
+
inputSchema: {}
|
|
436
|
+
}, async () => {
|
|
437
|
+
const result = await listAndroidDevices();
|
|
438
|
+
return {
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: "text",
|
|
442
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
443
|
+
}
|
|
444
|
+
],
|
|
445
|
+
isError: !result.success
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
// Tool: Android screenshot
|
|
449
|
+
server.registerTool("android_screenshot", {
|
|
450
|
+
description: "Take a screenshot from an Android device/emulator. Returns the image data that can be displayed.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
outputPath: z
|
|
453
|
+
.string()
|
|
454
|
+
.optional()
|
|
455
|
+
.describe("Optional path to save the screenshot. If not provided, saves to temp directory."),
|
|
456
|
+
deviceId: z
|
|
457
|
+
.string()
|
|
458
|
+
.optional()
|
|
459
|
+
.describe("Optional device ID (from list_android_devices). Uses first available device if not specified.")
|
|
460
|
+
}
|
|
461
|
+
}, async ({ outputPath, deviceId }) => {
|
|
462
|
+
const result = await androidScreenshot(outputPath, deviceId);
|
|
463
|
+
if (!result.success) {
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: "text",
|
|
468
|
+
text: `Error: ${result.error}`
|
|
469
|
+
}
|
|
470
|
+
],
|
|
471
|
+
isError: true
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
// Include image data if available
|
|
475
|
+
if (result.data) {
|
|
476
|
+
// Build info text with scale factor for coordinate conversion
|
|
477
|
+
let infoText = `Screenshot captured (${result.originalWidth}x${result.originalHeight})`;
|
|
478
|
+
if (result.scaleFactor && result.scaleFactor > 1) {
|
|
479
|
+
infoText += `\n⚠️ Image was scaled down to fit API limits. Scale factor: ${result.scaleFactor.toFixed(3)}`;
|
|
480
|
+
infoText += `\nTo tap/swipe: multiply image coordinates by ${result.scaleFactor.toFixed(3)} to get device coordinates.`;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: infoText
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
type: "image",
|
|
490
|
+
data: result.data.toString("base64"),
|
|
491
|
+
mimeType: "image/png"
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: `Screenshot saved to: ${result.result}`
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
// Tool: Android install app
|
|
506
|
+
server.registerTool("android_install_app", {
|
|
507
|
+
description: "Install an APK on an Android device/emulator",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
apkPath: z.string().describe("Path to the APK file to install"),
|
|
510
|
+
deviceId: z
|
|
511
|
+
.string()
|
|
512
|
+
.optional()
|
|
513
|
+
.describe("Optional device ID. Uses first available device if not specified."),
|
|
514
|
+
replace: z
|
|
515
|
+
.boolean()
|
|
516
|
+
.optional()
|
|
517
|
+
.default(true)
|
|
518
|
+
.describe("Replace existing app if already installed (default: true)"),
|
|
519
|
+
grantPermissions: z
|
|
520
|
+
.boolean()
|
|
521
|
+
.optional()
|
|
522
|
+
.default(false)
|
|
523
|
+
.describe("Grant all runtime permissions on install (default: false)")
|
|
524
|
+
}
|
|
525
|
+
}, async ({ apkPath, deviceId, replace, grantPermissions }) => {
|
|
526
|
+
const result = await androidInstallApp(apkPath, deviceId, { replace, grantPermissions });
|
|
527
|
+
return {
|
|
528
|
+
content: [
|
|
529
|
+
{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
532
|
+
}
|
|
533
|
+
],
|
|
534
|
+
isError: !result.success
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
// Tool: Android launch app
|
|
538
|
+
server.registerTool("android_launch_app", {
|
|
539
|
+
description: "Launch an app on an Android device/emulator by package name",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
packageName: z.string().describe("Package name of the app (e.g., com.example.myapp)"),
|
|
542
|
+
activityName: z
|
|
543
|
+
.string()
|
|
544
|
+
.optional()
|
|
545
|
+
.describe("Optional activity name to launch (e.g., .MainActivity). If not provided, launches the main activity."),
|
|
546
|
+
deviceId: z
|
|
547
|
+
.string()
|
|
548
|
+
.optional()
|
|
549
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
550
|
+
}
|
|
551
|
+
}, async ({ packageName, activityName, deviceId }) => {
|
|
552
|
+
const result = await androidLaunchApp(packageName, activityName, deviceId);
|
|
553
|
+
return {
|
|
554
|
+
content: [
|
|
555
|
+
{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
558
|
+
}
|
|
559
|
+
],
|
|
560
|
+
isError: !result.success
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
// Tool: Android list packages
|
|
564
|
+
server.registerTool("android_list_packages", {
|
|
565
|
+
description: "List installed packages on an Android device/emulator",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
deviceId: z
|
|
568
|
+
.string()
|
|
569
|
+
.optional()
|
|
570
|
+
.describe("Optional device ID. Uses first available device if not specified."),
|
|
571
|
+
filter: z
|
|
572
|
+
.string()
|
|
573
|
+
.optional()
|
|
574
|
+
.describe("Optional filter to search packages by name (case-insensitive)")
|
|
575
|
+
}
|
|
576
|
+
}, async ({ deviceId, filter }) => {
|
|
577
|
+
const result = await androidListPackages(deviceId, filter);
|
|
578
|
+
return {
|
|
579
|
+
content: [
|
|
580
|
+
{
|
|
581
|
+
type: "text",
|
|
582
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
583
|
+
}
|
|
584
|
+
],
|
|
585
|
+
isError: !result.success
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
// ============================================================================
|
|
589
|
+
// Android UI Input Tools (Phase 2)
|
|
590
|
+
// ============================================================================
|
|
591
|
+
// Tool: Android tap
|
|
592
|
+
server.registerTool("android_tap", {
|
|
593
|
+
description: "Tap at specific coordinates on an Android device/emulator screen",
|
|
594
|
+
inputSchema: {
|
|
595
|
+
x: z.number().describe("X coordinate in pixels"),
|
|
596
|
+
y: z.number().describe("Y coordinate in pixels"),
|
|
597
|
+
deviceId: z
|
|
598
|
+
.string()
|
|
599
|
+
.optional()
|
|
600
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
601
|
+
}
|
|
602
|
+
}, async ({ x, y, deviceId }) => {
|
|
603
|
+
const result = await androidTap(x, y, deviceId);
|
|
604
|
+
return {
|
|
605
|
+
content: [
|
|
606
|
+
{
|
|
607
|
+
type: "text",
|
|
608
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
609
|
+
}
|
|
610
|
+
],
|
|
611
|
+
isError: !result.success
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
// Tool: Android long press
|
|
615
|
+
server.registerTool("android_long_press", {
|
|
616
|
+
description: "Long press at specific coordinates on an Android device/emulator screen",
|
|
617
|
+
inputSchema: {
|
|
618
|
+
x: z.number().describe("X coordinate in pixels"),
|
|
619
|
+
y: z.number().describe("Y coordinate in pixels"),
|
|
620
|
+
durationMs: z
|
|
621
|
+
.number()
|
|
622
|
+
.optional()
|
|
623
|
+
.default(1000)
|
|
624
|
+
.describe("Press duration in milliseconds (default: 1000)"),
|
|
625
|
+
deviceId: z
|
|
626
|
+
.string()
|
|
627
|
+
.optional()
|
|
628
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
629
|
+
}
|
|
630
|
+
}, async ({ x, y, durationMs, deviceId }) => {
|
|
631
|
+
const result = await androidLongPress(x, y, durationMs, deviceId);
|
|
632
|
+
return {
|
|
633
|
+
content: [
|
|
634
|
+
{
|
|
635
|
+
type: "text",
|
|
636
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
isError: !result.success
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
// Tool: Android swipe
|
|
643
|
+
server.registerTool("android_swipe", {
|
|
644
|
+
description: "Swipe from one point to another on an Android device/emulator screen",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
startX: z.number().describe("Starting X coordinate in pixels"),
|
|
647
|
+
startY: z.number().describe("Starting Y coordinate in pixels"),
|
|
648
|
+
endX: z.number().describe("Ending X coordinate in pixels"),
|
|
649
|
+
endY: z.number().describe("Ending Y coordinate in pixels"),
|
|
650
|
+
durationMs: z
|
|
651
|
+
.number()
|
|
652
|
+
.optional()
|
|
653
|
+
.default(300)
|
|
654
|
+
.describe("Swipe duration in milliseconds (default: 300)"),
|
|
655
|
+
deviceId: z
|
|
656
|
+
.string()
|
|
657
|
+
.optional()
|
|
658
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
659
|
+
}
|
|
660
|
+
}, async ({ startX, startY, endX, endY, durationMs, deviceId }) => {
|
|
661
|
+
const result = await androidSwipe(startX, startY, endX, endY, durationMs, deviceId);
|
|
662
|
+
return {
|
|
663
|
+
content: [
|
|
664
|
+
{
|
|
665
|
+
type: "text",
|
|
666
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
667
|
+
}
|
|
668
|
+
],
|
|
669
|
+
isError: !result.success
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
// Tool: Android input text
|
|
673
|
+
server.registerTool("android_input_text", {
|
|
674
|
+
description: "Type text on an Android device/emulator. The text will be input at the current focus point (tap an input field first).",
|
|
675
|
+
inputSchema: {
|
|
676
|
+
text: z.string().describe("Text to type"),
|
|
677
|
+
deviceId: z
|
|
678
|
+
.string()
|
|
679
|
+
.optional()
|
|
680
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
681
|
+
}
|
|
682
|
+
}, async ({ text, deviceId }) => {
|
|
683
|
+
const result = await androidInputText(text, deviceId);
|
|
684
|
+
return {
|
|
685
|
+
content: [
|
|
686
|
+
{
|
|
687
|
+
type: "text",
|
|
688
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
isError: !result.success
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
// Tool: Android key event
|
|
695
|
+
server.registerTool("android_key_event", {
|
|
696
|
+
description: `Send a key event to an Android device/emulator. Common keys: ${Object.keys(ANDROID_KEY_EVENTS).join(", ")}`,
|
|
697
|
+
inputSchema: {
|
|
698
|
+
key: z
|
|
699
|
+
.string()
|
|
700
|
+
.describe(`Key name (${Object.keys(ANDROID_KEY_EVENTS).join(", ")}) or numeric keycode`),
|
|
701
|
+
deviceId: z
|
|
702
|
+
.string()
|
|
703
|
+
.optional()
|
|
704
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
705
|
+
}
|
|
706
|
+
}, async ({ key, deviceId }) => {
|
|
707
|
+
// Try to parse as number first, otherwise treat as key name
|
|
708
|
+
const keyCode = /^\d+$/.test(key)
|
|
709
|
+
? parseInt(key, 10)
|
|
710
|
+
: key.toUpperCase();
|
|
711
|
+
const result = await androidKeyEvent(keyCode, deviceId);
|
|
712
|
+
return {
|
|
713
|
+
content: [
|
|
714
|
+
{
|
|
715
|
+
type: "text",
|
|
716
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
717
|
+
}
|
|
718
|
+
],
|
|
719
|
+
isError: !result.success
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
// Tool: Android get screen size
|
|
723
|
+
server.registerTool("android_get_screen_size", {
|
|
724
|
+
description: "Get the screen size (resolution) of an Android device/emulator",
|
|
725
|
+
inputSchema: {
|
|
726
|
+
deviceId: z
|
|
727
|
+
.string()
|
|
728
|
+
.optional()
|
|
729
|
+
.describe("Optional device ID. Uses first available device if not specified.")
|
|
730
|
+
}
|
|
731
|
+
}, async ({ deviceId }) => {
|
|
732
|
+
const result = await androidGetScreenSize(deviceId);
|
|
733
|
+
if (!result.success) {
|
|
734
|
+
return {
|
|
735
|
+
content: [
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
text: `Error: ${result.error}`
|
|
739
|
+
}
|
|
740
|
+
],
|
|
741
|
+
isError: true
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
content: [
|
|
746
|
+
{
|
|
747
|
+
type: "text",
|
|
748
|
+
text: `Screen size: ${result.width}x${result.height} pixels`
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
// ============================================================================
|
|
754
|
+
// iOS Simulator Tools
|
|
755
|
+
// ============================================================================
|
|
756
|
+
// Tool: List iOS simulators
|
|
757
|
+
server.registerTool("list_ios_simulators", {
|
|
758
|
+
description: "List available iOS simulators",
|
|
759
|
+
inputSchema: {
|
|
760
|
+
onlyBooted: z
|
|
761
|
+
.boolean()
|
|
762
|
+
.optional()
|
|
763
|
+
.default(false)
|
|
764
|
+
.describe("Only show currently running simulators (default: false)")
|
|
765
|
+
}
|
|
766
|
+
}, async ({ onlyBooted }) => {
|
|
767
|
+
const result = await listIOSSimulators(onlyBooted);
|
|
768
|
+
return {
|
|
769
|
+
content: [
|
|
770
|
+
{
|
|
771
|
+
type: "text",
|
|
772
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
773
|
+
}
|
|
774
|
+
],
|
|
775
|
+
isError: !result.success
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
// Tool: iOS screenshot
|
|
779
|
+
server.registerTool("ios_screenshot", {
|
|
780
|
+
description: "Take a screenshot from an iOS simulator. Returns the image data that can be displayed.",
|
|
781
|
+
inputSchema: {
|
|
782
|
+
outputPath: z
|
|
783
|
+
.string()
|
|
784
|
+
.optional()
|
|
785
|
+
.describe("Optional path to save the screenshot. If not provided, saves to temp directory."),
|
|
786
|
+
udid: z
|
|
787
|
+
.string()
|
|
788
|
+
.optional()
|
|
789
|
+
.describe("Optional simulator UDID (from list_ios_simulators). Uses booted simulator if not specified.")
|
|
790
|
+
}
|
|
791
|
+
}, async ({ outputPath, udid }) => {
|
|
792
|
+
const result = await iosScreenshot(outputPath, udid);
|
|
793
|
+
if (!result.success) {
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: "text",
|
|
798
|
+
text: `Error: ${result.error}`
|
|
799
|
+
}
|
|
800
|
+
],
|
|
801
|
+
isError: true
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// Include image data if available
|
|
805
|
+
if (result.data) {
|
|
806
|
+
// Build info text with scale factor for coordinate conversion
|
|
807
|
+
let infoText = `Screenshot captured (${result.originalWidth}x${result.originalHeight})`;
|
|
808
|
+
if (result.scaleFactor && result.scaleFactor > 1) {
|
|
809
|
+
infoText += `\n⚠️ Image was scaled down to fit API limits. Scale factor: ${result.scaleFactor.toFixed(3)}`;
|
|
810
|
+
infoText += `\nTo tap/swipe: multiply image coordinates by ${result.scaleFactor.toFixed(3)} to get device coordinates.`;
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
content: [
|
|
814
|
+
{
|
|
815
|
+
type: "text",
|
|
816
|
+
text: infoText
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
type: "image",
|
|
820
|
+
data: result.data.toString("base64"),
|
|
821
|
+
mimeType: "image/png"
|
|
822
|
+
}
|
|
823
|
+
]
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
content: [
|
|
828
|
+
{
|
|
829
|
+
type: "text",
|
|
830
|
+
text: `Screenshot saved to: ${result.result}`
|
|
831
|
+
}
|
|
832
|
+
]
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
// Tool: iOS install app
|
|
836
|
+
server.registerTool("ios_install_app", {
|
|
837
|
+
description: "Install an app bundle (.app) on an iOS simulator",
|
|
838
|
+
inputSchema: {
|
|
839
|
+
appPath: z.string().describe("Path to the .app bundle to install"),
|
|
840
|
+
udid: z
|
|
841
|
+
.string()
|
|
842
|
+
.optional()
|
|
843
|
+
.describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
844
|
+
}
|
|
845
|
+
}, async ({ appPath, udid }) => {
|
|
846
|
+
const result = await iosInstallApp(appPath, udid);
|
|
847
|
+
return {
|
|
848
|
+
content: [
|
|
849
|
+
{
|
|
850
|
+
type: "text",
|
|
851
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
852
|
+
}
|
|
853
|
+
],
|
|
854
|
+
isError: !result.success
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
// Tool: iOS launch app
|
|
858
|
+
server.registerTool("ios_launch_app", {
|
|
859
|
+
description: "Launch an app on an iOS simulator by bundle ID",
|
|
860
|
+
inputSchema: {
|
|
861
|
+
bundleId: z.string().describe("Bundle ID of the app (e.g., com.example.myapp)"),
|
|
862
|
+
udid: z
|
|
863
|
+
.string()
|
|
864
|
+
.optional()
|
|
865
|
+
.describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
866
|
+
}
|
|
867
|
+
}, async ({ bundleId, udid }) => {
|
|
868
|
+
const result = await iosLaunchApp(bundleId, udid);
|
|
869
|
+
return {
|
|
870
|
+
content: [
|
|
871
|
+
{
|
|
872
|
+
type: "text",
|
|
873
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
874
|
+
}
|
|
875
|
+
],
|
|
876
|
+
isError: !result.success
|
|
877
|
+
};
|
|
878
|
+
});
|
|
879
|
+
// Tool: iOS open URL
|
|
880
|
+
server.registerTool("ios_open_url", {
|
|
881
|
+
description: "Open a URL in the iOS simulator (opens in default handler or Safari)",
|
|
882
|
+
inputSchema: {
|
|
883
|
+
url: z.string().describe("URL to open (e.g., https://example.com or myapp://path)"),
|
|
884
|
+
udid: z
|
|
885
|
+
.string()
|
|
886
|
+
.optional()
|
|
887
|
+
.describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
888
|
+
}
|
|
889
|
+
}, async ({ url, udid }) => {
|
|
890
|
+
const result = await iosOpenUrl(url, udid);
|
|
891
|
+
return {
|
|
892
|
+
content: [
|
|
893
|
+
{
|
|
894
|
+
type: "text",
|
|
895
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
896
|
+
}
|
|
897
|
+
],
|
|
898
|
+
isError: !result.success
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
// Tool: iOS terminate app
|
|
902
|
+
server.registerTool("ios_terminate_app", {
|
|
903
|
+
description: "Terminate a running app on an iOS simulator",
|
|
904
|
+
inputSchema: {
|
|
905
|
+
bundleId: z.string().describe("Bundle ID of the app to terminate"),
|
|
906
|
+
udid: z
|
|
907
|
+
.string()
|
|
908
|
+
.optional()
|
|
909
|
+
.describe("Optional simulator UDID. Uses booted simulator if not specified.")
|
|
910
|
+
}
|
|
911
|
+
}, async ({ bundleId, udid }) => {
|
|
912
|
+
const result = await iosTerminateApp(bundleId, udid);
|
|
913
|
+
return {
|
|
914
|
+
content: [
|
|
915
|
+
{
|
|
916
|
+
type: "text",
|
|
917
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
918
|
+
}
|
|
919
|
+
],
|
|
920
|
+
isError: !result.success
|
|
921
|
+
};
|
|
922
|
+
});
|
|
923
|
+
// Tool: iOS boot simulator
|
|
924
|
+
server.registerTool("ios_boot_simulator", {
|
|
925
|
+
description: "Boot an iOS simulator by UDID. Use list_ios_simulators to find available simulators.",
|
|
926
|
+
inputSchema: {
|
|
927
|
+
udid: z.string().describe("UDID of the simulator to boot (from list_ios_simulators)")
|
|
928
|
+
}
|
|
929
|
+
}, async ({ udid }) => {
|
|
930
|
+
const result = await iosBootSimulator(udid);
|
|
931
|
+
return {
|
|
932
|
+
content: [
|
|
933
|
+
{
|
|
934
|
+
type: "text",
|
|
935
|
+
text: result.success ? result.result : `Error: ${result.error}`
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
isError: !result.success
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
// Main function
|
|
942
|
+
async function main() {
|
|
943
|
+
const transport = new StdioServerTransport();
|
|
944
|
+
await server.connect(transport);
|
|
945
|
+
console.error("[rn-ai-debugger] Server started on stdio");
|
|
946
|
+
}
|
|
947
|
+
main().catch((error) => {
|
|
948
|
+
console.error("[rn-ai-debugger] Fatal error:", error);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
});
|
|
951
|
+
//# sourceMappingURL=index.js.map
|