npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yhuangsh/adbmcp

v0.7.0

Published

MCP server exposing the Android Debug Bridge (adb) as tools, resources and prompts.

Readme

adbmcp — Android Debug Bridge, as MCP tools

CI npm license

adbmcp is a Model Context Protocol server that exposes the Android Debug Bridge (adb) to AI agents: list devices, run shell commands, install apps, read logcat, take screenshots, drive the UI (tap/swipe/type/key events), query system services with dumpsys, transfer files, forward ports, manage app permissions and pair/connect to devices over Wi-Fi. It is a thin, predictable wrapper around the official adb binary — no re-implemented adb protocol, no test framework, no extra state.

  • Multi-device first — every device-scoped tool takes an optional device serial and every adb call is scoped with adb -s <serial>; ambiguity is an error listing the candidates.
  • Failures an agent can act on — adb stderr/exit codes are mapped to typed causes (NO_DEVICE, MULTIPLE_DEVICES, DEVICE_UNAUTHORIZED, DEVICE_OFFLINE, ADB_SERVER_CONFLICT, TIMEOUT, COMMAND_FAILED) with a hint for the next step.
  • Safety by construction — full MCP annotations on every tool plus server-side guardrails that are enforced even if a host ignores annotations.
  • stdio by default, Streamable HTTP optional — the client launches adbmcp as a subprocess; --http serves the same tools over /mcp for shared-device-host deployments.

Implements the MVP plus v2.0 items 1–7 in docs/design/product-design-v0.md.

Install

Requires Node.js >= 20 and Android platform-tools (adb) on PATH — or ADB_PATH pointing at the binary.

# from npm
npx -y @yhuangsh/adbmcp

# from a checkout
git clone https://github.com/yhuangsh/adbmcp.git && cd adbmcp
npm install
npm run build
node dist/index.js        # speaks MCP on stdin/stdout

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "adb": {
      "command": "npx",
      "args": ["-y", "adbmcp"],
      "env": {
        "ADB_PATH": "/Users/me/Library/Android/sdk/platform-tools/adb"
      }
    }
  }
}

Cursor (.cursor/mcp.json)

{
  "mcpServers": {
    "adb": {
      "command": "node",
      "args": ["/absolute/path/to/adbmcp/dist/index.js"],
      "env": {
        "ADBMCP_ALLOWED_DEVICES": "emulator-5554"
      }
    }
  }
}

Other stdio clients work the same way: a command plus args, with ADB_PATH / ADBMCP_* environment variables passed through. In stdio mode, stdout carries MCP protocol traffic only; diagnostics go to stderr.

Streamable HTTP (shared device host)

Use HTTP when several agents or MCP clients should share one machine's adb server, devices and allowlist — an emulator farm, a lab phone, or a remote desktop — instead of each client launching its own subprocess. stdio stays the default: with no flags, adbmcp speaks MCP on stdin/stdout.

node dist/index.js --http                                    # http://127.0.0.1:3331/mcp
ADBMCP_HTTP_TOKEN=secret node dist/index.js --http --host 0.0.0.0 --port 8080

| Flag / variable | Default | Meaning | | --- | --- | --- | | --http | off | Enable the Streamable HTTP transport. Without it the server is stdio-only. | | --port, ADBMCP_HTTP_PORT | 3331 | Listen port (--port wins over the env var). 0 asks the OS for an ephemeral port; the real URL is logged to stderr. | | --host, ADBMCP_HTTP_HOST | 127.0.0.1 | Bind address (--host wins over the env var). | | ADBMCP_HTTP_TOKEN | unset | When set, every /mcp request must send Authorization: Bearer <token>. Unset is allowed only on a loopback host. | | ADBMCP_HTTP_ALLOWED_HOSTS | derived | Comma-separated exact Host header values (host[:port]) for DNS-rebinding protection. Overrides the derived loopback/bound-host list. |

The endpoint is stateful: initialize creates a session, Mcp-Session-Id identifies it, and DELETE /mcp closes it. All sessions share one device host, one ADBMCP_ALLOWED_DEVICES allowlist and one logcat stream registry — streams started in one session can be read from another. Sessions are the same trust domain, not isolation between agents.

Client config (Claude Desktop, Cursor, or any Streamable HTTP client):

{
  "mcpServers": {
    "adb": {
      "type": "http",
      "url": "http://localhost:3331/mcp"
    }
  }
}

When ADBMCP_HTTP_TOKEN is set, add "headers": { "Authorization": "Bearer <token>" } to the server entry (client support for header config varies).

Security notes:

  • Loopback by default. Binding a non-loopback host without ADBMCP_HTTP_TOKEN is refused at startup; the error explains the two ways forward (set a token, or bind 127.0.0.1).
  • Token auth is pre-MCP. With a token configured, missing/incorrect credentials get 401 before any MCP session handling; comparison is constant-time (node:crypto timingSafeEqual).
  • DNS-rebinding protection. The SDK transport rejects requests whose Host header is not the bound host or a loopback alias (localhost, 127.0.0.1, ::1) on the actual port. Override with ADBMCP_HTTP_ALLOWED_HOSTS when a reverse proxy forwards a different hostname.
  • One device host, many agents. shell is available to every session; on a shared host prefer ADBMCP_ALLOWED_DEVICES and ADBMCP_SHELL_ALLOWED_PREFIXES, and put the server behind the token.
  • No in-process TLS. The server speaks plain HTTP; terminate TLS in a reverse proxy for non-loopback deployments and set ADBMCP_HTTP_ALLOWED_HOSTS to the forwarded Host value.
  • Only /mcp is served; POST bodies are capped at 1 MiB (413 above that), unsupported methods get 405, and unknown paths get 404.

Deploying over a VPN mesh (Tailscale example)

A common topology: your agent runs on a public server, and one or more device hosts (machines with USB-attached phones) are joined to a private mesh such as a Tailscale tailnet. Nothing needs to be exposed to the public internet — the agent reaches device hosts through the mesh.

Shape A — bind the mesh interface directly. On each device host, set the token, bind to the mesh IP (or 0.0.0.0), and allow the hostname the agent will dial:

ADBMCP_HTTP_TOKEN=<token> ADBMCP_HTTP_ALLOWED_HOSTS='phonehost.tailnet-name.ts.net:3331,127.0.0.1:3331,localhost:3331' \
  adbmcp --http --host 0.0.0.0 --port 3331

WireGuard encrypts mesh traffic; the bearer token stays as the application-level gate so a misconfigured ACL or a compromised mesh member still cannot touch your devices. ALLOWED_HOSTS must contain the exact Host header the agent sends — MagicDNS name and port. Note the override replaces the derived loopback defaults, so loopback entries are re-added for local health checks.

Shape B — loopback + tailscale serve (nice for always-on hosts). Keep adbmcp bound to 127.0.0.1 (optionally with a token still set), then let Tailscale expose it:

adbmcp --http --host 127.0.0.1 --port 3331 &
tailscale serve --bg --https=443 http://127.0.0.1:3331   # --https=443 is the default mode

Keep the token set even in this shape — tailscale serve terminates TLS and limits reachability to the tailnet, but adbmcp should still authenticate its clients. Point the agent at https://phonehost.tailnet-name.ts.net/mcp (tailscale serve passes the original Host header through, so the portless ADBMCP_HTTP_ALLOWED_HOSTS entry is correct here).

Caveats that apply to both shapes: one adbmcp node per device host (the agent configures one MCP server entry per host — there is no cross-host device registry); sessions live until the client deletes them or the process restarts, so a device host that suspends or redeploys drops its logcat streams and agents must re-initialize; and provision the token to the agent out-of-band (secret manager, not chat).

Tools

device is optional on every device-scoped tool and required when more than one device is connected. Annotation columns: RO = readOnlyHint, D = destructiveHint, I = idempotentHint, O = openWorldHint.

