Skip to main content

linc-nerve

Field-tested device control for AI coding agents — the self-healing tricks we learned keeping a physical Android/iOS bench alive, as an MCP server and a plain CLI.

nerve exposes 48 tools — screenshots, the UI element tree, tap/swipe/type, app install/launch, logs, recording — to any MCP client, and the same tools as nerve <tool> commands that print JSON.

Field notes

A device bench fails in ways a single emulator never shows you. Each of these is a behaviour you can read in the code:

  • adb can wedge while looking alive. After a wireless device drops, the adb server can sit in a busy-spin: every adb call hangs, and adb kill-server hangs too because it talks to the same wedged socket. A watchdog treats "unresponsive and server at ≥ 80% CPU" as the wedge signature and recovers by killing the server's PID, never by adb kill-server; an unresponsive server with an idle CPU is left alone. → src/linc_nerve/core/adb_health.py
  • A leaked UiAutomation session silently breaks the element tree. uiautomator dump is killed while another process (a leaked test driver) holds the device's single UiAutomation connection. Recovery is evidence-gated and escalates in tiers under one 45 s deadline, force-stopping only a known allowlist of test drivers — the Pixel Watch companion app, whose package name looks similar, is never touched. An empty element list therefore always means an empty screen, never a failed dump. → src/linc_nerve/core/android_ui.py
  • Readiness checks fail toward "unknown", not "ready". Wake/lock state is parsed from dumpsys, whose format varies by OEM and Android version; an unrecognised format reads as unknown, so a caller skips rather than tapping a locked screen. → src/linc_nerve/core/device_state.py
  • Screenshots handed to an agent are budgeted. Inline images for MCP agents are rescaled to about 300k pixels (≈ 400 vision tokens); the full-resolution file on disk is left untouched for everything else. → src/linc_nerve/mcp/_core.py

Quickstart

Android emulator first — no physical device, Xcode or WebDriverAgent needed.

Prerequisites: Python 3.13+ (uv fetches it for you; with plain pip you need it installed), uv, the Android SDK command-line tools, and platform-tools and emulator on your PATH (adb and emulator; with Homebrew's android-commandlinetools add $(brew --prefix)/share/android-commandlinetools/emulator to PATH). Then create an emulator.

On Apple silicon, replace x86_64 with arm64-v8a in the first two lines below. avdmanager asks whether to create a custom hardware profile; answer no.

sdkmanager "platform-tools" "emulator" "system-images;android-35;google_apis;x86_64"
avdmanager create avd -n nerve -k "system-images;android-35;google_apis;x86_64"
emulator -avd nerve -no-window &
adb wait-for-device

adb wait-for-device returns before Android has finished booting. Wait until adb shell getprop sys.boot_completed prints 1 (usually a minute or so on first boot) before step 1, or the first state call can hit a half-booted device.

Install: pipx install --python python3.13 linc-nerve (or python3.13 -m pip install linc-nerve inside a venv), after which the same commands run as nerve doctor, nerve discover and nerve state --device <serial>; and for Claude Code claude mcp add nerve -- nerve bellows.

1. Check the environment. The daemon, WebDriverAgent and $LINC_HOME (default ~/.linc) are optional, so on a fresh Android-only machine nothing fails (exit code 0); missing optional pieces show as ! warnings with an indented hint: line. The exact set of lines depends on your machine (macOS runs the xcode-clt check, a running daemon shows ✓). Sample from a fresh Linux machine, trimmed:

uvx linc-nerve doctor
  ✓ adb: adb responsive (/usr/local/bin/adb)
  ✓ device-auth: no unauthorized devices
  ✓ scrcpy-server: …/linc_nerve/vendor/scrcpy-server-v3.3.4
  ! wda-project: WDA_PROJECT_DIR not set — iOS tap/swipe/elements unavailable (Android works without it)
      hint: git clone https://github.com/appium/WebDriverAgent.git ~/.linc/wda/src && export WDA_PROJECT_DIR=~/.linc/wda/src
  ! wda-team-id: WDA_TEAM_ID not set — physical-iOS WDA signing will fail
      hint: …
  - xcode-clt: skipped — not macOS
  ! daemon: no nerve daemon on 127.0.0.1:8111 — CLI calls run in-process (the daemon is optional)
      hint: …
  ! linc-layout: missing directories: /home/you/.linc, /home/you/.linc/nerve/recordings — nerve creates them on first use
      hint: …

8 checks: 0 failed, 4 warned

2. Find the emulator.

uvx linc-nerve discover
{"devices": [{"serial": "emulator-5554", "platform": "android", "device_type": "emulator", …, "state": "online"}], "count": 1, "android": 1, "ios": 0}

With several devices attached, devices lists all of them; pass the emulator's serial to the later commands.

3. Read the screen — foreground app, element tree and a screenshot path in one call. Your launcher package and element count depend on the system image, and the screenshot lands in your temp directory ($TMPDIR on macOS, not /tmp):

uvx linc-nerve state --device emulator-5554
{"device": "emulator-5554", "platform": "android", "foreground": {"package": "com.google.android.apps.nexuslauncher", …}, "element_count": 31, "elements": [ … ], "screenshot": "<temp dir>/nerve-output/…png", …}

4. Give it to your agent. For Claude Code:

claude mcp add nerve -- uvx linc-nerve bellows

Run claude mcp list and check that nerve shows as connected, then paste a first prompt:

Take a screenshot of the emulator and tell me what app is in the foreground.

Every MCP tool is also a CLI command: the tool name minus nerve_, with _ → - (nerve_clear_app_data → nerve clear-app-data). Output is JSON on stdout; for text, put --human before the command (nerve --human discover, not nerve discover --human). Exit 0 success, 1 tool error, 2 usage error. nerve schema --json prints the full command tree.

iOS (experimental)

iOS simulators and physical devices work through WebDriverAgent (WDA), which you build once. nerve doctor prints the same steps as hints:

git clone https://github.com/appium/WebDriverAgent.git ~/.linc/wda/src
export WDA_PROJECT_DIR=~/.linc/wda/src
export WDA_TEAM_ID=<Apple Developer Team ID>   # physical devices only; simulators don't need it

Needs macOS with Xcode. Expect rough edges: WDA always binds port 8100, first start takes ~4 s on a simulator and 15–30 s on a physical device, physical devices need code signing, and watchOS is not supported.

Reference

Configuration

Environment variable Default Effect
NERVE_USAGE_LOG redacted Local usage log mode: off, redacted or full (see Trust)
LINC_HOME ~/.linc Where the usage log and recordings live; when set, the device-state database also moves to $LINC_HOME/.local/share/linc-nerve/devices.db (default ~/.local/share/linc-nerve/devices.db)
NERVE_ACTIONLOG_ROOTS $LINC_HOME/nerve/actionlogs Colon-separated list of allowed host directory roots for nerve_actionlog_start and nerve_replay files
WDA_PROJECT_DIR (none) WebDriverAgent checkout (iOS only)
WDA_TEAM_ID (none) Apple Developer Team ID (physical iOS only)
NERVE_HTTP_PORT 8111 Port of the optional HTTP daemon: the port the daemon started by nerve bellows listens on and the port nerve doctor probes (nerve daemon takes --port instead)
LINC_NERVE_DAEMON http://127.0.0.1:8111 Daemon URL that CLI commands call before falling back to in-process
ANDROID_ADB_SERVER_PORT / ADB_SERVER_SOCKET 127.0.0.1:5037 adb server nerve talks to (ADB_SERVER_SOCKET as tcp:<port> or tcp:<host>:<port> wins; loopback hosts only) — isolates nerve from the host's devices
NERVE_ALLOWED_HOSTS / NERVE_ALLOWED_ORIGINS (none) Extra Hosts / browser Origins the daemon accepts

HTTP daemon (optional)

nerve daemon (or nerve bellows, which also serves MCP over stdio) runs an HTTP server on 127.0.0.1:8111: GET /health, GET /devices, POST /tools/<tool_name> with a JSON body, and GET /stream?device=<serial> for a server-sent-events log stream. CLI commands use the daemon when one is running and otherwise run the tool in-process. A few stateful tools (recording, streaming, reservations, diff, session-stats) need the daemon, and say so instead of answering wrongly.

MCP Tools (48)

Tools are registered via @device_tool (requires a device serial) or @global_tool (no device needed) in src/linc_nerve/mcp/*_tools.py. This section — including the count — is generated from those registrations by python -m linc_nerve.docs_gen; tests/test_docs_sync.py fails CI when it is stale. Call nerve_capabilities for the per-platform support matrix.

Discovery & Capabilities

Tool Type Description
nerve_capabilities global Per-platform support matrix for nerve's capability groups
nerve_discover global List all connected devices (Android + iOS)
nerve_pool global Show device pool status — who has what reserved

Screenshots & Streaming

Tool Type Description
nerve_screenshot device Capture one screenshot — a stateless PNG of the current device display
nerve_stream_screenshot device Pull the current frame from an active screen-mirror streaming session
nerve_stream_start device Open a live screen-mirror session for warming up fast screenshots
nerve_stream_status global Report status and health of active screen-mirror streaming sessions
nerve_stream_stop device Tear down the live mirror session opened by nerve_stream_start

UI Inspection

Tool Type Description
nerve_diff device Return UI changes since the last nerve_state snapshot on this device
nerve_elements device Return the UI element tree only (clickable + text widgets) — no image bytes
nerve_state device Capture full device state in one call (screenshot + UI elements + foreground)

Navigation

Tool Type Description
nerve_navigate device Navigate to a named screen in an app using its registered deep link
nerve_navigate_screenshot device Navigate to a screen and capture a screenshot after the UI settles

Input

Tool Type Description
nerve_density device Read display density — dpi and the dp-to-px multiplier — for a device
nerve_key device Press a hardware/system key
nerve_long_press device Long press at screen coordinates
nerve_swipe device Two-point gesture from one xy coordinate to another — swipe, drag, or scroll
nerve_tap device Tap a single point on the device display at known x/y coordinates
nerve_type device Type text into the currently focused input field via the soft keyboard

App Management

Tool Type Description
nerve_apps device List installed apps on the device
nerve_clear_app_data device Wipe an app's data + cache → first-install state
nerve_grant_permission device Grant a runtime permission to an app (Android pm grant) — no dialog
nerve_install device Install an app on the device
nerve_kill device Force-stop / terminate an app
nerve_launch device Launch an app by package/bundle ID, optionally opening a deep link URI
nerve_uninstall device Uninstall an app from the device

Emulator / Simulator

Tool Type Description
nerve_emulator_list global List available Android emulators (AVDs) and iOS Simulators, and their running state
nerve_emulator_start global Start an Android emulator (by AVD name) or iOS Simulator (by UDID/Name)
nerve_emulator_stop device Stop a running Android emulator or iOS Simulator
nerve_inject_sensor device Inject sensor data into an Android emulator via its telnet console

Recording

Tool Type Description
nerve_extract_frames global Extract PNG frames at given timestamps from a host-side MP4 recording
nerve_record_fixed device Record a fixed-length screen video — self-terminating MP4 capture
nerve_record_start device Begin recording video to disk — open-ended MP4 capture on the device
nerve_record_stop device Finalise the MP4 file and end the durable recording session

Action Log

Tool Type Description
nerve_actionlog_start device Start an action log — a JSONL record of every nerve tool call on a device
nerve_actionlog_stop device Stop the action log on a device and report the file and entry count
nerve_replay device Replay an action log on a device and report whether its steps ran

Device Readiness

Tool Type Description
nerve_ensure_ready device Ensure a device is awake + unlocked before UI interaction, and report readiness
nerve_network device Report whether a device is online — default network transport + validated internet

Diagnostics & Observability

Tool Type Description
nerve_inspect device Non-invasive post-mortem snapshot of a process — pid, RSS, crash signature
nerve_logcat device Tail the last N lines from a device's logcat buffer or iOS system log
nerve_metrics global Performance metrics for nerve commands
nerve_session_stats global Get state tracking statistics for all devices
nerve_stream_logs device Get the SSE subscription URL for continuous real-time log streaming
nerve_usage_stats global Usage analytics over the shared MCP call log (~/.linc/mcp-calls.jsonl)

Reservation

Tool Type Description
nerve_release device Release a device reservation
nerve_reserve device Reserve a device exclusively for this session

Other

Tool Type Description
nerve_device_state device Control Android device power/idle/standby state for adversarial testing

Trust

  • No telemetry, no phone-home. nerve talks to your devices, your local adb server, and — for iOS — the WebDriverAgent it starts. Nothing else.
  • A local usage log, under your control. Each MCP tool call is appended to $LINC_HOME/mcp-calls.jsonl (default ~/.linc/mcp-calls.jsonl, rotated at 10 MB) with the tool name, timing, success and parameters. Set NERVE_USAGE_LOG=off|redacted|full:
    • redacted (default) — values of sensitive parameters (text, password, passcode, pin, secret, token, otp) and every value under extras and uri are replaced by their length only (no hash), including inside nested dicts and lists; binary values become their size, and long strings are truncated. The log file is created owner-only (0600).
    • off — nothing is written.
    • full — raw parameter values, including typed text (except nerve_type text sent with secret=true, which is length-only in every mode). Opt in only on a machine you trust.
  • The daemon is loopback-only. It binds 127.0.0.1, rejects non-loopback Hosts (DNS rebinding) and any browser Origin, and accepts tool calls only as application/json. Every value that reaches a device shell is validated or shell-quoted where it is used. Details and how to report a vulnerability: SECURITY.md.

How this repo works

This is a release mirror of LINC's private repository. The code is built by LINC's AI software factory under human review; each release lands as one commit. Issues are welcome; pull requests by arrangement — see CONTRIBUTING.md.


Built by LINC Innovations — we diagnose flaky device test suites.

Licensed under the Apache License 2.0. Bundles scrcpy-server (Apache-2.0) — see NOTICE.

Metadata

Release files for linc-nerve 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for linc-nerve 0.1.0
File Size Uploaded Provenance
linc_nerve-0.1.0.tar.gz 669.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for linc-nerve 0.1.0
File Interpreter ABI Platform
linc_nerve-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.1 MB

Release files / linc_nerve-0.1.0.tar.gz

Download URL linc_nerve-0.1.0.tar.gz
Size 669.4 kB
Tags Source
SHA-256 checksum
How to use checksums
bd0cd52c85d9fd79ac44cfc3c50085d9816a6ba5d22a7e90578f8985a5594eb1
BLAKE2b-256 checksum
How to use checksums
e074d2d8b8fd7509c1403d95942bccea1f74d23c8edd86689f3b5bcca13abef2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / linc_nerve-0.1.0-py3-none-any.whl

Download URL linc_nerve-0.1.0-py3-none-any.whl
Size 392.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
747798cf8c869e4e0f6d49426faada97221cfd55e70fef540afa3fe2d949ca19
BLAKE2b-256 checksum
How to use checksums
3e96e0a6ee09d63da4d6d344a5fc180e80700ea7f591580345dbb0c06ac45f2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Security for linc-nerve 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for linc_nerve-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page