MCP server for WP-CLI over local Docker, Pantheon Terminus, or SSH — run WP-CLI and byte-faithfully copy posts/meta between WordPress environments, with checksum verification.
Project description
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.
Tools
| Tool | What it does |
|---|---|
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_copy_post |
Byte-faithfully copy a post's post_content from one environment to another, with an md5 round-trip verification. |
wp_copy_post_meta |
Byte-faithfully copy a post's complete meta (all custom fields — serialized arrays, multiple values per key, ACF repeaters) between environments, with a canonical checksum verification. Copy all keys (full mirror) or an allow-list. |
Correctness guarantees
- Never routes content through the model's text. Payloads are read into the server and base64-encoded in code.
- Checksum-verified. Every copy 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.
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" }
// Copy a front page's block markup from local to production, verified
{ "tool": "wp_copy_post", "post_id": 42, "from": "local", "to": "production", "confirm": true }
// Copy ALL meta for a post (full mirror), verified
{ "tool": "wp_copy_post_meta", "post_id": 42, "from": "local", "to": "production", "confirm": true }
// Copy only specific meta keys
{ "tool": "wp_copy_post_meta", "post_id": 42, "from": "local", "to": "production",
"keys": ["_thumbnail_id", "my_field"], "confirm": true }
Each copy returns verified: true/false with src_md5 / dst_md5, the delivery mode, and per-side transport.
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
54 stdlib-only unit tests cover config parsing, transport selection (local/Terminus/SSH), the argv size guard, newline handling, PHP-key safety, and the full copy/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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_wp_cli_terminus-0.2.0.tar.gz.
File metadata
- Download URL: mcp_wp_cli_terminus-0.2.0.tar.gz
- Upload date:
- Size: 30.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6dad3706add72673213086457798dfc6442564731d7af1921f98c6f9365e4b54
|
|
| MD5 |
4da220dec8797eb0d0ea7a44c49de8d2
|
|
| BLAKE2b-256 |
f3964cbb175eba75657f9043e75014a508fe8ff787d8f6ee9a2089824d7d8ef8
|
Provenance
The following attestation bundles were made for mcp_wp_cli_terminus-0.2.0.tar.gz:
Publisher:
publish.yml on EarthmanWeb/mcp-wp-cli-terminus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_wp_cli_terminus-0.2.0.tar.gz -
Subject digest:
6dad3706add72673213086457798dfc6442564731d7af1921f98c6f9365e4b54 - Sigstore transparency entry: 2171598720
- Sigstore integration time:
-
Permalink:
EarthmanWeb/mcp-wp-cli-terminus@8aa0e776413f8ef765253eb1bc8a69db306c63a2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/EarthmanWeb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8aa0e776413f8ef765253eb1bc8a69db306c63a2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mcp_wp_cli_terminus-0.2.0-py3-none-any.whl.
File metadata
- Download URL: mcp_wp_cli_terminus-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66188b470d7cc50d1ebf50ac3f8c0ad429c6ff09f9f1ca000f69a1ac67691a13
|
|
| MD5 |
643219cbf53bf8f045c2bc35a9defbf9
|
|
| BLAKE2b-256 |
c078013341e42f525a8091459c7a894062e310075a3572f509503e4bb68f72fe
|
Provenance
The following attestation bundles were made for mcp_wp_cli_terminus-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on EarthmanWeb/mcp-wp-cli-terminus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_wp_cli_terminus-0.2.0-py3-none-any.whl -
Subject digest:
66188b470d7cc50d1ebf50ac3f8c0ad429c6ff09f9f1ca000f69a1ac67691a13 - Sigstore transparency entry: 2171598731
- Sigstore integration time:
-
Permalink:
EarthmanWeb/mcp-wp-cli-terminus@8aa0e776413f8ef765253eb1bc8a69db306c63a2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/EarthmanWeb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8aa0e776413f8ef765253eb1bc8a69db306c63a2 -
Trigger Event:
release
-
Statement type: