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, andwrite:sitespermissions. 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
- Call
GET /integrations/gitand choose a connected provider UUID. - Call
GET /integrations/git/{provider_uuid}/repositoriesto find the repository’sfull_nameand default branch. - 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
- Prepare a key with
POST /servers/{server_uuid}/git/deploy-keys. - Add only the returned
data.public_keyto the repository’s deploy-key settings. - 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
}'
- Continue only when
data.verifiedistrue. - 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:
servers.git.deploy-keys.storethroughxcloud_execute_write.- You add the returned public key to the repository.
servers.git.deploy-keys.verifythroughxcloud_execute_write.servers.sites.git.autowithrepository.deploy_key_uuidafter 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}/statusreturnsterminal: true.deploy_stateisdeployed.failed_stepsis 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 Acceptedas a successful deployment instead of pollingsites.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, andterminal. - 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, or connect GitHub and deploy Git projects for provider setup and deploy-key recovery in the UI.
- Review the complete xCloud Public API reference.
- Create and scope a token with How to access the xCloud API.
- Connect an AI client with How to connect xCloud MCP to your AI agent, and see what you can ask xCloud MCP to do.
- Grant a token or MCP connection access to several teams when the server lives in another team.
- Let the xCloud 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 with the site UUID and the failing event.