From 5f0acf0c30b379f6551b23e80a8cbe5d88c3ca3d Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:29:30 +0700 Subject: [PATCH] mcp: export Server.Discover for host introspection Expose the SEP-2575 discover payload as Server.Discover so hosts can inspect capabilities/instructions/supportedVersions without synthesizing an inbound request. Inbound server/discover still establishes session identity; customization continues via receiving middleware. Fixes #1092 --- mcp/server.go | 55 ++++++++++++++++++++++++++++++++++----------- mcp/server_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/mcp/server.go b/mcp/server.go index c189a8ee..4a473703 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -875,6 +875,41 @@ 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 full +// list of versions supported by this SDK. +// +// Discover is intended for host introspection (for example health checks) and +// does not mutate session state. Inbound server/discover RPCs still run the +// internal handler, which may establish session identity from the client +// request. +// +// 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(supportedProtocolVersions) + } else { + versions = slices.Clone(versions) + } + res := &DiscoverResult{ + SupportedVersions: versions, + Capabilities: s.capabilities(), + Instructions: s.opts.Instructions, + } + res.setDefaultCacheableValues() + 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, @@ -882,12 +917,6 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm // instructions, allowing clients to negotiate without performing the legacy // initialize handshake. func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams]) (*DiscoverResult, error) { - req.Session.mu.Lock() - versions := req.Session.supportedVersions - req.Session.mu.Unlock() - if versions == nil { - versions = slices.Clone(supportedProtocolVersions) - } // Read the request-scoped identity/capabilities before acquiring the // session lock: these accessors may fall back to Session.InitializeParams // (which also locks Session.mu), so calling them from inside updateState @@ -903,18 +932,18 @@ func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams] // is never surfaced to the client via Mcp-Session-Id; leaving // InitializeParams nil lets serveStatefulPOST's safety-net cleanup // close it instead of leaking. + req.Session.mu.Lock() + versions := req.Session.supportedVersions + req.Session.mu.Unlock() + if versions == nil { + versions = slices.Clone(supportedProtocolVersions) + } if supportedVersion := negotiateMutuallySupportedVersion(versions); supportedVersion >= protocolVersion20260728 { req.Session.updateState(func(state *ServerSessionState) { state.InitializeParams = init }) } - res := &DiscoverResult{ - SupportedVersions: versions, - Capabilities: s.capabilities(), - Instructions: s.opts.Instructions, - } - res.setDefaultCacheableValues() - return res, nil + return s.Discover(req.Session), nil } // filterSupportedVersions returns the subset of [supportedProtocolVersions] diff --git a/mcp/server_test.go b/mcp/server_test.go index 75d7455c..4f3a8d2d 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1017,6 +1017,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()