The traps we hit on the first build, what they look like, and how the skill + setup script handle them now. Useful when something the automation doesn't cover comes up.
When you create a WordPress Application Password, you give it a memorable name like "ClaudeMCP". This is a label, not a username. The actual username for HTTP Basic auth is your WP login (admin, test, your email-derived slug, etc.).
Symptom: curl -u "ClaudeMCP:..." returns 401 Unauthorized.
Fix: curl -u "<actual-wp-username>:..." — find via GET /wp-json/wp/v2/users.
The setup script handles this by listing public users when auth fails so you can pick the right slug.
Both mcp-adapter (WordPress org) and elementor-mcp (our fork, Digitizers/elementor-mcp — a fork of msrbuilds/elementor-mcp) ship only via GitHub. The WP REST API's /wp-json/wp/v2/plugins endpoint can install plugins by slug (from wp.org) but cannot install from arbitrary zip URLs.
Workaround: Download zips from GitHub Releases and install via WP-CLI (Local) or upload via WP Admin (live).
GitHub's auto-generated source zipballs (used when a release has no asset zip) wrap the code in a directory like Digitizers-elementor-mcp-<sha>/. WordPress uses this folder as the plugin slug — leading to a confused activation and broken paths.
Fix: Unzip, rename the folder to elementor-mcp/, re-zip. The setup script does this automatically.
The site is fully functional via HTTP, but WP-CLI invoked from a regular shell fails:
Error establishing a database connection.
Fix: Find the socket via:
find ~/Library/Application\ Support/Local/run -name 'mysqld.sock'Then call PHP with both socket overrides:
php -d mysqli.default_socket=$SOCK -d pdo_mysql.default_socket=$SOCK <wp-cli> <command>The setup script auto-discovers the right socket by trying each one until wp core version succeeds.
System-installed PHP/WP-CLI may not have the MySQL extensions Local's stack uses. The setup script uses Local's bundled binaries:
- PHP:
~/Library/Application Support/Local/lightning-services/php-*/bin/darwin-arm64/bin/php - WP-CLI:
/Applications/Local.app/Contents/Resources/extraResources/bin/wp-cli/posix/wp
Writing the file mid-session does nothing. You must quit and reopen Claude Code in the project directory.
It tries to return null for elementor_pro_version but the schema declares string. Calling it returns a validation error. Use list-pages as the smoke-test for whether the MCP is wired correctly.
// ✓ Correct
add-heading({
post_id: 11,
parent_id: "abc",
title: "...",
title_color: "#171615",
typography_font_family: "Cormorant Garamond"
})
// ✗ Wrong — error: "title is a required property of input"
add-heading({
post_id: 11,
parent_id: "abc",
settings: { title: "...", title_color: "#171615" }
})The exception is add-container, which takes settings: {} because containers have so many properties. Don't generalize from one to the other.
Without this flag, the other typography_* keys are silently ignored. Same for css_filters_css_filter: "custom" for image filters. This pattern is how Elementor distinguishes "use defaults" from "I'm explicitly setting this."
The schema confirms (and issue #32 was a bug about this) that the right keys are:
flex_direction—row|column|row-reverse|column-reverseflex_justify_content—flex-start|center|flex-end|space-between|space-around|space-evenlyflex_align_items—flex-start|center|flex-end|stretchflex_gap—{column, row, isLinked, unit}flex_wrap—nowrap|wrap
Note the flex_ prefix on justify_content and align_items — without it, the keys are dropped.
Setting background_color: "#F4F1EA" alone does nothing. You must first set:
background_background: "classic"
Then background_color, background_image, etc. apply. Same for background_overlay_background: "classic" before any background_overlay_* keys.
Schema quirk:
background_overlay_opacity: { unit: "px", size: 0.45 } // 0–1 rangeThe px unit doesn't mean pixels — it's just what the schema declares. The numeric range is 0–1.
Elementor's Heading widget renders inline HTML in the title field. So:
title: "where estates <em>are entrusted</em>"Cormorant Garamond's italic auto-loads if the font is set. If italics fail to render, check that Site Settings → Global Fonts → Primary has an italic variant available (Cormorant Garamond from Google Fonts ships italic by default).
padding: {
unit: "px",
top: "200",
right: "56",
bottom: "200",
left: "56",
isLinked: false
}isLinked: true means top=right=bottom=left (Elementor's "linked" UI control). Use false whenever any side differs.
This is the single biggest productivity lever.
- It's a one-off heading, image, button, text block
- The user will edit copy in Elementor's visual editor
- The widget maps 1:1 to the design (Heading for headings, Image for images, Button for buttons)
- Card grid (4+ identical cards) — saves dozens of widget calls and keeps spacing consistent
- Content inside Tabs/Accordion —
add-tabsonly takestab_contentas HTML strings, so any rich card grid inside a tab must be HTML - Complex hover effects — image scale on hover inside a parent
<a>, animated underline expanding on hover, gradient overlay reveal — these aren't exposed by widget controls - CSS pseudo-elements (
::before,::after) for decorative dividers, arrows, frames - CSS Grid layouts with media queries — Elementor's built-in container is flex-only on the free tier
- Scoped style block styling many child elements consistently (form fields, listing cards, neighborhood tiles)
When you need to style a native widget from an HTML widget elsewhere, scope styles to the parent Elementor element ID:
<style>
.elementor-element-f8d1545 .elementor-tab-title {
text-transform: uppercase !important;
letter-spacing: .26em !important;
}
.elementor-element-f8d1545 .elementor-tab-title.elementor-active {
border-bottom-color: #171615 !important;
}
</style>The f8d1545 is the element_id returned when the widget was created. Always grab and remember these IDs — they're the only stable selector.
If Pro is active: the add-form MCP tool builds a real, submitting Form
widget natively — no Fluent Forms, no shortcode, fully editable in Elementor. The
kit auto-detects Pro and the skill routes here. Confirm field/action keys via
get-widget-schema({ widget_type: "form" }) before building.
If free (fallback): Elementor's Form widget is Pro and add-form isn't
exposed. The free path:
- Build the form in Fluent Forms (or WPForms / CF7) — these have free plans
- Get the shortcode (e.g.,
[fluentform id="1"]) - Drop it via Elementor's
add-shortcodewidget
If you're at the visual-build stage and forms aren't wired yet, an HTML <form> with a JS-alert handler is fine as a placeholder. Flag it explicitly to the user as "form is visual only — doesn't capture submissions yet."
If Pro is active: use the native Theme Builder via create-theme-template
(header / footer / single / archive) + the native Nav Menu widget — no
UAE/HFE plugin. The kit detects Pro and the skill routes here.
If free (fallback): Theme Builder is Pro. The free path uses Header Footer Elementor (HFE) by Brainstorm Force or UAE:
- Install + activate HFE
- WP Admin → Header Footer Builder → Add New → set Type: Header (or Footer), Display On: Entire Website
- Edit with Elementor — the MCP can edit this template the same way it edits any page (find its post_id via
list-pages({post_type: "elementor-hf"}))
For a transparent header that swaps to solid on scroll: with Pro, set the
header Container's Sticky (Top) + a sticky-state background, and use Motion
Effects natively. Free workaround is either keep it solid throughout, or
hand-write a small CSS snippet via Customizer → Additional CSS.
- Install plugins or themes, including Elementor Pro itself (paid, not on wp.org — the kit only detects it; use WP-CLI or WP Admin)
- Set the static front page (use
wp option update show_on_front page; wp option update page_on_front <id>) - Pixel-perfect HTML→Elementor translation — Elementor's flexbox container model is the ceiling (Pro adds CSS Grid containers)
- Auto-fix broken layouts — you read the rendered output and emit corrective
update-elementcalls
Pro features (Theme Builder, Loop Grid, Form widget, Sticky/Motion, Popups, Dynamic Tags) are driven natively when Pro is active — they're auto-detected. They're only unavailable on the free tier, where the workarounds above apply.
get-page-structure({post_id})— confirms what's nested in whatget-element-settings({element_id})— shows the actual settings written to the DBcurl <site-url>and grep for your custom classes — confirms the page is actually rendering your widgets- View source in browser — Elementor wraps every widget in
.elementor-element.elementor-element-<id>so you can find any element by its ID
- elementor-mcp source (fork of msrbuilds/elementor-mcp) —
includes/abilities/class-*-abilities.phpfiles are the ground truth on each tool's behavior - WordPress MCP Adapter — explains the JSON-RPC plumbing and auth options
- Elementor's
_elementor_datapost meta format — every page is one giant nested JSON tree in this meta key. The MCP reads/writes this directly. - The container schema dump —
get-container-schema()returns ~50KB of JSON Schema. Read once at session start; bookmark the keys.