MCP server
- MCP endpoint
https://terrads.com/api/mcp - Updated October 16, 2026
Everything on these pages is also available as an MCP server, so an agent can drive runs with tool calls instead of HTTP requests. The tools are the API: same brands, products and runs, same credits, same errors. Nothing is reachable through one that is not reachable through the other.
Connecting
The endpoint is not under /api/v1 — it is exactly this URL, with no trailing
slash:
https://terrads.com/api/mcp
It speaks MCP over Streamable HTTP and accepts POST only. Any MCP client that
can send a custom header can connect: add
Authorization: Bearer ${TERRADS_API_KEY}
to the server's headers, with the key in the TERRADS_API_KEY environment
variable and the reference written the way your own client expands one — the
setup steps below give the exact form for Claude Code and for Cursor. A client
that does not expand environment variables takes the key's value in that spot
instead. Keys come from Settings → API keys, exactly as
for the API (authentication); the same plan checks
apply on every call, and a revoked key stops working at once. There is no
sign-in flow and no OAuth on this endpoint today, so if your client offers to
authenticate the server, skip that and check the key instead.
The server speaks the current protocol revision and the previous one, so older clients connect too.
Setting up a client
Step by step: Claude Code · Cursor.
No snippet below contains your key. The plugin asks for it when you enable it;
the two configuration snippets reference the TERRADS_API_KEY environment
variable, and each one says where that variable has to be set.
Plugin. In a Claude Code session, the plugin connects the server and installs a skill that knows the tools. Add the marketplace:
/plugin marketplace add terrads-hq/agent-mode
Then, in the same session, install the plugin:
/plugin install terrads@terrads
Enable it when asked and enter your key — Claude Code keeps it, so this path needs no environment variable.
Claude Code, without the plugin. From your project directory:
claude mcp add --transport http terrads https://terrads.com/api/mcp --header 'Authorization: Bearer ${TERRADS_API_KEY}'
Your Claude Code config stores the reference, not the value: the command above
does not read TERRADS_API_KEY at all. The key is read each time the server
connects, from the environment Claude Code itself runs in, so that is where the
variable has to be set. If the variable is unset, nothing prompts you for it: the
reference is sent as written and the server answers with the usual
authentication error. The command adds the server for this project only, the
default scope; add --scope user after claude mcp add to use it in every
project.
Cursor. Add the server to ~/.cursor/mcp.json:
{
"mcpServers": {
"terrads": {
"url": "https://terrads.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:TERRADS_API_KEY}"
}
}
}
}
Set TERRADS_API_KEY where Cursor's docs say to: in your shell profile (for
example export TERRADS_API_KEY=your-key in ~/.zshrc) or your system
environment, then quit Cursor completely and reopen it. Exporting it in an
already-open terminal isn't enough. The file
keeps only the reference; nothing in it is the key.
Other agents. The same skill installs on its own, for agents that manage their own MCP connections:
npx skills add terrads-hq/agent-mode
On its own the skill is the know-how, not a connection: it never reads a key
of its own and stops when no terrads server is connected, so connect the
server first with the header above. Installing both the plugin and the skill
gives you two copies of the skill; pick one.
The tools
| Tool | What it does |
|---|---|
list_brands | Your team's brands. A run belongs to one brand. |
list_products | Products you can advertise, optionally filtered to one brand. |
list_angles | A brand's saved angles. |
list_audiences | A brand's saved audiences. |
list_models | Models a run may use, with each one's current credit cost per render and the most a render can cost. |
create_run | Creates a run and returns at once with a runId. Spends credits; a video run, audio ads included, is free to create and charged when its plan is approved. |
get_run | Status, kind (image or video), event log, pendingInput — the gate waiting on you, if any — and webUrl, a video run's page in the app. |
list_runs | Your runs, newest first, with a cursor. |
answer_run | Answers a run's questions. |
approve_run_copy | Approves, edits or rejects a run's copy drafts. |
cancel_run | Cancels a run that has not finished — queued, analyzing, waiting for input, generating or verifying. Work not yet started is refunded; renders already in progress are not. |
get_run_results | The finished creatives as URLs, which expire: an image, a video's MP4 and its poster, or an audio ad's MP3 and its square picture. |
get_video_options | What a video brief can name: voices, music, captions, lengths, sizes, styles, what each AI motion choice makes, and the lengths an audio ad is made in. |
approve_run_plan | Approves a video run's plans with a credit ceiling — the charge of a video run. |
update_run_plan | Changes the words of a video plan's scenes — or an audio ad's script, headline and button text — before approval. Free. |
retry_run_plan | Plans again a video variation that could not be planned. Free. |
list_run_stills | A video run's stills, with signed image URLs that expire. |
reroll_run_still | Makes one still again, at its own price. |
approve_run_stills | Approves a video run's stills; its AI motion is made next. Free. |
Each tool's arguments and their bounds are published by the server itself when a client lists tools; they are the same fields the endpoint reference documents, with the same limits.
Creating a run
create_run takes the same brandId, config and optional maxCredits as
POST /runs — an image brief, or a video brief with "kind": "video" (see
video runs), or an audio ad: a video brief whose video.spot is
"audio" (see audio spots) — plus one argument the HTTP API carries as a header:
idempotencyKey. The rules are those of idempotency:
reuse the key when you retry the same call — the reply then carries
replayed: true and nothing is charged again — and use a new key for a new
run. A client that automatically re-sends the same request replays for free —
the arguments, and therefore the key, are identical. An agent that composes
the call again must reuse the key deliberately, and should reuse it
especially when a call failed or timed out without a reply: the run may
already exist. The reply echoes idempotencyKey so you always have it.
Keys are per surface. A key you already sent as an Idempotency-Key header on
POST /runs is not reusable here — reusing it answers idempotency_key_reused
rather than replaying the earlier run, because the two surfaces describe the
same brief differently. Mint a fresh key for the tool.
Because an agent that lost a reply sometimes retries with a fresh key, the
server also asks you to confirm a run that looks identical to a recent one
that is still in progress or completed (a run that failed or was cancelled
does not count — running it again is always allowed):
the call is refused with duplicate_run, and details.existingRunId names
the run it matched. If you really want a second run with the same brief,
call again with allowDuplicate: true.
{
"brandId": "…",
"config": { "productAssetId": "…", "angleId": "…", "audienceId": null, "references": [], "formatIds": ["feed"], "languageIds": ["en"], "concepts": 2, "modelSlug": "…", "copyMode": "auto", "copyTone": "", "cta": "", "creativeDirection": "", "smartAsk": false },
"idempotencyKey": "launch-2026-09-11-a"
}
The reply is runId, status, creditCost, replayed and idempotencyKey.
The interactive loop
An image run can pause for clarifying questions about your references and for
copy review; a run whose kind is video can pause for questions, always
pauses at its plan, and with AI motion at its stills — exactly as described
in the runs guide. With tools the loop is:
get_rununtilpendingInputis notnullor the status is terminal.- Answer the gate:
questionswithanswer_run, an answer for every open question;copywithapprove_run_copy, every draft, edited or not, and the concepts you reject;planwithapprove_run_plan— afterupdate_run_planorretry_run_planif the plans need it — which is the charge, so pass the price you agreed asmaxCredits;stillswithapprove_run_stills, afterlist_run_stillsand anyreroll_run_still. - Back to step 1.
get_run reports pendingInput from the run's whole history, so a client
that only asks for events after the last seq it saw still sees an open gate.
Polling stops at a gate only when you hand it to a person instead of
answering it: stop polling and send them to webUrl, a video run's page in
the app, where they can answer it. webUrl is null for an image run.
approve_run_copy on a video run is refused with unsupported_kind: a video
run has no copy review.
Results
get_run_results returns one entry per finished creative with an image URL
(for a video, its MP4 as videoUrl, the poster as the image, and any
translations made in the app in translations; for an audio ad, its MP3 as
audioUrl and a 640×640 picture as companionUrl) that expires
after expiresInSeconds. Download promptly, or call again for a
fresh set. Result entries in get_run's event log never carry a URL.
Errors
A tool that cannot do what was asked returns an error result rather than a
protocol failure. Its content is the same envelope every endpoint uses, with
the same codes and the same meanings as the errors page:
{ "error": { "code": "insufficient_credits", "message": "…" } }
Errors about the request itself — a missing or revoked key, a plan without
API access, a spent rate-limit budget — are answered on
the HTTP response before any tool runs, with the same status codes and
envelope as the API. Tool calls share the API's per-key and per-team budgets:
reads and lists spend the read tiers, create_run and the gate tools spend
the write tier.