# Hosting Is the Hardest Part of Building an MCP Server

> After 50,000 downloads of a WordPress MCP server, the lesson is that MCP fails at the host layer: WAFs and CDN rules block OAuth discovery first.

- Canonical: <https://royalplugins.com/blog/hosting-is-the-hardest-part-of-building-an-mcp-server/>
- Last updated: 2026-09-18
- HTML version: <https://royalplugins.com/blog/hosting-is-the-hardest-part-of-building-an-mcp-server/>

---

**The MCP spec gets all the attention: tool schemas, sampling, resources, prompts. But after 50,000 downloads of a WordPress MCP server and a support inbox full of “Claude can’t connect” tickets, the most consistent failure mode has nothing to do with tools. It’s the OAuth handshake never reaching the server — blocked at the host layer by security middleware, edge WAFs, or CDN rules the plugin can never see. If you’re building or evaluating an MCP server, the fragile part is the handshake, not the protocol.**

Every conversation about MCP servers is about the *tools* — what the model can do once it’s connected. Tool schemas, capability matrices, sampling patterns, resources, prompts. The protocol itself is thoughtful and well-specified, and after roughly ten months of writing MCP tool handlers for WordPress, I can tell you the tool layer is not where MCP struggles.

The handshake is where it struggles.

Many of the support tickets we’ve handled on [Royal MCP](https://royalplugins.com/royal-mcp/) that start with *“Claude can’t connect to my WordPress site”* follow the same shape. The plugin is installed and configured. The endpoint responds correctly to a manual `curl`. But when Claude tries to connect, the OAuth discovery request never reaches the plugin. Something in front of the site — a WAF rule, a bot-protection layer, a CDN policy, an nginx canonicalization redirect — intercepts the request and returns something that isn’t valid discovery metadata. Claude gives up. The user sees a generic *“couldn’t connect”* error with no diagnostic breadcrumbs.

The tools didn’t matter. The MCP server never got to run.

## The Taxonomy of Host-Layer Failures

Over the course of building this plugin and helping customers debug connection problems, we’ve catalogued over a dozen distinct host-layer failure modes. They fall into three categories.

### Category 1: Security middleware blocking non-browser traffic

This is the most common bucket by incident frequency — the pattern behind the largest share of the support tickets. Shared hosts run Imunify360, ModSecurity, Cloudflare Bot Fight Mode, or the “Block AI Bots” toggle — all of which use heuristic detection to distinguish human traffic from automated traffic. MCP clients look automated because they *are* automated. The middleware doesn’t know the difference between a legitimate agent trying to help the site owner and a scraper harvesting content.

The failure modes vary by vendor:

- **Imunify360** returns `HTTP 200` with a JSON body: `{"message":"Access denied by Imunify360 bot-protection..."}`. Because the status is 200, some MCP clients parse it as success and fail on the missing OAuth fields.
- **Cloudflare’s Agent-category block** (formerly the “Block AI Bots” toggle, split into Search / Training / Agent categories on September 15, 2026) returns a `403` with an HTML challenge page. MCP clients expect JSON; they get HTML and throw *“unexpected response.”* See [Cloudflare Blocks AI Bots & Breaks MCP](https://royalplugins.com/blog/cloudflare-block-ai-bots-breaks-mcp/) for the specific fix.
- **ModSecurity** with default OWASP CRS rules returns `406 Not Acceptable` on any request that looks bot-shaped, sometimes based purely on the User-Agent string.
- **Apache shared-hosting WAF layers** (Imunify360, mod\_security) return `429` on the `python-httpx` User-Agent that Anthropic’s OAuth backend uses, regardless of request rate. UA-targeted, not rate-based.

Why This Is Hard to Self-Diagnose

None of this is visible to the site owner. WordPress debug logs show nothing — the request never reaches PHP. The plugin can’t self-diagnose from inside the site either, because the loopback request from the site to itself often *does* work (localhost is usually whitelisted), even when external traffic from Claude’s IP is being blocked.

### Category 2: Web server config that assumes browsers

The OAuth 2.1 spec is unambiguous about the URLs. Discovery is at `/.well-known/oauth-authorization-server`. Registration is at whatever URL the discovery document advertises. But the *transport* still has to survive the web server config, and there are quirks that only bite non-browser clients.

The most common one: **nginx, Apache mod\_dir, and default `.htaccess` canonicalization all issue a `301` redirect when a URL points at a directory without a trailing slash.** For a browser, this is invisible — the browser follows the redirect and retries. For an OAuth client, it’s fatal on `POST` — the RFCs don’t permit following a 301 with the original POST body, so the request dies before it reaches the server.

Claude’s OAuth client hardcodes the bare path `/register` and doesn’t follow 301s on POST. If the host’s default canonicalization redirects `/register` to `/register/`, Dynamic Client Registration fails silently. The plugin metadata is correct. The endpoint responds correctly to a direct POST. But because there’s a redirect between “what Claude sends” and “what the server receives,” registration never completes.

The site owner sees the same generic *“couldn’t connect”* error.

### Category 3: URL-space claimers

The third category is other WordPress plugins and hosting providers claiming URL paths for their own use.

**SiteGround** and a handful of other managed hosts reserve `/.well-known/` at the nginx layer for Let’s Encrypt ACME challenges. Any request to `/.well-known/oauth-authorization-server` returns a nginx `404` before WordPress ever sees it. The plugin’s rewrite rules are correct; they just never fire, because nginx handled the request first.

**Membership plugins** (ARMember, MemberPress, Restrict Content Pro) sometimes intercept OAuth paths as “protected content” and serve their own HTML login page in response. The response is `HTTP 200`, but the body is HTML, not JSON. Discovery clients that strictly require JSON metadata fail silently.

**Stale static files** from earlier plugin versions occasionally survive on disk in `/webroot/.well-known/` and advertise OAuth endpoint URLs that no longer exist. The plugin serves fresh metadata from PHP; nginx serves the stale file first because static content wins over WordPress routing. Discovery documents advertise dead URLs, registration hits a 404, and the connection fails.

## What We Built in Response

The realization that the plugin needed to *diagnose the network layer it can’t control* changed how we think about MCP server design. Our OAuth notice module in the WordPress admin now runs four different classifiers on the site’s own discovery endpoint, using WordPress’s loopback HTTP API to probe it from the inside:

- `blocked` — HTTP 404 from an nginx-style static handler (SiteGround pattern)
- `stale_static` — HTTP 200 JSON with legacy REST-namespace URLs (leftover from pre-1.4.0 versions)
- `body_is_html` — HTTP 200 but the body starts with `<!doctype html>` (membership plugin intercept)
- `register_301` — POST to `/register` returns a 301 to `/register/` (nginx canonicalization)

A fifth classifier for the Imunify360 pattern (HTTP 200 + JSON denial body with a `message` field mentioning Imunify360) is queued for the next release. When any of these fires, the plugin surfaces an admin notice explaining what’s blocked, why it matters, and linking to a specific support article with the fix.

Every classifier corresponds to a support ticket we handled. Every notice exists because someone paid the “silent connect failure with no error message” tax and told us about it.

## The Takeaway for Anyone Building an MCP Server

If you’re building an MCP server — for WordPress, or anything else — the tool schema is the easy part.

The hard parts are:

1. **Discovery must survive the network.** Assume every path in `.well-known/` will be claimed, redirected, blocked, or intercepted somewhere in the wild. Build detection for the common failure modes into the server itself. Users can’t diagnose what they can’t see, and the CLI clients don’t surface useful errors.
2. **OAuth must survive edge middleware.** Non-browser clients trigger heuristic bot detection everywhere. Assume this will happen. Provide diagnostic tools, allowlist documentation, and clear escalation paths (“email your host with this exact request”) for common providers.
3. **The site owner is not the operator of the middleware.** In WordPress specifically, “hosting” often means shared hosting where the customer has no access to WAF rules, no visibility into bot-detection settings, and no relationship with the CDN vendor. The support workflow for MCP has to include “here’s what to send your host.”

Stuck on one of these right now?

If you’re on the site-owner side of a silent MCP connect failure, our [troubleshooting hub](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/) walks through the diagnostic curls that identify which category you’re in and what to do about it.

The MCP protocol is the last mile. The hosting layer is the entire road that gets you there. It’s the hardest part of the problem, and it’s the one that receives the least engineering attention.
