A standalone MCP server that lets AI agents control a physical iPhone over the network. Built on Apple's WebDriverAgent and optionally Tailscale for secure remote access from anywhere — not just your LAN.
Tap buttons, swipe through apps, take screenshots, type text, and inspect UI elements — all through natural language via any MCP-compatible client.
WDA-MCP needs full read+write MCP support: viewing the screen plus tapping, typing, and swiping. Read-only MCP = can see but can't control.
| You want to use | Entry point | Notes |
|---|---|---|
| Claude Code / Claude Desktop | server.py stdio |
Local MCP, the most stable path. |
| claude.ai web chat | server.py --http + HTTPS tunnel |
Good free chat-based control path. |
| Codex | server.py stdio |
Add wda to your Codex MCP config. |
| ChatGPT web / mobile browser web | server_chatgpt.py + HTTPS /mcp |
Uses ChatGPT Developer Mode. See CHATGPT.md. |
| ChatGPT iOS/Android native app | Not recommended yet | Treat the web version as the stable test path. |
If Claude already works for you, keep that configuration. ChatGPT has its own
server_chatgpt.pyentry point so the two paths don't get mixed together.
| Product | Custom MCP? | Read+Write? | Notes |
|---|---|---|---|
| claude.ai (web) | ✅ | ✅ | Best low-friction chat entry point. |
| Claude Desktop | ✅ | ✅ | Good desktop option. |
| Claude Code (CLI) | ✅ | ✅ | Best for development and debugging. |
| Product | Custom MCP? | Read+Write? | Min. plan | Recommended entry |
|---|---|---|---|---|
| ChatGPT web | ✅ Developer Mode | ✅ | Plus ($20/mo) | server_chatgpt.py + HTTPS /mcp |
| ChatGPT in a mobile browser | ✅ worth trying | ✅ | Plus | Same as above |
| ChatGPT native mobile app | ❌ write blocked | ❌ | — | Use mobile browser (chatgpt.com) instead |
| Codex CLI / Codex App | ✅ | ✅ | — | server.py stdio |
Custom MCP requires Developer Mode, available on Plus, Pro, Team, Enterprise, and Edu plans. Free and Go plans cannot add custom MCP servers (Go only gets OpenAI's built-in connectors). With Developer Mode on, all eligible plans get full read+write MCP — no per-plan restriction. Write actions may show a confirmation dialog.
| Product | Custom MCP? | Read+Write? | Notes |
|---|---|---|---|
| Gemini CLI | ✅ | ✅ | Gemini web does not support custom MCP. |
| Mistral Le Chat | ✅ | ✅ | Useful free chat entry point to test. |
| Cursor / Windsurf / Cline | ✅ | ✅ | Good inside development environments. |
Tools are organized by usage scenario. Set WDA_TOOLS env var to load only the groups you need — saves AI context tokens.
| Group | When | Tools |
|---|---|---|
setup |
First time / maintenance | wda_status wda_start wda_renew |
learn |
Scan an app's UI once, reuse cached coordinates | wda_learn_app |
core |
Everyday phone control (13 tools) | See below |
wechat |
Read/send WeChat messages in one call | wda_wechat_read wda_send_wechat |
util |
Notifications, clipboard, long press | wda_long_press wda_notifications wda_clipboard |
Core tools (13) — view, tap, type, navigate
| Tool | What it does |
|---|---|
wda_check |
See the screen: app + all visible text. Add detail=True for device/battery info |
wda_screenshot |
Capture screen as PNG (use wda_check first to save tokens) |
wda_source |
Full UI element tree as XML |
wda_find |
Search elements by text → tap coordinates |
wda_tap |
Tap a point (x, y) |
wda_tap_text |
Find element by text and tap it (auto-retries 3x) |
wda_type |
Type text into focused input |
wda_swipe |
Swipe between two points |
wda_scroll |
Scroll: down / up / left / right |
wda_home |
Go to home screen |
wda_back |
Go back (iOS edge swipe) |
wda_launch |
Open any app via Spotlight |
wda_open_url |
Open a URL in Safari |
By default all groups are loaded. To reduce token overhead, set WDA_TOOLS in your .env:
# Only basic phone control (12 tools)
WDA_TOOLS=core
# Phone control + WeChat (15 tools)
WDA_TOOLS=core,wechat
# Everything (22 tools, default)
# WDA_TOOLS=Device compatibility: All coordinates auto-calibrate on first use — works on any iPhone model and iOS version.
Instead of calling wda_find every time you interact with an app, scan its layout once and reuse cached coordinates:
> Learn WeChat's layout
wda_learn_app("微信")
→ Tab bar: 微信(49,807) 通讯录(147,807) 发现(245,807) 我(343,807)
→ Nav: title(196,75) +(349,75)
→ Search(196,125)
→ Saved to app_layouts/com.tencent.xin.json
Cached positions (tab bars, input fields, send buttons) are reused by wda_send_wechat and wda_wechat_read — dynamic elements like chat list order are still found in real time.
Read and send WeChat messages in a single MCP round-trip:
> Read recent messages
wda_wechat_read("Alice", count=10) # ~6-8s first time, 3s repeat
> Send a message
wda_send_wechat("Alice", "Hello!") # ~5s with verify
> Read then reply (navigation auto-skipped)
wda_wechat_read("Alice") # 3s — stays in chat
wda_send_wechat("Alice", "Got it!") # 5s — skips navigation
Performance (iPhone → Tailscale → server, measured end-to-end):
| Operation | Time | Notes |
|---|---|---|
| Read (first time) | ~6-8s | Navigate + 1 source call |
| Read (same contact) | 3s | Already in chat, 1 source call |
| Send (after read) | 5s | Skip navigation + verify |
| Send (verify=False) | ~2s | Skip navigation + no verify |
| Read → Send chain | ~8s | Navigation auto-skipped for send |
The bottleneck is WDA's source API (~1.7s per call on iPhone — UI tree traversal). All sleeps between tap/type/send have been removed; WDA requests are synchronous.
_current_chatstate: after read/send, a follow-up to the same contact skips navigation entirelyverify=False: skip post-send source call to save ~3s- Screenshot fallback: if verification fails, a debug screenshot is saved automatically
- macOS with Xcode installed
- iPhone connected via USB (for initial setup) or Tailscale (for remote)
- Python 3.12+ with
mcp[cli]installed - Python 3.13 (for remote start via Tailscale — the TCP tunnel requires Python 3.13's SSL PSK support)
- pymobiledevice3 installed on both Python 3.12 and 3.13
- A free Apple Developer account (for code signing)
bash scripts/check.sh # without iPhone check
bash scripts/check.sh <TAILSCALE_IP> # with iPhone connectivity checkThis verifies Python versions, pymobiledevice3, Tailscale, and optionally iPhone reachability. Fix anything marked ❌ before continuing.
pip install "mcp[cli]"
brew install pymobiledevice3 # optional, for device discoverygit clone https://github.com/appium/WebDriverAgent.git ~/Desktop/WebDriverAgent
cd ~/Desktop/WebDriverAgent
⚠️ Manual step required — this is the one step that cannot be fully automated with a free Apple ID. You must open Xcode and configure signing by hand.
Open WebDriverAgent.xcodeproj in Xcode:
- Select the WebDriverAgentRunner target
- Under Signing & Capabilities, choose your Apple ID team
- Set a unique Bundle Identifier (e.g.
com.yourname.WebDriverAgentRunner) - Build for your connected device:
xcodebuild build-for-testing \
-project WebDriverAgent.xcodeproj \
-scheme WebDriverAgentRunner \
-destination "id=$(xcrun xctrace list devices | grep iPhone | head -1 | grep -oE '[A-F0-9-]{25,}')" \
-allowProvisioningUpdatesGo to Settings → General → VPN & Device Management and trust the developer certificate.
cp config.example.env .env
# Edit .env with your device UDID and Tailscale IPFind your device UDID:
xcrun xctrace list devicesClaude and Codex use the original server.py entry point:
# stdio mode (for Claude Code / MCP clients)
python server.py
# HTTP mode (port 8200)
python server.py --httpChatGPT uses the dedicated server_chatgpt.py entry point in CHATGPT.md.
Add to your .mcp.json:
{
"mcpServers": {
"wda": {
"command": "python",
"args": ["/path/to/wda-mcp/server.py"],
"env": {
"WDA_TAILSCALE_IP": "100.x.x.x",
"WDA_DEVICE_ID": "your-device-udid",
"WDA_BUNDLE_ID": "com.yourname.WebDriverAgentRunner.xctrunner"
}
}
}
}Codex reads MCP servers from ~/.codex/config.toml. If your python is not Python 3.12+, set command to your full Python 3.12 path.
[mcp_servers.wda]
command = "python3.12"
args = ["/path/to/wda-mcp/server.py"]
cwd = "/path/to/wda-mcp"
startup_timeout_sec = 20
tool_timeout_sec = 300Restart Codex, then check /mcp or codex mcp list.
Goal: Prove WDA itself works: signing, certificates, UDID, Bundle ID, Xcode build, and WDA launch. Do not debug AP isolation, Tailscale, or 5G yet.
Pass criteria: the automation screen appears on the iPhone. If the Mac and iPhone are also on the same WiFi, wda_status and wda_screenshot return normally.
- Connect iPhone to the Mac over USB and keep it unlocked. The phone may stay on home WiFi, but do not unplug USB during this stage.
- Confirm the iPhone trusts the computer and the developer certificate under Settings → General → VPN & Device Management.
- Fill
.envwithWDA_DEVICE_ID,WDA_BUNDLE_ID, andWDA_PROJECT_DIR;WDA_TAILSCALE_IPcan stay empty for now. - Start the MCP server and call
wda_start(), or runWebDriverAgentRunnerdirectly in Xcode. - When the automation screen appears on the iPhone, WDA itself has passed the first gate. If the Mac and iPhone are also on the same WiFi, you can also test
wda_statusandwda_screenshot.
If stuck, check:
- The iPhone trusts the computer and developer certificate.
WDA_DEVICE_ID,WDA_BUNDLE_ID, andWDA_PROJECT_DIRare correct.- WDA builds in Xcode, and the Bundle ID matches the signing profile.
- The phone is unlocked, Auto-Lock is disabled, and
remotedis not stuck.
If USB validation fails, do not debug WiFi/5G yet.
Goal: Move from "WDA launches over USB" to "after unplugging USB, the Mac can still reach the iPhone over the same WiFi." Many first-time setups fail here, so only debug LAN reachability and Xcode wireless pairing at this stage.
Pass criteria: Connect via network is enabled in Xcode; after unplugging USB, the Mac can ping <iPhone WiFi IP> and curl http://<iPhone WiFi IP>:8100/status returns WDA status.
Keep these three checks separate:
| Goal | What you are testing | Common causes of failure |
|---|---|---|
| LAN direct access | Mac can reach http://<iPhone WiFi IP>:8100/status |
Different subnet, guest WiFi, AP/client isolation, dual-homed Mac/wrong route, VPN/proxy routing |
| Tailscale access | Mac can reach http://<iPhone Tailscale IP>:8100/status |
iPhone Tailscale offline, another VPN taking over, UDP blocked |
| Wireless WDA launch | Mac can reach the iPhone RemotePairing port 49152 |
Xcode network pairing not done, Tailscale/network handoff interrupted, locked phone, stuck remoted |
Do not unplug USB and leave immediately. Validate this at home first:
- Keep USB connected and confirm WDA is running on the iPhone.
- Put Mac and iPhone on the same normal WiFi / same router. Avoid guest, campus, hotel, or isolated office networks. If the Mac is on Ethernet and WiFi at the same time, temporarily disconnect the unrelated network.
- In Xcode, open Window → Devices and Simulators → select iPhone → enable Connect via network. Apple's wireless device pairing guide pairs over USB first, then disconnects the cable.
- Find the iPhone WiFi IP in Settings → WiFi → current network
i. - Test LAN direct access from the Mac:
ping <iPhone WiFi IP> route -n get <iPhone WiFi IP> | grep -E 'interface|gateway|source' curl -m 3 http://<iPhone WiFi IP>:8100/status
- If using Tailscale, test that path too:
tailscale status tailscale ping <iPhone Tailscale IP> curl -m 3 http://<iPhone Tailscale IP>:8100/status
- If you need to start/restart WDA wirelessly, test RemotePairing:
python3.12 - <<'PY' import socket ip = "<iPhone Tailscale IP or WiFi IP>" s = socket.socket() s.settimeout(3) s.connect((ip, 49152)) print("RemotePairing OK") PY
Apple's wireless device troubleshooting guide also recommends confirming the device is paired, Mac and device are on the same network, and ping <device IP> works.
If stuck, check:
- Mac and iPhone are on the same normal WiFi / same router, not guest, campus, hotel, or isolated office WiFi.
- If
ping <iPhone WiFi IP>fails, suspect different subnets, AP/client isolation, or a dual-homed Mac using the wrong route. - If the Mac is on Ethernet and WiFi at the same time, temporarily disconnect the unrelated network and test again.
- Xcode really completed Connect via network; if unsure, pair over USB again.
curl :8100only proves you can reach an already-running WDA. Wireless start/restart also needs49152.
Goal: Let Claude/Codex/MCP reliably control the iPhone on the same WiFi/router. 5G and VPS both build on this layer.
Pass criteria: curl http://<iPhone WiFi IP>:8100/status reliably returns WDA status, and wda_status / wda_screenshot work.
- Put iPhone on WiFi and Mac on the same WiFi/router.
- Start WDA over USB or Xcode and wait until the automation screen appears on the iPhone.
- Find the iPhone WiFi IP in Settings → WiFi → current network
i. - From the Mac, confirm WDA is reachable:
ping <iPhone WiFi IP> curl -m 3 http://<iPhone WiFi IP>:8100/status
- Once
curlreturns WDA status, Claude/Codex/MCP can control the phone through that WiFi IP.
If stuck, check:
- Same subnet and same normal SSID; do not use guest WiFi.
- AP isolation / client isolation / device-to-device blocking in router settings.
- Mac connected to Ethernet plus another WiFi, causing the wrong route to the iPhone IP.
- A global proxy stealing LAN routes; LAN ranges should go DIRECT.
If this step fails, do not debug 5G yet.
Goal: Control your iPhone from anywhere, not just the local network.
Pass criteria: tailscale status shows the iPhone online, tailscale ping <iPhone Tailscale IP> succeeds, and curl http://<iPhone Tailscale IP>:8100/status returns WDA status.
- Install Tailscale on both your Mac and iPhone
- Note the iPhone's Tailscale IP (e.g.
100.x.x.xfrom the100.64.0.0/10CGNAT range) - Set
WDA_TAILSCALE_IPin your.envor MCP config - WDA-MCP will try the Tailscale IP first. If that fails, it reads the WDA URL from
/tmp/wda_run.logand tries a few common LAN IPs. For first-time setup, explicitly settingWDA_TAILSCALE_IPis more reliable
If stuck, check:
- Tailscale is online on the iPhone; enable VPN On Demand / Connect On Demand if it drops in the background.
- Other iPhone VPN/proxy apps are not running at the same time. Test with only Tailscale enabled.
- Mac proxy/VPN rules are not stealing
100.64.0.0/10Tailscale routes. WDA_TAILSCALE_IPis the iPhone's Tailscale IP, not the Mac/VPS IP.- If Tailscale ping works but
8100does not, WDA may not be running; go back to the WiFi stage and start WDA first.
Goal: After the iPhone leaves WiFi, keep controlling the already-running WDA over 5G + Tailscale. If 49152 is reachable, then try remote start/restart.
Pass criteria: after switching to mobile data, tailscale ping <iPhone Tailscale IP> and curl http://<iPhone Tailscale IP>:8100/status still work. 49152 reachability is a bonus: it means remote start/restart may work.
WDA-MCP can control WDA while the iPhone is on mobile data — no USB, no same LAN required. Starting/restarting WDA depends on the iPhone RemotePairing service; the real test is whether port 49152 is reachable on the iPhone Tailscale IP, not whether the phone currently says WiFi or 5G. When RemotePairing port 49152 is reachable, wda_start() automatically:
Cellular remote start can still fail because of carrier networking, Tailscale reconnects, DERP/UDP limitations, VPN/proxy conflicts, or WiFi -> cellular handoff jitter. If 49152 is unreachable or WDA is unstable after trying, go back to the WiFi/LAN workflow above, start WDA on WiFi first, then use the important workflow below to switch to 5G and keep controlling it.
- Detects the iPhone's RemotePairing service via Tailscale
- Creates a TCP tunnel using
pymobiledevice3(requires Python 3.13 + sudo) - Launches WDA through the tunnel
- Checks whether pymobiledevice3 includes the DTX timing fix, and applies the local compatibility patch if an older install is missing it
Requirements for remote start/restart:
- iPhone is paired in Xcode with Connect via network
- RemotePairing port
49152is reachable on the iPhone Tailscale IP - Keep the iPhone WiFi toggle ON. It does not have to be connected to a WiFi network, but do not turn the WiFi toggle off
- If
49152disappears after switching to cellular, start WDA on WiFi first, then leave WiFi and keep controlling it - Tailscale running on both devices
sudoaccess on the Mac (tunnel creation requires root for the utun interface)- Python 3.13 with pymobiledevice3:
brew install python@3.13 && python3.13 -m pip install pymobiledevice3
Typical workflow:
- At home: iPhone on WiFi →
wda_start()launches WDA via Tailscale tunnel - Before leaving: "Forget This Network" to disconnect WiFi cleanly → iPhone switches to 5G instantly → WDA stays alive
- On the go: control iPhone via 5G + Tailscale from anywhere; if
49152remains reachable, remote restart can work too
If stuck, check:
- The WiFi toggle is still ON. Do not turn off the main WiFi switch in Settings.
- If
8100drops, check Tailscale, mobile carrier networking, DERP, and VPN/proxy conflicts. - If
8100stays up but49152drops, you can still control the running WDA but cannot restart it remotely yet. Starting on WiFi is the stable fallback. - If it drops during handoff, use the watcher below to see whether
ping,8100, or49152fails first. - If 5G remains unstable, stop digging and use the WiFi start-and-keepalive workflow.
One rule: keep the WiFi button blue (ON). Never turn it off.
Before leaving home:
- Connect to WiFi, confirm WDA is running (you see the automation screen on your phone)
- Turn on mobile data / 5G
- Disconnect from WiFi — Settings → WiFi → tap current network → "Forget This Network"
- Make sure no other WiFi will auto-connect (turn off "Auto-Join" for saved networks)
- Leave! WDA keeps running over 5G + Tailscale
This tested flow is the key to keeping WDA alive on 5G. Do not skip steps.
For daily use, still do the "Forget" step. Walking out of WiFi range naturally does NOT reliably keep WDA alive — the iPhone holds onto weak WiFi signal too long, causing the TCP tunnel to time out before switching to 5G. "Forget This Network" is instant and lets iOS cut over to 5G cleanly.
Why? The technical reason:
Action WiFi button WDA result Disconnect / walk out of range / Control Center tap 🔵 Blue (ON) ✅ WDA keeps running Settings → flip WiFi toggle OFF ⚪ Grey (OFF) ❌ WDA killed in ~5 seconds iOS treats "WiFi toggle OFF" as a signal to forcefully terminate all developer processes including WDA. This is an iOS system-level restriction with no workaround.
As long as the WiFi button is blue — even without connecting to any network — iOS leaves WDA alone.
If Tailscale works on WiFi but fails on mobile data (connection times out or gets refused), your carrier network may be blocking the WireGuard protocol that Tailscale uses for direct connections.
Fix: set up a custom DERP relay server. DERP is Tailscale's built-in relay — when direct connections are blocked, traffic goes through DERP instead.
-
Deploy a DERP server on any machine reachable from your mobile network (a VPS, or your Mac with port forwarding / Cloudflare Tunnel):
# On your VPS go install tailscale.com/cmd/derper@latest derper --hostname=your-derp.example.com --verify-clients -
Add it to your Tailscale ACL (in the admin console):
"derpMap": { "Regions": { "900": { "RegionID": 900, "RegionCode": "myrelay", "Nodes": [{ "Name": "my-derp", "RegionID": 900, "HostName": "your-derp.example.com" }] } } }
-
Optionally disable Tailscale's default DERP servers if they're also blocked — set
"OmitDefaultRegions": truein the derpMap.
After this, mobile data traffic routes through your DERP server and everything works — WDA remote start, screenshots, control, all of it. No code changes needed.
This section is for cross-stage debugging. First-time users should follow the 5 stages above, then come here only for the item that matches the failure.
8100: WDA HTTP service. If this works, you can control an already-running WDA: screenshot, tap, type, inspect UI.49152: iPhone RemotePairing service. This is needed for wireless start/restart.- WDA access is not the same as WDA launch. On 5G, if
8100works but49152does not, you can usually keep controlling the running WDA but cannot restart it remotely yet.
bash scripts/diagnose_usb_wifi.sh <iPhone WiFi IP> <iPhone Tailscale IP>When helping someone remotely, ask for the full diagnostic output, especially Mac local IPs, Mac network snapshot, Mac route to iPhone WiFi IP, LAN ping, WiFi WDA status, and Tailscale ping. Also ask whether the Mac is connected to Ethernet and WiFi at the same time, and which router/SSID the iPhone is actually using.
If the failure happens during WiFi -> cellular handoff, run a watcher from the Mac or VPS:
bash scripts/watch_handoff.sh <iPhone Tailscale IP>Interpret the first failing column: ping means Tailscale/network handoff, 8100 means WDA access dropped, and only 49152 failing means you can still control the running WDA but cannot restart it yet.
- AP/client isolation is common on guest, campus, hotel, and some ISP/router default WiFi networks. Symptom: both devices have internet, but
ping <iPhone WiFi IP>fails. - Do not use guest WiFi for the first setup. Use a normal home/private SSID or a travel router with client isolation off.
- 2.4 GHz / 5 GHz networks, mesh networks, or two routers may be on different subnets. Check whether Mac and iPhone IPs look like the same subnet, for example
192.168.1.xand192.168.1.y. - A Mac connected to both Ethernet and WiFi can route traffic through the wrong network. Example: Ethernet on router A, WiFi on router B, iPhone on router B. Use
route -n get <iPhone WiFi IP>and checkinterface/source address; if it points to the wrong network, unplug Ethernet, disable the unrelated WiFi, or change network service order before testing again. - Other iPhone VPN/proxy apps can conflict with Tailscale. Tailscale's own docs note that iOS usually only supports one active VPN at a time, and another VPN may drop Tailscale traffic. Test with only Tailscale enabled. If you need a proxy, prefer a Tailscale exit node. See Tailscale + other VPNs.
- Mac global proxy/VPN rules should bypass LAN and Tailscale ranges:
192.168.0.0/16,10.0.0.0/8,172.16.0.0/12, and100.64.0.0/10.
WDA started via xcodebuild is NOT accessible through the tunnel!
xcodebuildlaunches WDA on the phone's WiFi IP, for example192.168.1.14:8100, but that port is not routed through the pymobiledevice3 tunnel. To expose WDA through the tunnel, use:python3.12 -m pymobiledevice3 developer dvt xcuitest --rsd <tunnel-address> <port> <BundleID>
xcuitest connects then drops after ~20 seconds?
Older pymobiledevice3 releases need the DTX fix from PR #1665. The PR is merged to upstream master, but PyPI may lag. Run the compatibility patch only if your installed version is missing the fix:
sudo python3.12 scripts/patch_pymobiledevice3.pyThe script is safe to re-run and no-ops when the fix is already present.
Can't create a WiFi tunnel with Python 3.12?
iOS 18.2+ removed QUIC. TCP tunnel requires Python 3.13's SSL PSK support. Use Python 3.13 for the tunnel and Python 3.12 for xcuitest.
devicectl shows "connecting"?
Restart
remoted:sudo pkill -9 remoted # Wait 5 seconds; the device should become "available (paired)"
The local stdio path used by Claude Code / Codex never opens a port — it is safe. The risk is HTTP mode (server.py --http / server_chatgpt.py), which exposes real write actions: tap, type, screenshot, read clipboard, open any app. Do not run HTTP mode unauthenticated on a public URL.
The server now ships with built-in OAuth 2.0 + Bearer authentication. HTTP mode refuses to start without an access token.
python scripts/generate_oauth_creds.pyThis writes ~/.wda-oauth.json (chmod 600) with a random client_id, client_secret, and access_token. The access token is the Bearer header your MCP clients send. Treat it like a password.
bash scripts/start_http.shOr directly (foreground):
python server.py --httpThe runner reads ~/.wda-oauth.json (or WDA_OAUTH_* env vars), starts uvicorn on 0.0.0.0:8200, and exposes:
/mcp— the MCP endpoint. RequiresAuthorization: Bearer <access_token>from non-loopback clients./.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server— OAuth discovery./oauth/authorize,/oauth/token— full OAuth 2.0 flow (authorization_codeandclient_credentials) for MCP clients that prefer enrollment over a static Bearer token.
Loopback clients (127.0.0.1, ::1) are allowed without auth so a local agent on the same machine can connect over http://localhost:8200/mcp without needing the token.
| Client | How to authenticate |
|---|---|
| claude.ai custom connector | Paste Authorization: Bearer <access_token> as a custom header |
| ChatGPT Developer Mode | Paste the token, or let it run the OAuth authorization_code flow |
| curl / scripts | curl -H "Authorization: Bearer $TOKEN" https://your-url/mcp |
| Local Claude Code via stdio | No token needed — stdio bypasses HTTP entirely |
For a domain you keep online long-term, layer Cloudflare Access in front of the tunnel:
- Cloudflare Zero Trust → Access → Applications → Add self-hosted application
- Application domain = your tunnel hostname (e.g.
wda-mcp.example.com) - Add a policy, e.g.
Include → Emails → your@email.com, or GitHub / Google login
Even if your access token leaks, callers still need to clear Cloudflare Access first. ChatGPT/Claude.ai connectors can use Cloudflare Access Service Tokens (set as a header) for non-interactive auth.
- Treat ngrok /
*.trycloudflare.comURLs as temporary testing only. Stop the tunnel when done. - Never commit
~/.wda-oauth.jsonor paste the access token into screenshots, chats, issues, or git. - If you suspect a leak: regenerate with
python scripts/generate_oauth_creds.py --force, restart the server. - Do not weaken the runner (e.g. removing the
access_tokencheck) just to "make it work" with a misconfigured client. Fix the client.
⚠️ Before exposing anything publicly, complete the Security setup above (generate_oauth_creds.py+start_http.sh). The flow below covers how to expose port8200; the server enforces Bearer auth on top.
The most fun way to use WDA-MCP is through chat — talking to Claude naturally and having it control your phone. "Go check my messages", "screenshot my home screen", "open the red app on the second page" — all in conversation.
This requires HTTP mode since claude.ai needs to reach your MCP server over the internet.
# Install ngrok
brew install ngrok
# Start MCP server in HTTP mode
python server.py --http &
# Expose to internet
ngrok http 8200ngrok gives you a public URL like https://abc123.ngrok.io. Add this as a custom MCP server in claude.ai settings.
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared
# Create tunnel (one-time setup)
cloudflared tunnel login
cloudflared tunnel create wda-mcp
# Start MCP server
python server.py --http &
# Expose via tunnel
cloudflared tunnel --url http://localhost:8200For a permanent custom domain, configure the tunnel in your Cloudflare dashboard.
- Go to claude.ai → Settings → Connectors → Add custom connector
- Name: anything you like (e.g.
WDA) - URL: your public HTTPS URL with
/mcp(e.g.https://your-host/mcp) - Expand Advanced settings:
- OAuth Client ID: from
~/.wda-oauth.json - OAuth Client Secret: from
~/.wda-oauth.json
- OAuth Client ID: from
- Click Add — Claude.ai will auto-discover the OAuth endpoints and complete the handshake
- Set all WDA tools to "Always allow" so you don't have to approve each action
# Print your Client ID and Secret
python3 -c "import json,pathlib; d=json.loads(pathlib.Path('~/.wda-oauth.json').expanduser().read_text()); print(f'Client ID: {d[\"client_id\"]}'); print(f'Client Secret: {d[\"client_secret\"]}')"Why chat? Claude Code runs on your computer — it can already control your Mac directly. The magic of WDA-MCP is controlling your phone through natural conversation from anywhere. "Hey, take a screenshot of my phone" hits different when you're chatting on the couch.
Free Apple Developer certificates expire every 7 days. Set up auto-renewal:
# Add to crontab (runs every 6 days at 3 AM)
crontab -e
0 3 */6 * * cd ~/Desktop/wda-mcp && bash scripts/renew_wda.sh >> /tmp/wda_renew.log 2>&1Deploy wda-mcp on a VPS. Mac is only needed once to compile WDA.
┌──────────┐ ┌──────────────────────────┐ ┌──────────┐
│ Claude │──HTTP──→│ VPS │←Tailscale→│ iPhone │
│(anywhere)│ │ wda-mcp (MCP server) │ │ WDA │
└──────────┘ │ pymobiledevice3 │ │ Tailscale│
│ Tailscale (exit node) │ └──────────┘
└──────────────────────────┘
The VPS handles three roles:
- MCP server — Claude connects here
- Tailscale node — reaches iPhone via Tailscale
- Exit node (optional) — can replace an extra VPN/proxy path to reduce VPN conflicts
# Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --advertise-exit-node
# Python 3.13 + wda-mcp
git clone https://github.com/Qizhan7/wda-mcp.git && cd wda-mcp
pip install "mcp[cli]" pymobiledevice3
sudo python3 scripts/patch_pymobiledevice3.py
# Run
WDA_TAILSCALE_IP=<iPhone Tailscale IP> \
WDA_DEVICE_ID=<UDID> \
WDA_BUNDLE_ID=<Bundle ID> \
python server.py --httpFirst-time WDA compilation still requires a Mac + Xcode. After that, VPS handles everything.
┌──────────────────┐ MCP (stdio/HTTP) ┌──────────────────┐
│ AI Agent │◄────────────────────────►│ wda-mcp │
│ (Claude Code) │ │ server.py │
└──────────────────┘ └────────┬─────────┘
│ HTTP :8100
┌────────▼─────────┐
│ WebDriverAgent │
│ (on iPhone) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ iPhone Screen │
│ tap/swipe/type │
└──────────────────┘
Network options:
├─ USB (local only)
├─ LAN Wi-Fi (same network)
└─ Tailscale VPN (anywhere — WiFi, 5G, any network)
Q: Do I need a paid Apple Developer account?
A: No. A free account works, but you'll need to re-sign every 7 days (use wda_renew or the cron script).
Q: What iPhone models are supported? A: Any iPhone that supports WebDriverAgent — generally iPhone 6s and later on a compatible iOS version.
Q: The screenshot coordinates don't match what I see?
A: WDA uses points, not pixels. iPhone 14 Pro is 393×852 points. Use wda_source to get exact element coordinates.
Q: WDA keeps disconnecting? A: Make sure the iPhone doesn't auto-lock. Go to Settings → Display & Brightness → Auto-Lock → Never (at least during use).
Q: Can I use this without Tailscale?
A: Yes, but the Mac must be able to reach the iPhone WiFi IP. If needed, find ServerURLHere in /tmp/wda_run.log; automatic IP guessing is only a fallback. Tailscale just adds remote access.
The main setup guide assumes macOS + Xcode. If you don't have a Mac, here's what you need to know.
| Step | Mac required? | Alternative |
|---|---|---|
| Build WDA from source | Yes | One-time — borrow a Mac or use GitHub Actions (free macOS runners) |
| Sign WDA | Yes (Xcode) | Re-sign the .ipa on Windows with Sideloadly |
| Install WDA on iPhone | No | ideviceinstaller, go-ios install, or Sideloadly (all cross-platform) |
| Launch WDA on iPhone | No | go-ios runwda works on Linux/Windows |
| Control via HTTP | No | Any platform — it's just REST calls to port 8100 |
| 7-day certificate renewal | No | Re-sign with Sideloadly, or pay $99/year for a 1-year certificate |
If you already have the WDA .ipa file:
- Sign and install using Sideloadly (Windows/Mac) — plug in iPhone via USB, select the .ipa, enter your Apple ID, done
- Launch WDA: use
go-ios runwdavia USB - Run this MCP server:
python server.py— works anywhere - Every 7 days: re-sign with Sideloadly (~2 minutes)
Free Apple ID signing expires every 7 days. Use a computer to re-sign — avoid on-device signing tools as they may trigger Apple ID restrictions.
Purchase a developer or enterprise certificate from a signing service. With a valid certificate you can re-sign the .ipa using zsign (cross-platform CLI) or Sideloadly, and the signature lasts much longer (typically months to a year).
With an Apple Developer account, you get a 1-year certificate and API Key access — no 2FA needed in CI, no manual renewal.
- Fork this repo
- Add your signing credentials to GitHub repo Settings → Secrets:
APPLE_CERTIFICATE_P12— base64-encoded .p12 certificateAPPLE_CERTIFICATE_PASSWORD— certificate passwordAPPLE_PROVISIONING_PROFILE— base64-encoded .mobileprovisionDEVICE_UDID— your iPhone's UDID
- The included workflow (
.github/workflows/renew.yml) runs every 6 days on GitHub's free macOS runner, builds a fresh WDA, and uploads it to Releases - Download and install — or connect your Windows/Linux machine to auto-pull from Releases
- go-ios — install apps, launch WDA, forward ports. Linux/Windows/macOS. The strongest cross-platform option.
- pymobiledevice3 — device communication, developer disk mounting, port forwarding. Python, all platforms.
- Sideloadly — GUI tool for signing and installing IPAs on Windows/macOS. Handles free account re-signing.
- ideviceinstaller — CLI IPA installer for Linux.
Bottom line: You need a Mac exactly once to compile WDA. After that, everything runs on any platform.
- WebDriverAgent — Facebook/Appium's iOS automation framework
- pymobiledevice3 — Python library for iOS device communication
- Tailscale — Zero-config mesh VPN
- Model Context Protocol — Anthropic's open protocol for AI tool use
The idea for this project came from 蛋 and her Claude 蛋壳, who pointed out the pymobiledevice3 + WebDriverAgent approach as the most viable path to iOS remote control — no jailbreak, no third-party auth, just Xcode signing and a bit of patience with Apple's 7-day certificate cycle. Thanks for lighting the way.