# ALI Remote MCP server

Connect Claude to a rack of real iPhones, and to the agency around them. One endpoint, OAuth sign-in, and the connection acts as the person who approved it: it reaches exactly the phones that person can reach in the dashboard, and may do exactly what they could do by hand.

- Endpoint: `https://beta.aliremote.com/api/mcp`
- Transport: HTTP (streamable). No stdio, no local install.
- Authorization: OAuth 2.1 with PKCE. No API key to paste, no secret to store.

## Connect

In Claude Code:

```
claude mcp add --transport http ali https://beta.aliremote.com/api/mcp
```

Then run `/mcp` and choose to authenticate. A browser opens on the ALI consent screen showing the account you are signed into. Approve it once.

In Claude on the web or the desktop app, add it as a custom connector under Customize, Connectors, using the same URL.

In ChatGPT, turn on developer mode under Settings, Security and login, then create an app with the same URL and OAuth sign-in. In VS Code, run MCP: Add Server and choose HTTP. In Codex CLI, run `codex mcp add ali --url https://beta.aliremote.com/api/mcp` and then `codex mcp login ali`.

## Tools

| Tool | What it does | Permission | Runs without asking |
|---|---|---|---|
| `list_devices` | **List phones.** List the phones this connection can reach, with their names, whether each is online, and whether it is currently casting its screen. Call this first to find the public_id of a phone before doing anything else with it. A phone no relay reports any more is left out, so a phone missing here is one nothing can drive rather than one that does not exist; get_device still answers for an id you already hold. Paginated: pass the returned next_cursor back as cursor for the next page. | `devices.read` | yes |
| `get_device` | **Get one phone.** Read one phone's current state: whether it is online, whether it is casting, its screen size, iOS version and the box hosting it. Use this to check a phone is ready before driving it, and to poll after recast_device until casting is true. | `devices.read` | yes |
| `capture_screen` | **Capture the screen.** Take a screenshot of a phone and return it as an image. The coordinates you read off this image are the ones tap_device and swipe_device take, so capture before deciding where to tap, and again afterwards if you need to confirm what happened. | `devices.snapshot` | yes |
| `tap_device` | **Tap the screen.** Tap a point on the phone's screen. Coordinates are in the pixel space of the image capture_screen returns, so read the position off that image and send it as-is without scaling. If a tap lands somewhere other than where it was aimed, call recentre_pointer once and try again; some handsets need that occasionally. | `devices.control` | no, it changes something |
| `swipe_device` | **Swipe the screen.** Swipe the phone's screen, either by direction for paging through a feed, or between two exact points read off a screenshot for dragging a slider or an app switcher card. Moving a home screen icon also needs hold_ms. Give either direction, or all four of from_x, from_y, to_x and to_y. A direction swipe with no distance is drawn the way a hand draws it, a little differently every time; a swipe between two points, or one carrying a distance, is drawn exactly as you gave it. Set precise to change either. A gesture can be accepted and still move nothing, unlike a tap, so capture the screen afterwards when it matters rather than assuming it worked. | `devices.control` | no, it changes something |
| `type_text` | **Type text.** Type text into whatever field currently has focus on the phone. Tap the field first. Plain short ASCII goes over the keyboard directly; anything longer, or with emoji or accents, is pasted through the clipboard instead and can take a few seconds. | `devices.control` | no, it changes something |
| `press_button` | **Press a button.** Press a hardware, system or keyboard key on the phone: home, the app switcher, lock, screenshot, enter, the keyboard toggle, control centre, notifications, the volume keys, mute, backspace, tab, or an arrow. Use times to repeat it: backspace with times is the only way to clear a field that already has text in it, since type_text appends rather than replaces. | `devices.control` | no, it changes something |
| `set_device_state` | **Change a phone setting.** Turn a phone's wifi, cellular data, airplane mode or torch on or off, or set its screen brightness. Use level for brightness and on for everything else. | `devices.control` | no, it changes something |
| `rotate_ip` | **Rotate a phone's IP.** Get a phone a fresh public address and score it. On a phone with its own mobile data this cycles airplane mode, the way the guide says to before every sign-up; on proxy Wi-Fi it calls the router's rotation link, which rotates every phone on that router. A phone on ALI Wi-Fi has no address of its own and is refused. Takes about half a minute; this waits for the outcome and answers with the new address and its fraud score (0 clean to 100, with a risk word), or with a running rotation to check again by calling this tool once more. One rotation per phone at a time. | `devices.control` | no, it changes something |
| `set_device_label` | **Rename a phone or set its sticker.** Change what a phone is called, or the inventory sticker recorded for the handset. The sticker is unique across the fleet and can be used in place of a public_id anywhere a phone is named, which is what makes it worth setting on a phone that has none. Renaming does not touch the phone itself, only the record everything looks it up by. | `devices.write` | no, it changes something |
| `get_device_values` | **Read what a phone keeps.** List the values kept with a phone: what automations have recorded about it, such as an account's status, and what people have set for it, such as its handle or link. Each says when it was last set and by which automation. An automation reads the same values as {{phone.kept.name}}. | `devices.read` | yes |
| `set_device_value` | **Keep a value with a phone.** Keep a named value with a phone, or remove one by sending null. This is how a phone is given what is its own before an automation runs across a rack: its handle, its link, the caption it should post. An automation reads it as {{phone.kept.name}}. Setting a name that is already there replaces it, and the phone remembers what it was before. | `devices.write` | no, it changes something |
| `recentre_pointer` | **Re-centre the phone's pointer.** Put a phone's pointer back in a known place. Reach for it when a tap lands somewhere other than where it was aimed, or once before a sequence that will tap a lot; it is not something to do between every tap. It changes nothing on screen, so a screenshot before and after looks the same, and it works on a phone that is not casting. | `devices.control` | no, it changes something |
| `check_device_control` | **Check who is driving a phone.** Report whether a phone is free, held by a person, or held by an automation, and since when. Call this before a run that will hold a phone for a while, so a busy handset can be skipped rather than failing a step partway through. A phone somebody else is holding refuses taps until they stop or until take_device_control is used. | `devices.read` | yes |
| `take_device_control` | **Take control of a phone.** Take a phone that somebody else is currently driving. A phone held by an automation is handed over at once. A phone held by a person asks them first and waits about three seconds for an answer, so this tool can take that long to return; if they refuse, it reports that and does not take the phone. Reach for it when check_device_control or a refused tap says a phone is busy and the work cannot wait. Whoever is taken off keeps watching and can take the phone straight back. | `devices.control` | no, it changes something |
| `recast_device` | **Restart the phone's screen mirroring.** Re-establish a phone's screen mirroring. This is the fix for a phone that is online but not casting, which is the most common thing that goes wrong on a rack. It is expensive and disruptive: on a phone that is already casting it bounces the session and the screen is gone while it reconnects. It answers as soon as the box accepts the request, not when the phone is back, so poll get_device until casting is true. Budget up to two minutes, and treat a phone still dark after that as needing a person rather than another recast. | `devices.control` | no, it changes something |
| `close_app` | **Force-close the front app.** Force-close whatever app is in front, the way a person does by hand: open the App Switcher and flick its card away. Bring the app you mean to the front first (open it, or press home and tap it), then call this. It works on any casting phone and does not need the on-device helper, so it reaches a frozen or stuck app that the helper verbs cannot. It does not choose which app to close: it closes the one in front, so do not call it speculatively from an unknown screen. It reads the screen back before answering, so a success means that app is no longer showing - firmly on a still screen, less so on an app that animates anyway (a video, a feed), where the check cannot tell movement from a close. A failure means that app is still on screen, and is not a retry: with more than one app open the first flick may already have thrown a different app's card, and calling again throws another. Take a capture_screen and look instead. | `devices.control` | no, it changes something |
| `list_media` | **Browse the media vault.** List the photos and videos in the agency's media vault, newest first, with their folder, tags, whether they have been posted, and short-lived links to view or download each one. Filter by folder, tag, kind, posted, or a piece of the filename. Pass next_cursor back as cursor for the next page. Files expire from the vault after a few days; expires_at says when. | `media.read` | yes |
| `get_media` | **One file in the media vault.** Read one file from the media vault: its details, fresh links to view and download it, and every phone you can reach that it was sent to, with how each delivery went. Use it to follow a push_media across all its phones at once. | `media.read` | yes |
| `list_media_folders` | **Media vault folders.** List the folders in the media vault as a flat list with parent_id, so you can rebuild the tree, and how many files each holds. unfiled counts the files in no folder. | `media.read` | yes |
| `list_media_tags` | **Media vault tags.** List the tags used to label files in the media vault, with how many files carry each. | `media.read` | yes |
| `list_device_media` | **What was sent to a phone.** List the vault files sent to one phone, newest first, with how each delivery went. This is our record of what we put there, not a read of the phone's camera roll: photos taken or saved on the phone some other way do not appear. If the record and the phone may disagree, verify_media_on_devices re-checks it. | `media.read` | yes |
| `start_media_upload` | **Start uploading a file.** Step 1 of an upload. Reserve a slot in the media vault for a photo or video and get back a one-time upload_url. You then PUT the file's bytes to that URL yourself, with the Content-Type from upload_headers (the bytes do not go through this tool), and then call finish_media_upload with the asset_id you get here. Give the real byte size: the vault checks what actually arrives against it. Name a device to send the file to that phone when you finish; leave it out to keep the file in the vault and send it later with push_media. If the file is already online, import_media_from_url does all of this in one call. | `media.write` | no, it changes something |
| `finish_media_upload` | **Finish uploading a file.** Step 2 of an upload. Call this once you have PUT the bytes to the upload_url from start_media_upload. It checks with storage that the file really arrived and matches what was declared. With a device, it also queues the file for that phone: poll check_media_delivery afterwards. Without one, the file stays in the media vault, ready for push_media. Safe to call again if it fails: a repeat re-checks and re-queues rather than duplicating. The phone may show a one-time permission prompt the first time anything is sent to it. | `media.write` | no, it changes something |
| `import_media_from_url` | **Import a file from a link.** Fetch a photo or video from a public link straight into the media vault, in one call, with nothing to upload yourself. The link must answer with the file itself, not a web page around it: a direct link to the image or video. It is checked like any upload, so a page or an unsupported type is refused. Send it to phones afterwards with push_media. | `media.write` | no, it changes something |
| `check_media_delivery` | **Check whether a file arrived.** Report how a file is getting on with one phone: awaiting_upload, stored, queued, sending, delivered, failed, removing or removed. Poll this after finish_media_upload or push_media rather than assuming it worked, and read the error field when it says failed. For every phone at once, use get_media. | `media.write` | yes |
| `push_media` | **Send vault files to phones.** Send up to 100 files from the media vault to up to 200 phones in one go. Each phone gets every file, in its camera roll, optionally inside a named album. Delivery runs on a queue and takes minutes, so this answers with what was queued and what was skipped and why, and you follow up with get_media or check_media_delivery. A phone somebody is using is skipped rather than interrupted. Costs one upload per phone. The first time a phone receives anything it may show a one-time permission prompt that somebody must accept on the handset. | `media.write` | no, it changes something |
| `remove_media_from_devices` | **Take vault files back off phones.** Remove files this vault sent from the phones' camera rolls. Files recorded as delivered are removed and files still waiting to be sent are called off; a phone with files being sent right now is skipped until they land, so ask again then. The removal is confirmed by reading the phone back, so it is only reported done when it is. The first removal on a phone may wait on a one-time prompt somebody has to accept on the handset. The files stay in the vault. Costs one upload per phone. | `media.write` | no, it changes something |
| `verify_media_on_devices` | **Re-check what is on phones.** Re-read the phones' camera rolls and correct the record of which vault files are on them. Use it when a delivery reported failed but the file may have landed anyway, or before removing files from a phone you are unsure about. It runs in the background: read the result with list_device_media or get_media a minute later. Costs one upload per phone checked. | `media.write` | no, it changes something |
| `organise_media` | **Organise vault files.** Change several vault files at once: move them to a folder (null for no folder), mark them as posted or not, and add or take away tags. Give at least one change. Files that are not in the vault are listed under not_found rather than failing the rest. | `media.manage` | no, it changes something |
| `delete_media` | **Delete vault files.** Delete files from the media vault for good. This does not take them off any phone they were sent to: do that first with remove_media_from_devices if they should go from the phones too, because once a file is deleted from the vault it can no longer be removed through ALI. | `media.manage` | no, it changes something |
| `create_media_folder` | **Create a vault folder.** Create a folder in the media vault, at the top or inside another folder. Folders nest a few levels deep. | `media.manage` | no, it changes something |
| `update_media_folder` | **Rename or move a vault folder.** Rename a media vault folder, move it under another folder, or both. parent_id null moves it to the top. | `media.manage` | no, it changes something |
| `delete_media_folder` | **Delete a vault folder.** Delete a media vault folder and every folder beneath it. The files inside are kept and become unfiled; nothing is deleted from the vault or from any phone. | `media.manage` | no, it changes something |
| `create_media_tag` | **Create a vault tag.** Create a tag for labelling media vault files. Attach it with organise_media. | `media.manage` | no, it changes something |
| `rename_media_tag` | **Rename a vault tag.** Rename a media vault tag. Every file carrying it keeps it. | `media.manage` | no, it changes something |
| `delete_media_tag` | **Delete a vault tag.** Delete a media vault tag. It comes off every file that carried it; the files themselves are kept. | `media.manage` | no, it changes something |
| `get_usage_summary` | **Agency usage summary.** Read the agency's overall usage for a period: active minutes, how many people and phones were used, gestures sent, and a day-by-day trend. Use this to answer questions about how busy the agency has been. Figures cover the whole agency, not just the phones you can reach. | `analytics.read` | yes |
| `get_member_usage` | **Usage per person.** Read how much each person in the agency used the phones over a period: desk minutes, active minutes, how many phones they touched, how many sessions they sat, and gestures sent. Use this to compare activity across a team. Set include_devices to also get, for each person, which phones their minutes went to. Ask for that only when the question is about which phones, because it makes the answer much longer. | `analytics.read` | yes |
| `get_device_usage` | **Usage per phone.** Read how much each phone was used over a period: active minutes, how many people touched it, and when it was last active. Only covers the phones this connection can reach, so it is the report to use for questions about your own handsets. A phone no relay reports any more is left out, hours and all; get_member_usage with include_devices still counts the time someone spent on it. | `analytics.read` | yes |
| `get_sessions` | **Who was on which phone.** List individual sessions: one row per person per phone per stretch, saying who was on which handset from when to when, with the gestures they sent. This is the tool for questions about a particular phone or a particular moment - 'who was on X05 this morning', 'what was Cassie doing at 2pm', 'who last touched this phone before it broke'. Newest first; pass the returned next_cursor back as cursor for the next page. Only covers the phones this connection can reach. | `analytics.read` | yes |
| `get_activity` | **Recent activity.** Read when the agency works, as a 7x24 grid of worked minutes by hour of the local week (Monday first). Use this for questions about shift patterns and cover - which hours are busy, which are dead. It says when work happens, not who did it or on which phone: for that, call get_sessions. | `analytics.read` | yes |
| `list_members` | **List the team.** List the people in this agency with the roles each one holds. Call this to find a member's id before changing their roles. Requires that the connecting account can see the team in the dashboard. | `members.read` | yes |
| `get_member` | **Get one member.** Read one person in this agency: their username, email, and the roles they hold. | `members.read` | yes |
| `list_roles` | **List the roles.** List the roles this agency has, what each one allows, how many people hold it, and whether you are allowed to hand it out. Call this before inviting somebody or changing roles, to find the role id and to check assignable is true. | `members.read` | yes |
| `list_invitations` | **List invitations.** List invitations that have been sent and not yet accepted, with the role each one offers and how far it got. Statuses are sending, sent, opened, clicked, accepted, expired, revoked or failed. An opened status can be a mail client prefetching, so treat clicked and accepted as the reliable ones. | `members.read` | yes |
| `invite_member` | **Invite somebody to the agency.** Invite an email address to join this agency with a role, and send them the invitation mail. Call list_roles first and pick a role whose assignable is true; you cannot hand out a role above your own standing. The mail goes out immediately and to a real person, so confirm the address and the role before calling this. | `members.write` | no, it changes something |
| `withdraw_invitation` | **Withdraw an invitation.** Cancel an invitation that has not been accepted yet, so its link stops working. An invitation that was already accepted cannot be withdrawn; remove that person's roles instead. | `members.write` | no, it changes something |
| `resend_invitation` | **Resend an invitation.** Send an existing pending invitation again, with a fresh link. Use this when somebody says the mail never arrived or the link expired. It sends real mail to a real person, so do not call it repeatedly. | `members.write` | no, it changes something |
| `set_member_roles` | **Change a member's roles.** Replace the whole set of roles a member holds. This is not a delta: whatever you send becomes their roles, so read list_members first and send the full set you want. An empty list leaves the account able to do nothing, which is how you take somebody's access away without deleting them. You cannot act on somebody who outranks you or hand out a role above your own standing. | `members.write` | no, it changes something |
| `get_agency` | **Get the agency.** Read this agency's own details: its name, whether it is active, its timezone, how many phones it may hold and how many it holds, how many members it has, and whether two-factor is required. Use this to answer whether there is room for another phone before ordering one. | `agency.read` | yes |
| `list_groups` | **List phone groups.** List the groups the agency files its phones into, with how many phones are in each. Groups are the agency's own labels; a phone can be in several. Call this to find group ids before filing a phone. | `devices.read` | yes |
| `set_device_groups` | **File a phone into groups.** Set which groups a phone belongs to. This is not a delta: whatever you send becomes the phone's groups, so read its current ones first and send the full set. Naming a new group creates it. A group nothing is filed into disappears on its own. | `groups.write` | no, it changes something |
| `list_automations` | **List automations.** List this agency's automations with whether each is published, whether it is turned on, and how its last run went. Call this first to find an automation's id. | `automations.read` | yes |
| `list_automation_blocks` | **List the steps an automation can use.** List every kind of step an automation can be built from, with what it does, what settings it takes and which outputs it has. Call this before writing or changing a flow, so the steps you use exist and are wired by the right outputs. | `automations.read` | yes |
| `get_automation` | **Get one automation.** Read one automation: its published steps, its unpublished draft if there is one, its triggers, and its version history. Use this before changing a flow so you edit what is actually there. | `automations.read` | yes |
| `create_automation` | **Create an automation.** Create an empty automation with a name. It starts turned off and unpublished, so nothing runs until you add steps with update_automation and then publish it. | `automations.write` | no, it changes something |
| `update_automation` | **Change an automation.** Rename an automation, turn it on or off, or save a new draft of its steps. Saving a draft never refuses for a half-built flow: it answers with a list of problems to fix. Nothing you save here runs until you publish it. | `automations.write` | no, it changes something |
| `publish_automation` | **Publish an automation.** Make the current draft the version that runs. This is the step that checks the whole flow and refuses if anything is wrong, so a broken automation can never end up on a schedule. Runs already in progress keep the version they started with. | `automations.write` | no, it changes something |
| `run_automation` | **Run an automation now.** Start a run immediately. It runs with your access, so it reaches the phones you reach. If the automation declares inputs, which get_automation lists on its published steps, give a value for each one that has no default: a run missing one is refused before it touches a phone. It does not finish while you wait: poll get_automation_run to see how it went, and read its output for what the run handed back. | `automations.write` | no, it changes something |
| `list_automation_runs` | **List runs.** List recent runs, newest first, with how each one ended. Use it to answer whether last night worked. | `automations.read` | yes |
| `get_automation_run` | **Get one run, step by step.** Read one run and every step it took, in order, with which phone each step acted on and why any step was skipped or failed. This is the tool to use when asked why an automation did not do what somebody expected. | `automations.read` | yes |
| `cancel_automation_run` | **Stop a run.** Stop a run that is still going. It stops at its next step rather than mid-action, so a phone is never left half way through something. | `automations.write` | no, it changes something |
| `add_automation_trigger` | **Add a trigger.** Say when an automation should run: by hand only, on a schedule, or when a phone changes state. A scheduled trigger takes a five-field cron such as `0 2 * * *` for every night at two, read in the agency's timezone unless you name one. | `automations.write` | no, it changes something |
| `remove_automation_trigger` | **Remove a trigger.** Stop an automation being started that way. Its other triggers are left alone. | `automations.write` | no, it changes something |
| `delete_automation` | **Delete an automation.** Delete an automation and everything it recorded: its versions, its triggers and its run history. This cannot be undone. To simply stop it running, turn it off with update_automation instead. | `automations.write` | no, it changes something |
| `get_automation_batch` | **Read a batch of runs.** Read how an automation started on several phones is going: how many runs are in each state, whether all have finished, and for each phone its run, how it ended, why it failed and what it handed back. This is the tool to use after run_automation was given phones. | `automations.read` | yes |
| `list_automation_catalogue` | **Browse the automation catalogue.** List the ready-made automations ALI publishes for every agency: what each does, what it asks for when it runs, what it needs each phone to keep (such as its handle), which screens it was measured on, and whether this agency has already added it. Add one with add_catalogue_automation, then run it like any other. | `automations.read` | yes |
| `add_catalogue_automation` | **Add a catalogue automation to this agency.** Add one of the catalogue's automations to this agency, so it can be run, scheduled and used from the agency's own flows. Its steps stay ALI's: they cannot be changed here, and by default it follows new releases, so a fix made once reaches everybody. Send follows false to keep the version it was added at until the agency chooses to move. Adding one that is already there answers the same automation. | `automations.write` | no, it changes something |
| `list_result_datasets` | **List the kinds of result automations have recorded.** List the names automations have recorded results under, such as account_status or reel_stats, with how many rows each holds, how many different things they are about, and when the last one was recorded. Call this first to find what get_results can be asked for. Only covers the phones this connection can reach, plus rows about no phone. | `automations.read` | yes |
| `get_results` | **Read what automations have recorded.** Read the rows automations have recorded: structured results such as each account's status or each reel's numbers. Ask for latest to get only the newest row about each phone or subject, which is the current picture across the rack; leave it out for the history. changed keeps only rows that differ from the one before about the same thing, which is how to see what moved. Each row carries its values under data, the phone it came from, and whether it changed. Only covers the phones this connection can reach, plus rows about no phone. | `automations.read` | yes |

