MCP Tools: Forms and Assets

Five tools cover forms and five cover assets. Between them they can design a form from scratch, read everything people have submitted to it, and find, inspect, rename, relabel and delete the images, videos, audio files and documents in your library. What they cannot do is put a file into the library — uploading is not on the MCP surface — and none of them puts a form live by itself.

Reading forms

list_forms

GET /api/forms · Returns { forms } · Also available in the Leed Assistant

Takes no parameters. Returns form metadata — formId, formName, formPrototype, description, isDirty and modification stamps — not the field definitions. Deleted forms are excluded. This is the call that resolves a human name like “Contact us” into the formId every other tool here wants.

get_form

GET /api/forms/:formId · Returns { form } · Also available in the Leed Assistant

ParameterTypeRequiredDefaultNotes
formIdstringYes—From list_forms

The complete form, including its fields array. Read this before editing: update_form_draft merges what you send, but the fields array is replaced wholesale when you send it, so a model that wants to add one field must send all of them.

Creating and editing a form

create_form and update_form_draft share one field surface. The difference is which parts of it are mandatory.

FieldTypeRequired on createMeaning
formPrototypestringYesThe prototype the form is built from; slugified on save
formNamestringYesThe form’s name in the CMS
buttonstringYesSubmit button label
successCallbackstringYesWhere the visitor goes after a successful submission
failureCallbackstringYesWhere the visitor goes when submission fails
fieldsarray of field objectsYesThe form’s inputs, in order
headingstringNoHeading rendered above the form
descriptionstring (≤ 400 chars)NoShort description; also shown in form lists
detailsstringNoLonger supporting copy
welcomeBackMessagestringNoShown instead of the form to a visitor already identified
leedAutofillbooleanNoPre-fill from what Leed already knows about the visitor
autofill"linkedin" or nullNoThird-party autofill provider; send null to clear it
assetIdstring or nullNoAsset associated with the form
templateIdstring or nullNoTemplate the form renders with
emailTemplateIdinteger or nullNoThe email template a submission is answered with; takes precedence over templateId
emailInputsobject or nullNoValues for that email template’s blocks that are set on the form, keyed by the template’s input keys

The system columns — protected, isDirty, disabledAt and disabledBy — are not part of either tool’s input. The route marks a written form as needing publication itself, and a form cannot be made protected through the API.

The field object, in full

Every entry in fields is an object with this shape. formFieldId, name, required and type are mandatory on each one.

KeyTypeRequiredMeaning
formFieldIdstringYesStable id for the field
namestringYesThe submitted key — this is what maps onto a contact record
typetext | textarea | tel | email | checkbox | radio | select | countriesYesInput type
requiredbooleanYesWhether the visitor must fill it in
labelBeforestringNoLabel rendered before the input
labelAfterstringNoLabel rendered after the input
placeHolderstringNoPlaceholder text
valuestringNoDefault value
patternstringNoValidation pattern
rowsnumberNoRows for a textarea
multiplebooleanNoAllow multiple selections
selectedbooleanNoPre-selected state
optionsarray of { label, value }NoChoices for select, radio and checkbox
oninput / onchangestringNoClient-side handlers

Which name values map onto which contact fields — and which are simply captured as extra data — is enumerated in the Form Field Reference.

create_form

POST /api/forms · Returns { form } · Also available in the Leed Assistant

Takes the whole surface above; the six fields marked required must be present. Creating a form also creates its default form action, so the form is immediately wired up to record submissions once it is published.

{
  "formPrototype": "contact",
  "formName": "Contact us",
  "heading": "Talk to us",
  "button": "Send",
  "successCallback": "/thanks/",
  "failureCallback": "/contact/?error=1",
  "fields": [
    {
      "formFieldId": "email",
      "name": "email",
      "type": "email",
      "required": true,
      "labelBefore": "Work email"
    },
    {
      "formFieldId": "message",
      "name": "message",
      "type": "textarea",
      "required": false,
      "labelBefore": "How can we help?",
      "rows": 4
    }
  ]
}

update_form_draft

PUT /api/forms/:formId · Returns { form } · Also available in the Leed Assistant

ParameterTypeRequiredDefaultNotes
formIdstringYes—Which form to edit
any field from the table above—NounchangedPartial — send only what changes

Two forms of refusal are worth knowing. A form marked protected — Leed creates one of these automatically for the Docs MCP reader sign-in — returns 403 form_protected and cannot be edited through this tool at all. And sending a fields array replaces the existing one, so a partial fields array truncates the form.

What you cannot do

Deleting a form is absent from the MCP surface, not merely restricted: delete_form is a registered tool that a client is never shown, so a model cannot invoke it even by name. It is available in the Leed Assistant, where it stops and waits for your approval — see Leed Assistant. There is no per-form publish tool either; a form goes live with the rest of your pending changes.

Reading submissions

list_form_fills

GET /api/forms/:formId/fills · Returns { fills } · MCP only

ParameterTypeRequiredDefaultNotes
formIdstringYes—Resolve it with list_forms first
limitinteger, 1–100Nono limitOmitting it returns every submission
offsetinteger, ≥ 0No0Rows to skip

What a submission records, why a preview site never captures one, and how a fill updates the matching contact are all covered in Form Submissions.

Assets

list_assets

GET /api/assets/:type? · Returns { assets, count } · MCP only

ParameterTypeRequiredDefaultNotes
typeimage, video, audio or documentNoall typesNarrows to one media family
labelsstringNo—Comma-separated label ids; an asset carrying any of them matches
qstringNo—Case-insensitive substring match on name and filename
includeDeletedbooleanNofalseInclude soft-deleted assets
limitinteger, 1–200Nono limitOmitting it returns every match
offsetinteger, ≥ 0No0Rows to skip

The filters combine with AND, newest first. count is the total matching the filters, which is how you learn there is more behind a page. Each row carries trackingCounts — how many pages, forms, email templates and profiles use the asset — and never carries extracted document text or transcriptions; read those with get_asset. Requires asset:read.

get_asset

GET /api/assets/id/:assetId · Returns { asset } · MCP only

One asset by id; no type is needed, because an asset id is unique across all four types. The record adds tracking, the ids of everything using the asset, and — on a plan that includes content extraction — a document’s text or an audio or video file’s transcription and transcriptionSummary. Below that plan those are withheld and the response carries contentExtractionGated: true. A soft-deleted asset is not found here.

update_asset

PATCH /api/assets/:assetId · Returns { asset } · MCP only

ParameterTypeRequiredDefaultNotes
assetIdstringYes——
namestringNounchangedDisplay name
altTextstringNounchangedImages only — a 400 on any other type
labelsstring[]NounchangedReplaces the whole label set; [] clears it

These three are the only asset fields any MCP tool can write. A locked label the asset already carries survives a labels replace. Requires asset:update, Collaborator or above.

change_asset_labels

POST /api/assets/labels · MCP only · Confirm-gated, route mode

Adds addLabels and removes removeLabels on 1–200 assetIds at once, never replacing a whole set. Assets are processed one by one, so a bad id fails only its own line; an unknown label id refuses the whole call before anything is written. Unlike update_asset, naming a locked label in removeLabels does take it off. The first call is a dry run; repeat it with confirm: true to apply.

delete_asset

DELETE /api/assets/:assetId · Returns { asset } · MCP only · Confirm-gated, local mode

A soft delete: the record is kept with a deletedAt stamp, and the asset drops out of list_assets unless includeDeleted is set. It is refused with a 409 while any page, form, email template or profile still references the asset, and it needs asset:delete — Coordinator or above, a higher bar than updating.

Uploading is not an MCP action — here is what it is instead

An AI that has just generated an image, or that you have handed a file to, still needs a real answer. The simplest is the Leed CLI: leed asset upload <file> uploads a local file, deduplicates it against the library and returns ready-to-paste Leed Markdown. Underneath, uploading is a three-step authenticated HTTP flow against the same API the CMS itself uses. It runs under the same session or token as everything else and carries the same asset:create permission check, so it is not a way around any control — it is simply a surface MCP does not cover.

# 1. Ask for a one-shot direct-upload URL from Cloudflare Images.
curl -s -H "Authorization: Bearer $LEED_TOKEN" \
  https://app.leed.ai/api/assets/create/image
# → { "id": "<imageId>", "uploadURL": "https://upload.imagedelivery.net/...", ... }

# 2. PUT the bytes to that URL. No Leed auth header here — the URL is the credential.
curl -s -X PUT --upload-file ./diagram.png "$UPLOAD_URL"

# 3. Register the asset with Leed so it appears in the library.
curl -s -X POST -H "Authorization: Bearer $LEED_TOKEN" \
  -H "content-type: application/json" \
  https://app.leed.ai/api/assets \
  -d '{
        "type": "image",
        "imageId": "<imageId from step 1>",
        "name": "Architecture diagram",
        "filename": "diagram.png",
        "altText": "Three services connected to one database",
        "originalWidth": 1600,
        "originalHeight": 900,
        "svg": false,
        "labels": []
      }'
# → the created asset, including its assetId

altText is required on an image and must be non-empty — Leed generates one automatically for uploads that come through the CMS, but the direct API call will not do it for you. Only once step 3 returns an assetId can a page body reference the image, as ![alt](src){data-assetid="…"}.

Documents, audio and video each have their own GET /api/assets/create/<type> step with different query parameters, and video finalizes through a separate call rather than POST /api/assets. If you are doing this by hand rather than by script, the CMS path is faster and is described in Uploading Images; Asset Library covers what happens to a file after it lands.

If the tool you came here for is not on this page, the Operator MCP Tool Index lists every tool alphabetically with the page that documents its parameters.

ESC