Skip to content

wp-ops: One CLI and One MCP Server for WordPress DevOps

By Jasper Frumau WordPress

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

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:

CLIMCP server
Best forA known task you can nameA question you want answered
CoverageAll 74 commands14 tools
InvocationYou type itAn AI assistant calls it
Discoverywp-ops search, picker, completionsTool descriptions in the assistant’s context
Write safetyYour own judgmentEnforced confirmation gates
Needs an AI clientNoYes

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?

PlatformMeansCommands
trellisNeeds a Trellis project, vault, /srv/www, or the trellis CLI27
wordpressAny WordPress install — Valet, Herd, cPanel, Bedrock, Trellis17
anyNo WordPress involved at all30

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:

ToolWhat it does
wp_cliAny WP-CLI command against a registered site/env; --path added automatically
db_backupwp db export, gzipped, streamed to your machine
db_pullRemote database into local development, with URL search-replace and a pre-pull dev backup
files_pullrsync the remote uploads directory into local development
security_scanTargeted or general malware scanner, streamed over SSH
monitorCombined traffic, security, AI-crawler, and error-log analysis, returned as markdown
server_statusLive CPU, memory, disk, PHP-FPM, MySQL, Nginx, and recent OOM kills
remote_ttfb_auditTTFB measured from the server itself, across several crawler user agents
broken_link_auditInternal-link 4xx/5xx check — homepage sweep or recursive crawl
redirect_auditRedirect chains, HTTP→HTTPS, www canonicalization, security headers
schema_auditJSON-LD schema coverage across key pages
url_auditHardcoded dev URLs left in post_content after a migration
ip_reputation_checkIPs checked against AbuseIPDB, including a staleness audit of already-blocked IPs
admin_user_createTemporary 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.

CommandAnswers
traffic-monitorWhat real traffic is this site getting?
security-monitorIs anyone hammering wp-login or xmlrpc?
ai-bot-monitorHow much of this load is GPTBot, ClaudeBot, and friends?
error-monitorWhat is Nginx, PHP-FPM, MySQL, or systemd complaining about?
server-monitorIs the box out of memory, and has the OOM killer been busy?
traffic-by-countryIs this spike a market, or one datacenter?
remote-ttfb-uaIs the server slow, or is my connection slow?
404-checkerWhat 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 hostRunning the server yourself
Vulnerability scanningAutomatic, on their scheduleOn demand, any site, any time
Malware cleanupUploaded files removedRemoved, plus the entry point found and closed
PatchingNotify, then patch if you don’t actApplied when you decide, tested afterwards
Attack trafficHandled at platform levelDropped at Nginx before PHP runs — rate limits, fail2ban, deny lists
BackupsHeld by the hostPulled off the server to separate storage
Access logsRetainedRead for attack patterns
Site-specific fixesOut of scopeThe 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 when wp-ops search finds 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 wordpress to 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

Leave a Reply

Your email address will not be published.