Bug description
VirtualMCPServer health probes use HTTP GET against backend URLs and treat non-200 responses as unhealthy. However, in the MCP Streamable HTTP transport, GET is defined as an optional mechanism for opening server-initiated SSE streams. Servers that do not support SSE streams may legitimately return 405 Method Not Allowed (per spec) or 400 Bad Request (e.g. when session management is enabled and no session ID is provided). A GET failure therefore does not imply that the MCP backend is unavailable.
The backend is then marked unavailable and excluded from tool routing, even though POST-based MCP calls (initialize, ping, tools/list, tools/call) all succeed.
Root cause: vMCP is using a transport-level GET response as a backend health signal, although GET semantics are optional/use-case-specific in Streamable HTTP and do not indicate whether the backend can process MCP JSON-RPC requests.
This is distinct from #6339 (probing unadvertised capabilities) — here the probe method itself produces a false-negative health signal.
Steps to reproduce
- Deploy Tableau MCP (
ghcr.io/tableau/tableau-mcp) as a standalone Deployment with TRANSPORT=http, listening on port 3927 at path /tableau-mcp.
- Register it via
MCPServerEntry with remoteUrl: http://tableau-mcp.<ns>.svc.cluster.local:3927/tableau-mcp and transport: streamable-http.
- Observe
VirtualMCPServer status:
$ kubectl get virtualmcpserver mcphub -o jsonpath='{.status.discoveredBackends[?(@.name=="tableau-mcp")]}'
{"name":"tableau-mcp","status":"unavailable","message":"Backend unavailable"}
- Verify the backend is actually healthy:
# MCP ping — succeeds (no authentication or session required)
curl -X POST http://tableau-mcp:3927/tableau-mcp \
-H "accept: application/json, text/event-stream" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"ping"}'
# → {"jsonrpc":"2.0","id":"1","result":{}}
# HTTP GET — rejected (this is what the health probe sends)
curl http://tableau-mcp:3927/tableau-mcp
# → HTTP 400: "Invalid or missing session ID"
The flow:
vMCP health probe
│
│ GET /tableau-mcp
▼
Tableau MCP → 400 (no session ID on GET)
│
└─→ vMCP marks "unavailable"
└─→ tools/list excludes Tableau tools
But actual MCP communication works:
MCP Client
│
│ POST /tableau-mcp {"method":"ping"}
▼
Tableau MCP → 200 + {"result":{}}
- Tools from this backend do not appear in the vMCP's
tools/list response to clients.
Why GET fails on this backend
In Streamable HTTP, a server that supports GET uses it to open an SSE stream for server-initiated messages. A server that does not support SSE streams returns 405 Method Not Allowed per the MCP spec. Tableau MCP returns 400 because it requires a session ID (issued during initialize) for all requests, and a bare GET without a session context is meaningless.
Both 405 and 400 are legitimate responses from a healthy backend that simply does not offer SSE streams or requires session context.
Expected behavior
The health probe should use an MCP-level check (e.g. POST with {"jsonrpc":"2.0","id":"healthcheck","method":"ping"}) rather than HTTP GET. The MCP specification defines ping as a standard utility method that requires no authentication or session, making it the natural health check primitive.
Alternatively, allow per-backend health probe configuration (method, path, or disable).
Actual behavior
The health probe sends GET to the backend URL. Backends that legitimately reject GET (no SSE support, or session-managed endpoints) return 400/405, and vMCP marks them unavailable, blocking all tool routing to that backend.
Workaround
None that preserves correct health semantics. An nginx sidecar returning 200 on GET masks real failures and defeats the purpose of health checking.
Environment
- ToolHive: v0.46.0 (operator + CRDs)
- Kubernetes: v1.33.7
- Backend:
ghcr.io/tableau/tableau-mcp:latest (v4.7.1)
- Resource type:
MCPServerEntry (also reproducible with MCPRemoteProxy)
References
Bug description
VirtualMCPServerhealth probes use HTTPGETagainst backend URLs and treat non-200 responses as unhealthy. However, in the MCP Streamable HTTP transport,GETis defined as an optional mechanism for opening server-initiated SSE streams. Servers that do not support SSE streams may legitimately return405 Method Not Allowed(per spec) or400 Bad Request(e.g. when session management is enabled and no session ID is provided). AGETfailure therefore does not imply that the MCP backend is unavailable.The backend is then marked
unavailableand excluded from tool routing, even thoughPOST-based MCP calls (initialize,ping,tools/list,tools/call) all succeed.Root cause: vMCP is using a transport-level
GETresponse as a backend health signal, althoughGETsemantics are optional/use-case-specific in Streamable HTTP and do not indicate whether the backend can process MCP JSON-RPC requests.This is distinct from #6339 (probing unadvertised capabilities) — here the probe method itself produces a false-negative health signal.
Steps to reproduce
ghcr.io/tableau/tableau-mcp) as a standalone Deployment withTRANSPORT=http, listening on port 3927 at path/tableau-mcp.MCPServerEntrywithremoteUrl: http://tableau-mcp.<ns>.svc.cluster.local:3927/tableau-mcpandtransport: streamable-http.VirtualMCPServerstatus:The flow:
tools/listresponse to clients.Why GET fails on this backend
In Streamable HTTP, a server that supports
GETuses it to open an SSE stream for server-initiated messages. A server that does not support SSE streams returns405 Method Not Allowedper the MCP spec. Tableau MCP returns400because it requires a session ID (issued duringinitialize) for all requests, and a bareGETwithout a session context is meaningless.Both
405and400are legitimate responses from a healthy backend that simply does not offer SSE streams or requires session context.Expected behavior
The health probe should use an MCP-level check (e.g.
POSTwith{"jsonrpc":"2.0","id":"healthcheck","method":"ping"}) rather than HTTPGET. The MCP specification definespingas a standard utility method that requires no authentication or session, making it the natural health check primitive.Alternatively, allow per-backend health probe configuration (method, path, or disable).
Actual behavior
The health probe sends
GETto the backend URL. Backends that legitimately rejectGET(no SSE support, or session-managed endpoints) return 400/405, and vMCP marks themunavailable, blocking all tool routing to that backend.Workaround
None that preserves correct health semantics. An nginx sidecar returning
200onGETmasks real failures and defeats the purpose of health checking.Environment
ghcr.io/tableau/tableau-mcp:latest(v4.7.1)MCPServerEntry(also reproducible withMCPRemoteProxy)References
405 Method Not Allowedwhen SSE is not supported: https://modelcontextprotocol.io/specification/draft/basic/transports/streamable-httppingfor health checks: https://github.com/tableau/tableau-mcp/blob/main/docs/docs/enterprise/tableau-cloud.mdmcp.tableau.comdirectly)