mcp-wp-cli-terminus
The mcp-wp-cli-terminus server lets AI agents manage WordPress sites via WP-CLI across local Docker, Pantheon Terminus, and SSH environments — with checksum-verified, byte-faithful content transfers that never route through the AI model's text.
Configure WordPress sites (wp_init_config): Guided setup that auto-detects your Docker container, WordPress path, and Pantheon Terminus site, then writes a config file. Supports named multi-site [site:NAME] config sections.
Run any WP-CLI command (wp_cli): Execute any WP-CLI command against local or remote environments, with a production guard that blocks destructive operations unless confirm: true is explicitly passed.
Sync post content (wp_sync_post): Copy post_content between the same post ID across environments with MD5 round-trip verification. Supports block-level filtering (only_blocks/except_blocks) to preserve specific blocks on the destination.
Sync post meta (wp_sync_post_meta): Transfer complete custom fields (including serialized arrays, ACF repeaters, multiple values per key) to the same post ID in another environment, with checksum verification and support for full mirror or selective key syncing.
Clone post to new ID (wp_clone_post): Copy a post to a destination as a brand-new post with a new ID, including all fields and meta, with integrity verification and reporting of meta keys that may contain ID references needing manual remapping.
Surgical block editing (wp_block): Operate on individual Gutenberg/ACF blocks without touching the rest of the post. Supported operations: list, get, insert, replace, update-attrs, remove, move — all with re-parse MD5 verification and a preview mode to inspect changes before writing.
Create posts from scratch (wp_create_post): Author new posts from raw markup or structured block specs (serialized server-side), returning the new post ID for further compositional editing.
Key safety & correctness features:
Content is base64-encoded in code — never passed through the AI model's text
MD5/checksum verification on every sync, clone, and block write
Large payload support via STDIN (
eval-file -) to avoid LinuxMAX_ARG_STRLENlimitsDebug logging of failures only, with SSH strings redacted
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-wp-cli-terminuscopy post 42 from local to production"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-wp-cli-terminus
An MCP (Model Context Protocol) server that lets Claude and other AI agents run WP-CLI against WordPress — over local Docker, Pantheon Terminus, or SSH — and byte-faithfully copy posts and post meta between environments with checksum verification.
Built for developers using Claude Code / Claude Desktop (or any MCP client) to operate WordPress sites — including Pantheon multidevs reached through Terminus — without hand-assembling fragile wp eval commands.
wp-cli · wordpress · terminus · pantheon · mcp · model-context-protocol · claude · claude-code · anthropic · wordpress-migration · devops
Why
Moving a WordPress post's body or custom fields between environments (e.g. pushing a block-based front page from local to a Pantheon multidev) is deceptively hard to do correctly:
wp post update --post_contenttruncates at newlines (wp-cli#2712).Piping content over STDIN hangs on
terminus remote:wp(terminus#1615).Hand-pasting base64 into an agent prompt is lossy — a single flipped byte silently corrupts production.
Large payloads passed as one shell argument hit the Linux
MAX_ARG_STRLEN(131072 bytes) limit and fail withE2BIG.Post meta with serialized arrays / multiple values per key is easy to corrupt by re-serializing.
This server solves all of that: content is read, base64-encoded in code, delivered over a transport-safe path, and then re-read and checksum-compared to the source. A mismatch is reported, never silently trusted.
Related MCP server: wp-cli-mcp
Tools
Tool | What it does |
| Guided setup: detects your Docker container, WordPress path, and Terminus site, then writes |
| Run any WP-CLI command against a configured site — |
| Sync a post's |
| Sync a post's complete meta (serialized arrays, multiple values per key, ACF repeaters) onto the same post ID in another environment, with a canonical checksum verification. All keys (full mirror) or an allow-list. |
| Clone a post to another environment as a NEW post — the destination assigns its own ID (returned as |
| Surgically edit one Gutenberg/ACF block of a post — |
| Create a NEW post from scratch, with the body carried content-safe (base64 in code) — no hand-quoted |
Sync vs. clone vs. block
Block (
wp_block) is the single-block primitive: change, read, add, or reorder one block of a post without touching the rest. Reach for it instead of a whole-bodywp_sync_post(which overwrites every block) or hand-writtenstr_replace/evalsurgery onpost_content(which corrupts self-closing ACF blocks and inner blocks). Selectors:name:<blockName>(first of type),name:<blockName>#<N>(Nth, 0-based),anchor:<anchor>,index:<N>.Sync (
wp_sync_post,wp_sync_post_meta) updates an existing post that shares the same ID on both sides — the right tool when the environments were cloned from the same database (a Pantheon multidev, a staging copy). It fails if the destination ID doesn't exist.Clone (
wp_clone_post) creates a new post on the destination from a source post; the destination assigns a fresh ID. Use it when the content is new to the destination (e.g. pushing a locally-authored post/alert up to a multidev). Meta values that look like ID references (_thumbnail_id, ACF relationship/image fields) are copied verbatim and reported — never silently remapped across databases.
Correctness guarantees
Never routes content through the model's text. Payloads are read into the server and base64-encoded in code.
Checksum-verified. Every sync/clone re-reads the destination and compares it to the transferred source;
verified: false+ an error on any mismatch.Transport-agnostic. The same logic runs over local Docker, Pantheon Terminus, and WP-CLI
--ssh.Large payloads. Docker/SSH deliver PHP over STDIN (
wp eval-file -, exempt from the argv size limit); Terminus uses a size-guarded argv path and fails loud rather than emitting a rawE2BIG.Meta fidelity. Values are round-tripped so WordPress's own
maybe_serialize()reproduces the exact storedmeta_value— arrays stay arrays, and strings that merely look serialized stay strings.Production guard. Writes to a production destination require
confirm: truewhenPROD_GUARDis enabled.MCP-client tolerant. Some MCP clients (e.g. Claude Code) serialize object/array/number/boolean tool arguments as JSON strings before sending them (claude-code#5504, #24599). The tools coerce such stringified arguments back to their native types, so a
block/blocks/data/fieldsobject delivered as a string still works — while a genuine raw-markup string is never mis-parsed.
Install & run
The server is pure Python (stdlib only, zero dependencies).
With uvx (recommended — no install)
// Claude Desktop / Claude Code MCP config
{
"mcpServers": {
"wp-cli": {
"command": "uvx",
"args": ["mcp-wp-cli-terminus"]
}
}
}With pip
pip install mcp-wp-cli-terminus{
"mcpServers": {
"wp-cli": { "command": "mcp-wp-cli-terminus" }
}
}From source
git clone https://github.com/EarthmanWeb/mcp-wp-cli-terminus
cd mcp-wp-cli-terminus
python -m wp_cli_mcp # PYTHONPATH=src, or `pip install -e .`Configure
Guided setup with wp_init_config (recommended)
The easiest way to create the config is to ask your MCP client to run the wp_init_config tool. It works in two phases:
Detect — called with no arguments, it probes the environment (running Docker containers, the WordPress path inside them, and any authenticated Pantheon Terminus site) and reports what it found plus a list of anything it couldn't determine.
Write — the agent asks you for whatever was missing, then calls it again with
write=trueto save<project-root>/.serena/wp-cli.conf.
Just tell your agent: "set up the wp-cli config for this project" — it will call wp_init_config, fill in what it can, ask you for the rest, and write the file (it won't overwrite an existing config unless you say so).
Manual setup
The server reads <project-root>/.serena/wp-cli.conf at runtime (set CLAUDE_PROJECT_DIR to point at your project). Copy wp-cli.conf.example and edit:
DEFAULT_SITE=example-site
PROD_GUARD=true
[site:example-site]
LOCAL_CONTAINER=my-container # docker container running WP-CLI
LOCAL_PATH=/var/www/html # WordPress path inside the container
TERMINUS_SITE=example # Pantheon site — production routes over Terminus
TERMINUS_ENV=dev # default env (override per call)
# — or, for a non-Pantheon remote, omit TERMINUS_* and set:
# REMOTE_SSH=deploy@example.com:22/var/www/htmlProduction transport is chosen by config:
TERMINUS_SITE→terminus remote:wp; otherwiseREMOTE_SSH→ WP-CLI--ssh.Multi-site: add more
[site:NAME]sections and passsiteper call.
Never commit .serena/wp-cli.conf — it may contain hostnames/SSH strings. The shipped .gitignore excludes it.
Usage examples
// Run a WP-CLI command locally
{ "tool": "wp_cli", "args": "plugin list --status=active --format=json" }
// Run against production (Terminus or SSH per config)
{ "tool": "wp_cli", "args": "option get siteurl", "target": "production" }
// SYNC a front page's block markup onto the SAME post ID on production, verified
{ "tool": "wp_sync_post", "post_id": 42, "from": "local", "to": "production", "confirm": true }
// SYNC ALL meta for a post (full mirror) onto the same ID, verified
{ "tool": "wp_sync_post_meta", "post_id": 42, "from": "local", "to": "production", "confirm": true }
// SYNC only specific meta keys
{ "tool": "wp_sync_post_meta", "post_id": 42, "from": "local", "to": "production",
"keys": ["_thumbnail_id", "my_field"], "confirm": true }
// CLONE a locally-authored post to production as a NEW post (returns new_id)
{ "tool": "wp_clone_post", "post_id": 268529, "from": "local", "to": "production", "confirm": true }
// Clone but force the new post to draft
{ "tool": "wp_clone_post", "post_id": 268529, "from": "local", "to": "production",
"overrides": { "post_status": "draft" }, "confirm": true }
// LIST a post's top-level blocks (index, blockName, anchor, ACF data keys)
{ "tool": "wp_block", "op": "list", "post_id": 268483 }
// GET one block's parsed attrs + exact markup
{ "tool": "wp_block", "op": "get", "post_id": 268483, "selector": "name:acf/sps-hero-slideshow-block" }
// INSERT a feature-cards block just before the celebrations block (ACF data form)
{ "tool": "wp_block", "op": "insert", "post_id": 268483,
"position": "before:name:acf/sps-celebrations-block",
"block": { "name": "acf/sps-feature-cards-block", "data": { "cards": [268486, 268388, 268364] } } }
// UPDATE-ATTRS: switch the celebrations block to tag mode (merges into attrs.data)
{ "tool": "wp_block", "op": "update-attrs", "post_id": 268483,
"selector": "name:acf/sps-celebrations-block",
"data": { "source": "tag", "tag": 436, "posts_per_page": 10 } }
// REPLACE the 2nd paragraph with raw markup; MOVE / REMOVE by selector
{ "tool": "wp_block", "op": "replace", "post_id": 42, "selector": "name:core/paragraph#1",
"block": "<!-- wp:paragraph --><p>New copy</p><!-- /wp:paragraph -->" }
{ "tool": "wp_block", "op": "move", "post_id": 42, "selector": "anchor:cta", "position": "first" }
{ "tool": "wp_block", "op": "remove", "post_id": 42, "selector": "index:3" }
// PREVIEW a change without writing (returns the intended new_content_b64 + new_md5)
{ "tool": "wp_block", "op": "remove", "post_id": 42, "selector": "index:3", "preview": true }
// SYNC a post's body to production but PRESERVE the destination's hand-built slideshow
{ "tool": "wp_sync_post", "post_id": 268483, "from": "local", "to": "production",
"except_blocks": ["acf/sps-hero-slideshow-block"], "confirm": true }
// CREATE a new page from block specs in ONE call (no file, no arg-quoting) -> new_id
{ "tool": "wp_create_post", "title": "Landing", "post_type": "page", "status": "draft",
"blocks": [
{ "name": "acf/sps-feature-cards-block", "data": { "cards": [268486, 268388] } },
"<!-- wp:paragraph --><p>Intro copy.</p><!-- /wp:paragraph -->"
] }
// CREATE from raw markup, then keep building with wp_block on the returned new_id
{ "tool": "wp_create_post", "title": "Draft", "content": "<!-- wp:heading --><h2>Hi</h2><!-- /wp:heading -->" }
{ "tool": "wp_block", "op": "insert", "post_id": /* new_id */ 0, "position": "last",
"block": { "name": "acf/sps-celebrations-block", "data": { "source": "tag", "tag": 436 } } }wp_sync_* return verified: true/false with src_md5 / dst_md5, the delivery mode, and per-side transport. wp_clone_post returns new_id, verified, content_verified, meta_verified, and id_reference_keys (meta keys to review). wp_block read ops return the block list / one block's attrs+markup; write ops return wrote, verified (re-parse md5 round-trip), before_count/after_count, and target_index — and, when blocked by the production guard or preview: true, the intended new_content_b64 + new_md5 instead of writing. A block-filtered wp_sync_post (only_blocks/except_blocks) returns blocks_carried, intended_md5/reread_md5, and the filter applied.
Debug logging
Failures (non-zero WP-CLI exits) are appended to a log in your system temp dir — failures only, successes are never logged:
Location:
${TMPDIR}/wp-cli-mcp/failures.log(override withWP_CLI_MCP_LOG_DIR).Disable entirely with
WP_CLI_MCP_LOG=0.SSH connection strings are redacted in the log.
Requirements
Python 3.8+
WP-CLI reachable via one of: a local Docker container (
docker exec), Pantheon Terminus on the host, or a host WP-CLI with--ssh.
Tests
python -m unittest discover -s tests -v70 stdlib-only unit tests (split by area under tests/) cover config parsing, transport selection (local/Terminus/SSH), the argv size guard, newline handling, PHP-key safety, id-reference detection, and the full sync/clone/verify orchestration via an injectable command-runner seam (no real WP-CLI invoked).
Releasing
Releases publish to PyPI automatically via GitHub Actions (.github/workflows/publish.yml) using PyPI Trusted Publishing — no API token is stored in the repo. The workflow builds, runs the tests, and uploads on every published GitHub Release.
One-time setup (per project, on PyPI):
On PyPI, open the project → Publishing → Add a new publisher → GitHub, with:
Owner:
EarthmanWeb· Repository:mcp-wp-cli-terminusWorkflow name:
publish.yml· Environment:pypi
(Optional) In GitHub repo Settings → Environments, create an environment named
pypito gate/approve publishes.
Cut a release (this triggers the publish):
# 1. Bump the version in pyproject.toml (e.g. 0.1.0 -> 0.1.1), commit, push.
# 2. Tag + create the GitHub Release — the workflow does the rest:
gh release create v0.1.1 --title "v0.1.1" --notes "What changed"The action then builds, tests, and publishes mcp-wp-cli-terminus to PyPI. Within ~a minute uvx mcp-wp-cli-terminus (and the SWE plugin launcher) pick up the new version. You can also run it manually from the Actions tab (workflow_dispatch).
First release was published manually with
uv build && uv publish; subsequent releases use the workflow above.
License
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/EarthmanWeb/mcp-wp-cli-terminus'
If you have feedback or need assistance with the MCP directory API, please join our Discord server