ambivalence
Enables using an Android phone as a remote camera, allowing the agent to capture photos, switch between cameras, zoom, and control the torch through the paired ambivalence Android app.
Provides integration with Cloudflare Quick Tunnel via ambivalence hub --tunnel, creating a public HTTPS URL and QR code so a phone can connect to the hub from other networks.
Enables using an iOS phone as a remote camera, allowing the agent to capture photos, switch between cameras, zoom, and control the torch through the paired ambivalence iOS app.
Supports connecting the phone to the hub over a Tailscale tailnet, including using Tailscale Serve to provide HTTPS for the browser camera page.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ambivalencetake a photo of my desk with the back camera, zoom in on the chip labels"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ambivalence lets your coding agent use your phone as a camera. you pair the phone once. after that the agent takes photos, switches between the front and back camera, zooms and turns on the torch through MCP or the cli, and you don't touch the phone again. on the same wi-fi the phone finds the agent by itself. everything between the phone and the agent is end-to-end encrypted.
protocol | npm | releases | demo video | ios build notes
why
your agent can read your code, but it can't see the board on your desk, the label on a box or the error on another screen. ambivalence gives it the phone in your pocket. it sends still photos, not video, because vision models take stills. each photo is one jpeg over a websocket, so it works on slow networks too.
after the first pairing there is nothing to do on the phone. open the app and ask the agent. if the app is closed when the agent asks, the agent waits up to 30 seconds for you to open it. the link survives app restarts and network drops.
Related MCP server: camera-mcp
requirements
part | needs |
agent side | linux, macos or windows, amd64 or arm64. node 18+ if you install with npm |
android | android 8.0 or newer |
ios | ios 17 or newer, and an apple developer team to sign the app |
browser | any phone browser with camera access, over https |
optional | cloudflared for |
install
agent side
with npm:
npm install -g ambivalence
ambivalence versionthe package downloads the release binary for your platform and checks it against the release's checksums.txt. if your npm runs with install scripts disabled, the download happens on the first ambivalence command instead.
or download a binary from the latest release. the names are ambivalence-<os>-<arch>, for example ambivalence-darwin-arm64 or ambivalence-windows-amd64.exe:
curl -fsSL -o ambivalence https://github.com/Microck/ambivalence/releases/latest/download/ambivalence-linux-amd64
chmod +x ambivalence && mv ambivalence ~/.local/bin/or build it with go 1.26 or newer:
go install github.com/Microck/ambivalence/cmd/ambivalence@latestto check a downloaded binary by hand:
gh release download --repo Microck/ambivalence --pattern checksums.txt
sha256sum --check --ignore-missing checksums.txtphone
android: install ambivalence.apk from the release. allow installs from unknown sources when android asks. the app asks for camera access on first launch.
ios: the release has ambivalence-unsigned.ipa. sign it with your own team before you install it, as apps/ios/README.md describes. on first launch, allow camera access and local network access. without local network access the app cannot find the agent on your wi-fi.
no app: any phone browser works. see from a browser.
connect your agent
1. add the mcp server
claude code:
claude mcp add ambivalence -- ambivalence mcpcodex cli, in ~/.codex/config.toml:
[mcp_servers.ambivalence]
command = "ambivalence"
args = ["mcp"]any other mcp client:
{
"mcpServers": {
"ambivalence": {
"command": "ambivalence",
"args": ["mcp"]
}
}
}ambivalence mcp starts a hub on port 7842 inside the same process. you don't need to run ambivalence hub next to it.
2. pair once
open the app on the same wi-fi as the agent's computer. the app finds the hub and shows a 6-character code. give the code to your agent:
connect to my phone camera, the code is K9RBCY
the agent and the phone then both show a second code. if the two match, tap confirm on the phone. if they differ, tap cancel, because something between them is interfering.
3. use it
from then on, ask the agent whenever it needs to see something:
take a photo of the board and tell me which led is on
the mcp tools are:
tool | does |
| takes a photo and returns the jpeg to the model |
| pairs with a code, or reconnects to the linked phone |
| selects an exact camera ID, or the |
| turns the torch on or off |
| sets the zoom factor |
| lists selectable lenses, zoom ranges and supported manual controls |
| sets focus, shutter/ISO, exposure bias and white balance where supported |
| shows whether the phone is linked and online |
| closes the current session; the link stays saved |
See lens selection and manual controls for capability discovery, settings and platform limits.
without mcp
the cli does the same from a terminal:
ambivalence hub & # skip this if ambivalence mcp is running
ambivalence pair K9RBCY # once per phone
ambivalence capture -o shot.jpg # any time after
ambivalence torch on
ambivalence cameras # discover exact lens IDs and capabilities
ambivalence switch front
ambivalence configure '{"focusMode":"manual","focusPosition":0}'
ambivalence zoom 2reach the phone from other networks
on the same wi-fi you need nothing extra. hubs announce themselves over mdns, and the app connects to every hub it finds. guest and office networks often block devices from seeing each other. on those, and away from home, use one of the options below. you only set this up once, because the app saves the hub address.
tailscale
if the agent's computer and the phone are both on your tailnet, the phone can reach the hub from anywhere.
install tailscale on the phone and sign in to the same tailnet.
find the computer's tailnet address with
tailscale ip -4, or use its magicdns name, such asmy-laptop.tail1234.ts.net.in the app, type the hub url into the field at the bottom and tap connect:
ws://my-laptop.tail1234.ts.net:7842/v1pair with the code as above.
the hub listens on all interfaces by default (0.0.0.0:7842), so tailscale can reach it with no further setup. mdns does not cross the tailnet, which is why you type the address once. the app keeps this hub and also connects to hubs it finds on the local wi-fi.
for the browser page over tailscale you need https. tailscale serve provides it if https certificates are enabled for your tailnet:
tailscale serve --bg 7842
ambivalence hub --public https://my-laptop.tail1234.ts.netthe second command prints a qr code that opens the app already connected, and the https url for the browser.
cloudflare quick tunnel
ambivalence hub --tunnelthis needs cloudflared on your path. it prints a public https://….trycloudflare.com url and a qr code. scan the qr code with the phone's camera to open the app connected to the hub, or open the url in the phone's browser. the url changes every time you start the tunnel, so the phone needs the new one after a restart.
your own https proxy
if the hub already sits behind your own https reverse proxy, print the qr code with:
ambivalence hub --public https://camera.example.comthe proxy must pass websocket upgrades through on the /v1 path.
using a tunnel with mcp
ambivalence mcp runs its own hub, and two hubs cannot share port 7842. to put the mcp server behind a tunnel, run the hub yourself and start the mcp server without one:
ambivalence hub --tunnel{ "command": "ambivalence", "args": ["mcp", "--no-hub"] }with --no-hub, the mcp server connects to ws://127.0.0.1:7842/v1. set --hub or AMBIVALENCE_HUB to use a hub on another machine.
from a browser
the hub serves a camera page at /, so a phone can use its browser instead of the app. browsers only allow the camera on https pages, so open the page through one of the https options above (--tunnel, tailscale serve or your own proxy). allow camera access, and pair with the code it shows.
the browser page has three limits. it cannot find hubs on the wi-fi. it stops when you close the tab, though it reconnects by itself after a network drop while the tab is open. and it runs javascript that the hub serves, so a hub you don't control could read its link key. use the browser page only with a hub you run. the apps don't have this problem.
troubleshooting
the app shows no code: the phone and computer probably can't see each other. check that both are on the same network, that ios has local network access for the app, and that the computer's firewall allows tcp port 7842 and mdns (udp 5353). otherwise use tailscale or a tunnel.
the agent says linked phone is offline: open the app. the agent waits up to 30 seconds, then gives up.
the agent says phone is busy with another agent connection: another agent session holds the phone. close it, or wait for the phone to drop it, which takes up to a minute after that agent dies.
the agent says the phone rejected its link: the phone was paired with another agent since. a phone keeps one link at a time, so pair this agent again with the code on the phone.
the two confirmation codes differ: tap cancel and pair again. a mismatch means something between the phone and the agent changed the key exchange.
to unlink, pair again, or delete the device and key fields from the agent's config file.
how it works
flowchart LR
agent["agent<br/>ambivalence mcp / cli"] -- websocket --> hub["hub<br/>relay"]
phone["phone app<br/>camera"] -- websocket --> hub
agent <-. "end-to-end sealed" .-> phoneboth the agent and the phone open a connection to a hub, so the phone needs no open ports. the hub passes messages between them and never sees a secret.
pairing runs a p-256 key exchange. both sides show a code derived from that exchange, and the user checks that they match. a hub in the middle that swapped in its own keys would produce different codes. both sides store the resulting link key.
in every later session, the phone and the agent each prove they hold the key. after that, aes-256-gcm encrypts every request, reply and photo, and each message must arrive in exact order. a rogue hub can refuse to relay, but it cannot see photos, send its own requests, replay old ones or swap images.
the phone connects to its saved hub and to up to 4 hubs it discovers. the agent reaches it through whichever hub the agent uses.
docs/protocol.md has the full wire format.
where the link key lives
side | storage |
agent |
|
ios app | keychain, this device only |
android app | app-private shared preferences, not the hardware keystore |
browser page |
|
cli reference
ambivalence hub [--listen 0.0.0.0:7842] [--tunnel | --public https://…]
ambivalence mcp [--listen 0.0.0.0:7842] [--hub ws://…] [--no-hub] [--public-hub wss://…]
ambivalence pair CODE [--hub ws://…] [--timeout 2m]
ambivalence capture [-o shot.jpg] [--quality 80] [--max-width 1280]
ambivalence status | cameras
ambivalence torch on|off
ambivalence switch front|back
ambivalence zoom FACTOR
ambivalence host fake [--state phone.json] # a test phone that serves a test pattern
ambivalence versionvariable | meaning |
| hub url the agent connects to, default |
| address the hub listens on, default |
| public |
build from source
go test ./...
go build -o bin/ambivalence ./cmd/ambivalence
bash scripts/e2e-fake.sh # pairs a fake phone, restarts it and reconnectspart | stack | build |
cli, mcp server, hub | go | one static binary |
android app | kotlin, camerax |
|
ios app | swift, avfoundation, cryptokit | xcodegen and xcode, see apps/ios/README.md |
browser page | html and javascript | served by the hub from |
scripts/browserstack-smoke.py runs the whole flow on a real phone on browserstack. it pairs, checks that the confirmation codes match, taps confirm, takes photos, toggles the torch, switches cameras, restarts the app and takes another photo without a code.
license
deterministic browser flow tests
The root development-only package.json is separate from the published npm/
package. Run npm ci, npx e2e-web install chromium, and npm run test:e2e.
The tester-army/e2e suite uses no model calls and simulates camera denial rather
than requesting access to real hardware. It covers the host controls, invalid
hub addresses, mobile layout and recoverable errors. Existing Go tests,
scripts/e2e-fake.sh and browser-control unit tests cover separate contracts.
Physical camera quality, hardware constraints and native mobile acceptance still
require device testing; a simulated browser pass does not establish those.
This server cannot be deployed
Maintenance
Related MCP Connectors
Turns a phone into a camera+Bluetooth remote so AI assistants can see and control any PC.
A real Android phone in the cloud for MCP agents: observe the screen, act, reset to a saved state.
Capture photos remotely from mobile devices via S3-backed upload URLs
Give your AI agent a memory and body on your iPhone: set alarms, ring your phone, over MCP.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that provides LLM agents with direct access to webcam hardware for capturing high-resolution photos and recording video sequences. It enables autonomous agents to monitor environments and interact with the physical world through standard Model Context Protocol tools.4MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables interaction with local camera devices to capture and process images. It allows LLMs to access video devices with configurable settings such as resolution, orientation, and image format.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables capturing live images from a USB camera and checking camera health via natural language.MIT
- AlicenseAqualityBmaintenanceEnables an AI agent to capture a photo via a live camera preview, returning the image to the agent and saving it locally.1MIT