diff --git a/mcp/server.go b/mcp/server.go index 6811e5d9..aa177e4e 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -990,6 +990,43 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm return res, err } +// Discover returns the [DiscoverResult] the server would advertise via the +// SEP-2575 server/discover method. +// +// If ss is non-nil, SupportedVersions reflects the protocol versions that +// session's transport can serve. If ss is nil, SupportedVersions is the list +// this server advertises: every version from [SupportedProtocolVersions], +// narrowed by [ServerOptions.SupportedProtocolVersions] when that option is set. +// +// Discover is intended for host introspection (for example health checks) and +// does not mutate session state. It applies only the protocol cache default +// (cacheScope "public"). Inbound server/discover RPCs still run the internal +// handler, which may establish session identity from the client request and +// which settles cache hints via [ServerOptions.SetCacheable]. +// +// To customize responses to inbound server/discover requests, use +// [Server.AddReceivingMiddleware]. +func (s *Server) Discover(ss *ServerSession) *DiscoverResult { + var versions []string + if ss != nil { + ss.mu.Lock() + versions = ss.supportedVersions + ss.mu.Unlock() + } + if versions == nil { + versions = slices.Clone(s.protocolVersions) + } else { + versions = slices.Clone(versions) + } + res := &DiscoverResult{ + SupportedVersions: versions, + Capabilities: s.capabilities(), + Instructions: s.opts.Instructions, + } + res.normalize() + return res +} + // discover is the server-side handler for the SEP-2575 "server/discover" RPC. // // It returns the protocol versions supported by the underlying transport, @@ -1021,11 +1058,10 @@ func (s *Server) discover(ctx context.Context, req *ServerRequest[*DiscoverParam state.NegotiatedProtocolVersion = init.ProtocolVersion }) } - res := &DiscoverResult{ - SupportedVersions: versions, - Capabilities: s.capabilities(), - Instructions: s.opts.Instructions, - } + res := s.Discover(req.Session) + // Discover fills the protocol cache default for host callers. Hand + // resolveCacheable the unset value so SetCacheable still sees "no opinion". + res.Cacheable = Cacheable{} s.resolveCacheable(ctx, req, &res.Cacheable) return res, nil } diff --git a/mcp/server_test.go b/mcp/server_test.go index 52988db4..1da31415 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1064,6 +1064,62 @@ func TestClientRootCapabilities(t *testing.T) { } } +func TestServerDiscover(t *testing.T) { + const instructions = "use carefully" + s := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, &ServerOptions{ + Instructions: instructions, + HasTools: true, + }) + + t.Run("nil session uses SDK supported versions", func(t *testing.T) { + got := s.Discover(nil) + if got == nil { + t.Fatal("Discover(nil) returned nil") + } + if diff := cmp.Diff(supportedProtocolVersions, got.SupportedVersions); diff != "" { + t.Errorf("SupportedVersions mismatch (-want +got):\n%s", diff) + } + if got.Instructions != instructions { + t.Errorf("Instructions = %q, want %q", got.Instructions, instructions) + } + if got.Capabilities == nil || got.Capabilities.Tools == nil { + t.Errorf("Capabilities.Tools = %v, want non-nil when HasTools is set", got.Capabilities) + } + if got.CacheScope != "public" { + t.Errorf("CacheScope = %q, want public", got.CacheScope) + } + }) + + t.Run("session versions and no state mutation", func(t *testing.T) { + ctx := context.Background() + _, st := NewInMemoryTransports() + ss, err := s.Connect(ctx, st, nil) + if err != nil { + t.Fatal(err) + } + defer ss.Close() + + if ss.InitializeParams() != nil { + t.Fatal("session already initialized before Discover") + } + got := s.Discover(ss) + if got == nil { + t.Fatal("Discover(session) returned nil") + } + // In-memory transport does not filter versions; session should + // advertise the full SDK list. + if diff := cmp.Diff(supportedProtocolVersions, got.SupportedVersions); diff != "" { + t.Errorf("SupportedVersions mismatch (-want +got):\n%s", diff) + } + if got.Instructions != instructions { + t.Errorf("Instructions = %q, want %q", got.Instructions, instructions) + } + if ss.InitializeParams() != nil { + t.Error("Discover(session) mutated session identity; want no side effects") + } + }) +} + func TestServerRejectsDuplicateInitialize(t *testing.T) { ctx := context.Background()