| deploy_create_uploadA | Start a direct-to-S3 upload for binary or large files. Use this instead of deploy_files for binaries (PDF, image, video, zip) or any file > ~1 MB. The MCP server has no access to the user's filesystem and deploy_files ships content inline through Lambda (capped at ~6 MB JSON-RPC payloads); this tool returns presigned S3 PUT URLs so the file bytes flow directly from your environment to S3, never through the MCP server. Bundle into a zip first when: the upload contains more than 3 files OR any file is larger than ~1 MB. The fileWorker auto-extracts a single-zip upload after promotion, so subdirectories are preserved end-to-end and you avoid one PUT round-trip per file. Skip zipping only for the trivial single-small-file case (e.g. one HTML). Bash bundle-and-deploy template (the agent should adapt fileNames and the cleanup prompt):
TMP=$(mktemp -d)
zip -r "$TMP/site.zip" index.html styles.css img/ # add every file/dir to deploy
SIZE=$(stat -c%s "$TMP/site.zip" 2>/dev/null || stat -f%z "$TMP/site.zip")
# 1. call deploy_create_upload with { siteId, files: [{ fileName: "site.zip", fileSize: $SIZE }] }
# 2. PUT $TMP/site.zip to the returned URL(s) per the protocol below, capturing ETag
# 3. call deploy_finalize with { siteId, versionId, completions: [...] }
# 4. ASK THE USER: "Deploy succeeded. Remove temp folder $TMP? [y/N]"
# Only run rm -rf "$TMP" after explicit confirmation; otherwise leave it for them to inspect. Three-step protocol: Call this tool with { siteId, files: [{ fileName, fileSize }] }. Receive { versionId, files: { [fileName]: { uploadId, key, partUploadUrls: [{ part, url }], partSize, expiresAt } } }. For each file, slice the bytes into chunks of partSize and PUT each chunk to its partUploadUrls[i].url. Capture the ETag response header from every PUT - you will need it for finalize. Single-part (small file, one URL): curl -D - -X PUT --data-binary @file.pdf "$URL", then grep the response headers for ETag. Multi-part with dd (no temp files; reads each chunk in place):
count=$(jq ".files["large.zip"].partUploadUrls | length" envelope.json)
for i in $(seq 0 $((count-1))); do
url=$(jq -r ".files["large.zip"].partUploadUrls[$i].url" envelope.json)
etag=$(dd if=large.zip bs=5M skip=$i count=1 status=none | curl -sS -D - -X PUT --data-binary @- "$url" | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
echo "{ "PartNumber": $((i+1)), "ETag": $etag }" >> parts.json
done Multi-part in Python - prefer this over dd for files > ~50 MB (parallel PUTs, no temp files, cleaner error handling):
import json, requests
from concurrent.futures import ThreadPoolExecutor
env = json.load(open("envelope.json"))
info = env["files"]["large.zip"]
part_size = info["partSize"]
def upload_part(p):
with open("large.zip", "rb") as f: # own handle per thread
f.seek((p["part"] - 1) * part_size)
r = requests.put(p["url"], data=f.read(part_size))
r.raise_for_status()
return {"PartNumber": p["part"], "ETag": r.headers["ETag"]}
with ThreadPoolExecutor(max_workers=5) as ex: # cap concurrency at 5
parts = list(ex.map(upload_part, info["partUploadUrls"])) Call deploy_finalize with { siteId, versionId, completions: [{ uploadId, key, parts: [{ ETag, PartNumber }] }] } for every multi-part file. Single-part uploads (uploadId is empty in the start response) need no completion entry.
The site must already exist - call create_site first if you do not have a siteId. Deploying overwrites existing content; confirm overwrite with the user first. Anti-patterns - never do these to bypass a blocked or unavailable upload path: Do NOT base64-embed, data-URI, or otherwise inline binary content (images, PDFs, video, fonts, zips) into HTML/CSS/JSON or any other deployed file. Inlining bloats the page, breaks browser caching, reships the bytes on every redeploy, and hides the real environment problem from the user. Do NOT fall back to deploy_files for a binary that was meant for deploy_create_upload - the payload exceeds deploy_files' cap and the workaround is itself an anti-pattern. Do NOT silently downscale, recompress, or otherwise mutate the user's file to fit a smaller path - ask first.
When the upload path is unavailable, surface it to the user using the escalation order below.
Recovery escalation order when a PUT fails with a network/DNS error (connection refused, name resolution failure, timeout, sandbox-denied, proxy 403/407) OR your host environment has no HTTP-PUT capability at all (no bash/curl, no Python requests, no fetch). Do NOT retry blindly - a sandbox denial does not resolve on its own. Ask the user to enable egress (preferred fix). This is the cheapest option and unblocks all future uploads. Surface the exact sandbox configuration below and wait for the user to confirm before retrying. Most users can flip this setting in seconds; do not skip to step 2 without asking. If the user declines, cannot change the sandbox, or has no HTTP-PUT capability in-environment, present the failing presigned URL(s) verbatim and ask them to upload the file themselves (curl one-liner, browser, or any HTTP client they have). Provide a copy-pasteable command. Call deploy_finalize only after the user confirms each PUT returned 200 with an ETag (they will need to share the ETags back for multi-part uploads). Never route around the failure by base64-inlining the file into another deploy, recompressing, or falling back to deploy_files. See anti-patterns above.
Egress allowlist instructions for step 1: Read the failing URL's hostname. It will be either us.api.hostsmith.net, eu.api.hostsmith.net, or an S3 endpoint (copy verbatim from the failing URL). Infer the sandbox and give the user the exact configuration snippet: Cursor (signal: cwd contains .cursor/, or CURSOR_* env vars) - edit ~/.cursor/sandbox.json (user scope) or <repo>/.cursor/sandbox.json (repo scope): { "networkPolicy": { "allow": ["us.api.hostsmith.net", "eu.api.hostsmith.net"] } }
Reload Cursor. Claude Code (signal: CLAUDE_CODE_* env vars, or ~/.claude/ settings present) - edit ~/.claude/settings.json: { "sandbox": { "enabled": true, "network": { "allowedDomains": ["us.api.hostsmith.net", "eu.api.hostsmith.net"] } } }
If deniedDomains lists a matching host, remove it first - deny takes precedence. Codex CLI (signal: ~/.codex/ settings, CODEX_* env vars) - edit ~/.codex/config.toml: [sandbox_workspace_write]
network_access = true
Codex has no per-host allowlist at this layer - this enables network for workspace-write mode globally. Risk is bounded by the upload token in the URL. Unknown sandbox - tell the user: allow outbound HTTPS (port 443) to us.api.hostsmith.net and eu.api.hostsmith.net (and the failing S3 host if the URL points at S3) in whatever firewall/proxy they control. If a corporate HTTP proxy is in play, ensure CONNECT to those hostnames is permitted, or set NO_PROXY for direct routing.
After the user confirms the change, retry the failed PUT. Tokens in the URL (ut=... for partition-host URLs, X-Amz-Signature for S3 URLs) remain valid for 1 hour from issuance, so re-running the same URL within that window works without re-calling deploy_create_upload.
|