MCP form write tools
Use these tools to create forms, change form drafts and settings, and publish forms. Some changes save directly; draft proposals must be applied separately.
create_form
Creates a draft from a complete form config. It never publishes. It also creates a default owner email notification. Call get_form_config_schema before generating a config.
Input
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Non-empty form name. |
preview_config | object | Yes | Complete form config. If mode is omitted, it is set to page. If end is missing, a default ending is added. |
workspace_id | string | No | Workspace ID from get_workspace. |
Returns
Returns id, name, status: "draft", preview_config, created_at, updated_at, team_id, workspace_id, and edit_url. A personal key gets workspace_id: null.
Example call
{
"name": "November registration",
"preview_config": {
"mode": "page",
"pages": [
{
"type": "form",
"id": "p1",
"ui": [
{
"type": "short_text_input",
"name": "full_name",
"label": "What is your name?",
"required": true
},
{
"type": "email_input",
"name": "email",
"label": "What is your email?",
"required": true
}
]
}
]
}
}create_form_from_prompt
Creates a draft from a natural-language prompt. It never publishes.
Input
| Name | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | Description of the form to create. |
workspace_id | string | No | Workspace ID. |
Returns
Returns form_id, name, form_config, saved: true, team_id, workspace_id, message, and edit_url.
Example call
{
"prompt": "Create a registration form that asks for a person's name and email address."
}update_form
Renames a form, replaces its draft config, or attaches or clears one managed image. A name change can affect the title of an already published page. This is an immediate direct write and never publishes. Replacing the config or changing an image requires expected_updated_at to match the value returned by get_form.
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
name | string | No | New form name. |
preview_config | object | No | Replacement config; requires expected_updated_at. |
expected_updated_at | ISO timestamp or null | When replacing preview_config | Must match get_form.updated_at. Replacing the config without it returns the error name precondition_required, and data.current_updated_at gives the current value. A value that no longer matches returns conflict. |
image_asset | object | No | {target, asset_id | null}. Use a separate call with expected_updated_at; asset_id is never a URL. Supported targets are open_graph_image, favicon, logo, cover_image, and qr_logo. |
MCP cannot upload image files. Upload an image through REST POST https://api.makeform.ai/forms/{id}/image-assets, then attach it with update_form. The upload uses API-key-only authentication and a multipart/form-data body with exactly two fields: file and target. The open_graph_image, favicon, and qr_logo targets require a paid form.
Returns
Returns id, name, status, and updated_at. With image_asset, the result also includes image_asset with target, asset_id, binding_url, changed, and publishes_form: false.
Example call
Use the exact updated_at value returned by get_form.
{
"form_id": "FORM_ID",
"name": "November registration",
"expected_updated_at": "2026-11-11T18:56:04.351198+00:00"
}update_form_settings
Deep-merges partial settings into a form. Settings changes apply to a published form immediately, without a publish step. CAPTCHA is a form setting, configured with captcha: {enabled, provider: "turnstile"}. It works on hosted forms in page, tally, and typeform modes. You cannot enable it for a form served on a custom domain, and a form with CAPTCHA enabled does not accept Headless submissions.
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
settings | object | Yes | Partial settings using writable keys. |
expected_updated_at | ISO timestamp | No | Rejects a stale write. |
Writable settings keys:
| Group | Keys |
|---|---|
| Language and completion | form_language, redirect_on_completion, redirect_url (valid HTTP or HTTPS URL) |
| Timer | timer { enabled, show_clock, duration_seconds, warning_time_seconds, warning_message, trigger_page_id, expire_end_page_id } |
| Closing | close_form, close_form_message { title, description }, close_on_scheduled_date, close_date, close_date_timezone, total_submissions_limit |
| Behaviour | progress_bar, partial_submissions, duplicate_submissions { enabled, field_name, field_type }, auto_jump_to_next_page, save_answers_for_later |
| Anti-bot | anti_bot_lv1, captcha { enabled, provider: "turnstile" } |
| Display and branding | reduce_motion, hide_loading_screen, loading_screen_text (at most 100 characters), hide_powered_by_badge, hide_end_screen_branding, hide_email_branding |
| Hidden fields | hidden_fields[], each { id?, name, required? } |
| Tracking | google_analytics_id (G- plus 10 characters), meta_pixel_id |
| Sharing | share { site_name, title, description } |
| Embedding | allowed_embed_origins (at most 10) |
| Email verification | email_verification_sender { display_name } |
Unknown settings keys are rejected. A key omitted from the request keeps its stored value. Plain values replace stored values; objects merge key by key. Arrays replace the whole stored array. duplicate_submissions replaces its whole object, and null replaces the stored value, including objects and arrays.
For enabled timers, provide duration_seconds and warning_time_seconds. A scheduled close needs a future close_date timestamp and a close_date_timezone. timer.expire_end_page_id must refer to an ending in the published config.
Hidden-field names must start with a lowercase letter and contain only lowercase letters, digits, and underscores. They must be unique and must not match a visible field name, ignoring case. Keep each returned hidden-field id when sending the array back; to rename a hidden field, keep its id and change its name.
Embedding origins must use HTTPS, except that HTTP is accepted for localhost. They cannot contain a path, query, or wildcard, and each can be at most 300 characters. Sending an empty array removes the origin restriction.
Plan eligibility follows the form owner’s plan, or the team owner’s plan for a team form. Pro or Business is required to turn on partial_submissions, hide_powered_by_badge, hide_end_screen_branding, hide_loading_screen, or hide_email_branding; change loading_screen_text; set a new google_analytics_id or meta_pixel_id; or set a new share.site_name, share.title, or share.description. email_verification_sender requires Business. Turning paid values off, clearing them, or resending unchanged values is allowed. Plan errors are named requires_pro or requires_business.
Headless forms accept only close_form, close_on_scheduled_date, close_date, close_date_timezone, total_submissions_limit, hidden_fields, and hide_email_branding.
Returns
Returns form_id, settings, and reconciliation with status: "complete" and settings_may_be_stale: false.
Example call
{
"form_id": "FORM_ID",
"settings": {
"captcha": {
"enabled": true,
"provider": "turnstile"
}
}
}To reopen a form after it has closed because a scheduled date passed or the submission limit was reached, move or disable the scheduled close, or increase or remove the reached limit, along with setting close_form to false.
duplicate_form
Copies a form into a new draft.
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to copy. |
new_name | string | No | Name for the new draft. |
Returns
Returns id, name, status: "draft", source_form_id, message, and edit_url.
Example call
{
"form_id": "FORM_ID",
"new_name": "November registration copy"
}translate_form
Translates a draft into respondent languages. This tool requires Pro; otherwise it returns the requires_pro error. Translations are saved to the draft and are published with publish_form. Strings edited by the owner are kept, and the source text does not change.
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to translate. |
locales | string[] | Yes | Unique supported locales, at least one, excluding the source language. |
refresh | boolean | No | Redo earlier AI-translated strings. |
expected_updated_at | ISO timestamp | No | Rejects a stale write. |
Returns
Returns form_id, source_locale, enabled_locales, locales with each locale’s translated, kept, and still_missing values, updated_at, published: false, and message.
propose_form_draft
Validates a complete form config and returns a proposal without saving it. Call get_form_config_schema before generating a config. Use the returned draft_token and exact proposed_config with apply_form_draft. The token is bound to the action, form, user, calling credential, and proposed config. A changed draft or proposal context is rejected with data.name: "conflict".
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
proposed_config | object | Yes | Complete form config. |
Returns
Returns id, name, changed, apply_required, summary, delta, risk, publish_readiness, and message. When an apply is available, it also returns draft_token, expires_at, and proposed_config.
Example call
{
"form_id": "FORM_ID",
"proposed_config": {
"mode": "page",
"start": {
"id": "start",
"type": "start",
"ui": [],
"button_label": "Start"
},
"pages": [
{
"type": "form",
"id": "p1",
"ui": [
{
"type": "short_text_input",
"name": "full_name",
"label": "What is your name?",
"required": true
},
{
"type": "email_input",
"name": "email",
"label": "What is your email?",
"required": true
}
]
}
],
"end": [
{
"id": "e0",
"type": "end",
"ui": []
}
]
}
}apply_form_draft
Saves a proposed config to the draft. The draft must still be current, and proposed_config must match the proposal token exactly. This tool never publishes. The token is bound to the action, form, user, calling credential, and proposed config. A changed draft or proposal context is rejected with data.name: "conflict".
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
draft_token | string | Yes | Token returned by propose_form_draft. |
proposed_config | object | Yes | Exact config returned by the proposal. |
Returns
Returns id, name, status, updated_at, field_count, delta, and message.
Example call
{
"form_id": "FORM_ID",
"draft_token": "mf_mcp_act_DRAFT_TOKEN",
"proposed_config": {
"mode": "page",
"start": {
"id": "start",
"type": "start",
"ui": [],
"button_label": "Start"
},
"pages": [
{
"type": "form",
"id": "p1",
"ui": [
{
"type": "short_text_input",
"name": "full_name",
"label": "What is your name?",
"required": true
},
{
"type": "email_input",
"name": "email",
"label": "What is your email?",
"required": true
}
]
}
],
"end": [
{
"id": "e0",
"type": "end",
"ui": []
}
]
}
}propose_form_edit
Sends the current draft and an instruction to an AI model, then returns an edited config without saving it. Use the returned edit_token and proposed_config with apply_form_edit. The token is bound to the action, form, user, calling credential, and proposed config. A changed draft or proposal context is rejected with data.name: "conflict".
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
instruction | string | Yes | Requested draft change. |
Returns
Returns id, name, changed, apply_required, summary, delta, risk, publish_readiness, and message. When an apply is available, it also returns edit_token, expires_at, and proposed_config.
Example call
{
"form_id": "FORM_ID",
"instruction": "Add a required phone number question after the email question."
}apply_form_edit
Saves a proposed AI edit to the draft. It rejects stale drafts and never publishes. The token is bound to the action, form, user, calling credential, and proposed config. A changed draft or proposal context is rejected with data.name: "conflict".
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to update. |
edit_token | string | Yes | Token returned by propose_form_edit. |
proposed_config | object | Yes | Config returned by the proposal. |
Returns
Returns id, name, status, updated_at, preview_config_hash, delta, message, and, when present, warnings.
Example call
{
"form_id": "FORM_ID",
"edit_token": "mf_mcp_act_EDIT_TOKEN",
"proposed_config": {
"mode": "page",
"pages": [
{
"type": "form",
"id": "p1",
"ui": [
{
"type": "short_text_input",
"name": "full_name",
"label": "What is your name?",
"required": true
},
{
"type": "email_input",
"name": "email",
"label": "What is your email?",
"required": true
},
{
"type": "phone_input",
"name": "phone",
"label": "What is your phone number?",
"required": true
}
]
}
],
"end": [
{
"id": "e0",
"type": "end",
"ui": []
}
]
}
}publish_form
Previews publication first. To publish, call it again with confirm_publish: true and the publish_token from the preview. Confirmation also publishes draft translations. The token is bound to the action, form, user, calling credential, and proposed publication state. A changed publication context is rejected with data.name: "conflict".
Input
| Name | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Form to publish. |
confirm_publish | boolean | No | Set to true to confirm publication. Omit for a preview. |
publish_token | string | For confirmation | Token returned by the preview. |
label | string | No | Label up to 120 characters. |
Returns
The preview returns published: false, would_publish, confirmation_required, publish_token, expires_at, proposed_config_hash, and preview with the delta and risk. Confirmation returns published: true, id, name, published_at, version, url, field_count, and publish_config_hash.
Example call
Preview:
{
"form_id": "FORM_ID",
"label": "November registration"
}Confirm with the preview’s token:
{
"form_id": "FORM_ID",
"confirm_publish": true,
"publish_token": "mf_mcp_act_PUBLISH_TOKEN",
"label": "November registration"
}Action tokens expire after 10 minutes. If a token expires, run the preview or proposal again.
Tool errors
Tool errors return HTTP 200 with result.isError: true. The text in content[0].text is a JSON object wrapped in jsonrpc_error. Validation errors use code -32602, business-rule errors use -32600, and unexpected errors use -32603.
{
"jsonrpc_error": {
"code": -32600,
"message": "Replacing preview_config requires expected_updated_at. Call get_form and pass its updated_at so a newer edit is never overwritten.",
"data": {
"name": "precondition_required",
"current_updated_at": "2026-11-11T18:56:04.351Z"
}
}
}Input schema failures start with MCP error -32602: Input validation error. Deleting forms and managing custom domains are not available through MCP.