WordPress Deep Dive
Once you install mcp-adapter, your AI can directly read and write your site — but you also just exposed /wp-json/mcp/mcp-adapter-default-server to the public internet. That REST endpoint accepts any request carrying a WordPress Application Password, including those from bots, scanners, and prompt injection attackers.
If you don't put a Cloudflare Zero Trust layer in front, mcp-adapter is just a nicely packaged, easily enumerable REST API.
This article picks up from the June 17 Cloudflare Tunnel zero-port pattern and the June 30 WordPress 7.0 MCP integration walkthrough, and serves as the security closer for the AI integration layer. I cover the 6 real traps I hit, plus how to use Cloudflare Tunnel + Zero Trust Service Auth + mcp-adapter's own ability allowlist to compress the MCP endpoint down to "only my local Claude Code can call it."
Architecture: From Default mcp-adapter Exposure to Zero Trust
By default, mcp-adapter uses the WordPress REST API route:
/wp-json/mcp/mcp-adapter-default-server
This endpoint authenticates with WordPress Application Password. Anyone with a username + Application Password (including bots) can call every Ability you've registered — things like site-audit/find-broken-links, content/bulk-update-meta, and any custom Ability you've built.
After wiring it through Cloudflare Tunnel, the traffic path becomes:
Claude Code (local) → Cloudflare Edge (Zero Trust self-serve login) → cloudflared (loopback) → 127.0.0.1/wp-json/mcp
The key changes are:
1. **No public TCP port is listening** — cloudflared is outbound-only
2. HTTP layer hits Zero Trust first — requests without a Service Token or email OTP get 403'd at Cloudflare's edge
3. REST layer still requires Application Password — two layers of auth
4. mcp-adapter 0.5.0 ability allowlist — only expose the Abilities you actually need
Prerequisites
Confirm your environment before starting:
- WordPress 7.0+ (released 2026-05-20, mcp-adapter merged into core)
- `WordPress/mcp-adapter` plugin v0.5.0 (GitHub releases, 2026-06 introduced the observability subsystem)
- WordPress Abilities API (merged into 6.9+ core, `WordPress/abilities-api` repo archived 2026-02)
- A Cloudflare account (Free tier is enough — Zero Trust doesn't require a paid plan)
- A domain already on Cloudflare (a subdomain like `mcp.example.com`)
- Cloudflare Tunnel already configured (see the June 17 article for cloudflared setup)
Verify:
# Verify mcp-adapter is installed
wp plugin list --allow-root | grep mcp-adapter
# Expected: mcp-adapter 0.5.0 active ...
# Verify the Abilities API is registered
wp eval 'if ( function_exists( "wp_register_ability" ) ) { echo "OK"; } else { echo "NEED 6.9+"; }' --allow-root
# Verify cloudflared is running
ps aux | grep -E "[c]loudflared"
# Expected: at least one line containing "tunnel"
Core Deployment
Step 1: Restrict the mcp-adapter Default Server to Read-Only Abilities
The mcp_adapter_server_config filter introduced in mcp-adapter 0.5.0 lets you trim the default server's exposed ability set (PR #217 merged 2026-06-17).
Add this to a custom plugin or mu-plugin:
Activate the mu-plugin, then verify:
curl -s -u "your_user:your_app_password" \
"https://your-domain.com/wp-json/mcp/mcp-adapter-default-server" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'
# Expected output should only include tools named site-audit/* or content/read-*
If content/bulk-update-meta or any write tool shows up, the filter didn't run — 99% of the time that's a mu-plugin load order issue. Move the file to /wp-content/mu-plugins/ instead of dropping it in the theme's functions.php.
Step 2: Wire mcp.example.com Into Your Existing Tunnel
If you already have cloudflared running from the June 17 setup, don't open a new tunnel for MCP — just add a route:
# ~/.cloudflared/config.yml (append, don't overwrite)
tunnel:
credentials-file: /root/.cloudflared/.json
ingress:
# Existing rules stay as-is...
- hostname: mcp.example.com
service: http_status:404
originRequest:
noTLSVerify: true
- service: http_status:404
Then add a public hostname in the Cloudflare Zero Trust dashboard:
1. Zero Trust → Networks → Tunnels → select your tunnel → Configure → Public Hostname
2. Subdomain: mcp
3. Domain: example.com
4. Service Type: HTTP
5. URL: localhost
Note the http_status:404 placeholder first — in the next step you'll add a Zero Trust policy, and the tunnel must NOT forward to wp-json before that policy exists.
Restart cloudflared:
sudo systemctl restart cloudflared
sudo systemctl status cloudflared
Step 3: Add a Zero Trust Self-Hosted App for mcp.example.com
In the Zero Trust dashboard:
1. Access → Applications → Add an application → Self-hosted
2. Name: WordPress MCP (Personal)
3. Session duration: 24 hours (don't pick Always — Service Tokens will silently fail)
4. Application domain: mcp.example.com
5. Identity providers: pick One-time PIN (email verification code)
Add two Policies:
Policy 1 - My Claude Code Uses Service Auth
Name: Local Claude Code
Action: Allow
Session duration: 24 hours
Apply to: Service Auth -
Policy 2 - Browser Debug Uses OTP
Name: Browser Debug
Action: Allow
Session duration: 1 hour
Apply to: Emails - your-email@example.com
The Service Auth policy MUST come first (it's more specific). Anything that doesn't match either policy is denied.
Step 4: Embed the Service Token in Your Claude Code MCP Config
Cloudflare Service Tokens authenticate with a Client ID + Client Secret long-lived credential (not a JWT, and they can be revoked).
Generate: Access → Service Auth → Create Service Token → name it local-claude-code → grab the Client ID and Client Secret.
In Claude Code's MCP config:
{
"mcpServers": {
"wordpress-mcp": {
"url": "https://mcp.example.com/wp-json/mcp/mcp-adapter-default-server",
"headers": {
"CF-Access-Client-Id": "",
"CF-Access-Client-Secret": "",
"Authorization": "Basic " + base64("your_wp_user:your_app_password")
}
}
}
}
Or via claude mcp add-json:
claude mcp add-json wordpress-mcp '{
"url": "https://mcp.example.com/wp-json/mcp/mcp-adapter-default-server",
"headers": {
"CF-Access-Client-Id": "xxx.service-token",
"CF-Access-Client-Secret": "xxx",
"Authorization": "Basic '"$(echo -n 'your_user:your_app_password' | base64)""'"
}
}'
Verify:
claude mcp list
# Expected: wordpress-mcp: https://mcp.example.com/wp-json/mcp/mcp-adapter-default-server (14 tools)
Step 5: Enable mcp-adapter Observability for Audit Logging
The 0.5.0 observability handler invokes record_event() after each tools/call. Hook it into WordPress's debug log:
add_action( 'mcp_adapter_observability_event', function( $handler, $event, $payload ) {
if ( 'mcp.request' !== $event ) {
return;
}
if ( 'success' !== ( $payload['status'] ?? 'unknown' ) ) {
error_log( sprintf(
'[MCP] %s user=%s tool=%s',
$payload['status'],
$payload['user'] ?? 'anon',
$payload['method'] ?? 'unknown'
) );
}
}, 10, 3 );
Write to /var/log/nginx/wordpress-access.log or wp-content/debug.log (logrotate 7 days retention is fine).
Pitfalls: 6 Real Production Traps
Trap 1: mcp-adapter Default Server Exposes Every Registered Ability
**Symptom**: Right after installing mcp-adapter, tools/list returns every registered Ability, including destructive ones like plugin/install and user/create.
**Root cause**: The default server includes every registered Ability by default. The mcp_adapter_server_config filter was only introduced in 0.5.0.
**Fix**: You MUST install the mu-plugin from Step 1. If you skip this step, mcp-adapter is just a nicely packaged, easily enumerable REST API — anyone with an Application Password (including prompt injection attackers) can call plugin/install and drop a webshell on your site.
Trap 2: Wrong Application Password Setup Triggers 401 Storms on /wp-json/mcp
**Symptom**: nginx access log shows massive 401s, Claude Code keeps retrying, and mcp-adapter observability is full of auth_failed.
**Root cause**: WordPress 6.9+ introduced the application_password_is_api_request filter — Application Password requests must hit specific REST paths AND HTTP Basic Auth must use user_login, not user_email.
Fix:
// Don't use email, use login
$auth = 'Basic ' . base64_encode( 'my_wp_login:abcd EFGH ijkl MNOP qrst UVWX' );
// Note: keep the spaces inside the Application Password
On WordPress 6.8 and below, using the email works. On 7.0+ it returns 401. Verify:
curl -s -o /dev/null -w "%{http_code}" -u "your_login:your_app_pwd" \
"https://your-domain.com/wp-json/mcp/mcp-adapter-default-server" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Must return 200. 401 means the username format is wrong.
Trap 3: cloudflared → 127.0.0.1 over HTTPS Triggers 526 Invalid SSL
**Symptom**: Right after deployment, https://mcp.example.com/wp-json/mcp/... returns 526 Invalid SSL Certificate.
**Root cause**: cloudflared forwarding to http://localhost is fine, but your nginx is listening on 443 and only accepts SNI for example.com. Requests for mcp.example.com get a 526 from nginx.
Fix: Pick one of two paths:
Path A (recommended): Use a Cloudflare Origin Certificate
1. Cloudflare dashboard → SSL/TLS → Origin Server → Create Certificate
2. Cover *.example.com and example.com
3. Install the cert and key on nginx, enable HTTPS for mcp.example.com
4. Change cloudflared to forward https://localhost:443 (keep noTLSVerify: true)
Path B: cloudflared with noTLSVerify + Force HTTP
ingress:
- hostname: mcp.example.com
service: http://127.0.0.1:80
originRequest:
noTLSVerify: true
httpHostHeader: mcp.example.com
And configure nginx with a port-80 vhost for mcp.example.com, not port 443.
Trap 4: Zero Trust Policy Order — Service Auth Gets Stealthered by OTP
**Symptom**: Service Token requests get bounced to Cloudflare's OTP login page (/cdn-cgi/access/login). Claude Code receives HTML instead of JSON and fails to parse.
Root cause: Access policies match in order. Your OTP policy is sitting before the Service Auth policy. Email matches are wider than named Service Auth policies, so they win.
Fix: Drag the Service Auth policy to the top. Verify the order:
# Direct call with Service Token
curl -s -H "CF-Access-Client-Id: xxx" -H "CF-Access-Client-Secret: yyy" \
"https://mcp.example.com/wp-json/mcp/mcp-adapter-default-server" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Must return JSON directly, NOT a 302 redirect to the login page
If you still get 302, the policy order is wrong. Or your Service Token is named default — Cloudflare doesn't allow reserved words as Service Token names.
Trap 5: Abilities API Detection Fails — mcp-adapter Starts Silently Empty
**Symptom**: After installing mcp-adapter, tools/list returns an empty array, but mcp-adapter-default-server is registered. The site is on WordPress 6.9+, wp_register_ability exists, but wp_get_abilities() returns [].
**Root cause**: Ability registration hook timing. Most plugins register on the init hook, but mcp-adapter starts collecting on plugins_loaded — registration happens AFTER collection.
Fix: Move the registration hook earlier:
// Don't use init
add_action( 'plugins_loaded', function() {
wp_register_ability( 'site-audit/find-broken-links', [...] );
}, 5 ); // Priority 5, earlier than the default 10
mcp-adapter 0.5.0's PR #196 added a mcp_adapter_default_server_abilities filter on the default server. In a pinch, manually feed it an Ability list from a mu-plugin:
add_filter( 'mcp_adapter_default_server_abilities', function( $ids ) {
return [
'site-audit/find-broken-links',
'content/read-meta',
'content/read-taxonomies',
];
} );
Trap 6: Old cloudflared (2024.x) Doesn't Support Streamable HTTP
**Symptom**: Local Claude Code tries streamable HTTP for mcp-adapter, cloudflared log fills with 400 Bad Request: invalid HTTP version, but SSE works.
Root cause: cloudflared 2024.x still proxies over HTTP/1.1. HTTP/2 stream support only landed in early 2025 builds. mcp-adapter 0.5.0's streamable transport defaults to HTTP/2.
Fix:
cloudflared --version
# Must be 2025.4.0 or newer
If your distro's repo has an old cloudflared (Ubuntu 22.04 ships 2024.8.0 by default), use Cloudflare's official apt source:
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared
Hardening: Make mcp-adapter Truly Yours
After completing Steps 1-5, three more things you can tighten:
1. Add an IP Allowlist to the Default Server (Defense in Depth)
Cloudflare Tunnel's loopback IP is 127.0.0.1, but other services on the same machine might share that loopback. Add a localhost-only allowlist to your mcp.example.com nginx vhost:
location /wp-json/mcp/ {
allow 127.0.0.1;
allow ::1;
deny all;
try_files $uri $uri/ /index.php?$args;
}
Even if someone bypasses Cloudflare and hits nginx directly, the public can't reach it.
2. Rate-Limit the mcp-adapter Endpoint
mcp-adapter uses the WordPress REST API, but you can give /wp-json/mcp/ its own limit_req on nginx:
limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=10r/m;
location /wp-json/mcp/ {
limit_req zone=mcp_limit burst=20 nodelay;
try_files $uri $uri/ /index.php?$args;
}
10 r/m is plenty for Claude Code's daily use, burst=20 prevents accidental 429s.
3. Lock Down Transport Methods via mcp-adapter 0.5.0
The 0.5.0 mcp_adapter_server_config filter supports allowed_methods:
$config['allowed_methods'] = [ 'tools/list', 'tools/call' ];
// Don't allow 'resources/read', 'prompts/get' — mcp-adapter doesn't need them
Reduces attack surface.
Wrap-up
June 17 covered Cloudflare Tunnel to give your site zero public ports. June 30 covered mcp-adapter so AI can directly read and write the site. This article is the security bridge between them: mcp-adapter's default endpoint must pass through Zero Trust, Abilities must be allowlisted, cloudflared must be on a recent version, and observability must log to disk.
Full checklist:
- [ ] Step 1: `mcp_adapter_server_config` filter to restrict read-only Abilities
- [ ] Step 2: cloudflared ingress adds `mcp.example.com`
- [ ] Step 3: Zero Trust self-hosted app + Service Auth policy
- [ ] Step 4: Service Token embedded in Claude Code MCP headers
- [ ] Step 5: observability handler writes error logs to debug.log
- [ ] Hardening: nginx IP allowlist + rate-limit + transport method lockdown
mcp-adapter 0.5.0 (2026-06 release) and its observability subsystem are the lynchpin of this architecture. Earlier versions had no mcp_adapter_observability_event hook — every MCP call was a black box.
Next direction: WordPress 7.0 real-time collaboration (HTTP polling sync provider + multi-user edit conflict resolution), or a complete mcp-adapter 0.5.0 benchmark (latency comparison across different Abilities).
👉 Join MiniMax Token Plan: AI coding acceleration for businesses
👉 Join Zhipu Coding Plan: GLM-4.6/GLM-5 coding packages, China-stable, pay-per-token unlimited
👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: