mcp-wp-cli-terminus
Enables AI agents to run WP-CLI commands on WordPress sites, including byte-faithful post and meta copying with checksum verification, over local Docker, Pantheon Terminus, or SSH.
README
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.
<sub>wp-cli · wordpress · terminus · pantheon · mcp · model-context-protocol · claude · claude-code · anthropic · wordpress-migration · devops</sub>
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.
Tools
| Tool | What it does |
|---|---|
wp_init_config |
Guided setup: detects your Docker container, WordPress path, and Terminus site, then writes .serena/wp-cli.conf (asking you for anything it can't detect). |
wp_cli |
Run any WP-CLI command against a configured site — target: local (Docker) or target: production (Terminus or SSH, chosen by config). Destructive commands are guarded on production. |
wp_sync_post |
Sync a post's post_content onto the same post ID in another environment (updates an existing post; e.g. a multidev cloned from the same DB), with an md5 round-trip verification. |
wp_sync_post_meta |
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. |
wp_clone_post |
Clone a post to another environment as a NEW post — the destination assigns its own ID (returned as new_id). Use when the post doesn't exist on the destination yet. Copies fields + all meta, verifies, and reports meta keys that hold ID references for manual remapping. |
wp_block |
Surgically edit one Gutenberg/ACF block of a post — list / get / insert / replace / update-attrs / remove / move — leaving every other block byte-identical. Uses WP core parse_blocks()/serialize_blocks() (never string surgery), with a re-parse md5 verification and the same production guard. |
wp_create_post |
Create a NEW post from scratch, with the body carried content-safe (base64 in code) — no hand-quoted wp post create, no throwaway PHP file. Give content (raw markup) or blocks (specs serialized server-side); returns new_id to build up with wp_block. |
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/html
- Production 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 -v
70 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-terminus - Workflow name:
publish.yml· Environment:pypi
- Owner:
- (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
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
mcp-server-qdrant
这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器