• 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

ToolWhat it does
list_brandsYour team's brands. A run belongs to one brand.
list_productsProducts you can advertise, optionally filtered to one brand.
list_anglesA brand's saved angles.
list_audiencesA brand's saved audiences.
list_modelsModels a run may use, with each one's current credit cost per render and the most a render can cost.
create_runCreates 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_runStatus, 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_runsYour runs, newest first, with a cursor.
answer_runAnswers a run's questions.
approve_run_copyApproves, edits or rejects a run's copy drafts.
cancel_runCancels 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_resultsThe 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_optionsWhat 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_planApproves a video run's plans with a credit ceiling — the charge of a video run.
update_run_planChanges the words of a video plan's scenes — or an audio ad's script, headline and button text — before approval. Free.
retry_run_planPlans again a video variation that could not be planned. Free.
list_run_stillsA video run's stills, with signed image URLs that expire.
reroll_run_stillMakes one still again, at its own price.
approve_run_stillsApproves 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:

  1. get_run until pendingInput is not null or the status is terminal.
  2. Answer the gate: questions with answer_run, an answer for every open question; copy with approve_run_copy, every draft, edited or not, and the concepts you reject; plan with approve_run_plan — after update_run_plan or retry_run_plan if the plans need it — which is the charge, so pass the price you agreed as maxCredits; stills with approve_run_stills, after list_run_stills and any reroll_run_still.
  3. 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.