Upload an asset and embed it
CMS assets are uploaded out-of-band: the server hands you a presigned URL, you PUT the file directly to object storage, then you tell the server the upload is done. This avoids streaming binaries through the MCP/HTTP layer.
TL;DR
cms_request_asset_upload— server creates anuploaded: falserow, returnsuploadUrl+uploadExpiresAt.- PUT the binary to
uploadUrl(this is not an MCP call; the agent or a connector executes it directly against storage). cms_complete_asset_upload— flipsuploaded: true. Now the asset is referenceable.- Embed by writing the asset id into an entry's
datafield viacms_update_entry.
Step 1 — request the upload
{
"name": "cms_request_asset_upload",
"arguments": {
"name": "spring-launch-hero.jpg",
"mime": "image/jpeg",
"sizeBytes": 482301,
"altText": "Spring launch hero image — three product shots on a sunny patio.",
"metadata": { "campaign": "spring-2026" }
}
}
Response:
{
"id": "<assetId>",
"name": "spring-launch-hero.jpg",
"mime": "image/jpeg",
"sizeBytes": 482301,
"uploaded": false,
"uploadUrl": "https://storage.example/...?X-Amz-Signature=...",
"uploadExpiresAt": "2026-05-01T12:34:56Z"
}
Step 2 — PUT the binary
The presigned URL is a direct upload target. From a connector or the agent's environment:
curl --upload-file ./spring-launch-hero.jpg \
-H "Content-Type: image/jpeg" \
"<uploadUrl>"
Must complete before uploadExpiresAt (typically ~1 hour). Headers must match the mime you declared in step 1.
Step 3 — complete
{ "name": "cms_complete_asset_upload", "arguments": { "id": "<assetId>" } }
Flips uploaded: true. Until you call this, the asset is invisible to other tools and entries can't reference it. You can call complete as long as the row exists — the URL expires, but the row doesn't.
Step 4 — embed in an entry
Asset references in entries are stored as the asset id (or a structured { assetId, ... } object — depends on the field's collection schema). Read the entry, write the field, send the update:
{ "name": "cms_get_entry", "arguments": { "id": "<entryId>" } }
{
"name": "cms_update_entry",
"arguments": {
"id": "<entryId>",
"ifVersion": 12,
"data": { "...all other fields...": "...", "heroImage": "<assetId>" }
}
}
(Use skill://cms/publish-entry for the full update + publish dance.)
Auditing unused assets
{ "name": "cms_list_assets", "arguments": { "limit": 200 } }
For each asset you want to verify usage of, walk inbound references on every entry that might point at it — there's no cms_list_referencing_entries(assetId) shortcut today; you'd grep entry data jsonb. A pragmatic alternative is to use the search API:
{
"name": "cms_search",
"arguments": { "query": "<assetId>", "limit": 50 }
}
Then inspect those entries to confirm.
What NOT to do
- Don't call
cms_complete_asset_uploadbefore the binary PUT succeeds. The asset will be markeduploaded: truewith no actual file — entries referencing it will render broken. - Don't lose the
id. Without it you can't complete the upload, and the half-uploaded row sits as an orphan (no automatic GC). - Don't reuse one presigned URL for multiple files. Each
cms_request_asset_uploadmints a new URL bound to the size and mime you declared. - Don't delete an asset before checking inbound references.
cms_delete_assetremoves the row + storage file; entries that referenced it will render broken in the delivery API. Search/grep first.
Related
skill://cms/publish-entry— the update + publish dance for the entry that embeds the asset.skill://cms/migrate-content— when you're moving assets along with entries.