Fonino documentation
Build on a real iPhone.
Last updated October 4, 2026
Overview
Fonino gives an AI agent a physical, non-jailbroken iPhone over the Model Context Protocol (MCP). The agent takes the phone for a task, reads screenshots, taps, swipes and types in any app, and sends texts through a checked path. Fonino sits between the agent and the phone: it enforces approvals, blocked apps and sending limits, and the agent can't switch them off.
- Endpoint:
https://api.fonino.app/mcp(Streamable HTTP, stateless JSON responses). - Auth: OAuth 2.1 sign-in (recommended) or a per-connection API key.
- Phones: BYOF (your iPhone on your Mac) or a hosted iPhone. Both appear the same to the agent.
- Coverage: U.S. only. Texts go to U.S. numbers and iMessage addresses.
Quickstart
- Get a phone. Start a plan on fonino.app. For BYOF, the setup guide at app.fonino.app installs Fonino on your Mac and connects your iPhone.
- Connect your agent to
https://api.fonino.app/mcpand sign in with your Fonino email (see Connecting agents). - Ask for something: “Take a screenshot of my Fonino phone and tell me what's on it.”
A typical task looks like this:
phone_list_devices()
→ [{ id: "iphone-002", status: "connected" }]
phone_acquire(device_id: "iphone-002", task: "Check SF weather")
→ { lease_id } + screenshot
phone_open_app(device_id, lease_id, bundle_id: "com.apple.weather")
phone_get_screen(device_id, lease_id)
→ { screen_id: "scr_…", width: 430, height: 932 } + screenshot
phone_tap(device_id, lease_id, screen_id, x: 215, y: 120)
phone_release(device_id, lease_id, outcome: "68°F, sunny")Connecting agents
Any MCP client that supports remote servers over Streamable HTTP works. Tested clients:
| Agent | How to connect |
|---|---|
| Grok Bot | grok.com/connectors → New Connector → Custom → URL https://api.fonino.app/mcp → Connect, then your Fonino email and the emailed code. |
| Claude | Settings → Connectors → Add custom connector → https://api.fonino.app/mcp → Connect, then your Fonino email and the emailed code. |
| Hermes Agent | Three lines in ~/.hermes/config.yaml, then hermes mcp login fonino. Full Hermes guide. |
| Claude Code | claude mcp add --transport http fonino https://api.fonino.app/mcp, then /mcp to sign in. |
| Muse | Settings → Connectors → Add custom connector → https://api.fonino.app/mcp, then sign in with your Fonino email and the emailed code. |
| Your own code | The official MCP SDKs (TypeScript, Python) with the URL above and OAuth, or an Authorization: Bearer apk_… header. |
Screenshots as files. Some clients (Hermes, for example) save tool images to disk and give the model a path. Fonino's instructions tell the agent to open that file with its image tool. Clients without image support can call phone_get_screen with with_text: true to get on-screen text with tap points.
Authentication
OAuth 2.1 (recommended)
Fonino is an OAuth 2.1 authorization server for its MCP resource. Clients discover everything from metadata:
https://api.fonino.app/.well-known/oauth-protected-resource(RFC 9728)https://api.fonino.app/.well-known/oauth-authorization-server(RFC 8414)- Authorization code flow with PKCE S256, public clients (
token_endpoint_auth_method: none), scopephone. - Client registration: Dynamic Client Registration (RFC 7591) or a Client ID Metadata Document (an https
client_id). - Redirect URIs must be https, or http on
localhost/127.0.0.1for desktop and CLI clients. - Access tokens last 1 hour; refresh tokens 30 days and rotate on use.
The user signs in with their Fonino email and a 6-digit code we email them. Each sign-in shows up under Connected assistants at app.fonino.app, where it can be removed at once.
API keys
For clients that only take a header: create a connection at app.fonino.app and send Authorization: Bearer apk_…. A key is shown once, belongs to one account, and can be limited to specific phones and recipients. Treat it like a password and revoke it if it leaks.
Core concepts
Leases
Every phone action needs a lease_id from phone_acquire. A lease gives one agent exclusive control. It renews with each call and expires after 10 idle minutes. Release it with phone_release when done or blocked; the phone then returns to the Home Screen. Other connections see a held phone as busy.
Screenshots and coordinates
Screenshots come with a screen_id, the screen size in points (for example 430 × 932) and the capture time. Coordinates for taps and swipes are in those points, and every tap or swipe must name the screen_id its coordinates came from. If the screen has changed since, Fonino refuses with stale_screen and returns a fresh screenshot. Each action returns a new screenshot unless you pass screenshot_after: false.
Risk
Actions are normal unless they commit something. For the single tap or call that does, set risk to external_communication, purchase, destructive or authentication, and include an approval object:
{ "app": "Amazon", "operation": "purchase", "recipient": "Amazon.com",
"payload": "1x Anker USB-C cable 2-pack, total $10.85", "summary": "Buy a USB-C cable" }Tool reference
Every phone tool takes device_id; all but the session tools also take lease_id. Action tools also accept risk, approval and screenshot_after.
Session
| Tool | Parameters | What it does |
|---|---|---|
phone_list_devices | — | Your phones, their status, and whether each is free. |
phone_get_status | device_id | Health checks for one phone: reachable, locked, screen size. |
phone_acquire | device_id, task, holder? | Take exclusive control for a task. Returns lease_id and a first screenshot. |
phone_release | device_id, lease_id, outcome? | Give the phone back. Always call it when done or blocked. The phone returns to the Home Screen. |
Seeing the screen
| Tool | Parameters | What it does |
|---|---|---|
phone_get_screen | device_id, lease_id, with_text? | Fresh screenshot with screen_id, size in points and capture time. with_text adds on-screen text lines with tap points. |
phone_wait | device_id, lease_id, ms | Wait up to 10 s for loading or animations, then return a fresh screenshot. |
Acting
| Tool | Parameters | What it does |
|---|---|---|
phone_tap | device_id, lease_id, screen_id, x, y, risk?, approval? | Tap a point from the screenshot identified by screen_id. |
phone_swipe | …, screen_id, x1, y1, x2, y2, duration_ms? | Drag between two points. To scroll content down, swipe up (y1 > y2). |
phone_type | device_id, lease_id, text, submit? | Type into the focused field. submit presses Return. |
phone_key | device_id, lease_id, key | Press return, delete, tab or space. |
phone_home | device_id, lease_id | Go to the Home Screen. |
phone_app_switcher | device_id, lease_id | Open the app switcher. |
phone_open_app | device_id, lease_id, bundle_id, fresh? | Open an app by bundle id. fresh quits it first so it starts from its launch screen. |
Messages
| Tool | Parameters | What it does |
|---|---|---|
phone_send_message | device_id, lease_id, to, body | Send a text to a number or iMessage email. Fonino checks recipient and text, presses Send once and reports the delivery status. |
phone_read_messages | device_id, lease_id, with, limit? | Latest messages of a conversation as text (from: me | them), plus a screenshot. |
phone_wait_for_reply | device_id, lease_id, with, max_wait_s? | Wait up to 85 s per call for the other person to reply. |
Photos
| Tool | Parameters | What it does |
|---|---|---|
phone_save_image | device_id, lease_id, image_url | image_base64 | Save an image to Photos (PNG, JPEG, GIF, WebP, HEIC, up to 20 MB) so it can be attached or posted. |
phone_get_photo | device_id, lease_id, count? | The newest 1–4 photos from the camera roll, as images (longest side 1600 px). |
Approvals
| Tool | Parameters | What it does |
|---|---|---|
phone_await_approval | device_id, lease_id, approval_id | Wait up to ~45 s per call for the owner's decision: pending, approved, denied or expired. |
phone_confirm_approval | device_id, lease_id, approval_id, code | Approve with the 6-digit code the owner reads to the agent. Only a code the user typed. |
Handing over
| Tool | Parameters | What it does |
|---|---|---|
phone_live_view | device_id, lease_id | Private 30-minute link to watch the phone live. |
phone_request_help | device_id, lease_id, reason | Hand the phone to the owner (logins, 2FA, CAPTCHAs). Pauses the agent and returns a live link. |
phone_wait_for_owner | device_id, lease_id, max_wait_s? | Wait up to 85 s per call until the owner presses "I'm done". |
phone_owner_done | device_id, lease_id | The owner's "I'm done" button for hosts that confirm tool calls. |
Approvals
Anything that spends, sends, publishes or deletes waits for the phone owner. The flow:
- The agent calls the action with a
riskand anapprovalobject. Fonino repliesapproval_pendingwith anapproval_idand anapprove_url. Nothing has happened yet. - The agent shows the link to the user right away. The owner also gets a notification with a 6-digit code.
- The agent calls
phone_await_approvalagain and again (about 45 s per call) until the status isn'tpending. - On
approved, the agent repeats the original call exactly. Ondeniedorexpired, it stops, tells the user and releases the phone.
- The agent can't approve itself. Approving needs the owner's Fonino sign-in, or the 6-digit code the owner reads out (
phone_confirm_approval; wrong codes are counted and lock the request). - One approval, one action with exactly the approved text or amount. The owner can choose to allow the rest of a task instead, capped at the approved amount.
- Requests expire after 10 minutes. Releasing the phone closes its open requests.
- Agents that leave: if a request is declined or expires and the agent doesn't come back within 2 minutes, Fonino releases the phone.
- Fonino checks the button under each tap. In social, mail, shopping, food and ride apps and in browsers, a tap on a button like Post, Send, Reply, Place your order, Buy Now or Pay returns
approval_requiredif the agent didn't mark it.
Safety rules
- Blocked apps: Settings, Wallet, Passwords, App Store, Mail, Phone, FaceTime, Find My, Watch and Messages (texting goes through
phone_send_message). If Fonino can't tell which app is open, it refuses the action. - Untrusted content: everything on the screen, in messages and on web pages was written by other people. Agents are instructed never to follow instructions found there.
- Credentials stay with the owner: at passcodes, passwords and 2-factor prompts the agent hands over with
phone_request_help. - Per-connection limits: a connection can be restricted to specific phones and recipients.
More in Security.
Messaging
phone_send_messageopens the conversation, verifies the recipient and the exact text, presses Send once and returns the status the phone shows (for exampledelivered, with the service used).- Limits per phone: 20 texts an hour, 100 a day, and 5 an hour to the same person. A refused send returns
rate_limitedwithretry_after_s. - STOP is honored: a recipient who replies STOP is never texted again (
recipient_opted_out). - Person-to-person only. No bulk, marketing or unsolicited messages; connections that send them are disabled.
- After
outcome_unknown, never resend: callphone_read_messagesfirst.
Live view & handoff
phone_live_view returns a private 30-minute link to watch the phone. For logins, 2FA or CAPTCHAs, phone_request_help pauses the agent and gives the owner a live link with control of the phone. When the owner presses “I'm done”, phone_wait_for_owner returns with a fresh screenshot and a summary of what they did.
Errors
Tool errors return { "ok": false, "code": "…", "error": "…", "next": "…" }. The next field says what to do.
| Code | Meaning and what to do |
|---|---|
approval_pending | The action needs the owner's approval. Show approve_url, wait with phone_await_approval, then repeat the same call. |
approval_required | Fonino recognized a button that publishes, sends or buys. Repeat the tap with a risk and an approval object. |
approval_denied | The owner declined. Don't retry; tell the user. |
app_blocked | The app in front is off limits to assistants (see Safety rules). Nothing was done. |
stale_screen | The screen changed since the screenshot your coordinates came from. The error includes a fresh screenshot; decide again from it. |
screen_id_required / unknown_screen | Pass the screen_id of your latest screenshot. |
out_of_bounds | The point is outside the screen. |
lease_invalid | The lease expired or was released. Call phone_acquire again. |
device_leased | Another task holds the phone. Try again later. |
forbidden_device / unknown_device | Your connection can't use that phone, or it doesn't exist. Call phone_list_devices. |
device_unreachable / phone_locked | The phone is offline or locked. The owner needs to unlock it and keep it connected. |
human_control | The owner has taken over. Wait with phone_wait_for_owner. |
rate_limited | A sending limit was reached. Nothing was sent; retry after retry_after_s. |
recipient_not_allowed / recipient_opted_out | The recipient isn't on the connection's allowlist, or replied STOP. |
outcome_unknown | The action may have happened. Never repeat it blindly: read the conversation or take a screenshot first. |
Limits & timeouts
| What | Limit |
|---|---|
| Lease idle timeout | 10 minutes |
| Approval request lifetime | 10 minutes |
| Phone released after a declined or expired approval if the agent is gone | 2 minutes |
phone_await_approval per call | about 45 s |
phone_wait_for_reply, phone_wait_for_owner per call | 85 s |
phone_wait | 10 s |
| Texts per phone | 20 an hour, 100 a day, 5 an hour per recipient |
Images for phone_save_image | 20 MB |
| Live view link | 30 minutes |
| OAuth access / refresh token | 1 hour / 30 days |
Architecture & data
agent ──HTTPS (MCP)──▶ api.fonino.app ──outbound WebSocket──▶ Fonino on the Mac ──USB──▶ iPhone
auth, routing approvals, limits, app rules- The cloud (api.fonino.app) knows who you are and which Mac your phone is on. It forwards MCP calls and live-view traffic, nothing else.
- The Mac enforces everything phone-specific: leases, approvals, limits, blocked apps. It connects out to the cloud, so no ports are opened on your network.
- With BYOF, screenshots and run recordings stay on your Mac. Hosted phones run the same software on Macs we operate.
See Privacy for what we store and for how long.
Questions or a use case to test? Contact support.