WordPress Plugins
Free Tools
Pricing Blog Case Studies Switch to Royal Plugin Graveyard Support My Account Cart
Support / Royal MCP / Diagnose with curl

Diagnose Royal MCP Connection Failures with curl

The complete curl-based diagnostic walkthrough for Royal MCP OAuth failures. Covers the 4-probe sequence (discovery, protected resource, register, MCP endpoint), the dual-UA technique that surfaces UA-targeted blocks, how to read response headers, and a paste-ready bash + PowerShell script you can run against any site to capture a full diagnostic snapshot in seconds.

Need the 30-second version?

If you just want to know whether the problem is at your edge or inside WordPress, run the single probe in Step 0 of the troubleshooting checklist. This page is the deeper walkthrough for when you want to capture a complete diagnostic snapshot — useful for developers, support tickets, and anyone who needs to know exactly what each OAuth step looks like at the protocol level.

Who this is for

This walkthrough assumes basic familiarity with a terminal and HTTP. You don’t need to be a developer, but you should be comfortable copy-pasting commands and reading their output. If terminals are unfamiliar territory, start with the symptom-based router in the main troubleshooting checklist — it covers the same ground without requiring command-line work.

Specifically, you’ll find this page useful if you are:

Terminal setup

curl on every operating system

curl is preinstalled on Mac, Linux, and Windows 10+ (yes, Windows ships curl.exe at C:\Windows\System32\curl.exe). You probably already have it.

The one cross-platform footgun: Windows PowerShell aliases curl to Invoke-WebRequest, which has totally different flag syntax. Running the curl commands below in PowerShell with bare curl will throw “A parameter cannot be found that matches parameter name ‘sS’” or similar errors. Use curl.exe instead — the .exe suffix bypasses the alias and runs Windows’ built-in real curl with the same syntax as every other shell.

Quick syntax cheat sheet by shell
  • Mac Terminal, Linux, Windows cmd.exe, Git Bash, WSL: use curl
  • Windows PowerShell, PowerShell Core, Windows Terminal hosting PowerShell: use curl.exe

Throughout this doc, examples use curl. If you’re on PowerShell, mentally substitute curl.exe everywhere. Every flag and argument is identical otherwise.

Pretty-printing JSON output

Royal MCP’s OAuth endpoints return JSON. The raw output is on one line, which is hard to read. Three options for pretty-printing:

Option 1: jq (Mac, Linux, Windows-via-installer)

The gold standard. Install via Homebrew (brew install jq), apt (sudo apt install jq), or winget (winget install jqlang.jq). After install:

curl -sS https://example.com/.well-known/oauth-authorization-server | jq

Option 2: python -m json.tool (Mac, Linux, Windows — works everywhere Python is installed)

Built into Python. No install needed if you have Python. Works in cmd.exe, PowerShell, and *nix shells alike:

curl -sS https://example.com/.well-known/oauth-authorization-server | python -m json.tool

Option 3: PowerShell-native ConvertFrom-Json | ConvertTo-Json

If you’re in PowerShell anyway:

curl.exe -sS https://example.com/.well-known/oauth-authorization-server | ConvertFrom-Json | ConvertTo-Json -Depth 10

Or use Invoke-RestMethod which auto-parses JSON:

Invoke-RestMethod -Uri https://example.com/.well-known/oauth-authorization-server | ConvertTo-Json -Depth 10

Pretty-printing is optional — the diagnostic doesn’t depend on it. You can read the raw JSON output if you don’t want to install anything. Most of the examples below show output as it appears with pretty-printing applied, for readability.

The 4-Probe Sequence

Royal MCP’s OAuth flow involves four distinct endpoints, each of which can fail for different reasons. Probing all four with the same UA gives you a complete diagnostic baseline. Each probe is a single GET request — safe to run, doesn’t modify any state, no authentication required.

Run them in order. The output of each tells you whether to continue or where to jump.

Probe 1: OAuth Authorization Server Discovery

What it checks: whether Royal MCP’s OAuth discovery endpoint is healthy and returning the correct metadata shape.

curl -sS -i https://example.com/.well-known/oauth-authorization-server

The -i flag includes response headers in the output — important for several of the diagnoses below (cf-ray detection, content-type checks, redirect detection).

Healthy response shape (verified from demo.royalplugins.com):

HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: public, max-age=3600

{
  "issuer": "https://example.com",
  "authorization_endpoint": "https://example.com/authorize",
  "token_endpoint": "https://example.com/token",
  "registration_endpoint": "https://example.com/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:full"],
  "service_documentation": "https://royalplugins.com/support/royal-mcp/"
}

Three things to verify:

  1. issuer matches the URL you fetched (same domain, scheme, no auth. subdomain prefix)
  2. scopes_supported includes "mcp:full" (NOT openid, profile, email — those are CF Zero Trust signatures)
  3. Endpoint paths are at site root (/authorize, /token, /register) — NOT under /oauth/* prefix
If this probe alone fails

The probe sequence stops here. The other three probes won’t give you useful information if discovery itself is broken. Match your output to the patterns in Step 0 and jump to the specific fix doc.

Probe 2: OAuth Protected Resource Metadata

What it checks: the second discovery endpoint used by MCP clients for resource metadata (which OAuth server protects this resource, what scopes are required).

curl -sS -i https://example.com/.well-known/oauth-protected-resource

Healthy response shape:

HTTP/2 200
content-type: application/json; charset=utf-8

{
  "resource": "https://example.com/wp-json/royal-mcp/v1",
  "authorization_servers": ["https://example.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp:full"]
}

The resource field points to the MCP REST namespace (the actual protected resource that bearer tokens grant access to). The authorization_servers array lists which OAuth servers can issue valid tokens — this must include your site URL. If authorization_servers points to a different domain, you have the same discovery-hijack issue as Probe 1.

A 404 on this endpoint when Probe 1 succeeded means you’re on an older Royal MCP version — the protected-resource metadata endpoint was added later in the 1.4.x line. Not a blocker for OAuth — modern clients fall back to other discovery methods — but worth noting for completeness.

Probe 3: /register (GET)

What it checks: that the Dynamic Client Registration endpoint is registered as an OAuth route at the correct path. We probe with GET (not POST) because we’re testing route registration, not actually registering a client — the GET probe is safe and idempotent.

curl -sS -i https://example.com/register

Healthy response:

HTTP/2 405
content-type: application/json; charset=utf-8
cache-control: no-store, no-cache, must-revalidate
access-control-allow-methods: GET, POST, OPTIONS

{
  "error": "invalid_request",
  "error_description": "POST method required."
}

The 405 is correct — it confirms the route exists, just that it doesn’t accept GET. The JSON body uses the standard OAuth error format (not JSON-RPC), explaining what’s wrong from an OAuth client’s perspective.

What other responses mean

Probe 4: MCP Endpoint

What it checks: the actual MCP endpoint that Claude connects to after OAuth completes. This one’s a 401 by design (because we’re not sending a bearer token), but the 401 should come from Royal MCP, not from the edge layer.

curl -sS -i https://example.com/wp-json/royal-mcp/v1/mcp

Healthy response:

HTTP/2 401
content-type: application/json; charset=UTF-8
cache-control: no-store, no-cache, must-revalidate, private
www-authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"
allow: GET, POST, DELETE, OPTIONS

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32600,
    "message": "Authentication required. Use Authorization: Bearer <token> or X-Royal-MCP-API-Key header."
  }
}

Three healthy signals:

  1. Status is 401 (not 403, 404, or 500)
  2. Response body is a JSON-RPC error envelope (has jsonrpc: "2.0" and error fields)
  3. WWW-Authenticate header is present with Bearer scheme and a resource_metadata parameter pointing to Probe 2’s URL — this is the newer MCP-spec-compliant format that tells the client where to look up which OAuth server issues valid tokens
If you see a 401 in your browser instead of expected JSON

That’s actually correct behavior — the endpoint is API-only, not browser-viewable. The JSON-RPC error envelope is the right response, not a broken page. Don’t mistake this for an error in the plugin.

The Dual-UA Technique

Every probe above tested with curl’s default User-Agent. That’s a useful baseline, but it misses an entire class of failures: UA-targeted edge blocking, where your edge security (Cloudflare Bot Fight, cPanel ModSecurity, Apache Imunify360) allows browser UAs through but blocks Anthropic’s OAuth backend, which identifies itself as python-httpx/*.

To surface UA-targeted blocks, run each probe twice — once with a browser UA, once with python-httpx. If the results differ, you’ve found a UA-targeted block.

The four UAs that matter

UA string Used by Why test it
(curl default) Generic scripted clients Baseline; often passes most edge filters
python-httpx/0.27.0 Anthropic’s OAuth backend (claude.ai web /register, /token POSTs) Most likely UA to be blocked by edge security
Claude-User/1.0 Anthropic’s MCP session traffic (Claude Desktop and post-OAuth claude.ai) If this is blocked but python-httpx isn’t, the failure is at session establishment, not OAuth itself
Mozilla/5.0 ... Chrome/120.0.0.0 Real browsers (Claude.ai web UI for /authorize and the consent page) Confirms the “browser-side” portion of OAuth works

The two-line dual-UA probe (most useful one to start with)

Most edge blocks surface immediately on probe 1 (the discovery endpoint). Run these two commands back-to-back:

# Browser UA
curl -sS -i -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" https://example.com/.well-known/oauth-authorization-server

# python-httpx UA (Anthropic OAuth backend)
curl -sS -i -A "python-httpx/0.27.0" https://example.com/.well-known/oauth-authorization-server

The -A flag is curl shorthand for -H "User-Agent: ...". Cleaner to type.

How to interpret divergent results

Browser UA python-httpx UA Diagnosis
200 OK 200 OK No UA-targeted blocking. Edge layer is fine for OAuth. If Claude still fails, problem is plugin-side or downstream.
200 OK 403 + cf-ray Cloudflare Bot Fight Mode / AI Bots blocking python-httpx. Fix: CF Skip rule
200 OK 429 Too Many Requests Apache + Imunify360 / mod_security UA-fingerprint rule blocking python-httpx. Fix: Apache 429 fix
406 + “Mod Security” 406 + “Mod Security” cPanel ModSecurity blocking ALL non-trusted UAs. Fix: ModSec 406 fix
200, but issuer = auth.<sub> 200, but issuer = auth.<sub> CF Zero Trust Access hijacking discovery (UA-independent). Fix: CF Zero Trust fix
Persistence check — rules out burst rate limiting

If you see 429 on python-httpx, run the same probe again 60+ seconds later. If python-httpx still returns 429 while browser UA returns 200, the block is UA-targeted and persistent (Imunify360 / mod_security UA rule). If python-httpx returns 200 on the retry, the original 429 was burst rate limiting (less common in our experience but possible).

Reading Response Headers

The -i flag on curl includes response headers. Several headers are diagnostically valuable beyond just the status code.

Server

Identifies the web server software. Useful for matching to host-family gotchas:

cf-ray

Cloudflare-specific. Format: cf-ray: <hex-string>-<data-center-code>. Presence confirms the response came from CF’s edge (not your origin server). When debugging CF-related blocks, the absence of cf-ray while seeing server: cloudflare would be suspicious. The data-center code (e.g. IAD, SIN, LAX) tells you which CF POP served the request.

cf-cache-status

Cloudflare cache state. Values: HIT, MISS, BYPASS, DYNAMIC, EXPIRED, REVALIDATED. OAuth endpoints should be BYPASS or DYNAMIC — if you see HIT on an OAuth endpoint, that’s an OAuth-poisoning cache issue (CF is serving a cached OAuth response, which is incorrect for stateful auth flows).

cache-control

Royal MCP uses different cache-control headers per endpoint type, by design:

If you see the wrong cache-control on an auth or MCP endpoint (e.g. max-age=... instead of no-store), the OAuth response is being modified by an intermediate cache layer (LiteSpeed Cache, SpeedyCache, host-level edge cache, Cloudflare APO). That can poison OAuth state and break the flow.

content-type

OAuth endpoints return JSON (application/json; charset=utf-8). Other content-types indicate a problem:

location

Only present on 3xx redirects. Useful for the trailing-slash trap: if you see location: /register/ when you probed /register, that’s the canonicalization redirect that breaks OAuth POSTs.

www-authenticate

Should appear on the 401 response from the MCP endpoint. Format: www-authenticate: Bearer realm="Royal MCP". Required by RFC 6750 for proper bearer-token-protected resources. Absence isn’t fatal but is a diagnostic signal that the 401 may not be coming from Royal MCP itself.

Validating Response Content Shape

Status codes and headers tell you whether the request reached its target. The response body tells you whether what answered is actually Royal MCP — or something pretending to be it.

The healthy Royal MCP discovery shape

Probe 1 should return JSON matching this shape (with example.com substituted for your actual domain):

{
  "issuer": "https://example.com",
  "authorization_endpoint": "https://example.com/authorize",
  "token_endpoint": "https://example.com/token",
  "registration_endpoint": "https://example.com/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:full"],
  "service_documentation": "https://royalplugins.com/support/royal-mcp/"
}

Detecting hijacked discovery

If your discovery response is JSON but doesn’t match Royal MCP’s shape, something between you and WordPress has hijacked the endpoint. Compare against these four tells:

Field Royal MCP says Hijack signature
issuer Site root, e.g. https://example.com An auth. subdomain, e.g. https://auth.example.com
Endpoint paths At site root (/authorize, /token, /register) Prefixed with /oauth/ (/oauth/authorize, /oauth/token)
scopes_supported ["mcp:full"] Includes OIDC scopes: openid, profile, email, offline_access
token_endpoint_auth_methods_supported ["none", "client_secret_post"] Includes client_secret_basic

Any single one of these tells confirms hijacking. The most common culprit in 2026 is Cloudflare Zero Trust Access — when set up against your domain, CF intercepts /.well-known/ and serves its own OIDC discovery metadata. The fix is removal of the CF Access app + the auth.* CNAME — configuration won’t resolve it. Full walkthrough: CF Zero Trust hijack fix.

Detecting HTML-instead-of-JSON

If the response body starts with <!DOCTYPE html> or <html>, an interceptor (membership plugin, security plugin, theme template, or a redirect rule) is returning a page instead of letting Royal MCP respond. Common signatures:

Royal MCP 1.4.22+ auto-detects HTML-instead-of-JSON discovery and surfaces an admin notice with diagnostic info. Full walkthrough: OAuth discovery returns HTML.

The Complete Diagnostic Script

Copy-paste these scripts to capture a complete diagnostic snapshot of any site. Runs all 4 probes with all 4 UAs, captures status codes, key headers, and a snippet of each response body. Useful for batch diagnostics across multiple sites and for including in support tickets.

Bash version (Mac, Linux, Git Bash on Windows, WSL)

Save as diagnose-mcp.sh, make executable (chmod +x diagnose-mcp.sh), then run with your domain as the argument:

#!/usr/bin/env bash
# Usage: ./diagnose-mcp.sh https://example.com

set -u
SITE="${1:-}"
if [[ -z "$SITE" ]]; then
    echo "Usage: $0 https://example.com"
    exit 1
fi

# Strip trailing slash if present
SITE="${SITE%/}"

UAS=(
    ""
    "python-httpx/0.27.0"
    "Claude-User/1.0"
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"
)
UA_LABELS=(
    "default"
    "python-httpx"
    "Claude-User"
    "browser-chrome"
)

PROBES=(
    "/.well-known/oauth-authorization-server"
    "/.well-known/oauth-protected-resource"
    "/register"
    "/wp-json/royal-mcp/v1/mcp"
)
PROBE_LABELS=(
    "discovery"
    "protected-resource"
    "register-GET"
    "mcp-endpoint"
)

echo "=========================================="
echo "  Royal MCP Diagnostic for: $SITE"
echo "  $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "=========================================="

for i in "${!PROBES[@]}"; do
    PROBE="${PROBES[$i]}"
    PROBE_LABEL="${PROBE_LABELS[$i]}"
    echo ""
    echo "### Probe $((i+1)): $PROBE_LABEL ($PROBE)"
    echo "---"

    for j in "${!UAS[@]}"; do
        UA="${UAS[$j]}"
        UA_LABEL="${UA_LABELS[$j]}"

        if [[ -z "$UA" ]]; then
            RESULT=$(curl -sS -i -m 10 "$SITE$PROBE" 2>&1 | head -50)
        else
            RESULT=$(curl -sS -i -m 10 -A "$UA" "$SITE$PROBE" 2>&1 | head -50)
        fi

        STATUS=$(echo "$RESULT" | head -1 | grep -oE 'HTTP/[0-9.]+ [0-9]+ ?[A-Za-z ]*' | head -1)
        SERVER=$(echo "$RESULT" | grep -i '^server:' | head -1 | cut -d: -f2- | xargs)
        CF_RAY=$(echo "$RESULT" | grep -i '^cf-ray:' | head -1 | cut -d: -f2- | xargs)
        CONTENT_TYPE=$(echo "$RESULT" | grep -i '^content-type:' | head -1 | cut -d: -f2- | xargs)

        echo "  $UA_LABEL: $STATUS | server: ${SERVER:-?} | cf-ray: ${CF_RAY:-no} | content-type: ${CONTENT_TYPE:-?}"
    done
done

echo ""
echo "=========================================="
echo "  Diagnostic complete."
echo "  Compare results to https://royalplugins.com/support/royal-mcp/diagnose-mcp-with-curl.html"
echo "=========================================="

PowerShell version (Windows)

Save as diagnose-mcp.ps1, then run with:

.\diagnose-mcp.ps1 -Site https://example.com

If PowerShell complains about execution policy, run once: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

param(
    [Parameter(Mandatory=$true)]
    [string]$Site
)

# Strip trailing slash
$Site = $Site.TrimEnd('/')

$UAs = @(
    @{ Label = "default";         Value = "" }
    @{ Label = "python-httpx";    Value = "python-httpx/0.27.0" }
    @{ Label = "Claude-User";     Value = "Claude-User/1.0" }
    @{ Label = "browser-chrome";  Value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" }
)

$Probes = @(
    @{ Label = "discovery";          Path = "/.well-known/oauth-authorization-server" }
    @{ Label = "protected-resource"; Path = "/.well-known/oauth-protected-resource" }
    @{ Label = "register-GET";       Path = "/register" }
    @{ Label = "mcp-endpoint";       Path = "/wp-json/royal-mcp/v1/mcp" }
)

Write-Host "=========================================="
Write-Host "  Royal MCP Diagnostic for: $Site"
Write-Host "  $((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))"
Write-Host "=========================================="

for ($i = 0; $i -lt $Probes.Count; $i++) {
    $probe = $Probes[$i]
    Write-Host ""
    Write-Host "### Probe $($i + 1): $($probe.Label) ($($probe.Path))"
    Write-Host "---"

    foreach ($ua in $UAs) {
        $url = "$Site$($probe.Path)"
        $headers = @{}
        if ($ua.Value) { $headers["User-Agent"] = $ua.Value }

        try {
            $resp = Invoke-WebRequest -Uri $url -Headers $headers -Method Get `
                -MaximumRedirection 0 -UseBasicParsing -ErrorAction Stop `
                -TimeoutSec 10
            $status = "$([int]$resp.StatusCode) $($resp.StatusDescription)"
            $server = $resp.Headers["Server"]
            $cfRay  = $resp.Headers["cf-ray"]
            $ctype  = $resp.Headers["Content-Type"]
        } catch [System.Net.WebException] {
            $status = $_.Exception.Message
            $server = "?"; $cfRay = "?"; $ctype = "?"
            if ($_.Exception.Response) {
                $status = "$([int]$_.Exception.Response.StatusCode) $($_.Exception.Response.StatusDescription)"
                $server = $_.Exception.Response.Headers["Server"]
                $cfRay  = $_.Exception.Response.Headers["cf-ray"]
                $ctype  = $_.Exception.Response.Headers["Content-Type"]
            }
        }

        $cfRayDisplay = if ($cfRay) { $cfRay } else { "no" }
        $serverDisplay = if ($server) { $server } else { "?" }
        $ctypeDisplay  = if ($ctype) { $ctype } else { "?" }
        Write-Host ("  {0,-15} {1,-12} | server: {2} | cf-ray: {3} | content-type: {4}" -f $ua.Label, $status, $serverDisplay, $cfRayDisplay, $ctypeDisplay)
    }
}

Write-Host ""
Write-Host "=========================================="
Write-Host "  Diagnostic complete."
Write-Host "  Compare results to https://royalplugins.com/support/royal-mcp/diagnose-mcp-with-curl.html"
Write-Host "=========================================="

What clean output looks like

For a fully-healthy Royal MCP installation with no edge interference, you should see something like:

### Probe 1: discovery (/.well-known/oauth-authorization-server)
---
  default       200 OK       | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  python-httpx  200 OK       | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  Claude-User   200 OK       | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  browser-chrome 200 OK      | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8

### Probe 3: register-GET (/register)
---
  default       405          | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  python-httpx  405          | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  Claude-User   405          | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8
  browser-chrome 405         | server: nginx | cf-ray: no | content-type: application/json; charset=utf-8

### Probe 4: mcp-endpoint (/wp-json/royal-mcp/v1/mcp)
---
  default       401          | server: nginx | cf-ray: no | content-type: application/json; charset=UTF-8
  ...

All four UAs return identical results across all four probes. Status codes: 200 / 200 / 405 / 401. That’s the healthy baseline.

If any row diverges from its UA peers within the same probe, that’s a UA-targeted block — cross-reference with the diagnosis table in the dual-UA section above to identify which sibling gotcha applies.

Common Diagnostic Mistakes

Patterns we’ve seen mislead diagnosis in real support tickets. Worth flagging because they’re easy to fall into.

“I see 200 in my browser, so it’s fine”

False negative. Browsers send a browser User-Agent, which often passes through edge filters that block Anthropic’s OAuth backend (which uses python-httpx). Always test with both UAs. If browser succeeds but python-httpx fails, the connection will still break for real Claude users despite your “everything looks fine in browser” observation.

“I see 429, so I’m being rate limited”

Could be, but more often it’s a UA-targeted persistent block from Imunify360 or mod_security. The distinction matters because rate limits self-resolve while UA blocks need config changes. Always run the persistence check: probe twice with 60+ seconds between attempts. If python-httpx returns 429 both times while browser UA returns 200 both times, it’s a UA block, not rate limiting.

“I got rest_no_route, so Royal MCP isn’t installed”

Only if you probed a known-correct Royal MCP path. The most common version of this mistake: probing /wp-json/royal-mcp/v1/oauth/register (which doesn’t exist on any Royal MCP install — OAuth endpoints live at site root, not under /wp-json/) and concluding the plugin must be inactive. Always pair an ambiguous 404 with a known-good control probe like /.well-known/oauth-authorization-server before drawing conclusions.

“The browser shows JSON, so everything works” (when issuer is wrong)

Status code 200 + JSON response doesn’t mean Royal MCP is responding. Cloudflare Zero Trust Access can return 200 + JSON for /.well-known/oauth-authorization-server, but the JSON points to auth.<subdomain>.com instead of your site root. Always check the issuer field matches the URL you fetched.

“Activity Log shows oauth:token ERROR, so the request reached PHP”

Sometimes. Royal MCP’s Activity Log captures attempts that reach PHP, but it doesn’t capture intermittent successful-then-blocked retries. If 9 out of 10 of Claude’s /token POSTs are 429’d at the edge but 1 occasionally gets through and logs an error, you’ll see the log entry and think “ah, the request is reaching PHP, so the issue isn’t at the edge.’ The dual-UA curl probe is the authoritative test for whether the edge is blocking.

“The probe returns 200, so my CF Skip rule is working”

Depends what you probed. CF Skip rules are path-specific. A Skip rule applied to /.well-known/oauth-authorization-server won’t help if Claude is failing at /wp-json/royal-mcp/v1/mcp (the MCP endpoint itself). Probe all four paths after configuring Skip rules, not just discovery.

Still Stuck? Two Support Paths

If you’ve worked through the steps above and your connection still fails:

Community Support (free) — wp.org Plugin Forum

Post a new thread at wordpress.org/support/plugin/royal-mcp/. The Royal Plugins team monitors the forum regularly and community members often help disambiguate issues faster than email could. Include the diagnostic info listed below in your post.

Premium Support (paid) — direct one-on-one help

For priority response (24-hour SLA) and hands-on diagnostic help, our Premium Support tier is $149/year. Includes a 30-day “if it breaks again” follow-up window on every resolved ticket.

Information to include in your post or ticket

  • Your hosting provider and whether Cloudflare or another CDN is in front of the site
  • Royal MCP version from WP Admin → Plugins (1.4.22 or newer recommended for the auto-diagnostic notices)
  • Output of the curl probe(s) from this page — full response including headers (use curl -i)
  • Which Claude client you’re using (Claude Desktop, claude.ai web custom connector, Claude Code CLI, ChatGPT MCP, etc.)
  • The exact error message and any ofid_* reference code shown by the client
  • Screenshot of the most recent oauth: row in Royal MCP → Activity Logs (View Details expanded) — or confirmation the log is empty after a reproduced failure

Next steps based on what you found

Match your probe results to the relevant fix article:

CF Zero Trust hijacks discovery
Probe 1 returns JSON with issuer = auth.<sub> or OIDC scopes.
Cloudflare blocking AI bots / OAuth
python-httpx UA returns 403 with cf-ray header; browser UA returns 200.
cPanel ModSecurity blocking MCP
All UAs return 406 with literal “Mod Security” phrase in the response body.
Apache + Imunify360 blocking python-httpx
python-httpx UA returns 429 persistently across multiple probes; browser UA returns 200.
Discovery returns HTML instead of JSON
Probe 1 returns 200 with HTML body (login page, theme page, access-denied screen).
SiteGround returns 404 for /.well-known/
Probe 1 returns 404 from nginx; you’re on SiteGround / similar host.
Web server 301-redirects /register
Probe 3 returns 301 / 302 with location: /register/.
All probes return healthy responses, but Claude still fails
Edge is fine. Move to plugin-layer diagnostic (Steps 1–4 of the troubleshooting checklist).