Skip to content

vMCP health probe uses HTTP GET, breaking backends that only accept POST (e.g. Tableau MCP) #6497

Description

@yoojiyeon-ktc

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

  1. Deploy Tableau MCP (ghcr.io/tableau/tableau-mcp) as a standalone Deployment with TRANSPORT=http, listening on port 3927 at path /tableau-mcp.
  2. Register it via MCPServerEntry with remoteUrl: http://tableau-mcp.<ns>.svc.cluster.local:3927/tableau-mcp and transport: streamable-http.
  3. Observe VirtualMCPServer status:
$ kubectl get virtualmcpserver mcphub -o jsonpath='{.status.discoveredBackends[?(@.name=="tableau-mcp")]}'
{"name":"tableau-mcp","status":"unavailable","message":"Backend unavailable"}
  1. 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":{}}
  1. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions