Skip to main content

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.

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 manage_options capability (an Administrator by default) - the same permission the form builder screen requires. Remote MCP clients authenticate with Application Passwords, generated per-user under Users → Profile → Application Passwords.

The adapter's default server endpoint is:

/wp-json/mcp/mcp-adapter-default-server

Check that the abilities are registered:

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":{}}}}'

On the adapter's default server the abilities are reached through its mcp-adapter-execute-ability tool, as above, rather than appearing as tools of their own.

Connecting Claude Desktop

Claude Desktop talks to local programs, not to URLs, so a small bridge runs on your machine and forwards to your site: @automattic/mcp-wordpress-remote. It needs no install - npx fetches it on first run. Node.js is the only prerequisite.

1. Create an Application Password

On your WordPress site, go to Users → Profile → Application Passwords, enter a name like Claude Desktop, and copy the generated password. It is shown once. This is not your login password, and you can revoke it from the same screen at any time.

The user you create it for must be an Administrator - the abilities require manage_options.

2. Edit the Claude Desktop config

Open Settings → Developer → Edit Config, which opens claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add a localform entry. If the file already has an mcpServers block, add the entry inside it rather than replacing it:

{
"mcpServers": {
"localform": {
"command": "npx",
"args": ["-y", "@automattic/mcp-wordpress-remote"],
"env": {
"WP_API_URL": "https://example.com/wp-json/mcp/mcp-adapter-default-server",
"OAUTH_ENABLED": "false",
"WP_API_USERNAME": "your-wordpress-username",
"WP_API_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
}
}
}
}

WP_API_URL is the full endpoint path, not just the site URL. OAUTH_ENABLED: "false" is what makes the bridge use the Application Password - leave it out and it tries to run an OAuth flow instead.

3. Restart Claude Desktop

Quit it fully (not just the window) and reopen. The localform server appears under the tools icon in the message box. Then ask for a form:

Make a registration form on my site for a two-day workshop on 12 and 13 August, ask which days people are coming, and charge €25, or €15 for students.

Claude discovers the abilities, builds the form, sets the event details and gives you back the URL of the published page.

If the tools do not appear

What you seeUsually means
The server is missing entirelyInvalid JSON in the config file - a trailing comma or a missing brace.
401 in the logsThe Application Password did not arrive. See below.
Tools appear, but is not exposed via MCPThe site is running an older LocalForm. Update it.
Missing Mcp-Session-IdSomething is calling the endpoint directly instead of through the bridge.

Claude Desktop writes per-server logs to ~/Library/Logs/Claude/ (macOS) or %APPDATA%\Claude\logs\ (Windows).

A persistent 401 with a password you know is right is almost always the host, not the config: many Apache and CGI setups drop the Authorization header before PHP sees it. Adding this to .htaccess fixes it:

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

Two other host-level causes: Application Passwords require HTTPS unless the site sets WP_ENVIRONMENT_TYPE to local or development, and security plugins such as Wordfence can disable Application Passwords or the REST API outright.

The abilities

AbilityWhat it does
localform/list-formsEvery form, with its slug, title, status and public URL.
localform/get-formOne form in full, by slug.
localform/save-formCreates a form, or replaces the one with the same slug.

All three sit in a localform ability category and require the manage_options 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": 15, "details": "with a valid card" }
]
}
}

Everything on the form settings screen is available under its own key - success_message, 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.

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-form replaces the whole form. To change one thing, call get-form first and send back the full object - sending only the changed keys will clear the rest.