# Totalum App Builder API — AI Agent

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# AI Agent

> Prompt the agent that builds and changes your app, and follow it live.

The AI agent writes and changes the project's code from natural-language prompts. A run is **asynchronous**: [Run AI Agent](#run-ai-agent) answers at once with `status: "init"`, then you poll [Get Agent Status](#get-agent-status) every 10–15 seconds until `status` is `"done"`. Every finished run that changed files is saved as a new [version](https://www.totalum.app/docs/api/versions) and deployed to the project's **development preview** — never to the published site, which only changes when you [publish](https://www.totalum.app/docs/api/deployments). The agent checks its work in its own browser and reads the app's [logs](https://www.totalum.app/docs/api/server) (development and live); when a user says "it doesn't work", it reads the error logs first.

## Run AI Agent

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/start
cost: Metered, at least 3 credits
```

Send one prompt to the agent (or a multi-prompt batch). One run at a time per project. Once the running prompt's change is in the preview and the run is only finishing, the next prompt is accepted and waits: it starts as soon as that run ends. One prompt can wait; [Stop Agent](#stop-agent) cancels the waiting one first.

A prompt is priced by the work the agent does: at least 3 credits, usually 3 to 50. A question that changes nothing is answered in seconds for 3 credits and creates no version. A run that fails without producing anything is refunded. The agent works up to about 40 minutes on one prompt, then wraps up and saves what it built; most prompts take a few minutes.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `prompt` | string | Yes¹ | The instruction, up to 40,000 characters |
| `tier` | string | No | `"auto"` (default), `"cheap"`, `"normal"` or `"expensive"` — see [Agent tiers](#agent-tiers). `model` and `modelTier` are accepted as aliases |
| `inputFiles` | object[] | No | Up to 10 files for this prompt. Say in the prompt what each one is for ("use this as the logo") |
| `inputFiles[].name` | string | Yes | File name |
| `inputFiles[].imageDescription` | string | No | What the file shows |
| `inputFiles[].url` | string | Yes | Public URL, or the `url` returned by [Upload File](https://www.totalum.app/docs/api/source-files#upload-file) |
| `multiPrompt` | object | No | Run a batch of prompts one after another, unattended. Omit for normal use |
| `multiPrompt.prompts` | string[] | No² | 1–50 prompts, run in order |
| `multiPrompt.letTotalumDecide` | boolean | No² | `true` = Totalum plans the prompts from `prompt` (the goal) |

¹ Not needed when `multiPrompt.prompts` is given.
² Exactly one of `prompts` or `letTotalumDecide: true`.

`fastMode` and `effort` are accepted for compatibility and ignored (an ignored option is listed in `data.warnings`; the run still starts).

### Agent tiers

| Tier | Use it for |
|---|---|
| `"auto"` | **The default.** Totalum picks the tier for each prompt from the prompt, the recent conversation and the project: `cheap` for small edits and questions, `normal` for builds and features, `expensive` only when a fix keeps failing or you ask for the best |
| `"cheap"` | The lowest price for real work: landing pages, redesigns, small apps, edits |
| `"normal"` | Full apps and features, with more thinking |
| `"expensive"` | The most capable agent, for complex apps and hard bugs. Uses the most credits |

With `"auto"`, the run's final message carries `autoMode: true` and `modelUsed`: the tier that ran (`"cheap"`, `"normal"` or `"expensive"`), charged as that tier.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | Project id |
| `data.status` | string | Always `"init"` |
| `data.message` | string | How to follow the run |
| `data.expectedMinutes` | number \| undefined | Estimated minutes — an estimate, never a deadline |
| `data.expectedFinishAt` | string \| undefined | Start + `expectedMinutes` (ISO 8601) |
| `data.warnings` | string[] \| undefined | Options that were ignored |
| `data.runId` | string \| undefined | The run's id (single prompt) |
| `data.multiPrompt.totalPrompts` | number \| undefined | Batches only: number of prompts (absent while Totalum plans them) |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Add a contact form that emails me every message"}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/start
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "barber-bookings",
    "status": "init",
    "message": "Process started. Estimated time: about 6 minutes. This is only an approximation… Fetch GET /api/v1/vcaas/projects/barber-bookings/agent/status every 10-15 seconds to track progress; the status there is the only reliable signal that the run has finished.",
    "expectedMinutes": 6,
    "expectedFinishAt": "2026-10-08T11:16:00.000Z",
    "runId": "run_01k6xc4n7d"
  }
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROMPT` | 400 | `prompt` is missing |
| `INVALID_MULTI_PROMPT` | 400 | `multiPrompt` needs exactly one of `prompts` or `letTotalumDecide: true` |
| `TOO_MANY_PROMPTS` | 400 | More than 50 prompts |
| `INVALID_PROMPT_ITEM` | 400 | A prompt in `prompts` is empty |
| `VALIDATION_ERROR` | 400 | The prompt is over 40,000 characters, or a field has the wrong type |
| `INSUFFICIENT_CREDITS` | 402 | Your balance is below 1 credit |
| `PROJECT_CREDIT_LIMIT_REACHED` | 403 | The project's monthly development cap is reached — raise it with [Update Credit Limits](https://www.totalum.app/docs/api/limits#update-credit-limits) |
| `AGENT_RUNNING` | 409 | A prompt is already running and its change is not in the preview yet, or a prompt is already waiting (a batch answers `400 AGENT_ALREADY_RUNNING`) |
| `PROJECT_PAUSED_BY_USER` | 409 | The project is paused — [resume](https://www.totalum.app/docs/api/projects#resume-project) it first |
| `OPERATION_IN_PROGRESS` | 409 | A restore, import or other long operation holds the project |
| `PROJECT_NOT_FOUND` | 404 | No such project in your account |
| `RATE_LIMITED` | 429 | Your account sent more prompts this minute than its plan allows, or has the most prompts running at once (all projects together, see [Limits](https://www.totalum.app/docs/limits)) — retry after `Retry-After` seconds |

:::info A run that runs out of credits
If your balance reaches 0 during a run, the run stops: what it built is saved as a version, it ends with `INSUFFICIENT_CREDITS`, and you are charged only what it used. A run that crosses the project's development cap stops the same way with `PROJECT_CREDIT_LIMIT_REACHED`.
:::

## Get Agent Status

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/status
cost: Free
```

Whether a prompt is running, and the messages of the current (or last) run. Poll every 10–15 seconds. When `status` becomes `"done"`, read the preview URL again from [Get Project](https://www.totalum.app/docs/api/projects#get-project).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | Project id |
| `data.status` | string | `"init"` (running), `"done"` (the last run finished) or `"idle"` (never prompted) |
| `data.startedAt` | string \| null | Start of the current or last run |
| `data.runId` | string \| undefined | The current or last run |
| `data.realtimeConversation` | object[] | The run's messages, oldest first (the user's prompt is in [Get Full Conversation](#get-full-conversation)) |
| `data.realtimeConversation[].author` | string | `"user"` or `"agent"` |
| `data.realtimeConversation[].message` | string | Text |
| `data.realtimeConversation[].messageType` | string | `"regular"`, `"starting"`, `"building"` (a status note), `"finished"` (the run's summary), `"error"`, `"limit-reached"` or `"individual"` (a one-off notice) |
| `data.realtimeConversation[].createdAt` | string | ISO 8601 |
| `data.realtimeConversation[].versionId` | string \| undefined | The version this message created |
| `data.realtimeConversation[].secretKeysNeeded` | object \| undefined | Secrets the app needs: `{ NAME: { isProvided, description } }`. Add them with [Create Secret](https://www.totalum.app/docs/api/secrets#create-secret) |
| `data.realtimeConversation[].files` | object[] \| undefined | Files attached to the message |
| `data.realtimeConversation[].autoMode` | boolean \| undefined | `true` on the final message of an `auto` run |
| `data.realtimeConversation[].modelUsed` | string \| undefined | The tier an `auto` run used |
| `data.creditsSpent` | number \| undefined | What the last run cost, once `"done"` |
| `data.expectedMinutes` | number \| null | Estimate of the running prompt |
| `data.expectedFinishAt` | string \| null | Start + estimate |
| `data.multiPrompt` | object \| null | The batch, when one exists: `{ status, totalPrompts, currentPromptIndex, prompts[{ order, prompt, status }], startedAt, updatedAt }`. `status`: `"executing"`, `"paused"`, `"done"`, `"cancelled"` |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/status
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "barber-bookings",
    "status": "done",
    "startedAt": "2026-10-08T11:10:00.000Z",
    "realtimeConversation": [
      {
        "author": "agent",
        "message": "Added a contact form on /contact. Every message is saved and e-mailed to you. I sent a test message: saved, 200.",
        "messageType": "finished",
        "versionId": "ver_01k6xd2m8q",
        "createdAt": "2026-10-08T11:15:40.000Z",
        "autoMode": true,
        "modelUsed": "normal"
      }
    ],
    "creditsSpent": 7.4,
    "expectedMinutes": null,
    "expectedFinishAt": null,
    "multiPrompt": null,
    "runId": "run_01k6xc4n7d"
  }
}
```
:::

:::info Right after "done"
For a few seconds after a run ends, the preview may answer `503` with the header `x-totalum-preview: updating` while the new version goes live. Retry after the `retry-after` header; the preview never serves the previous version in that window.
:::

## Get Agent Progress

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/progress
cost: Free
```

A step-by-step view of the current run (or the last one): its stage, the files it changed, the builds and checks it ran. Use it to show a live "building" timeline. `data` is `null` before the first run.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.runId` | string | The run |
| `data.state` | string | `"queued"`, `"provisioning"`, `"running"`, `"finishing"`, `"done"`, `"failed"`, `"cancelled"` or `"incomplete"` |
| `data.stage` | string | Where the run is: `"queued"`, `"sandbox"`, `"starting"`, `"thinking"`, `"working"`, `"build"`, `"deploy"`, `"fixing"`, `"wrapping-up"`, `"entry-page"`, `"done"`, `"failed"`, `"cancelled"`, `"incomplete"` |
| `data.startedAt` | string | Start time |
| `data.finishedAt` | string \| undefined | End time |
| `data.turns` | number | Agent turns so far |
| `data.steps` | object[] | Up to the 40 latest steps: `{ at, kind, file?, task?, ok?, added?, removed? }`. `kind`: `read`, `search`, `create`, `edit`, `delete`, `command`, `web`, `browser`, `integration`, `helper`, `plan`, `waiting`; `task` (commands): `build`, `test`, `install`, `database`, `check`, `save`, `other` |
| `data.totalSteps` | number | All steps of the run |
| `data.files` | object[] | Files changed: `{ path, change: "create" \| "edit" \| "delete", added, removed }` |
| `data.counts` | object | `{ reads, searches, edits, commands, failedCommands }` |
| `data.build` | object \| undefined | `{ ok }` of the last build |
| `data.tests` | object \| undefined | `{ runs, failed, failedFlows? }`: the checks the agent ran on the pages it changed |
| `data.versionId` | string \| undefined | The version the run saved |
| `data.failure` | string \| undefined | Why it failed: `INSUFFICIENT_CREDITS`, `PROJECT_CREDIT_LIMIT_REACHED`, `DEPLOY_FAILED`, `RUN_TIMED_OUT`, `RUN_INTERRUPTED`, `CONTENT_BLOCKED`, … or `OTHER` |
| `data.creditsSpent` | number \| undefined | What the run cost, once it ended |
| `data.previewAt` | string \| undefined | When the preview last started serving this run's new build (reload your preview frame when it changes) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/progress
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "runId": "run_01k6xc4n7d",
    "state": "running",
    "stage": "working",
    "startedAt": "2026-10-08T11:10:00.000Z",
    "turns": 12,
    "steps": [
      { "at": "2026-10-08T11:12:01.000Z", "kind": "edit", "file": "src/app/contact/page.tsx", "added": 64, "removed": 0 },
      { "at": "2026-10-08T11:12:30.000Z", "kind": "command", "task": "database", "ok": true }
    ],
    "totalSteps": 18,
    "files": [{ "path": "src/app/contact/page.tsx", "change": "create", "added": 64, "removed": 0 }],
    "counts": { "reads": 6, "searches": 2, "edits": 4, "commands": 3, "failedCommands": 0 }
  }
}
```
:::

## Get Full Conversation

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/full-conversation
cost: Free
```

The project's whole chat: every prompt and every agent message, oldest first (the latest 1,000).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | Project id |
| `data.conversation` | object[] | Messages with the fields of `realtimeConversation[]` in [Get Agent Status](#get-agent-status) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/full-conversation
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "barber-bookings",
    "conversation": [
      { "author": "user", "message": "Add a contact form that emails me every message", "messageType": "regular", "createdAt": "2026-10-08T11:10:00.000Z" },
      { "author": "agent", "message": "Added a contact form on /contact…", "messageType": "finished", "versionId": "ver_01k6xd2m8q", "createdAt": "2026-10-08T11:15:40.000Z" }
    ]
  }
}
```
:::

## Stop Agent

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/stop
cost: Free
```

Stop the running prompt (and the rest of a multi-prompt batch). What the run built so far is kept; you pay only for what it used.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | `"Agent process cancelled successfully"` |
| `data.paused` | boolean | `false` |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/stop
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "message": "Agent process cancelled successfully", "paused": false } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `NO_PROCESS_RUNNING` | 400 | No prompt is running |

