Open source · MIT · MCP

Let your AI agent debug the WebView on your user’s phone.

Add one script to your H5 page or app WebView. Your user reads out a short device code, and Claude Code, Cursor or any MCP client can see the page, read console and network, and click through the bug — while the user watches and can take over at any time.

  • No USB cable
  • No remote-debugging port
  • No app rebuild beyond one script

01Live demo

A real bug, fixed on a “real” phone

The phone in this demo runs the actual page SDK inside a demo shop. Its gateway is mocked in your browser, so nothing leaves this tab — but every snapshot, click and console line below is real.

Ready

AI agent

connected to the rwd MCP server

Watch an agent connect to the phone, reproduce a checkout bug and find the root cause.

Customer’s phoneK7Q2-9M4X

Runs entirely in your browser: the real SDK (sdk.js) in an iframe, a WebSocket shim that tunnels to an in-page mock gateway, and a scripted agent issuing real RPCs.

02How it works

Three steps from “it’s broken on my phone” to a root cause

  1. Add the page SDK

    One script in <head>, as early as possible, so console and network are recorded from the first line. In an app, inject the same script at document start.

  2. The user shares a device code

    Every phone gets a permanent code like K7Q2-9M4X in a small pill. Users can tap Ask for help, and the device jumps to the top of the agent’s list.

  3. Your agent connects over MCP

    Add the gateway’s hosted MCP endpoint with an API key. Then just describe the problem — the agent finds the device, asks for approval and starts working.

index.html
<!-- In <head>, before your own scripts -->
<script src="https://rwd.example.com/v1/sdk.js"></script>
<script>
  RemoteWebviewDebugger.run("https://rwd.example.com/v1", "rwd_pk_…", {
    userId: currentUser?.id ?? null, // optional: find devices by user
    helpButton: true,                // show the “Ask for help” button
  })
</script>
terminal
claude mcp add --transport http rwd https://rwd.example.com/v1/mcp \
  --header "Authorization: Bearer rwd_sk_live_…"
Device K7Q2-9M4X can’t check out — the total shows “--”. Find out why.

03What the agent gets

The DevTools workflow, through MCP, on someone else’s phone

  • Accessibility snapshots

    Playwright-style YAML with element refs. The agent reads the page as structure, not pixels — and acts on refs.

  • Real interactions

    click, fill, type, press, select, hover, scroll, drag — with actionability checks and events React and Vue accept.

  • Console and network

    Recorded from page load in bounded buffers, so errors from before the agent joined are still there. Bodies on request.

  • Multi-page sessions

    List → detail → checkout, or a second WebView: all pages of a device form one session, with a cross-page timeline.

  • Session changes, pushed

    Every tool result ends with what changed — new page, navigation, an error, the user taking over. No polling.

  • Task card on the phone

    The agent shows its plan and progress on the device, so the user always knows what it is doing and why.

  • Screenshots

    Native pixels when the app provides a bridge, DOM rendering otherwise. Masking applies to both.

  • Any MCP client

    Streamable HTTP at /v1/mcp for Claude Code, Cursor and friends; a tiny stdio bridge for everything else.

All tools 32

  • list_devices
  • connect_device
  • wait_for_device
  • list_pages
  • select_page
  • wait_for_page
  • take_snapshot
  • find_elements
  • take_screenshot
  • click
  • fill
  • type_text
  • press_key
  • select_option
  • hover
  • scroll
  • drag
  • batch_actions
  • navigate_page
  • go_back
  • reload_page
  • wait_for
  • list_console_messages
  • list_network_requests
  • get_network_request
  • get_storage
  • evaluate_script
  • set_task
  • get_session_timeline
  • wait_for_event
  • wait_for_control
  • disconnect_device

04Privacy & control

The user’s phone stays the user’s phone

Remote debugging only works if people trust it. Every session is visible, approved and interruptible.

  • Approval first

    An agent asks to attach; the user sees who is asking and picks a level on the phone. Only real taps count — a script can’t approve itself.

  • Take over or end, any time

    While the agent works, the page is locked behind a glowing edge. One tap on Take over pauses the agent; End disconnects it.

  • Masked by default

    Passwords, and anything marked data-rwd-mask, show as •••• in snapshots, screenshots, console and network data. Add selectors and regex patterns per project.

  • Sensitive clicks need a yes

    Mark buttons with data-rwd-confirm (“Submit order”, “Delete account”) and the user confirms each agent click on them.

  • Permission levels

    observe reads, interact operates, advanced adds scripts, request bodies and storage. Capped per key and per project.

  • Audit log

    Every attach, approval, RPC and takeover is recorded with who, what and when — never page content or parameter values.

05Native apps

In your app, it’s one user script

Download the SDK once and inject it at document start — WKUserScript on iOS, addDocumentStartJavaScript on Android. Your web pages don’t change at all.

  • One device code everywhere

    Pass a stable deviceId (and keep the deviceToken) so the code survives across WebViews and domains.

  • Native controls

    Show your own take over / hand back / end buttons with nativeControl(); page scripts can’t call it.

  • Native approval dialogs

    nativeApproval: true hands attach requests and confirmations to your app’s own UI.

  • Real-pixel screenshots

    A small bridge (WKWebView snapshot, Android bitmap) gives the agent true pixels instead of a DOM rendering.

RemoteDebugger.swift
import WebKit

/// Call before webView.load(…): fetch the SDK once, inject it at document start.
func installRemoteDebugger(into controller: WKUserContentController,
                           userId: String?, done: @escaping () -> Void) {
    let gateway = "https://rwd.example.com/v1"
    URLSession.shared.dataTask(with: URL(string: gateway + "/sdk.js")!) { data, _, _ in
        DispatchQueue.main.async {
            defer { done() }
            guard let data, let sdk = String(data: data, encoding: .utf8) else { return }
            let options: [String: Any] = [
                "deviceId": stableDeviceId, // a UUID kept in UserDefaults
                "userId": userId ?? NSNull(),
                "helpButton": true,
            ]
            let json = String(data: try! JSONSerialization.data(withJSONObject: options), encoding: .utf8)!
            let boot = sdk + "\n;RemoteWebviewDebugger.run(\"\(gateway)\", \"rwd_pk_…\", \(json));"
            controller.addUserScript(WKUserScript(source: boot,
                                                  injectionTime: .atDocumentStart,
                                                  forMainFrameOnly: true))
        }
    }.resume()
}

06Self-hosted

One Go binary. Your servers. Your data.

The gateway embeds the web console and the page SDK. SQLite out of the box, Postgres when you need it, Docker Compose and Caddy for HTTPS.

  • Console

    Devices, API keys, members and roles, QR invites, policies and an audit log — plus an integration guide filled with your own keys.

  • Storage

    SQLite file by default; set RWD_DATABASE_URL=postgres://… to use Postgres.

  • Ops

    Prometheus metrics at /v1/metrics, versioned SDK URLs with SRI, health check at /v1/healthz.

Deployment guide

Quick start
git clone https://github.com/ZaneL1u/remote-webview-debugger
cd remote-webview-debugger

# build the binary (console and SDK embedded), ship it, start it
RWD_DOMAIN=rwd.example.com \
  deploy/platform/deploy.sh you@your-server amd64
# → console  https://rwd.example.com
# → gateway  https://rwd.example.com/v1  (MCP at /v1/mcp)

# or run it locally
make -j16
RWD_ADMIN_PASSWORD=change-me \
  gateway/bin/rwd-darwin-arm64 serve --addr :8080

Building needs Go and Node; the server only needs Docker.

07FAQ

Questions people ask

Does the user have to install anything?

No. For H5 pages the SDK is part of your page. In an app you inject it from native code. The user only sees a small pill with their device code and, when an agent attaches, an approval dialog.

What can the agent see and do?

Only what the user approved: the WebView pages of that device, at the granted level. It never controls anything outside the WebView. Masked fields stay masked, and page content is passed to the model as untrusted data.

Which agents work with it?

Any MCP client. Claude Code and Cursor use the hosted Streamable HTTP endpoint directly; clients that only speak stdio use rwd mcp, a small bridge to the same endpoint.

Which platforms are supported?

Any modern mobile WebView or browser: WKWebView on iOS, Android System WebView, in-app browsers, and plain mobile Safari or Chrome. Desktop browsers work too.

What about bad networks or blocked WebSockets?

The page keeps a light heartbeat WebSocket with exponential back-off. If it can’t connect three times in a row it falls back to a once-a-minute presence poll, and the gateway wakes it up when an agent wants to connect.

Does it slow my page down?

It’s one script. Console and network records live in bounded ring buffers (500 entries each, response bodies capped at 5 MB), and an idle connection only exchanges heartbeats.

Does it work alongside eruda, vConsole or PageSpy?

Yes — their nodes are skipped by snapshots and hit-testing, our own traffic stays out of their network logs, and it can even live as a tab inside eruda.

Is the demo above real?

The SDK is real and unmodified; only the gateway is simulated in your browser. A WebSocket shim tunnels the SDK’s connection to it with postMessage, and a scripted agent sends the same RPCs a real gateway would.