Every tool that changes something is marked destructive, including a single tap and a single invitation, so Claude asks before it runs. A tap cannot be undone, and an invitation is a real email to a real person.

## Permissions

The consent screen asks for these, and you can see them again later on the connected apps page:

- `devices.read`
- `devices.snapshot`
- `devices.write`
- `devices.control`
- `media.read`
- `media.write`
- `media.manage`
- `analytics.read`
- `members.read`
- `members.write`
- `agency.read`
- `groups.write`
- `automations.read`
- `automations.write`

## Ending a connection

Open https://beta.aliremote.com/api-and-mcp and disconnect it. The next call fails immediately: the tokens are deleted rather than left to expire.

## Limits

A connection spends credits, and every call is priced by what it does: a read costs least, an action more, a screenshot more again. Three layers pay for that. The included allowance is free and resets at UTC midnight. It belongs to the agency and every API key and connection draws on the same one, so a new connection does not start the day over; it scales with how many phones the agency has, on top of a flat base that does not. Above the allowance the calls come out of the agency's purchased credits, the same balance its API keys draw on, which is held until it is spent rather than reset. When both are gone a call is refused until somebody with billing access tops up, or until the allowance comes back at midnight. Above both sits a ceiling that refuses whether or not there is a balance, so a loop that goes wrong costs one day rather than the balance.

## What a connection cannot do

- Reach a phone the person who approved it cannot reach.
- See a screen the person who approved it cannot open. Holding the scope is not enough; an ordinary member cannot read the team through it.
- Hand out a role above the standing of the person who approved it.
- Reach another agency's phones or people, ever, including for ALI staff.
- Use a tool reserved for ALI staff. Such a tool is not offered to a connection that cannot use it.
- Outlive its revocation. Disconnecting takes effect on the next call.
- Delete an account, change agency settings, or touch billing.
