MCP server
https://terrads.com/api/mcpUpdated September 12, 2026Everything 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.
The server speaks the current protocol revision and the previous one, so older clients connect too.
Setting up a client
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.
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 in the environment Cursor starts from — a desktop app
does not inherit a terminal's exports. 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. |
create_run | Creates a run and returns at once with a runId. Spends credits. |
get_run | Status, event log and pendingInput — the gate waiting on you, if any. |
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. |
get_run_results | The finished creatives as image URLs, which expire. |
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, 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
A run can pause twice — for clarifying questions about your references, and for copy review — exactly as described in the runs guide. With tools the loop is:
get_rununtilpendingInputis notnullor the status is terminal.- If
pendingInput.gateisquestions, callanswer_runwith an answer for every open question. If it iscopy, callapprove_run_copywith every draft, edited or not, and the concepts you reject. - 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.
Results
get_run_results returns one entry per finished creative with an image URL
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.