---
title: Build an app over MCP
description: The loop an MCP client follows to start a thread, wait for the agent, answer its questions, read the code, and publish the app.
---

Tools that start work return right away. The agent inside AINOX App keeps working in the background, and your client checks on it with `get_thread`. Every thread is in one of three states:

| `status` | Meaning | What to do next |
| --- | --- | --- |
| `running` | The agent is still replying. | Wait, then call `get_thread` again, or cancel with `stop_run`. |
| `waiting` | The agent paused on a tool call that needs your answer. | Read `pendingToolCalls` and answer with `resolve_tool_call`. |
| `idle` | The reply is finished. | Read it with `list_messages`, then `send_message` or `publish`. |

## The loop

1. **Start a thread**

    Call `create_thread` with what to build, in plain language:

    ```json
    { "prompt": "A one-page tip calculator with a dark theme" }
    ```

    It returns the new `threadId`, and a `url` that opens the thread in the web app. You can watch the agent work there too.

2. **Poll until the agent stops**

    Call `get_thread` every few seconds until `status` is no longer `running`. Building a first version often takes a minute or more.

    Keep the interval at two seconds or longer. Each key allows 120 requests a minute, shared by everything using it.

    A reply ends after at most 40 tool steps. When it runs short, the agent stops adding features, makes sure the app runs, and ends with a summary of what is done and what is left. Send a follow-up with `send_message` to carry on.

3. **Answer any questions**

    When `status` is `waiting`, `pendingToolCalls` lists what the agent is waiting on. Answer each call with `resolve_tool_call`, which resumes the run, then go back to polling. See [Answering tool calls](#answering-tool-calls).

4. **Read the reply and the code**

    Once `status` is `idle`, `list_messages` returns the conversation, with each message's text and the names of the tools the agent called. Use `list_files` and `read_file` to read the project itself.

5. **Ask for changes**

    Call `send_message` with a follow-up prompt and poll again. It refuses while the thread is `running` or `waiting`, so finish or stop the current turn first.

6. **Publish**

    Call `publish`. The first publish needs a `name`, which becomes the thread's title and, lowercased and hyphenated, the app's subdomain:

    ```json
    { "threadId": "…", "name": "tip-calc" }
    ```

    It returns the `deploymentId` and the public `url`, but the build runs afterwards. Poll `get_thread` until `deployment.status` is `completed`. Publish again after later changes to update the same deployment; `name` is ignored from then on, so the subdomain can't change.

## What the agent builds

Every thread starts from the same starter project, so a new thread already has code before the agent writes any: Vite, React 19, TypeScript, and Tailwind CSS v4, with every shadcn/ui component under `src/components/ui/` and `lucide-react` for icons. The agent switches to another stack only when you ask for one.

When an app must keep data across reloads or share it between visitors, the agent gives it a Postgres database, with the schema as SQL migrations in `db/migrations/`. There is no login: every visitor is the same anonymous user, so don't ask for an app that stores secrets or personal data. A published app gets its own empty database, separate from the preview's, and applies new migration files on each publish.

## Answering tool calls

Each entry in `pendingToolCalls` has a `runId` and `toolCallId` to send back, a `toolName`, a `kind`, and an `input`.

### Questions (`kind: "answers"`)

The agent asks questions with its `ask_user` tool, often before it starts a new app or a major feature, so a new thread often stops at `waiting` before the agent writes any code. `input.questions` lists them, each with an `id`, a `type`, and the `options` to choose from. Answer with `action: "answer"` and an `answers` object keyed by question ID:

| Question `type` | Answer value |
| --- | --- |
| `single_select` | One option's `label`, as a string |
| `multi_select` | An array of option labels |
| `text` | The text of the answer |
| A question with `required: false` | `null` to skip it |

```json
{
  "threadId": "…",
  "runId": "…",
  "toolCallId": "…",
  "action": "answer",
  "answers": { "palette": "Dark", "features": ["Split the bill", "Round up"] }
}
```

Labels must match an offered option exactly.

### Approvals (`kind: "approval"`)

None of the agent's tools ask for approval today, since they run inside the thread's own sandbox, but the server still supports the flow. For a tool that needs approval, `input` holds the command or arguments the agent wants to run. Send `action: "approve"`, or `action: "decline"` with an optional `reason` the agent reads before it tries something else.

## Deployment status

`get_thread` includes `deployment` once a thread has been published. Its `status` moves through these values:

| `status` | Meaning |
| --- | --- |
| `cloning` | Unpacking the project's files. |
| `uploading` | Storing the source for the build. |
| `waiting` | Queued for a build worker. |
| `active` | Building. |
| `completed` | Live at `deployment.url`. |
| `failed` | The build failed. Ask the agent to fix it with `send_message`, then publish again. |
| `unreachable` | The upload server did not answer. Try again shortly. |

Don't call `publish` again while a build is in progress.

## Renaming and deleting

The MCP server can't rename a thread, delete one, or take a published app offline. Do these from the thread's menu in the [web app](/web-app#manage-chats)'s sidebar: **Rename** changes only the thread's title, not the app's URL, and **Delete** removes the conversation, its preview data, and its published app with that app's data. Deleting cannot be undone.

## Stopping a reply

`stop_run` cancels the reply in progress and returns `stopped: false` when nothing was running. A tool call the agent is waiting on stays pending, so a `waiting` thread still needs `resolve_tool_call` before it accepts new messages.
