ScreenSync MCP guide

Testing Android Bluetooth and BLE with Claude Code and MCP

Automate Android Bluetooth Low Energy scanning, GATT service discovery, pairing PIN dialogs, and BLE telemetry on physical devices via Claude Code and MCP.

Testing Android Bluetooth and BLE with Claude Code and MCP

Testing Bluetooth Low Energy (BLE) functionality represents one of the most notoriously brittle frontiers in modern Android mobile software engineering. Connected smart devices—ranging from medical continuous glucose monitors (CGMs), industrial IoT telemetry gateways, and automotive digital keys to fitness wearables, point-of-sale BLE payment readers, and smart locks—depend directly on physical 2.4 GHz radio-frequency (RF) chipsets, dynamic signal RSSI attenuation, hardware link-layer bonding, and asynchronous Generic Attribute Profile (GATT) server sessions. In automated continuous integration (CI) environments, Android Studio virtual emulators provide either non-functional Bluetooth stubs or synthetic HAL emulations that cannot simulate real-world radio attenuation, multipath RF interference, peripheral advertising intervals, or genuine GATT connection latency. Furthermore, in-process testing frameworks like Espresso and UI Automator struggle fundamentally at system security boundaries; when Android's operating system triggers a system-level Bluetooth pairing dialog or requests runtime permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT), in-process test runners throw target view not found exceptions because the security modal belongs to the out-of-process Android system UI (com.android.settings or com.android.systemui). Operating over the Model Context Protocol (MCP), ScreenSync MCP bridges Claude Code directly to physical Android devices. By pairing native Android MediaProjection display streaming (~94 ms median latency over LAN, requiring zero root and zero ADB overhead) with ADB-driven hardware control and Logcat telemetry, developers can execute comprehensive end-to-end Bluetooth Low Energy test suites directly from their terminal.

Key Takeaways

  • Real RF Hardware Grounding: ScreenSync MCP streams authentic Android display frames and coordinates real hardware Bluetooth testing, bypassing the non-functional Bluetooth stubs and simulated radios of Android Studio emulators.
  • Out-of-Process System Dialog Handling: While Espresso crashes on OS-level Bluetooth pairing request dialogs and PIN prompts, Claude Code inspects the system UI tree via get_ui_hierarchy and dispatches native touch events via control_tap.
  • Zero-ADB Viewfinder Streaming: ScreenSync MCP captures device displays via Android's native MediaProjection API at approximately 94 milliseconds median latency, preserving full ADB bandwidth for high-speed hardware control and telemetry.
  • GATT Callback & Telemetry Triage: Debug notoriously opaque Android Bluetooth faults—such as the infamous GATT_ERROR 133, connection parameter renegotiation drops, and bonding timeouts—in real time using get_logcat.
  • Android 12+ Runtime Permission Verification: Automate the verification of split Bluetooth permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE) and location prerequisites across fresh installations.
  • Unified 226-Tool MCP Ecosystem: ScreenSync MCP equips terminal agents with 29 phone and desktop tools (12 control_*, 3 os_*, 14 capture/telemetry) plus 197 web_* browser tools to validate multi-device and hybrid IoT web portals.

The Android BLE Testing Dilemma: Virtual Emulators, Espresso, and RF Blind Spots

Android Bluetooth Low Energy testing fails on virtual emulators and in-process runners because synthetic stubs cannot replicate physical 2.4 GHz RF propagation, dynamic RSSI variance, or hardware GATT negotiation. Espresso cannot interact with out-of-process system pairing dialogs. ScreenSync MCP bridges this gap by combining physical display capture with terminal-driven ADB control.

Automated mobile software testing has achieved remarkable maturity across standard UI flows, database transactions, and RESTful network interactions. However, Bluetooth Low Energy (BLE) testing remains stubbornly resistant to traditional test automation paradigms. When engineering teams build connected device applications, they confront a harsh architectural divide between Android's simulated runtime environments and authentic physical radio hardware.

1. The Emulator Hardware Barrier

Android Studio virtual devices (AVDs) running on QEMU-based hypervisors do not possess real physical 2.4 GHz Bluetooth transceivers. Although Android emulators provide an option to emulate Bluetooth via host adapter passthrough or synthetic rootcanal software stacks, this approach breaks down in production-grade testing:

  • HCI Passthrough Flaws: Host-to-guest Bluetooth Host Controller Interface (HCI) bridging often suffers from severe driver impedance mismatches between the host operating system (Windows, macOS, or Linux) and the guest Android HAL.
  • No Peripheral RF Simulation: Synthetic emulator stacks cannot simulate physical advertising packets, packet collision in congested RF environments, path loss, or fluctuating Received Signal Strength Indication (RSSI) values as devices move.
  • Timing & Connection Intervals: Emulators execute in virtualized CPU clocks where high-resolution connection event timing (e.g., 7.5 ms to 4000 ms connection intervals) is distorted by host CPU scheduling and hypervisor thread preemption.

2. The Espresso In-Process Sandbox Limitation

Espresso has long served as the gold standard for in-process Android UI testing. Compiled directly into the instrumentation test runner alongside the target application, Espresso synchronizes with Android's main UI looper (Looper.getMainLooper()) and idling resources. However, this tight process coupling becomes an architectural liability during Bluetooth testing:

  • Process Boundary Enforcement: Android enforces strict sandboxing between applications. The moment an application requests pairing or bonding with a secure BLE peripheral, the Android OS interrupts the app by launching an out-of-process system modal dialog (com.android.settings.bluetooth.BluetoothPairingDialog or a system overlay managed by com.android.systemui).
  • Fatal Instrumentation Exceptions: Because Espresso's view matchers (onView(withText("Pair"))) can only search within the target application's view hierarchy, Espresso cannot perceive the system dialog. The test runner stalls until timing out with a fatal NoMatchingViewException, aborting the automated test suite.
  • Runtime Permission Blindness: Android 12 (API level 31) split Bluetooth access into separate runtime permissions. When an app requests android.permission.BLUETOOTH_SCAN or android.permission.BLUETOOTH_CONNECT, the OS permission dialog is rendered by com.google.android.permissioncontroller. In-process frameworks cannot interact with this modal, forcing teams to bypass permission testing entirely using artificial GrantPermissionRule stubs.