| Tool | What it does | RO | D | I | O | | --- | --- | --- | --- | --- | --- | | list_devices | adb devices -l: serial, state, model, allowlist status | ✅ | — | ✅ | — | | get_device_info | Curated build properties (model, Android version, SDK, ABI) + screen size/density | ✅ | — | ✅ | — | | dumpsys_query | Curated dumpsys dump — battery/activity/window/connectivity, capped at 64 KB | ✅ | — | ✅ | — | | shell | Arbitrary adb shell command — the power-user escape hatch, guardrailed | — | — | — | ✅ | | list_packages | pm list packages, optional substring filter | ✅ | — | ✅ | — | | install_apk | adb install a host APK, replace for -r | — | — | — | — | | uninstall_package | adb uninstall — destructive, keep_data for -k | — | ✅ | ✅ | — | | grant_permission | pm grant — destructive: grants a runtime permission | — | ✅ | ✅ | — | | revoke_permission | pm revoke — destructive: the app loses the permission | — | ✅ | ✅ | — | | clear_app_data | pm clear — destructive, refused unless the package is in ADBMCP_CLEAR_ALLOWED_PACKAGES | — | ✅ | ✅ | — | | logcat_snapshot | Bounded adb logcat -d, optionally filtered | ✅ | — | ✅ | — | | start_logcat_stream | Start a long-lived adb logcat, returns a stream_id | ✅ | — | — | — | | read_logcat_stream | Drain buffered output since the last read | ✅ | — | — | — | | stop_logcat_stream | Stop the stream, return unread output | ✅ | — | ✅ | — | | screenshot | exec-out screencap -p, returned as PNG image content | ✅ | — | ✅ | — | | pull_file | adb pull to the host; refuses to overwrite unless overwrite: true | — | — | — | — | | push_file | adb push; remote path restricted to allowed roots | — | — | — | ✅ | | forward_port | adb forward (e.g. tcp:8080 → tcp:8080) | — | — | ✅ | — | | remove_forward | adb forward --remove | — | — | ✅ | — | | reverse_port | adb reverse — the device listens on remote and forwards to host local | — | — | ✅ | — | | remove_reverse | adb reverse --remove (takes the remote spec) | — | — | ✅ | — | | list_forwards | adb forward --list as JSON {serial, local, remote} | ✅ | — | ✅ | — | | list_reverses | adb reverse --list as JSON {serial, remote, local} | ✅ | — | ✅ | — | | connect | adb connect host:port — TCP/IP wireless without pairing | — | — | ✅ | ✅ | | disconnect | adb disconnect [host:port]; no address disconnects every TCP/IP device | — | — | ✅ | ✅ | | wireless_pair | adb pair host:pair_port code — Android 11+ pairing, step one of connect | — | — | ✅ | ✅ | | start_activity | am start by component or action, wait for -W | — | — | — | — | | launch_package | Resolve a package's LAUNCHER activity, then am start -n; returns the component, wait for -W | — | — | — | — | | force_stop_app | am force-stop | — | — | ✅ | — | | get_ui_hierarchy | uiautomator dump XML with bounds/ids/text; to_file dumps to /data/local/tmp and returns {path, bytes} for pull_file | ✅ | — | ✅ | — | | tap | input tap x y — tap a screen coordinate (device pixels) | — | — | — | — | | swipe | input swipe x1 y1 x2 y2 [duration_ms] — scroll or drag | — | — | — | — | | type_text | input text with %s space encoding and device-shell quoting; non-ASCII (e.g. CJK) goes through the ADB_INPUT_B64 broadcast when a capable IME (ADB Keyboard) is active — see Non-ASCII text input | — | — | — | — | | key_event | input keyevent — named key (HOME, BACK, …) or keycode 0–300 | — | — | — | — | | adb_server_restart | kill-server + start-server recovery for version conflicts | — | — | — | — |

Also shipped:

  • Resource adb://devices — live device list as JSON (serial, state, model, allowed). State is never cached; re-read the resource to refresh.
  • Prompts — debug_crash, install_and_launch, capture_bug_report: step-by-step orchestrations of the tools above.

uninstall_package, grant_permission, revoke_permission and clear_app_data are the tools with destructiveHint: true; shell, push_file, connect, disconnect and wireless_pair are the openWorldHint: true tools. They are never merged with read-only tools, so hosts can auto-approve reads and confirm writes.

Non-ASCII text input

Android's input text is ASCII-only: typing non-ASCII text (e.g. CJK) through it crashes on the device with a NullPointerException instead of typing anything (issue #3). type_text therefore has two paths:

  • ASCII text (default and unchanged) is sent as input text with %s space encoding and device-shell quoting — one round trip, no extra device calls.
  • Non-ASCII text is sent as am broadcast -a ADB_INPUT_B64 --es msg <base64-utf8> to the active input method. This requires a broadcast-capable IME to be the active IME; the reference implementation is ADB Keyboard (com.android.adbkeyboard/.AdbIME). On every non-ASCII call adbmcp queries settings get secure default_input_method (never cached) and broadcasts only when the active IME matches a configured capable component.

One-time setup, run from your own shell (adbmcp detects the IME but deliberately never installs or switches IMEs on its own):

adb install ADBKeyboard.apk                      # from the ADBKeyBoard releases
adb shell ime enable com.android.adbkeyboard/.AdbIME
adb shell ime set com.android.adbkeyboard/.AdbIME   # must be the ACTIVE input method

Failure modes, both typed and actionable instead of leaking the device-side NPE:

  • [IME_UNAVAILABLE] — the text is non-ASCII but the active IME is not broadcast-capable (or no IME is selected). The message names the active IME and the hint gives the ime enable / ime set commands above.
  • [TEXT_NOT_ASCII] — ADBMCP_TYPE_TEXT_NON_ASCII=reject opts out of the broadcast path, so non-ASCII text is refused before any device call.

IMEs other than ADB Keyboard can be declared broadcast-capable with ADBMCP_IME_BROADCAST_COMPONENTS (see the table below); both the short pkg/.Class form and the flattened pkg/pkg.Class form match.

Configuration

| Variable | Default | Meaning | | --- | --- | --- | | ADB_PATH | adb (from PATH) | Path to the adb binary. Must match the version of any running adb server. | | ADBMCP_ALLOWED_DEVICES | unset (all allowed) | Comma-separated serial allowlist. Operations on other serials are refused. | | ADBMCP_SHELL_BLOCKED_PATTERNS | reboot, su, mkfs, wipe, rm of / (split/reordered flags and /* included), pipelines into sh/bash | Comma-separated JavaScript regex sources tested against the whole shell command. Overriding replaces the defaults. | | ADBMCP_SHELL_ALLOWED_PREFIXES | unset (no prefix restriction) | When set, a shell command must start with one of these comma-separated prefixes. | | ADBMCP_PUSH_ALLOWED_ROOTS | /sdcard, /storage/emulated/0, /data/local/tmp | Remote roots push_file may write to. | | ADBMCP_CLEAR_ALLOWED_PACKAGES | unset (refuse every clear) | Comma-separated package allowlist for clear_app_data. There is no allow-all mode: unset or empty refuses every clear. | | ADBMCP_TYPE_TEXT_NON_ASCII | ime | How type_text handles non-ASCII text (see Non-ASCII text input): ime sends the ADB_INPUT_B64 broadcast when a capable IME is active, reject fails with [TEXT_NOT_ASCII]. | | ADBMCP_IME_BROADCAST_COMPONENTS | com.android.adbkeyboard/.AdbIME | Comma-separated IME component ids accepted as broadcast-capable for the ADB_INPUT_B64 path. pkg/.Class and flattened pkg/pkg.Class forms both match. | | ADBMCP_TIMEOUT_MS | 30000 | Default subprocess timeout (timeout_ms per call overrides it). | | ADBMCP_MAX_OUTPUT_BYTES | 65536 | Head+tail byte cap per call; the dropped middle is reported inline. | | ADBMCP_LOGCAT_BUFFER_BYTES | 1048576 | Ring-buffer cap per logcat stream; overflow is reported as dropped_bytes. | | ADBMCP_HTTP_PORT | 3331 | HTTP mode listen port (equivalent to --port; the flag wins). | | ADBMCP_HTTP_HOST | 127.0.0.1 | HTTP mode bind address (equivalent to --host; the flag wins). | | ADBMCP_HTTP_TOKEN | unset | Bearer token required on every /mcp request when set. Non-loopback binds without it are refused. | | ADBMCP_HTTP_ALLOWED_HOSTS | derived | Comma-separated exact Host header values accepted by DNS-rebinding protection. |

Safety notes

  • Annotations are advisory, guardrails are authoritative. The host decides what to auto-approve; adbmcp enforces its own limits regardless: blocked shell patterns, the device allowlist, remote push roots, and the pull overwrite check.
  • shell is arbitrary code execution on the device. It is annotated as not read-only and open-world, blocklisted by default, and documented as the escape hatch — keep shell disabled or prefix-restricted (ADBMCP_SHELL_ALLOWED_PREFIXES) on shared or CI machines. The default blocklist covers reboot/power-state changes, su, mkfs, wipe, rm of / (including split/reordered flags and the /* glob), and pipelines into sh/bash such as curl … | sh or … | sudo bash. This is a guardrail, not a sandbox: a deliberately obfuscated command can still get through. A pattern-based guardrail: matching runs on the raw command text, including inside quotes, so even prose like echo "use curl | sh to install" is refused — precision always yields to safety here. maintainer can replace the patterns with ADBMCP_SHELL_BLOCKED_PATTERNS.
  • uninstall_package deletes app data unless keep_data: true. clear_app_data deletes app data permanently and is refused unless the package is listed in ADBMCP_CLEAR_ALLOWED_PACKAGES (unset/empty refuses every clear). grant_permission / revoke_permission change an app's granted permissions. All four are marked destructiveHint: true.
  • Device scoping is explicit. With multiple devices connected, calls without device fail with MULTIPLE_DEVICES and the candidate serials instead of guessing.
  • Filesystem writes are bounded. pull_file never overwrites silently; push_file refuses writes outside the allowed remote roots and rejects .. traversal and relative paths. Root containment is lexical: it does not resolve on-device symlinks, so a link pre-created inside an allowed root (e.g. /data/local/tmp/x -> /data/system) can still redirect a push outside it.
  • No privilege escalation. adbmcp never runs adb root/su itself and never retries a permission failure with elevation.
  • The adb server is shared host state. adbmcp never kills or restarts it implicitly; adb_server_restart exists for the version-conflict case and says so in its description.
  • Logcat streams are cleaned up. Streams are killed on stop_logcat_stream, and all remaining streams are terminated on SIGINT/SIGTERM (server shutdown).

Development

npm install
npm run build      # tsc -> dist/
npm test           # vitest, no adb binary or device required
npm run typecheck

Tests drive a fake adb runner and a fake stream spawner, so the whole suite runs without platform-tools, a device or an emulator. Layout:

src/
  index.ts            entrypoint: stdio by default, --http for Streamable HTTP
  http.ts             HTTP sessions, bearer auth, loopback guard, DNS-rebinding allowlist
  server.ts           assembles config + runner + tools/resources/prompts
  config.ts           env config, shell/device/push/clear guardrails
  devices.ts          device parsing, resolution rule, list_devices tool
  adb/runner.ts       subprocess per call: -s scoping, timeouts, head+tail caps
  adb/errors.ts       typed causes mapped from adb stderr/exit codes
  streams/registry.ts long-running logcat handles + ring buffer
  tools/              one module per tool group (35 tools, MVP + v2.0 items 1–6 + v0.6.0 additions)
  resources/devices.ts, prompts/index.ts
test/                 vitest suites (fakes in test/helpers/)

Status

Implements the MVP plus v2.0 items 1–7 from the design (§4.2): reverse port forwards and structured forward/reverse listings, pm grant/revoke plus allowlist-gated clear_app_data, wireless TCP/IP connect/disconnect/wireless_pair, first-class input injection (tap, swipe, type_text, key_event — typed schemas that fix the input text quoting footgun, no DSL), the curated dumpsys_query, and the pulled-forward Streamable HTTP transport. v0.4.0 was a hardening/polish release (no new tools): shell guardrails now also block pipe-to-shell payloads and split/globbed root-deleting rm, address-targeted disconnect failures are reported as typed errors, and reverse_port rejects forward-only device-side specs pre-spawn. v0.5.0 adds Streamable HTTP (/mcp, stateful sessions, one shared device/stream context, optional bearer auth, loopback binding guard, DNS-rebinding protection). v0.6.0 adds launch_package (resolve a package's launcher activity and start it, returning the component) and get_ui_hierarchy to_file (keep large dumps on-device as {path, bytes} for pull_file instead of returning structurally truncated inline XML). v0.6.1 teaches type_text non-ASCII (CJK) input: text rides the ADB_INPUT_B64 broadcast to a capable IME (ADB Keyboard by default) when one is the active input method, with typed IME_UNAVAILABLE/TEXT_NOT_ASCII errors otherwise (ADBMCP_TYPE_TEXT_NON_ASCII chooses the posture). v0.7.0 moves onto the MCP TypeScript SDK v2 packages and serves both protocol revisions: legacy clients keep stateful Streamable HTTP sessions while 2026-07-28 clients get stateless serving (server/discover, resultType, per-request _meta), same guardrails on both paths. Not implemented: screenrecord, bugreport, iOS, emulator lifecycle management. Windows is best-effort (POSIX-first, like most of the ecosystem).

Contributing

Bug reports and use-case-driven feature requests are welcome — see CONTRIBUTING.md for what makes a good issue, how triage works, and the development setup.