Five tools cover forms and one covers assets. Between them they can design a form from scratch, read everything people have submitted to it, and enumerate every image, video, audio file and document in your library. What they cannot do is put a form live or put a file into the library — publishing a form is not on the MCP surface at all, and neither is uploading.
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
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
formId | string | Yes | — | 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.
| Field | Type | Required on create | Meaning |
|---|---|---|---|
formPrototype | string | Yes | The prototype the form is built from; slugified on save |
formName | string | Yes | The form’s name in the CMS |
button | string | Yes | Submit button label |
successCallback | string | Yes | Where the visitor goes after a successful submission |
failureCallback | string | Yes | Where the visitor goes when submission fails |
fields | array of field objects | Yes | The form’s inputs, in order |
heading | string | No | Heading rendered above the form |
description | string (≤ 400 chars) | No | Short description; also shown in form lists |
details | string | No | Longer supporting copy |
welcomeBackMessage | string | No | Shown instead of the form to a visitor already identified |
leedAutofill | boolean | No | Pre-fill from what Leed already knows about the visitor |
autofill | "linkedin" or null | No | Third-party autofill provider; send null to clear it |
assetId | string or null | No | Asset associated with the form |
templateId | string or null | No | Template the form renders with |
protected | boolean | No | Marks the form system-owned — do not set it |
isDirty | boolean | No | The route sets this for you |
disabledAt / disabledBy | timestamp / string | No | Disables the form |
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.
| Key | Type | Required | Meaning |
|---|---|---|---|
formFieldId | string | Yes | Stable id for the field |
name | string | Yes | The submitted key — this is what maps onto a contact record |
type | text | textarea | tel |
required | boolean | Yes | Whether the visitor must fill it in |
labelBefore | string | No | Label rendered before the input |
labelAfter | string | No | Label rendered after the input |
placeHolder | string | No | Placeholder text |
value | string | No | Default value |
pattern | string | No | Validation pattern |
rows | number | No | Rows for a textarea |
multiple | boolean | No | Allow multiple selections |
selected | boolean | No | Pre-selected state |
options | array of { label, value } | No | Choices for select, radio and checkbox |
oninput / onchange | string | No | Client-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
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
formId | string | Yes | — | Which form to edit |
| any field from the table above | — | No | unchanged | Partial — 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
Publishing a form and deleting a form are both absent from the MCP surface, not merely restricted. publish_form and delete_form are registered tools that a client is never shown, so a model cannot invoke them even by name. Both are available in the Leed Assistant, where they stop and wait for your approval — see Leed Assistant.
Reading submissions
get_form_fills
GET /api/forms/:formId/fills · Returns { fills } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
formId | string | Yes | — | Resolve it with list_forms first |
limit | integer, 1–100 | No | no limit | Omitting it returns every submission |
offset | integer, ≥ 0 | No | 0 | Rows 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 } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
type | image | video | audio | document |
Returns only active assets — anything soft-deleted is filtered out. This is the only asset tool that exists. There is no upload tool, no delete tool, no alt-text tool and no update tool on the MCP surface, in either the MCP or assistant registry. If a model reports that it cannot find a tool to attach an image, it is not misconfigured; the tool does not exist.
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. 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 assetIdaltText 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 {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.