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

Updated September 25, 2026 · 13 min read

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
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.
  • For xCloud MCP: a connected MCP client with full read and write access. Follow How to connect xCloud MCP to your 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:

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:

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:

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.

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.

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:

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:

{
  "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:

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:
{
  "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:
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:
{
  "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:

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:

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

If a deployment fails in a way the diagnosis endpoint does not explain, feel free to reach out to our support team with the site UUID and the failing event.