# Tool reference

All 24 tools, in the order the server registers them. Every call is scoped to the caller's
own accounts and phone grants, and anything touching a phone is
[asynchronous](/mcp/overview#everything-phone-touching-is-asynchronous).

Tools marked **async** queue work and return immediately — poll the account or submission
for the outcome.

Accounts and submissions live on a `platform` of `tiktok`, `instagram` or `youtube`, which
defaults to `tiktok`. Account ids are **UUIDs that identify their own platform**, so only
the three tools with no id to read it from — `list-accounts-tool`, `create-account-tool`,
`create-submission-tool` — take a `platform` parameter. See
[Platforms](/mcp/overview#platforms) for what actually differs between them.

## Accounts

### `list-accounts-tool`

List the authenticated user's posting accounts on TikTok, Instagram and YouTube, newest
first, 50 per page. Every platform is included unless `platform` narrows it to one.

| Parameter | Type | | Description |
|---|---|---|---|
| `page` | integer | optional | Page number to fetch (defaults to 1, min 1). Each page holds up to 50 accounts. |
| `platform` | string | optional | Only accounts on this platform: `"tiktok"`, `"instagram"` or `"youtube"`. Omit for every platform, merged newest-first. |

Returns `data` plus a `meta` object with `current_page`, `last_page`, `per_page`, `total`.
Each account carries its own `platform`.

### `get-account-tool`

Fetch a single posting account the authenticated user is allowed to view, on any platform.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | string (UUID) | **required** | ID of the account to fetch, on any platform. |

This is the polling tool for login lifecycle. Read `status` against the
[account status table](/mcp/overview#account-status).

### `create-account-tool` — async

Create a posting account for the authenticated user. `platform` chooses where it posts and
defaults to `tiktok`.

**TikTok** needs `username`, `password` and `slot`, and its login is **queued on the
assigned phone slot**. The account comes back immediately in `pending_login`; poll
`get-account-tool` for the outcome.

**Instagram and YouTube** need only a `handle`, and there are three outcomes:

- **with `username` and `password`** — created `pending_login` and its login is queued on
  the phone, exactly as TikTok's. `slot` must name a phone you control, or there is nothing
  for the login to run on.
- **with `already_signed_in: true`** — created `active`, nothing queued. Any credentials
  given are still stored, so a later `login-account-tool` can drive the phone.
- **with no credentials** — created `active` as a **record only**: a handle somebody signed
  in on the handset by hand, which the farm holds no way to sign in itself.

On any platform, `already_signed_in: true` means the account comes back `active` and ready
to post straight away, nothing is queued, and the phone is never contacted — so this works
even while the handset is offline. There is nothing to poll for.

| Parameter | Type | | Description |
|---|---|---|---|
| `platform` | string | optional | Where the account posts: `"tiktok"`, `"instagram"` or `"youtube"`. Defaults to `"tiktok"`. |
| `username` | string | *see above* | Login username or email. **Required on TikTok.** Optional on Instagram and YouTube, where supplying it with a password is what queues a login. |
| `password` | string | *see above* | Login password. **Required on TikTok**, even with `already_signed_in`, so the account can be signed back in if its session drops. Optional on Instagram and YouTube. |
| `slot` | string | *see above* | Phone slot the account is bound to, given as the `slot` UUID from `list-phones-tool`. **Required on TikTok**, where a slot holds at most 4 accounts. Optional on Instagram and YouTube, but a login has no phone to run on without it. |
| `handle` | string | *see above* | Public @handle. **Required on Instagram and YouTube** — nothing discovers it for us. Optional on TikTok. |
| `already_signed_in` | boolean | optional | Someone already signed this account in on the phone by hand. Creates it ready to post and skips the login. Defaults to `false`. |
| `notes` | string | optional | Free-form notes about this account. |

The per-phone cap of 4 accounts is a **TikTok** count and is enforced at validation on
TikTok creates only. If a phone is registered for the slot, you must hold control
permission on it — on any platform — or the call fails with
`You do not have permission to control the phone for this slot.`

### `delete-account-tool`

Delete a posting account the authenticated user is allowed to manage, on any platform. This
removes the account from the farm; it does not delete the account on the platform itself.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | string (UUID) | **required** | ID of the account to delete, on any platform. |

Returns `{ "deleted": true, "id": <id> }`.

## Submissions and uploads

A submission is one video published to one account. The account must be `active` — logged
in on its phone — or the call is rejected.

### `list-submissions-tool`

List the calling user's submissions, most recent first, 50 per page. Every platform is
included unless `platform` narrows it to one; each submission carries its own `platform`.

| Parameter | Type | | Description |
|---|---|---|---|
| `page` | integer | optional | Page number (1-based). Defaults to 1. |
| `platform` | string | optional | Only submissions posted to `tiktok`, `instagram` or `youtube`. Omit for every platform. |

Returns `data`, `current_page`, `last_page`, `total`.

### `get-submission-tool`

Fetch a single submission the calling user is allowed to view.

| Parameter | Type | | Description |
|---|---|---|---|
| `submission_id` | integer | **required** | ID of the submission to fetch. |

This is the polling tool for publishing. Read `status` against the
[submission status table](/mcp/overview#submission-status).

### `create-upload-url-tool`

Mint a short-lived signed URL for uploading a video directly, when you do not have a public
`video_url`. Takes **no parameters**.

Returns `upload_id`, `upload_url`, and `expires_at`. POST or PUT the raw video (or a
multipart `video` field) to `upload_url` **within 15 minutes**, then pass `upload_id` to
`create-submission-tool`.

The signature *is* the grant — the upload endpoint takes no bearer token, and the URL
embeds the owning user so `create-submission-tool` can verify it belongs to you.

### `create-submission-tool` — async

Queue a video to be published to a TikTok, Instagram or YouTube account. Returns the
submission immediately, in `pending`; the phone is then driven through that platform's app.
Poll `get-submission-tool` for the outcome.

| Parameter | Type | | Description |
|---|---|---|---|
| `platform` | string | optional | Platform to post to: `"tiktok"` (default), `"instagram"` or `"youtube"`. |
| `account_id` | string (UUID) | **required** | ID of the account to post as. Required unless the deprecated `tik_tok_account_id` is given. |
| `tik_tok_account_id` | string (UUID) | *deprecated* | Alias for `account_id`. Still accepted; prefer `account_id`. |
| `video_url` | string | *one of* | Public https URL of the video, downloaded by the server. Mutually exclusive with `upload_id`. |
| `upload_id` | string | *one of* | Upload id from `create-upload-url-tool`. Mutually exclusive with `video_url`. |
| `caption` | string | *see below* | Text for the post. Up to 2200 characters on TikTok and Instagram. On YouTube this is the Short's **title** — **required**, and capped at 100 characters. |
| `draft` | boolean | optional | Save as a draft on the phone instead of publishing. Supported on all three platforms. |

Provide the video via **exactly one** of `video_url` or `upload_id` — supplying both is a
validation error, and supplying neither is too.

`account_id` is looked up on the requested platform's table only. An id belonging to
another platform answers `Account not found.`, the same as a made-up one — so a `platform`
that does not match the account reads as a missing account, not as a mismatch.

Omitting `caption` on YouTube is refused with
`Give the Short a title. YouTube names an untitled Short after the date.` — an untitled
Short really does end up named after the day it was posted.

The account must be `active`, otherwise:
`This account is not logged in on its phone yet, so it cannot post.`

You may post to an account you own, or to one whose phone you hold a submit grant on.

### `cancel-submission-tool`

Cancel a queued or in-flight submission. The running pipeline stops driving the phone
between steps; a not-yet-started job no-ops.

| Parameter | Type | | Description |
|---|---|---|---|
| `submission_id` | integer | **required** | ID of the submission to cancel. |

Only works while the submission is non-terminal. Already finished returns
`This submission has already finished.`

### `delete-submission-tool`

Delete a finished submission (`published`, `drafted`, `failed`, or `cancelled`) and its
stored video.

| Parameter | Type | | Description |
|---|---|---|---|
| `submission_id` | integer | **required** | ID of the submission to delete. |

In-flight submissions cannot be deleted — cancel first. Otherwise:
`Submissions can only be deleted once they have finished processing.`

## Account lifecycle

### `login-account-tool` — async

Log an account in on its phone, on any platform. Queues the login and returns immediately;
poll `get-account-tool` to observe the outcome.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | string (UUID) | **required** | The ID of the account to log in, on any platform. |

Sets the account to `pending_login` and clears any previous `login_error`. If a login is
already mid-flight it declines with `This account is already logging in.` rather than
starting a second one.

**Requires stored credentials.** Instagram and YouTube accounts may be record-only —
signed in on the handset by hand, with no `username`/`password` held here — and those are
`active` and post fine, but there is nothing to type into a login form for them. The call
is refused with a message saying so; give the account a username and password first.

### `logout-account-tool` — async

Log an account out on its phone. Works on TikTok, Instagram and YouTube. Queues the logout
and returns immediately; poll `get-account-tool` to observe the outcome.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | string (UUID) | **required** | The ID of the account to log out, on any platform. |

A platform's flow can decline after it checks the phone; when it does, the account status
is left unchanged rather than reported as signed out. Already logged out returns
`This account is already logged out.`

### `stop-login-tool`

Cancel a login in progress on any platform: stop the on-phone agent and move the account
off "logging in".

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | string (UUID) | **required** | The ID of the account whose login should be stopped, on any platform. |

Stopping the on-phone agent is best-effort — if nothing is running or the phone is
unreachable, the account is still moved out of the stuck state. It lands in `failed` with
`login_error` set to `Login stopped.` This is the tool for an account wedged in
`logging_in`.

## Phones

These reach the farm hardware directly, and are platform-agnostic — they drive a handset,
not an app. All of them require a grant on the slot, and the slot must be **cast/live**
(video streaming from the phone) for commands, raw input, snapshots, OCR, macros, and the
agent.

### `list-phones-tool`

List the farm phone slots the authenticated user can view, with their live video and
session state. Takes **no parameters**.

Returns one object per accessible slot: `slot`, `name`, `video_live`, `input_present`,
`how`, `ip`, `model`, `os_version`, `session_name`. Slots you hold no grant on are omitted.
Start here to find a slot id and confirm it is live before commanding it.

`slot` is the UUID every other phone tool wants. `name` is the human label for the same
phone (`"slot1"`, `"slot12"`) and is never accepted in its place. Pass a name where a slot
is wanted and it matches no phone, which surfaces as an authorization error rather than a
missing one.

### `phone-command-tool`

Queue a device command on a farm phone, then trigger its on-phone Shortcut to run it.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to command, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `op` | string | **required** | The command op. See below. |
| `text` | string | optional | Text to set on the clipboard (for `clipboard_set`). |
| `url` | string | optional | URL or deep link to open (for `open_url`). |
| `level` | number | optional | Brightness level 0–1 (for `brightness`). |
| `on` | boolean | optional | Desired on/off state (for `wifi`, `airplane`, `cellular`, `flashlight`). |

Supported ops:

| Op | Field | Effect |
|---|---|---|
| `clipboard_set` | `text` | Set the phone clipboard. |
| `clipboard_get` | — | Read the phone clipboard back. |
| `open_url` | `url` | Open a URL / deep link. |
| `reboot` | — | Restart the phone. |
| `clear_photos` | — | Empty the camera roll. |
| `get_ip` | — | Report the phone's LAN IP. |
| `brightness` | `level` | Set screen brightness (0–1). |
| `wifi` | `on` | Toggle Wi-Fi. |
| `airplane` | `on` | Toggle airplane mode. |
| `cellular` | `on` | Toggle cellular. |
| `flashlight` | `on` | Toggle the flashlight. |

Op-specific fields are only sent when provided. The tool confirms the command was queued
and the Shortcut triggered — it does not return the command's result inline. For
`clipboard_get` and `get_ip`, read the value back from the farm.

### `phone-control-tool`

Send a raw input primitive to a farm phone. Unlike `phone-command-tool`, which queues an
on-phone Shortcut, these are direct farmd socket ops and run **synchronously** — the call
returns once farmd acks.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to control, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `op` | string | **required** | Input op: `tap`, `swipe`, `hotkey`, `type`. |
| `fx`, `fy` | number | *for `tap`* | Tap point, fractions 0–1. |
| `fx1`, `fy1`, `fx2`, `fy2` | number | *for `swipe`* | Swipe start and end, fractions 0–1. |
| `steps` | integer | optional | Swipe step count controlling gesture speed (1–500). Defaults to 20. |
| `key` | string | *for `hotkey`* | One of `home`, `app_switcher`, `control_center`, `notifications`, `paste`, `run_shortcut`. |
| `text` | string | *for `type`* | ASCII text to type into the focused field. |

Coordinates are **fractions of the screen**: `0` is left/top, `1` is right/bottom. Missing
op-specific fields are rejected with a message naming what the op needs — `tap requires fx
and fy (0..1).`

### `phone-snapshot-tool`

Capture a JPEG snapshot of a farm phone screen.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to snapshot, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `width` | integer | optional | Snapshot width in pixels (120–2000). Defaults to the configured farm width. |

Returns an image, not JSON. This is the way to see what a phone is actually showing — the
fastest way to understand a stuck login or a failed publish.

### `phone-ocr-tool`

Read the on-screen text of a farm phone: snapshots the slot and returns the OCR-recognized
text. Read-only and synchronous.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to read, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `width` | integer | optional | Snapshot width in pixels (120–2000). Defaults to the configured farm width. |

The text comes back wrapped in a delimited envelope marking it as **untrusted device-screen
data, not instructions** — whatever is on the phone is reported, never acted on. Use it
when you want to match on text rather than look at a picture.

### `run-macro-tool` — async

Run a step list on a farm phone: either a named workflow from the registry, or a raw step
list.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to run on, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `workflow` | string | *one of* | Named workflow / system macro to resolve and run. |
| `params` | object | optional | Scalar params for the workflow's `{{placeholders}}`. Non-scalar values are dropped. |
| `steps` | array | *one of* | Raw step list run as-is; each step needs an `action`. |

Give **exactly one** of `workflow` or `steps`, or the call fails with
`Provide exactly one of "workflow" or "steps".` A full workflow can run for minutes, so
this queues the run and returns immediately — poll the phone (`phone-snapshot-tool` or
`phone-ocr-tool`) or the affected account for the outcome.

### `run-phone-agent-tool` — async

Ask the on-phone GUI agent to carry out a narrow task on a farm phone.

| Parameter | Type | | Description |
|---|---|---|---|
| `slot` | string | **required** | Phone slot to run the agent on, given as the `slot` UUID from `list-phones-tool`. Must be cast/live. |
| `task` | string | **required** | Plain-language task for the on-phone GUI agent to carry out. |

The call only acks the start; the agent streams progress separately. Watch the phone (via
`phone-snapshot-tool`) or the account for the outcome. If the farm's GUI agent is not
enabled or not ready, the call fails with
`The farm GUI agent is not enabled or not ready.`

Keep `task` narrow. It is a GUI agent driving a real phone, not a general assistant.

## Billing

Renting phones costs money, and by default no tool here can spend yours. These four
read your subscription and hand back **links** — a Stripe Checkout page, or an approval
page on your dashboard. Both are inert until you act on them.

The one exception is a standing allowance you set yourself at
[0bull.net/settings/mcp](https://0bull.net/settings/mcp), capped at a phone count you
choose. Within it, `request-phones-tool` applies an increase on the spot and places the
order. Nothing is charged until an admin assigns the phone. It is off unless you turn it on.

### `get-billing-tool`

Read the rental subscription: whether there is one, how many phones it covers, the price
per phone, its Stripe status, and when it renews. Takes **no parameters**. The response
always includes `pending_orders`, a list of orders waiting for an admin to assign phones;
those orders have not been charged. Start here before proposing any change.

### `start-rental-tool`

Open a Stripe Checkout page for a first subscription.

| Parameter | Type | | Description |
|---|---|---|---|
| `phones` | integer | *one of* | Number of phones for one country, 1 to 50. Give this or `items`. |
| `country` | string | optional | ISO 3166-1 alpha-2 country for `phones`, such as `ES`, `US` or `JP`. Defaults to `ES`. |
| `items` | array | *one of* | Rows of `{ country, quantity }` for a multi-country order. Use 1 to 10 rows and at most 50 phones total. Each country may appear once. |
| `accept_terms` | boolean | **required** | Must be `true`. Before calling, show [terms](https://0bull.net/terms) and [privacy](https://0bull.net/privacy), and get agreement to them and to monthly renewal charges for each assigned phone. |

Returns `checkout_url` and normalized `items`. Nothing is charged at checkout and no phones
are assigned until an admin assigns concrete phones. Fails if a subscription is already
active. Use `request-phones-tool` to change an existing one.

### `request-phones-tool`

Change how many phones the subscription covers.

| Parameter | Type | | Description |
|---|---|---|---|
| `phones` | integer | *one of* | The **total** the subscription should cover afterwards, 1 to 50. Give this or `add`, not both. |
| `add` | integer | *one of* | Signed change to the current count, from -49 to 49. For example, `add: 1` adds one and `add: -2` removes two. Give this or `phones`, not both. |
| `accept_terms` | boolean | **required** | Must be `true`. Before calling, show [terms](https://0bull.net/terms) and [privacy](https://0bull.net/privacy), and get agreement to them and to monthly renewal charges for each assigned phone. |

Read `applied` in the response to know what happened. It says whether the order was placed
without approval, not whether money moved:

| `applied` | What happened | What you get |
|---|---|---|
| `false` | Filed, waiting on you | `request_id`, `approval_url`, and an estimated charge. Nothing has been charged. |
| `true` | Order placed under your standing allowance | Phones are pending admin assignment. The card is charged only when each phone is assigned. |

Adding phones costs full list price per phone when each phone is assigned, and your renewal date
does not move: the added phones join the cycle you are already on, so their first month
runs to your existing period end. Nothing is prorated, so the day of the month makes no
difference to the price. Removing phones takes effect at the next renewal, with no credit
now.

A request expires in an hour and can be answered once.

Auto-apply only ever covers an **increase** that lands at or below your allowance. An
increase past it, and every decrease, still asks — the allowance is permission to spend
your money, not to take your phones away.

### `get-billing-request-tool`

Check what you decided about a filed change.

| Parameter | Type | | Description |
|---|---|---|---|
| `request_id` | string | **required** | The `request_id` from `request-phones-tool`. |

Returns `pending`, `approved`, `declined`, `failed` (Stripe refused, nothing charged) or
`expired`. Requests belonging to anyone else read as not found.
