# How to Deploy Git Projects with the xCloud Public API and MCP

> Deploy a Git project to an xCloud server from CI or an AI agent: detect the repo, dry-run, deploy with an idempotency key, poll status, handle private repos.

This tutorial shows developers and automation teams how to deploy a supported Git project to an existing xCloud server through either the Public API or xCloud MCP. You will preview repository detection, handle public or private repository access, set the app type and runtime configuration, start an idempotent deployment, poll its status, configure a domain, and recover from a failed attempt. Use the dashboard guide instead if you want to complete the same workflow interactively.

## Choose the right path

The dashboard, Public API, and MCP all use the same xCloud deployment capabilities, but they suit different workflows.

| Path | Best for | Starting point |
|---|---|---|
| Dashboard | One-off deployments with a visual form | [Deploy Git projects in one click](/docs/deploy-git-projects-in-one-click-with-xcloud/) |
| Public API | CI/CD, scripts, and deterministic integrations | `https://app.xcloud.host/api/v1` |
| xCloud MCP | Agent-assisted deployment with human approval | `https://app.xcloud.host/mcp` |

The API and MCP paths below create real sites. Preview the resolved configuration first, then confirm the billable deployment only after you have checked the repository, server, app type, domain, and runtime settings.

## Prerequisites
- An xCloud account with access to the target team and server.
- A provisioned xCloud server compatible with the project runtime.
- A Git repository containing a supported WordPress, Laravel, Custom PHP, Node.js, or Lovable project.
- For the Public API: a token with `read:servers`,`write:servers`,`read:sites`, and `write:sites` permissions. Follow [How to access the xCloud API](/docs/how-to-access-the-xcloud-api/).
- For xCloud MCP: a connected MCP client with full read and write access. Follow [How to connect xCloud MCP to your AI agent](/docs/how-to-connect-xcloud-mcp-to-ai-agent/).
- For a private repository: either a connected Git provider or permission to add an SSH deploy key to the repository.
- For a live domain: access to its DNS records.
- The required environment values, build command, start command, web root, and runtime version for the application.

## Deploy through the Public API

This example deploys a Node.js static site to an xCloud staging hostname. Replace every placeholder before running the commands. Keep the API token outside source control and CI logs.

### Step 1: Set the API variables

Set the API base URL, token, server UUID, and repository URL in your shell:

```bash
export XCLOUD_API_BASE='https://app.xcloud.host/api/v1'
export XCLOUD_API_TOKEN='xc_live_xxxxxxxxxxxx'
export SERVER_UUID='11111111-1111-4111-8111-111111111111'
export REPOSITORY_URL='https://github.com/your-org/your-app.git'
```

**Expected result:** your shell has the values needed by the remaining requests. The token value is not printed.

### Step 2: Confirm that the target server is provisioned

List provisioned servers before choosing a target:

```bash
curl --fail-with-body -sS \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  "${XCLOUD_API_BASE}/servers?status=provisioned&per_page=100"
```

Choose a server from `data.items` and copy its `uuid` into `SERVER_UUID`. Check its `stack` and installed runtime against your project.

**Expected result:** the selected server has `status: "provisioned"`. Do not silently choose the first server in an automation workflow when several servers are available.

### Step 3: Preview repository detection and compatibility

Call the side-effect-free detection endpoint with the intended server:

```bash
curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  "${XCLOUD_API_BASE}/git/detect" \
  --data "{
    \"repository_url\": \"${REPOSITORY_URL}\",
    \"server_uuid\": \"${SERVER_UUID}\",
    \"include_deploy_script\": true
  }"
```

Review these response fields before continuing:

| Field | What to check |
|---|---|
| `data.reachable` | Must be `true` before xCloud can inspect the repository. |
| `data.default_branch` | Use this branch unless you intentionally deploy another one. |
| `data.detection.site_type` | Confirm the detected app type. You can explicitly supply `laravel`,`nodejs`,`custom-php`,`wordpress`, or `lovable`. |
| `data.detection.web_root` | Confirm the directory xCloud should serve, such as `dist` or `public`. |
| `data.detection.build_command` and `start_command` | Compare the suggestions with the project's own documentation. |
| `data.repository_access` | Branch on `status`,`code`, and `next_actions`, not on the prose message. |
| `data.compatibility.compatible` | Must be `true` for the native Git path. A Docker server may instead return `docker_deployable: true` and `deploy_via: "docker_compose"`. |

**Expected result:** the response identifies a supported app type and a compatible deployment path. Detection is a preview and creates nothing.

### Step 4: Preview the resolved site without creating it

Use the auto-deploy endpoint with `dry_run: true`. The example deliberately sets the important Node.js values instead of relying on every detected default.

