MAX ELMAN

Getting Novamira Working Behind GridPane and Cloudflare

Connecting Claude to a WordPress site through Novamira took four fixes: one at GridPane’s firewall, two in nginx, and one at Cloudflare. None of them were in WordPress.

Novamira is a WordPress plugin that exposes a site to AI clients like Claude and ChatGPT over the REST API, using MCP. The connection is direct: the AI client talks to my server, and nothing passes through Novamira’s infrastructure. Claude.ai connects with OAuth, so the site has to answer a handful of discovery and token endpoints correctly.

The site runs on a GridPane-managed server (nginx, Redis page cache, the 7G firewall) with Cloudflare in front. The plugin installed fine, the REST API answered from the server itself, and the Claude connector still failed. Here’s how I tracked it down.

Why AI connectors get blocked

When you add a custom connector on Claude.ai, the requests to your site come from Anthropic’s servers, not your browser. They arrive from datacenter IP ranges with a generic HTTP client user agent like python-httpx. To most security layers, that looks exactly like a bot.

The requests that need to get through are:

  • /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, the OAuth discovery documents
  • /wp-json/mcp/novamira-oauth, the MCP endpoint itself
  • /wp-json/novamira/v1/oauth/*, the token, registration and revocation endpoints

These have to reach PHP as dynamic requests: no bot challenge, no redirect, no page cache. The only step that happens in your own browser is the authorize page in wp-admin, where you approve the connection.

Diagnosing it with curl

Novamira saw Cloudflare and suggested emailing hosting support. Before doing that, I simulated the AI client from my own machine by spoofing its user agent:

curl -i -A "python-httpx/0.27" https://example.com/.well-known/oauth-protected-resource

It returned a 403. The useful part was reading the response closely. The headers said server: cloudflare, but there was no cf-mitigated header, and the error page footer said nginx. Cloudflare had passed the request through; my own server refused it.

That distinction matters because Cloudflare rewrites the server header on every response, so it always looks like Cloudflare is answering. The quick way to tell who blocked you:

  • Cloudflare block or challenge: a branded “Attention Required” or “Just a moment” page, usually with a cf-mitigated header
  • Origin block: a plain nginx or Apache error page with no Cloudflare branding

Running the same request with -A "Mozilla/5.0" returned a 404 instead of a 403. So there were two separate problems: something filtering on user agent, and something not routing the path to WordPress.

Fix 1: GridPane’s 7G bad-bot filter

GridPane ships with the 7G firewall, and its bad-bot section blocks any user agent containing “python.” That catches python-httpx, so every request from Claude.ai got a 403 before WordPress ever ran.

I turned off the bad-bots section in the site’s GridPane security settings and reran the python curl. The 403 became the same 404 a browser got, which confirmed it.

You can’t scope 7G’s bot check to specific paths. It runs at the server level, before nginx picks a location block, so a location-level exception won’t help. Fix 4 below moves this protection to Cloudflare instead, where path exceptions are easy. The rest of 7G (query string and request URI rules) stays on.

Fix 2: Route the OAuth discovery paths to WordPress

The 404 came from nginx’s handling of /.well-known/. Like most nginx setups, GridPane has a location block there for Let’s Encrypt certificate challenges. It only serves files that exist on disk. Novamira generates the OAuth discovery documents dynamically in PHP, so nginx never handed those requests to WordPress.

The fix is a more specific location block. GridPane loads any file in the site’s nginx folder ending in -main-context.conf into the server block:

nano /var/www/example.com/nginx/novamira-oauth-main-context.conf
location ^~ /.well-known/oauth- {
    try_files $uri /index.php?$args;
}

The longer ^~ prefix wins over the generic /.well-known/ block, so certificate renewals keep working. Test and reload with nginx -t && nginx -s reload. After that, the discovery endpoint returned 200 with Novamira’s JSON.

Fix 3: Keep the discovery documents out of the page cache

The 200 response came with x-grid-srcache-store: STORE and a TTL of about 24 hours. GridPane’s Redis page cache had stored the discovery document. It worked, but after a plugin update or a URL change, clients would get stale OAuth metadata for up to a day.

GridPane reads Redis cache exclusions from files ending in -skip-redis-cache-context.conf (GridPane docs):

nano /var/www/example.com/nginx/novamira-skip-redis-cache-context.conf
if ($request_uri ~* "^/.well-known/oauth-") {
    set $skip_cache 1;
    set $skip_reason "${skip_reason}-novamira-oauth";
}

Reload with nginx -t && gp ngx reload and purge the cache. The response headers then show x-grid-srcache-skip: -novamira-oauth with fetch and store both set to BYPASS. The /wp-json/ endpoints didn’t need this, since GridPane doesn’t cache their 401 responses.

Fix 4: Move bad-bot blocking to Cloudflare

With 7G’s bot filter off, I rebuilt it at Cloudflare using two custom rules under Security → WAF → Custom rules. Order matters, so the skip rule goes first.

Rule 1: skip security for the Novamira paths. Keep this scoped to Novamira’s own routes rather than all of /wp-json/, so the rest of the REST API (user listings and so on) stays protected. Action: Skip. Check all remaining custom rules, managed rules, Super Bot Fight Mode rules, Browser Integrity Check, Security Level and User Agent Blocking.

(starts_with(http.request.uri.path, "/wp-json/mcp/")) or (starts_with(http.request.uri.path, "/wp-json/novamira/")) or (starts_with(http.request.uri.path, "/.well-known/oauth-"))

Rule 2: block common bad-bot user agents. Action: Block. This is the useful core of 7G’s list. I used the wildcard operator because it’s case-insensitive and available on every plan; lower() was rejected on mine.

(http.user_agent wildcard "*python*") or (http.user_agent wildcard "*pycurl*") or (http.user_agent wildcard "*winhttp*") or (http.user_agent wildcard "*nikto*") or (http.user_agent wildcard "*sqlmap*") or (http.user_agent wildcard "*zmeu*") or (http.user_agent wildcard "*httrack*") or (http.user_agent wildcard "*heritrix*") or (http.user_agent wildcard "*nutch*") or (http.user_agent wildcard "*harvest*") or (http.user_agent wildcard "*dotbot*") or (http.user_agent wildcard "*casper*") or (http.user_agent wildcard "*binlar*")

If you’re on Cloudflare’s free plan with Bot Fight Mode on, note that it can’t be bypassed by a skip rule. You’d have to turn it off entirely. Super Bot Fight Mode on paid plans can be skipped as above.

The result: a python user agent hitting the homepage gets Cloudflare’s “Sorry, you have been blocked” page, while the same user agent on the OAuth endpoints gets a clean 200.

Checklist

All three of these should pass with the python user agent before you add the connector in Claude:

curl -i -A "python-httpx/0.27" https://example.com/.well-known/oauth-protected-resource
curl -i -A "python-httpx/0.27" https://example.com/.well-known/oauth-authorization-server
curl -i -A "python-httpx/0.27" https://example.com/wp-json/mcp/novamira-oauth
EndpointExpected response
oauth-protected-resource200, JSON, not cached
oauth-authorization-server200, JSON, not cached
/wp-json/mcp/novamira-oauth401 JSON with a www-authenticate: Bearer resource_metadata=... header

That 401 is the success case. It’s WordPress telling the client to authenticate and where to find the OAuth metadata. A 403, a 404, an HTML page or a Cloudflare challenge means some layer is still in the way.

The lesson for anyone on managed WordPress hosting: when an AI connector fails, the problem is usually a security or caching layer that assumes every visitor is a browser. Read the response headers, figure out which layer answered, and carve out exceptions by path, never by user agent.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *