mcp: Implement server-side support for discover method (SEP-2575) (#987)

## Summary
Part 3 of the SEP-2575 ("Make MCP Stateless") implementation. Builds on
PR #975 which landed the client-side discover/fallback flow and the
per-request `_meta` plumbing. This PR adds the **server-side**
counterparts:

- `server/discover` handler (replaces the
stub),ProtocolVersionSupporter`
- transport hook for per-transport version filtering,
- transport-agnostic version validation and method-removal enforcement
in `ServerSession.handle`,
- HTTP-level wire compliance for SEP-2575 error codes/status codes,
- bug fix in `internal/jsonrpc2`,`processResult` with an `MCPGODEBUG`
opt-out for backward compatibility.
This commit is contained in:
Guglielmo Colombo 2026-06-08 17:52:10 +02:00 • committed by GitHub
commit dd978160ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -766,9 +766,45 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm
// discover is the server-side handler for the SEP-2575 "server/discover" RPC.
//
// TODO: Complete implementation.
func (s *Server) discover(context.Context, *ServerRequest[*DiscoverParams]) (*DiscoverResult, error) {
return nil, jsonrpc2.ErrMethodNotFound
// 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) {
versions := req.Session.supportedVersions
if versions == nil {
versions = slices.Clone(supportedProtocolVersions)
}
req.Session.updateState(func(state *ServerSessionState) {
state.InitializeParams = &InitializeParams{
ProtocolVersion: req.ProtocolVersion(),
Capabilities: req.ClientCapabilities(),
ClientInfo: req.ClientInfo(),
}
})
return &DiscoverResult{
SupportedVersions: versions,
Capabilities: s.capabilities(),
ServerInfo: s.impl,
Instructions: s.opts.Instructions,
}, nil
}
// filterSupportedVersions returns the subset of [supportedProtocolVersions]
// that the Transport can serve. If t does not implement [ProtocolVersionSupporter], every
// SDK-supported version is included.
func filterSupportedVersions(t Transport) []string {
pvs, ok := t.(ProtocolVersionSupporter)
if !ok {
return slices.Clone(supportedProtocolVersions)
}
out := make([]string, 0, len(supportedProtocolVersions))
for _, v := range supportedProtocolVersions {
if pvs.SupportsProtocolVersion(v) {
out = append(out, v)
}
}
return out
}
func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListToolsResult, error) {
@ -1093,6 +1129,11 @@ func (s *Server) Connect(ctx context.Context, t Transport, opts *ServerSessionOp
return nil, err
}
// Compute the protocol versions this session can serve, filtered by the
// transport's capabilities (if it implements [ProtocolVersionSupporter]).
// The list is consumed by the SEP-2575 server/discover handler.
ss.supportedVersions = filterSupportedVersions(t)
// Start keepalive before returning the session to avoid race conditions with Close.
// This is safe because the spec allows sending pings before initialization (see ServerSession.handle for details).
if s.opts.KeepAlive > 0 {
@ -1177,6 +1218,12 @@ type ServerSession struct {
mcpConn Connection
keepaliveCancel context.CancelFunc
// supportedVersions is the subset of [supportedProtocolVersions] that the
// transport can actually serve, computed once at connection time from
// [ProtocolVersionSupporter] (if implemented by the transport) and used by
// the SEP-2575 server/discover handler.
supportedVersions []string
mu sync.Mutex
state ServerSessionState
}
@ -1511,6 +1558,23 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
if perRequestErr != nil {
return nil, perRequestErr
}
// server/discover is exempt from the version-validation gate so that a
// client probing with a version the server doesn't implement can still
// learn the server's supported versions from the response (whether the
// handler returns a DiscoverResult or an UnsupportedProtocolVersionError
// with a Data.Supported payload).
if validatedMeta.usesNewProtocol &&
!slices.Contains(supportedProtocolVersions, validatedMeta.initializeParams.ProtocolVersion) {
data, _ := json.Marshal(UnsupportedProtocolVersionData{
Supported: supportedProtocolVersions,
Requested: validatedMeta.initializeParams.ProtocolVersion,
})
return nil, &jsonrpc.Error{
Code: CodeUnsupportedProtocolVersion,
Message: "unsupported protocol version",
Data: data,
}
}
switch req.Method {
case methodInitialize, methodPing, notificationInitialized, methodSetLevel: