# Fix: mcp_token_exchange_failed on Apache Shared Hosts

> mcp_token_exchange_failed right after consent on an Apache shared host: Imunify360, mod_security or BitNinja is blocking the OAuth backend with a 429.

- Canonical: <https://royalplugins.com/support/royal-mcp/apache-python-httpx-429-token-exchange/>
- HTML version: <https://royalplugins.com/support/royal-mcp/apache-python-httpx-429-token-exchange/>

---

If your Apache shared host returns `HTTP 429 Too Many Requests` specifically for the `python-httpx` User-Agent — while browser User-Agents pass through fine — you’ve hit a UA-targeted block from Imunify360, mod\_security with UA-fingerprint rules, BitNinja, or a similar managed security platform. The block kills Claude’s OAuth `/token` exchange because Anthropic’s OAuth backend uses python-httpx. This page covers how to confirm it, why it happens, and how to get your host to fix it.

### You cannot fix this from inside WordPress.

This block lives at your hosting provider’s edge security stack, *before* requests reach WordPress. No plugin setting, theme change, or cache clear will help. The fix is a configuration change at your host — you’ll need to open a hosting support ticket. The text you’ll need is in the “Paste-Ready Hosting Ticket” section below.

## Is this your issue?

This pattern fits a specific signature. The more of these that match, the more likely it’s the right diagnosis:

- Your site is on **Apache shared hosting** (no Cloudflare in front, no Nginx-only setup, no LiteSpeed Web Server). Run any curl probe and you’ll see `server: Apache` in the response headers
- OAuth flow **completes the consent screen in your browser** — you click Authorize, the page redirects normally
- Right after redirect, Claude shows `mcp_token_exchange_failed`, “Authorization with the MCP server failed,” or “Couldn’t reach the MCP server”
- Royal MCP Activity Log shows `oauth:token → ERROR` rows after reproducing the failure — OR the log stays empty entirely (depending on how aggressively the host blocks)
- **Royal MCP discovery (the `/.well-known/` endpoint) works fine in a browser** — the failure is specifically at `/token`, not at discovery

Different from Cloudflare-based blocks

If you have Cloudflare in front of your site (curl responses show `server: cloudflare` + a `cf-ray` header), the right doc is [Cloudflare blocking Claude MCP OAuth](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/) instead. CF’s block returns 403 with branded HTML; the Apache shared-host block returns 429 with a plain “Too Many Requests” body. Same general failure family, different vendor & fix path.

## Confirm the block with a dual-UA curl probe

The diagnostic test: hit your OAuth discovery endpoint twice from your local machine — once with a python-httpx User-Agent (which Anthropic’s backend uses), once with a browser User-Agent. If python-httpx returns 429 while browser returns 200, this gotcha applies.

### Test with python-httpx UA

**Mac Terminal, Linux, Windows Command Prompt, Git Bash:**

```
curl -sS -i -A "python-httpx/0.27.0" https://example.com/.well-known/oauth-authorization-server | head -10
```

**Windows PowerShell:**

```
curl.exe -sS -i -A "python-httpx/0.27.0" https://example.com/.well-known/oauth-authorization-server | Select-Object -First 10
```

### Test with browser UA

**Mac Terminal, Linux, Windows Command Prompt, Git Bash:**

```
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 | head -10
```

**Windows PowerShell:**

```
curl.exe -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 | Select-Object -First 10
```

### Interpret the results

| python-httpx | browser UA | Diagnosis |
| --- | --- | --- |
| **429 Too Many Requests** | 200 OK | ✅ **This gotcha applies.** UA-targeted block from Apache managed security. Continue to the persistence check below. |
| 200 OK | 200 OK | This block isn’t affecting you (at least not right now). If Claude still fails, check [Step 0 of the troubleshooting checklist](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/#step-0-edge-probe) for other patterns. |
| 403 Forbidden + `cf-ray` | Either | This is a Cloudflare block, not an Apache host block. See [Cloudflare blocking Claude MCP OAuth](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/) instead. |
| 406 Not Acceptable + “Mod Security” | 406 Not Acceptable + “Mod Security” | cPanel ModSecurity blocking all non-trusted UAs. See [ModSecurity 406 fix](https://royalplugins.com/support/royal-mcp/modsecurity-406-bot-fingerprint/) instead. |

### The 429 response body looks like this

Plain text body, ISO-8859-1 encoding, no JSON. The minimal “Too Many Requests” response is the signature of Apache’s default 429 handler when the managed security layer issues the block.

```
HTTP/1.1 429 Too Many Requests
Server: Apache
Content-Type: text/html; charset=iso-8859-1

Too Many Requests
```

## Persistence check — rules out burst rate limiting

One 429 might just mean “you sent too many requests too fast.” A *persistent* 429 specifically on python-httpx, while browser UA returns 200, is a different thing entirely — it’s a UA-targeted rule that fires regardless of request rate.

To confirm it’s UA-targeted (not rate-based):

1. Run the python-httpx probe above. Note the result (likely 429)
2. **Wait 60+ seconds** (give any burst rate limiter time to reset)
3. Run the python-httpx probe again
4. If it’s still 429 on the second attempt while the browser UA still returns 200 on a fresh run, the block is UA-targeted and persistent

Why this distinction matters for the hosting ticket

If you tell your host “I’m being rate limited,” they may just raise your request quota, which won’t help (the block isn’t rate-based). If you tell them “a UA-fingerprint rule is blocking python-httpx specifically,” that points them to the actual mechanism (Imunify360 / mod\_security UA rule / BitNinja) and the correct fix (whitelist or path exclusion). The paste-ready ticket text below uses the right framing.

## Why this breaks the OAuth /token exchange

Royal MCP’s OAuth flow uses different participants at different stages. Specifically, Anthropic’s OAuth backend uses the `python-httpx` HTTP client for the server-to-server `/register` and `/token` POSTs. When the host blocks that User-Agent, the OAuth flow dies at the final step:

1. **Claude initiates OAuth**. The user’s browser handles the redirect to `/authorize` — this is a browser request with a browser UA, so it passes through the host’s filter without issue
2. The user sees Royal MCP’s consent screen, clicks Authorize. **Browser stuff still works.** The browser redirects back to Claude with an authorization code
3. Now Claude’s backend takes over. **It POSTs to `/token` using python-httpx** to exchange the authorization code for an access token
4. Your Apache host’s managed security stack sees the python-httpx UA and returns **429 Too Many Requests** — the request never reaches WordPress, never reaches Royal MCP’s `/token` handler
5. Claude receives 429, surfaces it to you as `mcp_token_exchange_failed` or “Authorization with the MCP server failed”

From your perspective, the OAuth flow looked like it was working — the consent screen rendered correctly, you clicked Authorize, the page redirected — right up until the very last step where Claude needed to actually obtain the token. That mismatch (browser stuff works, backend stuff fails) is the diagnostic fingerprint of UA-targeted blocking.

## Which security stack is your host running?

You don’t need to know this to open the hosting ticket — the ticket text below works regardless. But if you want to give the host more context, here’s how the three most common Apache-shared-host security stacks identify themselves:

#### Imunify360

Default 429 response is plain text “Too Many Requests” with `Content-Type: text/html; charset=iso-8859-1` and no specific Imunify branding. Most common stack on Apache shared hosting in Europe, Africa, and Asia-Pacific regions. The fix on the host side is adding a Network Rule or Firewall Exclusion scoped to your domain + the python-httpx UA pattern.

#### mod\_security (with custom UA rules)

Default 429 may include a brief reference number (e.g. `Reference #18.abcd...`) but often is identical to Apache’s default. Sometimes returns 403 instead of 429 depending on rule configuration. The fix on the host side is adding a SecRule with `allow,nolog` action scoped to your vhost + the User-Agent pattern.

#### BitNinja / other managed bot defense

Default response varies by vendor. May include vendor-specific HTML if the customer ever installed the vendor’s “branded” error page templates. Usually identifiable by your hosting brand — some smaller managed-WordPress hosts in Europe and Asia bundle BitNinja, Patchstack, or similar managed defense products. The fix on the host side is vendor-specific allow rules.

Don’t spend time trying to identify the exact stack yourself. The ticket text below describes the symptom in vendor-neutral language, and your host’s support team will know which specific tool they use. They’ll apply the right kind of allow rule.

## Paste-ready hosting support ticket

Copy this and submit it to your hosting provider’s support team. Replace `example.com` with your actual domain.

```
Subject: User-Agent "python-httpx" receiving 429 Too Many Requests on
my domain — need allowlist or path exclusion

Hi support,

My WordPress site at example.com runs a plugin (Royal MCP) that needs
to accept OAuth requests from Anthropic's backend service. Anthropic's
OAuth client identifies itself with User-Agent "python-httpx".

Your managed security stack is currently returning 429 Too Many Requests
for any request with that User-Agent, even single, non-bursty requests
made 60+ seconds apart. Browser User-Agents pass through normally.

This is reproducible from any external IP:

    curl -A "python-httpx/0.27.0" \
        https://example.com/.well-known/oauth-authorization-server

returns 429 Too Many Requests, while

    curl -A "Mozilla/5.0" \
        https://example.com/.well-known/oauth-authorization-server

returns 200 OK with the expected JSON.

The 429 persists across multiple requests minutes apart, so this is
NOT a burst rate limit — it's a User-Agent-targeted block (likely
an Imunify360 rule, a mod_security UA rule, or a similar managed
bot-defense rule).

Could you please either:

1. Whitelist User-Agent matching python-httpx/* on this domain, OR

2. Exclude these specific paths from User-Agent-based filtering:
   - /register
   - /authorize
   - /token
   - /.well-known/oauth-authorization-server
   - /.well-known/oauth-protected-resource
   - /wp-json/royal-mcp/v1/mcp

Option 1 is preferable as it allows the plugin's authentication to
work site-wide. Option 2 is an acceptable fallback if a domain-wide
UA allowlist isn't available.

After you apply the change, I'll re-run the curl test to confirm
the block is cleared. Happy to provide any additional logs or
diagnostic info you need.

Thanks!
```

A note about how to phrase this for the host

This text is intentionally vendor-neutral (it doesn’t name “Imunify360” or “mod\_security” in the subject line) because some host support staff get defensive if you tell them which tool you think the problem is. The body mentions the likely tools in passing, which gives them context without sounding accusatory. Most host support teams will quickly identify the right rule once they have the curl repro.

## Verify the fix worked

After your host confirms the change has been applied, re-run the dual-UA probe:

```
curl -sS -i -A "python-httpx/0.27.0" https://example.com/.well-known/oauth-authorization-server | head -5
```

Expected output:

```
HTTP/1.1 200 OK
Server: Apache
Content-Type: application/json; charset=utf-8
```

If you now see 200 OK on both browser UA and python-httpx UA, the block is cleared. Continue to the reconnection step below.

If the python-httpx probe still returns 429:

- Wait 15–30 minutes — some managed security platforms cache rules at the edge and take time to propagate
- If still failing after 30 minutes, reply to your hosting ticket. The host’s support team may have applied the allow rule at the wrong scope (account-wide instead of domain-specific, or vice versa) — common when the rule UI is confusing
- Share the failing curl output with the host as proof the change didn’t take effect

## Reconnect Claude with fresh OAuth state

Once curl confirms python-httpx now returns 200, Claude still has stale connection state from the failed attempts. Wipe and reconnect:

1. In Claude (web or Desktop), **delete the existing Royal MCP connector entirely**
2. In WordPress, go to **Royal MCP → Settings** and click **Reset OAuth State** (button available in Royal MCP 1.4.17+)
3. Wait 30 seconds for any caching layers to settle
4. Re-add the connector in Claude with **only the server URL** (e.g. `https://example.com/wp-json/royal-mcp/v1/mcp`). Leave Advanced Settings completely empty
5. Complete the OAuth flow when prompted — should succeed this time

## 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

## This is part of a family of UA-targeted blocks

The Apache 429 block is one of three closely related patterns. All three break Royal MCP OAuth in the same way (UA-discrimination at the edge, OAuth dies at `/token`) but on different host families with different status codes. Worth knowing in case you migrate hosts and hit a sibling pattern later, or in case your initial diagnosis is wrong:

| Host family | Edge layer | Status code | Body signature | Fix doc |
| --- | --- | --- | --- | --- |
| Cloudflare in front | CF Bot Fight Mode / AI Bots | **403** | CF-branded HTML, `cf-ray` header | [CF AI bots fix](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/) |
| cPanel shared hosting | ModSecurity bot-fingerprint | **406** | “Generally a 406 error is caused because a request has been blocked by Mod Security” | [ModSec 406 fix](https://royalplugins.com/support/royal-mcp/modsecurity-406-bot-fingerprint/) |
| Apache + managed security | Imunify360 / mod\_security UA rule / BitNinja | **429** | Plain text “Too Many Requests”, no vendor branding | **(this page)** |

All three resolve the same way: open a hosting support ticket asking for a UA whitelist or path-based exclusion for the OAuth endpoints. Only the status code, host family, and which support team you contact differ.

## Related host-side OAuth failures

If the curl probe shows browser UA returning 200 but python-httpx returning something other than 429, try these adjacent failure modes:

[Cloudflare blocking AI bots / OAuth

python-httpx UA returns 403 with `cf-ray` header. Cloudflare Bot Fight Mode / AI Bots blocking Anthropic’s backend.](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/)
[cPanel ModSecurity returns 406

All UAs return 406 with literal “Mod Security” phrase in body. cPanel-bundled ModSec fingerprinting non-browser HTTP clients.](https://royalplugins.com/support/royal-mcp/modsecurity-406-bot-fingerprint/)
[Cloudflare Zero Trust hijacks discovery

Discovery returns JSON but `issuer` points to `auth.<sub>`. Different from this Apache 429 issue — that’s discovery-layer hijack, this is /token-layer block.](https://royalplugins.com/support/royal-mcp/cloudflare-zero-trust-shadows-mcp-discovery/)
[Full curl diagnostic walkthrough

The complete 4-probe sequence with dual-UA testing, response header reading, and a paste-ready bash + PowerShell diagnostic script.](https://royalplugins.com/support/royal-mcp/diagnose-mcp-with-curl/)
[Back to troubleshooting start-here

Return to the symptom-based router if you’re not sure which fix applies to your situation.](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/)

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