Skip to main content
Glama

wp-remote-bridge

A local MCP server (Node/TypeScript, stdio transport) plus a small PHP bridge that gives an AI agent — or any MCP client — controlled, audited, reversible access to a WordPress site hosted on shared hosting with no SSH, no WP-CLI, and no remote MySQL (port 3306 closed). The only access the host gives you is FTP/SFTP (and sometimes phpMyAdmin).

Two capabilities:

  1. Guarded SQL against the site's MySQL/MariaDB database (read by default, write only under explicit, time-boxed, per-environment policy).

  2. Script execution (wp_run): run a PHP script, written and tested ahead of time, with WordPress fully loaded — the equivalent of wp eval-file on a server that doesn't have WP-CLI.

Both are reached over HTTPS through a tiny PHP file dropped at a random path in the web root; FTP/SFTP is only used for the bridge's own lifecycle (deploy/enable/disable/remove) and for uploading wp_run scripts.

Architecture

┌──────────────────────────┐        stdio        ┌────────────────────────────────┐
│ MCP client (agent)       │ ───────────────────▶ │ wp-remote-bridge (Node/TS)     │
└──────────────────────────┘                      │  - typed MCP tools (zod)       │
                                                   │  - client-side SQL hint        │
                                                   │  - HMAC request signing        │
                                                   │  - local JSONL audit log       │
                                                   └───────┬────────────────┬───────┘
                                      HTTPS (signed POST)  │                │ SFTP/FTP(S)
                                                            ▼                ▼ (deploy/enable/disable/
                                          ┌─────────────────────────┐      remove bridge, upload
                                          │ <webRoot>/<random-path>/│      wp_run scripts)
                                          │   bridge.php            │
                                          │   run.php               │
                                          │   .htaccess             │
                                          └───────┬─────────────────┘
                                                  │ DB credentials read from the site itself
                                                  │ wp_run: require <webRoot>/wp-load.php
                                                  ▼ PDO, localhost
                                          ┌─────────────────────────┐
                                          │ MySQL / MariaDB         │
                                          └─────────────────────────┘

                                          ┌─────────────────────────┐
                                          │ <privateRoot>/wrb/      │  scripts/, logs/, nonces/,
                                          │                         │  enabled-until, secret
                                          └─────────────────────────┘  (outside the web root)

Related MCP server: AgentLinkWP

Security model

  • Signed, obfuscated protocol. A single POST, JSON body base64-encoded in field p (plain SQL or PHP script names in a POST body are often blocked by mod_security on shared hosting). Headers X-WRB-Ts (epoch seconds), X-WRB-Nonce (128-bit random), X-WRB-Sig = HMAC-SHA256(secret, ts\nnonce\nsha256(body)). The bridge rejects clock skew > 60 s, a replayed nonce, or a signature that fails hash_equals.

  • Random bridge path. The bridge files live at a random, unguessable path under the web root, generated once and kept in local client state.

  • Kill switch. The bridge only answers while <privateRoot>/wrb/enabled-until (outside the web root) holds a future timestamp; otherwise it returns a bare 404 with no body. bridge_enable sets it (2 h default, 8 h max); bridge_disable clears it. Expiry is automatic even if you forget to disable it.

  • Per-environment policy baked into the bridge. Each environment declares allowWrite (SQL writes) and allowRun (wp_run), both false by default. The policy is written into the bridge file itself at deploy time and enforced there — not read from the request, so even a valid HMAC signature can't write to or run a script on a closed environment. Changing the policy requires redeploying.

  • Bridge-side SQL classification is the only authority. The client does a cosmetic classification for agent ergonomics, but the PHP bridge classifies every statement itself (read / write / forbidden) and never trusts the client's judgment.

  • Read-only transactions, one statement per call. Reads run inside START TRANSACTION READ ONLY + ROLLBACK; PDO has multi-statements and emulated prepares disabled; a bare ; outside a string/comment is rejected.

  • Hard blacklist, rejected in every mode: DROP DATABASE, CREATE USER, GRANT, REVOKE, SET GLOBAL, INTO OUTFILE, INTO DUMPFILE, LOAD DATA, LOAD_FILE(, SHUTDOWN, KILL, INSTALL PLUGIN, CALL, PREPARE, HANDLER, LOCK TABLES.

  • Bounds. At most 1000 rows and 2 MB per response (truncated: true beyond that), a 15 s statement timeout.

  • Backup before write. A real (non-dry-run) write requires a backup_ref from a backup taken on the touched table less than 30 minutes earlier; destructive tools default to dry_run: true.

  • No secrets anywhere in output. Credentials, passwords, and HMAC secrets never appear in responses, logs, or error messages; PDO errors and script output are sanitized first.

  • Audit on both sides. Every call is logged by the bridge (<privateRoot>/wrb/logs/wrb.log, outside the web root) and by the client (~/.wp-remote-bridge/audit.jsonl): timestamp, environment, action, a SHA-256 fingerprint of the SQL or script, truncated SQL, duration, rows/affected or status — never the SQL parameters or any secret.

Requirements

  • Node.js ≥ 20

  • A target site running PHP ≥ 8.1 with the pdo_mysql extension

  • MySQL or MariaDB reachable as localhost from PHP on that host (this is exactly the case the tool is built for: the database isn't reachable from outside)

  • FTP, FTPS, or SFTP access to the site

Install & build

git clone https://github.com/seangoogoo/wp-remote-bridge-mcp.git
cd wp-remote-bridge-mcp
npm install
npm run build        # tsc → dist/
npm run gen-secret    # prints a 64-hex-character HMAC secret, one per environment

Configuration

Nothing site-specific is committed to this repository. You describe each site in your own, gitignored envs.json (see envs.example.json for the shape, with no secrets):

{
  "<env-name>": {
    "label": "human-readable label",
    "siteUrl": "https://example.com",
    "transport": {
      "type": "local" | "ftp" | "ftps" | "sftp",
      "host": "…",        // required unless type is "local"
      "port": 22,          // optional
      "webRoot": "/www",   // where the bridge files get dropped
      "privateRoot": "/"   // where wrb/ (outside the web root) is created; must not be inside webRoot
    },
    "dbCredentials": {
      "mode": "wp-config"  // reads literal define('DB_*', '…') from wp-config.php, no execution
      // or:
      // "mode": "include", "includePath": "../secrets.php",
      // "constants": { "name": "DB_NAME", "user": "DB_USER", "password": "DB_PASSWORD", "host": "DB_HOST" }
    },
    "serverSecret": {
      "mode": "file"       // bridge_deploy drops the HMAC secret at <privateRoot>/wrb/secret
      // or:
      // "mode": "env", "name": "SOME_CONSTANT" // exposed by the dbCredentials "include" file; requires that mode
    },
    "tablePrefix": "wp_",  // WordPress table prefix, used by the wp_option_* tools (default "wp_")
    "allowWrite": false,   // SQL writes; false by default
    "allowRun": false      // wp_run; false by default
  }
}

Credentials and secrets are never stored in envs.json. They're read from the process environment (e.g. via .envrc/direnv):

Variable

Purpose

WRB_ENVS

Path to your envs.json

WRB_HOME

Local state + audit log directory (default ~/.wp-remote-bridge)

WRB_SECRET_<ENV>

HMAC secret for environment <ENV>

WRB_FTP_USER_<ENV> / WRB_FTP_PASSWORD_<ENV>

FTP/SFTP credentials for <ENV> (not needed for a "local" transport)

Naming rule (envVarName in src/config.ts): <PREFIX>_<ENV-UPPERCASED-WITH-DASHES-AS-UNDERSCORES>, e.g. environment site-staging → WRB_SECRET_SITE_STAGING, WRB_FTP_USER_SITE_STAGING.

See .env.example for a template to copy into .envrc or your shell environment.

Registering with an MCP client

For Claude Code (or any client reading .mcp.json):

{
  "mcpServers": {
    "wp-remote-bridge": {
      "command": "node",
      "args": ["/absolute/path/to/wp-remote-bridge-mcp/dist/index.js"],
      "env": {
        "WRB_ENVS": "/absolute/path/to/your/envs.json",
        "WRB_SECRET_STAGING": "…",
        "WRB_FTP_USER_STAGING": "…",
        "WRB_FTP_PASSWORD_STAGING": "…"
      }
    }
  }
}

The Inspector doesn't inherit your shell environment either; pass variables explicitly:

npx @modelcontextprotocol/inspector --cli -e WRB_ENVS=/tmp/envs.json -e WRB_HOME=/tmp/wrb-home \
  -e WRB_SECRET_LOCAL=<hex> node dist/index.js --method tools/list

Typical workflow

  1. bridge_deploy — drops bridge.php, run.php, .htaccess at a random path under webRoot, creates wrb/ (scripts/, logs/, nonces/) outside the web root, and bakes the environment's allowWrite/allowRun policy into the bridge. Re-running it redeploys in place (needed after any policy change).

  2. bridge_enable — opens the kill switch for up to 8 hours (2 h default).

  3. Work: db_list_tables / db_describe / db_query / db_explain to read; db_backup_table then db_execute (with confirm and a fresh backup_ref) for a guarded write; or upload and run a script:

    • wp_script_upload uploads a PHP script to wrb/scripts/ and returns its SHA-256.

    • wp_run executes it with WordPress loaded, verifying that SHA-256 first. Scripts that process many items must batch themselves: a positional <label> [offset] [limit] convention (shared hosts cut long-running HTTP requests), each call reporting how many items it processed and the next offset, and must be idempotent — rerunning a batch must not create duplicates. The bridge installs a minimal WP_CLI shim: log()/warning()/success() append to the captured output, error() appends an error line, sets status: 'error', and halts the script (like WP_CLI::halt); WP_CLI::runcommand() is not supported and throws — no nested WP-CLI subcommands.

    • wp_script_remove deletes the script once you're done with it.

  4. bridge_disable — close the kill switch when done (it also expires on its own).

After upgrading wp-remote-bridge, run bridge_deploy again on each environment: the bridge file embeds its configuration (policy, table prefix) and only supports the actions it was rendered with.

Tools

Tool

Main inputs

Effect

Guard

bridge_status

env

Local deploy state, then a signed ping if deployed: bridge version, environment, DB engine/version, PHP version, kill-switch expiry. A disabled or absent bridge is returned as data, not an error.

—

bridge_deploy

env

Deploys/redeploys bridge.php, run.php, .htaccess; creates wrb/; bakes in allowWrite/allowRun.

—

bridge_enable

env, minutes (≤ 480)

Opens the kill switch.

—

bridge_disable

env

Closes the kill switch.

—

bridge_remove

env

Deletes the bridge files and confirms a 404 afterwards. wrb/ (logs, secret, scripts) is kept outside the web root.

—

db_list_tables

env, like?

Tables: engine, estimated rows, size.

read-only

db_describe

env, table

Columns, indexes, SHOW CREATE TABLE.

read-only

db_query

env, sql, params?, limit?

SELECT/SHOW/DESCRIBE/EXPLAIN, ≤ 1000 rows / 2 MB.

I1, I2, I5

db_explain

env, sql

EXPLAIN FORMAT=JSON, without executing the statement.

read-only

db_backup_table

env, table, where?

Paginated SQL export to a timestamped local backup; returns a backup_ref.

read-only

db_execute

env, sql, params?, confirm, backup_ref, dry_run=true

INSERT/UPDATE/DELETE/REPLACE against a single table. With dry_run, runs inside a transaction and rolls back, still reporting affected; refused when the statement mentions a non-InnoDB table or a view.

allowWrite (I6), fresh backup_ref (I7), confirm

wp_option_get

env, name

Reads one row of <tablePrefix>options. PHP-serialized values are decoded (decoded: true) unless they contain an object, in which case the raw string is returned (decoded: false). Values over 2 MiB are not returned (truncated: true, bytes). Reads the database directly: the WordPress object cache may differ.

read-only

wp_option_set

env, name, value, create=false, confirm, backup_ref, dry_run=true

Writes one row of <tablePrefix>options. Arrays/objects and options already stored serialized are written with PHP serialize(); scalars follow WordPress conventions (true → '1', false/null → ''). Refuses options holding PHP objects; a missing option needs create: true. dry_run only reads and reports would_affect / op (no write is sent to the database).

allowWrite (I6), fresh backup_ref on <tablePrefix>options (I7), confirm

db_search_replace

env, table, search, replace, columns?, after?, limit? (≤ 500, default 200), dry_run=true, confirm?, backup_ref?

Serialization-aware search/replace in one table with a single-column primary key (PHP-serialized and double-serialized values are rewritten with correct lengths; cells holding PHP objects or invalid serialized data are skipped and counted). Processes one batch per call in primary-key order: call again with after = next_after until it is null. A real run locks the batch (SELECT … FOR UPDATE) and writes it in one transaction, and is refused on non-InnoDB tables. dry_run (counts only, read-only transaction) is allowed even when allowWrite is false.

real run: allowWrite (I6), confirm, fresh backup_ref on that table (I7)

db_schema_refresh

env

Refreshes the cached schema snapshot backing the wrb://{env}/schema resource.

read-only

wp_script_upload

env, localPath

Uploads a script to wrb/scripts/, returns its SHA-256.

—

wp_run

env, script, args[], user?, defines?, confirm

Runs an uploaded script with WordPress loaded; returns captured output.

allowRun (I6), fingerprint check, confirm

wp_script_remove

env, script

Deletes a script from wrb/scripts/.

—

MCP resource: wrb://{env}/schema — a cached table/column schema snapshot for env, useful as background context for an agent (kept fresh by db_schema_refresh).

Testing

npm test          # vitest
npx tsc --noEmit

Pure TypeScript tests (config parsing, HMAC signing, tool wiring against an in-memory MCP transport) run everywhere. The PHP bridge integration tests additionally spin up php -S against a rendered bridge and need a local MySQL/MariaDB instance with a wrb_test database and a wrb_test/wrb_test_pw user (see tests/helpers/bridge-harness.ts); they detect that setup automatically and are skipped if it's not present.

Limitations

  • Not a general file manager: only the bridge's own files and wp_run scripts are written by FTP/SFTP. Managing the rest of the site's files is out of scope.

  • No phpMyAdmin scraping — that approach is fragile and version-dependent.

  • db_execute only targets a single table per statement (needed to determine which backup is required); multi-table writes must be split into several calls.

  • db_execute with dry_run relies on a transaction rollback, so the bridge refuses a dry run whose SQL mentions a non-InnoDB table or a view (conservative whole-word match on table names). Writes made by triggers on other tables are not detected.

  • wp_run has no nested WP-CLI subcommand support (WP_CLI::runcommand() throws) — it's a minimal wp eval-file equivalent, not a full WP-CLI shim.

  • Destructive DDL (DROP DATABASE, GRANT, CREATE USER, …) is hard-blocked everywhere, with no override.

  • The tool has no built-in notion of "production": closing an environment to writes is a configuration choice (allowWrite/allowRun: false) you make per site, not something the tool infers.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to interact with WordPress sites over MCP, providing read-only tools for AEO/GEO readiness, AI visibility, traffic, request logs, bot identification, page checks, and schema/markdown previews, plus opt-in write tools to draft, edit, and publish posts with permission checks and auditing.
    2
    GPL 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP-compatible AI agents to securely manage multiple self-hosted WordPress sites, including content editing, theme management, and maintenance operations with fine-grained permission controls.
    -
  • A
    license
    B
    quality
    C
    maintenance
    Enables MCP-compatible clients to securely operate a WordPress site through a signed REST API, managing plugins, content, themes, menus, media, users, WooCommerce, Elementor, audits, and allowlisted WP-CLI commands.
    58
    65 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants and agents to communicate directly with a WordPress environment, executing PHP code and WP-CLI commands, inspecting and editing theme and plugin files in a sandbox, and managing posts, content, and Gutenberg blocks in real time.
    MIT