MCP: building forms with an AI assistant
LocalForm registers three WordPress Abilities that let an AI assistant list, read and build forms on your site - including the event details. They are part of the free plugin; with Pro installed they also cover multiple event dates and multiple event prices.
With the MCP Adapter installed, an assistant like Claude can be asked to "create a registration form for the summer school with two dates and a student rate", and it will build the form, set the event details and publish the page.
:::tip Prefer not to leave the admin? LocalForm Pro adds Build with AI, which drives these same abilities from a panel inside WordPress, using an AI provider connected to your site. No MCP client, no bridge, no Application Password. :::
Requirements
- WordPress 6.9 or later - the Abilities API ships in core from 6.9.
- The MCP Adapter plugin, which turns registered abilities into MCP tools. Install the
mcp-adapter.zipfrom its latest release, not a zip of the branch: the branch is a Composer checkout with novendor/, and it activates without complaint and then registers no endpoint at all, logging onlyThe Composer autoloader was not found.
Without the adapter the abilities are still registered, they are simply never called. Below WordPress 6.9 nothing is registered at all.
Authentication
Abilities run as the logged-in WordPress user, and all three require the localform_manage_forms capability - the same permission the form builder screen requires, and one every administrator has. See Who can use LocalForm. Remote MCP clients authenticate with Application Passwords, generated per-user under Users → Profile → Application Passwords.
To check the abilities are registered without a client in the way, the adapter ships a WP-CLI transport:
wp mcp-adapter serve --user=1 --server=mcp-adapter-default-server \
<<< '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"mcp-adapter-execute-ability","arguments":{"ability_name":"localform/list-forms","parameters":{}}}}'
Connecting a client
Connect Claude to your site with MCP walks the whole setup through end to end, with screenshots: installing the adapter, creating the Application Password, the claude_desktop_config.json entry, and what each failure message means.
The short version: the adapter's default server is at
/wp-json/mcp/mcp-adapter-default-server
and LocalForm's abilities are reached through that server's mcp-adapter-execute-ability tool rather than appearing as tools of their own. tools/list returns the adapter's own three - mcp-adapter-discover-abilities, mcp-adapter-get-ability-info and mcp-adapter-execute-ability - and a call looks like:
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "mcp-adapter-execute-ability",
"arguments": { "ability_name": "localform/list-forms", "parameters": {} }
}
}
Over HTTP every request after initialize must carry the Mcp-Session-Id header that initialize returned, or the server answers Invalid Request: Missing Mcp-Session-Id header.
The abilities
| Ability | What it does |
|---|---|
localform/list-forms | Every form, with its slug, title, status and public URL. |
localform/get-form | One form in full, by slug. |
localform/save-form | Creates a form, or replaces the one with the same slug. |
All three sit in a localform ability category and require the localform_manage_forms capability.
get-form and save-form speak the same shape, so the working pattern is read, edit, write back: fetch a form, change what needs changing, hand the whole object back.
A form is identified by its slug:
- A slug that does not exist yet creates a new form, and its public page.
- A slug that exists replaces that form's configuration. Its responses are kept - reconfiguring a form never discards submissions.
- Omit the slug on a new form and one is derived from the title.
The form shape
{
"title": "Summer School 2026",
"slug": "summer-school-2026",
"description": "Two days of workshops.",
"status": "publish",
"fields": [
{ "id": "field_name", "type": "text", "name": "full_name", "label": "Name", "required": true },
{ "id": "field_email", "type": "email", "name": "email", "label": "Email", "required": true }
],
"event_location": "Town Hall",
"event_organizer": "Culture Department",
"pro_event_dates": {
"dates": [
{ "date": "2026-08-12", "start_time": "09:00", "end_time": "17:00", "label": "Day one" },
{ "date": "2026-08-13", "start_time": "09:00", "end_time": "17:00", "label": "Day two" }
],
"question": {
"enabled": true,
"mode": "multiple",
"required": true,
"label": "Which days will you attend?"
}
},
"pro_event_prices": {
"prices": [
{ "label": "Adults", "price": 25 },
{ "label": "Students", "price": 12.5, "details": "with a valid card" }
]
}
}
Everything on the form settings screen is available under its own key - success_message, submit_label, redirect_url, webhook_url, max_submissions, registration_opens, registration_closes, closed_message, event_date, event_time, payment_price and the rest. Values are sanitized exactly as they are when saved from the builder, so an assistant cannot write something the builder would reject.
Fields
type must be one of the types LocalForm knows, including the ones Pro adds - see Field types. name is the key the answer is stored and sent under, and is required on every field except the static text_block, image_block and section blocks.
Keep field id values stable across saves. They are what event dates and field templates point at. If an assistant regenerates ids on every save, those references break. Omitting id on a genuinely new field is fine - one is generated.
Event dates and prices
Set pro_event_dates.question.enabled and the "which dates are you coming to?" field is built for you - radio buttons for mode: "single", checkboxes for mode: "multiple", placed at the top of the form, with its options kept in step with the dates. Do not add a field for it yourself, and leave field_id alone; it is filled in with the id of the generated field. This is the same field the Event Dates panel writes in the builder.
Prices are decimals, both here and in core's single payment_price: 6.23 is as valid as 25, and amounts are kept to two places. Don't type the key as a whole number from a payload that happens to carry a round price.
Dropping the pro_event_dates or pro_event_prices key from a saved form clears that configuration, the same as emptying the panel in the builder. Disabling the question removes its field.
See Multiple event dates and Multiple event prices for what these do on the published form.
Notes
- Forms saved this way go through the same import path as a restored backup, so the public page is created or updated automatically.
save-formreplaces the whole form. To change one thing, callget-formfirst and send back the full object - sending only the changed keys will clear the rest.