@ggboi360/mobile-dev-mcp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2300 +1,976 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * Mobile Dev MCP - Read-Only Debugging Tools for Mobile Development
4
+ *
5
+ * A read-only MCP server for observing and debugging mobile apps:
6
+ * - Taking screenshots
7
+ * - Viewing device information
8
+ * - Reading logs
9
+ * - Inspecting UI hierarchy
10
+ * - Analyzing screen content
11
+ *
12
+ * License: Dual (MIT + Elastic License 2.0)
13
+ */
2
14
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
16
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { exec, spawn } from "child_process";
6
- import { promisify } from "util";
7
- import * as fs from "fs";
8
- import * as path from "path";
9
- import * as http from "http";
10
- import WebSocket from "ws";
11
- // License module
12
- import { checkLicense, requireAdvanced, requireBasic, licenseTools, handleLicenseTool, loadConfig, TIER_LIMITS, } from "./license.js";
13
- const execAsync = promisify(exec);
14
- // Configuration (loaded from user config)
17
+ import { ADB, XCRUN, execAsync, sleep, parseUiTree, findElementInTree, captureAndroidScreenshot, captureIosScreenshot, listConnectedDevices, loadConfig, validateDeviceId, validatePackageName, validateUdid, validateLogFilter, validateLogLevel, } from "./utils.js";
18
+ import { checkLicense, canAccessTool, getMaxLogLines, getMaxDevices, getLicenseStatus, setLicenseKey, } from "./license.js";
19
+ // ============================================================================
20
+ // CONFIG
21
+ // ============================================================================
15
22
  const userConfig = loadConfig();
16
23
  const CONFIG = {
17
24
  metroPort: userConfig.metroPort || 8081,
18
- logBufferSize: userConfig.logBufferSize || 100,
19
25
  screenshotDir: process.env.TEMP || "/tmp",
20
26
  };
21
- // Log buffers
22
- let metroLogBuffer = [];
23
- let adbLogBuffer = [];
24
- let metroProcess = null;
25
- let adbLogProcess = null;
26
- // Screenshot history for Pro users
27
- let screenshotHistory = [];
28
- const MAX_SCREENSHOT_HISTORY = 20;
29
27
  // ============================================================================
30
- // TOOL DEFINITIONS
28
+ // TOOL DEFINITIONS (21 tools)
31
29
  // ============================================================================
32
- const coreTools = [
33
- // === FREE TIER TOOLS ===
34
- {
35
- name: "get_metro_logs",
36
- description: "Get recent logs from Metro bundler. Returns the last N lines of Metro output. Useful for seeing build errors, warnings, and bundle status.",
37
- inputSchema: {
38
- type: "object",
39
- properties: {
40
- lines: {
41
- type: "number",
42
- description: "Number of log lines to retrieve (default: 50, max: 50 for free tier)",
43
- default: 50,
44
- },
45
- filter: {
46
- type: "string",
47
- description: "Optional filter string to match (e.g., 'error', 'warning')",
48
- },
49
- },
50
- },
51
- },
52
- {
53
- name: "get_adb_logs",
54
- description: "Get logs from Android device/emulator via ADB logcat. Filters for React Native and JavaScript logs by default.",
55
- inputSchema: {
56
- type: "object",
57
- properties: {
58
- lines: {
59
- type: "number",
60
- description: "Number of log lines to retrieve (default: 50, max: 50 for free tier)",
61
- default: 50,
62
- },
63
- filter: {
64
- type: "string",
65
- description: "Tag filter for logcat (default: 'ReactNativeJS'). Use '*' for all logs.",
66
- default: "ReactNativeJS",
67
- },
68
- level: {
69
- type: "string",
70
- enum: ["V", "D", "I", "W", "E", "F"],
71
- description: "Minimum log level: V(erbose), D(ebug), I(nfo), W(arn), E(rror), F(atal)",
72
- default: "I",
73
- },
74
- },
75
- },
76
- },
30
+ const tools = [
31
+ // === FREE TIER - Screenshots (2 tools) ===
77
32
  {
78
33
  name: "screenshot_emulator",
79
- description: "Capture a screenshot from the currently running Android emulator. Returns the screenshot as a base64-encoded image.",
34
+ description: "Capture a screenshot from the currently running Android emulator. Returns base64-encoded PNG image.",
80
35
  inputSchema: {
81
36
  type: "object",
82
37
  properties: {
83
38
  device: {
84
39
  type: "string",
85
- description: "Specific device ID (from 'adb devices'). Leave empty for default device.",
40
+ description: "Specific device ID. Leave empty for default device.",
86
41
  },
87
42
  },
88
43
  },
89
44
  },
90
45
  {
91
- name: "list_devices",
92
- description: "List all connected Android devices and emulators via ADB. Shows device IDs and status.",
93
- inputSchema: {
94
- type: "object",
95
- properties: {},
96
- },
97
- },
98
- {
99
- name: "check_metro_status",
100
- description: "Check if Metro bundler is running and get its current status. Returns bundle status and any pending builds.",
46
+ name: "screenshot_ios_simulator",
47
+ description: "Capture a screenshot from an iOS Simulator. Returns base64-encoded PNG image.",
101
48
  inputSchema: {
102
49
  type: "object",
103
50
  properties: {
104
- port: {
105
- type: "number",
106
- description: "Metro port (default: 8081)",
107
- default: 8081,
51
+ udid: {
52
+ type: "string",
53
+ description: "Simulator UDID. Leave empty for booted simulator.",
108
54
  },
109
55
  },
110
56
  },
111
57
  },
58
+ // === FREE TIER - Device Info (5 tools) ===
112
59
  {
113
- name: "get_app_info",
114
- description: "Get information about an installed app on the Android device, including version and permissions.",
115
- inputSchema: {
116
- type: "object",
117
- properties: {
118
- packageName: {
119
- type: "string",
120
- description: "The app package name (e.g., 'com.myapp' or 'host.exp.exponent')",
121
- },
122
- },
123
- required: ["packageName"],
124
- },
60
+ name: "list_devices",
61
+ description: "List all connected Android devices and emulators via ADB.",
62
+ inputSchema: { type: "object", properties: {} },
125
63
  },
126
64
  {
127
- name: "clear_app_data",
128
- description: "Clear app data and cache on Android device. Useful for testing fresh installs.",
65
+ name: "list_ios_simulators",
66
+ description: "List all available iOS Simulators with state and iOS version.",
129
67
  inputSchema: {
130
68
  type: "object",
131
69
  properties: {
132
- packageName: {
133
- type: "string",
134
- description: "The app package name to clear data for",
70
+ onlyBooted: {
71
+ type: "boolean",
72
+ description: "Only show booted simulators (default: false)",
73
+ default: false,
135
74
  },
136
75
  },
137
- required: ["packageName"],
138
- },
139
- },
140
- {
141
- name: "restart_adb",
142
- description: "Restart the ADB server. Useful when ADB becomes unresponsive or devices are not detected.",
143
- inputSchema: {
144
- type: "object",
145
- properties: {},
146
76
  },
147
77
  },
148
78
  {
149
79
  name: "get_device_info",
150
- description: "Get detailed information about the connected Android device including OS version, screen size, and available memory.",
80
+ description: "Get detailed information about connected Android device (OS version, screen size, memory).",
151
81
  inputSchema: {
152
82
  type: "object",
153
83
  properties: {
154
84
  device: {
155
85
  type: "string",
156
- description: "Specific device ID. Leave empty for default device.",
86
+ description: "Specific device ID. Leave empty for default.",
157
87
  },
158
88
  },
159
89
  },
160
90
  },
161
91
  {
162
- name: "start_metro_logging",
163
- description: "Start capturing Metro bundler logs by watching a log file. Point this at your Metro output file or pipe Metro to a file.",
92
+ name: "get_ios_simulator_info",
93
+ description: "Get detailed information about an iOS Simulator.",
164
94
  inputSchema: {
165
95
  type: "object",
166
96
  properties: {
167
- logFile: {
97
+ udid: {
168
98
  type: "string",
169
- description: "Path to Metro log file to watch. If not provided, will try common locations.",
99
+ description: "Simulator UDID. Leave empty for booted simulator.",
170
100
  },
171
101
  },
172
102
  },
173
103
  },
174
104
  {
175
- name: "stop_metro_logging",
176
- description: "Stop the background Metro log capture.",
177
- inputSchema: {
178
- type: "object",
179
- properties: {},
180
- },
181
- },
182
- // === PRO TIER TOOLS ===
183
- {
184
- name: "stream_adb_realtime",
185
- description: "[PRO] Start real-time ADB log streaming. Logs are continuously captured in the background.",
105
+ name: "get_app_info",
106
+ description: "Get information about an installed app on Android device.",
186
107
  inputSchema: {
187
108
  type: "object",
188
109
  properties: {
189
- filter: {
110
+ packageName: {
190
111
  type: "string",
191
- description: "Tag filter for logcat (default: 'ReactNativeJS')",
192
- default: "ReactNativeJS",
112
+ description: "The app package name (e.g., 'com.myapp')",
193
113
  },
194
114
  },
115
+ required: ["packageName"],
195
116
  },
196
117
  },
118
+ // === FREE TIER - Logs (4 tools) ===
197
119
  {
198
- name: "stop_adb_streaming",
199
- description: "[PRO] Stop real-time ADB log streaming.",
200
- inputSchema: {
201
- type: "object",
202
- properties: {},
203
- },
204
- },
205
- {
206
- name: "screenshot_history",
207
- description: "[PRO] Get previously captured screenshots. Stores up to 20 screenshots.",
120
+ name: "get_metro_logs",
121
+ description: "Get recent logs from Metro bundler. Useful for build errors and warnings.",
208
122
  inputSchema: {
209
123
  type: "object",
210
124
  properties: {
211
- count: {
125
+ lines: {
212
126
  type: "number",
213
- description: "Number of recent screenshots to retrieve (default: 5)",
214
- default: 5,
215
- },
216
- },
217
- },
218
- },
219
- {
220
- name: "watch_for_errors",
221
- description: "[PRO] Start watching logs for specific error patterns. Returns when an error is detected.",
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- patterns: {
226
- type: "array",
227
- items: { type: "string" },
228
- description: "Error patterns to watch for (e.g., ['Error', 'Exception', 'crash'])",
127
+ description: "Number of lines (default: 50, limit varies by tier)",
128
+ default: 50,
229
129
  },
230
- timeout: {
231
- type: "number",
232
- description: "Timeout in seconds (default: 60)",
233
- default: 60,
130
+ filter: {
131
+ type: "string",
132
+ description: "Optional filter string (e.g., 'error', 'warning')",
234
133
  },
235
134
  },
236
135
  },
237
136
  },
238
137
  {
239
- name: "multi_device_logs",
240
- description: "[PRO] Get logs from multiple devices simultaneously.",
138
+ name: "get_adb_logs",
139
+ description: "Get logs from Android device via ADB logcat. Filters for React Native by default.",
241
140
  inputSchema: {
242
141
  type: "object",
243
142
  properties: {
244
- devices: {
245
- type: "array",
246
- items: { type: "string" },
247
- description: "Array of device IDs to get logs from",
248
- },
249
143
  lines: {
250
144
  type: "number",
251
- description: "Number of log lines per device",
252
- default: 30,
253
- },
254
- },
255
- },
256
- },
257
- // === INTERACTION TOOLS (Advanced) ===
258
- {
259
- name: "tap_screen",
260
- description: "[PRO] Tap on the screen at specific coordinates. Use this to interact with UI elements.",
261
- inputSchema: {
262
- type: "object",
263
- properties: {
264
- x: {
265
- type: "number",
266
- description: "X coordinate to tap",
267
- },
268
- y: {
269
- type: "number",
270
- description: "Y coordinate to tap",
271
- },
272
- device: {
273
- type: "string",
274
- description: "Specific device ID (optional)",
145
+ description: "Number of lines (default: 50, limit varies by tier)",
146
+ default: 50,
275
147
  },
276
- },
277
- required: ["x", "y"],
278
- },
279
- },
280
- {
281
- name: "input_text",
282
- description: "[PRO] Type text into the currently focused input field.",
283
- inputSchema: {
284
- type: "object",
285
- properties: {
286
- text: {
148
+ filter: {
287
149
  type: "string",
288
- description: "Text to type",
150
+ description: "Tag filter (default: 'ReactNativeJS'). Use '*' for all.",
151
+ default: "ReactNativeJS",
289
152
  },
290
- device: {
153
+ level: {
291
154
  type: "string",
292
- description: "Specific device ID (optional)",
155
+ enum: ["V", "D", "I", "W", "E", "F"],
156
+ description: "Minimum log level",
157
+ default: "I",
293
158
  },
294
159
  },
295
- required: ["text"],
296
160
  },
297
161
  },
298
162
  {
299
- name: "press_button",
300
- description: "[PRO] Press a hardware button (back, home, recent apps, volume, power).",
163
+ name: "get_ios_simulator_logs",
164
+ description: "Get recent logs from iOS Simulator.",
301
165
  inputSchema: {
302
166
  type: "object",
303
167
  properties: {
304
- button: {
168
+ lines: {
169
+ type: "number",
170
+ description: "Number of lines (default: 50)",
171
+ default: 50,
172
+ },
173
+ filter: {
305
174
  type: "string",
306
- enum: ["back", "home", "recent", "volume_up", "volume_down", "power", "enter"],
307
- description: "Button to press",
175
+ description: "Filter logs by subsystem or content",
308
176
  },
309
- device: {
177
+ udid: {
310
178
  type: "string",
311
- description: "Specific device ID (optional)",
179
+ description: "Simulator UDID. Leave empty for booted simulator.",
312
180
  },
313
181
  },
314
- required: ["button"],
315
182
  },
316
183
  },
317
184
  {
318
- name: "swipe_screen",
319
- description: "[PRO] Swipe on the screen from one point to another. Use for scrolling or gestures.",
185
+ name: "check_metro_status",
186
+ description: "Check if Metro bundler is running and get its status.",
320
187
  inputSchema: {
321
188
  type: "object",
322
189
  properties: {
323
- startX: {
324
- type: "number",
325
- description: "Starting X coordinate",
326
- },
327
- startY: {
328
- type: "number",
329
- description: "Starting Y coordinate",
330
- },
331
- endX: {
332
- type: "number",
333
- description: "Ending X coordinate",
334
- },
335
- endY: {
336
- type: "number",
337
- description: "Ending Y coordinate",
338
- },
339
- duration: {
190
+ port: {
340
191
  type: "number",
341
- description: "Swipe duration in milliseconds (default: 300)",
342
- default: 300,
343
- },
344
- device: {
345
- type: "string",
346
- description: "Specific device ID (optional)",
192
+ description: "Metro port (default: 8081)",
193
+ default: 8081,
347
194
  },
348
195
  },
349
- required: ["startX", "startY", "endX", "endY"],
350
196
  },
351
197
  },
198
+ // === FREE TIER - License (1 tool) ===
352
199
  {
353
- name: "launch_app",
354
- description: "[PRO] Launch an app by its package name.",
355
- inputSchema: {
356
- type: "object",
357
- properties: {
358
- packageName: {
359
- type: "string",
360
- description: "Package name of the app (e.g., 'com.example.myapp')",
361
- },
362
- device: {
363
- type: "string",
364
- description: "Specific device ID (optional)",
365
- },
366
- },
367
- required: ["packageName"],
368
- },
200
+ name: "get_license_status",
201
+ description: "Get your current license tier and available features.",
202
+ inputSchema: { type: "object", properties: {} },
369
203
  },
204
+ // === ADVANCED TIER - UI Inspection (5 tools) ===
370
205
  {
371
- name: "install_apk",
372
- description: "[PRO] Install an APK file to the device.",
206
+ name: "get_ui_tree",
207
+ description: "[ADVANCED] Get the current UI hierarchy from Android device. Returns all visible elements with text, bounds, and properties.",
373
208
  inputSchema: {
374
209
  type: "object",
375
210
  properties: {
376
- apkPath: {
377
- type: "string",
378
- description: "Path to the APK file",
379
- },
380
211
  device: {
381
212
  type: "string",
382
213
  description: "Specific device ID (optional)",
383
214
  },
384
- },
385
- required: ["apkPath"],
386
- },
387
- },
388
- // === iOS SIMULATOR TOOLS ===
389
- {
390
- name: "list_ios_simulators",
391
- description: "List all available iOS Simulators. Shows device name, UDID, state (Booted/Shutdown), and iOS version.",
392
- inputSchema: {
393
- type: "object",
394
- properties: {
395
- onlyBooted: {
215
+ compressed: {
396
216
  type: "boolean",
397
- description: "Only show booted simulators (default: false)",
398
- default: false,
217
+ description: "Return only interactive elements (default: true)",
218
+ default: true,
399
219
  },
400
220
  },
401
221
  },
402
222
  },
403
223
  {
404
- name: "screenshot_ios_simulator",
405
- description: "Capture a screenshot from an iOS Simulator. Returns the screenshot as a base64-encoded image.",
224
+ name: "find_element",
225
+ description: "[ADVANCED] Find a UI element by text, resource ID, or content description.",
406
226
  inputSchema: {
407
227
  type: "object",
408
228
  properties: {
409
- udid: {
410
- type: "string",
411
- description: "Simulator UDID. Leave empty for the booted simulator.",
412
- },
229
+ text: { type: "string", description: "Element text to find" },
230
+ resourceId: { type: "string", description: "Resource ID to find" },
231
+ contentDescription: { type: "string", description: "Accessibility label" },
232
+ device: { type: "string", description: "Specific device ID (optional)" },
413
233
  },
414
234
  },
415
235
  },
416
236
  {
417
- name: "get_ios_simulator_logs",
418
- description: "Get recent logs from an iOS Simulator. Useful for debugging React Native iOS apps.",
237
+ name: "wait_for_element",
238
+ description: "[ADVANCED] Wait for a UI element to appear on screen.",
419
239
  inputSchema: {
420
240
  type: "object",
421
241
  properties: {
422
- udid: {
423
- type: "string",
424
- description: "Simulator UDID. Leave empty for the booted simulator.",
425
- },
426
- filter: {
427
- type: "string",
428
- description: "Filter logs by subsystem or message content",
429
- },
430
- lines: {
242
+ text: { type: "string", description: "Element text to wait for" },
243
+ resourceId: { type: "string", description: "Resource ID to wait for" },
244
+ contentDescription: { type: "string", description: "Accessibility label" },
245
+ timeout: {
431
246
  type: "number",
432
- description: "Number of recent log lines (default: 50)",
433
- default: 50,
434
- },
435
- },
436
- },
437
- },
438
- {
439
- name: "get_ios_simulator_info",
440
- description: "Get detailed information about an iOS Simulator including device type, iOS version, and state.",
441
- inputSchema: {
442
- type: "object",
443
- properties: {
444
- udid: {
445
- type: "string",
446
- description: "Simulator UDID. Leave empty for the booted simulator.",
447
- },
448
- },
449
- },
450
- },
451
- {
452
- name: "boot_ios_simulator",
453
- description: "[PRO] Boot an iOS Simulator by UDID or device name.",
454
- inputSchema: {
455
- type: "object",
456
- properties: {
457
- udid: {
458
- type: "string",
459
- description: "Simulator UDID or device name (e.g., 'iPhone 15 Pro')",
247
+ description: "Maximum wait time in ms (default: 5000)",
248
+ default: 5000,
460
249
  },
250
+ device: { type: "string", description: "Specific device ID (optional)" },
461
251
  },
462
- required: ["udid"],
463
252
  },
464
253
  },
465
254
  {
466
- name: "shutdown_ios_simulator",
467
- description: "[PRO] Shutdown an iOS Simulator.",
255
+ name: "get_element_property",
256
+ description: "[ADVANCED] Get a specific property of a UI element.",
468
257
  inputSchema: {
469
258
  type: "object",
470
259
  properties: {
471
- udid: {
260
+ text: { type: "string", description: "Element text to find" },
261
+ resourceId: { type: "string", description: "Resource ID to find" },
262
+ property: {
472
263
  type: "string",
473
- description: "Simulator UDID. Use 'all' to shutdown all simulators.",
264
+ enum: ["text", "enabled", "checked", "selected", "focused", "clickable", "scrollable"],
265
+ description: "Property to retrieve",
474
266
  },
267
+ device: { type: "string", description: "Specific device ID (optional)" },
475
268
  },
476
- required: ["udid"],
269
+ required: ["property"],
477
270
  },
478
271
  },
479
272
  {
480
- name: "install_ios_app",
481
- description: "[PRO] Install an app (.app bundle) on an iOS Simulator.",
273
+ name: "assert_element",
274
+ description: "[ADVANCED] Verify a UI element exists or has expected state.",
482
275
  inputSchema: {
483
276
  type: "object",
484
277
  properties: {
485
- appPath: {
486
- type: "string",
487
- description: "Path to the .app bundle",
488
- },
489
- udid: {
490
- type: "string",
491
- description: "Simulator UDID. Leave empty for the booted simulator.",
278
+ text: { type: "string", description: "Element text to verify" },
279
+ resourceId: { type: "string", description: "Resource ID to verify" },
280
+ shouldExist: {
281
+ type: "boolean",
282
+ description: "Whether element should exist (default: true)",
283
+ default: true,
492
284
  },
285
+ isEnabled: { type: "boolean", description: "Expected enabled state" },
286
+ isChecked: { type: "boolean", description: "Expected checked state" },
287
+ device: { type: "string", description: "Specific device ID (optional)" },
493
288
  },
494
- required: ["appPath"],
495
289
  },
496
290
  },
291
+ // === ADVANCED TIER - Screen Analysis (3 tools) ===
497
292
  {
498
- name: "launch_ios_app",
499
- description: "[PRO] Launch an app on an iOS Simulator by bundle identifier.",
293
+ name: "suggest_action",
294
+ description: "[ADVANCED] Analyze the screen and suggest what action to take based on the current UI state. Returns suggestions without executing.",
500
295
  inputSchema: {
501
296
  type: "object",
502
297
  properties: {
503
- bundleId: {
504
- type: "string",
505
- description: "App bundle identifier (e.g., 'com.example.myapp')",
506
- },
507
- udid: {
298
+ goal: {
508
299
  type: "string",
509
- description: "Simulator UDID. Leave empty for the booted simulator.",
300
+ description: "What you're trying to accomplish (e.g., 'login', 'send message', 'navigate to settings')",
510
301
  },
302
+ device: { type: "string", description: "Specific device ID" },
511
303
  },
512
- required: ["bundleId"],
304
+ required: ["goal"],
513
305
  },
514
306
  },
515
307
  {
516
- name: "terminate_ios_app",
517
- description: "[PRO] Terminate (force quit) an app on an iOS Simulator.",
308
+ name: "analyze_screen",
309
+ description: "[ADVANCED] Get a detailed analysis of what's currently on the screen.",
518
310
  inputSchema: {
519
311
  type: "object",
520
312
  properties: {
521
- bundleId: {
522
- type: "string",
523
- description: "App bundle identifier to terminate",
524
- },
525
- udid: {
526
- type: "string",
527
- description: "Simulator UDID. Leave empty for the booted simulator.",
528
- },
313
+ device: { type: "string", description: "Specific device ID" },
529
314
  },
530
- required: ["bundleId"],
531
315
  },
532
316
  },
533
317
  {
534
- name: "ios_open_url",
535
- description: "[PRO] Open a URL in the iOS Simulator (deep links, universal links).",
318
+ name: "get_screen_text",
319
+ description: "[ADVANCED] Extract all visible text from the current screen.",
536
320
  inputSchema: {
537
321
  type: "object",
538
322
  properties: {
539
- url: {
540
- type: "string",
541
- description: "URL to open (e.g., 'myapp://screen' or 'https://example.com')",
542
- },
543
- udid: {
544
- type: "string",
545
- description: "Simulator UDID. Leave empty for the booted simulator.",
546
- },
323
+ device: { type: "string", description: "Specific device ID" },
547
324
  },
548
- required: ["url"],
549
325
  },
550
326
  },
327
+ // === ADVANCED TIER - License (1 tool) ===
551
328
  {
552
- name: "ios_push_notification",
553
- description: "[PRO] Send a push notification to an iOS Simulator.",
329
+ name: "set_license_key",
330
+ description: "Activate a license key to unlock premium features.",
554
331
  inputSchema: {
555
332
  type: "object",
556
333
  properties: {
557
- bundleId: {
558
- type: "string",
559
- description: "App bundle identifier",
560
- },
561
- payload: {
562
- type: "object",
563
- description: "Push notification payload (APS format)",
564
- },
565
- udid: {
334
+ licenseKey: {
566
335
  type: "string",
567
- description: "Simulator UDID. Leave empty for the booted simulator.",
336
+ description: "Your license key from mobiledevmcp.dev",
568
337
  },
569
338
  },
570
- required: ["bundleId", "payload"],
339
+ required: ["licenseKey"],
571
340
  },
572
341
  },
573
- {
574
- name: "ios_set_location",
575
- description: "[PRO] Set the simulated GPS location on an iOS Simulator.",
576
- inputSchema: {
577
- type: "object",
578
- properties: {
579
- latitude: {
580
- type: "number",
581
- description: "Latitude coordinate",
582
- },
583
- longitude: {
584
- type: "number",
585
- description: "Longitude coordinate",
586
- },
587
- udid: {
588
- type: "string",
589
- description: "Simulator UDID. Leave empty for the booted simulator.",
590
- },
591
- },
592
- required: ["latitude", "longitude"],
593
- },
594
- },
595
- // === REACT DEVTOOLS TOOLS ===
596
- {
597
- name: "setup_react_devtools",
598
- description: "[PRO] Set up React DevTools connection for debugging. Configures port forwarding and checks connectivity.",
599
- inputSchema: {
600
- type: "object",
601
- properties: {
602
- port: {
603
- type: "number",
604
- description: "DevTools port (default: 8097)",
605
- default: 8097,
606
- },
607
- device: {
608
- type: "string",
609
- description: "Specific Android device ID (optional)",
610
- },
611
- },
612
- },
613
- },
614
- {
615
- name: "check_devtools_connection",
616
- description: "[PRO] Check if React DevTools is connected and get connection status.",
617
- inputSchema: {
618
- type: "object",
619
- properties: {
620
- port: {
621
- type: "number",
622
- description: "DevTools port (default: 8097)",
623
- default: 8097,
624
- },
625
- },
626
- },
627
- },
628
- {
629
- name: "get_react_component_tree",
630
- description: "[PRO] Get the React component hierarchy from connected DevTools. Shows component names and structure.",
631
- inputSchema: {
632
- type: "object",
633
- properties: {
634
- port: {
635
- type: "number",
636
- description: "DevTools port (default: 8097)",
637
- default: 8097,
638
- },
639
- depth: {
640
- type: "number",
641
- description: "Maximum depth to traverse (default: 5)",
642
- default: 5,
643
- },
644
- },
645
- },
646
- },
647
- {
648
- name: "inspect_react_component",
649
- description: "[PRO] Inspect a specific React component by ID. Returns props, state, and hooks.",
650
- inputSchema: {
651
- type: "object",
652
- properties: {
653
- componentId: {
654
- type: "number",
655
- description: "Component ID from the component tree",
656
- },
657
- port: {
658
- type: "number",
659
- description: "DevTools port (default: 8097)",
660
- default: 8097,
661
- },
662
- },
663
- required: ["componentId"],
664
- },
665
- },
666
- {
667
- name: "search_react_components",
668
- description: "[PRO] Search for React components by name or pattern.",
669
- inputSchema: {
670
- type: "object",
671
- properties: {
672
- query: {
673
- type: "string",
674
- description: "Component name or pattern to search for",
675
- },
676
- port: {
677
- type: "number",
678
- description: "DevTools port (default: 8097)",
679
- default: 8097,
680
- },
681
- },
682
- required: ["query"],
683
- },
684
- },
685
- ];
686
- // Combine core tools with license tools
687
- const tools = [...coreTools, ...licenseTools];
688
- // ============================================================================
689
- // TOOL IMPLEMENTATIONS
690
- // ============================================================================
691
- async function getMetroLogs(lines = 50, filter) {
692
- // Check license/trial status
693
- const check = await requireBasic("get_metro_logs");
694
- if (!check.allowed)
695
- return check.message;
696
- const license = await checkLicense();
697
- const tierLimits = TIER_LIMITS[license.tier];
698
- const maxLines = Math.min(lines, tierLimits.maxLogLines);
699
- let logs = metroLogBuffer.slice(-maxLines);
700
- if (filter) {
701
- logs = logs.filter((line) => line.toLowerCase().includes(filter.toLowerCase()));
702
- }
703
- if (logs.length === 0) {
704
- try {
705
- const response = await fetchMetroStatus(CONFIG.metroPort);
706
- let result = `Metro is running but no logs captured yet.\nMetro status: ${response}\n\nTip: Use 'start_metro_logging' with a log file path, or pipe Metro output:\n npx expo start 2>&1 | tee metro.log`;
707
- if (check.message)
708
- result += `\n\n${check.message}`;
709
- return result;
710
- }
711
- catch {
712
- let result = `No Metro logs available. Metro may not be running.\n\nTo capture logs:\n1. Start Metro with output to file: npx expo start 2>&1 | tee metro.log\n2. Use start_metro_logging tool with logFile parameter`;
713
- if (check.message)
714
- result += `\n\n${check.message}`;
715
- return result;
716
- }
717
- }
718
- const header = license.valid
719
- ? `📋 Metro Logs (${logs.length} lines):`
720
- : `📋 Metro Logs (${logs.length} lines, trial mode):`;
721
- let result = `${header}\n${"─".repeat(50)}\n${logs.join("\n")}`;
722
- if (check.message)
723
- result += `\n\n${check.message}`;
724
- return result;
725
- }
726
- async function getAdbLogs(lines = 50, filter = "ReactNativeJS", level = "I") {
727
- // Check license/trial status
728
- const check = await requireBasic("get_adb_logs");
729
- if (!check.allowed)
730
- return check.message;
731
- const license = await checkLicense();
732
- const tierLimits = TIER_LIMITS[license.tier];
733
- const maxLines = Math.min(lines, tierLimits.maxLogLines);
734
- try {
735
- await execAsync("adb version");
736
- let command;
737
- if (filter === "*") {
738
- command = `adb logcat -d -t ${maxLines} *:${level}`;
739
- }
740
- else {
741
- command = `adb logcat -d -t ${maxLines} ${filter}:${level} *:S`;
742
- }
743
- const { stdout, stderr } = await execAsync(command);
744
- if (stderr && !stdout) {
745
- return `ADB Error: ${stderr}`;
746
- }
747
- let result = stdout || "No logs found matching the filter.";
748
- if (check.message)
749
- result += `\n\n${check.message}`;
750
- return result;
751
- }
752
- catch (error) {
753
- if (error.message.includes("not recognized") || error.message.includes("not found")) {
754
- return "ADB is not installed or not in PATH. Please install Android SDK Platform Tools.";
755
- }
756
- if (error.message.includes("no devices")) {
757
- return "No Android devices/emulators connected. Start an emulator or connect a device.";
758
- }
759
- return `Error getting ADB logs: ${error.message}`;
760
- }
761
- }
762
- async function screenshotEmulator(device) {
763
- // Check license/trial status
764
- const check = await requireBasic("screenshot_emulator");
765
- if (!check.allowed) {
766
- return { success: false, error: check.message };
767
- }
768
- try {
769
- const deviceFlag = device ? `-s ${device}` : "";
770
- const screenshotPath = path.join(CONFIG.screenshotDir, `screenshot_${Date.now()}.png`);
771
- // Capture screenshot
772
- await execAsync(`adb ${deviceFlag} exec-out screencap -p > "${screenshotPath}"`);
773
- // Read and convert to base64
774
- const imageBuffer = fs.readFileSync(screenshotPath);
775
- const base64Data = imageBuffer.toString("base64");
776
- // Clean up temp file
777
- fs.unlinkSync(screenshotPath);
778
- // Save to history for Advanced users
779
- const license = await checkLicense();
780
- if (license.valid && license.tier === "advanced") {
781
- screenshotHistory.unshift({
782
- timestamp: new Date().toISOString(),
783
- data: base64Data,
784
- });
785
- if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
786
- screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
787
- }
788
- }
789
- return {
790
- success: true,
791
- data: base64Data,
792
- mimeType: "image/png",
793
- trialMessage: check.message,
794
- };
795
- }
796
- catch (error) {
797
- return {
798
- success: false,
799
- error: `Failed to capture screenshot: ${error.message}`,
800
- };
801
- }
802
- }
803
- async function listDevices() {
804
- // Check license/trial status
805
- const check = await requireBasic("list_devices");
806
- if (!check.allowed)
807
- return check.message;
808
- try {
809
- const { stdout } = await execAsync("adb devices -l");
810
- const lines = stdout.trim().split("\n");
811
- if (lines.length <= 1) {
812
- let result = `No devices connected.\n\nTo connect:\n- Start an Android emulator (Android Studio, Genymotion)\n- Or connect a physical device with USB debugging enabled`;
813
- if (check.message)
814
- result += `\n\n${check.message}`;
815
- return result;
816
- }
817
- let result = stdout;
818
- if (check.message)
819
- result += `\n\n${check.message}`;
820
- return result;
821
- }
822
- catch (error) {
823
- return `Error listing devices: ${error.message}`;
824
- }
825
- }
826
- async function fetchMetroStatus(port) {
827
- return new Promise((resolve, reject) => {
828
- const req = http.get(`http://localhost:${port}/status`, (res) => {
829
- let data = "";
830
- res.on("data", (chunk) => (data += chunk));
831
- res.on("end", () => resolve(data || "Metro is running"));
832
- });
833
- req.on("error", (err) => reject(err));
834
- req.setTimeout(3000, () => {
835
- req.destroy();
836
- reject(new Error("Timeout"));
837
- });
838
- });
839
- }
840
- async function checkMetroStatus(port = 8081) {
841
- // Check license/trial status
842
- const check = await requireBasic("check_metro_status");
843
- if (!check.allowed)
844
- return check.message;
845
- try {
846
- const status = await fetchMetroStatus(port);
847
- let result = `✅ Metro is running on port ${port}\nStatus: ${status}`;
848
- if (check.message)
849
- result += `\n\n${check.message}`;
850
- return result;
851
- }
852
- catch {
853
- let result = `❌ Metro does not appear to be running on port ${port}.\n\nTo start Metro:\n npx expo start\n # or\n npx react-native start`;
854
- if (check.message)
855
- result += `\n\n${check.message}`;
856
- return result;
857
- }
858
- }
859
- async function getAppInfo(packageName) {
860
- // Check license/trial status
861
- const check = await requireBasic("get_app_info");
862
- if (!check.allowed)
863
- return check.message;
864
- try {
865
- const { stdout } = await execAsync(`adb shell dumpsys package ${packageName}`);
866
- const lines = stdout.split("\n");
867
- const relevantInfo = [];
868
- for (const line of lines) {
869
- if (line.includes("versionName") ||
870
- line.includes("versionCode") ||
871
- line.includes("targetSdk") ||
872
- line.includes("dataDir") ||
873
- line.includes("firstInstallTime") ||
874
- line.includes("lastUpdateTime")) {
875
- relevantInfo.push(line.trim());
876
- }
877
- }
878
- if (relevantInfo.length === 0) {
879
- let result = `Package ${packageName} not found on device.`;
880
- if (check.message)
881
- result += `\n\n${check.message}`;
882
- return result;
883
- }
884
- let result = `📱 App Info for ${packageName}:\n${relevantInfo.join("\n")}`;
885
- if (check.message)
886
- result += `\n\n${check.message}`;
887
- return result;
888
- }
889
- catch (error) {
890
- return `Error getting app info: ${error.message}`;
891
- }
892
- }
893
- async function clearAppData(packageName) {
894
- // Check license/trial status
895
- const check = await requireBasic("clear_app_data");
896
- if (!check.allowed)
897
- return check.message;
898
- try {
899
- await execAsync(`adb shell pm clear ${packageName}`);
900
- let result = `✅ Successfully cleared data for ${packageName}`;
901
- if (check.message)
902
- result += `\n\n${check.message}`;
903
- return result;
904
- }
905
- catch (error) {
906
- return `Error clearing app data: ${error.message}`;
907
- }
908
- }
909
- async function restartAdb() {
910
- // Check license/trial status
911
- const check = await requireBasic("restart_adb");
912
- if (!check.allowed)
913
- return check.message;
914
- try {
915
- await execAsync("adb kill-server");
916
- await execAsync("adb start-server");
917
- const { stdout } = await execAsync("adb devices");
918
- let result = `✅ ADB server restarted successfully.\n\nConnected devices:\n${stdout}`;
919
- if (check.message)
920
- result += `\n\n${check.message}`;
921
- return result;
922
- }
923
- catch (error) {
924
- return `Error restarting ADB: ${error.message}`;
925
- }
926
- }
927
- async function getDeviceInfo(device) {
928
- // Check license/trial status
929
- const check = await requireBasic("get_device_info");
930
- if (!check.allowed)
931
- return check.message;
932
- try {
933
- const deviceFlag = device ? `-s ${device}` : "";
934
- const commands = [
935
- `adb ${deviceFlag} shell getprop ro.build.version.release`,
936
- `adb ${deviceFlag} shell getprop ro.build.version.sdk`,
937
- `adb ${deviceFlag} shell getprop ro.product.model`,
938
- `adb ${deviceFlag} shell getprop ro.product.manufacturer`,
939
- `adb ${deviceFlag} shell wm size`,
940
- `adb ${deviceFlag} shell wm density`,
941
- ];
942
- const results = await Promise.all(commands.map((cmd) => execAsync(cmd)
943
- .then(({ stdout }) => stdout.trim())
944
- .catch(() => "N/A")));
945
- let result = `📱 Device Information:
946
- ─────────────────────────────
947
- Android Version: ${results[0]}
948
- SDK Level: ${results[1]}
949
- Model: ${results[3]} ${results[2]}
950
- Screen Size: ${results[4].replace("Physical size: ", "")}
951
- Screen Density: ${results[5].replace("Physical density: ", "")} dpi`;
952
- if (check.message)
953
- result += `\n\n${check.message}`;
954
- return result;
955
- }
956
- catch (error) {
957
- return `Error getting device info: ${error.message}`;
958
- }
959
- }
960
- // ============================================================================
961
- // FIXED: Metro Logging Implementation
962
- // ============================================================================
963
- async function startMetroLogging(logFile) {
964
- // Check license/trial status
965
- const check = await requireBasic("start_metro_logging");
966
- if (!check.allowed)
967
- return check.message;
968
- if (metroProcess) {
969
- let result = "Metro logging is already running. Use 'stop_metro_logging' first.";
970
- if (check.message)
971
- result += `\n\n${check.message}`;
972
- return result;
973
- }
974
- metroLogBuffer = [];
975
- // If a log file is provided, tail it
976
- if (logFile) {
977
- if (!fs.existsSync(logFile)) {
978
- let result = `Log file not found: ${logFile}\n\nCreate it by running:\n npx expo start 2>&1 | tee ${logFile}`;
979
- if (check.message)
980
- result += `\n\n${check.message}`;
981
- return result;
982
- }
983
- // Use PowerShell's Get-Content -Wait on Windows, tail -f on Unix
984
- const isWindows = process.platform === "win32";
985
- if (isWindows) {
986
- metroProcess = spawn("powershell", [
987
- "-Command",
988
- `Get-Content -Path "${logFile}" -Wait -Tail 100`,
989
- ]);
990
- }
991
- else {
992
- metroProcess = spawn("tail", ["-f", "-n", "100", logFile]);
993
- }
994
- metroProcess.stdout?.on("data", (data) => {
995
- const lines = data.toString().split("\n").filter((l) => l.trim());
996
- metroLogBuffer.push(...lines);
997
- if (metroLogBuffer.length > CONFIG.logBufferSize) {
998
- metroLogBuffer = metroLogBuffer.slice(-CONFIG.logBufferSize);
999
- }
1000
- });
1001
- metroProcess.stderr?.on("data", (data) => {
1002
- metroLogBuffer.push(`[STDERR] ${data.toString().trim()}`);
1003
- });
1004
- metroProcess.on("error", (err) => {
1005
- metroLogBuffer.push(`[ERROR] ${err.message}`);
1006
- });
1007
- let result = `✅ Metro log capture started!\nWatching: ${logFile}\n\nUse 'get_metro_logs' to retrieve captured logs.`;
1008
- if (check.message)
1009
- result += `\n\n${check.message}`;
1010
- return result;
1011
- }
1012
- // No log file provided - give instructions
1013
- let result = `📋 Metro Log Capture Setup
1014
- ─────────────────────────────
1015
-
1016
- To capture Metro logs, you have two options:
1017
-
1018
- Option 1: Pipe Metro to a file (Recommended)
1019
- npx expo start 2>&1 | tee metro.log
1020
-
1021
- Then run: start_metro_logging with logFile="metro.log"
1022
-
1023
- Option 2: Check common log locations
1024
- - Expo: .expo/logs/
1025
- - React Native: Check Metro terminal output
1026
-
1027
- Option 3: Use ADB logs instead
1028
- For device-side JavaScript logs, use 'get_adb_logs'
1029
-
1030
- ─────────────────────────────
1031
- Once you have a log file, call this tool again with the logFile parameter.`;
1032
- if (check.message)
1033
- result += `\n\n${check.message}`;
1034
- return result;
1035
- }
1036
- async function stopMetroLogging() {
1037
- // Check license/trial status
1038
- const check = await requireBasic("stop_metro_logging");
1039
- if (!check.allowed)
1040
- return check.message;
1041
- if (metroProcess) {
1042
- metroProcess.kill();
1043
- metroProcess = null;
1044
- }
1045
- const logCount = metroLogBuffer.length;
1046
- let result = `✅ Metro logging stopped. ${logCount} log lines were captured.`;
1047
- if (check.message)
1048
- result += `\n\n${check.message}`;
1049
- return result;
1050
- }
1051
- // ============================================================================
1052
- // PRO FEATURE IMPLEMENTATIONS
1053
- // ============================================================================
1054
- async function streamAdbRealtime(filter = "ReactNativeJS") {
1055
- const check = await requireAdvanced("stream_adb_realtime");
1056
- if (!check.allowed)
1057
- return check.message;
1058
- if (adbLogProcess) {
1059
- return "ADB streaming is already running. Use 'stop_adb_streaming' first.";
1060
- }
1061
- adbLogBuffer = [];
1062
- adbLogProcess = spawn("adb", ["logcat", `${filter}:V`, "*:S"]);
1063
- adbLogProcess.stdout?.on("data", (data) => {
1064
- const lines = data.toString().split("\n").filter((l) => l.trim());
1065
- adbLogBuffer.push(...lines);
1066
- if (adbLogBuffer.length > 500) {
1067
- adbLogBuffer = adbLogBuffer.slice(-500);
1068
- }
1069
- });
1070
- adbLogProcess.on("error", (err) => {
1071
- adbLogBuffer.push(`[ERROR] ${err.message}`);
1072
- });
1073
- return `✅ [PRO] Real-time ADB streaming started!\nFilter: ${filter}\n\nUse 'get_adb_logs' to retrieve the live buffer.`;
1074
- }
1075
- function stopAdbStreaming() {
1076
- if (adbLogProcess) {
1077
- adbLogProcess.kill();
1078
- adbLogProcess = null;
1079
- }
1080
- return `✅ ADB streaming stopped. Buffer contained ${adbLogBuffer.length} lines.`;
1081
- }
1082
- async function getScreenshotHistory(count = 5) {
1083
- const check = await requireAdvanced("screenshot_history");
1084
- if (!check.allowed)
1085
- return check.message;
1086
- if (screenshotHistory.length === 0) {
1087
- return "No screenshots in history. Take screenshots using 'screenshot_emulator' first.";
1088
- }
1089
- const recent = screenshotHistory.slice(0, count);
1090
- return `📸 [PRO] Screenshot History (${recent.length} of ${screenshotHistory.length}):\n\n${recent
1091
- .map((s, i) => `${i + 1}. ${s.timestamp}`)
1092
- .join("\n")}\n\nNote: Full image data available in tool response.`;
1093
- }
1094
- async function watchForErrors(patterns = ["Error", "Exception"], timeout = 60) {
1095
- const check = await requireAdvanced("watch_for_errors");
1096
- if (!check.allowed)
1097
- return check.message;
1098
- return new Promise((resolve) => {
1099
- const startTime = Date.now();
1100
- const checkInterval = setInterval(async () => {
1101
- // Check ADB logs for patterns
1102
- try {
1103
- const { stdout } = await execAsync("adb logcat -d -t 50 *:E");
1104
- for (const pattern of patterns) {
1105
- if (stdout.toLowerCase().includes(pattern.toLowerCase())) {
1106
- clearInterval(checkInterval);
1107
- resolve(`🚨 [PRO] Error detected!\nPattern: "${pattern}"\n\nRelevant logs:\n${stdout}`);
1108
- return;
1109
- }
1110
- }
1111
- }
1112
- catch { }
1113
- // Check timeout
1114
- if (Date.now() - startTime > timeout * 1000) {
1115
- clearInterval(checkInterval);
1116
- resolve(`✅ [PRO] No errors detected in ${timeout} seconds.`);
1117
- }
1118
- }, 2000);
1119
- });
1120
- }
1121
- async function multiDeviceLogs(devices, lines = 30) {
1122
- const check = await requireAdvanced("multi_device_logs");
1123
- if (!check.allowed)
1124
- return check.message;
1125
- if (!devices || devices.length === 0) {
1126
- const { stdout } = await execAsync("adb devices");
1127
- return `No devices specified. Available devices:\n${stdout}`;
1128
- }
1129
- const results = await Promise.all(devices.map(async (device) => {
1130
- try {
1131
- const { stdout } = await execAsync(`adb -s ${device} logcat -d -t ${lines} ReactNativeJS:V *:S`);
1132
- return `📱 Device: ${device}\n${"─".repeat(30)}\n${stdout}`;
1133
- }
1134
- catch (error) {
1135
- return `📱 Device: ${device}\n${"─".repeat(30)}\nError: ${error.message}`;
1136
- }
1137
- }));
1138
- return `📋 [PRO] Multi-Device Logs\n${"═".repeat(50)}\n\n${results.join("\n\n")}`;
1139
- }
1140
- // ============================================================================
1141
- // INTERACTION TOOLS (Advanced)
1142
- // ============================================================================
1143
- async function tapScreen(x, y, device) {
1144
- const check = await requireAdvanced("tap_screen");
1145
- if (!check.allowed)
1146
- return check.message;
1147
- try {
1148
- const deviceFlag = device ? `-s ${device}` : "";
1149
- await execAsync(`adb ${deviceFlag} shell input tap ${x} ${y}`);
1150
- let result = `✅ Tapped at (${x}, ${y})`;
1151
- if (check.message)
1152
- result += `\n\n${check.message}`;
1153
- return result;
1154
- }
1155
- catch (error) {
1156
- return `Error tapping screen: ${error.message}`;
1157
- }
1158
- }
1159
- async function inputText(text, device) {
1160
- const check = await requireAdvanced("input_text");
1161
- if (!check.allowed)
1162
- return check.message;
1163
- try {
1164
- const deviceFlag = device ? `-s ${device}` : "";
1165
- // Escape special characters for shell
1166
- const escapedText = text.replace(/([\\'"$ `])/g, "\\$1").replace(/ /g, "%s");
1167
- await execAsync(`adb ${deviceFlag} shell input text "${escapedText}"`);
1168
- let result = `✅ Typed: "${text}"`;
1169
- if (check.message)
1170
- result += `\n\n${check.message}`;
1171
- return result;
1172
- }
1173
- catch (error) {
1174
- return `Error inputting text: ${error.message}`;
1175
- }
1176
- }
1177
- async function pressButton(button, device) {
1178
- const check = await requireAdvanced("press_button");
1179
- if (!check.allowed)
1180
- return check.message;
1181
- const keyMap = {
1182
- back: 4,
1183
- home: 3,
1184
- recent: 187,
1185
- volume_up: 24,
1186
- volume_down: 25,
1187
- power: 26,
1188
- enter: 66,
1189
- };
1190
- const keyCode = keyMap[button];
1191
- if (!keyCode) {
1192
- return `Unknown button: ${button}. Available: ${Object.keys(keyMap).join(", ")}`;
1193
- }
1194
- try {
1195
- const deviceFlag = device ? `-s ${device}` : "";
1196
- await execAsync(`adb ${deviceFlag} shell input keyevent ${keyCode}`);
1197
- let result = `✅ Pressed: ${button.toUpperCase()}`;
1198
- if (check.message)
1199
- result += `\n\n${check.message}`;
1200
- return result;
1201
- }
1202
- catch (error) {
1203
- return `Error pressing button: ${error.message}`;
1204
- }
1205
- }
1206
- async function swipeScreen(startX, startY, endX, endY, duration = 300, device) {
1207
- const check = await requireAdvanced("swipe_screen");
1208
- if (!check.allowed)
1209
- return check.message;
1210
- try {
1211
- const deviceFlag = device ? `-s ${device}` : "";
1212
- await execAsync(`adb ${deviceFlag} shell input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`);
1213
- let result = `✅ Swiped from (${startX}, ${startY}) to (${endX}, ${endY})`;
1214
- if (check.message)
1215
- result += `\n\n${check.message}`;
1216
- return result;
1217
- }
1218
- catch (error) {
1219
- return `Error swiping: ${error.message}`;
1220
- }
1221
- }
1222
- async function launchApp(packageName, device) {
1223
- const check = await requireAdvanced("launch_app");
1224
- if (!check.allowed)
1225
- return check.message;
1226
- try {
1227
- const deviceFlag = device ? `-s ${device}` : "";
1228
- // Get the main activity using monkey
1229
- await execAsync(`adb ${deviceFlag} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`);
1230
- let result = `✅ Launched: ${packageName}`;
1231
- if (check.message)
1232
- result += `\n\n${check.message}`;
1233
- return result;
1234
- }
1235
- catch (error) {
1236
- return `Error launching app: ${error.message}\n\nMake sure the package name is correct.`;
1237
- }
1238
- }
1239
- async function installApk(apkPath, device) {
1240
- const check = await requireAdvanced("install_apk");
1241
- if (!check.allowed)
1242
- return check.message;
1243
- if (!fs.existsSync(apkPath)) {
1244
- return `APK file not found: ${apkPath}`;
1245
- }
1246
- try {
1247
- const deviceFlag = device ? `-s ${device}` : "";
1248
- const { stdout } = await execAsync(`adb ${deviceFlag} install -r "${apkPath}"`);
1249
- let result = `✅ APK installed successfully!\n\n${stdout}`;
1250
- if (check.message)
1251
- result += `\n\n${check.message}`;
1252
- return result;
1253
- }
1254
- catch (error) {
1255
- return `Error installing APK: ${error.message}`;
1256
- }
1257
- }
1258
- async function checkXcodeInstalled() {
1259
- try {
1260
- await execAsync("xcrun simctl help");
1261
- return true;
1262
- }
1263
- catch {
1264
- return false;
1265
- }
1266
- }
1267
- async function listIosSimulators(onlyBooted = false) {
1268
- const check = await requireBasic("list_ios_simulators");
1269
- if (!check.allowed)
1270
- return check.message;
1271
- if (process.platform !== "darwin") {
1272
- return "iOS Simulators are only available on macOS.";
1273
- }
1274
- try {
1275
- if (!(await checkXcodeInstalled())) {
1276
- return "Xcode Command Line Tools not installed. Run: xcode-select --install";
1277
- }
1278
- const { stdout } = await execAsync("xcrun simctl list devices -j");
1279
- const data = JSON.parse(stdout);
1280
- const results = [];
1281
- results.push("📱 iOS Simulators\n" + "═".repeat(50));
1282
- for (const [runtime, devices] of Object.entries(data.devices)) {
1283
- const deviceList = devices;
1284
- const filteredDevices = onlyBooted
1285
- ? deviceList.filter((d) => d.state === "Booted")
1286
- : deviceList.filter((d) => d.isAvailable);
1287
- if (filteredDevices.length > 0) {
1288
- // Extract iOS version from runtime identifier
1289
- const runtimeName = runtime.split(".").pop()?.replace(/-/g, " ") || runtime;
1290
- results.push(`\n${runtimeName}:`);
1291
- for (const device of filteredDevices) {
1292
- const status = device.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown";
1293
- results.push(` ${status} ${device.name}`);
1294
- results.push(` UDID: ${device.udid}`);
1295
- }
1296
- }
1297
- }
1298
- if (results.length === 1) {
1299
- let result = onlyBooted
1300
- ? "No booted simulators found. Boot one with 'boot_ios_simulator'."
1301
- : "No iOS Simulators available. Open Xcode to download simulator runtimes.";
1302
- if (check.message)
1303
- result += `\n\n${check.message}`;
1304
- return result;
1305
- }
1306
- let result = results.join("\n");
1307
- if (check.message)
1308
- result += `\n\n${check.message}`;
1309
- return result;
1310
- }
1311
- catch (error) {
1312
- return `Error listing iOS Simulators: ${error.message}`;
1313
- }
1314
- }
1315
- async function screenshotIosSimulator(udid) {
1316
- const check = await requireBasic("screenshot_ios_simulator");
1317
- if (!check.allowed) {
1318
- return { success: false, error: check.message };
1319
- }
1320
- if (process.platform !== "darwin") {
1321
- return { success: false, error: "iOS Simulators are only available on macOS." };
1322
- }
1323
- try {
1324
- const target = udid || "booted";
1325
- const screenshotPath = path.join(CONFIG.screenshotDir, `ios_screenshot_${Date.now()}.png`);
1326
- await execAsync(`xcrun simctl io ${target} screenshot "${screenshotPath}"`);
1327
- const imageBuffer = fs.readFileSync(screenshotPath);
1328
- const base64Data = imageBuffer.toString("base64");
1329
- fs.unlinkSync(screenshotPath);
1330
- // Save to history for Advanced users
1331
- const license = await checkLicense();
1332
- if (license.valid && license.tier === "advanced") {
1333
- screenshotHistory.unshift({
1334
- timestamp: new Date().toISOString(),
1335
- data: base64Data,
1336
- });
1337
- if (screenshotHistory.length > MAX_SCREENSHOT_HISTORY) {
1338
- screenshotHistory = screenshotHistory.slice(0, MAX_SCREENSHOT_HISTORY);
1339
- }
1340
- }
1341
- return {
1342
- success: true,
1343
- data: base64Data,
1344
- mimeType: "image/png",
1345
- trialMessage: check.message,
1346
- };
1347
- }
1348
- catch (error) {
1349
- if (error.message.includes("No devices are booted")) {
1350
- return { success: false, error: "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'." };
1351
- }
1352
- return { success: false, error: `Failed to capture iOS screenshot: ${error.message}` };
1353
- }
1354
- }
1355
- async function getIosSimulatorLogs(udid, filter, lines = 50) {
1356
- const check = await requireBasic("get_ios_simulator_logs");
1357
- if (!check.allowed)
1358
- return check.message;
1359
- if (process.platform !== "darwin") {
1360
- return "iOS Simulators are only available on macOS.";
1361
- }
1362
- const license = await checkLicense();
1363
- const tierLimits = TIER_LIMITS[license.tier];
1364
- const maxLines = Math.min(lines, tierLimits.maxLogLines);
1365
- try {
1366
- const target = udid || "booted";
1367
- // Use predicate filter if provided
1368
- let command = `xcrun simctl spawn ${target} log show --last 5m --style compact`;
1369
- if (filter) {
1370
- command += ` --predicate 'eventMessage CONTAINS "${filter}" OR subsystem CONTAINS "${filter}"'`;
1371
- }
1372
- const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 });
1373
- if (stderr && !stdout) {
1374
- return `Error: ${stderr}`;
1375
- }
1376
- const logLines = stdout.split("\n").slice(-maxLines);
1377
- let result = `📋 iOS Simulator Logs (${logLines.length} lines):\n${"─".repeat(50)}\n${logLines.join("\n")}`;
1378
- if (check.message)
1379
- result += `\n\n${check.message}`;
1380
- return result;
1381
- }
1382
- catch (error) {
1383
- if (error.message.includes("No devices are booted")) {
1384
- return "No iOS Simulator is booted. Boot one first with 'boot_ios_simulator'.";
1385
- }
1386
- return `Error getting iOS logs: ${error.message}`;
1387
- }
1388
- }
1389
- async function getIosSimulatorInfo(udid) {
1390
- const check = await requireBasic("get_ios_simulator_info");
1391
- if (!check.allowed)
1392
- return check.message;
1393
- if (process.platform !== "darwin") {
1394
- return "iOS Simulators are only available on macOS.";
1395
- }
1396
- try {
1397
- const { stdout } = await execAsync("xcrun simctl list devices -j");
1398
- const data = JSON.parse(stdout);
1399
- // Find the target device
1400
- let targetDevice = null;
1401
- let targetRuntime = "";
1402
- for (const [runtime, devices] of Object.entries(data.devices)) {
1403
- const deviceList = devices;
1404
- const found = deviceList.find((d) => udid ? d.udid === udid : d.state === "Booted");
1405
- if (found) {
1406
- targetDevice = found;
1407
- targetRuntime = runtime;
1408
- break;
1409
- }
1410
- }
1411
- if (!targetDevice) {
1412
- return udid
1413
- ? `Simulator with UDID ${udid} not found.`
1414
- : "No booted simulator found. Boot one first or specify a UDID.";
1415
- }
1416
- const runtimeName = targetRuntime.split(".").pop()?.replace(/-/g, " ") || targetRuntime;
1417
- let result = `📱 iOS Simulator Info
1418
- ${"─".repeat(40)}
1419
- Name: ${targetDevice.name}
1420
- UDID: ${targetDevice.udid}
1421
- State: ${targetDevice.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown"}
1422
- Runtime: ${runtimeName}
1423
- Available: ${targetDevice.isAvailable ? "Yes" : "No"}`;
1424
- if (check.message)
1425
- result += `\n\n${check.message}`;
1426
- return result;
1427
- }
1428
- catch (error) {
1429
- return `Error getting simulator info: ${error.message}`;
1430
- }
1431
- }
1432
- async function bootIosSimulator(udid) {
1433
- const check = await requireAdvanced("boot_ios_simulator");
1434
- if (!check.allowed)
1435
- return check.message;
1436
- if (process.platform !== "darwin") {
1437
- return "iOS Simulators are only available on macOS.";
1438
- }
1439
- try {
1440
- await execAsync(`xcrun simctl boot "${udid}"`);
1441
- let result = `✅ iOS Simulator booted: ${udid}\n\nOpening Simulator app...`;
1442
- // Open Simulator app to show the booted device
1443
- try {
1444
- await execAsync("open -a Simulator");
1445
- }
1446
- catch { }
1447
- if (check.message)
1448
- result += `\n\n${check.message}`;
1449
- return result;
1450
- }
1451
- catch (error) {
1452
- if (error.message.includes("Unable to boot device in current state: Booted")) {
1453
- return "Simulator is already booted.";
1454
- }
1455
- return `Error booting simulator: ${error.message}`;
1456
- }
1457
- }
1458
- async function shutdownIosSimulator(udid) {
1459
- const check = await requireAdvanced("shutdown_ios_simulator");
1460
- if (!check.allowed)
1461
- return check.message;
1462
- if (process.platform !== "darwin") {
1463
- return "iOS Simulators are only available on macOS.";
1464
- }
1465
- try {
1466
- if (udid.toLowerCase() === "all") {
1467
- await execAsync("xcrun simctl shutdown all");
1468
- let result = "✅ All iOS Simulators have been shut down.";
1469
- if (check.message)
1470
- result += `\n\n${check.message}`;
1471
- return result;
1472
- }
1473
- await execAsync(`xcrun simctl shutdown "${udid}"`);
1474
- let result = `✅ iOS Simulator shut down: ${udid}`;
1475
- if (check.message)
1476
- result += `\n\n${check.message}`;
1477
- return result;
1478
- }
1479
- catch (error) {
1480
- if (error.message.includes("Unable to shutdown device in current state: Shutdown")) {
1481
- return "Simulator is already shut down.";
1482
- }
1483
- return `Error shutting down simulator: ${error.message}`;
1484
- }
1485
- }
1486
- async function installIosApp(appPath, udid) {
1487
- const check = await requireAdvanced("install_ios_app");
1488
- if (!check.allowed)
1489
- return check.message;
1490
- if (process.platform !== "darwin") {
1491
- return "iOS Simulators are only available on macOS.";
1492
- }
1493
- if (!fs.existsSync(appPath)) {
1494
- return `App not found: ${appPath}`;
1495
- }
1496
- try {
1497
- const target = udid || "booted";
1498
- await execAsync(`xcrun simctl install ${target} "${appPath}"`);
1499
- let result = `✅ App installed successfully on iOS Simulator!`;
1500
- if (check.message)
1501
- result += `\n\n${check.message}`;
1502
- return result;
1503
- }
1504
- catch (error) {
1505
- return `Error installing app: ${error.message}`;
1506
- }
1507
- }
1508
- async function launchIosApp(bundleId, udid) {
1509
- const check = await requireAdvanced("launch_ios_app");
1510
- if (!check.allowed)
1511
- return check.message;
1512
- if (process.platform !== "darwin") {
1513
- return "iOS Simulators are only available on macOS.";
1514
- }
1515
- try {
1516
- const target = udid || "booted";
1517
- await execAsync(`xcrun simctl launch ${target} "${bundleId}"`);
1518
- let result = `✅ Launched: ${bundleId}`;
1519
- if (check.message)
1520
- result += `\n\n${check.message}`;
1521
- return result;
1522
- }
1523
- catch (error) {
1524
- return `Error launching app: ${error.message}\n\nMake sure the bundle ID is correct and the app is installed.`;
1525
- }
1526
- }
1527
- async function terminateIosApp(bundleId, udid) {
1528
- const check = await requireAdvanced("terminate_ios_app");
1529
- if (!check.allowed)
1530
- return check.message;
1531
- if (process.platform !== "darwin") {
1532
- return "iOS Simulators are only available on macOS.";
1533
- }
1534
- try {
1535
- const target = udid || "booted";
1536
- await execAsync(`xcrun simctl terminate ${target} "${bundleId}"`);
1537
- let result = `✅ Terminated: ${bundleId}`;
1538
- if (check.message)
1539
- result += `\n\n${check.message}`;
1540
- return result;
1541
- }
1542
- catch (error) {
1543
- return `Error terminating app: ${error.message}`;
1544
- }
1545
- }
1546
- async function iosOpenUrl(url, udid) {
1547
- const check = await requireAdvanced("ios_open_url");
1548
- if (!check.allowed)
1549
- return check.message;
1550
- if (process.platform !== "darwin") {
1551
- return "iOS Simulators are only available on macOS.";
1552
- }
1553
- try {
1554
- const target = udid || "booted";
1555
- await execAsync(`xcrun simctl openurl ${target} "${url}"`);
1556
- let result = `✅ Opened URL: ${url}`;
1557
- if (check.message)
1558
- result += `\n\n${check.message}`;
1559
- return result;
1560
- }
1561
- catch (error) {
1562
- return `Error opening URL: ${error.message}`;
1563
- }
1564
- }
1565
- async function iosPushNotification(bundleId, payload, udid) {
1566
- const check = await requireAdvanced("ios_push_notification");
1567
- if (!check.allowed)
1568
- return check.message;
1569
- if (process.platform !== "darwin") {
1570
- return "iOS Simulators are only available on macOS.";
1571
- }
1572
- try {
1573
- const target = udid || "booted";
1574
- const payloadPath = path.join(CONFIG.screenshotDir, `push_${Date.now()}.json`);
1575
- // Write payload to temp file
1576
- fs.writeFileSync(payloadPath, JSON.stringify(payload));
1577
- await execAsync(`xcrun simctl push ${target} "${bundleId}" "${payloadPath}"`);
1578
- // Clean up
1579
- fs.unlinkSync(payloadPath);
1580
- let result = `✅ Push notification sent to ${bundleId}`;
1581
- if (check.message)
1582
- result += `\n\n${check.message}`;
1583
- return result;
1584
- }
1585
- catch (error) {
1586
- return `Error sending push notification: ${error.message}`;
1587
- }
1588
- }
1589
- async function iosSetLocation(latitude, longitude, udid) {
1590
- const check = await requireAdvanced("ios_set_location");
1591
- if (!check.allowed)
1592
- return check.message;
1593
- if (process.platform !== "darwin") {
1594
- return "iOS Simulators are only available on macOS.";
1595
- }
1596
- try {
1597
- const target = udid || "booted";
1598
- await execAsync(`xcrun simctl location ${target} set ${latitude},${longitude}`);
1599
- let result = `✅ Location set to: ${latitude}, ${longitude}`;
1600
- if (check.message)
1601
- result += `\n\n${check.message}`;
1602
- return result;
1603
- }
1604
- catch (error) {
1605
- return `Error setting location: ${error.message}`;
1606
- }
1607
- }
342
+ ];
1608
343
  // ============================================================================
1609
- // REACT DEVTOOLS INTEGRATION
344
+ // TOOL IMPLEMENTATIONS
1610
345
  // ============================================================================
1611
- // Store for DevTools WebSocket connection
1612
- let devToolsWs = null;
1613
- let devToolsConnected = false;
1614
- let componentTree = new Map();
1615
- let pendingRequests = new Map();
1616
- let requestId = 0;
1617
- async function setupReactDevTools(port = 8097, device) {
1618
- const check = await requireAdvanced("setup_react_devtools");
1619
- if (!check.allowed)
1620
- return check.message;
1621
- const results = [];
1622
- results.push("🔧 React DevTools Setup\n" + "═".repeat(50));
1623
- // Step 1: Set up ADB port forwarding (for Android)
1624
- try {
1625
- const deviceFlag = device ? `-s ${device}` : "";
1626
- await execAsync(`adb ${deviceFlag} reverse tcp:${port} tcp:${port}`);
1627
- results.push(`\n✅ ADB port forwarding configured: tcp:${port} -> tcp:${port}`);
1628
- }
1629
- catch (error) {
1630
- results.push(`\n⚠️ ADB port forwarding failed: ${error.message}`);
1631
- results.push(" (This is OK if using iOS Simulator or DevTools is on same machine)");
1632
- }
1633
- // Step 2: Check if DevTools server is available
1634
- try {
1635
- const isRunning = await checkDevToolsServer(port);
1636
- if (isRunning) {
1637
- results.push(`✅ React DevTools server detected on port ${port}`);
1638
- }
1639
- else {
1640
- results.push(`⚠️ React DevTools server not detected on port ${port}`);
1641
- results.push("\nTo start React DevTools:");
1642
- results.push(" npx react-devtools");
1643
- results.push(" # or install globally:");
1644
- results.push(" npm install -g react-devtools && react-devtools");
1645
- }
1646
- }
1647
- catch (error) {
1648
- results.push(`⚠️ Could not check DevTools server: ${error.message}`);
346
+ async function handleTool(name, args, tier) {
347
+ // Check tool access
348
+ if (!canAccessTool(name, tier)) {
349
+ return {
350
+ content: [{
351
+ type: "text",
352
+ text: `This tool requires ADVANCED tier ($18/mo). Your tier: ${tier.toUpperCase()}. Upgrade at https://mobiledevmcp.dev/pricing`,
353
+ }],
354
+ };
1649
355
  }
1650
- // Step 3: Connection instructions
1651
- results.push("\n📋 Next Steps:");
1652
- results.push("1. Make sure React DevTools standalone is running (npx react-devtools)");
1653
- results.push("2. Your React Native app should connect automatically in dev mode");
1654
- results.push("3. Use 'check_devtools_connection' to verify the connection");
1655
- results.push("4. Use 'get_react_component_tree' to inspect components");
1656
- let result = results.join("\n");
1657
- if (check.message)
1658
- result += `\n\n${check.message}`;
1659
- return result;
1660
- }
1661
- async function checkDevToolsServer(port) {
1662
- return new Promise((resolve) => {
1663
- const ws = new WebSocket(`ws://localhost:${port}`);
1664
- const timeout = setTimeout(() => {
1665
- ws.close();
1666
- resolve(false);
1667
- }, 3000);
1668
- ws.on("open", () => {
1669
- clearTimeout(timeout);
1670
- ws.close();
1671
- resolve(true);
1672
- });
1673
- ws.on("error", () => {
1674
- clearTimeout(timeout);
1675
- resolve(false);
1676
- });
1677
- });
1678
- }
1679
- async function checkDevToolsConnection(port = 8097) {
1680
- const check = await requireAdvanced("check_devtools_connection");
1681
- if (!check.allowed)
1682
- return check.message;
1683
- const results = [];
1684
- results.push("🔍 React DevTools Connection Status\n" + "═".repeat(50));
1685
- // Check if server is running
1686
- const serverRunning = await checkDevToolsServer(port);
1687
- if (!serverRunning) {
1688
- results.push(`\n❌ DevTools server not found on port ${port}`);
1689
- results.push("\nTo start React DevTools:");
1690
- results.push(" npx react-devtools");
1691
- let result = results.join("\n");
1692
- if (check.message)
1693
- result += `\n\n${check.message}`;
1694
- return result;
356
+ const device = args.device;
357
+ // Validate device ID if provided (security: prevent shell injection)
358
+ if (device && !validateDeviceId(device)) {
359
+ return {
360
+ content: [{ type: "text", text: "Invalid device ID format. Device IDs should be alphanumeric with dashes, colons, or periods." }],
361
+ };
1695
362
  }
1696
- results.push(`\n✅ DevTools server running on port ${port}`);
1697
- // Try to connect and get basic info
1698
- try {
1699
- const connection = await connectToDevTools(port);
1700
- if (connection.connected) {
1701
- results.push("✅ Successfully connected to DevTools");
1702
- if (connection.rendererCount > 0) {
1703
- results.push(`✅ ${connection.rendererCount} React renderer(s) connected`);
1704
- results.push("\n📱 App is connected and ready for inspection!");
1705
- results.push(" Use 'get_react_component_tree' to view components");
363
+ const deviceArg = device ? `-s ${device}` : "";
364
+ switch (name) {
365
+ // === SCREENSHOTS ===
366
+ case "screenshot_emulator": {
367
+ try {
368
+ const base64 = await captureAndroidScreenshot(device);
369
+ return {
370
+ content: [
371
+ { type: "text", text: "Screenshot captured successfully" },
372
+ { type: "image", data: base64, mimeType: "image/png" },
373
+ ],
374
+ };
1706
375
  }
1707
- else {
1708
- results.push("⚠️ No React renderers connected");
1709
- results.push("\nMake sure your React Native app is running in development mode.");
376
+ catch (error) {
377
+ return { content: [{ type: "text", text: `Failed to capture screenshot: ${error.message}` }] };
1710
378
  }
1711
379
  }
1712
- else {
1713
- results.push("⚠️ Connected to server but no app detected");
1714
- }
1715
- }
1716
- catch (error) {
1717
- results.push(`⚠️ Connection test failed: ${error.message}`);
1718
- }
1719
- let result = results.join("\n");
1720
- if (check.message)
1721
- result += `\n\n${check.message}`;
1722
- return result;
1723
- }
1724
- async function connectToDevTools(port) {
1725
- return new Promise((resolve, reject) => {
1726
- const ws = new WebSocket(`ws://localhost:${port}`);
1727
- let rendererCount = 0;
1728
- let resolved = false;
1729
- const timeout = setTimeout(() => {
1730
- if (!resolved) {
1731
- resolved = true;
1732
- ws.close();
1733
- resolve({ connected: true, rendererCount: 0 });
380
+ case "screenshot_ios_simulator": {
381
+ // Validate UDID if provided (defense in depth - also validated in captureIosScreenshot)
382
+ const iosUdid = args.udid;
383
+ if (iosUdid && !validateUdid(iosUdid)) {
384
+ return {
385
+ content: [{ type: "text", text: "Invalid iOS Simulator UDID format. Must be UUID format or 'booted'." }],
386
+ };
1734
387
  }
1735
- }, 5000);
1736
- ws.on("open", () => {
1737
- // DevTools protocol: request operations
1738
- // The standalone DevTools uses a different protocol than we might expect
1739
- // For now, we'll just confirm connection
1740
- });
1741
- ws.on("message", (data) => {
1742
388
  try {
1743
- const message = JSON.parse(data.toString());
1744
- if (message.event === "operations" || message.event === "roots") {
1745
- rendererCount++;
1746
- }
1747
- }
1748
- catch { }
1749
- });
1750
- ws.on("close", () => {
1751
- if (!resolved) {
1752
- resolved = true;
1753
- clearTimeout(timeout);
1754
- resolve({ connected: true, rendererCount });
1755
- }
1756
- });
1757
- ws.on("error", (err) => {
1758
- if (!resolved) {
1759
- resolved = true;
1760
- clearTimeout(timeout);
1761
- reject(err);
1762
- }
1763
- });
1764
- // Give it time to receive messages
1765
- setTimeout(() => {
1766
- if (!resolved) {
1767
- resolved = true;
1768
- clearTimeout(timeout);
1769
- ws.close();
1770
- resolve({ connected: true, rendererCount });
389
+ const base64 = await captureIosScreenshot(iosUdid);
390
+ return {
391
+ content: [
392
+ { type: "text", text: "iOS screenshot captured successfully" },
393
+ { type: "image", data: base64, mimeType: "image/png" },
394
+ ],
395
+ };
1771
396
  }
1772
- }, 2000);
1773
- });
1774
- }
1775
- async function getReactComponentTree(port = 8097, depth = 5) {
1776
- const check = await requireAdvanced("get_react_component_tree");
1777
- if (!check.allowed)
1778
- return check.message;
1779
- try {
1780
- const tree = await fetchComponentTree(port, depth);
1781
- if (!tree || tree.length === 0) {
1782
- let result = `📊 React Component Tree\n${"═".repeat(50)}\n\nNo components found.\n\nMake sure:\n1. React DevTools standalone is running (npx react-devtools)\n2. Your React Native app is running in development mode\n3. The app is connected to DevTools`;
1783
- if (check.message)
1784
- result += `\n\n${check.message}`;
1785
- return result;
1786
- }
1787
- const lines = [];
1788
- lines.push("📊 React Component Tree");
1789
- lines.push("═".repeat(50));
1790
- lines.push("");
1791
- for (const node of tree) {
1792
- const indent = " ".repeat(node.depth);
1793
- const typeIcon = node.type === "function" ? "ƒ" : node.type === "class" ? "◆" : "○";
1794
- lines.push(`${indent}${typeIcon} ${node.name} [id:${node.id}]`);
1795
- if (node.key) {
1796
- lines.push(`${indent} key: "${node.key}"`);
397
+ catch (error) {
398
+ return { content: [{ type: "text", text: `Failed to capture iOS screenshot: ${error.message}` }] };
1797
399
  }
1798
400
  }
1799
- lines.push("");
1800
- lines.push(`Total: ${tree.length} components (depth: ${depth})`);
1801
- lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
1802
- let result = lines.join("\n");
1803
- if (check.message)
1804
- result += `\n\n${check.message}`;
1805
- return result;
1806
- }
1807
- catch (error) {
1808
- let result = `Error fetching component tree: ${error.message}\n\nMake sure React DevTools is running: npx react-devtools`;
1809
- if (check.message)
1810
- result += `\n\n${check.message}`;
1811
- return result;
1812
- }
1813
- }
1814
- async function fetchComponentTree(port, maxDepth) {
1815
- return new Promise((resolve, reject) => {
1816
- const ws = new WebSocket(`ws://localhost:${port}`);
1817
- const components = [];
1818
- let resolved = false;
1819
- const timeout = setTimeout(() => {
1820
- if (!resolved) {
1821
- resolved = true;
1822
- ws.close();
1823
- resolve(components);
1824
- }
1825
- }, 10000);
1826
- ws.on("open", () => {
1827
- // Send a request to get the tree
1828
- // The DevTools protocol varies, so we listen for operations
1829
- });
1830
- ws.on("message", (data) => {
401
+ // === DEVICE INFO ===
402
+ case "list_devices": {
1831
403
  try {
1832
- const message = JSON.parse(data.toString());
1833
- // Handle different message types from React DevTools
1834
- if (message.event === "operations") {
1835
- // Parse operations to build component tree
1836
- const ops = message.payload;
1837
- if (Array.isArray(ops)) {
1838
- // Operations array contains component tree data
1839
- parseOperations(ops, components, maxDepth);
1840
- }
404
+ const devices = await listConnectedDevices();
405
+ const maxDevices = getMaxDevices(tier);
406
+ if (devices.length === 0) {
407
+ return { content: [{ type: "text", text: "No devices connected. Start an emulator or connect a device." }] };
1841
408
  }
1842
- else if (message.event === "roots") {
1843
- // Root components
1844
- if (Array.isArray(message.payload)) {
1845
- for (const root of message.payload) {
1846
- components.push({
1847
- id: root.id || components.length,
1848
- name: root.displayName || root.name || "Root",
1849
- type: root.type || "root",
1850
- depth: 0,
1851
- });
1852
- }
1853
- }
409
+ const limited = devices.slice(0, maxDevices);
410
+ let result = `Connected devices (showing ${limited.length}/${devices.length}):\n`;
411
+ result += limited.map((d) => ` ${d.id} - ${d.status}`).join("\n");
412
+ if (devices.length > maxDevices) {
413
+ result += `\n\n[Upgrade to ADVANCED to see all ${devices.length} devices]`;
1854
414
  }
415
+ return { content: [{ type: "text", text: result }] };
1855
416
  }
1856
- catch { }
1857
- });
1858
- ws.on("error", (err) => {
1859
- if (!resolved) {
1860
- resolved = true;
1861
- clearTimeout(timeout);
1862
- // Return empty array on error, not reject
1863
- resolve(components);
1864
- }
1865
- });
1866
- ws.on("close", () => {
1867
- if (!resolved) {
1868
- resolved = true;
1869
- clearTimeout(timeout);
1870
- resolve(components);
1871
- }
1872
- });
1873
- // Give time to receive data
1874
- setTimeout(() => {
1875
- if (!resolved) {
1876
- resolved = true;
1877
- clearTimeout(timeout);
1878
- ws.close();
1879
- resolve(components);
1880
- }
1881
- }, 3000);
1882
- });
1883
- }
1884
- function parseOperations(ops, components, maxDepth) {
1885
- // React DevTools operations format varies by version
1886
- // This is a simplified parser
1887
- let depth = 0;
1888
- for (let i = 0; i < ops.length && components.length < 100; i++) {
1889
- const op = ops[i];
1890
- if (typeof op === "object" && op.id !== undefined) {
1891
- if (depth <= maxDepth) {
1892
- components.push({
1893
- id: op.id,
1894
- name: op.displayName || op.name || `Component_${op.id}`,
1895
- type: op.type === 1 ? "function" : op.type === 2 ? "class" : "other",
1896
- depth: Math.min(op.depth || depth, maxDepth),
1897
- key: op.key,
1898
- });
417
+ catch (error) {
418
+ return { content: [{ type: "text", text: `Failed to list devices: ${error.message}` }] };
1899
419
  }
1900
420
  }
1901
- }
1902
- }
1903
- async function inspectReactComponent(componentId, port = 8097) {
1904
- const check = await requireAdvanced("inspect_react_component");
1905
- if (!check.allowed)
1906
- return check.message;
1907
- try {
1908
- const inspection = await fetchComponentDetails(componentId, port);
1909
- if (!inspection) {
1910
- let result = `Component with ID ${componentId} not found.\n\nUse 'get_react_component_tree' to get valid component IDs.`;
1911
- if (check.message)
1912
- result += `\n\n${check.message}`;
1913
- return result;
1914
- }
1915
- const lines = [];
1916
- lines.push(`🔍 Component Inspection: ${inspection.name}`);
1917
- lines.push("═".repeat(50));
1918
- lines.push(`ID: ${componentId}`);
1919
- lines.push(`Type: ${inspection.type}`);
1920
- if (inspection.props && Object.keys(inspection.props).length > 0) {
1921
- lines.push("\n📦 Props:");
1922
- for (const [key, value] of Object.entries(inspection.props)) {
1923
- const displayValue = formatValue(value);
1924
- lines.push(` ${key}: ${displayValue}`);
421
+ case "list_ios_simulators": {
422
+ if (process.platform !== "darwin") {
423
+ return { content: [{ type: "text", text: "iOS Simulators are only available on macOS" }] };
1925
424
  }
1926
- }
1927
- else {
1928
- lines.push("\n📦 Props: (none)");
1929
- }
1930
- if (inspection.state && Object.keys(inspection.state).length > 0) {
1931
- lines.push("\n💾 State:");
1932
- for (const [key, value] of Object.entries(inspection.state)) {
1933
- const displayValue = formatValue(value);
1934
- lines.push(` ${key}: ${displayValue}`);
425
+ try {
426
+ const { stdout } = await execAsync(`${XCRUN} simctl list devices --json`);
427
+ const data = JSON.parse(stdout);
428
+ const onlyBooted = args.onlyBooted;
429
+ let result = "iOS Simulators:\n";
430
+ for (const [runtime, devices] of Object.entries(data.devices)) {
431
+ const filteredDevices = onlyBooted
432
+ ? devices.filter((d) => d.state === "Booted")
433
+ : devices;
434
+ if (filteredDevices.length > 0) {
435
+ result += `\n${runtime}:\n`;
436
+ for (const device of filteredDevices) {
437
+ result += ` ${device.name} (${device.udid}) - ${device.state}\n`;
438
+ }
439
+ }
440
+ }
441
+ return { content: [{ type: "text", text: result }] };
1935
442
  }
1936
- }
1937
- if (inspection.hooks && inspection.hooks.length > 0) {
1938
- lines.push("\n🪝 Hooks:");
1939
- for (const hook of inspection.hooks) {
1940
- lines.push(` ${hook.name}: ${formatValue(hook.value)}`);
443
+ catch (error) {
444
+ return { content: [{ type: "text", text: `Failed to list simulators: ${error.message}` }] };
1941
445
  }
1942
446
  }
1943
- let result = lines.join("\n");
1944
- if (check.message)
1945
- result += `\n\n${check.message}`;
1946
- return result;
1947
- }
1948
- catch (error) {
1949
- let result = `Error inspecting component: ${error.message}`;
1950
- if (check.message)
1951
- result += `\n\n${check.message}`;
1952
- return result;
1953
- }
1954
- }
1955
- function formatValue(value) {
1956
- if (value === null)
1957
- return "null";
1958
- if (value === undefined)
1959
- return "undefined";
1960
- if (typeof value === "string")
1961
- return `"${value.substring(0, 100)}${value.length > 100 ? "..." : ""}"`;
1962
- if (typeof value === "number" || typeof value === "boolean")
1963
- return String(value);
1964
- if (Array.isArray(value))
1965
- return `Array(${value.length})`;
1966
- if (typeof value === "object") {
1967
- const keys = Object.keys(value);
1968
- if (keys.length === 0)
1969
- return "{}";
1970
- return `{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
1971
- }
1972
- if (typeof value === "function")
1973
- return "ƒ()";
1974
- return String(value).substring(0, 50);
1975
- }
1976
- async function fetchComponentDetails(componentId, port) {
1977
- return new Promise((resolve) => {
1978
- const ws = new WebSocket(`ws://localhost:${port}`);
1979
- let resolved = false;
1980
- let details = null;
1981
- const timeout = setTimeout(() => {
1982
- if (!resolved) {
1983
- resolved = true;
1984
- ws.close();
1985
- resolve(details);
1986
- }
1987
- }, 5000);
1988
- ws.on("open", () => {
1989
- // Request inspection of specific element
1990
- const request = {
1991
- event: "inspectElement",
1992
- payload: {
1993
- id: componentId,
1994
- rendererID: 1,
1995
- requestID: Date.now(),
1996
- },
1997
- };
1998
- ws.send(JSON.stringify(request));
1999
- });
2000
- ws.on("message", (data) => {
447
+ case "get_device_info": {
2001
448
  try {
2002
- const message = JSON.parse(data.toString());
2003
- if (message.event === "inspectedElement" && message.payload) {
2004
- const p = message.payload;
2005
- details = {
2006
- name: p.displayName || p.name || `Component_${componentId}`,
2007
- type: p.type || "unknown",
2008
- props: p.props || {},
2009
- state: p.state,
2010
- hooks: p.hooks,
2011
- };
449
+ const props = [
450
+ "ro.product.model",
451
+ "ro.build.version.release",
452
+ "ro.build.version.sdk",
453
+ "ro.product.manufacturer",
454
+ ];
455
+ let result = "Device Information:\n";
456
+ for (const prop of props) {
457
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} shell getprop ${prop}`);
458
+ result += ` ${prop}: ${stdout.trim()}\n`;
2012
459
  }
460
+ // Screen size
461
+ const { stdout: screenSize } = await execAsync(`${ADB} ${deviceArg} shell wm size`);
462
+ result += ` Screen: ${screenSize.trim()}\n`;
463
+ // Memory (avoid shell piping - process in Node.js)
464
+ const { stdout: memInfo } = await execAsync(`${ADB} ${deviceArg} shell cat /proc/meminfo`);
465
+ const memLines = memInfo.split("\n").slice(0, 3); // Limit to first 3 lines in Node.js
466
+ result += ` Memory:\n${memLines.map((l) => ` ${l}`).join("\n")}`;
467
+ return { content: [{ type: "text", text: result }] };
2013
468
  }
2014
- catch { }
2015
- });
2016
- ws.on("error", () => {
2017
- if (!resolved) {
2018
- resolved = true;
2019
- clearTimeout(timeout);
2020
- resolve(null);
2021
- }
2022
- });
2023
- ws.on("close", () => {
2024
- if (!resolved) {
2025
- resolved = true;
2026
- clearTimeout(timeout);
2027
- resolve(details);
2028
- }
2029
- });
2030
- // Give time to receive response
2031
- setTimeout(() => {
2032
- if (!resolved) {
2033
- resolved = true;
2034
- clearTimeout(timeout);
2035
- ws.close();
2036
- resolve(details);
469
+ catch (error) {
470
+ return { content: [{ type: "text", text: `Failed to get device info: ${error.message}` }] };
2037
471
  }
2038
- }, 3000);
2039
- });
2040
- }
2041
- async function searchReactComponents(query, port = 8097) {
2042
- const check = await requireAdvanced("search_react_components");
2043
- if (!check.allowed)
2044
- return check.message;
2045
- try {
2046
- const tree = await fetchComponentTree(port, 10);
2047
- const queryLower = query.toLowerCase();
2048
- const matches = tree.filter((component) => component.name.toLowerCase().includes(queryLower));
2049
- if (matches.length === 0) {
2050
- let result = `🔍 Search Results for "${query}"\n${"═".repeat(50)}\n\nNo components found matching "${query}".\n\nTip: Try a partial name or check if the app is connected to DevTools.`;
2051
- if (check.message)
2052
- result += `\n\n${check.message}`;
2053
- return result;
2054
- }
2055
- const lines = [];
2056
- lines.push(`🔍 Search Results for "${query}"`);
2057
- lines.push("═".repeat(50));
2058
- lines.push(`\nFound ${matches.length} matching component(s):\n`);
2059
- for (const match of matches.slice(0, 20)) {
2060
- const typeIcon = match.type === "function" ? "ƒ" : match.type === "class" ? "◆" : "○";
2061
- lines.push(`${typeIcon} ${match.name} [id:${match.id}]`);
2062
- }
2063
- if (matches.length > 20) {
2064
- lines.push(`\n... and ${matches.length - 20} more matches`);
2065
- }
2066
- lines.push("\nUse 'inspect_react_component' with an [id] to see props/state");
2067
- let result = lines.join("\n");
2068
- if (check.message)
2069
- result += `\n\n${check.message}`;
2070
- return result;
2071
- }
2072
- catch (error) {
2073
- let result = `Error searching components: ${error.message}`;
2074
- if (check.message)
2075
- result += `\n\n${check.message}`;
2076
- return result;
2077
- }
2078
- }
2079
- // ============================================================================
2080
- // MCP SERVER SETUP
2081
- // ============================================================================
2082
- const server = new Server({
2083
- name: "claude-mobile-dev-mcp",
2084
- version: "0.1.0",
2085
- }, {
2086
- capabilities: {
2087
- tools: {},
2088
- },
2089
- });
2090
- // Handle tool listing
2091
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
2092
- tools,
2093
- }));
2094
- // Handle tool execution
2095
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2096
- const { name, arguments: args } = request.params;
2097
- try {
2098
- // License tools
2099
- if (name === "get_license_status" || name === "set_license_key") {
2100
- const result = await handleLicenseTool(name, args || {});
2101
- return { content: [{ type: "text", text: result }] };
2102
472
  }
2103
- // Core tools
2104
- switch (name) {
2105
- case "get_metro_logs": {
2106
- const result = await getMetroLogs(args?.lines, args?.filter);
2107
- return { content: [{ type: "text", text: result }] };
473
+ case "get_ios_simulator_info": {
474
+ if (process.platform !== "darwin") {
475
+ return { content: [{ type: "text", text: "iOS Simulators only available on macOS" }] };
2108
476
  }
2109
- case "get_adb_logs": {
2110
- const result = await getAdbLogs(args?.lines, args?.filter, args?.level);
2111
- return { content: [{ type: "text", text: result }] };
477
+ // Validate UDID if provided
478
+ const simUdid = args.udid || "booted";
479
+ if (simUdid !== "booted" && !validateUdid(simUdid)) {
480
+ return {
481
+ content: [{ type: "text", text: "Invalid iOS Simulator UDID format. Must be UUID format or 'booted'." }],
482
+ };
2112
483
  }
2113
- case "screenshot_emulator": {
2114
- const result = await screenshotEmulator(args?.device);
2115
- if (result.success && result.data) {
2116
- const content = [
2117
- {
2118
- type: "image",
2119
- data: result.data,
2120
- mimeType: result.mimeType,
2121
- },
2122
- ];
2123
- // Add trial warning if present
2124
- if (result.trialMessage) {
2125
- content.push({ type: "text", text: result.trialMessage });
484
+ try {
485
+ const udid = simUdid;
486
+ const { stdout } = await execAsync(`${XCRUN} simctl list devices --json`);
487
+ const data = JSON.parse(stdout);
488
+ for (const devices of Object.values(data.devices)) {
489
+ for (const device of devices) {
490
+ if (device.udid === udid || (udid === "booted" && device.state === "Booted")) {
491
+ return {
492
+ content: [{
493
+ type: "text",
494
+ text: JSON.stringify(device, null, 2),
495
+ }],
496
+ };
497
+ }
2126
498
  }
2127
- return { content };
2128
499
  }
2129
- return { content: [{ type: "text", text: result.error }] };
2130
- }
2131
- case "list_devices": {
2132
- const result = await listDevices();
2133
- return { content: [{ type: "text", text: result }] };
500
+ return { content: [{ type: "text", text: "Simulator not found" }] };
2134
501
  }
2135
- case "check_metro_status": {
2136
- const result = await checkMetroStatus(args?.port);
2137
- return { content: [{ type: "text", text: result }] };
502
+ catch (error) {
503
+ return { content: [{ type: "text", text: `Failed to get simulator info: ${error.message}` }] };
2138
504
  }
2139
- case "get_app_info": {
2140
- const result = await getAppInfo(args?.packageName);
2141
- return { content: [{ type: "text", text: result }] };
505
+ }
506
+ case "get_app_info": {
507
+ const packageName = args.packageName;
508
+ // Validate package name (security: prevent shell injection)
509
+ if (!validatePackageName(packageName)) {
510
+ return {
511
+ content: [{ type: "text", text: "Invalid package name format. Package names should be like 'com.example.app'." }],
512
+ };
2142
513
  }
2143
- case "clear_app_data": {
2144
- const result = await clearAppData(args?.packageName);
2145
- return { content: [{ type: "text", text: result }] };
514
+ try {
515
+ // Quote package name and avoid shell piping - process in Node.js
516
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} shell dumpsys package "${packageName}"`);
517
+ const outputLines = stdout.split("\n").slice(0, 50).join("\n"); // Limit to 50 lines in Node.js
518
+ return { content: [{ type: "text", text: outputLines }] };
2146
519
  }
2147
- case "restart_adb": {
2148
- const result = await restartAdb();
2149
- return { content: [{ type: "text", text: result }] };
520
+ catch (error) {
521
+ return { content: [{ type: "text", text: `Failed to get app info: ${error.message}` }] };
2150
522
  }
2151
- case "get_device_info": {
2152
- const result = await getDeviceInfo(args?.device);
523
+ }
524
+ // === LOGS ===
525
+ case "get_metro_logs": {
526
+ const requestedLines = args.lines || 50;
527
+ const maxLines = getMaxLogLines(tier);
528
+ // Ensure lines is a positive integer
529
+ const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
530
+ try {
531
+ const port = CONFIG.metroPort;
532
+ const { stdout } = await execAsync(`curl -s http://localhost:${port}/status`, { timeout: 2000 });
533
+ let result = `Metro Status (port ${port}): ${stdout}\n`;
534
+ result += `\n[Metro logs require 'start_metro_logging' to capture output]`;
535
+ if (lines < requestedLines) {
536
+ result += `\n[Showing ${lines} lines, upgrade to ADVANCED for more]`;
537
+ }
2153
538
  return { content: [{ type: "text", text: result }] };
2154
539
  }
2155
- case "start_metro_logging": {
2156
- const result = await startMetroLogging(args?.logFile);
2157
- return { content: [{ type: "text", text: result }] };
540
+ catch {
541
+ return {
542
+ content: [{
543
+ type: "text",
544
+ text: `Metro not responding on port ${CONFIG.metroPort}. Is it running?`,
545
+ }],
546
+ };
2158
547
  }
2159
- case "stop_metro_logging": {
2160
- const result = await stopMetroLogging();
2161
- return { content: [{ type: "text", text: result }] };
548
+ }
549
+ case "get_adb_logs": {
550
+ const requestedLines = args.lines || 50;
551
+ const maxLines = getMaxLogLines(tier);
552
+ // Ensure lines is a positive integer (security: prevent injection via negative/float values)
553
+ const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
554
+ const filter = args.filter || "ReactNativeJS";
555
+ const level = args.level || "I";
556
+ // Validate filter (security: prevent shell injection)
557
+ if (!validateLogFilter(filter)) {
558
+ return {
559
+ content: [{ type: "text", text: "Invalid log filter format. Filters should be alphanumeric with underscores (e.g., 'ReactNativeJS', '*')." }],
560
+ };
2162
561
  }
2163
- // PRO FEATURES
2164
- case "stream_adb_realtime": {
2165
- const result = await streamAdbRealtime(args?.filter);
2166
- return { content: [{ type: "text", text: result }] };
562
+ // Validate level
563
+ if (!validateLogLevel(level)) {
564
+ return {
565
+ content: [{ type: "text", text: "Invalid log level. Use one of: V, D, I, W, E, F." }],
566
+ };
2167
567
  }
2168
- case "stop_adb_streaming": {
2169
- const result = stopAdbStreaming();
568
+ try {
569
+ const filterArg = filter === "*" ? "" : `-s ${filter}:${level}`;
570
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} logcat -d ${filterArg} -t ${lines}`, { timeout: 5000 });
571
+ let result = stdout || "No logs found";
572
+ if (lines < requestedLines) {
573
+ result += `\n\n[Showing ${lines} lines, upgrade to ADVANCED for more]`;
574
+ }
2170
575
  return { content: [{ type: "text", text: result }] };
2171
576
  }
2172
- case "screenshot_history": {
2173
- const result = await getScreenshotHistory(args?.count);
2174
- return { content: [{ type: "text", text: result }] };
577
+ catch (error) {
578
+ return { content: [{ type: "text", text: `Failed to get logs: ${error.message}` }] };
2175
579
  }
2176
- case "watch_for_errors": {
2177
- const result = await watchForErrors(args?.patterns, args?.timeout);
2178
- return { content: [{ type: "text", text: result }] };
580
+ }
581
+ case "get_ios_simulator_logs": {
582
+ if (process.platform !== "darwin") {
583
+ return { content: [{ type: "text", text: "iOS Simulator logs only available on macOS" }] };
2179
584
  }
2180
- case "multi_device_logs": {
2181
- const result = await multiDeviceLogs(args?.devices, args?.lines);
2182
- return { content: [{ type: "text", text: result }] };
585
+ const requestedLines = args.lines || 50;
586
+ const maxLines = getMaxLogLines(tier);
587
+ // Ensure lines is a safe positive integer (security: prevent tail injection)
588
+ const lines = Math.max(1, Math.min(Math.floor(requestedLines), maxLines));
589
+ try {
590
+ // Avoid shell piping - process in Node.js
591
+ const { stdout } = await execAsync(`log show --predicate 'subsystem CONTAINS "com.apple.CoreSimulator"' --last 5m --style compact`, { timeout: 10000 });
592
+ // Limit output lines in Node.js instead of shell tail
593
+ const outputLines = stdout.split("\n").slice(-lines).join("\n");
594
+ return { content: [{ type: "text", text: outputLines || "No recent logs found" }] };
2183
595
  }
2184
- // INTERACTION TOOLS
2185
- case "tap_screen": {
2186
- const result = await tapScreen(args?.x, args?.y, args?.device);
2187
- return { content: [{ type: "text", text: result }] };
596
+ catch (error) {
597
+ return { content: [{ type: "text", text: `Failed to get iOS logs: ${error.message}` }] };
2188
598
  }
2189
- case "input_text": {
2190
- const result = await inputText(args?.text, args?.device);
2191
- return { content: [{ type: "text", text: result }] };
599
+ }
600
+ case "check_metro_status": {
601
+ const port = args.port || CONFIG.metroPort;
602
+ // Validate port (security: prevent injection via port number)
603
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
604
+ return {
605
+ content: [{ type: "text", text: "Invalid port number. Port must be between 1 and 65535." }],
606
+ };
2192
607
  }
2193
- case "press_button": {
2194
- const result = await pressButton(args?.button, args?.device);
2195
- return { content: [{ type: "text", text: result }] };
608
+ try {
609
+ const { stdout } = await execAsync(`curl -s http://localhost:${port}/status`, { timeout: 2000 });
610
+ return {
611
+ content: [{
612
+ type: "text",
613
+ text: `Metro bundler is running on port ${port}\nStatus: ${stdout}`,
614
+ }],
615
+ };
2196
616
  }
2197
- case "swipe_screen": {
2198
- const result = await swipeScreen(args?.startX, args?.startY, args?.endX, args?.endY, args?.duration, args?.device);
2199
- return { content: [{ type: "text", text: result }] };
617
+ catch {
618
+ return {
619
+ content: [{
620
+ type: "text",
621
+ text: `Metro bundler is NOT running on port ${port}`,
622
+ }],
623
+ };
2200
624
  }
2201
- case "launch_app": {
2202
- const result = await launchApp(args?.packageName, args?.device);
2203
- return { content: [{ type: "text", text: result }] };
625
+ }
626
+ // === UI TREE (ADVANCED) ===
627
+ case "get_ui_tree": {
628
+ try {
629
+ const compressed = args.compressed !== false;
630
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
631
+ const elements = parseUiTree(stdout);
632
+ const filtered = compressed
633
+ ? elements.filter((el) => el.clickable || el.text || el.contentDescription)
634
+ : elements;
635
+ return {
636
+ content: [{
637
+ type: "text",
638
+ text: JSON.stringify({
639
+ elementCount: filtered.length,
640
+ elements: filtered.map((el) => ({
641
+ text: el.text,
642
+ resourceId: el.resourceId,
643
+ className: el.className.split(".").pop(),
644
+ contentDescription: el.contentDescription,
645
+ bounds: el.bounds,
646
+ clickable: el.clickable,
647
+ center: el.centerX && el.centerY ? { x: el.centerX, y: el.centerY } : undefined,
648
+ })),
649
+ }, null, 2),
650
+ }],
651
+ };
2204
652
  }
2205
- case "install_apk": {
2206
- const result = await installApk(args?.apkPath, args?.device);
2207
- return { content: [{ type: "text", text: result }] };
653
+ catch (error) {
654
+ return { content: [{ type: "text", text: `Failed to get UI tree: ${error.message}` }] };
2208
655
  }
2209
- // iOS SIMULATOR TOOLS
2210
- case "list_ios_simulators": {
2211
- const result = await listIosSimulators(args?.onlyBooted);
2212
- return { content: [{ type: "text", text: result }] };
656
+ }
657
+ case "find_element": {
658
+ // Validate search parameters have reasonable length (prevent DoS)
659
+ const MAX_SEARCH_LEN = 500;
660
+ const searchText = args.text;
661
+ const searchResourceId = args.resourceId;
662
+ const searchContentDesc = args.contentDescription;
663
+ if ((searchText && searchText.length > MAX_SEARCH_LEN) ||
664
+ (searchResourceId && searchResourceId.length > MAX_SEARCH_LEN) ||
665
+ (searchContentDesc && searchContentDesc.length > MAX_SEARCH_LEN)) {
666
+ return { content: [{ type: "text", text: "Search parameters too long (max 500 chars)" }] };
2213
667
  }
2214
- case "screenshot_ios_simulator": {
2215
- const result = await screenshotIosSimulator(args?.udid);
2216
- if (result.success && result.data) {
2217
- const content = [
2218
- {
2219
- type: "image",
2220
- data: result.data,
2221
- mimeType: result.mimeType,
2222
- },
2223
- ];
2224
- if (result.trialMessage) {
2225
- content.push({ type: "text", text: result.trialMessage });
668
+ try {
669
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
670
+ const elements = parseUiTree(stdout);
671
+ const found = findElementInTree(elements, {
672
+ text: searchText,
673
+ resourceId: searchResourceId,
674
+ contentDescription: searchContentDesc,
675
+ });
676
+ if (found) {
677
+ return {
678
+ content: [{
679
+ type: "text",
680
+ text: JSON.stringify({
681
+ found: true,
682
+ element: {
683
+ text: found.text,
684
+ resourceId: found.resourceId,
685
+ className: found.className,
686
+ contentDescription: found.contentDescription,
687
+ bounds: found.bounds,
688
+ center: { x: found.centerX, y: found.centerY },
689
+ clickable: found.clickable,
690
+ enabled: found.enabled,
691
+ },
692
+ }, null, 2),
693
+ }],
694
+ };
695
+ }
696
+ return { content: [{ type: "text", text: JSON.stringify({ found: false }) }] };
697
+ }
698
+ catch (error) {
699
+ return { content: [{ type: "text", text: `Failed to find element: ${error.message}` }] };
700
+ }
701
+ }
702
+ case "wait_for_element": {
703
+ // Cap timeout at 60 seconds to prevent indefinite blocking
704
+ const MAX_TIMEOUT = 60000;
705
+ const requestedTimeout = args.timeout || 5000;
706
+ const timeout = Math.max(1000, Math.min(requestedTimeout, MAX_TIMEOUT));
707
+ const pollInterval = 500;
708
+ const startTime = Date.now();
709
+ // Validate search parameters have reasonable length (prevent DoS)
710
+ const MAX_SEARCH_LEN = 500;
711
+ const searchText = args.text;
712
+ const searchResourceId = args.resourceId;
713
+ const searchContentDesc = args.contentDescription;
714
+ if ((searchText && searchText.length > MAX_SEARCH_LEN) ||
715
+ (searchResourceId && searchResourceId.length > MAX_SEARCH_LEN) ||
716
+ (searchContentDesc && searchContentDesc.length > MAX_SEARCH_LEN)) {
717
+ return { content: [{ type: "text", text: "Search parameters too long (max 500 chars)" }] };
718
+ }
719
+ while (Date.now() - startTime < timeout) {
720
+ try {
721
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 5000 });
722
+ const elements = parseUiTree(stdout);
723
+ const found = findElementInTree(elements, {
724
+ text: searchText,
725
+ resourceId: searchResourceId,
726
+ contentDescription: searchContentDesc,
727
+ });
728
+ if (found) {
729
+ return {
730
+ content: [{
731
+ type: "text",
732
+ text: JSON.stringify({
733
+ found: true,
734
+ waitTime: Date.now() - startTime,
735
+ element: {
736
+ text: found.text,
737
+ center: { x: found.centerX, y: found.centerY },
738
+ },
739
+ }, null, 2),
740
+ }],
741
+ };
2226
742
  }
2227
- return { content };
2228
743
  }
2229
- return { content: [{ type: "text", text: result.error }] };
2230
- }
2231
- case "get_ios_simulator_logs": {
2232
- const result = await getIosSimulatorLogs(args?.udid, args?.filter, args?.lines);
2233
- return { content: [{ type: "text", text: result }] };
744
+ catch {
745
+ // Ignore errors during polling
746
+ }
747
+ await sleep(pollInterval);
2234
748
  }
2235
- case "get_ios_simulator_info": {
2236
- const result = await getIosSimulatorInfo(args?.udid);
2237
- return { content: [{ type: "text", text: result }] };
749
+ return {
750
+ content: [{
751
+ type: "text",
752
+ text: JSON.stringify({ found: false, timeout: true, waitTime: timeout }),
753
+ }],
754
+ };
755
+ }
756
+ case "get_element_property":
757
+ case "assert_element": {
758
+ try {
759
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
760
+ const elements = parseUiTree(stdout);
761
+ const found = findElementInTree(elements, {
762
+ text: args.text,
763
+ resourceId: args.resourceId,
764
+ contentDescription: args.contentDescription,
765
+ });
766
+ if (name === "get_element_property") {
767
+ const property = args.property;
768
+ if (found) {
769
+ return {
770
+ content: [{
771
+ type: "text",
772
+ text: JSON.stringify({ [property]: found[property] }),
773
+ }],
774
+ };
775
+ }
776
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Element not found" }) }] };
777
+ }
778
+ // assert_element
779
+ const shouldExist = args.shouldExist !== false;
780
+ const exists = !!found;
781
+ const passed = shouldExist === exists;
782
+ return {
783
+ content: [{
784
+ type: "text",
785
+ text: JSON.stringify({
786
+ passed,
787
+ exists,
788
+ shouldExist,
789
+ element: found ? { text: found.text, enabled: found.enabled } : null,
790
+ }, null, 2),
791
+ }],
792
+ };
2238
793
  }
2239
- case "boot_ios_simulator": {
2240
- const result = await bootIosSimulator(args?.udid);
2241
- return { content: [{ type: "text", text: result }] };
794
+ catch (error) {
795
+ return { content: [{ type: "text", text: `Failed: ${error.message}` }] };
2242
796
  }
2243
- case "shutdown_ios_simulator": {
2244
- const result = await shutdownIosSimulator(args?.udid);
2245
- return { content: [{ type: "text", text: result }] };
797
+ }
798
+ // === SCREEN ANALYSIS (ADVANCED) ===
799
+ case "suggest_action": {
800
+ const goal = args.goal;
801
+ // Type guard and validation
802
+ if (!goal || typeof goal !== "string") {
803
+ return { content: [{ type: "text", text: "Goal parameter is required and must be a string" }] };
2246
804
  }
2247
- case "install_ios_app": {
2248
- const result = await installIosApp(args?.appPath, args?.udid);
2249
- return { content: [{ type: "text", text: result }] };
805
+ // Validate goal has reasonable length (prevent DoS via long strings)
806
+ const MAX_GOAL_LEN = 1000;
807
+ if (goal.length > MAX_GOAL_LEN) {
808
+ return { content: [{ type: "text", text: `Goal too long (max ${MAX_GOAL_LEN} chars)` }] };
2250
809
  }
2251
- case "launch_ios_app": {
2252
- const result = await launchIosApp(args?.bundleId, args?.udid);
2253
- return { content: [{ type: "text", text: result }] };
810
+ try {
811
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
812
+ const elements = parseUiTree(stdout);
813
+ const clickableElements = elements.filter((el) => el.clickable && (el.text || el.contentDescription));
814
+ const suggestions = [];
815
+ const goalLower = goal.toLowerCase();
816
+ for (const el of clickableElements) {
817
+ const text = (el.text || el.contentDescription || "").toLowerCase();
818
+ if (goalLower.includes("login") && (text.includes("login") || text.includes("sign in"))) {
819
+ suggestions.push({
820
+ action: "tap",
821
+ target: el.text || el.contentDescription,
822
+ reasoning: "This button appears to initiate login",
823
+ });
824
+ }
825
+ if (goalLower.includes("search") && (text.includes("search") || el.className.includes("EditText"))) {
826
+ suggestions.push({
827
+ action: el.className.includes("EditText") ? "input" : "tap",
828
+ target: el.text || el.contentDescription || "search field",
829
+ reasoning: "This appears to be a search input",
830
+ });
831
+ }
832
+ if (goalLower.includes("settings") && text.includes("setting")) {
833
+ suggestions.push({
834
+ action: "tap",
835
+ target: el.text || el.contentDescription,
836
+ reasoning: "This navigates to settings",
837
+ });
838
+ }
839
+ if (goalLower.includes("back") && text.includes("back")) {
840
+ suggestions.push({
841
+ action: "tap",
842
+ target: el.text || el.contentDescription,
843
+ reasoning: "This goes back",
844
+ });
845
+ }
846
+ }
847
+ if (suggestions.length === 0 && clickableElements.length > 0) {
848
+ suggestions.push({
849
+ action: "analyze",
850
+ target: "screen",
851
+ reasoning: `No direct match for "${goal}". ${clickableElements.length} clickable elements found. Use analyze_screen for details.`,
852
+ });
853
+ }
854
+ return {
855
+ content: [{
856
+ type: "text",
857
+ text: JSON.stringify({
858
+ goal,
859
+ suggestions,
860
+ clickableElementCount: clickableElements.length,
861
+ note: "These are SUGGESTIONS only. MobileDevMCP is read-only and does not perform actions.",
862
+ }, null, 2),
863
+ }],
864
+ };
2254
865
  }
2255
- case "terminate_ios_app": {
2256
- const result = await terminateIosApp(args?.bundleId, args?.udid);
2257
- return { content: [{ type: "text", text: result }] };
866
+ catch (error) {
867
+ return { content: [{ type: "text", text: `Failed to analyze screen: ${error.message}` }] };
2258
868
  }
2259
- case "ios_open_url": {
2260
- const result = await iosOpenUrl(args?.url, args?.udid);
2261
- return { content: [{ type: "text", text: result }] };
869
+ }
870
+ case "analyze_screen": {
871
+ try {
872
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
873
+ const elements = parseUiTree(stdout);
874
+ const analysis = {
875
+ totalElements: elements.length,
876
+ clickableElements: elements.filter((el) => el.clickable).length,
877
+ textElements: elements.filter((el) => el.text).length,
878
+ inputFields: elements.filter((el) => el.className.includes("EditText")).length,
879
+ buttons: elements.filter((el) => el.className.includes("Button")).length,
880
+ visibleText: elements
881
+ .filter((el) => el.text)
882
+ .map((el) => el.text)
883
+ .slice(0, 20),
884
+ interactiveElements: elements
885
+ .filter((el) => el.clickable && (el.text || el.contentDescription))
886
+ .map((el) => ({
887
+ text: el.text || el.contentDescription,
888
+ type: el.className.split(".").pop(),
889
+ bounds: el.bounds,
890
+ }))
891
+ .slice(0, 15),
892
+ };
893
+ return {
894
+ content: [{
895
+ type: "text",
896
+ text: JSON.stringify(analysis, null, 2),
897
+ }],
898
+ };
2262
899
  }
2263
- case "ios_push_notification": {
2264
- const result = await iosPushNotification(args?.bundleId, args?.payload, args?.udid);
2265
- return { content: [{ type: "text", text: result }] };
900
+ catch (error) {
901
+ return { content: [{ type: "text", text: `Failed to analyze screen: ${error.message}` }] };
2266
902
  }
2267
- case "ios_set_location": {
2268
- const result = await iosSetLocation(args?.latitude, args?.longitude, args?.udid);
2269
- return { content: [{ type: "text", text: result }] };
903
+ }
904
+ case "get_screen_text": {
905
+ try {
906
+ const { stdout } = await execAsync(`${ADB} ${deviceArg} exec-out uiautomator dump /dev/tty`, { timeout: 10000 });
907
+ const elements = parseUiTree(stdout);
908
+ const allText = elements
909
+ .filter((el) => el.text || el.contentDescription)
910
+ .map((el) => el.text || el.contentDescription)
911
+ .filter((text, index, arr) => arr.indexOf(text) === index);
912
+ return {
913
+ content: [{
914
+ type: "text",
915
+ text: JSON.stringify({
916
+ textCount: allText.length,
917
+ text: allText,
918
+ }, null, 2),
919
+ }],
920
+ };
2270
921
  }
2271
- // REACT DEVTOOLS TOOLS
2272
- case "setup_react_devtools": {
2273
- const result = await setupReactDevTools(args?.port, args?.device);
2274
- return { content: [{ type: "text", text: result }] };
922
+ catch (error) {
923
+ return { content: [{ type: "text", text: `Failed to get screen text: ${error.message}` }] };
2275
924
  }
2276
- case "check_devtools_connection": {
2277
- const result = await checkDevToolsConnection(args?.port);
2278
- return { content: [{ type: "text", text: result }] };
925
+ }
926
+ // === LICENSE ===
927
+ case "get_license_status": {
928
+ try {
929
+ const status = await getLicenseStatus();
930
+ return { content: [{ type: "text", text: status }] };
2279
931
  }
2280
- case "get_react_component_tree": {
2281
- const result = await getReactComponentTree(args?.port, args?.depth);
2282
- return { content: [{ type: "text", text: result }] };
932
+ catch (error) {
933
+ return { content: [{ type: "text", text: `Failed to get license status: ${error.message}` }] };
2283
934
  }
2284
- case "inspect_react_component": {
2285
- const result = await inspectReactComponent(args?.componentId, args?.port);
935
+ }
936
+ case "set_license_key": {
937
+ try {
938
+ const result = await setLicenseKey(args.licenseKey);
2286
939
  return { content: [{ type: "text", text: result }] };
2287
940
  }
2288
- case "search_react_components": {
2289
- const result = await searchReactComponents(args?.query, args?.port);
2290
- return { content: [{ type: "text", text: result }] };
941
+ catch (error) {
942
+ return { content: [{ type: "text", text: `Failed to set license key: ${error.message}` }] };
2291
943
  }
2292
- default:
2293
- return {
2294
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
2295
- isError: true,
2296
- };
2297
944
  }
945
+ default:
946
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
947
+ }
948
+ }
949
+ // ============================================================================
950
+ // MCP SERVER SETUP
951
+ // ============================================================================
952
+ const server = new Server({
953
+ name: "mobile-dev-mcp",
954
+ version: "1.0.0",
955
+ }, {
956
+ capabilities: {
957
+ tools: {},
958
+ },
959
+ });
960
+ // List available tools
961
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
962
+ const license = await checkLicense();
963
+ // Filter tools based on tier
964
+ const availableTools = tools.filter((tool) => canAccessTool(tool.name, license.tier));
965
+ return { tools: availableTools };
966
+ });
967
+ // Handle tool calls
968
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
969
+ const { name, arguments: args } = request.params;
970
+ try {
971
+ const license = await checkLicense();
972
+ const result = await handleTool(name, args || {}, license.tier);
973
+ return result;
2298
974
  }
2299
975
  catch (error) {
2300
976
  return {
@@ -2303,14 +979,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2303
979
  };
2304
980
  }
2305
981
  });
2306
- // Start the server
982
+ // Start server
2307
983
  async function main() {
2308
984
  const transport = new StdioServerTransport();
2309
985
  await server.connect(transport);
2310
- // Log startup info to stderr (won't interfere with MCP protocol)
2311
986
  const license = await checkLicense();
2312
- console.error(`Mobile Dev MCP Server v0.1.0`);
2313
- console.error(`License: ${license.tier.toUpperCase()}`);
987
+ console.error(`Mobile Dev MCP v1.0.0 - Read-Only Debugging`);
988
+ console.error(`License: ${license.tier.toUpperCase()} (${license.tier === "free" ? 8 : 21} tools)`);
2314
989
  console.error(`Ready for connections...`);
2315
990
  }
2316
991
  main().catch((error) => {