MCP tools
The Corte MCP server gives agents the same primitives as the REST API, shaped as
tools. It speaks Streamable HTTP at https://api.corte.so/mcp and
authenticates either with browser consent (OAuth 2.1) or with a normal
API key.
For setup and the conceptual tour, see Connect agents with MCP. This page is the reference: every tool, every parameter, every return shape.
Transport
POST https://api.corte.so/mcp
Authorization: Bearer corte_at_… (or corte_sk_…)
Content-Type: application/json
JSON-RPC over a single POST. The server is stateless — every request is
self-contained, there's no session to establish or resume. GET /mcp and
DELETE /mcp return 405.
claude mcp add --transport http corte https://api.corte.so/mcp
A browser window opens for you to approve what the agent may reach. Any client that follows the MCP authorization spec connects this way; a client that only sends headers uses an API key instead.
Connect with OAuth
/mcp is an OAuth 2.1 protected resource. An unauthenticated call answers
401 with
WWW-Authenticate: Bearer resource_metadata="https://api.corte.so/.well-known/oauth-protected-resource"
which is enough for a client to find everything else on its own:
| Endpoint | What it is |
|---|---|
/.well-known/oauth-protected-resource | Names the resource and its authorization server (RFC 9728) |
/.well-known/oauth-authorization-server | Endpoints, grants and scopes (RFC 8414) |
POST /oauth/register | Dynamic client registration (RFC 7591) — public clients, token_endpoint_auth_method: none |
GET /oauth/authorize | Sign-in and the consent screen |
POST /oauth/token | Code exchange and refresh |
PKCE with S256 is required. Redirect URIs must be https, or http on
loopback (localhost, 127.0.0.1, ::1) on any port. Access tokens
(corte_at_…) last an hour; the refresh token rotates on every use and lasts
30 days.
A client that names no scope is granted read-write across every area except
your account, which stays read-only. account:rw is never issued to a connected
app — an app cannot mint API keys, change your plan, or disconnect its
neighbours.
What's different from REST
Nearly every tool wraps a REST primitive, with three deliberate differences:
Tools wait by default. generate, run_application, and run_workflow
block on completion up to a waitSeconds budget, then return whatever state
they reached. Agents work better with one call than with a polling loop they
have to invent. Pass waitSeconds: 0 to return immediately.
Results are JSON text. Every tool returns a single text content block containing pretty-printed JSON. Parse it as JSON.
Most errors are plain messages. Tool failures surface as MCP errors carrying
the REST error message — Workflow not found,
Unknown model, and so on. The structured { error: { code } } envelope isn't
preserved. The exception is anything the account itself blocked: an empty
balance, an unconfirmed address, a paid-only model, full storage. Those come back
as a readable result — see When the account blocks the
call.
:::warning Agents spend real credits MCP tools run as your account. Every generation or workflow run an agent starts costs exactly what clicking Generate would. Give agents their own key so you can revoke them independently, and watch Settings → Usage. :::
Account
get_credits
Current credit balance. Cheap; an agent should call it before expensive work.
Input — none.
Returns
{
"monthlyBudgetCredits": 3500,
"purchasedCredits": 500,
"spentCreditsThisPeriod": 1288,
"remainingCredits": 2712
}
list_projects
The projects the account can work in — its own and the shared ones it is a
member of. This is how an agent turns "in my Playground project" into the
projectId every other tool accepts; without one, work lands in and bills to
defaultProjectId. Needs account:read.
No parameters.
Returns { projects, defaultProjectId }. Each project carries id,
name, description, role (owner or contributor), owner
({ email, name }), remainingCredits (what the project can spend — a shared
project spends its owner's credits) and updatedAt.
Credits
An agent that runs out of credits mid-task can hand you the way to fix it. Both tools return a payment link; you open it in your browser and pay there. The card never goes near the agent, and neither tool moves money on its own — until somebody pays the link, nothing has happened.
Both need account:read. Creating a link neither reads your account nor changes
it, so it doesn't cost the account:rw grant that also mints API keys.
create_topup_checkout
A payment link for a one-off top-up. 100 credits per dollar; purchased credits never expire.
| Parameter | Type | Required | Description |
|---|---|---|---|
dollars | integer | yes | Whole dollars, 5–1,000. Buys dollars × 100 credits. |
Returns
{
"url": "https://checkout.stripe.com/c/pay/cs_live_…",
"dollars": 25,
"credits": 2500,
"firstTopupBonusCredits": 250,
"expiresAt": "2026-08-31T14:00:00.000Z"
}
firstTopupBonusCredits is the one-time bonus this account still has coming on
its first purchase, or 0 once it's been collected. expiresAt is when the link
stops working, or null where the provider doesn't say.
Credits land within about a minute of payment. Poll get_credits
rather than assuming, and don't retry the work that failed until the balance has
actually moved.
create_subscription_checkout
A payment link for a plan. Yearly bills once at 20% off; credits still arrive monthly.
| Parameter | Type | Required | Description |
|---|---|---|---|
tier | "standard" | "pro" | "max" | yes | Which plan to buy. |
interval | "month" | "year" | no | Defaults to month. |
Returns { "url": string, "tier": string, "interval": string }.
Asking for the plan the account is already on is refused, the same way
POST /v1/billing/checkout refuses it.
When the account blocks the call
Eight failures aren't bugs or bad arguments — the call was fine and the account
is what stopped it. Any tool can return one, and it comes back as an isError
result whose text is JSON:
{
"error": "insufficient_credits",
"message": "Insufficient credits",
"balance": 12,
"estimatedCredits": 40,
"nextStep": "Ask the user to add credits: call create_topup_checkout({dollars}) and give them the link — they pay in their browser and the credits land within a minute. Then poll get_credits and retry. Do not retry before the balance changes."
}
| Field | When |
|---|---|
error | Always — the error code. |
message | Always — the same sentence REST would return. |
balance | insufficient_credits and plan_required: the caller's own remainingCredits. Omitted where the payer is somebody else. |
estimatedCredits | Where the refused work knew its cost — a generation's estimate, a transcription's price. |
nextStep | Always — one sentence naming what to do, written for the agent. |
The eight codes and what each one actually needs:
| Code | What clears it |
|---|---|
insufficient_credits | A top-up or a plan. |
owner_insufficient_credits | The project owner adding credits — this work is billed to them, so buying on your own account won't unblock it. |
plan_required | Any purchase. Pro models unlock on a first top-up or a plan, not on a particular tier. |
email_unverified | Clicking the link in the confirmation email. Nothing to buy. |
owner_email_unverified | The project owner confirming theirs. |
account_suspended · owner_account_suspended | Contacting support. |
storage_limit | Deleting files, or a plan with more storage. Storage is a hard quota with no overage billing. |
nextStep adapts to what the calling key can actually do: a key without
account:read is pointed at the app rather than at a checkout tool it can't
call, and a workspace with billing turned off is never told to buy anything.
Models & generation
list_models
The generation catalog with capabilities and pricing.
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | "image" | "video" | "audio" | "upscale" | no | Filter by model kind. |
Returns { "models": [...], "total": number } — the same
model objects as GET /v1/models,
always the full catalog.
generate
Submit a generation on any model, and wait for it.
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | yes | Model id from list_models. |
params | object | yes | Must match the model's kind. See Generation parameters. |
projectId | string | no | Bill to a shared project — the owner pays. |
waitSeconds | number | no | Default 60, max 300. 0 returns immediately. |
Returns { "job": GenerationJob, "estimatedCredits": number }.
If the job is still running when the wait budget expires, the job is returned in
its current state — poll with get_generation. The wait timing out is not an
error.
{
"model": "nano-banana-2",
"params": {
"kind": "image",
"prompt": "Editorial flat lay of a trail-running shoe on slate, cold morning light",
"aspectRatio": "4:5",
"numImages": 2
},
"waitSeconds": 120
}
{
"job": {
"id": "a91c2d7e-5f80-4c39-b6a2-13e7d0f4c885",
"model": "nano-banana-2",
"kind": "image",
"status": "succeeded",
"resultUrls": ["https://media.corte.so/…?X-Amz-Signature=…"],
"resultStorageIds": ["c74a1e08-9b6f-4d13-a205-e8f30b7c9d61"],
"costCredits": 24,
"completedAt": "2026-07-28T21:02:55.410Z"
},
"estimatedCredits": 24
}
get_generation
Fetch a generation job by id — the polling tool.
| Parameter | Type | Required | Description |
|---|---|---|---|
jobId | string | yes | Job id. Must belong to the key's account. |
Returns { "job": GenerationJob }. A finished job carries results[] in
place of raw result URLs: each result has its storageId, a downloadUrl
on Corte's own host that opens for 24 hours without signing in, and a
libraryUrl that is permanent and opens the asset in Corte for the signed-in
person — the link to show them. For a permanent public URL, publish_asset.
Assets
list_assets
A project's media — images, video and audio the account uploaded, imported or
generated. Reading assets needs only media:read; importing new ones is
media:rw.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
kind | "image" | "video" | "audio" | no | Filter on the content type's major part. |
limit · offset | number | no | Paging. Default limit 50. |
Returns { objects, total }. Each object carries storageId, filename,
contentType, sizeBytes, createdAt, origin, a downloadUrl that opens
for 24 hours and a permanent libraryUrl.
total counts the filtered set, so paging within a kind is safe.
Requires membership of the project — the same check the media picker makes.
get_asset
One stored file by id: its metadata plus a downloadUrl that opens for 24
hours without signing in, a thumbDownloadUrl for images and video, and a
permanent libraryUrl that opens it in Corte.
| Parameter | Type | Required | Description |
|---|---|---|---|
storageId | string | yes | From list_assets, upload_asset, or a generation result. |
Use it to check what an id actually refers to before placing it in a content document or handing it to a model.
:::note Unknown, not forbidden A file the account neither owns nor shares through a project reads as unknown. That is deliberate: a "forbidden" answer would confirm the id exists, turning the tool into a way to probe for other people's files. :::
upload_asset
Import media from an https URL into storage. Returns the storageId every
other tool accepts as an input.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | yes | https URL of an image, video, or audio file. |
filename | string | no | Overrides the name derived from the URL. |
Returns
{
"storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8",
"url": "https://media.corte.so/…?X-Amz-Signature=…",
"sizeBytes": 482113
}
There is no MCP tool for uploading local bytes — agents work from URLs. For local files use the REST ticket flow.
Applications
list_applications
The curated Applications catalog with each entry's input requirements.
Input — none.
Returns { "applications": [...] } — the same key as
GET /v1/applications, with
the objects trimmed to what an agent needs to choose and run one:
{
"applications": [
{
"id": "flatlay-to-model",
"name": "Flatlay to Model",
"kind": "image",
"description": "Upload a flatlay of clothing or accessories and get a styled photo of a model wearing the items…",
"inputKind": "image",
"minImages": 1,
"maxImages": 1,
"acceptsText": true,
"presets": [
{ "id": "studio-white", "name": "Studio White" },
{ "id": "street-style", "name": "Street Style" }
]
}
],
"total": 43
}
Preset prompts are omitted — only id and name. Use
GET /v1/applications if you
need the full objects.
run_application
Run an Application and wait for the generation.
| Parameter | Type | Required | Description |
|---|---|---|---|
applicationId | string | yes | Id from list_applications. |
imageStorageIds | string[] | cond. | For image-input Applications. |
videoStorageId | string | cond. | For video-input Applications. |
audioStorageId | string | cond. | For audio-input Applications (voice cleanup). |
durationSeconds | number | cond. | Input media length — required for duration-billed Applications (video upscale, voice cleanup). |
text | string | no | Extra guidance. Only when acceptsText is true. |
presetId | string | no | A preset id from the Application. |
projectId | string | no | Bill to a shared project — the owner pays. |
waitSeconds | number | no | Default 60, max 300. 0 returns immediately. |
Returns { "job": GenerationJob, "estimatedCredits": number }.
Validation is identical to
POST /v1/applications/:id/run
— the two share one implementation.
Content
Tools over the Content management API — enough for an agent to model a CMS, fill it, publish it, and hand the app a read token. Every tool calls the same code the REST routes do, so permissions, validation and versioning behave identically.
projectId is optional throughout; omitted, it means the account's default
project.
list_content_types
The project's schema registry, each type with its fields and documentCount.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
limit · offset | number | no | Paging. |
get_content_type
| Parameter | Type | Required | Description |
|---|---|---|---|
contentTypeId | string | yes | Type id. |
version | number | no | A historical schema version. |
create_content_type
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
name | string | yes | camelCase machine name. Immutable afterwards. |
title | string | yes | What editors see. |
description | string | no | Free text. |
fields | object[] | yes | Field definitions — see field types. |
titleField | string | no | Field that names documents in lists. |
Reference fields need to: [typeName], and those types must already exist —
create the target type first.
update_content_type
| Parameter | Type | Required | Description |
|---|---|---|---|
contentTypeId | string | yes | Type id. |
baseVersion | number | yes | Must equal the type's latestVersion. |
title · description · fields · titleField | no | Omitted keys keep their stored value. |
Changing fields mints a new schema version and goes live immediately. A stale
baseVersion errors — refetch with get_content_type and retry.
delete_content_type
| Parameter | Type | Required | Description |
|---|---|---|---|
contentTypeId | string | yes | Type id. |
force | boolean | no | Also delete every document of this type. |
Refused outright while another type embeds this one — fix those fields first.
Refused while documents use it unless force, which deletes those documents
too, along with every schema version. Not reversible.
list_content_documents
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
type | string | no | Content type machine name. |
status | "draft" | "published" | "changed" | no | Filter by state. |
q | string | no | Searches the type's title field (every locale of a localized one). Requires type. |
missingLocale | string | no | Only documents with an empty localized field in this locale. Requires type. |
limit · offset | number | no | Paging. |
Rows of a type with localized fields carry locales: per-locale coverage
(complete, partial, outdated or empty, with the field paths behind it).
get_content_document
Returns the draft and published payloads, the schema, and valid/issues —
whether the draft would publish. The CRDT binaries the browser editor uses are
stripped; an agent has no local document to apply them to.
create_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
type | string | yes | Content type machine name. |
draft | object | no | Field values keyed by field name. |
Media is { "storageId": "…" }, references are { "documentId": "…" }. The
document starts unpublished.
update_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
documentId | string | yes | Document id. |
draft | object | yes | The fields you are changing. Others keep their values. |
A partial draft is merged over the document's current draft, so changing one
field leaves the rest alone. Pass a field as null to clear it. The same rule
holds one level down for a localized field: { "title": { "de": "…" } } sets
the German and keeps the other locales.
No version to pass: the draft merges with anyone editing live. The response
carries valid and issues; a draft saves even when invalid, and publish is
what refuses.
publish_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
documentId | string | yes | Document id. |
baseVersion | number | yes | The draftVersion from get_content_document. |
A mismatched baseVersion means someone edited the document since you looked.
Publishing an invalid draft is refused.
unpublish_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
documentId | string | yes | Document id. |
force | boolean | no | Unpublish despite inbound published references. |
Removes the live copy; the draft and the revision history stay. Anything reading published content stops seeing it immediately.
delete_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
documentId | string | yes | Document id. |
force | boolean | no | Delete despite inbound published references. |
Deletes the draft, the published copy and the whole revision history. Not
reversible — prefer unpublish_content_document when the intent is only to take
something off a live site. Forcing past the reference guard leaves those
references dangling.
list_content_revisions
Draft saves plus publish, unpublish and restore steps, newest first.
get_content_settings
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
The project's locales (code, title, fallback, required), its default
locale, and the translation settings (tone-of-voice brief, auto-translate on publish).
Read it before adding a locale or translating.
update_content_settings
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
defaultLocale | string | no | Must be one of locales. |
locales | object[] | no | Replaces the whole list — read it first and resend it with the change. |
translation | object | no | { brief?, autoTranslateOnPublish? }; omitted keys keep their value. |
Refused when a code repeats, the default is not in the list, or a fallback points at itself, at an unknown locale, or around a loop.
translate_content_document
| Parameter | Type | Required | Description |
|---|---|---|---|
documentId | string | yes | Document id. |
locales | string[] | no | Target codes. Default: every locale but the source. |
fields | string[] | no | Localized field paths. Default: all of them. |
sourceLocale | string | no | Default: the project's default locale. |
mode | "missing" | "outdated" | "all" | no | Default outdated. |
Machine-translates into the draft — nothing is published. missing fills
empty values; outdated also redoes machine translations whose source text
changed; all rewrites hand-written translations too, so confirm with the user
first. Media fields are skipped. Costs credits, billed to the project owner;
the response reports translated and skipped paths per locale and the
credits spent. See Locales and translation.
create_content_delivery_token
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the default project. |
name | string | yes | What the token is for. |
mode | "published" | "preview" | no | Default published. |
Requires project ownership.
:::caution The secret is returned once
create_content_delivery_token returns a secret that cannot be recovered.
Write it straight into the application's configuration — a
CORTE_CONTENT_TOKEN environment variable, say — before doing anything else,
and don't echo it back into the conversation. Use published mode for anything
a browser can read; preview reads unpublished drafts and belongs on a server.
:::
Workflows
list_workflows
List workflows in a project, most recently updated first — the account's
default project unless projectId is given. Scoping matches
GET /v1/workflows: every member's
workflows in a shared project, not just the agent's.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | A shared project to list; omit for the default project. |
Returns
{
"workflows": [
{ "id": "7c93b1de-…", "name": "Product Reshoot Pipeline", "nodeCount": 6, "updatedAt": "2026-07-27T09:41:22.006Z", "projectId": "1b6d4f90-…" }
],
"total": 12
}
get_workflow
Inspect a workflow and discover which node ids accept run inputs. This is
the tool to call before run_workflow.
| Parameter | Type | Required | Description |
|---|---|---|---|
workflowId | string | yes | Workflow id. Same access rule as REST — project members included. |
Returns
{
"workflow": { "id": "7c93b1de-…", "name": "Product Reshoot Pipeline", "nodeCount": 6 },
"nodes": [
{ "id": "productPhoto", "type": "imageInput" },
{ "id": "concept", "type": "prompt" },
{ "id": "reshoot", "type": "generateImage" },
{ "id": "final", "type": "output" }
],
"inputNodes": [
{ "nodeId": "productPhoto", "type": "imageInput", "takes": "storageId", "current": "b0e7c9a1-…" },
{ "nodeId": "concept", "type": "prompt", "takes": "text", "current": "Alpine trail-running capsule, cold dawn light" }
]
}
inputNodes is the useful part: takes says which key to send ("text" or
"storageId"), and current shows the saved value that will be used if you
omit the node.
run_workflow
Execute a workflow server-side with optional per-node overrides, and wait.
| Parameter | Type | Required | Description |
|---|---|---|---|
workflowId | string | yes | Workflow id. |
inputs | object | no | Map of node id → { text } or { storageId }. Omitted nodes keep saved values. |
projectId | string | no | Billing context for a personal workflow run inside a shared project. |
waitSeconds | number | no | Default 120, max 600. 0 returns immediately. |
Returns { "run": Run } with nodeResults populated as far as the run got.
{
"workflowId": "7c93b1de-2a48-4f60-91b7-5d0e6a2c8f13",
"inputs": {
"concept": { "text": "Alpine trail-running capsule, cold dawn light" },
"productPhoto": { "storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8" }
},
"waitSeconds": 300
}
Input validation matches the REST endpoint exactly — see
POST /v1/workflows/:id/execute.
get_workflow_run
Poll a run for status and per-node results.
| Parameter | Type | Required | Description |
|---|---|---|---|
workflowId | string | yes | The workflow the run belongs to. |
runId | string | yes | Run id. Visibility matches REST: all members see a project workflow's runs. |
Returns { "run": Run } — status plus
node results with freshly signed
media URLs. nodeResults grows between polls while the run is running.
Build
Apps Corte builds, hosts and deploys. Reading them needs build:read; asking an
app's coding agent for a change, or deploying one, needs build:rw.
These are the tools that let an agent maintain a website on its own — read what the site is doing, ask for a change, publish it.
list_build_apps
The Build apps the account can work in — its own, and those in projects it has been invited to.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit · offset | number | no | Paging. Default limit 25. |
Returns { items, total, limit, offset }. Each item carries the id the
other Build tools take, plus name, githubRepo, defaultBranch,
deployTarget and access — manage for apps the account owns or created
(it may also change the address, secrets and database, or delete the app) and
edit for apps shared with it as a contributor (chat, build and deploy).
get_build_app
One app's current state, in a single call.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | From list_build_apps. |
Returns { app, session, recentCheckpoints, checkpointTotal, recentDeploys, deployTotal }.
sessionis the app's live or parked builder thread, ornull.- A checkpoint is a commit the app's coding agent made. Newest first; its
shais whattrigger_deploytakes. - A deploy carries
statusand, once live, the publicurl.
The two lists are capped at 10 — a summary to act on, not a paging surface.
create_build_app
Create an app and hand its builder the brief — the "New app" step in one call. The brief is the first message on the app's thread, exactly as if the owner had typed it.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | The app's name — becomes its public subdomain (<slug>.corte.sh), so name the product, not the request. |
prompt | string | yes | The brief the builder starts from. Answer a skill's interview inside it (see get_skill) and the builder starts without asking. |
projectId | string | no | Own the app in a shared project; defaults to the account's default project. |
Returns { app, sessionId, messageId, publicUrl }. Starts a sandbox and
spends the owner's credits. Returns as soon as the brief is queued — follow with
get_build_thread.
update_build_app
Rename an app and/or move its public address.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | The app. |
name | string | no | New display name. |
slug | string | no | New public subdomain (<slug>.corte.sh). Lowercase letters, digits, hyphens. |
Returns { app, publicUrl }. The old address stops serving once the new
one is live.
get_build_thread
What the builder has said and done — the reply side of send_build_message
and create_build_app.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | The app. |
sessionId | string | no | A specific thread; defaults to the newest. |
since | number | no | Only events after this id — pass back the previous call's lastEventId. |
waitSeconds | number | no | Block up to this long (max 300) for a reply, a question, a checkpoint or the thread ending. Default 0. |
Returns { session, lastEventId, replies, pendingQuestion, checkpoints, errors, settled }.
session.status is queued | provisioning | running | awaiting_input | completed | failed | cancelled.
A pendingQuestion is answered with send_build_message.
list_skills · get_skill
The platform skills a builder can run — packaged procedures for a kind of site
or section (scroll-world: a scroll-scrubbed camera flight through generated
scenes). list_skills takes no parameters and returns { skills: [{ name, description }] }; get_skill takes name and returns { name, description, interview, body }, where interview is the list of questions a brief should
answer up front. Mention the skill by name in the brief.
send_build_message
Ask an app's coding agent to change the app, in plain language, exactly as its owner would.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | The app to change. |
text | string | yes | What you want changed. |
Returns { appId, sessionId, messageId, startedSession }.
:::caution Accepted is not done
This returns as soon as the request is queued, not when the change exists. The
builder works asynchronously in its own sandbox. Poll get_build_app — a new
checkpoint means it committed something — and remember that a committed change
is not live until it is deployed.
:::
If the app has no live thread, one is opened, which starts a sandbox and spends
the owner's credits. startedSession tells you which happened.
trigger_deploy
Publish an app to its live URL.
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | string | yes | The app to deploy. |
checkpointSha | string | no | Defaults to the newest checkpoint. |
Returns the deploy, which starts queued. Poll get_build_app for how it
ended and the URL it went to.
This makes changes live for real visitors.
:::note Unknown, not forbidden An app the account does not own reads as unknown, for the same reason assets do: a "forbidden" answer would confirm the id exists. :::
Agents
Agents are standing automations: a prompt that runs on
a schedule, on a content event, or on demand, with its own scopes. Reading them
needs agents:read; creating, editing or running one needs agents:rw.
These tools exist so an assistant can author an automation for someone, not just perform a task. Agents are owner-only — only a project's owner sees or manages its agents — and a project holds at most 20.
:::warning An agent is a credential Creating one hands out standing, unattended access to the account, exercised on a schedule until the agent is disabled or deleted. Two rules bound it, both enforced on this path exactly as on REST:
- An agent can never exceed its creator. A key scoped to
content:rwcannot create an agent holdingbuild:rw. The request is refused, not trimmed. - The
accountarea is refused outright, at any level and to anyone. It reaches API key creation, so a run could mint itself a credential that outlives the agent — and revocability is the whole point of a per-run credential. :::
list_agents
The agents this account has created.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | One project's agents. Omit for every agent you created. |
limit · offset | number | no | Paging. Default limit 50. |
Returns { agents, total, limit, offset }.
get_agent
One agent in full — its prompt, scopes, schedule, events, and lastRunAt /
nextRunAt.
| Parameter | Type | Required | Description |
|---|---|---|---|
agentId | string | yes | From list_agents. |
Returns { agent }. Read this before update_agent: an update replaces the
fields it is given, so you need to know what is already there.
list_agent_recipes
The curated agent recipes — ready-made automations, each with a prompt and suggested scopes, schedule, time zone and model. Takes no parameters.
Returns { recipes }. Read it before create_agent: when one matches what
the user described, create the agent with that recipe's id as recipeId so it
tracks improvements to the recipe automatically.
A suggestion is not a grant — suggestedScopes are still bounded by what the
caller holds, so a recipe can never widen what you can hand out.
create_agent
Create an agent.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Short label the owner will recognise. |
prompt | string | yes | The standing instruction, run unattended. Max 20,000 characters. |
scopes | string[] | yes | What it may do — "area:read" or "area:rw". See below. |
projectId | string | no | Defaults to the account's default project. |
description | string | no | One line on what it's for. |
scheduleCron | string | no | 5-field cron. Omit for a manual-only agent. |
scheduleTz | string | no | IANA zone name for the cron. Defaults to UTC. |
events | string[] | no | Content events that trigger a run. |
modelId | string | no | Defaults to Auto. |
recipeId | string | no | Track a recipe from list_agent_recipes. |
enabled | boolean | no | Default true. |
Returns { agent }, including the id the other agent tools take.
Scopes are the same vocabulary as API keys:
content, media, integrations, generation, workflows, cuts,
projects, plugins, community, build and agents, each :read or :rw.
rw covers reading too. account is not grantable to an agent.
scheduleCron is standard 5-field cron — minute, hour, day-of-month, month,
day-of-week. 0 9 * * 1-5 is 09:00 every weekday. scheduleTz must be an
IANA zone name (America/New_York, Europe/Lisbon) — an abbreviation like
EST or an offset is refused, because only a zone name follows daylight saving.
Omit both and the agent is manual-only: it fires only via run_agent or an
event.
events accepts exactly document.publish, document.unpublish and
document.delete — a run per matching change in the agent's project. Draft saves
never trigger anything.
update_agent
Edit an agent. Only the fields you send change.
| Parameter | Type | Required | Description |
|---|---|---|---|
agentId | string | yes | The agent to edit. |
everything from create_agent | no | Except projectId, which cannot change. |
Returns { agent }.
Two fields replace rather than merge: scopes and events. Send the full
list you want the agent to end up with, or it loses what you left out — and the
replacement is bounded by what you hold, exactly as at creation.
scheduleCron: nullremoves the schedule and leaves the agent manual-only.enabled: falsepauses it without deleting it. Re-enabling resumes from the next slot; missed slots are not made up.
run_agent
Start a run now, without waiting for the schedule.
| Parameter | Type | Required | Description |
|---|---|---|---|
agentId | string | yes | The agent to run. |
idempotencyKey | string | no | Repeat the same key to retry safely. |
Returns { run } with a status of queued, launching or running.
:::caution Queued is not finished
This returns as soon as the run is queued. The agent works asynchronously and
spends the owner's credits, so a successful call is not the work being done.
get_agent shows lastRunAt once a run lands; the full run feed —
GET /v1/agents/:id/runs, and in the app — is REST-only.
:::
A run is refused with a reason when the account is already at its concurrent-run limit. A disabled agent still runs when asked directly.
Integrations
The outside services a project can act through — a Shopify store, an ad
account, a Notion workspace. A connection belongs to a project: every
member can use it, and it never follows the person who connected it into
their other projects. Reading needs integrations:read.
list_integrations
What this project is connected to, and what it could still connect.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | no | Defaults to the project this credential acts in, else the default project. |
Returns { connected, available }. connected lists live connections —
provider id, label, and a status of connected, expiring or expired
(never credentials); the provider-specific tools like shopify_list_assets
only work for these, and an expiring one needs reconnecting before its tools
are reliable. available lists the providers the platform supports that the
project has not connected, with connectable saying whether this server can
connect them. When a request needs data no connected service provides, look
here before calling it impossible — the right answer is usually to suggest
connecting the provider that covers it.
Connections are made in the app, in Settings → Integrations, and only by a person — there is no tool that connects a service.
The canonical agent flow
Ask an agent for "run my Product Reshoot workflow on this photo" and a well-behaved run looks like:
upload_asset { url }→ a committedstorageId.get_workflow { workflowId }→ readinputNodesto learn the node ids and what each one takes.run_workflow { workflowId, inputs: { productPhoto: { storageId }, concept: { text } } }→ waits up to 120s by default.get_workflow_runif step 3 returned still-running→ outputs land inrun.nodeResults[*].output.value, withstorageIdalongside for chaining.
For one-shot generation, the flow collapses to list_models → generate, or
list_applications → run_application.
Tool index
| Tool | Waits? | REST equivalent |
|---|---|---|
get_credits | — | GET /v1/account |
create_topup_checkout | — | POST /v1/billing/topup |
create_subscription_checkout | — | POST /v1/billing/checkout |
list_models | — | GET /v1/models |
generate | 60s / max 300 | POST /v1/generations |
get_generation | — | GET /v1/generations/:id |
list_assets | — | GET /v1/projects/:id/storage |
get_asset | — | GET /v1/storage/:storageId |
upload_asset | — | POST /v1/uploads/import |
list_applications | — | GET /v1/applications |
run_application | 60s / max 300 | POST /v1/applications/:id/run |
list_workflows | — | GET /v1/workflows |
get_workflow | — | GET /v1/workflows/:id |
run_workflow | 120s / max 600 | POST /v1/workflows/:id/execute |
get_workflow_run | — | GET /v1/workflows/:id/runs/:runId |
list_content_types | — | GET /v1/projects/:id/content/types |
get_content_type | — | GET /v1/content/types/:id |
create_content_type | — | POST /v1/projects/:id/content/types |
update_content_type | — | PUT /v1/content/types/:id |
delete_content_type | — | DELETE /v1/content/types/:id |
list_content_documents | — | GET /v1/projects/:id/content/documents |
get_content_document | — | GET /v1/content/documents/:id |
create_content_document | — | POST /v1/projects/:id/content/documents |
update_content_document | — | PUT /v1/content/documents/:id |
publish_content_document | — | POST /v1/content/documents/:id/publish |
unpublish_content_document | — | POST /v1/content/documents/:id/unpublish |
delete_content_document | — | DELETE /v1/content/documents/:id |
list_content_revisions | — | GET /v1/content/documents/:id/revisions |
create_content_delivery_token | — | POST /v1/projects/:id/content/tokens |
list_build_apps | — | none — Build's REST is not public API |
get_build_app | — | none — Build's REST is not public API |
send_build_message | — | none — Build's REST is not public API |
trigger_deploy | — | none — Build's REST is not public API |
create_build_app | — | none — Build's REST is not public API |
get_build_thread | — | none — Build's REST is not public API |
update_build_app | — | none — Build's REST is not public API |
list_skills · get_skill | — | none — platform metadata |
list_integrations | — | GET /v1/integrations + /catalog |
list_agents | — | GET /v1/agents |
get_agent | — | GET /v1/agents/:id |
list_agent_recipes | — | GET /v1/agent-recipes |
create_agent | — | POST /v1/agents |
update_agent | — | PATCH /v1/agents/:id |
run_agent | — | POST /v1/agents/:id/run |
The four Build tools are the only ones with no REST counterpart on
api.corte.so: Build runs in its own cluster, and MCP is how it is reached from
outside the app.
Not exposed as tools
Transcriptions, project and member management, file deletion, storage listing, API key management, and the Content delivery, webhook and GraphQL endpoints have no MCP tools — they're REST-only. An agent that needs them should call the REST API directly with the same key.