```bash
curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  "${XCLOUD_API_BASE}/servers/${SERVER_UUID}/sites/git/auto" \
  --data "{
    \"dry_run\": true,
    \"site_type\": \"nodejs\",
    \"repository\": {
      \"url\": \"${REPOSITORY_URL}\",
      \"branch\": \"main\"
    },
    \"domain\": {
      \"mode\": \"staging\",
      \"name\": \"git-api-demo\"
    },
    \"serving_mode\": \"static\",
    \"node_version\": \"20\",
    \"install_command\": \"npm ci\",
    \"build_command\": \"npm run build\",
    \"web_root\": \"dist\",
    \"env_file_content\": \"NODE_ENV=production\\nPUBLIC_API_URL=https://api.example.com\",
    \"generate_ai_script\": false
  }"
```

The `install_command` and `build_command` fields cover this static Node.js build. Use the optional `deploy_script` field only when the repository needs an additional post-pull command on every deployment, such as `npm run postdeploy`. Keep `generate_ai_script: false` when you want the explicit configuration above instead of an AI-generated script, and confirm that any named package script exists in the repository before deploying.

**Expected result:** xCloud returns `200` with `data.dry_run: true` and a resolved `data.would_create` preview. No site is created and no idempotency key is consumed.

### Step 5: Start the deployment with an idempotency key

After a human checks the dry-run result, send the same configuration without `dry_run`. Add a unique, stable `Idempotency-Key` so retrying the request cannot create a duplicate site.

```bash
export IDEMPOTENCY_KEY='git-api-demo-2026-09-25-001'

curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  "${XCLOUD_API_BASE}/servers/${SERVER_UUID}/sites/git/auto" \
  --data "{
    \"site_type\": \"nodejs\",
    \"repository\": {
      \"url\": \"${REPOSITORY_URL}\",
      \"branch\": \"main\"
    },
    \"domain\": {
      \"mode\": \"staging\",
      \"name\": \"git-api-demo\"
    },
    \"serving_mode\": \"static\",
    \"node_version\": \"20\",
    \"install_command\": \"npm ci\",
    \"build_command\": \"npm run build\",
    \"web_root\": \"dist\",
    \"env_file_content\": \"NODE_ENV=production\\nPUBLIC_API_URL=https://api.example.com\",
    \"generate_ai_script\": false
  }"
```

Save `data.uuid` as `SITE_UUID` and `data.poll_url` as `POLL_URL` from the `202 Accepted` response.

**Expected result:** xCloud accepts one deployment and returns the site UUID, domain, branch, initial status, and polling URL. A `202` means queued, not deployed.

### Step 6: Poll until the deployment reaches a terminal state

Request the returned polling URL, waiting for the response's `data.poll_after_seconds` value between requests:

```bash
export SITE_UUID='22222222-2222-4222-8222-222222222222'

curl --fail-with-body -sS \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  "${XCLOUD_API_BASE}/sites/${SITE_UUID}/status"
```

Continue until `data.terminal` is `true`. Branch on `data.deploy_state`, which is one of `deployed`,`failed`, or `cancelled` after completion. Also check `data.failed_steps`: a site can report `deployed` while still carrying an unrecovered failed step that requires verification.

**Expected result:**`data.deploy_state` is `deployed`,`data.terminal` is `true`, and `data.failed_steps` is empty.

### Step 7: Use a live domain and verify DNS

For a live domain, replace the staging `domain` object before the dry run and deployment:

```json
{
  "mode": "live",
  "name": "app.example.com",
  "ssl_provider": "xcloud"
}
```

The accepted deployment response includes `data.domain_setup.required_dns`,`verify_url`, and `next_actions`. Add the returned DNS record at your DNS provider, then check resolution:

```bash
curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  "${XCLOUD_API_BASE}/servers/${SERVER_UUID}/dns/check" \
  --data '{"domain":"app.example.com"}'
```

**Expected result:**`data.resolves_to_server` becomes `true`. Follow `data.next_actions` until it is empty, and keep polling the site while SSL is being issued. If xCloud manages a connected Cloudflare zone, use `cloudflare: true` in the deploy request instead of manually adding the record.

## Deploy a private repository

A private repository requires a connected provider or a verified deploy key. Never paste a private key or personal access token into the deployment payload.

### Connected provider path
1. Call `GET /integrations/git` and choose a connected provider UUID.
2. Call `GET /integrations/git/{provider_uuid}/repositories` to find the repository's `full_name` and default branch.
3. Use this repository object in detection and deployment:

```json
{
  "provider_uuid": "33333333-3333-4333-8333-333333333333",
  "full_name": "your-org/private-app",
  "branch": "main"
}
```

