wp-ops: One CLI and One MCP Server for WordPress DevOps
Every WordPress shop that manages its own servers ends up with the same pile: a database backup script here, a malware scanner there, an Ansible playbook in a project that has since been archived, an Nginx log parser someone wrote during an incident and never documented. The scripts work. Finding them, remembering their arguments, and knowing which of them will run against a plain cPanel install versus a Trellis server — that is the part that doesn’t scale. WordPress DevOps is rarely one hard problem; it is fifty small ones with no shared front door.
wp-ops is our answer to that: a WordPress DevOps CLI and MCP server in one Go binary. It carries 74 WordPress operations commands — backups, deployments, malware scans, log monitoring, SEO audits, image conversion, release automation — plus an MCP server that exposes fourteen of those same operations as tools an AI assistant can call directly. Same operations, two front doors: a terminal for a person, an MCP endpoint for an agent.
Quick Summary: wp-ops is an open-source Go CLI plus MCP server for WordPress operations, built around Trellis and Bedrock but usable on any WordPress install. Commands describe themselves through manifest comments, so the catalog, help text, argument prompts, and shell completions are all generated rather than maintained by hand. The MCP server makes fourteen of those operations callable by Claude Code, Mistral Vibe, or Codex CLI, with write operations gated behind explicit confirmation and the highest-blast-radius ones deliberately left unimplemented. In practice it means site builds, client maintenance, and server monitoring all run from one place — with or without an assistant driving.
What This Post Covers
- The problem: ops knowledge that lives in ten repositories
- One binary, two interfaces
- Commands that describe themselves
- Grouped by domain, filtered by platform
- The WordPress MCP server: the same operations, callable by an agent
- Safety as a design feature, not a prompt instruction
- What changes day to day: build, maintain, monitor
- What your host already does — and where it stops
- Client server DevOps without a DevOps team
- Editor-agnostic by design
- What we would keep if we started over
- Try it
- Frequently asked questions
The Problem: Ops Knowledge That Lives in Ten Repositories
Running WordPress sites on your own infrastructure is not hard because any single task is hard. It is hard because the tasks are many, they are irregular, and each one carries a small amount of context you only need twice a year.
Pulling a production database into local development is four steps, and the third one — the URL search-replace — is the one that quietly breaks a site if you skip it. Scanning for malware means knowing which of two scanners to run and how to get it onto a server without leaving a PHP file behind. Checking whether an Nginx access log shows a real traffic spike or a bot swarm means remembering a specific awk incantation. None of this is difficult. All of it is forgettable.
The failure mode isn’t dramatic. It is that the scan doesn’t get run, the backup is a week older than you thought, and the log that would have shown the attack starting three days ago is rotated away before anyone looks at it. Maintenance work that is annoying to start is maintenance work that doesn’t happen.
One Binary, Two Interfaces
Worth stating plainly: this is not a product we built to sell. It is the tooling we run our own managed hosting on, opened up because there was no reason not to. That shapes all of it — the Trellis bias, the order things got automated in, and safety rules strict enough that we trust it against client servers. Trellis users get the most out of it; everything that generalized cleanly is available to any WordPress install.
wp-ops started in April 2025 as a documentation repository with a growing collection of Bash scripts and Ansible playbooks, wrapped in a Bash CLI. That wrapper grew to roughly 2,400 lines before it was replaced by a Go implementation, which became the only CLI in version 4.0.0. The current release is 5.4.0.
Going to Go bought three things that mattered more than the language itself. Homebrew installs a prebuilt binary, so there is no toolchain to set up on a new machine. The scripts, playbooks, and documentation are embedded in the binary, so once it is on your PATH the clone can be deleted. And a compiled catalog means wp-ops search and tab completion are instant rather than a shell loop over a directory tree.
brew install imagewize/tap/wp-ops
wp-ops init # install shell completions, one time
wp-ops # interactive picker
wp-ops search webp # find a command by name or description
wp-ops docs oom # search the guides, not just the commands
wp-ops doctor # check dependencies and environment
The second interface is an MCP server. Model Context Protocol is the standard that lets an AI assistant call external tools, and the wp-ops MCP server wraps the same underlying operations the CLI runs. That is the important design constraint: the MCP tools are not a separate implementation with its own bugs. They call the same scanner, the same rsync, the same WP-CLI dispatch.
Which one you reach for depends on whether you already know what you want to run:
| CLI | MCP server | |
|---|---|---|
| Best for | A known task you can name | A question you want answered |
| Coverage | All 74 commands | 14 tools |
| Invocation | You type it | An AI assistant calls it |
| Discovery | wp-ops search, picker, completions | Tool descriptions in the assistant’s context |
| Write safety | Your own judgment | Enforced confirmation gates |
| Needs an AI client | No | Yes |
Commands That Describe Themselves
The part of the design we would defend hardest is the manifest. Every script carries structured comments in its header, and the CLI parses them into a catalog at build time.
# @desc Run traffic, security, AI-bot, and error monitoring together and save timestamped reports
# @category monitoring
# @platform trellis
# @runs server
# @requires gawk
# @arg hours optional {24} How many hours back to analyze
# @arg domain optional {example.com} Site domain
# @example ssh web@example.com 'bash -s' < monitor.sh
From those seven lines the CLI generates the command’s entry in wp-ops list, its --help output, the argument prompts in the interactive picker, its searchable description, and its shell completion. Nothing is registered twice. Adding a script to the right directory with a correct header is the entire process of adding a command — there is no central registry file to forget to update, and a malformed manifest fails the build rather than producing a command that silently misbehaves.
The @runs field is a small detail with an outsized effect. Most commands execute on your machine and reach out over SSH. A handful — the Nginx log monitors — have to run on the server, because that is where the logs are. Those are tagged server, badged as such in listings, and if you run one locally it prints the SSH invocation instead of failing on a missing log path:
ssh web@example.com 'bash -s' < scripts/monitoring/monitor.sh
Nothing gets installed on the server for that. The script is streamed to the remote shell’s stdin, runs, and is gone.
Grouped by Domain, Filtered by Platform
For a long time the catalog was organized by directory, which meant “backup” was split in two: shell scripts under scripts/backup/, Ansible playbooks under trellis/backup/. Version 5.0.0 changed the grouping to the domain declared in each manifest. Roughly 40% of the catalog had been living in a domain that no single directory contained.
wp-ops list
Monitoring (17) Log monitoring, uptime checks, and traffic analysis
Backup (10) Database and file backups — Ansible and shell
Content ( 8) Block pattern screenshots, page creation, pattern validation
Images ( 7) Image resizing, WebP/AVIF conversion, Openverse downloads
SEO ( 7) Redirect, schema, orphan-content audits, noindex management
Security ( 6) Malware scanning, fail2ban, IP blocking, admin recovery
Misc ( 5) Trellis updater, WooCommerce variations, one-off utilities
Release ( 4) Plugin/theme release and asset upload automation
Git ( 3) PR creation, repo traffic stats, git log helpers
MCP Server ( 3) MCP server development and runtime commands
Diagnostics ( 2) WordPress transient and post-count diagnostics
Sync ( 2) rsync a theme or package into a site
Alongside domain, every command declares a platform, which answers a different question: will this actually run against the site in front of me?
| Platform | Means | Commands |
|---|---|---|
trellis | Needs a Trellis project, vault, /srv/www, or the trellis CLI | 27 |
wordpress | Any WordPress install — Valet, Herd, cPanel, Bedrock, Trellis | 17 |
any | No WordPress involved at all | 30 |
That distinction matters when you inherit a client site on shared hosting. wp-ops list --platform wordpress shows exactly what you can run today, before you have migrated anything. Two thirds of the catalog does not require Trellis.
The WordPress MCP Server: The Same Operations, Callable by an Agent
The CLI solved discovery for a person. The MCP server solves it for an assistant. Without it, asking an AI coding assistant to check a production site means it writes ad-hoc ssh and wp commands from scratch every time — the paths wrong as often as right, the --path=web/wp flag forgotten, no idea which site key maps to which server.
With it, the assistant calls a typed tool against a named site and environment, and a central registry resolves the rest. Fourteen tools are implemented:
| Tool | What it does |
|---|---|
wp_cli | Any WP-CLI command against a registered site/env; --path added automatically |
db_backup | wp db export, gzipped, streamed to your machine |
db_pull | Remote database into local development, with URL search-replace and a pre-pull dev backup |
files_pull | rsync the remote uploads directory into local development |
security_scan | Targeted or general malware scanner, streamed over SSH |
monitor | Combined traffic, security, AI-crawler, and error-log analysis, returned as markdown |
server_status | Live CPU, memory, disk, PHP-FPM, MySQL, Nginx, and recent OOM kills |
remote_ttfb_audit | TTFB measured from the server itself, across several crawler user agents |
broken_link_audit | Internal-link 4xx/5xx check — homepage sweep or recursive crawl |
redirect_audit | Redirect chains, HTTP→HTTPS, www canonicalization, security headers |
schema_audit | JSON-LD schema coverage across key pages |
url_audit | Hardcoded dev URLs left in post_content after a migration |
ip_reputation_check | IPs checked against AbuseIPDB, including a staleness audit of already-blocked IPs |
admin_user_create | Temporary WordPress administrator for lockout recovery |
Two of these deserve a note. url_audit exists because of a specific recurring failure: WordPress patterns resolve theme URLs through get_template_directory_uri(), and when a pattern is inserted into a page the resulting absolute URL is written into the database. Content built on a .test local domain therefore arrives in production with .test image URLs baked in, producing mixed-content warnings and broken images. It is a five-minute fix if you catch it and an embarrassing one if a client catches it first.
And monitor bundles all five monitoring scripts into a throwaway remote temp directory for the run, which means it works on a server that has never been provisioned for monitoring at all. Nothing is left behind. That is the difference between “we monitor sites we’ve set up for monitoring” and “we can monitor any server we have SSH access to, starting now.”
Safety as a Design Feature, Not a Prompt Instruction
Handing an AI assistant SSH access to a production WordPress site is exactly as reckless as it sounds, unless the tools themselves constrain what can happen. Telling a model to “be careful” is not a control. The wp-ops MCP server enforces four rules in code.
Read-only verbs run; everything else needs confirmation
The wp_cli tool inspects the verb. list, get, exists, status, info, version, search, check-update, doctor, and export run immediately. Updates, deletes, search-replace, eval, and plugin installs require confirm: true, which is meant to be set only after the specific command has been approved in conversation. The --path flag is added automatically and rejected if passed explicitly, so it cannot be pointed somewhere else.
Nothing gets written to the remote host
The malware scanner is streamed over SSH stdin (php - <path>) rather than copied up. Database exports stream back over stdout. There is no scp-then-forget-to-delete step, which is how a scanner file ends up sitting in a webroot for six months.
Destructive operations are asymmetric on purpose
db_pull and files_pull exist. db_push and files_push deliberately do not. Overwriting a disposable local development database is a bad afternoon; overwriting a production one is a different category of event. The pull direction is implemented, the push direction is left to a human running the Ansible playbook with full attention. Even db_pull requires confirm: true and takes a backup of the local database before touching it.
Output is capped
wp_cli output is truncated at 15,000 characters, with the notice suggesting --format=count, --fields=, or --posts_per_page= to narrow the query. A wp post list on a site with a few thousand posts would otherwise dump tens of kilobytes into the assistant’s context and crowd out everything else. The audit tools take a summary: true parameter that omits passing pages entirely, typically halving the output.
Note: These constraints are what makes the assistant useful rather than nerve-wracking. The tool boundary is the safety boundary. If the only thing standing between a model and wp db drop is a sentence in a system prompt, the design is wrong.
What Changes Day to Day: Build, Maintain, Monitor
Building
Theme development against a real site is the loop that benefits most. Our Elayne block theme is installed on its demo site as a pinned Composer dependency, which is correct for production and hostile to iteration — testing a local change would otherwise mean tagging a release. One command syncs the working copy in instead:
SITE_ROOT=~/code/imagewize.com/demo/web/app \
wp-ops rsync-package-to-site theme elayne ~/code/elayne
The content category covers the rest of the build loop: creating pages from WP-CLI, validating block pattern files, and screenshotting patterns via Playwright — which spins up a temporary page, captures the pattern, deletes the page, and converts the result to WebP. If you have ever hand-cropped forty pattern screenshots for a theme submission, that one command is the whole reason to install this. Most of our WordPress development work now starts from this loop rather than from a blank Bedrock install.
Local FSE work has its own set of traps that no CLI can fix, most of them about database records silently overriding filesystem templates. We wrote those up separately in FSE theme development in Trellis and Bedrock.
Maintaining
Maintenance is where the CLI-plus-MCP pairing earns its keep, because maintenance work is mostly triggered by a question rather than a schedule. “Is that client site still on PHP 8.3?” is a question, and the answer used to cost an SSH session and a lookup of which path that particular install uses.
wp-ops db-backup example.com production # gzipped export, streamed to your machine
wp-ops db-pull example.com production # into dev, with URL search-replace
wp-ops files-pull example.com production # uploads, via rsync
wp-ops scanner-wrapper # both malware scanners in sequence
The SEO category runs the checks that otherwise get skipped because each one is individually small: redirect chain audits, schema coverage, orphaned pages that nothing links to internally, and blog content categorization. Our own Article schema implementation came out of exactly that kind of audit — a gap that was invisible until something enumerated it.
Monitoring
Seventeen monitoring commands is the largest category, and that is not an accident. Most WordPress “hosting incidents” are visible in the access log hours before they are visible on the site.
| Command | Answers |
|---|---|
traffic-monitor | What real traffic is this site getting? |
security-monitor | Is anyone hammering wp-login or xmlrpc? |
ai-bot-monitor | How much of this load is GPTBot, ClaudeBot, and friends? |
error-monitor | What is Nginx, PHP-FPM, MySQL, or systemd complaining about? |
server-monitor | Is the box out of memory, and has the OOM killer been busy? |
traffic-by-country | Is this spike a market, or one datacenter? |
remote-ttfb-ua | Is the server slow, or is my connection slow? |
404-checker | What internal links are broken right now? |
Two posts came directly out of running these: a look at which AI bots actually crawl a WordPress site, and the Nginx 444 response we now use against scanner traffic. Neither would have been written without a command that made the log readable in ten seconds. When contact form spam followed the same pattern, rate limiting at the Nginx level came out of the same logs.
The remote-ttfb-ua command is the one that surprises people. It measures TTFB from the server itself, across several crawler user agents, which removes local network and DNS variance from the measurement — and occasionally reveals that a site is fast for browsers and slow for Googlebot specifically. That is a very different problem from the ones in our WordPress speed optimization checklist, and you cannot see it from a browser.
What Your Host Already Does — And Where It Stops
Worth being straight about this, because much of what wp-ops does sounds like something you already pay a host for.
Shared hosting has become genuinely good at security. Automatic vulnerability scanning, malware cleanup, patches applied on the customer’s behalf. One Dutch host publishes the policy plainly: they detect the issue, notify the customer with an explanation of the fix, and if nothing has happened within two weeks, they patch it themselves. That is a sensible policy and a real improvement on where the industry sat five years ago.
It also marks the boundary precisely. A host protects its platform. It cannot take responsibility for one specific site — which plugin let the thing in, whether checkout still works once the patch lands, whether the same hole reopens next month. Notify-then-wait exists because it is not their site to change. Auto-cleaning uploaded malware removes the symptom; somebody still has to find the cause.
Owning the stack is what changes the available answers:
| A managed host | Running the server yourself | |
|---|---|---|
| Vulnerability scanning | Automatic, on their schedule | On demand, any site, any time |
| Malware cleanup | Uploaded files removed | Removed, plus the entry point found and closed |
| Patching | Notify, then patch if you don’t act | Applied when you decide, tested afterwards |
| Attack traffic | Handled at platform level | Dropped at Nginx before PHP runs — rate limits, fail2ban, deny lists |
| Backups | Held by the host | Pulled off the server to separate storage |
| Access logs | Retained | Read for attack patterns |
| Site-specific fixes | Out of scope | The actual job |
None of this makes managed hosting the wrong choice. For most small sites it is exactly right, and the scanning and patching in that left column is why. It does mean “my host scans for malware” and “someone is looking after my site” are different statements — and it is worth knowing which one you are buying.
Client Server DevOps Without a DevOps Team
This is the part that actually matters commercially, so it is worth saying plainly.
A small studio running managed hosting for client sites is doing the work a dedicated DevOps function would do at a larger company: provisioning, deployments, backups, patching, log analysis, incident response, security scanning. The work does not shrink because the company is small. What changes is that there is no one whose full-time job it is, so it competes with billable client work and loses.
Tooling is the only real lever there. Three effects compound:
- Onboarding a client server takes an afternoon, not a week. Add the site to the registry with its SSH host and WordPress root, and every audit, backup, scan, and monitor works against it. Two thirds of the catalog does not need Trellis, so this holds for an inherited cPanel site too.
- Checks that used to be quarterly become incidental. Running a malware scan is now one sentence in a conversation, so it happens while looking at something else, not during a scheduled maintenance window that keeps getting moved.
- The ops knowledge stops living in one person’s head. Manifest headers and searchable documentation mean the procedure is written down at the point of use — which matters for handover, for continuity, and for the day the person who wrote the script is on a plane.
Clients do not buy any of this directly. They buy a site that is up, fast, backed up, and not serving pharma spam. Everything above is what makes it realistic to promise that at a small-studio price — it is the operational layer underneath our managed WordPress hosting, and the reason a one-person consultancy can run production infrastructure for a portfolio of client sites without a night shift.
Editor-Agnostic by Design
MCP’s stdio transport is a core part of the protocol, not a vendor extension: the client spawns the server as a local process and talks over stdin and stdout, killing it on disconnect. Any client that can spawn a local process can use it. We have verified three — Claude Code and Claude Desktop, the Mistral Vibe CLI, and the OpenAI Codex CLI.
Vibe reads [[mcp_servers]] entries from a config.toml, and the same server registers in a few lines:
# ~/.vibe/config.toml
[[mcp_servers]]
name = "wp_ops"
transport = "stdio"
command = "~/code/wp-ops/mcp-server/run.sh"
args = []
Register it once at the user level — not per project — so the tools are available in every session, including sessions in unrelated repositories. The site registry is central and multi-site, so there is no reason to scope it to one project directory. wp-ops mcp-register checks all three config files, reports which ones are missing an entry, and prints the exact block to paste with the real path on your machine already filled in. It never writes to a config file itself.
A Streamable HTTP transport also exists, for the case where a client genuinely cannot spawn a local process — a cloud-hosted client, or several clients sharing one long-running instance. For anything on your own machine, stdio is simpler: no token, no port, no process to remember to stop.
Being editor-agnostic is a deliberate hedge. Model and tooling preferences change faster than infrastructure does, and we would rather not rebuild our operations layer because we switched assistants. We wrote about the related question of how much project context to give an agent versus how much to encode in the tools — the same instinct applies here. Capability belongs in the tool, not in the prompt.
What We Would Keep If We Started Over
Four things, after roughly 600 commits and a full rewrite.
- Self-describing commands, from the first script. The manifest header is a few minutes of work per command and it eliminates an entire class of drift between what a tool does and what its documentation claims. We retrofitted it; adding it up front would have been cheaper.
- Organize by what a thing is for, not where it lives. Directory-based grouping split “backup” across two categories for months. Domain and platform are metadata now, and the directory layout is an implementation detail.
- Documentation is a first-class search target. A lot of what a repo like this knows is prose, not scripts — OOM diagnosis, Nginx redirect patterns, migration URL methods.
wp-ops docs <term>searches the guides, and whenwp-ops searchfinds no command it points at the documentation instead of shrugging. - Build the safety rails before the convenience. The confirmation gates, the streamed-not-copied execution, and the deliberately absent push tools were designed in, not bolted on after an incident. That order is not optional once an agent is holding the keys.
Try It
wp-ops is MIT-licensed and on GitHub. The CLI is useful on its own, with no MCP server and no AI assistant involved.
brew install imagewize/tap/wp-ops
wp-ops init # shell completions
wp-ops doctor # what's installed, what's missing
wp-ops # interactive picker
# or build from source, needs Go 1.26+
git clone https://github.com/imagewize/wp-ops.git && cd wp-ops
go build -o wp-ops ./go
Run wp-ops doctor first. It reports which external tools the scripts rely on — WP-CLI, Ansible, ImageMagick, gh, cwebp, Node — are actually present, so you find out before a command fails halfway through. For the MCP server, mcp-server/README.md covers the site registry and per-client setup.
Frequently Asked Questions
- What is wp-ops? wp-ops is an open-source WordPress DevOps tool distributed as a single Go binary. It bundles 74 operations commands — backups, malware scanning, Nginx log monitoring, SEO audits, deployments, and content workflows — and ships an MCP server that exposes fourteen of them as tools an AI assistant can call directly.
- Do I need Trellis to use wp-ops? No. 27 of the 74 commands require a Trellis project, but 17 run against any WordPress install — Valet, Herd, cPanel, Bedrock — and 30 involve no WordPress at all. Run
wp-ops list --platform wordpressto see what applies to a non-Trellis site. - Doesn’t my host already do this? Partly, and increasingly well. Good hosts scan for vulnerabilities, clean up uploaded malware, and will patch a site if the owner doesn’t act. What they cannot do is take responsibility for one specific site — find which plugin let the malware in, confirm checkout still works after a patch, or drop attack traffic at the server before it reaches WordPress. A host protects its platform, which is a different job from looking after your site.
- Do I need an AI assistant to use it? No. The CLI is the primary interface and works entirely on its own. The MCP server is an optional second front door onto the same operations.
- Is it safe to let an AI assistant run these against production? The tools are built so that safety does not depend on the model behaving well. Read-only WP-CLI verbs run immediately; updates, deletes, search-replace, and eval require an explicit confirmation flag meant to be set only after you have approved that specific command. Database and file pushes to production are deliberately not implemented at all.
- Does anything get installed on my server? No. Scripts are streamed over SSH to the remote shell’s stdin and are gone when they finish. Database exports stream back over stdout without touching disk on the remote host.
- Which AI clients does the MCP server work with? Any client that supports MCP’s stdio transport. Claude Code, Claude Desktop, the Mistral Vibe CLI, and the OpenAI Codex CLI are verified. A Streamable HTTP transport is available for clients that cannot spawn a local process.
- How do I add my own command? Drop the script in the appropriate category directory with a manifest header declaring its description, category, platform, requirements, and arguments. The catalog, help text, argument prompts, and shell completions are generated from that header — there is no registry file to update.
Done Managing Your Own Server?
We offer managed WordPress hosting built on Trellis — Nginx, PHP 8.3, Redis, automated deployments via Ansible, and Bedrock structure on Hetzner EU. No shared hosting, no page builders, no surprises.
- Trellis + Bedrock on Hetzner EU (Frankfurt / Helsinki)
- Nginx + FastCGI caching + Redis object cache
- Automated deployments via Ansible, SSL via Let’s Encrypt
- From €49/month — or €65/hour for one-off server work