diff --git a/pkg/vmcp/server/peer_probe.go b/pkg/vmcp/server/peer_probe.go index 9947760f8b..08c9d827ce 100644 --- a/pkg/vmcp/server/peer_probe.go +++ b/pkg/vmcp/server/peer_probe.go @@ -27,8 +27,14 @@ type peerProbeKey struct{} // peerProbeMethods are the MCP methods a peer probe may send: the handshake and // the capability lists that the health checker's ListCapabilities runs. A probe // never calls a tool, reads a resource or gets a prompt. +// +// server/discover is the Modern (2026-07-28) handshake. Once the peer has +// cached a backend as Modern, its health checker opens with it instead of +// initialize. Refusing it makes the peer read a 403 as an auth failure, mark +// the backend unauthenticated and open its circuit breaker. var peerProbeMethods = map[string]bool{ "initialize": true, + "server/discover": true, "notifications/initialized": true, "ping": true, "tools/list": true, diff --git a/pkg/vmcp/server/serve_health_probe_test.go b/pkg/vmcp/server/serve_health_probe_test.go index 3b39a80e90..0451afea8c 100644 --- a/pkg/vmcp/server/serve_health_probe_test.go +++ b/pkg/vmcp/server/serve_health_probe_test.go @@ -30,6 +30,14 @@ func TestPeerProbeMiddleware(t *testing.T) { listBody = `{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}` callBody = `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo"}}` getBody = `{"jsonrpc":"2.0","id":4,"method":"prompts/get","params":{"name":"p"}}` + // The Modern (2026-07-28) health probe opens with server/discover + // instead of initialize (pkg/vmcp/client probeRevision and + // modernListCapabilities). + discoverBody = `{"jsonrpc":"2.0","id":5,"method":"server/discover","params":{"_meta":{` + + `"io.modelcontextprotocol/protocolVersion":"2026-07-28",` + + `"io.modelcontextprotocol/clientCapabilities":{},` + + `"io.modelcontextprotocol/clientInfo":{"name":"toolhive-vmcp","version":"test"}}}}` + listenBody = `{"jsonrpc":"2.0","id":6,"method":"subscriptions/listen","params":{}}` ) tests := []struct { name string @@ -41,6 +49,8 @@ func TestPeerProbeMiddleware(t *testing.T) { }{ {"probe initialize is marked", http.MethodPost, health.ProbeHeaderValue, initBody, true, true}, {"probe tools/list is marked", http.MethodPost, health.ProbeHeaderValue, listBody, true, true}, + {"probe server/discover is marked", http.MethodPost, health.ProbeHeaderValue, discoverBody, true, true}, + {"probe subscriptions/listen is refused", http.MethodPost, health.ProbeHeaderValue, listenBody, false, false}, {"probe tools/call is refused", http.MethodPost, health.ProbeHeaderValue, callBody, false, false}, {"probe prompts/get is refused", http.MethodPost, health.ProbeHeaderValue, getBody, false, false}, {"probe with an unparsable body is refused", http.MethodPost, health.ProbeHeaderValue, `not json`, false, false},