Skip to main content
Glama
isaganiesteron

WordPress MCP Server

WordPress MCP Server

A Model Context Protocol (MCP) server that gives ClickUp Brain direct execution access to WordPress — publishing, scheduling, and updating posts, setting featured images, and writing RankMath SEO meta — across every Contractor Scale client site, without relaying through Donna.

Runs on Cloudflare Workers, following the same architecture as the sibling MCPs in this fleet (google-ads-mcp-cs, meta-ads-mcp-cs, gsc-mcp-cs): a single-file Worker, dual SSE/streamable-HTTP transport, static API-key auth, in-memory session map.

Why this exists

Brain had the WordPress Connector skill (knowledge of how to structure WP REST calls) but no execution path of its own — every publish/update request had to be delegated to Donna via the Donna MCP, which introduced stale credential-path bugs and premature "blocked on credentials" messages before Donna even attempted execution. This Worker gives Brain the same publish/edit/schedule capability Donna has, in one loop. See OPERATIONS-11119.

Related MCP server: wp-mcp-control-server

Features

  • 8 WordPress tools covering publish/schedule, update, list/query, featured images, RankMath SEO meta, and a raw-request escape hatch (see Available Tools)

  • Supabase-backed multi-tenant credentials — reads wp_credentials per client_slug (54+ client sites), the same table and application-password auth model as contractor-scale/skills/wordpress

  • Dual Transport: SSE (/sse) and Streamable HTTP (/mcp) — ClickUp Brain connects via /mcp

  • Cloudflare Workers: Serverless, global edge network

  • Fail-closed credential resolution: a client with no stored application password errors clearly instead of hanging or silently succeeding

Available Tools

Tool

Purpose

wp_list_clients

List every client site configured in Supabase wp_credentials (no secrets returned)

wp_list_posts

Query posts on a client site by status, search term, or slug

wp_get_post

Get a single post by ID (e.g. to confirm a publish/update landed)

wp_publish_post

Create a post — status: "draft" | "publish" | "future" (+ date) for scheduling

wp_update_post

Update an existing post's content, status, slug, or scheduled date

wp_set_featured_image

Fetch an image from a URL, upload it to the site's media library, and assign it as a post's featured image

wp_set_seo_meta

Set RankMath SEO title/description/focus keyword on a post. Rejects non-RankMath sites server-side (checks wp_credentials.seo_plugin) before making any WP call; also requires RankMath's show_in_rest enabled for those meta keys on the target site

wp_raw_request

Escape hatch for anything not covered above (e.g. DELETE /wp-json/elementor/v1/cache) — logs a warning so recurring use can graduate to a dedicated tool

Every tool except wp_list_clients takes a client argument (the wp_credentials.client_slug).

Getting Started

1. Prerequisites

  • Node.js 18+ and npm

  • A Cloudflare account with Workers enabled

  • Access to the cs-shared Doppler project (for SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY)

2. Clone and install

git clone https://github.com/isaganiesteron/wordpress-mcp-cs.git
cd wordpress-mcp-cs
npm install

3. Configure local secrets

cp wrangler.jsonc.example wrangler.jsonc
cp .dev.vars.example .dev.vars

Fill in .dev.vars with real values (never commit this file — it's gitignored):

API_KEY=<any local test value>
SUPABASE_URL=<from Doppler cs-shared/dev>
SUPABASE_SECRET_KEY=<Doppler's SUPABASE_SERVICE_ROLE_KEY value>

Pull real values without ever printing them, if Doppler CLI is set up:

doppler run --project cs-shared --config dev -- <write env vars to .dev.vars>

4. Test locally

npm run dev
curl http://localhost:8787/

5. Deploy to Cloudflare Workers

npm run deploy

SUPABASE_URL / SUPABASE_SECRET_KEY are synced automatically to the deployed Worker by the centralized pipeline in the contractor-scale repo (tools/scripts/maintenance/sync-doppler-to-cloudflare-worker.js, TARGETS.wordpress-mcp) on a daily cron + Doppler webhook. API_KEY is never auto-synced (by design, per that script's NEVER_AUTO set — it's a unique per-worker secret) and must be set once manually:

wrangler secret put API_KEY

If you set API_KEY and then the sync script runs wrangler@3 secret bulk for the Supabase keys, re-verify API_KEY still authenticates afterward — during this Worker's initial deploy, that sequence caused API_KEY to briefly stop resolving until it was re-set with wrangler@4. Only seen once; flagged here in case it recurs.

Using with ClickUp Brain

  1. Deploy this Worker (above)

  2. In ClickUp, go to App Center → MCP Servers → Connect an MCP Server

  3. Fill in:

    • Name: WordPress MCP

    • URL: https://wordpress-mcp.isagani.workers.dev/mcp — use /mcp, not /sse. Brain's setup flow does a GET that expects a response to fully complete; /sse holds the connection open indefinitely (that's correct SSE behavior, but it reads as a stuck/hung setup in the UI).

    • Authentication Method: if there's no direct "API Key" option, use Custom Headers with header name X-API-Key and the deployed API_KEY value

  4. Run the smoke test in TEST_PROMPT.md

Project Structure

.
├── src/
│   └── index.ts          # Entire MCP server: config, Supabase/WP helpers, tools, framework code
├── test/
│   └── index.spec.ts     # Vitest (currently the stock starter test — needs real coverage)
├── postman/               # Postman collection (currently the starter's example-tool requests)
├── TEST_PROMPT.md         # WordPress MCP smoke test prompts (for ClickUp Brain or manual curl)
├── wrangler.jsonc.example # Copy to wrangler.jsonc (gitignored)
├── .dev.vars.example      # Copy to .dev.vars (gitignored) — API_KEY, SUPABASE_URL, SUPABASE_SECRET_KEY
├── package.json
├── tsconfig.json
└── README.md

API Endpoints

Health check

  • GET / — server info + available transport endpoints (no API key required)

Streamable HTTP transport (/mcp) — what ClickUp Brain uses

  • POST /mcp — JSON-RPC message. On initialize, the server mints a session ID and returns it in the Mcp-Session-Id response header; subsequent requests must include that header.

  • DELETE /mcp — terminate a session (header shape-validated only, not persisted server-side)

SSE transport (/sse)

  • GET /sse — opens an SSE stream, emits an endpoint event with the session-specific message URL, then keeps the connection open with a 30s keepalive ping

  • POST /sse — direct HTTP fallback (no open stream)

  • POST /sse/message?sessionId={id} — send a JSON-RPC message on an active SSE session

Credential Resolution

Every tool (except wp_list_clients) calls resolveClient(client, env), which:

  1. Queries Supabase wp_credentials for the given client_slug via PostgREST (env.SUPABASE_URL / env.SUPABASE_SECRET_KEY)

  2. Fails closed with a clear error if no row exists, auth_type isn't application_password, or no token is stored

  3. Builds a Basic auth header (btoa(username:token), spaces stripped from the displayed application password) for the site's own /wp-json/... REST API

This mirrors contractor-scale/skills/wordpress/scripts/_wp.js — same table, same auth model, same fail-closed error messages — just reading from Worker secrets instead of process.env, with no local-file fallback (Workers have no filesystem).

Known Risks / Things to Verify Per-Site

  • RankMath meta over REST depends on show_in_rest being registered for rank_math_title/rank_math_description/rank_math_focus_keyword on that specific site. Confirmed working on at least one site in production; not guaranteed across all 54+. (wp_set_seo_meta does enforce seo_plugin === 'rankmath' server-side, but can't verify show_in_rest without attempting the write.)

  • Redirects are out of scope — RankMath/Yoast expose no REST redirect route; that's handled separately by contractor-scale/skills/wordpress/scripts/wp-redirect.js via the Redirection plugin.

  • First request to a given client's live site through a cold Worker isolate can take 10-25s (TLS/DNS through the Workers runtime) — a retry succeeds quickly. Not a bug, just latency to expect on the first call after a deploy.

Troubleshooting

"client 'X' has no token stored" / "auth_type='X'; only supports 'application_password'"

The wp_credentials row for that slug either doesn't have an application password provisioned, or uses a different auth method. Provision one in WordPress: Users → Profile → Application Passwords.

ClickUp Brain's "Connect an MCP Server" spinner never finishes

You're pointed at /sse instead of /mcp. See Using with ClickUp Brain above.

wp_set_seo_meta succeeds (HTTP 200) but the meta doesn't show up in RankMath

That site likely doesn't have show_in_rest enabled for the RankMath meta keys. This is a per-site WordPress/plugin configuration issue, not a bug in this Worker.

Deployment fails

  • wrangler login / confirm the right Cloudflare account (isagani, account ID e9251afb5c2abd46a9504aa5d714aceb) via CLOUDFLARE_ACCOUNT_ID

  • Confirm the worker name in wrangler.jsonc is wordpress-mcp

Resources

License

MIT (scaffolded from isaganiesteron/typingmind-mcp-cloudflare-starter)

Related MCP Connectors

Related MCP Servers