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.
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:
- A developer trying to understand why Royal MCP isn’t connecting on your site (or a client’s site) and want the full protocol-level picture
- Support staff (Royal Plugins or otherwise) diagnosing a remote ticket where you can’t access the customer’s WordPress admin
- An agency running a batch diagnostic across multiple client sites — the reproducible script at the bottom is built for this
- Anyone who came from Step 0 and wants a more thorough probe set than the single discovery check
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.
- Mac Terminal, Linux, Windows
cmd.exe, Git Bash, WSL: usecurl - 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:
issuermatches the URL you fetched (same domain, scheme, noauth.subdomain prefix)scopes_supportedincludes"mcp:full"(NOTopenid,profile,email— those are CF Zero Trust signatures)- Endpoint paths are at site root (
/authorize,/token,/register) — NOT under/oauth/*prefix
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
- 404 Not Found with WordPress’s standard 404 page: OAuth endpoints are not registered. Either Royal MCP is not active, or rewrite rules need flushing (Settings → Permalinks → Save Changes).
- 404 Not Found with
{"code":"rest_no_route",...}: WordPress REST API responded, but no route matches. This is misleading —/registerat site root is the correct Royal MCP path, but if you accidentally probed/wp-json/royal-mcp/v1/registeror similar, that’s a non-existent URL. Make sure you’re hitting site-root/register— OAuth endpoints don’t live under/wp-json/*. - 301 / 302 redirect to
/register/(with trailing slash): your web server is adding a trailing slash via canonicalization. This breaks OAuth POST requests. Fix at Trailing-slash 301 fix. - 200 OK with HTML: a plugin or theme has hijacked the
/registerpath (often happens with membership plugins that create a page with slugregister). Disable conflicting plugins/themes.
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:
- Status is 401 (not 403, 404, or 500)
- Response body is a JSON-RPC error envelope (has
jsonrpc: "2.0"anderrorfields) WWW-Authenticateheader is present withBearerscheme and aresource_metadataparameter 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
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 |
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:
server: cloudflare— CF is the edge layer. Look forcf-rayin the same response.server: nginxornginx/1.x.x— could be SiteGround, Hostinger, o2switch, WP Engine, Kinsta, or a self-hosted nginx setup.server: ApacheorApache/2.x— cPanel shared hosting (InMotion, A2, Bluehost, HostGator) or managed-WordPress Apache stacks. ModSec and Imunify360 candidates.server: LiteSpeed— LiteSpeed Web Server (different from LiteSpeed Cache plugin). Has its own caching and OAuth-relevant 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:
- Discovery endpoints (
/.well-known/oauth-authorization-server,/.well-known/oauth-protected-resource):public, max-age=3600— metadata is safe to cache for an hour because endpoint shapes rarely change. - Auth endpoints (
/register,/authorize,/token):no-store, no-cache, must-revalidate— auth flows are stateful and must never be cached. - MCP endpoint (
/wp-json/royal-mcp/v1/mcp):no-store, no-cache, must-revalidate, private— per-session data, never cacheable.
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:
text/html— discovery has been hijacked, OR a 4xx/5xx error is being served as an HTML error page instead of the OAuth JSON error envelopetext/plain— usually static-file serving (SiteGround if you placed static files in.well-known/and the Content-Type wasn’t set correctly)- Missing entirely — usually empty response body (a clear-cut sign of an upstream timeout or proxy issue)
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:
- A WordPress login form — usually a membership plugin like MemberPress, Restrict Content Pro, or Paid Memberships Pro that registered the URL as a protected page
- A “page not found” or 404 themed page — rewrite rules not flushed, or a theme template intercepting the path
- A Cloudflare access-denied page — CF Access (without OIDC hijack) blocking the path
- A “Bot Detected” or similar challenge page — bot-protection middleware (Imperva, Akamai Bot Manager, hosting-provided bot defense)
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: