vpnhide/tools/agent-mcp
Danila Gornushko c523e34747
feat(lsposed): add debug host control bridge (#194)
* feat(lsposed): add debug agent bridge

* feat(tools): add agent bridge MCP server

* fix(lsposed): address agent bridge review feedback

* fix(tools): format agent bridge MCP server

* fix(tools): sort MCP server imports

* fix(lsposed): centralize agent bridge lifecycle

* fix(tools): detach adb stdin in agent bridge MCP server

adb subprocesses inherited the server's stdin, which carries the MCP
JSON-RPC frames, so the adb child consumed handshake bytes and wedged
every MCP session. Run adb with stdin=DEVNULL. Switch the server shebang
to the project's uv run --script convention and add smoke-test.py, an
end-to-end MCP handshake check.

* feat(lsposed): move agent control toggle into the Developer section

The agent bridge is debug-only, so its toggle belongs with the other
developer knobs rather than the Debugging tools section. Gate it behind
BuildConfig.DEBUG so release builds (whose AgentControlBridge is a no-op
stub) don't show a dead switch.

* feat(lsposed): ship agent bridge in release, off by default

The bridge was debug-only (real impl in src/debug, no-op stub in
src/release, toggle gated by BuildConfig.DEBUG), which hid it from the
release builds used for development. Move AgentControlBridge into the
main source set so it builds everywhere, drop the BuildConfig.DEBUG
guards, and keep the toggle off by default. Because an enabled bridge
opens a loopback port other apps can use to detect VPN Hide, surface a
dashboard info note while it is on. The host token path falls back to
logcat on release builds, where run-as is unavailable.

* style(lsposed): expression body for startServer

ktlint's function-expression-body rule fires now that the early
BuildConfig.DEBUG return is gone and startServer is a single expression.
2026-06-30 00:31:17 +03:00
..
.gitignore feat(lsposed): add debug host control bridge (#194) 2026-06-30 00:31:17 +03:00
README.md feat(lsposed): add debug host control bridge (#194) 2026-06-30 00:31:17 +03:00
server.py feat(lsposed): add debug host control bridge (#194) 2026-06-30 00:31:17 +03:00
smoke-test.py feat(lsposed): add debug host control bridge (#194) 2026-06-30 00:31:17 +03:00

VPN Hide Agent Bridge

Host-side MCP bridge for driving a VPN Hide build through adb.

The Android app starts a local HTTP bridge on 127.0.0.1:27193 when Settings -> Developer -> Agent control is enabled. It is off by default and ships in release builds too. The bridge is loopback-only and requires a bearer token stored in the app-private file files/agent_bridge_token. While it is on, the dashboard shows a note — leaving the port open is an on-device fingerprint, so turn it off when you are done.

Prerequisites

  • A VPN Hide APK installed on the target device (debug or release).
  • VPN Hide opened at least once.
  • Settings -> Developer -> Agent control enabled.
  • adb can see the device.
  • uv is installed on the host.

For multi-device setups, pass --serial <adb-serial>.

Raw HTTP

Forward the bridge:

adb forward tcp:27193 tcp:27193

Read the token. On a debug build, run-as reaches the app sandbox:

TOKEN="$(adb shell run-as dev.okhsunrog.vpnhide cat files/agent_bridge_token)"

On a release build run-as is unavailable, so read the token the bridge logs once at startup:

TOKEN="$(adb logcat -d -s VpnHideAgentBridge | sed -n 's/.*token=\([A-Za-z0-9_-]*\).*/\1/p' | tail -1)"

List available functions:

curl -s \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:27193/functions

Call a function:

curl -s \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fn":"getStatisticsCaptureBaseline","args":{}}' \
  http://127.0.0.1:27193/call

MCP server

Run directly:

uv run tools/agent-mcp/server.py --serial 3B241FDJG003LP

Example MCP client config:

{
  "mcpServers": {
    "vpnhide": {
      "command": "uv",
      "args": [
        "run",
        "/absolute/path/to/vpnhide/tools/agent-mcp/server.py",
        "--serial",
        "3B241FDJG003LP"
      ]
    }
  }
}

The MCP server fetches /functions from the app at startup and exposes one MCP tool per bridge function. Tool calls are forwarded to POST /call with the same bearer token.

Smoke test

smoke-test.py drives the full host -> MCP -> HTTP bridge -> app path: it launches the server over stdio, performs the MCP handshake, lists tools, and calls one read-only tool. Use it to confirm a debug build with Agent control enabled is reachable:

./tools/agent-mcp/smoke-test.py --serial 3B241FDJG003LP

It exits non-zero on any failure.