guide · 5 min read
How can a CDN or WAF break Streamable HTTP even when JSON-RPC works locally?
A safe edge-debugging runbook for methods, headers, buffering, timeouts, request bodies, Origin checks, and scoped proxy changes.
Published · Updated
Most “works locally, fails behind the CDN” MCP bugs are contract mismatches at the edge. Check the endpoint, methods, headers, buffering, timeouts, and Origin handling before replacing the server.
This is a documentation-derived runbook. I have not run it against a third-party CDN or WAF, and it does not recommend weakening a security boundary to make a test pass.
Start with the contract
The MCP transport specification says Streamable HTTP uses one MCP endpoint that supports POST and GET. Client POSTs carry one JSON-RPC message and must advertise both application/json and text/event-stream in Accept. A request can receive JSON or an SSE stream.
Send the same safe, read-only initialize or tool request to the origin and edge. Keep the payload synthetic and redact credentials. Compare:
| Contract detail | What to compare | Typical edge symptom |
|---|---|---|
| Route and method | Same URL, POST/GET behavior | 404 or 405 before MCP initializes |
| Request headers | Accept, Content-Type, Origin, authorization |
401, 403, 406, or an unparseable response |
| Response type | JSON versus text/event-stream |
Client waits for a stream or rejects JSON |
| Response timing | First byte and gaps between events | A stream appears frozen or closes early |
| Request body | One intact JSON-RPC message | Invalid JSON, missing method, or empty input |
| Caching | No shared cache of session or tool responses | Another request receives the wrong state |
A 405 is evidence about one route/method combination, not proof that the server implementation is wrong.
Buffering can make a live stream look dead
Nginx’s proxy module documentation says response buffering is on by default. With buffering enabled, Nginx reads from the upstream into buffers; with it disabled, the response is passed through as it arrives. That difference matters when the MCP server is returning an SSE stream and the client is waiting for an event.
If the direct origin emits events but the edge delivers them in one delayed block, inspect buffering on the MCP location. A narrowly scoped proxy_buffering off may be appropriate for that endpoint, but do not disable buffering globally because one streaming route needs it. Make the change only after checking response type, authorization, and the volume of the route.
The server can also send the X-Accel-Buffering response header, which Nginx documents as another way to enable or disable response buffering. Treat that as an explicit endpoint contract. Do not let arbitrary application input set it.
Timeouts are not the same as a slow tool
Nginx documents proxy_read_timeout as the time between successive reads from the upstream, with a default of 60 seconds. It is not simply “the maximum total request time.” If an SSE connection has no data for longer than the configured interval, the edge can close it even though the MCP server is still working.
Do not respond with an infinite timeout. Decide what the workflow needs:
- If the tool should return quickly, keep the timeout short and return a clear retryable error.
- If the workflow is long-running, use a bounded timeout plus an explicit status or task handle.
- If the server uses SSE, make sure the application and client have a documented reconnect path.
The MCP specification says a disconnect should not automatically be interpreted as cancellation. That is an important distinction for side effects. A network drop is not permission to run a purchase, message, or deletion again.
Preserve the request body and headers
Nginx also documents request buffering. With it enabled, the full body is read before the request is sent upstream; with it disabled, the body is sent as it arrives. Do not flip it blindly because a JSON-RPC request failed.
For a narrow MCP location, verify that the edge preserves:
- The POST method.
- The complete JSON-RPC body.
- The
AcceptandContent-Typeheaders. - The
Originheader where the server needs it. - The authorization or session headers required by your server.
The protocol’s security guidance requires servers to validate Origin and use proper authentication. A WAF rule that blocks an invalid Origin is doing useful work. Do not “fix” the failure by accepting every Origin, allowing unauthenticated access, or relying on a user-agent string as identity.
A safe diagnosis order
Use an owned fixture and one read-only tool:
- Call the origin directly and save status, headers, body type, and timing.
- Send the identical request through the edge.
- Find the first difference, rather than comparing final prose errors.
- Change one scoped proxy setting.
- Repeat the same fixture and keep the before/after response pair.
If you cannot identify the first difference, the next useful artifact is a redacted request/response packet for the infrastructure owner—not a broad WAF bypass.
The decision
Treat the CDN or WAF as part of the MCP transport. Preserve the protocol contract, isolate the MCP route, tune buffering and read timeouts only for that route, and keep Origin validation and authentication intact. If the origin passes and the edge fails, you have an edge configuration problem to fix. If both fail, return to the server contract. Either way, the smallest safe next step is a controlled comparison, not a security shortcut.
