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
22 changes: 18 additions & 4 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ const DefaultPageSize = 1000
// sessions by using [Server.Run].
type Server struct {
// fixed at creation
impl *Implementation
opts ServerOptions
impl *Implementation
opts ServerOptions
supportedVersions []string

mu sync.Mutex
prompts *featureSet[*serverPrompt]
Expand Down Expand Up @@ -135,6 +136,11 @@ type ServerOptions struct {
// trade-offs and usage guidance.
SchemaCache *SchemaCache

// SupportedVersions optionally specifies the list of MCP protocol versions
// that this server supports. If empty (the zero value), the default set of
// supported versions is used (see supportedProtocolVersions in shared.go).
SupportedVersions []string

// GetSessionID provides the next session ID to use for an incoming request.
// If nil, a default randomly generated ID will be used.
//
Expand Down Expand Up @@ -184,7 +190,7 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
opts.Logger = ensureLogger(nil)
}

return &Server{
s := &Server{
impl: impl,
opts: opts,
prompts: newFeatureSet(func(p *serverPrompt) string { return p.prompt.Name }),
Expand All @@ -196,6 +202,14 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
resourceSubscriptions: make(map[string]map[*ServerSession]bool),
pendingNotifications: make(map[string]*time.Timer),
}

if len(opts.SupportedVersions) > 0 {
s.supportedVersions = slices.Clone(opts.SupportedVersions)
} else {
s.supportedVersions = supportedProtocolVersions
}

return s
}

// AddPrompt adds a [Prompt] to the server, or replaces one with the same name.
Expand Down Expand Up @@ -1485,7 +1499,7 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
return &InitializeResult{
// TODO(rfindley): alter behavior when falling back to an older version:
// reject unsupported features.
ProtocolVersion: negotiatedVersion(params.ProtocolVersion),
ProtocolVersion: negotiatedVersion(params.ProtocolVersion, s.supportedVersions),
Capabilities: s.capabilities(),
Instructions: s.opts.Instructions,
ServerInfo: s.impl,
Expand Down
43 changes: 43 additions & 0 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1007,3 +1007,46 @@ func TestServerCapabilitiesOverWire(t *testing.T) {
})
}
}

func TestServerSupportedVersions(t *testing.T) {
customVersions := []string{"2025-11-25", "2025-03-26"}

t.Run("custom SupportedVersions is set", func(t *testing.T) {
s := NewServer(&Implementation{Name: "test", Version: "1.0"}, &ServerOptions{
SupportedVersions: customVersions,
})
if s.supportedVersions == nil {
t.Fatal("supportedVersions is nil, want non-nil")
}
if diff := cmp.Diff(customVersions, s.supportedVersions); diff != "" {
t.Errorf("supportedVersions mismatch (-want +got):\n%s", diff)
}
})

t.Run("zero value SupportedVersions uses default", func(t *testing.T) {
s := NewServer(&Implementation{Name: "test", Version: "1.0"}, nil)
if diff := cmp.Diff(supportedProtocolVersions, s.supportedVersions); diff != "" {
t.Errorf("supportedVersions mismatch (-want +got):\n%s", diff)
}
})

t.Run("negotiatedVersion with custom supported versions", func(t *testing.T) {
tests := []struct {
clientVersion string
want string
}{
{"2025-11-25", "2025-11-25"}, // supported, use as-is
{"2025-03-26", "2025-03-26"}, // supported, use as-is
{"2025-06-18", "2025-11-25"}, // not in custom list, fall back to first
{"2024-11-05", "2025-11-25"}, // not in custom list, fall back to first
}
for _, tc := range tests {
t.Run(tc.clientVersion, func(t *testing.T) {
got := negotiatedVersion(tc.clientVersion, customVersions)
if got != tc.want {
t.Errorf("negotiatedVersion(%q, customVersions) = %q, want %q", tc.clientVersion, got, tc.want)
}
})
}
})
}
8 changes: 4 additions & 4 deletions mcp/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ var supportedProtocolVersions = []string{
}

// negotiatedVersion returns the effective protocol version to use, given a
// client version.
func negotiatedVersion(clientVersion string) string {
// client version and the server's supported versions list.
func negotiatedVersion(clientVersion string, supportedVersions []string) string {
// In general, prefer to use the clientVersion, but if we don't support the
// client's version, use the latest version.
//
// This handles the case where a new spec version is released, and the SDK
// does not support it yet.
if !slices.Contains(supportedProtocolVersions, clientVersion) {
return latestProtocolVersion
if !slices.Contains(supportedVersions, clientVersion) {
return supportedVersions[0]
}
return clientVersion
}
Expand Down
19 changes: 11 additions & 8 deletions mcp/streamable.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,18 +387,21 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
if protocolVersion == "" {
protocolVersion = protocolVersion20250326
}
if !slices.Contains(supportedProtocolVersions, protocolVersion) {
http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)

// Get the server to access its supported versions
server := h.getServer(req)
if server == nil {
// The getServer argument to NewStreamableHTTPHandler returned nil.
http.Error(w, "no server available", http.StatusBadRequest)
return
}

if !slices.Contains(server.supportedVersions, protocolVersion) {
http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(server.supportedVersions, ",")), http.StatusBadRequest)
return
}

if sessInfo == nil {
server := h.getServer(req)
if server == nil {
// The getServer argument to NewStreamableHTTPHandler returned nil.
http.Error(w, "no server available", http.StatusBadRequest)
return
}
if sessionID == "" {
// In stateless mode, sessionID may be nonempty even if there's no
// existing transport.
Expand Down
2 changes: 1 addition & 1 deletion mcp/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ func (c *ioConn) sessionUpdated(state ServerSessionState) {
// was not required.
protocolVersion = protocolVersion20250326
}
protocolVersion = negotiatedVersion(protocolVersion)
protocolVersion = negotiatedVersion(protocolVersion, supportedProtocolVersions)
c.sessionMu.Lock()
c.protocolVersion = protocolVersion
c.sessionMu.Unlock()
Expand Down
Loading