## Resume Multi-Prompt Batch

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/multi-prompt/resume
cost: The prompts it runs
```

When a prompt of a batch fails, the batch pauses there (`multiPrompt.status: "paused"`) because the next prompts build on it. This runs the failed prompt again, then the rest. [Stop Agent](#stop-agent) cancels the batch instead.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | Project id |
| `data.status` | string | `"init"` |
| `data.message` | string | Which prompt the batch continues from |
| `data.multiPrompt.totalPrompts` | number | Prompts in the batch |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/multi-prompt/resume
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "projectId": "barber-bookings", "status": "init", "message": "Multi-prompt run continued from prompt 2.", "multiPrompt": { "totalPrompts": 3 } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `INVALID_AUTO_STATUS` | 400 | No batch is paused |
| `AGENT_RUNNING` | 400 | A prompt is running |

## Clear Agent Context

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/clear-context
cost: Free
```

The next prompt starts with a fresh agent memory: it works from the project's current code and its change history, plus the messages sent after the clear. Restate anything from before that still matters.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | `"Agent context cleared"` |
| `data.contextClearPending` | boolean | `true` until the next prompt starts |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/clear-context
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "message": "Agent context cleared", "contextClearPending": true } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `AGENT_RUNNING` | 400 | A prompt is running |

## Get System Prompt

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/system-prompt
cost: Free
```

The project's own instructions for the agent, added to every prompt (house style, rules, context).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.systemPrompt` | string \| null | The instructions, or `null` |
| `data.updatedAt` | string \| null | When they were last set |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/system-prompt
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "systemPrompt": "Write every page in Spanish. Use our brand blue #1d4ed8.", "updatedAt": "2026-10-08T09:00:00.000Z" } }
```
:::

## Set System Prompt

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/agent/system-prompt
cost: Free
```

