# Diagnose Royal MCP Connection Failures with curl

> Diagnose Royal MCP OAuth connection failures with curl: the 4-probe sequence, dual-UA testing, header reading, and a paste-ready bash and PowerShell script.

- Canonical: <https://royalplugins.com/support/royal-mcp/diagnose-mcp-with-curl/>
- Last updated: 2026-09-21
- HTML version: <https://royalplugins.com/support/royal-mcp/diagnose-mcp-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](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/#step-0-edge-probe). 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](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/) — 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](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/#step-0-edge-probe)** 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.

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](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/#step-0-edge-probe) 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). Also possible: your host intercepts root paths before WordPress sees them — if Save Changes doesn’t restore the 405, run Probe 3b (`/authorize`) below to confirm; a 404 there too suggests a host-layer intercept, see [the root-path intercept fix](https://royalplugins.com/support/royal-mcp/acme-first-host-well-known-intercepted/).
- **404 Not Found** with `{"code":"rest_no_route",...}`: WordPress REST API responded, but no route matches. This is misleading — `/register` at site root is the correct Royal MCP path, but if you accidentally probed `/wp-json/royal-mcp/v1/register` or 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](https://royalplugins.com/support/royal-mcp/oauth-register-trailing-slash-301/).
- **200 OK with HTML**: a plugin or theme has hijacked the `/register` path (often happens with membership plugins that create a page with slug `register`). Disable conflicting plugins/themes.
- **466 or other 4xx not documented above**: a host WAF or CDN is intercepting the request before it reaches WordPress. See [Non-standard 4xx from /register](https://royalplugins.com/support/royal-mcp/oauth-register-nonstandard-4xx/).

### Probe 3b (optional): `/authorize` with a dummy client\_id

**What it checks:** a second, independent signal that the OAuth root rewrites are registered. Useful when Probe 3 returns 404 and you want to isolate whether the rewrite itself is missing (both probes will 404) or whether something specific is intercepting `/register` alone (Probe 3b returns 400, Probe 3 returns 404). Unlike `/register` and `/token`, `/authorize` is spec-required to accept GET, so this probe is safe.

```
curl -sS -i "https://example.com/authorize?client_id=nonexistent&response_type=code&redirect_uri=https://claude.ai/api/mcp/auth_callback&code_challenge=x&code_challenge_method=S256&state=y"
```

**Healthy response:** HTTP 400 with an `invalid_client` JSON body — confirms Royal MCP’s `/authorize` handler received and evaluated the request. If instead you see 404 from your host or 200 with an HTML page, jump to the same fixes listed under Probe 3.

### 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](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/) |
| 200 OK | **429 Too Many Requests** | Apache + Imunify360 / mod\_security UA-fingerprint rule blocking python-httpx. Fix: [Apache 429 fix](https://royalplugins.com/support/royal-mcp/apache-python-httpx-429-token-exchange/) |
| **406 + “Mod Security”** | **406 + “Mod Security”** | cPanel ModSecurity blocking ALL non-trusted UAs. Fix: [ModSec 406 fix](https://royalplugins.com/support/royal-mcp/modsecurity-406-bot-fingerprint/) |
| 201 or 405 | **466 (or any 460–469) + HTML “Access Denied” body** | Intermediary WAF (managed host, CDN edge, ModSec rule action override) fingerprinting the ChatGPT-flavored DCR POST specifically. Symptom in the wild: Claude works, ChatGPT fails DCR. Fix: [Non-standard 4xx on /register fix](https://royalplugins.com/support/royal-mcp/oauth-register-nonstandard-4xx/) |
| 200, but `issuer` = `auth.<sub>` | 200, but `issuer` = `auth.<sub>` | CF Zero Trust Access hijacking discovery (UA-independent). Fix: [CF Zero Trust fix](https://royalplugins.com/support/royal-mcp/cloudflare-zero-trust-shadows-mcp-discovery/) |

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:

- `server: cloudflare` — CF is the edge layer. Look for `cf-ray` in the same response.
- `server: nginx` or `nginx/1.x.x` — could be SiteGround, Hostinger, o2switch, WP Engine, Kinsta, or a self-hosted nginx setup.
- `server: Apache` or `Apache/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 envelope
- `text/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](https://royalplugins.com/support/royal-mcp/cloudflare-zero-trust-shadows-mcp-discovery/).

### 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](https://royalplugins.com/support/royal-mcp/well-known-served-as-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/"
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/"
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/](https://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](https://royalplugins.com/premium-support/) 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.](https://royalplugins.com/support/royal-mcp/cloudflare-zero-trust-shadows-mcp-discovery/)
[Cloudflare blocking AI bots / OAuth

python-httpx UA returns 403 with `cf-ray` header; browser UA returns 200.](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/)
[cPanel ModSecurity blocking MCP

All UAs return 406 with literal “Mod Security” phrase in the response body.](https://royalplugins.com/support/royal-mcp/modsecurity-406-bot-fingerprint/)
[Apache + Imunify360 blocking python-httpx

python-httpx UA returns 429 persistently across multiple probes; browser UA returns 200.](https://royalplugins.com/support/royal-mcp/apache-python-httpx-429-token-exchange/)
[Discovery returns HTML instead of JSON

Probe 1 returns 200 with HTML body (login page, theme page, access-denied screen).](https://royalplugins.com/support/royal-mcp/well-known-served-as-html/)
[SiteGround returns 404 for `/.well-known/`

Probe 1 returns 404 from nginx; you’re on SiteGround / similar host.](https://royalplugins.com/support/royal-mcp/siteground-well-known-404/)
[Web server 301-redirects `/register`

Probe 3 returns 301 / 302 with `location: /register/`.](https://royalplugins.com/support/royal-mcp/oauth-register-trailing-slash-301/)
[All probes return healthy responses, but Claude still fails

Edge is fine. Move to plugin-layer diagnostic (Steps 1–4 of the troubleshooting checklist).](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/#checklist)

[Back to Troubleshooting Start Here](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/)
[Royal MCP Support Hub](https://royalplugins.com/support/royal-mcp/)