This path can also support `enable_push_deploy: true` when the connected provider has permission to create the webhook.

### SSH deploy-key path
1. Prepare a key with `POST /servers/{server_uuid}/git/deploy-keys`.
2. Add only the returned `data.public_key` to the repository's deploy-key settings.
3. Verify access:

```bash
curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer ${XCLOUD_API_TOKEN}" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  "${XCLOUD_API_BASE}/servers/${SERVER_UUID}/git/deploy-keys/44444444-4444-4444-8444-444444444444/verify" \
  --data '{
    "repository_url":"git@github.com:your-org/private-app.git",
    "branch":"main",
    "detect":true
  }'
```
1. Continue only when `data.verified` is `true`.
2. Pass the verified key to the deployment:

```json
{
  "url": "git@github.com:your-org/private-app.git",
  "branch": "main",
  "deploy_key_uuid": "44444444-4444-4444-8444-444444444444"
}
```

If deployment is abandoned, delete the unused key with `DELETE /servers/{server_uuid}/git/deploy-keys/{key_uuid}`. A private SSH URL without a verified `deploy_key_uuid` is rejected.

## Deploy through xCloud MCP

MCP uses the same Public API contract. Connect your client to `https://app.xcloud.host/mcp`, or use `https://app.xcloud.host/mcp?profile=compact` when the client limits the number of tools.

### Step 1: Ask the agent to discover the current workflow

Use a prompt that identifies the repository and asks for a preview before any write:

```text
I want to deploy a Node.js project from https://github.com/your-org/your-app.git to xCloud. Find the Git deployment workflow, list my provisioned servers, and analyze the repository against the server I choose. Do not create a site yet.
```

The agent should call `xcloud_agent_search`, then use the returned guidance. On the compact profile, the key operation sequence is:

| Stage | MCP operation ID | Compact executor |
|---|---|---|
| List candidate servers | `servers.index` | `xcloud_execute_read` |
| Analyze repository | `git.detect` | `xcloud_execute_read` |
| Deploy with detection | `servers.sites.git.auto` | `xcloud_execute_destructive` |
| Poll deployment | `sites.status` | `xcloud_execute_read` |
| Diagnose failure | `sites.deploy-diagnosis` | `xcloud_execute_read` |
| Read resolved configuration | `sites.deploy-config` | `xcloud_execute_read` |
| Retry a failed deployment | `sites.provision-retry` | `xcloud_execute_destructive` |

**Expected result:** the agent presents the detected app type, branch, serving mode, commands, web root, server compatibility, domain, and any access actions without creating a site.

### Step 2: Approve the exact resolved configuration

After reviewing the preview, give explicit approval with the values you accept:

```text
Deploy the repository to the server I selected as a Node.js static site. Use branch main, Node.js 20, npm ci, npm run build, web root dist, and the staging label git-api-demo. Use an idempotency key. Show me the final request before setting confirm to true.
```

The agent must use `servers.sites.git.auto` through `xcloud_execute_destructive` with `confirm: true` only after your approval. The operation returns `202 Accepted` and a polling URL.

**Expected result:** the agent reports that xCloud accepted the deployment, then polls `sites.status` until `terminal: true`. It does not describe `202 Accepted` as a completed deployment.

### Step 3: Handle private repository access

For a connected provider, ask the agent to use `integrations.git.index` and `integrations.git.repositories`. For an SSH repository, the MCP sequence is:
1. `servers.git.deploy-keys.store` through `xcloud_execute_write`.
2. You add the returned public key to the repository.
3. `servers.git.deploy-keys.verify` through `xcloud_execute_write`.
4. `servers.sites.git.auto` with `repository.deploy_key_uuid` after explicit approval.

The human step cannot be skipped. If verification fails, the agent should stop rather than start cloning.

## Verify the deployment

A deployment is complete only when all of these checks pass:
- `GET /sites/{uuid}/status` returns `terminal: true`.
- `deploy_state` is `deployed`.
- `failed_steps` is empty, or each listed step has been reviewed and resolved.
- The site's domain returns the expected application response.
- For a live domain, DNS resolves as expected and the SSL state no longer blocks serving.
- The deployed branch, app type, serving mode, web root, runtime version, and environment configuration match the approved preview.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| `401 Unauthorized` | The token is missing or invalid. | Send the token as `Authorization: Bearer ...` and create a new token if it was revoked. |
| `403 Forbidden` | The token lacks a required scope or team grant. | Add the required server/site scope and select a team granted to the token. There is no silent team fallback. |
| `repository_access_probe_unavailable` | The repository host or fallback probe could not answer. This is not proof that the repository is missing. | Retry later, connect a provider, or explicitly supply a verified app type after confirming the repository yourself. |
| `deploy_key_required` | A private SSH repository has no adopted key. | Prepare the key, add the public half to the repository, verify it, and pass `deploy_key_uuid`. |
| `deploy_key_not_verified` | xCloud cannot clone with the prepared key. | Check that the exact public key is attached to the correct repository and verify again. |
| `incompatible_server` | The selected server cannot run the native deployment. | Use a recommended compatible stack. If detection returns `docker_deployable: true`, follow the Docker deployment path. |
| `repository_branch_not_found` | The requested branch does not exist or is not visible. | Use `default_branch` from detection or select an existing provider branch. |
| `site_type_undetectable` | Detection could not safely choose an app type. | Set `site_type` explicitly after reviewing the repository. |
| Deployment remains `in_progress` | A package install or build can remain at one percentage for more than a minute. | Wait for `poll_after_seconds`. Inspect events only after terminal failure or no progress for about five minutes. |
| `deploy_state: failed` | A provisioning, clone, dependency, build, runtime, port, or web-root step failed. | Call `GET /sites/{uuid}/deploy-diagnosis`, inspect `next` and `next_hint`, then read `GET /sites/{uuid}/deploy-config`. Correct only supported fields and retry the same site with `POST /sites/{uuid}/provision-retry`. |
| `429 Too Many Requests` | The API rate limit was exceeded. | Honor `Retry-After`. Authenticated API requests default to 60 per minute; Git detection is limited to 30 per minute and Git deployment to 20 per minute. |

For detailed task output, list `GET /sites/{uuid}/events`, then request `GET /sites/{uuid}/events/{task_uuid}`. Event output is bounded and credential-shaped values are redacted. The default event-detail response returns the end of the output, where failures usually state their cause.

## Common mistakes
- Treating `202 Accepted` as a successful deployment instead of polling `sites.status`.
- Deploying without a dry run or repository detection preview.
- Retrying a create request without reusing the same `Idempotency-Key`.
- Choosing a server silently when automation finds several candidates.
- Trusting a detected build command, start command, or web root without comparing it with the repository.
- Passing secrets in the repository URL, deploy script, command line, or logs.
- Starting a private-repository deployment before the deploy key reports `verified: true`.
- Parsing human-readable error text instead of stable fields such as `repository_access.code`,`next_actions`,`deploy_state`, and `terminal`.
- Deleting and recreating a failed site before using deploy diagnosis and `provision-retry`.

## Frequently asked questions

### Does a 202 Accepted response mean the site is deployed?

No. 202 means xCloud queued the deployment. Poll the site status endpoint until terminal is true and deploy_state is deployed, and check that failed_steps is empty.

### Can I preview a deployment without creating a site?

Yes. Call the repository detection endpoint for app type, web root and compatibility, then send the auto-deploy request with dry_run set to true. Neither call creates a site or consumes an idempotency key.

### How do I deploy a private repository through the API?

Either use a connected Git provider (list providers, pick the repository by full name) or prepare an SSH deploy key on the server, add the returned public key to the repository, verify it, and pass the deploy key UUID in the deployment request. Never send a private key or personal access token in the payload.

### Which MCP tool creates the site?

On the compact profile, servers.sites.git.auto runs through xcloud_execute_destructive with confirm set to true, and only after you have approved the previewed configuration. Reads such as git.detect and sites.status run through xcloud_execute_read.

## Next steps
- Do it from the dashboard instead: [Deploy Git projects in one click](/docs/deploy-git-projects-in-one-click-with-xcloud/), or [connect GitHub and deploy Git projects](/docs/how-to-integrate-a-git-provider-with-xcloud/) for provider setup and deploy-key recovery in the UI.
- Review the complete [xCloud Public API reference](https://app.xcloud.host/api/v1/docs).
- Create and scope a token with [How to access the xCloud API](/docs/how-to-access-the-xcloud-api/).
- Connect an AI client with [How to connect xCloud MCP to your AI agent](/docs/how-to-connect-xcloud-mcp-to-ai-agent/), and see [what you can ask xCloud MCP to do](/what-you-can-ask-xcloud-mcp-to-do/).
- Grant a token or MCP connection [access to several teams](/docs/multi-team-api-tokens-and-mcp-access/) when the server lives in another team.
- Let the [xCloud Agent Skills](/docs/install-and-use-xcloud-ai-agent-skills/) run this whole sequence for you in Claude Code.

If a deployment fails in a way the diagnosis endpoint does not explain, feel free to reach out to our [support team](/docs/access-built-in-support-portal-in-xcloud/) with the site UUID and the failing event.