Set or clear the project's instructions for the agent.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `systemPrompt` | string \| null | Yes | Up to 20,000 characters; `null` or `""` clears it |
| `systemPromptBase64` | string | No | The same text as base64 (UTF-8), instead of `systemPrompt` |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.systemPrompt` | string \| null | The saved instructions |
| `data.updatedAt` | string | When they were saved |

**Example request**

```bash
curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"systemPrompt":"Write every page in Spanish. Use our brand blue #1d4ed8."}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/system-prompt
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "systemPrompt": "Write every page in Spanish. Use our brand blue #1d4ed8.", "updatedAt": "2026-10-08T09:00:00.000Z" } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | Not text, or over 20,000 characters |

## List MCP Servers

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/mcp-servers
cost: Free
```

The MCP servers you added for the agent to use while it builds (header values are never returned).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.servers` | object[] | The servers |
| `data.servers[].id` | string | Server id |
| `data.servers[].name` | string | Name |
| `data.servers[].transport` | string | `"http"` or `"sse"` |
| `data.servers[].url` | string | Server URL |
| `data.servers[].headerNames` | string[] | Names of the headers sent (values are write-only) |
| `data.servers[].enabled` | boolean | Whether the agent uses it |
| `data.servers[].verifiedAt` | string | When it last answered the handshake |
| `data.servers[].tools` | string[] | Its tools |
| `data.servers[].createdAt` | string | When it was added |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/mcp-servers
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "servers": [{ "id": "mcp_01k6xe", "name": "crm", "transport": "http", "url": "https://mcp.example.com/mcp", "headerNames": ["Authorization"], "enabled": true, "verifiedAt": "2026-10-08T09:00:00.000Z", "tools": ["find_customer"], "createdAt": "2026-10-08T09:00:00.000Z" }] } }
```
:::

## Verify MCP Server

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/mcp-servers/verify
cost: Free
```

Check that a server answers the MCP handshake and lists at least one tool, without saving it.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `mcp` | object | Yes | The server |
| `mcp.name` | string | Yes | 2–41 characters: lowercase letters, digits, `_`, `-`. Not `totalum`, `playwright`, `figma`, `github` or `context7` |
| `mcp.transport` | string | Yes | `"http"` (streamable HTTP) or `"sse"` |
| `mcp.url` | string | Yes | A public `https` URL |
| `mcp.headers` | object | No | Up to 20 headers, e.g. `{ "Authorization": "Bearer …" }` |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.ok` | boolean | `true` |
| `data.serverInfo` | object | `{ name, version }` |
| `data.protocolVersion` | string | MCP protocol version |
| `data.tools` | object[] | `{ name, description? }` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"mcp":{"name":"crm","transport":"http","url":"https://mcp.example.com/mcp","headers":{"Authorization":"Bearer abc"}}}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/mcp-servers/verify
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "ok": true, "serverInfo": { "name": "crm", "version": "1.0.0" }, "protocolVersion": "2025-06-18", "tools": [{ "name": "find_customer" }] } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MCP_VERIFY_FAILED` | 400 | The handshake failed. `errorDetails`: `{ stage: "connect" \| "initialize" \| "tools_list" \| "auth", message, authHint? }` |
| `VALIDATION_ERROR` | 400 | A field is invalid or the name is reserved |

## Add MCP Server

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/mcp-servers
cost: Free
```

Verify and add an MCP server for the agent (at most 10 per project). Same body as [Verify MCP Server](#verify-mcp-server).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `mcp` | object | Yes | `{ name, transport, url, headers? }` as in Verify MCP Server |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.server` | object | The stored server, with the fields of `servers[]` in [List MCP Servers](#list-mcp-servers) |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"mcp":{"name":"crm","transport":"http","url":"https://mcp.example.com/mcp"}}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/mcp-servers
```

:::success Success · 201 Created
```json
{ "errors": null, "data": { "server": { "id": "mcp_01k6xe", "name": "crm", "transport": "http", "url": "https://mcp.example.com/mcp", "headerNames": [], "enabled": true, "verifiedAt": "2026-10-08T09:00:00.000Z", "tools": ["find_customer"], "createdAt": "2026-10-08T09:00:00.000Z" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MCP_LIMIT_REACHED` | 400 | The project already has 10 servers |
| `MCP_VERIFY_FAILED` | 400 | The server did not pass the handshake |
| `VALIDATION_ERROR` | 400 | A field is invalid or the name is taken or reserved |

## Update MCP Server

```endpoint
method: PATCH
path: /api/v1/vcaas/projects/:projectId/agent/mcp-servers/:mcpId
cost: Free
```

Turn a server on or off, or change it (a changed configuration is verified again).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |
| `mcpId` | string | The server id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | No | Use it or not |
| `mcp` | object | No | A new `{ name, transport, url, headers? }` |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.server` | object | The server after the change |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/mcp-servers/mcp_01k6xe
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "server": { "id": "mcp_01k6xe", "name": "crm", "transport": "http", "url": "https://mcp.example.com/mcp", "headerNames": [], "enabled": false, "verifiedAt": "2026-10-08T09:00:00.000Z", "tools": ["find_customer"], "createdAt": "2026-10-08T09:00:00.000Z" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MCP_SERVER_NOT_FOUND` | 404 | No such server |
| `MCP_VERIFY_FAILED` | 400 | The new configuration did not pass the handshake |

## Delete MCP Server

```endpoint
method: DELETE
path: /api/v1/vcaas/projects/:projectId/agent/mcp-servers/:mcpId
cost: Free
```

Remove an MCP server.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |
| `mcpId` | string | The server id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.deleted` | boolean | `true` |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/mcp-servers/mcp_01k6xe
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "deleted": true } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MCP_SERVER_NOT_FOUND` | 404 | No such server |
