# Tools

Framehood exposes a small, functional toolset over [MCP](/guide/mcp). You think
in outcomes; the server picks the model. Every generation tool requires an `out`
filename.

::: tip Discover at runtime
Use `models(list)` to browse reachable models and `models(guide, model=…)` for a
model's parameters and prompting tips (or `GET /v1/models/{kind}` for the raw
input schema).
:::

## What the MCP server exposes

The core generation and account tools:

`image` · `video` · `audio` · `qa` · `files` · `billing` · `org` · `get_status`

Two helpers — browse the catalog and send feedback:

`models` · `submit_feedback`

Three more manage your workspace:

`library` · `project` · `api_keys`

::: warning Actors temporarily disabled
The `actor` tool and the actor-dependent actions (`image(actor_sheet)`,
`video(scene)`, and `actor_id` routing on `image` / `video`) are **temporarily
disabled** while we rework them. The rest of the toolset is unaffected.
:::

The [CLI](/guide/cli) and the [REST API](/reference/api) wrap these same tools
and add a few surface-only endpoints (raw model access, uploads, and the Stripe
card/portal billing flows) — see [CLI / REST surface](#cli-rest-surface) below.

::: tip ChatGPT clients see live status text
`image`, `video`, `audio`, and `qa` — the tools that submit a generation job —
carry status strings (e.g. "Generating image…" while running, "Image job
submitted — poll get_status" once the call settles) that ChatGPT's MCP client
shows while the call is in flight. These jobs are usually still processing
when the call settles, so the status text reflects submission, not a finished
asset. Other clients are unaffected; this is purely a ChatGPT UI nicety.
:::

## Result shape

Every MCP tool result carries a small, predictable envelope on top of its own
fields: `ok` (boolean), `result_kind` (`"job"` for an async generation still in
flight or its terminal outcome, `"result"` for an immediate data/management
reply, or `"error"`), plus `tool` and `action` identifying the call. Nothing
about a tool's existing response changes shape or gets removed — the envelope
is purely additive, so code written against the old response keeps working
unchanged.

Job-shaped results (`image`, `video`, `audio`, `qa`, and `get_status` for a
**single** job — the default `status` action, or `cancel`) also carry
`artifacts[]` — a normalized `[{url, type?}, ...]` list of the job's output
files — alongside the original `outputs` map, which is unchanged. Use whichever
is more convenient: `outputs.image_url` etc. for the legacy shape, or
`artifacts[]` to iterate output files generically regardless of media type.

`get_status`'s batch (`job_ids`) and `list` actions are `result_kind: "result"`,
not `"job"` — the envelope describes the call as a whole (summary-only, no
top-level `artifacts[]`); the individual entries inside `jobs[]` keep their
plain `{job_id, status, ...}` shape (and their own `outputs`, if terminal).

Three optional steering fields may also appear (each omitted entirely when
there is nothing to say):

- `note` (string) — advice about this specific result, e.g. "prompt was
  truncated" or "URL expires in 24h".
- `next_step` (object) — the recommended next call, machine-readable:
  `{tool, action?, args?, why}`. `args` carries concrete values (e.g.
  `{"job_id": "job_abc123"}`) you can pass verbatim. Non-terminal job results
  carry a `get_status` poll here; some errors carry a recovery call on top of
  the prose `hints`. **Breaking (server 2.12.0):** `next_step` used to be a
  prose string on submit/poll replies — it is now always this object on MCP.
- `notifications[]` (`{type, text, id?, url?}`, max 3) — global account-level
  notices, independent of the tool called. Currently always absent; the
  channel ships ahead of its producers.

## MCP tools

::: tip What you can pass as a media reference
Every media input — `image_url`, `video_url`, `audio`, `reference_images`,
`clips`, `tracks`, `vo`, `music`, and the like across `image` / `video` /
`audio` / `qa` — accepts any of:

- **A cdn URL** returned by an earlier job or by `files(upload / import_remote)`
  — **public or private**. A private `cdn.framehood.ai/…` URL works as a
  reference **as-is — you do not need to `publish` it first.** (`publish` only
  mints a *public*, shareable/downloadable URL; it is not required to reuse a
  file in another job.)
- **A library `asset_id`** — the id of any item in your library (from
  `library(list)`, a generated result, or `files(import_to_library)`). Pass it
  straight in as the `image_url` / `video_url` / etc. and it resolves to the
  asset automatically — no URL lookup, no publish. Library items are shared
  across your organization, so a teammate's `asset_id` works too; an id from a
  different organization (or a deleted item) returns a clear "not found" error.
- **A data-URI** (`data:<mime>;base64,…`, up to 25 MB) — a local file inlined
  into the call; it is uploaded for you and the resulting URL is used. Best for
  **small** files (e.g. a downscaled reference image): some clients truncate a
  large inline payload in transit — for larger or original-quality files use
  `files(create_upload)` + PUT instead.

The `asset_id` and data-URI shortcuts are resolved by the `image` / `video` /
`audio` / `qa` tools. If you call the REST job API directly
(`POST /v1/jobs/{kind}`), pass a media **URL** (an asset's `url`) rather than a
bare `asset_id` or data-URI.

Calling from ChatGPT? A chat attachment does **not** populate a tool's media
field (these tools don't declare `openai/fileParams`) — pass a **URL** or
`asset_id` instead, bringing a file in first via `files(import_remote)` /
`files(upload)`. See [ChatGPT / OpenAI MCP compatibility](/guide/chatgpt).
:::

### image

Create and edit still images (and image→video).

| Action | Key arguments | Does |
|--------|---------------|------|
| `create` | `prompt`, `out`, `tier?`, `format?` | text → image |
| `edit` | `image_url` **or** `reference_images[]`, `prompt`, `out` | modify an image. Pass up to 10 references as `reference_images` and cite them in the prompt as `@Image1`…`@ImageN` — for identity work a headshot plus a full-body shot holds a face far better than one image. `image_url` is the single-image shorthand |
| `upscale` | `image_url`, `out` | higher resolution |
| `animate` | `image_url`, `out`, `duration?` | image → short video |
| `actor_sheet` | `actor_id`, `out_prefix`, `variations?` | multi-angle character sheet |

`tier`: `draft` < `fine` < `photo`. `actor_id` (`act_…`) routes `create`/`animate`
through an actor's LoRA. Some models are reachable only by name — e.g. Nano Banana
Pro: `image(create, model="nano_banana", …)` (it is not a tier); see the
[`models`](#models) tool.

`seed?` (any action): an optional integer for reproducible output — reuse the
same seed (with the same prompt/params) to get consistent results. Ignored, with
no error, by models that don't support seeding.

### video

Generate, edit, and compose video.

| Action | Key arguments | Does |
|--------|---------------|------|
| `create` | `prompt`, `out`, `reference_images?`, `duration?`, `format?` | text → video (no actor needed) |
| `edit` | `video_url`, `prompt`, `out`, `duration?` | restyle / edit footage |
| `edit_ref` | `video_url`, `prompt`, `reference_images`, `out` | edit guided by reference photos (`@Image1…`) |
| `swap` | `video_url`, `image_url`, `swap_mode`, `out` | replace a person, object, or background |
| `lipsync` | `video_url`, `audio_url`, `out` | match lips to an audio track |
| `captions` | `video_url`, `out` | burn in auto subtitles |
| `upscale` | `video_url`, `out` | higher resolution |
| `assemble` | `clips`, `out`, audio bed (`music?`, `vo?`, `vo_offset_sec?`, `vo_cues?`, `sfx_ambient?` …) | combine clips + transitions and mix an audio bed into one video |
| `mix_audio` | `video_url`, `tracks`, `out` | overlay VO / music / SFX onto a video |
| `blur` | `video_url`, `out`, `region?`, `sigma?`, `windows?` | blur/redact a region (or the whole frame), optionally only during time windows |
| `watermark` | `video_url`, `image_url`, `out`, `position?`, `x?`, `y?`, `margin?`, `opacity?`, `scale?`, `window?` | overlay a logo/watermark PNG at a corner (or explicit `x`/`y`), for the whole clip or a time window |
| `scene` | `actor_id`, `scene_prompt`, `out` | actor full-scene composite (image → motion → optional speech) |

`swap_mode`: `person` · `object` · `background`. In `edit_ref`, the images you
pass in `reference_images` are cited in the prompt as `@Image1…@Image4`. `scene`
routes through an actor (`act_…`) and can auto-script a spoken line from the
actor's personality.

For MORE than one narration line, `assemble` accepts `vo_cues`
(`[{audio_url, start_sec, level?}, …]`) — each line is placed at its own timecode
in one call, INSTEAD of `vo`/`vo_offset_sec` (passing both is an error).
`mix_audio` `tracks` accepts the same cue shape: a plain URL, or a
`{audio_url, start_sec?, level?, label?}` object placed at a timecode. Levels are
static per cue; pass `duck: true` to auto-duck the music bed under the voice —
`assemble` keys it off the clip audio + VO, and `mix_audio` off tracks labeled
`"music"` and `"vo"`.

`seed?` (any action): an optional integer for reproducible output — reuse the
same seed (with the same prompt/params) to get consistent results. Ignored, with
no error, by models that don't support seeding.

### audio

| Action | Key arguments | Does |
|--------|---------------|------|
| `speak` | `text`, `out`, `voice?` | text → voice |
| `sfx` | `prompt`, `out` | sound effects |
| `music` | `prompt`, `out`, `lyrics?` | music / ambient |
| `mix` | `tracks` (2+; or 1+ with `music?`/`music_level?`), `out` | blend audio files — flat mix, or a music bed ducked under the voice |
| `trim` | `audio`, `start_sec?`, `duration_sec?`, `out` | cut an audio file to a window |
| `concat` | `tracks`, `out` | join audio in sequence |

To overlay audio onto a video, use `video(mix_audio)`.

### qa

Inspect media for problems. Returns a pass/fail report.

| Action | Key arguments | Does |
|--------|---------------|------|
| `full` | `video`, `plan?` | run nine checks at once |
| `person` | `image1`, `image2` | same person in two images? |
| `voice` | `audio` | consistent voice? |
| `scene` | `video`, `plan` | does the video match the scene plan? |
| `transcript` | `video\|audio`, `expected_text?`, `language?` | transcribe media (video OR audio) with timecoded segments; if `expected_text` is given, also check the spoken words match |
| `image` | `image_url`, `description` | does the image match the description? |
| `describe` | `video`, `fps?`, `focus?` | timecoded scene-by-scene description (what happens, speech, sounds, music); returns a `job_id` — poll `get_status` |

Omit `expected_text` on `transcript` for transcription-only mode (no compare, verdict `PASS`); the result carries `actual_transcript`, `duration_sec`, and timecoded `segments` either way.

### files

Manage your storage.

| Action | Key arguments | Does |
|--------|---------------|------|
| `list` | `prefix?`, `cursor?` | list your stored files |
| `upload` | `filename` + `url` \| `data` | bring a file into your storage — from a hosted URL, or a LOCAL file's raw bytes via `data` (data-URI / base64, ≤25 MB) |
| `create_upload` | `filename`, `content_type` | get a token + `upload_url` to PUT a LARGE / original-quality local file directly (no 25 MB cap) |
| `import_remote` | `download_url` (or `url`) | pull a remote file (e.g. a file an assistant/chat app handed you as a URL) into your storage in one call — no manual download/re-upload. Returns a Framehood `url` you can pass straight into `image`/`video`/`audio`/`qa`. |
| `delete` | `filename` | remove a file |
| `publish` | `filename` | make a file public, returns a `public_url` |
| `unpublish` | `filename` | make a published file private again |
| `download` | `filename` | get a usable URL to fetch the bytes |
| `import_to_library` | `filename` (the uploaded key), `name?`, `project_id?` | mint a library asset (`asset_id`) from one of your uploaded files, optionally assigning it to a project — so uploads can be organized/assigned like generated assets |

::: tip You don't need to publish to reuse a file
`publish` is only for sharing a file **publicly** — it returns a `public_url`
anyone can open or download. To use a file as a reference in another job
(`image_url`, `video_url`, …) you do **not** publish it: pass its private cdn
URL, or its library `asset_id`, directly (see *What you can pass as a media
reference* above).
:::

::: tip Bringing in a file from another app
If another app or assistant hands you a file only as a URL (a chat
attachment, a generated image it made elsewhere), don't pass that URL
straight into `image`/`video`/`audio`/`qa` — import it first:

1. `files(import_remote, download_url="https://…")` → get back `{ url:
   "https://cdn.framehood.ai/…", content_type, size }`.
2. Use that returned `url` as `image_url` / `video_url` / etc. in any
   generation or `qa` call.

Only `https://` sources are accepted, and only image/video/audio files —
anything else (a PDF, an SVG, an `http://` link) is rejected with a clear
error. Files are capped at 200 MB. The URL you passed in is never stored or
returned — only the resulting Framehood file.
:::

::: tip Organizing an uploaded file into a project
Generated results are added to your library automatically, but a file you
brought in yourself (`upload` / `create_upload` / `import_remote`) has no
library entry, so it can't be assigned to a project. `import_to_library`
mints one:

1. `files(import_to_library, filename="<the uploaded key>", project_id?="<id>")`
   → get back `{ asset_id, url, type, size, project_id }`.
2. Use `asset_id` with `project(assign)` (or pass `project_id` above to assign
   in the same call).

It references your existing file — nothing is copied, and it's free. Only
image/video/audio files can be imported. It resolves your OWN private uploads
only — not a published (`public/`) file, and not a teammate's file. Re-importing
the same file creates a new library asset each time.
:::

### actor

::: warning Temporarily disabled
Actors are being reworked — the `actor` tool is not currently advertised over
MCP. The reference below is kept for when it returns.
:::

Create and manage persistent actors. An actor carries visual identity, voice,
personality, wardrobe, and motion across image / video / audio generations —
pass its `actor_id` (`act_…`) to those tools to reuse the same actor. Training
a custom actor is a paid feature; the free tier can use built-in actors.

| Action | Key arguments | Does |
|--------|---------------|------|
| `create` | `name`, `images_data_url` | train a new actor from a ZIP of reference photos; optional voice, personality, wardrobe, motion, brand |
| `update` | `actor_id` | change voice, personality, wardrobe, motion, or brand |
| `list` | `prefix?` | list your actors, optionally filtered by name prefix |
| `get` | `actor_id` | fetch one actor's full profile |
| `delete` | `actor_id` | remove an actor |
| `batch` | `actor_id`, `prompts`, `kind?`, `duration?`, `tier?` | generate many images or videos for one actor in a single call |

`images_data_url` is a public ZIP URL with 4–30 reference photos. Attach a
voice via `voice_id` (existing) or `voice_sample_url` (a clean 30–60s sample).
Voice cloning is additionally gated behind its own flag, independent of the
actor rework above — it stays disabled (pending pricing) even once actors
return; linking an existing voice via `voice_id` is unaffected.

### billing

`billing(action)` — credits, plan, and upgrades for your organization. Read
actions are open to any member; the rest are owner-only.

| Action | Key arguments | Does |
|--------|---------------|------|
| `balance` | — | shared credit balance + your role |
| `transactions` | `limit?` | recent credit ledger (newest first) |
| `plan` | — | your tier, monthly allowance, manage link (owner) |
| `plans` | — | available packages |
| `subscribe` | `step` | owner: Stripe Checkout link for a package |
| `preview` | `step` | owner: prorated cost of switching |
| `change` | `step` | owner: switch package, prorated, charged now |
| `cancel` | `reactivate?` | owner: cancel at period end, or resume |
| `manage` | — | owner: Stripe Customer Portal link |
| `topup` | `amount_eur` | owner: buy a one-off batch of extra credits now (min €20, at the extra-usage rate); returns a hosted invoice link |
| `extra_usage` | — | owner: view the auto-top-up (Extra usage) config |
| `set_extra_usage` | `enabled?`, `trigger_below?`, `amount_eur?`, `extra_usage_cap_eur?` | owner: configure automatic overflow top-ups |
| `request_upgrade` | `note?` | member: email the owner to top up |

See [Credits & plans](/guide/billing).

### org

`org(action)` — your organization. Members share a credit pool and actors.

| Action | Key arguments | Does |
|--------|---------------|------|
| `info` | — | your org + your role |
| `members` | — | list members (role + suspended state) |
| `spend` | — | per-member credit spend |
| `trend` | `days?` | daily org spend (7–90 days, default 30) |
| `invite` | `email` | owner: invite by email |
| `accept_invite` | `token` | join an org with an invite token |
| `remove` | `email` | owner: remove a member |
| `set_role` | `email`, `role` | owner: `member`↔`admin` |
| `suspend` / `enable` | `email` | owner or admin: block / unblock a member |

A suspended member loses org access and can't draw on the shared credit pool.
Plan and payment live in the `billing` tool; out of credits, members use
`billing(request_upgrade)`.

### get_status

`get_status(action)` — poll or manage jobs.

| Action | Key arguments | Does |
|--------|---------------|------|
| `status` | `job_id` | poll one job → `queued` · `running` · `succeeded` (with `outputs`) · `failed` (with `error`) |
| `status` (batch) | `job_ids` | check up to 50 jobs in ONE call → `{summary, jobs[]}` |
| `list` | `kind?`, `status?`, `since?`, `limit?`, `cursor?` | the generation-history feed (`since` = RFC3339 or `"1h"` / `"30m"` shorthand) |
| `cancel` | `job_id` | cancel a non-terminal job — a queued job cancels immediately, no charge. A running job's cancel is a best-effort request: it errors and the job keeps running if the model doesn't support cancellation at all; otherwise the request is accepted but the job may still complete and be charged if the model doesn't honor a mid-run stop — poll `status` for the outcome |

`status` is the default action.

**Submitting several jobs?** Fire all the submits in parallel, collect the
returned job ids, then poll ONCE with `job_ids=[…]` every ~30–60s until
`summary.running + summary.queued` is 0 — don't poll jobs individually. Each
batch entry carries `status`, `done`, and (when terminal) `outputs`/`credits`
or `error`; an id you don't own (or that doesn't exist) comes back as
`status: "not_found"` without failing the batch.

### models

`models(action)` — browse the model catalog. Every tool already picks a sensible
default model, so reach for this only when you want a specific one.

| Action | Key arguments | Does |
|--------|---------------|------|
| `list` | `tool?` | reachable models grouped by tool (optionally filtered to `image` / `video` / `audio` / `qa`) |
| `guide` | `model` | one model's parameters + prompting tips |

To use a specific model, pass `model` (and optional `params`) to the generation
tool — e.g. `image(create, model="flux_pro", params={…})`.

### submit_feedback

`submit_feedback` — send a note to the Framehood team: a bug, a feature request,
or general feedback.

| Argument | Does |
|----------|------|
| `message` | the feedback text |
| `category?` | `bug` · `feature` · `other` |
| `context?` | what you were doing / which tool or model |
| `job_id?` | the job this feedback is about (e.g. `job_abc123`) — links the ticket to that generation |

## Workspace tools (also over MCP)

These three are advertised over MCP alongside the generation and account tools
and let you organize and authenticate your work.

### library

`library(action)` — search and manage your generated assets:

- `list` (`query`, `type`, `project`, `limit`, `offset`) — search past generations
  by prompt/name, filter by media type or project, paginate.
- `trashed` — list the trash.
- `trash` (`id`) / `restore` (`id`) — soft-delete or recover an asset. The trash is
  auto-purged after 10 days. You can trash your own assets; org owners/admins any.

### project

`project(action)` — group generations into projects:

- `list` — your personal projects + the org's shared ones.
- `create` (`name`, `visibility` `personal`|`shared`, `description`).
- `update` (`id`) — rename, change visibility / description, or owner.
- `delete` (`id`) — owner only; the assets stay in the library.
- `assign` (`asset_id`, `id`) — put an asset in a project; omit `id` to unassign.
- `use` (`id`) — set your active/default project; new generations land there
  automatically. Omit `id` to clear.
- `current` — show your active project.

### api_keys

`api_keys(action)` — programmatic keys for the REST API and CLI:

- `list` — your keys (metadata only).
- `create` — mint a key; the secret is returned **once**.
- `delete` (`key`) — revoke a key.

## CLI / REST surface

The [CLI](/guide/cli) and the [REST API](/reference/api) drive the same tools
listed above — the CLI maps its commands onto them, and the REST API exposes the
same operations as plain HTTP endpoints. They additionally surface a few things
the MCP tools intentionally don't:

- **Raw job submission** — `POST /v1/jobs/{kind}` to run a specific model kind
  directly. (Over MCP you don't submit to a kind — pass `model` + `params` to
  `image` / `video` / `audio` / `qa`, and browse the catalog with the `models`
  tool.)
- **Uploads** — `PUT /upload` and `POST /upload-from-url` to bring your own input
  files into storage.
- **Browser billing flows** — the Stripe card-entry and customer-portal endpoints
  (`POST /billing/card`, `/billing/portal`, …), which finish in a browser.

See the [REST API reference](/reference/api) for the full endpoint list. Over
MCP your client handles auth, so you rarely need to touch these directly.
