Fix: mcp_token_exchange_failed on Apache Shared Hosts
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.
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: Apachein 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 → ERRORrows 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
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 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 for other patterns. |
403 Forbidden + cf-ray |
Either | This is a Cloudflare block, not an Apache host block. See Cloudflare blocking Claude MCP OAuth instead. |
| 406 Not Acceptable + “Mod Security” | 406 Not Acceptable + “Mod Security” | cPanel ModSecurity blocking all non-trusted UAs. See ModSecurity 406 fix 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):
- Run the python-httpx probe above. Note the result (likely 429)
- Wait 60+ seconds (give any burst rate limiter time to reset)
- Run the python-httpx probe again
- 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
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:
- 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 - The user sees Royal MCP’s consent screen, clicks Authorize. Browser stuff still works. The browser redirects back to Claude with an authorization code
- Now Claude’s backend takes over. It POSTs to
/tokenusing python-httpx to exchange the authorization code for an access token - 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
/tokenhandler - Claude receives 429, surfaces it to you as
mcp_token_exchange_failedor “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!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:
- In Claude (web or Desktop), delete the existing Royal MCP connector entirely
- In WordPress, go to Royal MCP → Settings and click Reset OAuth State (button available in Royal MCP 1.4.17+)
- Wait 30 seconds for any caching layers to settle
- 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 - 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/. 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
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 |
| 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 |
| 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.