Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -875,19 +875,48 @@ 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,
// the server's capabilities, the server's identity, and the server's
// 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
Expand All @@ -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]
Expand Down
56 changes: 56 additions & 0 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading