A MainWP Labs project, powered by MainWP
A CLI for managing your MainWP Dashboard from the terminal. List sites, push updates, sync data, run batch operations across dozens of sites. The command is mainwpcontrol.
Looking for the MCP Server instead? MainWP MCP Server is for conversational AI management inside Claude, Cursor, or any MCP-compatible client. MainWP Control is for automation: cron jobs, CI/CD pipelines, monitoring scripts, and batch operations. Both talk to the same Abilities API with the same safety model.
On Windows? Use Git Bash and every example below works without changes. For scheduled workflows (cron), see WSL.
You need Node.js 20+ and a MainWP Dashboard (v6+) with an Application Password.
npm install -g @mainwp/control
mainwpcontrol login
mainwpcontrol abilities listYou should see something like this:
Abilities (87 total)
Sites
Name Description Type
---------------- -------------------- --------------
list-sites-v1 List MainWP sites 📖 read
mainwpcontrol abilities run list-sites-v1
get-site-v1 Get site details 📖 read
mainwpcontrol abilities run get-site-v1
sync-sites-v1 Sync all sites ✏️ write
mainwpcontrol abilities run sync-sites-v1
That's it. You're connected and you can see every operation your Dashboard supports.
abilities list shows every operation available on your Dashboard. These are called "abilities" and they cover sites, plugins, themes, updates, clients, tags, and more.
Each ability has a name (like list-sites-v1) that you pass to abilities run to execute it. The list tells you whether each one is read-only, a write operation, or destructive.
List all your sites:
mainwpcontrol abilities run list-sites-v1 --jsonCheck for pending updates across sites:
mainwpcontrol abilities run list-updates-v1 --jsonGet details for a specific site:
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --jsonWindows? This works as-is in Git Bash. In PowerShell, escape the inner quotes:
'{\"site_id_or_domain\": 1}'. Or skip quoting entirely with--input-file(details).
Preview a destructive action before running it:
mainwpcontrol abilities run delete-site-v1 \
--input '{"site_id_or_domain": "mysite.com"}' \
--dry-run --jsonNothing changes until you explicitly pass --confirm.
Update plugins and wait for completion:
mainwpcontrol abilities run update-site-plugins-v1 \
--input '{"site_id": 1}' \
--wait --json--wait blocks until the operation finishes. Useful in CI pipelines.
Pre-built keychain binaries are included for macOS, Windows, and Linux (x64 and arm64). On other platforms you may need C++ build tools during installation.
npm install -g @mainwp/control
# Interactive login (stores credentials in your OS keychain)
mainwpcontrol loginUse this when no OS keychain is available, or if keytar fails to build.
npm install -g @mainwp/control
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
mainwpcontrol login --url https://dashboard.example.com --username adminWhen the OS keychain is unavailable, credentials are not stored on disk. Keep MAINWP_APP_PASSWORD set for each run.
If keytar is installed but broken, set MAINWPCONTROL_NO_KEYTAR=1 to skip loading it.
New to the Command Line?
If you haven't used a terminal before, here's what you need to know.
A terminal is where you type commands instead of clicking buttons. You'll see it called "command line" or "shell" in different places.
How to open it:
- macOS: Open Terminal (search in Spotlight, or look in Applications > Utilities)
- Windows: Open Git Bash (installed with Git for Windows). If you don't have it, PowerShell works too — see the quoting notes below.
- Linux: Open your distribution's Terminal app (usually in the applications menu)
npm is the Node.js package manager. It downloads and installs JavaScript packages. The -g flag installs globally, which makes mainwpcontrol available as a command anywhere on your system, not only in one project folder.
An environment variable is a named value that programs can read. They're commonly used for passwords and API keys.
Setting one:
# macOS / Linux (lasts until you close the terminal)
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
# Windows PowerShell (lasts until you close the window)
$env:MAINWP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx'For long-term storage, use the OS keychain (the default when you run mainwpcontrol login) or a restricted-permission .env file rather than pasting credentials into shell profile files.
WordPress Application Passwords let external tools like mainwpcontrol access your site without using your main login password. They look like groups of four characters separated by spaces (e.g., abcd efgh ijkl mnop qrst uvwx).
To create one: Log into WordPress admin > Users > Your Profile > scroll to Application Passwords > enter a name like "mainwpcontrol" > click Add New Application Password > copy the generated password.
When you run a command, the output appears in your terminal. A few things to know:
--jsontellsmainwpcontrolto output structured JSON (useful for scripting and piping to other tools)- Exit codes indicate success (
0) or failure (1through5). You won't see them directly, but scripts and CI use them to decide what happens next. Runecho $?(macOS/Linux) orecho $LASTEXITCODE(PowerShell) after a command to check.
When you pass JSON with --input, quoting depends on your shell:
# macOS / Linux / Git Bash on Windows
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
# Windows PowerShell
mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --jsonGit Bash on Windows (comes with Git for Windows) handles quoting the same way macOS and Linux do. If you use Git Bash, all the examples in this documentation work without changes.
PowerShell strips the inner double quotes unless you escape them with backslashes. If this gets annoying, put your parameters in a file and use --input-file:
mainwpcontrol abilities run get-site-v1 --input-file params.json --jsonThis works the same on every platform. See Input from File for details.
Your Dashboard exposes its operations as "abilities." You browse them, pick one, and run it. Every ability has a versioned name like list-sites-v1 that you pass to abilities run.
# List all abilities
mainwpcontrol abilities list
# Filter by category
mainwpcontrol abilities list --category sites
# Get full details and input schema for an ability
mainwpcontrol abilities info list-sites-v1
# Run an ability
mainwpcontrol abilities run list-sites-v1 --json
# Run with input parameters
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
# Windows PowerShell: escape inner quotes (Git Bash doesn't need this)
mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --json
# Or use a file (works everywhere)
mainwpcontrol abilities run get-site-v1 --input-file params.json --jsonEach mainwpcontrol login creates a profile, a named connection to a Dashboard, identified by hostname. If you manage multiple Dashboards, run login once per Dashboard to create a profile for each.
# List all profiles
mainwpcontrol profile list
# Switch active profile
mainwpcontrol profile use production.example.com
# Use a profile for one command without switching
mainwpcontrol abilities list --profile staging.example.com
# Delete a profile and its keychain credentials
mainwpcontrol profile delete staging.example.comdoctor checks your configuration, credentials, and Dashboard connectivity. Run it first if something isn't working.
# Check configuration and connectivity
mainwpcontrol doctor
# Verbose output
mainwpcontrol doctor -v
# JSON output
mainwpcontrol doctor --jsonIf you have an LLM API key, you can talk to your Dashboard in plain English instead of constructing commands. Good for exploration, not required for anything.
Set one of these environment variables to enable it:
# Pick one (Anthropic, OpenAI, Google, or OpenRouter)
export ANTHROPIC_API_KEY='sk-ant-...'
mainwpcontrol chat
mainwpcontrol chat "list all sites with pending updates"See Chat Mode Configuration for all supported providers and flags.
These flags work on every command.
| Flag | Description |
|---|---|
--json |
Structured JSON output |
--quiet / -q |
Suppress output (exit code only) |
--profile <name> |
Use a specific profile |
--debug |
Show redacted debug diagnostics on stderr |
--help |
Show help |
Extra flags for abilities run. These control input, safety checks, and batch job behavior.
| Flag | Description |
|---|---|
--input / -i |
Input parameters as JSON (use - for stdin) |
--input-file |
Read input from a JSON file |
--dry-run |
Preview changes without executing |
--confirm |
Execute a destructive ability |
--force |
Skip interactive confirmation (CI mode) |
--wait |
Block until batch job completes |
--wait-timeout |
Max seconds to wait (default: 300) |
Abilities are the operations your MainWP Dashboard exposes through its REST API. Each one has:
- A versioned name (e.g.,
list-sites-v1,delete-site-v1) - An input schema (what parameters it accepts)
- Annotations that tell you what kind of operation it is
The annotations matter:
- Readonly: Safe to run anytime. Cannot modify data.
- Destructive: Permanently changes or deletes data. Requires
--dry-runpreview, then--confirmto execute. - Idempotent: Safe to re-run. Same result on repeated calls.
Run mainwpcontrol abilities info <name> to see the full schema and annotations for any ability.
A profile is a named connection to a MainWP Dashboard. It stores the Dashboard URL and username. Your password stays in the OS keychain (or in the MAINWP_APP_PASSWORD environment variable when no keychain is available).
Running mainwpcontrol login creates a profile automatically, named after the Dashboard hostname:
# Creates profile "staging.example.com"
mainwpcontrol login --url https://staging.example.com --username admin
# Creates profile "production.example.com"
mainwpcontrol login --url https://production.example.com --username adminThe profile file at ~/.config/mainwpcontrol/profiles.json never contains passwords.
Destructive operations follow a two-step pattern: preview first, then execute.
# Step 1: Preview (nothing changes)
mainwpcontrol abilities run delete-site-v1 \
--input '{"site_id_or_domain": "mysite.com"}' \
--dry-run --json
# Step 2: Execute after reviewing the preview
mainwpcontrol abilities run delete-site-v1 \
--input '{"site_id_or_domain": "mysite.com"}' \
--confirm --force --json--dry-run and --confirm are mutually exclusive. You cannot pass both.
In CI/scripted workflows where you've already validated the operation, pass --confirm --force directly to skip the interactive prompt.
Operations that affect many items (200+) are automatically queued as batch jobs. The command returns a job_id immediately, and you can watch progress:
mainwpcontrol jobs watch <job-id>
# With a timeout
mainwpcontrol jobs watch <job-id> --timeout 120Or use --wait on the original command to block until completion:
mainwpcontrol abilities run sync-sites-v1 --wait --wait-timeout 300 --json# Non-interactive login
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
mainwpcontrol login --url https://dashboard.example.com --username admin
# Silent execution with exit codes
mainwpcontrol abilities run list-sites-v1 --json --quiet
echo "Exit code: $?"
# Pipeline branching on exit codes
if mainwpcontrol abilities run check-sites-v1 --json --quiet; then
echo "All sites healthy"
else
echo "Issues detected"
fi# From a JSON file
mainwpcontrol abilities run update-site-plugins-v1 --input-file params.json --json
# From stdin
echo '{"site_id_or_domain": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json
# Heredoc
mainwpcontrol abilities run get-site-v1 --input - --json <<EOF
{"site_id_or_domain": 1}
EOFChat requires one of these environment variables:
| Variable | Provider |
|---|---|
ANTHROPIC_API_KEY |
Anthropic Claude |
OPENAI_API_KEY |
OpenAI GPT |
GOOGLE_API_KEY |
Google Gemini |
OPENROUTER_API_KEY |
OpenRouter |
LOCAL_LLM_API_KEY |
Local LLM (with optional LOCAL_LLM_URL) |
Additional chat flags: --provider, --model, --max-turns, --max-context-messages, --no-stream.
In non-TTY environments (pipes, CI), mainwpcontrol chat without a message argument exits with guidance instead of hanging.
# Bash
source /path/to/mainwp-control/scripts/completions/mainwpcontrol.bash
# Zsh
source /path/to/mainwp-control/scripts/completions/mainwpcontrol.zshSettings live at ~/.config/mainwpcontrol/settings.json:
{
"defaultJsonOutput": true,
"timeout": 30000,
"debug": false,
"llmProvider": "openai",
"chatContextMessages": 20
}Step-by-step guides for common automation patterns:
| Workflow | Description |
|---|---|
| Daily Health Check | Cron job that checks site connectivity and alerts via Slack |
| Plugin Deployment Verification | GitHub Actions workflow to verify a plugin exists across all sites |
| Monthly Batch Updates | Preview and apply updates safely, scripted and GitHub Actions variants |
| Input from File | Pass complex parameters via JSON files, stdin pipes, or heredocs |
| Monitoring Integration | Send site metrics to Datadog, StatsD, or other monitoring tools |
| Code | Meaning | CI Usage |
|---|---|---|
| 0 | Success | Continue pipeline |
| 1 | User/input error | Fix command syntax |
| 2 | Auth/config error | Check credentials |
| 3 | Network error | Retry or check connectivity |
| 4 | API error | Check ability parameters |
| 5 | Internal error | Report bug |
| Variable | Description |
|---|---|
MAINWP_APP_PASSWORD |
Application password for non-interactive login and commands when keychain is unavailable |
MAINWPCONTROL_NO_KEYTAR |
Set to 1 to skip keytar (keychain) loading entirely |
MAINWP_ALLOW_HTTP |
Set to 1 to allow insecure HTTP Dashboard URLs |
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic Claude |
OPENAI_API_KEY |
OpenAI GPT |
GOOGLE_API_KEY |
Google Gemini |
OPENROUTER_API_KEY |
OpenRouter |
LOCAL_LLM_API_KEY |
Local LLM provider (required to enable local provider) |
LOCAL_LLM_URL |
Local endpoint URL (optional, defaults to localhost) |
MAINWP_LLM_PROVIDER |
Override auto-detected provider |
MAINWP_LLM_MODEL |
Specify model to use |
All settings in ~/.config/mainwpcontrol/settings.json:
| Setting | Type | Description |
|---|---|---|
defaultJsonOutput |
boolean | Default to JSON output |
timeout |
number | HTTP request timeout in milliseconds |
debug |
boolean | Enable debug output |
llmProvider |
string | Default LLM provider for chat |
chatContextMessages |
number | Max messages in chat context |
skipSSLVerification |
boolean | Disable TLS verification (insecure, prefer per-profile setting via login --skip-ssl-verify) |
allowInsecureHttp |
boolean | Allow http:// Dashboard URLs without MAINWP_ALLOW_HTTP=1 |
"keytar failed to build" or native module errors during install
Keytar requires native C++ compilation on some platforms. If it fails:
- Use environment variable auth instead (bypasses keytar entirely):
export MAINWP_APP_PASSWORD='your-application-password' mainwpcontrol login --url https://dashboard.example.com --username admin
- Or skip keytar explicitly by setting
MAINWPCONTROL_NO_KEYTAR=1before running commands.
The pre-built binaries cover macOS, Windows, and Linux (x64/arm64). If you're on a different platform or architecture, you'll need C++ build tools (gcc, g++, make) or the env var approach.
"command not found" after install
This usually means your npm global bin directory isn't in your system PATH.
- Find where npm installs global packages:
npm config get prefix
- Add the
binsubdirectory to your PATH. For example, if the prefix is/usr/local:# Add to ~/.bashrc, ~/.zshrc, or your shell profile: export PATH="/usr/local/bin:$PATH"
- Restart your terminal (or run
source ~/.zshrc/source ~/.bashrc) and try again.
On Windows, the npm global directory is usually already in PATH after installing Node.js.
"connection refused" or network errors
If mainwpcontrol login or commands fail with connection errors:
- Check the Dashboard URL. Make sure it's the full URL with
https://(e.g.,https://dashboard.example.com). Don't include a trailing slash. - Verify HTTPS.
mainwpcontrolrequires HTTPS by default. If your Dashboard uses HTTP (not recommended), setMAINWP_ALLOW_HTTP=1. - Check firewall/network. Make sure your machine can reach the Dashboard:
curl -I https://dashboard.example.com
- SSL certificate issues. If using a self-signed certificate, you can use
mainwpcontrol login --skip-ssl-verify(not recommended for production).
npm run build # Build the project
npm test # Run tests (unit + e2e, no network needed)
npm run lint # Check code stylenpm run test:live runs tests against a real MainWP Dashboard, including workflow documentation validation. These require a running Dashboard and credentials:
export MAINWP_API_URL=https://your-dashboard.example.com
export MAINWP_USER=your-admin-username
export MAINWP_APP_PASSWORD='your-application-password'
npm run test:liveThe live suite includes API tests (login, abilities discovery, read-only execution, safety model, exit codes) and workflow doc tests (validates that every jq expression, field name, and data pipeline documented in docs/workflows/ works against the real API).
Live tests are safe: they only run read-only operations and --dry-run previews, never mutations.
GPL-3.0-or-later
- Node.js 20 LTS or later
- MainWP Dashboard 6+ with Abilities API
- WordPress Application Password