I shipped elementor-mcp-agent v1.0 today — an open-source Model Context Protocol server that lets Claude (and any MCP client) drive WordPress Elementor across many client sites. It’s MIT, on npm as elementor-mcp-agent, listed in the official MCP Registry.
Repo: github.com/Mogacode-ma/elementor-mcp-agent
What I want to write about isn’t the architecture — it’s the 7 bugs I caught only because I forced myself to test end-to-end against a live WordPress install before publishing. Three of them would have silently corrupted data on a client site in production. The others would have just looked broken. All of them would have been a customer-support nightmare.
This post is the bug list, with context. If you’re building an MCP server (or any automation against an opinionated SaaS), these patterns apply.
The setup
I run a small agency. We manage ~50 WordPress + Elementor Pro sites for clients across Belgium, France, Luxembourg and Morocco. Day-to-day operations are predictable: text updates, image swaps, template synchronization, plugin updates, backups. Cumulatively painful.
MCP felt like the right shape for it: type-checked tools, explicit side effects, confirmation dance for destructive ops. I built 24 tools across 7 categories (sites, pages, widgets, templates, WP-CLI, screenshots, fleet versions).
My v0.1 passed npm run build, npm test, ESLint, tsc --noEmit. I almost published it without touching a real Elementor install.
I’m glad I didn’t.
Bug #1 — WordPress REST API silently drops writes to unregistered postmeta keys
The symptom: I implemented automated backups by writing the current _elementor_data to a timestamped postmeta key (_elementor_data_backup_2026-05-22T04-35-37-847Z) via PUT /wp-json/wp/v2/pages/{id}. The API returned 200 OK. My code marked the backup as successful. The subsequent elementor_find_replace applied with confidence.
When I tried to verify via wp post meta list 8 | grep backup, the result was empty.
The root cause: WordPress REST API requires register_meta() with show_in_rest: true for a postmeta key to be writable through REST. Plugins like Elementor register their canonical keys (_elementor_data, _elementor_page_settings). Custom keys you invent at runtime are silently dropped by REST.
200 OK. No warning. No error.
The fix: switched backup to WP-CLI via SSH (always works). Local JSON file as fallback when SSH isn’t configured. REST is no longer trusted for non-canonical writes.
The lesson: any REST API that requires schema registration for meta-like fields will silently swallow your writes when the schema isn’t there. Test that the value actually persists. Don’t trust the response code.
Bug #2 — wp binary not in non-interactive SSH PATH
After fixing #1, I tried again. New error:
/bin/bash: line 1: wp: command not found
I’d been testing in my local terminal where wp was aliased. On managed WordPress hosts (Infomaniak, in this case), wp-cli is usually installed at ~/bin/wp.phar and invoked as php ~/bin/wp.phar. The non-interactive shell that ssh user@host "command" uses has a stripped-down PATH that doesn’t include ~/bin.
The fix: auto-detection at first SSH connection — probe command -v wp, fall back to ~/bin/wp.phar, then ~/wp-cli.phar. Cache per-site. Allow explicit override via ssh.wp_cli_path config field.
The lesson: non-interactive SSH sessions have a different PATH than interactive ones. If your tool spawns remote commands, never assume a binary is in PATH. Probe or accept an explicit path.
Bug #3 — SSH banner pollutes stderr and corrupts output parsing
OpenSSH recently started printing a warning on connections that don’t use a post-quantum key exchange algorithm:
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
It goes to stderr. My code captured stderr and concatenated it into the error message. So a successful wp post meta get call would still surface this warning as if it were an error.
The fix: filter known-benign stderr lines after capture. Whitelist by substring (post-quantum, openssh.com/pq, decrypt later, etc.) before deciding the operation failed.
The lesson: stderr is not synonymous with “error.” Tools that pipe stderr into error-handling paths need to filter benign banners.
Bug #4 — “Default Kit” returned as a widget in template_type=widget filters
The query /wp-json/wp/v2/elementor_library?meta_key=_elementor_template_type&meta_value=widget was supposed to return the global widgets on a site. It returned the Default Kit (Elementor’s site-wide design tokens) which has _elementor_template_type=kit, not widget.
The root cause: WordPress REST meta_value filtering is unreliable for meta keys that aren’t register_meta-declared. In practice, it falls back to a meta_key presence check, ignoring the value.
The fix: fetch all elementor_library entries (no meta_value filter), then filter client-side on meta._elementor_template_type === 'widget'.
The lesson: don’t trust server-side filters on unregistered meta. Always validate the response structure matches your intent.
Bug #5 — _elementor_page_settings is an object via REST, a string via WP-CLI
When I added duplicate_elementor_page, the copy path did:
read source → copy data + page_settings → write to new page via REST PUT
It failed with:
"Le paramètre meta._elementor_page_settings n'est pas de type object."
The cause: GET /wp-json/wp/v2/pages/{id}?context=edit returns meta._elementor_page_settings as a parsed object (Elementor registers it that way). wp post meta get returns it as a serialised JSON string. My code typed it as string everywhere, so when REST returned an object and I passed it back via REST PUT (expecting JSON string serialization in my templating), the API rejected the type mismatch.
The fix: normalize on read. If you got a string, JSON.parse() it before REST writes. If you got an object, JSON.stringify() it before WP-CLI writes. Don’t assume one transport’s representation matches the other.
The lesson: when two transports read/write the same field with different serialization rules, normalize at the boundary, not in the middle.
Bug #6 — Headless Chrome cold-start exceeds 30s
I built a screenshot_page tool using chrome --headless --screenshot=... via child_process.spawn. Locally it was instant. On the first call after a long idle period, it timed out at 30s. Chrome was downloading codec libraries or whatever it does on first-launch in headless mode.
The fix: bump the default timeout from 30s to 60s. Document that this is a one-time cost per Chrome cold start.
The lesson: cold-start latency on local tools is real and often invisible during dev. Set timeouts based on worst-case, not happy-path.
Bug #7 — Templates listing had the same filter bug as globals (Bug #4)
I’d written the templates listing code right after the globals code, and copy-pasted the same broken meta filter pattern. Caught it when I tested list_elementor_templates with type='widget' and got back the Default Kit again.
The fix: same client-side filter pattern.
The lesson: a bug in one place is a bug in N places where the same pattern is reused. Search the codebase for the smell before declaring the bug fixed.
What I’d take from this for any MCP build
-
End-to-end test against a live target before publishing — always. Unit tests + lint + typecheck told me v0.1 was “ready.” End-to-end told me 7 things were broken.
-
Don’t trust REST silent acceptance. 200 OK means “I received your request,” not “I persisted your change.” Verify post-write that the value actually exists.
-
Filter at the boundary, not in the middle. Type mismatches between transports (REST vs CLI, JSON vs YAML, etc.) are easier to handle at the read/write edge than to track through your domain code.
-
stderr ≠ error. Don’t conflate them.
-
PATH is not what you think it is in non-interactive shells. Probe or accept explicit paths.
-
Cold starts are real. Set timeouts based on worst case.
-
Update the skill / playbook that taught you the safe patterns. I patched the upstream WordPress-Elementor skill on GitHub with the three REST quirks I’d surfaced, so the next person doesn’t have to learn them the hard way (PR #92 on jezweb/claude-skills).
Try it
npx -y elementor-mcp-agent
You’ll need:
- A WordPress site with Elementor installed
- An Application Password for an admin user
- Optionally SSH access for WP-CLI tools
Config snippet for Claude Desktop / Code in the README.
The MCP is on the official registry: search “elementor-mcp-agent” in your favourite MCP-aware client.
If you hit a bug, open an issue with the exact tool call + response (sanitize tokens). I’ll usually patch within a day. Battle-tested against an agency fleet — but every WordPress install is its own snowflake, so PRs and bug reports welcome.
⭐ if it saved you time.
Built by MogaCode in Essaouira.