Repository navigation
mcp-client: streamable-http transport is rejected with HTTP 403 by WAF-fronted MCP servers (version-negotiation probe) #11767
Replies: 4 comments
|
The mcp-remote comparison is a useful workaround. For a fix, I’d avoid falling back on every 403: it can also mean genuinely denied access, and an automatic retry may hide that. |
|
Could this be related to how the client handles the initial handshake? Since Also, does the 403 response include any WAF headers or request ID? That might help narrow down whether the server is rejecting the request itself or something about the SDK's handshake. Just a thought, but I think comparing those two requests would be a good starting point. |
|
Thanks both — I went and captured the probe instead of guessing. Two findings, and one of them corrects my original framing. 1. What the probe actually isReading new Client({ name: "dsh-mcp-client", version: "0.0.1" }, {
capabilities: {},
versionNegotiation: { mode: "auto" },
listChanged: { tools: { autoRefresh: false, debounceMs: 0 } }
});So DSH opts into negotiation explicitly; the SDK default is That is the request behind the 2. The endpoint does not reject that requestI reproduced the exact setup in a standalone Node 24 process (same SDK 2.2.0, same
Checked from the same host as well: So the probe shape itself is fine, and "the WAF blocks the version-negotiation probe" (my original title) is wrong as a general claim. The 403 is specific to the request as issued from the DSH host process. That part I still cannot see from outside the app, and it is the only missing piece. 3. On the 403 body — one real captureEarlier, while the IP was rate-limited, the same endpoint answered with a bare ALB page and no WAF request id: That one was a temporary rate-based block (cleared after ~5 minutes, 4. Agreed on not falling back on 403
Agreed, and the SDK already refuses to treat 403 as retryable for that reason. The minimal fix looks smaller than a fallback: pass config:
serverName: tavily
transport: streamable-http
url: …
versionNegotiation: legacy # or auto | "2026-07-28"gives an explicit, documented opt-out where the modern handshake misbehaves, without turning a 403 into "retry differently". Happy to test whichever build exposes it. Would a host-side capture help you more than this? If you tell me what to instrument — or expose a debug flag on |
|
Agreed, the probe isn't the problem, the two 403s stay separate, and versionNegotiation as plugin config beats any fallback. Default it to legacy (the SDK's own default) so the modern handshake is an explicit opt-in, and log the effective mode + negotiated version once at connect - this failure was expensive to diagnose mainly because the mode was invisible. |
Uh oh!
There was an error while loading. Please reload this page.
mcp-client:
streamable-httptransport is rejected with HTTP 403 by WAF-fronted MCP servers (version-negotiation probe)Environment
0.2.1-rc.5, desktop profile, Windows 11 (PowerShell 7)@deepseek-ai/dsh-mcp-clientwith bundled@modelcontextprotocol/client@2.2.0https://mcp.tavily.com/mcp/?tavilyApiKey=…,serverInfo: tavily-mcp 4.1.4Expected
A bundle entry with
connects, lists tools and registers them on
ctx.tools.Actual
It is the client, not the server or the key
Same URL, same machine, same moment:
streamable-http→403(HTML body from the WAF, not a JSON-RPC error).mcp-remote(MCP SDK 1.x proxy, stdio → http) with the identical URL → works:tools/listreturns 5 tools,tavily_searchreturns results.stdio+mcp-remote→ works and has been stable for repeated calls.So the failing part is the request(s) the v2 client issues around
initialize— the version-negotiation probe — and the WAF rejects that shape.Additional data points
Accept: application/jsonalone (withouttext/event-stream) returns406.403— including the plain REST APIhttps://api.tavily.com/search. After ~5 minutes the block cleared andcurlwas200again, while the DSH client still returned403. So the client-side failure is not explained by the temporary block.Suggested fix
initializewith the spec-default protocol version, instead of failing the whole connection.negotiateProtocolVersion: false.User-Agent/Accept-Language) — many WAF-fronted MCP endpoints appear to expect them.Workaround for users
Use the stdio bridge, e.g.
This is what I am running now; everything works, but it costs an extra process and is a separate failure point (the bridge has already dropped with
SdkError: Connection closedon its own once).All reactions