3. The Brittleness of Mock BLE Stubs

To circumvent emulator and framework limitations, developers frequently resort to mocking Android's Bluetooth APIs using libraries like Mockito or Robolectric. They mock BluetoothManager, BluetoothAdapter, and BluetoothGattCallback. While unit testing state machines with mock stubs provides basic code coverage, it offers zero assurance of real-world functionality:

  • Callback Sequence Hallucinations: In real Android hardware, BluetoothGattCallback.onConnectionStateChange does not fire immediately; it involves physical link-layer handshakes, encryption exchanges, and firmware state transitions. Mock stubs simulate idealized, deterministic callback ordering that masks real-world race conditions.
  • OEM Stack Divergence: Android's Bluetooth stack (Fluoride/Fluoride-GD in modern Android) behaves differently across device manufacturers. Samsung One UI, Google Pixel stock Android, and Xiaomi HyperOS apply distinct aggressive power-saving policies, background connection timeouts, and MTU negotiation limits that mock stubs never expose.

ScreenSync MCP resolves this dilemma by giving terminal-based AI agents—specifically Claude Code—direct multimodal perception and hardware control over authentic, physical Android devices.

Testing Dimension Android Studio Emulator Mock BLE Stubs (Mockito / Robolectric) Cloud Device Farms (SauceLabs / BrowserStack) ScreenSync MCP + Claude Code (Physical Device)
Real RF & Peripheral Discovery Synthetic stubs only; cannot discover real hardware BLE peripherals Completely mocked; zero actual radio-frequency interaction Limited; devices isolated in faraday cages cannot reach external peripherals Direct hardware RF; scans and connects to real physical BLE beacons and sensors
System Pairing PIN Dialogs Fails or hangs; cannot interact with out-of-process system modals Bypassed; pairing logic is stubbed out in software Slow and brittle; high network latency causes modal dismissal timeouts Full interaction; inspects system UI tree via get_ui_hierarchy and taps PIN
GATT Lifecycle & Callback Timing Distorted by hypervisor scheduling; hides real connection latency Instantaneous and artificial; masks real-world race conditions High WAN latency (500-1500 ms) distorts event sequence logging Authentic hardware timing; captures exact GATT state transitions in real time
RSSI Fluctuation & Distance Simulation Unsupported; static synthetic signal values Mocked; fixed integer values Impossible; stationary devices in server racks Realistic RF propagation; tests live RSSI filtering, sorting, and beacon trilateration
HCI & Driver Crash Triage (Logcat) Generic emulator HAL; hides OEM driver crashes and firmware faults Impossible; no hardware driver execution occurs Restricted logcat streaming; limited ADB shell command permissions Unrestricted telemetry; streams raw driver logs and GATT faults via get_logcat
Local Execution Latency & Zero Egress Fast locally, but provides zero hardware confidence Instantaneous, but provides zero hardware confidence High WAN latency (500-1500 ms) and heavy recurring bandwidth costs Local Wi-Fi or USB reverse socket (~94 ms median); zero cloud data egress
Setup & Maintenance Overhead Heavy RAM footprint; requires nested virtualization and host drivers Complex codebase refactoring to inject mock interfaces Expensive recurring SaaS billing and proprietary client SDKs Free local desktop hub (Node.js 18+), free Android app, open MCP protocol

Android Bluetooth Low Energy Architecture: ScanFilters, GATT Callbacks, and Runtime Permissions

Android BLE architecture organizes wireless communication into peripheral advertising, central scanning via BluetoothLeScanner, and bidirectional attribute exchange via BluetoothGatt. On Android 12+, apps require explicit BLUETOOTH_SCAN and BLUETOOTH_CONNECT runtime permissions. ScreenSync MCP allows Claude Code to validate scanner filters, GATT state callbacks, and permission dialogs on physical hardware.

To design comprehensive automated tests, software engineers must master the core architectural components of Android's Bluetooth Low Energy stack. Android implements the standard Bluetooth 4.0+ Generic Attribute Profile (GATT) specification, adopting a client-server paradigm where the Android device typically operates as a GATT Client (Central device) communicating with a remote GATT Server (Peripheral device).

1. Peripheral Scanning and ScanSettings

Bluetooth Low Energy devices advertise their presence by broadcasting structured 31-byte advertising packets (and optional scan response packets) containing their device name, transmit power level, manufacturer data, and Service UUIDs. In Android, central scanning is managed by BluetoothLeScanner:

val scanner = bluetoothAdapter.bluetoothLeScanner
val scanFilters = listOf(
    ScanFilter.Builder()
        .setServiceUuid(ParcelUuid(UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")))
        .build()
)
val scanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
    .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
    .build()

scanner.startScan(scanFilters, scanSettings, scanCallback)

During testing, apps must balance scan aggressiveness against battery consumption. Using SCAN_MODE_LOW_LATENCY guarantees 100% duty-cycle scanning suitable for active pairing screens, whereas background scanning must use SCAN_MODE_LOW_POWER. If an app fails to enforce strict ScanFilter constraints, the Android OS throttles scanning or floods the application with hundreds of unrelated ambient advertising packets, causing UI stutter.

2. The Android 12+ Permission Overhaul

Starting in Android 12 (API level 31), Google overhauled Bluetooth permissions to decouple Bluetooth hardware access from coarse location tracking. Applications targeting Android 12 or higher must declare and request the following runtime permissions:

  • android.permission.BLUETOOTH_SCAN: Required to discover nearby BLE devices. If the application does not derive physical user location from Bluetooth beacons, developers can add android:usesPermissionFlags="neverForLocation" to the manifest declaration, exempting the app from requesting background location.
  • android.permission.BLUETOOTH_CONNECT: Required to query device names, establish GATT connections, read and write characteristics, and manage pairing bonds.
  • android.permission.BLUETOOTH_ADVERTISE: Required only if the mobile device acts as a peripheral broadcasting advertisements.
  • Legacy Support (Android 11 and earlier): Devices running Android 11 (API 30) or earlier still require ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION plus system location services (GPS) toggled ON to perform BLE scanning.

Developers must verify that their applications request these permissions gracefully. When building apps with modern reactive UI toolchains, developers can apply testing Android runtime permissions with Claude Code and MCP to verify permission dialogues and edge cases, and integrate with testing Jetpack Compose apps with Claude Code and MCP for reactive state validation.

3. Production Kotlin Implementation: Jetpack Compose BLE Manager

The following production-grade Kotlin implementation demonstrates a defensive BLE scanning and GATT connection architecture in Jetpack Compose. It includes asynchronous scanning, connection state management, MTU negotiation, and characteristic streaming:

package com.example.screensync.bluetooth

import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.util.UUID

// Target Peripheral Service and Characteristic UUIDs
val SERVICE_UUID: UUID = UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")
val DATA_CHAR_UUID: UUID = UUID.fromString("0000FFE1-0000-1000-8000-00805F9B34FB")

data class DiscoveredDevice(
    val device: BluetoothDevice,
    val name: String,
    val address: String,
    val rssi: Int
)

enum class BleConnectionState {
    DISCONNECTED,
    CONNECTING,
    DISCOVERING_SERVICES,
    CONNECTED,
    FAILED
}

@SuppressLint("MissingPermission")
class BleViewModel(private val context: Context) : ViewModel() {
    private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
    private var bleScanner: BluetoothLeScanner? = null
    private var activeGatt: BluetoothGatt? = null

    private val _discoveredDevices = MutableStateFlow<List<DiscoveredDevice>>(emptyList())
    val discoveredDevices: StateFlow<List<DiscoveredDevice>> = _discoveredDevices.asStateFlow()

    private val _connectionState = MutableStateFlow(BleConnectionState.DISCONNECTED)
    val connectionState: StateFlow<BleConnectionState> = _connectionState.asStateFlow()

    private val _telemetryData = MutableStateFlow("No telemetry received")
    val telemetryData: StateFlow<String> = _telemetryData.asStateFlow()

    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            val device = result.device
            val name = result.scanRecord?.deviceName ?: device.name ?: "Unknown BLE Device"
            val address = device.address
            val rssi = result.rssi

            viewModelScope.launch {
                val currentList = _discoveredDevices.value.toMutableList()
                val existingIndex = currentList.indexOfFirst { it.address == address }
                val updatedItem = DiscoveredDevice(device, name, address, rssi)

                if (existingIndex >= 0) {
                    currentList[existingIndex] = updatedItem
                } else {
                    currentList.add(updatedItem)
                }
                _discoveredDevices.value = currentList.sortedByDescending { it.rssi }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            Log.e("ScreenSyncBLE", "BLE Scan Failed with error code: $errorCode")
        }
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            Log.i("ScreenSyncBLE", "onConnectionStateChange: status=$status, newState=$newState")
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    _connectionState.value = BleConnectionState.DISCOVERING_SERVICES
                    Log.i("ScreenSyncBLE", "Connected to GATT server. Requesting MTU 512...")
                    gatt.requestMtu(512)
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    _connectionState.value = BleConnectionState.DISCONNECTED
                    Log.w("ScreenSyncBLE", "Disconnected from GATT server. Closing GATT instance.")
                    gatt.close()
                    activeGatt = null
                }
            } else {
                Log.e("ScreenSyncBLE", "GATT Error encountered: status=$status")
                _connectionState.value = BleConnectionState.FAILED
                gatt.close()
                activeGatt = null
            }
        }

        override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
            Log.i("ScreenSyncBLE", "MTU changed to $mtu with status: $status")
            Log.i("ScreenSyncBLE", "Discovering services...")
            gatt.discoverServices()
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                val service = gatt.getService(SERVICE_UUID)
                val characteristic = service?.getCharacteristic(DATA_CHAR_UUID)

                if (characteristic != null) {
                    _connectionState.value = BleConnectionState.CONNECTED
                    Log.i("ScreenSyncBLE", "Target characteristic found. Enabling notifications...")
                    gatt.setCharacteristicNotification(characteristic, true)
                } else {
                    Log.e("ScreenSyncBLE", "Target characteristic $DATA_CHAR_UUID not found!")
                    _connectionState.value = BleConnectionState.FAILED
                }
            } else {
                Log.e("ScreenSyncBLE", "Service discovery failed with status: $status")
                _connectionState.value = BleConnectionState.FAILED
            }
        }

        @Deprecated("Deprecated in Java")
        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
            val payload = String(characteristic.value, Charsets.UTF_8)
            Log.i("ScreenSyncBLE", "Incoming telemetry notification: $payload")
            _telemetryData.value = payload
        }
    }

    fun startScanning() {
        _discoveredDevices.value = emptyList()
        bleScanner = bluetoothAdapter?.bluetoothLeScanner
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()
        bleScanner?.startScan(null, settings, scanCallback)
        Log.i("ScreenSyncBLE", "Started low-latency BLE scan.")
    }

    fun stopScanning() {
        bleScanner?.stopScan(scanCallback)
        Log.i("ScreenSyncBLE", "Stopped BLE scan.")
    }

    fun connectToDevice(device: BluetoothDevice) {
        stopScanning()
        _connectionState.value = BleConnectionState.CONNECTING
        Log.i("ScreenSyncBLE", "Connecting to device: ${device.address}")
        // TRANSPORT_LE is critical for Dual-Mode Bluetooth chips
        activeGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
    }

    fun disconnect() {
        activeGatt?.disconnect()
    }
}

@Composable
fun BleScannerScreen(viewModel: BleViewModel) {
    val devices by viewModel.discoveredDevices.collectAsState()
    val state by viewModel.connectionState.collectAsState()
    val telemetry by viewModel.telemetryData.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "Status: $state",
            style = MaterialTheme.typography.titleMedium,
            color = MaterialTheme.colorScheme.primary
        )
        Text(
            text = "Telemetry: $telemetry",
            style = MaterialTheme.typography.bodyMedium,
            modifier = Modifier.padding(vertical = 4.dp)
        )

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Button(onClick = { viewModel.startScanning() }) {
                Text("Start Scan")
            }
            Button(onClick = { viewModel.stopScanning() }) {
                Text("Stop Scan")
            }
            Button(onClick = { viewModel.disconnect() }) {
                Text("Disconnect")
            }
        }

        Divider(modifier = Modifier.padding(vertical = 8.dp))

        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(devices, key = { it.address }) { item ->
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(12.dp),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        Column {
                            Text(text = item.name, style = MaterialTheme.typography.bodyLarge)
                            Text(text = item.address, style = MaterialTheme.typography.bodySmall)
                            Text(text = "RSSI: ${item.rssi} dBm", style = MaterialTheme.typography.bodySmall)
                        }
                        Button(onClick = { viewModel.connectToDevice(item.device) }) {
                            Text("Connect")
                        }
                    }
                }
            }
        }
    }
}
BLE Lifecycle Phase Key API / Callback Primary Failure Mode ScreenSync MCP Verification Tool Success Assertion Criteria
Adapter Initialization BluetoothAdapter.isEnabled() Bluetooth radio disabled or in airplane mode get_logcat + ADB shell Command cmd bluetooth_manager enable succeeds; status is active
Runtime Permissions requestPermissions(BLUETOOTH_SCAN) Permission denied; scan throws SecurityException get_ui_hierarchy + control_tap System dialog detected; agent taps "While using the app"
Peripheral Advertising Discovery ScanCallback.onScanResult() Empty scan list; scanner throttled by Android OS get_ui_hierarchy + get_latest_screenshot Peripheral card renders in LazyColumn with authentic MAC and RSSI
GATT Handshake connectGatt(TRANSPORT_LE) Connection fails with status 133 or link timeout get_logcat Logcat records onConnectionStateChange: status=0, newState=2
MTU Negotiation BluetoothGatt.requestMtu(512) Callback ignored or dropped by peripheral firmware get_logcat Logcat records onMtuChanged: mtu=512, status=0
Service Discovery BluetoothGatt.discoverServices() Service list empty or target characteristic missing get_ui_hierarchy + get_logcat UI state advances to CONNECTED; target UUID verified in logcat
Data Notification Streaming onCharacteristicChanged() Notifications not enabled on CCCD descriptor get_ui_hierarchy + get_logcat Telemetry string updates dynamically on screen without latency drift
Graceful Teardown BluetoothGatt.close() Unclosed handles cause memory leaks and handle exhaustion get_logcat Logcat confirms gatt.close() invoked; zero zombie client connections

How ScreenSync MCP Bridges Claude Code to Physical Bluetooth Hardware

ScreenSync MCP bridges Claude Code to physical Bluetooth hardware via a decoupled three-tier architecture: local stdio process communication for agent reasoning, MediaProjection streaming for sub-100ms visual confirmation, and ADB daemon sockets for hardware control. This enables terminal agents to inspect peripheral dashboards, confirm system pairing dialogs, and monitor GATT telemetry simultaneously.

The architectural breakthrough of ScreenSync MCP lies in its decoupled multi-modal foundation. When testing complex wireless hardware interactions, an AI coding agent requires three distinct operational capabilities: cognitive reasoning over source code, visual inspection of rendered displays, and unrestricted operating system control.

1. The Multi-Tier Architecture

ScreenSync MCP divides responsibility across three high-performance tiers:

  • Cognitive Agent Plane (Local stdio Process Pipe): Claude Code executes in the developer's workstation terminal. It communicates with the ScreenSync desktop hub over standard input/output (stdio) using JSON-RPC. Because communication is local, tool calls execute in sub-millisecond timeframes, eliminating the latency and overhead of cloud orchestration layers.
  • Visual Verification Plane (Native Android MediaProjection): Display capture is completely decoupled from ADB. The ScreenSync MCP Android client (available free on Google Play as com.screensync.mcp) initiates a native MediaProjection session upon user consent. It streams hardware-accelerated video frames over local Wi-Fi or USB reverse sockets directly to the desktop hub at ~94 ms median latency. Because display streaming bypasses ADB, the USB ADB channel remains 100% free for high-throughput commands and Logcat streaming.
  • Hardware & OS Control Plane (ADB Daemon Socket): Operating system automation, input event injection, and telemetry extraction execute through direct ADB sockets over physical USB or wireless debugging. Claude Code invokes device control tools (control_tap, control_swipe, control_type) and queries system diagnostics (get_ui_hierarchy, get_logcat).

2. Defeating the System Dialog Barrier

The single greatest obstacle in mobile test automation has always been interacting with out-of-process system dialogs. When an Android app initiates a secure Bluetooth bonding sequence, the Android operating system displays a system-level pairing modal:

<!-- DUMP FROM get_ui_hierarchy DURING BLUETOOTH PAIRING -->
<node package="com.android.settings" class="android.widget.FrameLayout">
  <node class="android.widget.TextView" text="Pair with Smart Beacon?" />
  <node class="android.widget.EditText" resource-id="com.android.settings:id/pin_values" text="" />
  <node class="android.widget.Button" resource-id="android:id/button2" text="Cancel" />
  <node class="android.widget.Button" resource-id="android:id/button1" text="Pair" />
</node>

Because ScreenSync MCP's get_ui_hierarchy extracts the complete active Window Manager node tree—regardless of whether the top window belongs to your app or the Android OS—Claude Code perceives the system dialog immediately. It locates the PIN input field, enters the passkey via control_type, and clicks "Pair" via control_tap. What previously broke Espresso and Appium CI runs now executes autonomously in seconds.

3. The 226-Tool Ecosystem Advantage

ScreenSync MCP provides an extensive catalog of 226 MCP tools:

  • 29 Phone & Desktop Tools: 12 device control tools (control_tap, control_swipe, control_type, control_key, control_open_url, control_launch_app), 3 operating system automation tools (os_mouse_click, os_type, os_hotkey), and 14 capture and telemetry tools (get_latest_screenshot, get_ui_hierarchy, get_logcat, record_screen, get_device_status).
  • 197 Browser Tools: The web_* suite operates through an authenticated Chrome/Edge browser extension. In connected IoT environments, mobile devices frequently sync telemetry to enterprise cloud dashboards. With ScreenSync MCP, Claude Code can trigger a Bluetooth data transfer on the Android phone and immediately verify that the telemetry payload arrived in an administrative web portal using browser tools like web_navigate and web_table_extract. For an exhaustive breakdown of every capability, consult our complete Android MCP tool reference deep dive.
Architectural Plane Transport Channel Median Latency Role in BLE Test Automation
Cognitive Agent Plane Local stdio JSON-RPC < 1 ms Claude Code parses developer instructions, plans BLE test flows, inspects logs, and refactors Kotlin code
Visual Verification Plane Native MediaProjection (Wi-Fi / USB) ~94 ms Streams live 60 FPS display frames; validates scanner animations, radar widgets, and RSSI graphs
OS & Hardware Control Plane ADB Daemon Socket (USB / Wireless) 10-25 ms Injects precision touch taps, enters pairing PINs, toggles radio states, and pipes Logcat telemetry
Enterprise Browser Plane Authenticated MV3 Extension 15-40 ms Validates cloud telemetry dashboards, backend MQTT bridges, and web pairing portals in real Chrome tabs

Workstation Setup: Desktop Hub, Physical Device, and Claude Code Registration

Setting up ScreenSync MCP for Bluetooth testing requires starting the Node.js 18+ desktop hub, registering the stdio MCP server in Claude Code, and connecting a physical Android 7.0+ device via USB or Wi-Fi. Establishing ADB reverse port forwarding ensures uninterrupted MediaProjection streaming during wireless peripheral testing.

Engineering teams can deploy this automated testing environment on Windows, macOS, or Linux through five streamlined steps:

Step 1: Workstation Environment Prerequisites

Ensure your development machine satisfies the following baseline requirements:

  1. Host Workstation: Windows 10/11, macOS, or Linux running Node.js 18 or higher with Android Platform Tools (adb) installed and present in your system PATH.
  2. Physical Smartphone: An authentic Android device running Android 7.0 (API level 24) or higher connected via USB cable or local Wi-Fi.
  3. Application Build: Debug APK installed on the phone containing your Bluetooth Low Energy implementation.
  4. Physical BLE Peripheral: A hardware BLE beacon, smart sensor, or secondary test phone running a BLE peripheral advertiser.

Step 2: Unpack and Launch the Desktop Hub

Download the ScreenSync MCP desktop hub distribution from screensyncmcp.tech and extract the archive to a permanent directory on your workstation. Run the platform launcher:

# On Windows (PowerShell or Command Prompt)
cd C:\tools\screensync-hub
.\start-hub.bat

# On macOS or Linux (Terminal)
cd ~/tools/screensync-hub
chmod +x start-hub.sh
./start-hub.sh

The desktop hub initializes an HTTP server on port 3000 to ingest local MediaProjection video frames and starts a standard Model Context Protocol server over stdio. The console output displays your unique authentication token (SCREEN_SYNC_TOKEN). For complete setup instructions, visit the official ScreenSync MCP setup guide.

Step 3: Register ScreenSync MCP with Claude Code

Register the desktop hub with the Claude Code CLI in your terminal:

claude mcp add screensync node "/path/to/screensync-hub/screensync-hub.js"

If you are developing inside Claude Desktop, configure claude_desktop_config.json (Settings > Developer > Edit Config):

{
  "mcpServers": {
    "screensync": {
      "command": "node",
      "args": ["/path/to/screensync-hub/screensync-hub.js"],
      "env": {
        "SCREEN_SYNC_TOKEN": "your_authenticated_token_here"
      }
    }
  }
}

(Note: ScreenSync MCP integrates with local stdio MCP clients including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. ChatGPT's web application requires remote HTTPS endpoints and does not support local stdio processes).

Step 4: Install the Mobile App and Authorize MediaProjection

  1. Download and install the free ScreenSync MCP application (package com.screensync.mcp) from Google Play.
  2. Launch the application. Allow local network mDNS discovery to automatically locate your workstation hub on the local Wi-Fi subnet, or scan the pairing QR code displayed in the hub console.
  3. Tap Start Projection and confirm Android's native MediaProjection screen capture consent prompt.
  4. Unlock Developer Options on your smartphone (Settings > About Phone > tap Build Number 7 times) and enable USB Debugging (or Wireless Debugging). If you prefer wireless operation, review our guide on pairing Android wireless debugging to an MCP server without USB.
  5. Verify ADB connectivity by running adb devices in your workstation terminal.

Step 5: Establish USB Reverse Port Forwarding

To ensure completely uninterrupted video streaming—even when testing airplane mode or operating in radio-shielded hardware labs—configure USB reverse socket forwarding:

adb reverse tcp:3000 tcp:3000

With reverse port forwarding active, MediaProjection video frames flow directly across the physical USB cable to port 3000 on your workstation, guaranteeing sub-100ms display latency regardless of Wi-Fi congestion.

Practical BLE QA Workflows: Scanning, System Pairing Dialogs, GATT Exploration, and Telemetry

Practical BLE testing workflows validate four critical phases: discovering advertised peripherals, confirming system-level Bluetooth pairing dialogs, reading and writing GATT characteristics, and handling connection drop recovery. Claude Code automates these workflows by combining semantic UI tree extraction with real-time MediaProjection frame analysis, touch injection, and Logcat telemetry parsing. If your peripheral streaming also involves audio output or headset disconnections, see our guide on testing Android audio and Media3 with Claude Code and MCP.

Workflow 1: Validating BLE Scan Discovery and RSSI Sorting

The initial phase of any Bluetooth testing suite is confirming that the mobile application discovers nearby advertising peripherals, extracts manufacturer data correctly, and sorts items by signal strength.

Developer Prompt:
"Claude, launch our BLE scanner app com.example.screensync.bluetooth. Verify that Bluetooth scanning initializes within 2 seconds. Tap the 'Start Scan' button. Ensure that at least one peripheral appears in the list within 5 seconds. Assert that the peripheral displays a valid MAC address and an RSSI value between -30 dBm and -95 dBm. If no devices appear, capture logcat and flag whether the scan was throttled."

Step 1: Launch and Extract Layout Hierarchy

Claude Code calls control_launch_app with package com.example.screensync.bluetooth. Once loaded, it calls get_ui_hierarchy to confirm that the activity is in the foreground and locate the "Start Scan" button:

<node text="Start Scan" resource-id="com.example.screensync.bluetooth:id/btn_scan" clickable="true" />

Step 2: Dispatch Scan Tap and Assert Discovered Peripherals

Claude Code invokes control_tap on the coordinates of the "Start Scan" button. It waits 3000 milliseconds and queries get_ui_hierarchy again. It parses the returned nodes inside the LazyColumn:

<node class="android.widget.TextView" text="Smart Sensor 01" />
<node class="android.widget.TextView" text="C4:4F:33:18:A2:9B" />
<node class="android.widget.TextView" text="RSSI: -58 dBm" />

Claude Code confirms that the device was discovered, parses the RSSI integer (-58), and asserts that it falls within expected physical proximity boundaries.

Step 3: Visual Frame Inspection via get_latest_screenshot

To ensure the Compose UI didn't glitch or render cropped card boundaries, Claude Code calls get_latest_screenshot. It inspects the rendered frame captured via MediaProjection, confirming that the radar scan indicator and peripheral cards are crisp, legible, and visually balanced.

Workflow 2: Traversing OS Bluetooth Pairing Request & PIN Dialogs

Secure peripherals require cryptographic bonding. When the app initiates a connection, the Android OS triggers a system pairing prompt.

Developer Prompt:
"Claude, connect to the peripheral with MAC C4:4F:33:18:A2:9B. When the Android system Bluetooth pairing dialog appears, enter the security PIN '123456' and confirm pairing. Verify via get_logcat that bonding succeeds and transitions to BOND_BONDED."

Step 1: Trigger Peripheral Connection

Claude Code locates the "Connect" button adjacent to C4:4F:33:18:A2:9B in the UI hierarchy and calls control_tap.

Step 2: Detect System Pairing Dialog in com.android.settings

Two seconds later, Claude Code calls get_ui_hierarchy. It detects that the top window has transitioned from the test app to the Android settings package:

<node package="com.android.settings" class="android.widget.FrameLayout">
  <node class="android.widget.TextView" text="Bluetooth pairing request" />
  <node class="android.widget.EditText" resource-id="com.android.settings:id/pin_values" focused="true" />
  <node class="android.widget.Button" resource-id="android:id/button1" text="Pair" />
</node>

Step 3: Enter PIN and Tap "Pair"

Claude Code calls control_type with text "123456". It then locates the "Pair" button node (android:id/button1) and calls control_tap.

Step 4: Confirm Bonding in Logcat

Claude Code calls get_logcat with filter BluetoothDevice:

D BluetoothDevice: onBondingStateChanged: device C4:4F:33:18:A2:9B, bondState: BOND_BONDING
D BluetoothDevice: onBondingStateChanged: device C4:4F:33:18:A2:9B, bondState: BOND_BONDED
I ScreenSyncBLE: Peripheral successfully bonded and encrypted.

The agent verifies that the bond state transitioned to BOND_BONDED, confirming cryptographic key exchange.

Workflow 3: GATT Service Exploration and Characteristic Write/Notification

Once bonded, the application negotiates MTU size, discovers GATT services, and subscribes to characteristic notifications.

Developer Prompt:
"Claude, verify that the BLE connection establishes GATT services. Confirm that MTU increases to 512, target characteristic 0000FFE1 is discovered, and telemetry notifications begin streaming. Assert that the incoming telemetry text updates on the screen within 4 seconds."

Step 1: Assert Service Discovery and MTU via Logcat

Claude Code calls get_logcat with filter ScreenSyncBLE:

I ScreenSyncBLE: Connected to GATT server. Requesting MTU 512...
I ScreenSyncBLE: MTU changed to 512 with status: 0
I ScreenSyncBLE: Discovering services...
I ScreenSyncBLE: Target characteristic found. Enabling notifications...

Step 2: Assert Telemetry Render in UI Hierarchy

Claude Code inspects get_ui_hierarchy to assert that the UI composable updated from "No telemetry received" to the live payload string:

<node class="android.widget.TextView" text="Telemetry: TEMP=24.5C;HUM=48%;BAT=98%" />

By verifying both the low-level Logcat stream and the high-level UI render, Claude Code guarantees full end-to-end stack integrity. For a comprehensive overview of autonomous test orchestration, explore our autonomous mobile app testing Claude Code guide.

Workflow 4: Simulating Connection Drops, Radio Toggles, and Reconnection Resiliency

In real-world mobile environments, users walk out of Bluetooth range or accidentally toggle Bluetooth off. Applications must handle connection losses gracefully without crashing.

Developer Prompt:
"Claude, simulate a sudden Bluetooth radio failure while connected. Disable Bluetooth using ADB shell. Assert that the app displays a 'DISCONNECTED' banner within 2 seconds without crashing. Then re-enable Bluetooth, trigger reconnection, and verify that telemetry resumes."

Step 1: Force Disable Bluetooth Radio via ADB

Claude Code executes the system command to power off the Bluetooth adapter:

adb shell cmd bluetooth_manager disable

Step 2: Assert Disconnection UI State

Claude Code immediately calls get_ui_hierarchy and asserts that the status label transitioned to disconnected:

<node class="android.widget.TextView" text="Status: DISCONNECTED" />

It confirms via get_logcat that BluetoothGattCallback.onConnectionStateChange fired with newState=0 and that the app cleanly invoked gatt.close().

Step 3: Re-Enable Bluetooth and Assert Auto-Reconnect

Claude Code turns the Bluetooth radio back on:

adb shell cmd bluetooth_manager enable

It monitors Logcat and UI hierarchy as the application's auto-reconnect logic detects the enabled adapter, scans for the previously bonded peripheral, and re-establishes the GATT session. Developers can evaluate broader battery and CPU impacts during these reconnect cycles by following our guide on automated Android performance profiling with AI.

Bluetooth QA Scenario ScreenSync MCP Tools Used Primary Assertion Mechanism Critical Defect Caught
BLE Scan Throttling get_ui_hierarchy, get_logcat Logcat ScanCallback.onScanFailed App exceeds Android 7.0+ scan frequency limit (5 scans per 30s)
System Pairing PIN Failure get_ui_hierarchy, control_tap, control_type UI hierarchy of com.android.settings Wrong PIN entry or system pairing modal dismissal timeout
GATT Status 133 Collision get_logcat Logcat onConnectionStateChange: status=133 Zombie GATT instances or rapid reconnect without gatt.close()
MTU Truncation Bug get_logcat, get_ui_hierarchy Telemetry string length analysis Failure to request MTU causes 20-byte payload truncation
CCCD Notification Drop control_tap, get_logcat Logcat descriptor write status App forgets to write ENABLE_NOTIFICATION_VALUE to descriptor 0x2902
Radio Toggle Crash ADB shell + get_ui_hierarchy UI crash dialog detection Missing null check on bluetoothAdapter when radio is powered off

Triaging GATT Error 133, Bonding Timeouts, and Logcat Telemetry

Android GATT error 133 represents an internal Bluetooth stack failure caused by exhausted connection handles, rapid unclosed reconnect attempts, or hardware MTU negotiation conflicts. Claude Code triages these failures by streaming system Logcat via get_logcat, diagnosing driver state transitions, and verifying that applications properly invoke BluetoothGatt.close() to release hardware resources.

Every Android developer who has worked with Bluetooth Low Energy has encountered the dreaded GATT Status 133 (GATT_ERROR or 0x85 in hexadecimal). It is the most common, generic, and frustrating error returned by Android's BluetoothGattCallback.onConnectionStateChange.

1. Root Causes of GATT Status 133

Because Android's Bluetooth stack wraps hundreds of distinct underlying link-layer and driver failures into error code 133, diagnosing it requires disciplined telemetry analysis:

  • Exhausted Connection Handles (The 32-Client Limit): Android's native Bluetooth daemon maintains an internal array of GATT client registrations. If an app calls device.connectGatt() repeatedly without calling BluetoothGatt.close() on old instances, the stack runs out of client handles. Once full, every subsequent connection attempt immediately fails with status 133.
  • Rapid Reconnection Collisions: If an app attempts to reconnect immediately upon receiving STATE_DISCONNECTED without inserting a brief backoff delay (e.g., 300 to 1000 milliseconds), the host stack attempts to open a connection while the controller is still tearing down the previous link-layer session.
  • Firmware Timing & Connection Interval Mismatch: If the peripheral firmware rejects the connection parameters requested by Android, or if the peripheral stops acknowledging link-layer supervision packets, the connection times out and Android reports 133.
  • Dual-Mode Transport Ambiguity: On modern smartphones with dual-mode (BR/EDR + BLE) Bluetooth chips, calling device.connectGatt(context, false, callback) without specifying BluetoothDevice.TRANSPORT_LE causes Android to attempt connection over classic Bluetooth first, triggering an immediate 133 rejection on BLE-only peripherals.

2. Autonomous Logcat Triage with Claude Code

When an automated test run fails to connect, Claude Code invokes get_logcat with targeted tag filters (BluetoothGatt, bt_btif, BTAgent):

// LOGCAT TELEMETRY PARSED BY CLAUDE CODE
D BluetoothGatt: connect() - device: C4:4F:33:18:A2:9B, auto: false
D BluetoothGatt: registerApp()
D BluetoothGatt: registerApp() - registering client GATT app
D bt_btif  : btif_gattc_open: status=0
E bt_btif  : gattc_conn_cb: if=5 st=133 reason=0x0133
D BluetoothGatt: onClientConnectionState() - status=133 clientIf=5 device=C4:4F:33:18:A2:9B

Claude Code parses the raw log lines and correlates the failure mode:

  1. clientIf=5: The client interface registration succeeded, proving that Android's handle pool is not exhausted.
  2. reason=0x0133: The failure originated from the link layer controller timing out during the initial encryption handshake.
  3. Remediation: Claude Code modifies the application's connection retry policy, introducing exponential backoff and ensuring that gatt.close() is strictly invoked inside STATE_DISCONNECTED before retrying.
// DEFENSIVE GATT TEARDOWN AND RETRY PATTERN
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) {
        Log.w("ScreenSyncBLE", "Cleaning up GATT handle after status=$status")
        gatt.disconnect()
        gatt.close() // MANDATORY: Free hardware handle in Android OS

        // Exponential backoff before reconnecting
        coroutineScope.launch {
            delay(1000)
            device.connectGatt(context, false, this@BleCallback, BluetoothDevice.TRANSPORT_LE)
        }
    }
}
Logcat Tag & Error Hex Code Underlying Hardware Cause Claude Code Autonomous Remediation
GATT_ERROR 0x85 (133) Exhausted GATT client handles or link-layer timeout Enforce gatt.close() on every disconnect; add 1000ms retry backoff
GATT_CONN_TIMEOUT 0x08 (8) Peripheral went out of range or stopped advertising Check RSSI proximity; assert peripheral battery and power state
GATT_AUTH_FAIL 0x05 (5) Bluetooth bonding key mismatch or PIN rejected Clear bond state via device.removeBond(); re-trigger pairing dialog
GATT_INSUF_AUTHENTICATION 0x0F (15) Read/Write attempted on encrypted characteristic prior to bonding Defer characteristic read until BOND_BONDED state is confirmed
GATT_NO_RESOURCES 0x80 (128) Android internal heap allocation failure in Bluetooth stack Restart Bluetooth stack via cmd bluetooth_manager restart
GATT_INSUF_ENCRYPTION 0x0F (15) Peripheral requires MITM protection not supported by app Update pairing capability to KeyboardDisplay in OS settings

Implementation Checklist for Automated Android Bluetooth Testing

  • [ ] Ensure workstation runs Node.js 18+ with Android Platform Tools adb configured in system PATH.
  • [ ] Unpack the ScreenSync MCP desktop hub and execute start-hub.bat (Windows) or start-hub.sh (macOS/Linux).
  • [ ] Register ScreenSync MCP with Claude Code via claude mcp add screensync node "/path/to/screensync-hub.js".
  • [ ] Install the ScreenSync MCP client (version 2.5.7+) on physical Android 7.0+ hardware from Google Play.
  • [ ] Authorize native MediaProjection screen capture on the Android device for low-latency (~94 ms) local display streaming.
  • [ ] Execute adb reverse tcp:3000 tcp:3000 to guarantee uninterrupted video streaming during wireless peripheral tests.
  • [ ] Verify that runtime permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT) are requested and handled via control_tap.
  • [ ] Validate that BLE scanning initializes with ScanSettings.SCAN_MODE_LOW_LATENCY and populates peripheral cards within 5000 ms.
  • [ ] Automate system-level Bluetooth pairing request modals in com.android.settings using get_ui_hierarchy and control_type.
  • [ ] Confirm that MTU negotiation (requestMtu(512)) and GATT service discovery complete cleanly with status 0.
  • [ ] Triage GATT status 133 errors and connection handle leaks using real-time Logcat filtering via get_logcat.
  • [ ] Test connection recovery by toggling Bluetooth radios with cmd bluetooth_manager disable and verifying auto-reconnection.
Target Bluetooth QA Phase Verification Command / Tool Assertion Mechanism Success Criteria
MCP Hub Registration claude mcp list JSON-RPC stdio handshake ScreenSync MCP registered with 226 tools active
Physical Display Mirror get_latest_screenshot Native MediaProjection stream Live BLE dashboard rendered at ~94 ms median latency
Runtime Permissions get_ui_hierarchy + control_tap Permission dialog node inspection Agent grants BLUETOOTH_SCAN without crashing
Peripheral Discovery get_ui_hierarchy Accessibility node tree dump Peripheral MAC and RSSI appear in Compose LazyColumn
System Pairing Traversal control_type + control_tap System dialog node inspection Enters PIN '123456' and clicks 'Pair' in com.android.settings
GATT Service Discovery get_logcat Logcat filter ScreenSyncBLE State transitions to GATT_SUCCESS (status 0)
MTU 512 Negotiation get_logcat Logcat onMtuChanged MTU size negotiated to 512 without truncation
Telemetry Streaming get_ui_hierarchy Node text value inspection Live sensor string renders on screen within 4000 ms
Radio Failure Recovery ADB shell bluetooth_manager UI status text and Logcat Status updates to DISCONNECTED; auto-reconnects on radio restore
FAQ

Questions, answered

Why can't Android Studio emulators reliably test Bluetooth Low Energy peripherals?

Android Studio emulators lack physical 2.4 GHz Bluetooth radio hardware and rely on synthetic software stubs. These emulators cannot simulate physical RF advertising, distance-based RSSI signal attenuation, or real GATT connection intervals, making physical Android devices with ScreenSync MCP essential for testing authentic wireless behavior.

How does ScreenSync MCP handle Android system Bluetooth pairing dialogs?

While in-process frameworks like Espresso crash because system pairing dialogs belong to out-of-process system packages like com.android.settings, ScreenSync MCP uses get_ui_hierarchy to inspect the full Window Manager node tree. Claude Code reads the dialog, enters the PIN with control_type, and confirms pairing via control_tap.

What causes the infamous Android GATT error 133 during automated testing?

GATT status 133 represents a generic stack failure caused by unclosed BluetoothGatt instances exhausting Android's 32-handle limit, rapid reconnect loops without backoff, or link-layer encryption timeouts. ScreenSync MCP streams Logcat via get_logcat, allowing Claude Code to diagnose the exact root cause and refactor connection retry logic.

Does ScreenSync MCP capture screen frames over ADB or root?

No. ScreenSync MCP captures device displays using Android's native MediaProjection API with one-time user authorization. Frames stream directly to the desktop hub over local Wi-Fi or reverse USB sockets at approximately 94 milliseconds median latency, requiring zero root privileges and consuming zero ADB bandwidth.

How do you test Android 12+ Bluetooth runtime permissions with Claude Code?

Android 12 requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT runtime permissions. When an app launches on a clean device, ScreenSync MCP captures the system permission dialog via get_ui_hierarchy. Claude Code identifies the 'While using the app' button and dispatches control_tap to grant permissions dynamically.

Can Claude Code simulate BLE connection drops and reconnection recovery?

Yes. Claude Code uses ADB shell commands such as cmd bluetooth_manager disable to force-kill the Bluetooth radio on demand. It then asserts that the application displays a disconnected banner without crashing, re-enables the radio, and verifies that the app's auto-reconnect logic restores GATT telemetry.

How does ScreenSync MCP support end-to-end IoT testing with web dashboards?

ScreenSync MCP provides 29 phone and desktop tools alongside 197 web_* browser tools. In connected IoT workflows, Claude Code can trigger a Bluetooth data transfer from a physical sensor to the Android app, then switch to a real Chrome tab to verify cloud data synchronization in an administrative portal.

Can ScreenSync MCP be used with Cursor and VS Code in addition to Claude Code?

Yes. ScreenSync MCP operates as a standard Model Context Protocol server over stdio. It integrates seamlessly with any MCP-compliant developer environment that supports local process execution, including Claude Code, Claude Desktop, Cursor, and VS Code agent mode.

Written by

ScreenSync MCP team

The ScreenSync MCP team at EpsolDev builds the Android app, the desktop MCP hub and the browser extension that let AI agents see and control a real phone. These guides come out of that work.