# MCP tools (/mcp/tools)



17 tools. A `spend` connection sees all 17. A `read` connection sees 12: the 5 spend tools are not registered. Setup: [connect a client](/mcp/connect).

| Tool                     | Access    | Limit per person | What it does                                           |
| ------------------------ | --------- | ---------------- | ------------------------------------------------------ |
| `riffads_ping`           | Read      | none             | Workspace, plan, whether this connection may spend     |
| `list_capabilities`      | Read      | 120/min          | A page of what this workspace can generate             |
| `get_capability_schema`  | Read      | 120/min          | One capability's JSON Schema and an example config     |
| `estimate_generation`    | Read      | 60/min           | The number for `max_credits`. Reserves nothing         |
| `list_actors`            | Read      | 120/min          | A page of actors                                       |
| `list_voices`            | Read      | 120/min          | A page of voices                                       |
| `get_credit_balance`     | Read      | 120/min          | Available credits and the agent spend ceiling in force |
| `submit_generation`      | **Spend** | 20/min           | Starts one generation                                  |
| `generate_talking_actor` | **Spend** | 20/min           | `submit_generation` for `actor_ultra`                  |
| `wait_for_generation`    | Read      | 120/min          | Blocks up to 20 seconds, answers when done             |
| `get_generation`         | Read      | 120/min          | Reads one generation now                               |
| `get_batch`              | Read      | 120/min          | Reads every variant of one submit                      |
| `create_upload`          | **Spend** | 30/min           | Reserves an `ast_` id and a signed PUT URL             |
| `finalize_upload`        | **Spend** | 30/min           | Confirms the bytes landed, answers `usable`            |
| `list_templates`         | Read      | 120/min          | Runnable workflow templates and their inputs           |
| `run_workflow`           | **Spend** | 20/min           | Runs a whole workflow                                  |
| `get_workflow_run`       | Read      | 120/min          | Reads a run node by node                               |

Limits count per person, not per connection. Over a limit: `rate_limited` with `retry_after_seconds`.

## Results and refusals [#results-and-refusals]

* **Success:** one text block of JSON with `ok: true` plus the tool's fields. Files also come as `resource_link` blocks with signed URLs.
* **Refusal:** `isError` set. Body: `ok: false`, `code`, `message`, `retryable`, `credits_charged`.
* **Schemas are strict.** Unknown keys are refused. `maxCredits` instead of `max_credits` is a refusal.
* **`capability_id` is a free string**, not an enum. New capabilities work with an old tool list.
* **Branch on `code` and `retryable`**, never on `message`. Every code: [errors](/reference/errors).

```json title="A refusal"
{
  "ok": false,
  "code": "max_credits_exceeded",
  "message": "This generation costs more than the max_credits you set (600 credits). This one needs 660 credits. Raise max_credits, or ask for something cheaper, such as a shorter or lower resolution render.",
  "retryable": false,
  "credits_charged": 0,
  "limit": { "bound_by": "max_credits", "limit_credits": 600, "required_credits": 660 }
}
```

`limit.bound_by` names the cap that refused: `max_credits` (yours to raise) or a workspace cap (ask an owner or admin).

## Orient [#orient]

### riffads\_ping [#riffads_ping]

Read. No arguments. Spends nothing, no rate limit.

```json title="Result"
{
  "ok": true,
  "user_id": "...",
  "organization_id": "org_...",
  "workspace": "Northwind Studio",
  "plan": "growth",
  "mode": "spend",
  "can_spend_credits": true,
  "server_time": "2026-09-17T10:04:11.882Z"
}
```

`mode: "read"` means no spend tools until a person changes it.

## Discover [#discover]

### list\_capabilities [#list_capabilities]

Read. Paged.

| Argument | Type        | Required | Notes                                |
| -------- | ----------- | -------- | ------------------------------------ |
| `limit`  | integer > 0 | no       | Capped server side                   |
| `cursor` | string      | no       | `next_cursor` from the previous page |

Returns `capabilities[]`, `total`, `next_cursor`.

```json title="Result, trimmed"
{
  "ok": true,
  "next_cursor": "gpt_image_2",
  "capabilities": [
    {
      "capability_id": "actor_ultra",
      "name": "Talking Actor",
      "description": "OmniHuman 1.5: an actor speaks your script.",
      "category": "avatar",
      "output_kind": "video",
      "status": "available"
    }
  ]
}
```

* `status`: `available`, `requires_plan`, `coming_soon`, `retired`, `unknown`.
* `requires_plan` rows carry `required_plan`.
* Retired rows are left out of the list.
* Category order: avatar, video, image, preset, tool.

Every id: [capabilities](/capabilities).

### get\_capability\_schema [#get_capability_schema]

Read.

| Argument        | Type   | Required |
| --------------- | ------ | -------- |
| `capability_id` | string | **yes**  |

Returns `{ capability: { capability_id, name, description, category, output_kind, config_schema, example_config } }`.

* `config_schema`: JSON Schema for `config`.
* `example_config`: the smallest config that validates.
* Refusals may add `capability_status` and `required_plan`.

<Callout type="info" title="Some rules do not fit in JSON Schema">
  A schema-valid config can still be refused (some settings depend on other fields). Run `estimate_generation` to check a config before you submit.
</Callout>

## Price [#price]

### estimate\_generation [#estimate_generation]

Read. Reserves nothing, charges nothing.

| Argument        | Type   | Required |
| --------------- | ------ | -------- |
| `capability_id` | string | **yes**  |
| `config`        | object | **yes**  |

```json title="Result. The numbers are arbitrary"
{
  "ok": true,
  "capability_id": "veo_31",
  "credits": 400,
  "is_ceiling": false,
  "seconds": 8,
  "breakdown": [
    { "label": "Video, 1080p with audio", "unit": "second", "quantity": 8, "credits": 400 }
  ]
}
```

* `is_ceiling: true`: an upper bound, not the exact number.
* `seconds` and `note` show up only when they apply.
* Numbers change. Estimate every time, do not cache.

<Callout type="warn" title="Do not send the estimate back as max_credits">
  Send at least `ceil(estimate * 1.1)`. The bare estimate is refused with `max_credits_exceeded`.
</Callout>

## Library [#library]

### list\_actors [#list_actors]

Read. Paged.

| Argument | Type        | Required | Notes                          |
| -------- | ----------- | -------- | ------------------------------ |
| `search` | string      | no       | Matches names and descriptions |
| `limit`  | integer > 0 | no       | Capped server side             |
| `cursor` | string      | no       | From the previous page         |

Row: `{ actor_id, name, description, gender, age_band, tags[], default_voice_id, default_voice_name, is_platform }`, plus `next_cursor`. `is_platform` is `true` for RiffAds actors, `false` for your workspace's own. No preview images.

### list\_voices [#list_voices]

Read. Paged.

| Argument   | Type        | Required | Notes                                        |
| ---------- | ----------- | -------- | -------------------------------------------- |
| `search`   | string      | no       | Matches voice names                          |
| `language` | string      | no       | Exact language tag, for example `en` or `es` |
| `limit`    | integer > 0 | no       | Capped server side                           |
| `cursor`   | string      | no       | From the previous page                       |

Row: `{ voice_id, name, language, gender, tags[], is_cloned, is_premium, is_platform }`, plus `total` and `next_cursor`. No preview audio.

### get\_credit\_balance [#get_credit_balance]

Read. No arguments.

```json title="Result. The numbers are arbitrary"
{
  "ok": true,
  "credits_available": 4820,
  "credits_on_hold": 660,
  "agent_limits": {
    "max_credits_per_generation": 600,
    "daily_credits_limit": 2000,
    "spent_last_24_hours": 740,
    "daily_credits_remaining": 1260,
    "most_one_generation_may_cost": 600,
    "bound_by": "org_per_generation"
  }
}
```

* `most_one_generation_may_cost`: the highest `max_credits` a submit can pass right now.
* `bound_by`: `max_credits`, `org_per_generation`, `api_key_budget` or `org_daily`. API key fields never appear on MCP.

## Spend [#spend]

Spend tools are marked `destructiveHint: true`, so some clients ask before calling them.

### submit\_generation [#submit_generation]

**Spend.** Answers when the job is **accepted**, not when it is ready.

<TypeTable
  type="{
  capability_id: {
    type: 'string',
    required: true,
    description: 'Exactly as list_capabilities returned it.',
  },
  config: {
    type: 'object',
    required: true,
    description: 'Matches get_capability_schema. Unknown keys are refused.',
  },
  max_credits: {
    type: 'integer',
    required: true,
    description: 'Most this request may cost, all variants. Send at least ceil(estimate * 1.1).',
  },
  variants: {
    type: 'integer',
    required: false,
    description: 'Several takes of one config, up to 4. Each is charged. Image and video only.',
  },
  actor_id: {
    type: 'string',
    required: false,
    description: 'act_ id from list_actors. Required for a talking actor unless you pass actor_image_asset_id.',
  },
  actor_image_asset_id: {
    type: 'string',
    required: false,
    description: 'ast_ image id that finalize_upload called usable. Used as the face instead of a library actor.',
  },
  voice_id: {
    type: 'string',
    required: false,
    description: 'voc_ id from list_voices. Optional if the actor has a default voice.',
  },
  approved_voice_generation_id: {
    type: 'string',
    required: false,
    description: 'Talking actor: generation_id of the finished tts generation for this exact script and voice.',
  },
}"
/>

```json title="Arguments"
{
  "capability_id": "veo_31",
  "config": { "prompt": "...", "duration": 8, "resolution": "1080p" },
  "max_credits": 440
}
```

```json title="Result, trimmed"
{
  "ok": true,
  "generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033",
  "generation_ids": ["gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033"],
  "run_id": "run_9f2c1b",
  "replay": false,
  "capability_id": "veo_31",
  "capability_name": "Veo 3.1",
  "output_kind": "video",
  "outputs_expected": 1,
  "credits": { "credits_held": 440, "credits_charged": null, "settlement": "open", "terminal": false },
  "charge_summary": "440 credits are on hold. Nothing has been charged so far.",
  "next_action": "Call wait_for_generation with this generation_id..."
}
```

* `generation_ids` is always set. `batch_group_id` (`bg_`) appears only when `variants` is above 1.
* `run_id: null` is not a failure. Follow `generation_id`.
* `replay: true`: the same request already ran. You get that generation back, no second charge.
* `variants` above the limit is refused, never clamped.
* `voice_id` (argument) is not `voiceId` (inside `config`).
* No output link on submit.

<Callout type="warn" title="One agent submission at a time">
  A second submit while one is running gets `submission_in_flight` (retryable). Wait for the first, then send.
</Callout>

### generate\_talking\_actor [#generate_talking_actor]

**Spend.** Same as `submit_generation` with `capability_id: "actor_ultra"`.

<TypeTable
  type="{
  script: {
    type: 'string',
    required: true,
    description: 'Word for word the same script as the tts generation.',
  },
  actor_id: {
    type: 'string',
    required: true,
    description: 'act_ id from list_actors.',
  },
  approved_voice_generation_id: {
    type: 'string',
    required: true,
    description: 'generation_id of the finished tts generation for this script, voice and actor.',
  },
  max_credits: {
    type: 'integer',
    required: true,
    description: 'Most this request may cost. Send at least ceil(estimate * 1.1).',
  },
  voice_id: {
    type: 'string',
    required: false,
    description: 'voc_ id from list_voices. Optional if the actor has a default voice.',
  },
  settings: {
    type: 'object',
    required: false,
    description: 'Other actor_ultra settings from get_capability_schema. Omit for defaults.',
  },
}"
/>

<Callout type="error" title="Two generations, in this order">
  1. `submit_generation` with `capability_id: "tts"`. Wait until it completes.
  2. `generate_talking_actor` with that `generation_id` as `approved_voice_generation_id`.

  Audio made for a different actor, voice or script is refused.
</Callout>

```json title="Step 1: the voice (submit_generation)"
{
  "capability_id": "tts",
  "config": { "script": "Three weeks in and I am not going back." },
  "actor_id": "act_4f1e...",
  "voice_id": "voc_9a20...",
  "max_credits": 40
}
```

```json title="Step 2: the video (generate_talking_actor)"
{
  "script": "Three weeks in and I am not going back.",
  "actor_id": "act_4f1e...",
  "voice_id": "voc_9a20...",
  "approved_voice_generation_id": "gen_1f88...",
  "max_credits": 700
}
```

* The `max_credits` values are placeholders. Estimate each step.
* `script` wins over a `script` inside `settings`.
* `actor_ultra` has no `aspect_ratio`. Sending one is refused as an unknown key. Output matches the start frame's shape.
* Result shape matches `submit_generation`.

Full walkthrough: [talking actor](/guides/talking-actor).

## Follow [#follow]

### wait\_for\_generation [#wait_for_generation]

Read. The right way to follow a job.

| Argument        | Type   | Required |
| --------------- | ------ | -------- |
| `generation_id` | string | **yes**  |

Blocks up to **20 seconds** and answers the moment the job ends. `still_running: true`: call it again right away.

```json title="Result, trimmed"
{
  "ok": true,
  "still_running": true,
  "waited_seconds": 20,
  "age_seconds": 74,
  "retry_after_seconds": 0,
  "generation": {
    "generation_id": "gen_0193c8...",
    "status": "rendering",
    "outputs": []
  },
  "next_action": "Still working..."
}
```

* Branch on `still_running`, not `status`.
* `age_seconds` is the job's total age.
* Pass a `progressToken` to get `notifications/progress` over an event stream.

### get\_generation [#get_generation]

Read. One check, now. Do not loop it: use `wait_for_generation`.

| Argument        | Type   | Required |
| --------------- | ------ | -------- |
| `generation_id` | string | **yes**  |

```json title="Result, trimmed"
{
  "ok": true,
  "output_urls_expire_in_seconds": 600,
  "generation": {
    "generation_id": "gen_0193c8...",
    "capability_id": "veo_31",
    "status": "completed",
    "batch_group_id": null,
    "batch_index": null,
    "outputs": [
      {
        "index": 0,
        "kind": "video",
        "url": "https://...signed...",
        "file_name": "veo-31-0.mp4",
        "width": 1080,
        "height": 1920
      }
    ],
    "outputs_expected": 1,
    "outputs_delivered": 1,
    "credits": { "credits_held": 440, "credits_charged": 400, "settlement": "captured", "terminal": true },
    "charge_summary": "400 credits were charged.",
    "error": null,
    "created_at": "2026-09-17T10:04:11.882Z",
    "completed_at": "2026-09-17T10:09:52.104Z"
  },
  "next_action": "Done. The output links are short lived..."
}
```

* Statuses: `queued`, `rendering`, `post_processing`, `completed`, `failed`. A canceled job reads `failed`.
* `credits.credits_charged` is `null` until `credits.terminal` is `true`. Null is unknown, not zero.
* Each file also comes as a `resource_link` with `mimeType` and a title like `video 1 of 1`.

<Callout type="warn" title="Links die after 600 seconds">
  Download the file or hand the link on right away. Expired? Call `get_generation` again for a fresh link. See [results](/guides/results).
</Callout>

### get\_batch [#get_batch]

Read. Every variant of one submit.

| Argument         | Type   | Required |
| ---------------- | ------ | -------- |
| `batch_group_id` | string | **yes**  |

Returns `status` (`running`, `completed`, `failed`, `partial`), `variants`, `variants_finished`, `outputs_delivered`, one `credits` total, `charge_summary`, `generations[]`, and a `resource_link` per delivered variant.

`get_batch` does not wait. Call `wait_for_generation` on a running id, then `get_batch` again. Batch `credits_charged` stays `null` until every variant ends.

## Upload a file [#upload-a-file]

Three steps. The middle one is your own HTTP call, not a tool.

<Steps>
  <Step>
    ### create\_upload [#create_upload]

    **Spend.** Reserves a slot. No bytes, no credits.

    <TypeTable
      type="{
  filename: {
    type: 'string',
    required: true,
    description: 'Display name. Up to 255 characters.',
  },
  content_type: {
    type: 'string',
    required: true,
    description: 'Exact MIME type. Anything else is refused.',
  },
  size_bytes: {
    type: 'integer',
    required: true,
    description: 'Real size in bytes. A mismatch is refused.',
  },
  checksum_sha256: {
    type: 'string',
    required: false,
    description: 'Base64 SHA-256. If sent, storage verifies the bytes.',
  },
}"
    />

    | Kind  | Types                                                                            | Max    |
    | ----- | -------------------------------------------------------------------------------- | ------ |
    | Image | `image/jpeg`, `image/png`, `image/webp`, `image/gif`                             | 20 MB  |
    | Video | `video/mp4`, `video/webm`, `video/quicktime`                                     | 100 MB |
    | Audio | `audio/mpeg`, `audio/mp3`, `audio/wav`, `audio/x-wav`, `audio/webm`, `audio/ogg` | 25 MB  |

    Returns `asset_id`, `upload_url`, `upload_expires_at`, `upload_expires_in_seconds`, `kind`, `content_type`, `size_bytes`, `max_bytes_for_kind`, `content_checked`, `next_action`.
  </Step>

  <Step>
    ### PUT the bytes [#put-the-bytes]

    One HTTP `PUT` to `upload_url`. Same content type, exact byte count, within **5 minutes**.
  </Step>

  <Step>
    ### finalize\_upload [#finalize_upload]

    **Spend.** Call after the `PUT` returns.

    | Argument   | Type   | Required |
    | ---------- | ------ | -------- |
    | `asset_id` | string | **yes**  |

    Returns `{ asset_id, scan_status, usable, kind, duration_ms, next_action }`.

    * `scan_status`: `pending`, `clean`, `flagged`.
    * Use the `ast_` id in a config only after `usable: true`.
    * `usable: false` with `flagged` is final. Use another file.
    * Calling twice returns the same answer.
    * `duration_ms` is measured server side. Null for images.
  </Step>
</Steps>

Only images are content checked. `content_checked` tells you. See [content policy](/policy/content).

## Workflows [#workflows]

Run a workflow built in the app, or a template. Agents cannot build or edit workflows. See [workflows](/guides/workflows).

### list\_templates [#list_templates]

Read. No arguments.

Returns `templates[]`. Each: `{ template_key, name, description, category, step_count, inputs[] }`.

```json title="One input"
{
  "node": "f_brand",
  "label": "Brand context",
  "field": "text",
  "field_label": "value",
  "kind": "text",
  "max_length": 8000,
  "filled": true
}
```

* `kind`: `text`, `setting`, `actor`, `voice`.
* `setting` inputs carry `accepts`. `text` inputs carry `max_length`.
* `filled: true`: the node already has a value.
* Node ids cannot be guessed. Read them here.

### run\_workflow [#run_workflow]

**Spend.** Every step is charged.

<TypeTable
  type="{
  template_key: {
    type: 'string',
    required: false,
    description: 'From list_templates. This OR workflow_id. The first run makes a copy named with (agent), later runs reuse it.',
  },
  workflow_id: {
    type: 'string',
    required: false,
    description: 'A wf_ workflow in this workspace. This OR template_key.',
  },
  inputs: {
    type: 'array',
    required: false,
    description: 'Up to 60 { node, field, value }. Omitted fields keep their value. The saved workflow is not changed.',
  },
  max_credits: {
    type: 'integer',
    required: true,
    description: 'Most the whole run may cost, every step.',
  },
}"
/>

* Exactly one of `template_key` or `workflow_id`. Both or neither: `invalid_config`.
* `value` is a string, number or boolean. Field `actor` takes an `act_` id, field `voice` takes a `voc_` id.
* All or nothing: one bad input refuses the whole run. The message names the node and field.
* `count` cannot be set. Asset inputs cannot be filled.

```json title="Arguments"
{
  "template_key": "ugc_face_cam",
  "inputs": [
    { "node": "f_brand", "field": "text", "value": "A refillable cleaning spray sold direct." }
  ],
  "max_credits": 1200
}
```

```json title="Result, trimmed"
{
  "ok": true,
  "workflow_run_id": "wfr_7c1e...",
  "workflow_id": "wf_2ab9...",
  "template_key": "ugc_face_cam",
  "billable_node_count": 6,
  "applied_inputs": [{ "node": "f_brand", "field": "text" }],
  "replay": false,
  "next_action": "This run has 6 step(s) to work through..."
}
```

`billable_node_count` counts the steps that will run and charge. No output link on `run_workflow`.

### get\_workflow\_run [#get_workflow_run]

Read. The only way to follow a run. There is no wait tool for runs.

| Argument          | Type   | Required |
| ----------------- | ------ | -------- |
| `workflow_run_id` | string | **yes**  |

Answers now. Call about every 30 seconds until `still_running` is `false`.

Returns `workflow_run_id`, `workflow_id`, `status`, `still_running`, `nodes[]`, `credits`, `started_at`, `finished_at`, `next_action`, and `output_urls_expire_in_seconds` once a file is ready.

* Run status: `queued`, `running`, `completed`, `partial`, `failed`, `canceled`.
* Node: `{ node_id, label, status, credits, generations[], text, error }`. Writer nodes return `text`, no file.
* Every file of every node comes as a `resource_link`.
* `credits.credits_charged` is `null` until the run ends. `credits_charged_so_far` grows while it runs.
* `partial`: some steps finished, failed steps charged nothing.

## Not on MCP [#not-on-mcp]

* No cancel tool. A job canceled elsewhere reads `failed`.
* No outbound webhooks.
* No list of past generations. Keep your ids.
* No MCP resources or prompts.
* No workflow authoring.
* Nothing posts to Meta, TikTok, YouTube or X.

Id prefixes: [statuses and ids](/reference/statuses-and-ids).
