mcp: add 2026-07-28 to the supported protocol versions (#1020)

This PR adds the protocol version `2026-07-28` to the list of supported
protocol versions and fixes legacy tests hardcoding the legacy protocol
version for those use cases.
This commit is contained in:
Guglielmo Colombo 2026-06-24 12:02:56 +02:00 • committed by GitHub
commit 7f4aa4a0ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 355 additions and 218 deletions

View file

@ -27,8 +27,12 @@ func TestServerListChangedNotifications(t *testing.T) {
wantNotifyCount int64
}{
{
name: "Default: notification sent",
serverOpts: nil,
name: "Default: notification sent",
serverOpts: &ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true},
},
},
wantNotifyCount: 1,
},
{

View file

@ -403,7 +403,7 @@ func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeRe
Meta: Meta{
MetaKeyProtocolVersion: protocolVersion,
MetaKeyClientInfo: c.impl,
MetaKeyClientCapabilities: caps,
MetaKeyClientCapabilities: caps.toV2(),
},
}
req := &DiscoverRequest{Session: cs, Params: params}
@ -518,7 +518,7 @@ func injectRequestMeta[T any, P interface {
m[MetaKeyClientInfo] = cs.client.impl
}
if _, ok := m[MetaKeyClientCapabilities]; !ok {
m[MetaKeyClientCapabilities] = cs.client.capabilities(res.ProtocolVersion)
m[MetaKeyClientCapabilities] = cs.client.capabilities(res.ProtocolVersion).toV2()
}
params.SetMeta(m)
return params

View file

@ -10,7 +10,6 @@ import (
"context"
"fmt"
"log/slog"
"slices"
"sync/atomic"
"testing"
@ -644,13 +643,6 @@ func TestClientCapabilitiesOverWire(t *testing.T) {
// don't overlap with the SDK. The test then asserts the resulting session
// state and whether the legacy initialize handshake ran.
func TestClientConnectDiscover(t *testing.T) {
// Temporarily enable 2026-07-28 support in the SDK for this test
oldSupported := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, supportedProtocolVersions...)
t.Cleanup(func() {
supportedProtocolVersions = oldSupported
})
const otherVersionsOnly = "1999-01-01"
tests := []struct {
@ -686,7 +678,7 @@ func TestClientConnectDiscover(t *testing.T) {
return nil, jsonrpc2.ErrMethodNotFound
},
wantInitialize: true,
wantVersion: latestProtocolVersion,
wantVersion: protocolVersion20251125,
},
{
name: "unsupported protocol version falls back to initialize",
@ -697,7 +689,7 @@ func TestClientConnectDiscover(t *testing.T) {
}
},
wantInitialize: true,
wantVersion: latestProtocolVersion,
wantVersion: protocolVersion20251125,
},
{
name: "no overlapping supported version falls back to initialize",
@ -709,7 +701,7 @@ func TestClientConnectDiscover(t *testing.T) {
}, nil
},
wantInitialize: true,
wantVersion: latestProtocolVersion,
wantVersion: protocolVersion20251125,
},
}
@ -771,10 +763,6 @@ func TestClientConnectDiscover(t *testing.T) {
// request sent by Client.Connect carries the SEP-2575 per-request _meta triple:
// protocolVersion, clientInfo, and clientCapabilities.
func TestClientConnectDiscover_RequestContents(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
type captured struct {
@ -850,10 +838,6 @@ func TestClientConnectDiscover_RequestContents(t *testing.T) {
func TestInMemory_E2E_DiscoverSuccess(t *testing.T) {
ctx := context.Background()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "stdio-like-server", Version: "v1"}, nil)
ct, st := NewInMemoryTransports()
ss, err := server.Connect(ctx, st, nil)
@ -889,15 +873,11 @@ func TestInMemory_E2E_DiscoverSuccess(t *testing.T) {
// TestInMemory_E2E_DiscoverFallback_NoOverlap verifies the fallback path
// over an InMemory (STDIO-equivalent) transport: the client probes with
// _meta.protocolVersion = 2026-07-28, but the server's supported list does
// NOT include that version (the default for an SDK server that hasn't
// shimmed supportedProtocolVersions).
// _meta.protocolVersion = 2026-07-28, but the server overrides discover via
// middleware to advertise only legacy versions, so the client must fall
// back to the legacy initialize handshake.
func TestInMemory_E2E_DiscoverFallback_NoOverlap(t *testing.T) {
ctx := context.Background()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "vpre-like-server", Version: "v1"}, nil)
// Intercept discover and reply as if we were a server that only
// supports legacy versions.
@ -932,9 +912,11 @@ func TestInMemory_E2E_DiscoverFallback_NoOverlap(t *testing.T) {
if ir == nil {
t.Fatal("InitializeResult is nil after fallback initialize")
}
if ir.ProtocolVersion != latestProtocolVersion {
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if ir.ProtocolVersion != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (legacy fallback after no-overlap discover)",
ir.ProtocolVersion, latestProtocolVersion)
ir.ProtocolVersion, protocolVersion20251125)
}
// Prove the session is usable after fallback.
@ -949,10 +931,6 @@ func TestInMemory_E2E_DiscoverFallback_NoOverlap(t *testing.T) {
func TestInMemory_E2E_DiscoverFallback_MethodNotFound(t *testing.T) {
ctx := context.Background()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "vpre-server", Version: "v1"}, nil)
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
return func(ctx context.Context, method string, req Request) (Result, error) {
@ -981,9 +959,11 @@ func TestInMemory_E2E_DiscoverFallback_MethodNotFound(t *testing.T) {
if ir == nil {
t.Fatal("InitializeResult is nil after fallback initialize")
}
if ir.ProtocolVersion != latestProtocolVersion {
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if ir.ProtocolVersion != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (legacy fallback after MethodNotFound)",
ir.ProtocolVersion, latestProtocolVersion)
ir.ProtocolVersion, protocolVersion20251125)
}
if _, err := cs.ListTools(ctx, nil); err != nil {
@ -998,10 +978,6 @@ func TestInMemory_E2E_DiscoverFallback_MethodNotFound(t *testing.T) {
func TestInMemory_E2E_DiscoverFallback_UnsupportedProtocolVersion(t *testing.T) {
ctx := context.Background()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "strict-server", Version: "v1"}, nil)
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
return func(ctx context.Context, method string, req Request) (Result, error) {
@ -1033,9 +1009,11 @@ func TestInMemory_E2E_DiscoverFallback_UnsupportedProtocolVersion(t *testing.T)
if ir == nil {
t.Fatal("InitializeResult is nil after fallback initialize")
}
if ir.ProtocolVersion != latestProtocolVersion {
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if ir.ProtocolVersion != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (legacy fallback after UnsupportedProtocolVersion)",
ir.ProtocolVersion, latestProtocolVersion)
ir.ProtocolVersion, protocolVersion20251125)
}
}
@ -1045,10 +1023,6 @@ func TestInMemory_E2E_DiscoverFallback_UnsupportedProtocolVersion(t *testing.T)
// UnsupportedProtocolVersionError carries Data.Supported, and the client
// selects a mutually supported version from that list and retries.
func TestClientConnectDiscover_UnsupportedVersionNegotiation(t *testing.T) {
oldSupported := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(oldSupported)...)
t.Cleanup(func() { supportedProtocolVersions = oldSupported })
ctx := context.Background()
const unsupportedClientVersion = "2099-12-31"

View file

@ -143,6 +143,31 @@ func incTool(_ context.Context, _ *CallToolRequest, args incInput) (*CallToolRes
return nil, incOutput{args.X + 1}, nil
}
// confirmThenGreet exercises the multi-round-trip pattern in
// conformance fixtures.
func confirmThenGreet(_ context.Context, req *CallToolRequest, _ struct{}) (*CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &CallToolResult{
InputRequests: InputRequestMap{
"who": &ElicitParams{
Message: "What is your name?",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"name": {Type: "string"},
},
},
},
},
RequestState: "step=1",
}, nil, nil
}
name := req.Params.InputResponses["who"].(*ElicitResult).Content["name"].(string)
return &CallToolResult{
Content: []Content{&TextContent{Text: "hello " + name}},
}, nil, nil
}
var iconObj = Icon{
Source: "foobar",
MIMEType: "image/png",
@ -193,6 +218,11 @@ func runServerTest(t *testing.T, test *conformanceTest) {
Title: "contentTool",
Description: "return resourceLink content with Icon",
}, contentTool)
case "confirmThenGreet":
AddTool(s, &Tool{
Name: "confirmThenGreet",
Description: "ask the user for their name via elicitation, then greet them",
}, confirmThenGreet)
default:
t.Fatalf("unknown tool %q", tn)
}

View file

@ -19,19 +19,13 @@ import (
func Example_lifecycle() {
ctx := context.Background()
// Create a client and server.
// Wait for the client to initialize the session.
// Create a client and server. Under protocol version 2026-07-28, there
// is no dedicated initialize/initialized handshake: the session is
// implicitly live the moment the client issues its first request.
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
InitializedHandler: func(context.Context, *mcp.InitializedRequest) {
fmt.Println("initialized!")
},
})
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
// Connect the server and client using in-memory transports.
//
// Connect the server first so that it's ready to receive initialization
// messages from the client.
t1, t2 := mcp.NewInMemoryTransports()
serverSession, err := server.Connect(ctx, t1, nil)
if err != nil {
@ -50,7 +44,7 @@ func Example_lifecycle() {
if err := serverSession.Wait(); err != nil {
log.Fatal(err)
}
// Output: initialized!
// Output:
}
// !-lifecycle

View file

@ -171,7 +171,10 @@ func TestEndToEnd(t *testing.T) {
c.AddRoots(&Root{URI: "file://" + rootAbs})
// Connect the client.
cs, err := c.Connect(ctx, ct, nil)
//
// Pin the session to 2025-11-25 so the legacy
// semantics apply.
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -758,7 +761,10 @@ func TestMiddleware(t *testing.T) {
c.AddSendingMiddleware(traceCalls[*ClientSession](&cbuf, "S1"), traceCalls[*ClientSession](&cbuf, "S2"))
c.AddReceivingMiddleware(traceCalls[*ClientSession](&cbuf, "R1"), traceCalls[*ClientSession](&cbuf, "R2"))
cs, err := c.Connect(ctx, ct, nil)
// Pin to 2025-11-25 because the test's expected wire sequence asserts
// the legacy initialize / notifications/initialized handshake, which
// 2026-07-28 replaces with server/discover.
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -1903,7 +1909,10 @@ func TestKeepAliveFailure_Logged(t *testing.T) {
Logger: slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelError})),
}
c := NewClient(testImpl, clientOpts)
cs, err := c.Connect(ctx, ct, nil)
// Pin to 2025-11-25: KeepAlive uses the ping RPC, which is removed
// in 2026-07-28, so keepalive is only meaningful on legacy protocol
// versions.
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -2570,18 +2579,10 @@ func newSubListenServer() *Server {
return s
}
func enableNewProtocol(t *testing.T) {
t.Helper()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
}
// TestSubscriptionsListen_InMemory exercises the listen flow over the
// session-shared in-memory transport (semantically equivalent to STDIO).
// Cancellation here propagates via notifications/cancelled.
func TestSubscriptionsListen_InMemory(t *testing.T) {
enableNewProtocol(t)
events := make(chan subListenEvent, 64)
server := newSubListenServer()
ct, st := NewInMemoryTransports()
@ -2597,7 +2598,6 @@ func TestSubscriptionsListen_InMemory(t *testing.T) {
// stateless HTTP server (SEP-2575). Each listen rides its own SSE response
// stream; cs.Close() tears it down.
func TestSubscriptionsListen_Streamable(t *testing.T) {
enableNewProtocol(t)
events := make(chan subListenEvent, 64)
server := newSubListenServer()
handler := NewStreamableHTTPHandler(
@ -2615,7 +2615,6 @@ func TestSubscriptionsListen_Streamable(t *testing.T) {
// auto-listen on connect, and therefore does not receive any acknowledgment
// or downstream notifications.
func TestSubscriptionsListen_NoHandlersNoListen(t *testing.T) {
enableNewProtocol(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@ -2694,7 +2693,6 @@ type resourceSubEvent struct {
// subscriptionsListen handler so HTTP disconnect is observed directly; this
// is tracked separately.
func TestResourceSubscriptions_Streamable(t *testing.T) {
enableNewProtocol(t)
subCh := make(chan string, 8)
unsubCh := make(chan string, 8)
@ -2764,7 +2762,6 @@ func TestResourceSubscriptions_Streamable(t *testing.T) {
// over an in-memory (stdio-equivalent) transport: the per-URI Subscribe path
// uses notifications/cancelled rather than HTTP disconnect for teardown.
func TestResourceSubscriptions_InMemory(t *testing.T) {
enableNewProtocol(t)
subCh := make(chan string, 8)
unsubCh := make(chan string, 8)
@ -2849,7 +2846,6 @@ func TestResourceSubscriptions_InMemory(t *testing.T) {
// second call: it returns nil without invoking SubscribeHandler again and
// without opening a second listen stream.
func TestResourceSubscriptions_Subscribe_Idempotent(t *testing.T) {
enableNewProtocol(t)
subCh := make(chan string, 8)
unsubCh := make(chan string, 8)
@ -2914,7 +2910,6 @@ func TestResourceSubscriptions_Subscribe_Idempotent(t *testing.T) {
// stream with a distinct subscription ID. Unsubscribing one does not affect
// the other.
func TestResourceSubscriptions_MultipleURIs(t *testing.T) {
enableNewProtocol(t)
subCh := make(chan string, 8)
unsubCh := make(chan string, 8)
@ -3032,7 +3027,6 @@ func TestResourceSubscriptions_MultipleURIs(t *testing.T) {
// notification is fanned out to BOTH sessions, each with its own subscription
// ID; closing one session does not affect deliveries to the other.
func TestSubscriptionsListen_MultipleSessions(t *testing.T) {
enableNewProtocol(t)
server := newSubListenServer()
@ -3110,7 +3104,6 @@ func TestSubscriptionsListen_MultipleSessions(t *testing.T) {
// auto-listen branch (the other two list-changed types are covered by
// runSubscriptionsListenTest, but resources is not).
func TestSubscriptionsListen_ResourceListChanged(t *testing.T) {
enableNewProtocol(t)
events := make(chan subListenEvent, 16)
server := NewServer(testImpl, nil)
@ -3182,7 +3175,6 @@ func TestSubscriptionsListen_ResourceListChanged(t *testing.T) {
// session removes its entries from the server's three per-type subscription
// maps via Server.disconnect.
func TestSubscriptionsListen_DisconnectScrubsMaps(t *testing.T) {
enableNewProtocol(t)
events := make(chan subListenEvent, 16)
server := newSubListenServer()

View file

@ -23,10 +23,6 @@ func TestMultiRoundTrip_ManualRetry(t *testing.T) {
Reason string `json:"reason,omitempty"`
}
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
srv := NewServer(testImpl, nil)
@ -95,9 +91,6 @@ func TestMultiRoundTrip_ManualRetry(t *testing.T) {
}
func TestMultiRoundTrip_AutoRetry(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
tests := []struct {
name string
@ -220,9 +213,6 @@ func TestMultiRoundTrip_MaxRetries(t *testing.T) {
wantRetries: maxMultiRoundTripRetries,
},
}
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@ -322,7 +312,7 @@ func TestMultiRoundTrip_ServerMiddleware(t *testing.T) {
},
})
c.AddRoots(&Root{URI: "file:///workspace", Name: "workspace"})
cs, err := c.Connect(t.Context(), ct, &ClientSessionOptions{})
cs, err := c.Connect(t.Context(), ct, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() error = %v", err)
}
@ -340,9 +330,6 @@ func TestMultiRoundTrip_ServerMiddleware(t *testing.T) {
}
func TestMultiRoundTrip_GetPrompt_AutoRetry(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
@ -382,9 +369,6 @@ func TestMultiRoundTrip_GetPrompt_AutoRetry(t *testing.T) {
}
func TestMultiRoundTrip_GetPrompt_ManualRetry(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
@ -434,9 +418,6 @@ func TestMultiRoundTrip_GetPrompt_ManualRetry(t *testing.T) {
}
func TestMultiRoundTrip_ReadResource_AutoRetry(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
@ -475,9 +456,6 @@ func TestMultiRoundTrip_ReadResource_AutoRetry(t *testing.T) {
}
func TestMultiRoundTrip_ReadResource_ManualRetry(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()

View file

@ -856,7 +856,9 @@ 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)
}
@ -1335,7 +1337,17 @@ func (s *Server) Connect(ctx context.Context, t Transport, opts *ServerSessionOp
// 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.
//
// The write is guarded by ss.mu to establish a happens-before edge with
// the matching read in Server.discover, which runs on the jsonrpc2 read
// goroutine spawned inside connect(). The two are not concurrent in
// wall-clock terms (no incoming message is dispatched until the caller
// has fed the transport, which happens after Server.Connect returns),
// but without the lock the Go memory model gives the read goroutine no
// guarantee of seeing this write, and -race flags it.
ss.mu.Lock()
ss.supportedVersions = filterSupportedVersions(t)
ss.mu.Unlock()
// 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).
@ -1795,7 +1807,7 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
}
switch req.Method {
case methodInitialize, methodPing, notificationInitialized, methodSetLevel, methodSubscribe, methodUnsubscribe:
case methodInitialize, methodPing, notificationInitialized, notificationRootsListChanged, methodSetLevel, methodSubscribe, methodUnsubscribe:
if validatedMeta.usesNewProtocol {
ss.server.opts.Logger.Error("method removed in the new protocol", "method", req.Method)
return nil, &jsonrpc.Error{

View file

@ -284,7 +284,7 @@ func Example_mrtr() {
// uses it to fulfil any input request the tool handler returns.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
ElicitationHandler: func(_ context.Context, _ *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"name": "Ada"}}, nil
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"name": "MCP Go"}}, nil
},
})

View file

@ -1290,9 +1290,6 @@ func TestServerCapabilitiesOverWire(t *testing.T) {
// that opts into the new protocol via `_meta.protocolVersion` must be
// rejected with `Method not found` (-32601).
func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
tests := []struct {
name string
@ -1407,9 +1404,6 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) {
// `ping`) all return Method not found when the request opts into the new
// protocol via `_meta.protocolVersion`.
func TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
newProtoMeta := map[string]any{
"_meta": map[string]any{

View file

@ -35,7 +35,7 @@ const (
//
// It is the version that the client sends in the initialization request, and
// the default version used by the server.
latestProtocolVersion = protocolVersion20251125
latestProtocolVersion = protocolVersion20260728
protocolVersion20260728 = "2026-07-28"
protocolVersion20251125 = "2025-11-25"
protocolVersion20250618 = "2025-06-18"
@ -44,6 +44,7 @@ const (
)
var supportedProtocolVersions = []string{
protocolVersion20260728,
protocolVersion20251125,
protocolVersion20250618,
protocolVersion20250326,
@ -58,8 +59,11 @@ func negotiatedVersion(clientVersion string) string {
//
// This handles the case where a new spec version is released, and the SDK
// does not support it yet.
// Cap the supported versions at the legacy protocolVersion20251125, as this
// method is used by the initialize method which is deprecated in
// version protocolVersion20260728.
if !slices.Contains(supportedProtocolVersions, clientVersion) {
return latestProtocolVersion
return protocolVersion20251125
}
return clientVersion
}
@ -536,7 +540,7 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientInfo),
}
}
capabilities, ok := decodeMetaValue[*ClientCapabilities](meta, MetaKeyClientCapabilities)
capabilities, ok := decodeMetaValue[*clientCapabilitiesV2](meta, MetaKeyClientCapabilities)
if !ok {
return nil, &jsonrpc.Error{
Code: jsonrpc.CodeInvalidParams,
@ -546,7 +550,7 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
logLevel, _ := decodeMetaValue[LoggingLevel](meta, MetaKeyLogLevel)
return &validatedMeta{usesNewProtocol: true, initializeParams: &InitializeParams{
ProtocolVersion: protocolVersion,
Capabilities: capabilities,
Capabilities: capabilities.toV1(),
ClientInfo: clientInfo,
}, logLevel: logLevel}, nil
}
@ -659,8 +663,8 @@ func (r *ServerRequest[P]) ClientInfo() *Implementation {
// back to the session-level [InitializeParams].
func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities {
if m := getRequestMeta(r); m != nil {
if v, ok := decodeMetaValue[*ClientCapabilities](m, MetaKeyClientCapabilities); ok {
return v
if v, ok := decodeMetaValue[*clientCapabilitiesV2](m, MetaKeyClientCapabilities); ok {
return v.toV1()
}
}
if r.Session != nil {

View file

@ -160,7 +160,8 @@ var (
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
ProtocolVersion: latestProtocolVersion,
// Pin negotiated version to 2025-11-25
ProtocolVersion: protocolVersion20251125,
ServerInfo: &Implementation{Name: "testServer", Version: "v1.0.0"},
}
initResp = resp(1, initResult, nil)
@ -193,13 +194,13 @@ func TestStreamableClientTransportLifecycle(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
header: header{
"Content-Type": "text/event-stream",
},
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"DELETE", "123", "", ""}: {},
},
@ -210,7 +211,10 @@ func TestStreamableClientTransportLifecycle(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response uses
// hardcoded id=1, which only matches when initialize is the first
// request. Under 2026-07-28 the client probes server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -244,7 +248,7 @@ func TestStreamableClientRedundantDelete(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
status: http.StatusMethodNotAllowed,
@ -260,7 +264,10 @@ func TestStreamableClientRedundantDelete(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response uses
// hardcoded id=1, which only matches when initialize is the first
// request. Under 2026-07-28 the client probes server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -307,14 +314,14 @@ func TestStreamableClientGETHandling(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
header: header{
"Content-Type": test.contentType,
},
status: test.status,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"DELETE", "123", "", ""}: {optional: true},
},
@ -324,7 +331,9 @@ func TestStreamableClientGETHandling(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
session, err := client.Connect(ctx, transport, &ClientSessionOptions{
protocolVersion: protocolVersion20251125,
})
if err == nil {
defer session.Close()
}
@ -376,14 +385,14 @@ func TestStreamableClientStrictness(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: test.initializedStatus,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
header: header{
"Content-Type": "text/event-stream",
},
status: test.getStatus,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"POST", "123", methodListTools, ""}: {
header: header{
@ -401,7 +410,11 @@ func TestStreamableClientStrictness(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL, strict: test.strict}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response
// uses hardcoded id=1, which only matches when initialize is
// the first request. Under 2026-07-28 the client probes
// server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if (err != nil) != test.wantConnectError {
t.Errorf("client.Connect() returned error %v; want error: %t", err, test.wantConnectError)
}
@ -441,7 +454,7 @@ func TestStreamableClientUnresumableRequest(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
cs, err := client.Connect(ctx, transport, nil)
cs, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err == nil {
cs.Close()
t.Fatalf("Connect succeeded unexpectedly")
@ -515,7 +528,7 @@ func TestStreamableClientResumption_Cancelled(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
header: header{
@ -558,7 +571,11 @@ data: { "jsonrpc": "2.0", "method": "notifications/message", "params": { "level"
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
cs, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response
// uses hardcoded id=1, which only matches when initialize is
// the first request. Under 2026-07-28 the client probes
// server/discover first.
cs, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -658,7 +675,7 @@ func TestStreamableClientTransientErrors(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
status: http.StatusMethodNotAllowed,
@ -686,7 +703,11 @@ func TestStreamableClientTransientErrors(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response
// uses hardcoded id=1, which only matches when initialize is
// the first request. Under 2026-07-28 the client probes
// server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
@ -747,7 +768,7 @@ func TestStreamableClientRetryWithoutProgress(t *testing.T) {
},
{"POST", "test-session", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "test-session", "", ""}: {
// Disable standalone SSE stream to simplify the test.
@ -789,7 +810,10 @@ data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info"
MaxRetries: maxRetries,
}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response uses
// hardcoded id=1, which only matches when initialize is the first
// request. Under 2026-07-28 the client probes server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
@ -850,13 +874,13 @@ func TestStreamableClientDisableStandaloneSSE(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
getRequestKey: {
header: header{
"Content-Type": "text/event-stream",
},
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
optional: !test.expectGETRequest,
},
{"DELETE", "123", "", ""}: {
@ -873,7 +897,11 @@ func TestStreamableClientDisableStandaloneSSE(t *testing.T) {
DisableStandaloneSSE: test.disableStandaloneSSE,
}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response
// uses hardcoded id=1, which only matches when initialize is
// the first request. Under 2026-07-28 the client probes
// server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -967,7 +995,7 @@ func TestStreamableClientOAuth_AuthorizationHeader(t *testing.T) {
},
{"POST", "123", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "123", "", ""}: {
header: header{
@ -991,7 +1019,10 @@ func TestStreamableClientOAuth_AuthorizationHeader(t *testing.T) {
OAuthHandler: oauthHandler,
}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
// Pin to 2025-11-25: the fixture's canned initialize response uses
// hardcoded id=1, which only matches when initialize is the first
// request. Under 2026-07-28 the client probes server/discover first.
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -1253,13 +1284,6 @@ var discoverResult = &DiscoverResult{
func TestStreamableClientConnect_DiscoverSuccess(t *testing.T) {
ctx := context.Background()
// Temporarily enable 2026-07-28 support in the SDK for this test
oldSupported := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, supportedProtocolVersions...)
t.Cleanup(func() {
supportedProtocolVersions = oldSupported
})
var (
gotDiscoverMu sync.Mutex
gotDiscover *jsonrpc.Request
@ -1378,11 +1402,11 @@ func TestStreamableClientConnect_DiscoverMethodNotFound(t *testing.T) {
},
{"POST", "fallback", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "fallback", "", ""}: {
header: header{"Content-Type": "text/event-stream"},
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
optional: true,
},
{"DELETE", "fallback", "", ""}: {optional: true},
@ -1394,14 +1418,16 @@ func TestStreamableClientConnect_DiscoverMethodNotFound(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("Connect: %v", err)
}
defer session.Close()
if got := session.InitializeResult().ProtocolVersion; got != latestProtocolVersion {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, latestProtocolVersion)
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if got := session.InitializeResult().ProtocolVersion; got != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, protocolVersion20251125)
}
}
@ -1443,11 +1469,11 @@ func TestStreamableClientConnect_DiscoverUnsupportedVersion(t *testing.T) {
},
{"POST", "fallback", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"GET", "fallback", "", ""}: {
header: header{"Content-Type": "text/event-stream"},
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
optional: true,
},
{"DELETE", "fallback", "", ""}: {optional: true},
@ -1459,14 +1485,16 @@ func TestStreamableClientConnect_DiscoverUnsupportedVersion(t *testing.T) {
transport := &StreamableClientTransport{Endpoint: httpServer.URL}
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, transport, nil)
session, err := client.Connect(ctx, transport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("Connect: %v", err)
}
defer session.Close()
if got := session.InitializeResult().ProtocolVersion; got != latestProtocolVersion {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, latestProtocolVersion)
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if got := session.InitializeResult().ProtocolVersion; got != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, protocolVersion20251125)
}
}
@ -1503,7 +1531,7 @@ func TestStreamableClientConnect_DiscoverMethodNotFoundVPre(t *testing.T) {
},
{"POST", "fallback", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"DELETE", "fallback", "", ""}: {optional: true},
},
@ -1523,8 +1551,10 @@ func TestStreamableClientConnect_DiscoverMethodNotFoundVPre(t *testing.T) {
}
defer session.Close()
if got := session.InitializeResult().ProtocolVersion; got != latestProtocolVersion {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, latestProtocolVersion)
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if got := session.InitializeResult().ProtocolVersion; got != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, protocolVersion20251125)
}
}
@ -1558,7 +1588,7 @@ func TestStreamableClientConnect_DiscoverUnsupportedVersionVPre(t *testing.T) {
},
{"POST", "fallback", notificationInitialized, ""}: {
status: http.StatusAccepted,
wantProtocolVersion: latestProtocolVersion,
wantProtocolVersion: protocolVersion20251125,
},
{"DELETE", "fallback", "", ""}: {optional: true},
},
@ -1578,8 +1608,10 @@ func TestStreamableClientConnect_DiscoverUnsupportedVersionVPre(t *testing.T) {
}
defer session.Close()
if got := session.InitializeResult().ProtocolVersion; got != latestProtocolVersion {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, latestProtocolVersion)
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if got := session.InitializeResult().ProtocolVersion; got != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (initialize fallback)", got, protocolVersion20251125)
}
}
@ -1590,12 +1622,6 @@ func TestStreamableClientConnect_DiscoverUnsupportedVersionVPre(t *testing.T) {
func TestStreamableClientConnect_DiscoverUnsupportedVersionNegotiation(t *testing.T) {
ctx := context.Background()
oldSupported := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, supportedProtocolVersions...)
t.Cleanup(func() {
supportedProtocolVersions = oldSupported
})
const unsupportedClientVersion = "2099-12-31"
var discoverCalls atomic.Int32

View file

@ -32,7 +32,7 @@ func ExampleStreamableHTTPHandler() {
defer httpServer.Close()
// The SDK is currently permissive of some missing keys in "params".
resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {"protocolVersion":"2025-11-25"}}`, httpServer.URL)
fmt.Println(resp)
// Output:
// {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-11-25","serverInfo":{"name":"server","version":"v0.1.0"}}}

View file

@ -182,7 +182,11 @@ func TestStreamableTransports(t *testing.T) {
if sid == "" {
t.Fatalf("empty session ID")
}
if g, w := session.mcpConn.(*streamableClientConn).initializedResult.ProtocolVersion, latestProtocolVersion; g != w {
// Stateful HTTP servers reject 2026-07-28 (which requires
// StreamableHTTPOptions.Stateless = true), so the client falls
// back to the legacy initialize handshake which explicitly
// requests 2025-11-25.
if g, w := session.mcpConn.(*streamableClientConn).initializedResult.ProtocolVersion, protocolVersion20251125; g != w {
t.Fatalf("got protocol version %q, want %q", g, w)
}
@ -200,7 +204,10 @@ func TestStreamableTransports(t *testing.T) {
if g := session.ID(); g != sid {
t.Errorf("session ID: got %q, want %q", g, sid)
}
if g, w := lastHeader.Get(protocolVersionHeader), latestProtocolVersion; g != w {
// Stateful HTTP requires the legacy protocol (see the earlier
// initializeResult assertion); the client sends the matching
// MCP-Protocol-Version header.
if g, w := lastHeader.Get(protocolVersionHeader), protocolVersion20251125; g != w {
t.Errorf("got protocol version header %q, want %q", g, w)
}
want := &CallToolResult{
@ -332,10 +339,15 @@ func TestStreamableServerShutdown(t *testing.T) {
opts.KeepAlive = 50 * time.Millisecond
}
client := NewClient(testImpl, &opts)
// Pin to 2025-11-25: this test relies on the legacy hanging GET
// (stateful case) or Ping-based keepalive (stateless case) for
// the client to notice the server shutdown. Both mechanisms are
// removed in 2026-07-28 (SEP-2575), so the new protocol would
// leave the client waiting forever.
clientSession, err := client.Connect(ctx, &StreamableClientTransport{
Endpoint: httpServer.URL,
MaxRetries: -1, // avoid slow tests during exponential retries
}, nil)
}, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
@ -680,7 +692,12 @@ func TestServerTransportCleanup(t *testing.T) {
}
}
client.AddReceivingMiddleware(pingMiddleware)
clientSession, err := client.Connect(ctx, &StreamableClientTransport{Endpoint: httpServer.URL}, nil)
// Pin to 2025-11-25: this test counts server-side sessions, but
// the 2026-07-28 client probes server/discover first against a
// stateful HTTP server (which rejects the new protocol), opening
// an extra HTTP connection and therefore an extra session. Pinning
// to 2025-11-25 skips the discover probe.
clientSession, err := client.Connect(ctx, &StreamableClientTransport{Endpoint: httpServer.URL}, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -1636,7 +1653,7 @@ func TestStreamableStateless(t *testing.T) {
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
ProtocolVersion: latestProtocolVersion,
ProtocolVersion: protocolVersion20251125,
ServerInfo: &Implementation{Name: "test", Version: "v1.0.0"},
}, nil)
// This version of sayHi expects
@ -1904,9 +1921,6 @@ func TestStreamableGET(t *testing.T) {
// as specified in SEP-2243.
func TestStreamableMcpHeaderValidation(t *testing.T) {
// Temporarily register the future version so the handler accepts it.
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), minVersionForStandardHeaders)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
server.AddTool(
@ -2055,9 +2069,6 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
// validation errors return a JSON-RPC error with code -32020 and
// Content-Type application/json, per SEP-2243.
func TestStreamableMcpHeaderValidationErrorFormat(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), minVersionForStandardHeaders)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
server.AddTool(
@ -2204,9 +2215,6 @@ func TestStreamableMcpHeaderVersionGating(t *testing.T) {
// TestStreamableParamHeadersClientSetsHeaders verifies that the client sets
// Mcp-Param-* headers on tool calls when the tool has x-mcp-header annotations.
func TestStreamableParamHeadersClientSetsHeaders(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), minVersionForStandardHeaders)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
server.AddTool(
@ -2306,9 +2314,6 @@ func TestStreamableParamHeadersClientSetsHeaders(t *testing.T) {
// tools with invalid x-mcp-header annotations at registration time, and
// that valid tools are returned by ListTools.
func TestStreamableFilterValidToolsIntegration(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), minVersionForStandardHeaders)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
noop := func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) {
@ -2624,8 +2629,12 @@ func TestStreamableSessionTimeout(t *testing.T) {
defer httpServer.Close()
// Connect a client to create a session.
// Pin to 2025-11-25: under 2026-07-28 the client probes server/discover
// first against a stateful HTTP server (which rejects the new
// protocol), opening an extra HTTP connection and therefore an extra
// session. Pinning to 2025-11-25 skips the discover probe.
client := NewClient(testImpl, nil)
session, err := client.Connect(ctx, &StreamableClientTransport{Endpoint: httpServer.URL}, nil)
session, err := client.Connect(ctx, &StreamableClientTransport{Endpoint: httpServer.URL}, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("client.Connect() failed: %v", err)
}
@ -2915,7 +2924,11 @@ func Test_ExportErrSessionMissing(t *testing.T) {
Endpoint: ts.URL,
}
client := NewClient(impl, nil)
session, err := client.Connect(ctx, clientTransport, nil)
// Pin to 2025-11-25: under 2026-07-28 the client probes server/discover
// first against a stateful HTTP server (which rejects the new
// protocol), opening an extra HTTP connection and therefore an extra
// session. Pinning to 2025-11-25 skips the discover probe.
session, err := client.Connect(ctx, clientTransport, &ClientSessionOptions{protocolVersion: protocolVersion20251125})
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
@ -3355,9 +3368,6 @@ func TestStreamableStateless_NewProtocolSession_NoFakeInit(t *testing.T) {
// SEP-2575: the MCP-Protocol-Version header is mandatory for new-protocol
// requests and must be a supported version. The 2026-07-28 version is
// not yet in the global list, so register it for the duration of the test.
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
capture := &statelessHandlerCapture{}
mcpServer := NewServer(testImpl, nil)
@ -3432,9 +3442,6 @@ func TestStreamableStateless_NewProtocolSession_NoFakeInit(t *testing.T) {
func TestStreamableStateful_RejectsNewProtocol(t *testing.T) {
// Make 2026-07-28 a "known" version so that the request reaches servePOST
// (otherwise the early header validation at ServeHTTP rejects it).
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(testImpl, nil)
AddTool(server, &Tool{Name: "noop"},
@ -3497,9 +3504,6 @@ func TestStreamableStateful_RejectsNewProtocol(t *testing.T) {
// (the rejection in TestStreamableStateful_RejectsNewProtocol must not
// fire on Stateless: true).
func TestStreamableStateless_AcceptsNewProtocol(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(testImpl, nil)
AddTool(server, &Tool{Name: "noop"},
@ -3536,15 +3540,22 @@ func TestStreamableStateless_AcceptsNewProtocol(t *testing.T) {
}
// TestStreamableClientUnsupportedVersionFallback exercises the full
// SEP-2575 fallback. The client requests protocolVersion20260728, which the server does
// not advertise in supportedProtocolVersions. The server therefore rejects
// the server/discover POST at the transport-level header validation with a
// plain HTTP 400 ("Bad Request: Unsupported protocol version ..."). The
// streamable client must recognize this body, keep the connection alive, and
// successfully complete the legacy initialize handshake.
//
// TODO: once 20260630 is part of supportedProtocolVersion on server side, modify the list in the test to keep it consistent.
// SEP-2575 fallback. The client requests protocolVersion20260728, which the
// server is configured to NOT advertise in supportedProtocolVersions for the
// duration of this test. The server therefore rejects the server/discover
// POST at the transport-level header validation with a plain HTTP 400 ("Bad
// Request: Unsupported protocol version ..."). The streamable client must
// recognize this body, keep the connection alive, and successfully complete
// the legacy initialize handshake.
func TestStreamableClientUnsupportedVersionFallback(t *testing.T) {
// Remove protocolVersion20260728 from the server-side supported list so
// the fallback path is exercised.
orig := supportedProtocolVersions
supportedProtocolVersions = slices.DeleteFunc(slices.Clone(orig), func(v string) bool {
return v == protocolVersion20260728
})
t.Cleanup(func() { supportedProtocolVersions = orig })
ctx := context.Background()
server := NewServer(testImpl, nil)
@ -3568,9 +3579,11 @@ func TestStreamableClientUnsupportedVersionFallback(t *testing.T) {
if ir == nil {
t.Fatal("InitializeResult is nil; expected legacy initialize to populate it")
}
if ir.ProtocolVersion != latestProtocolVersion {
// The fallback initialize explicitly requests protocolVersion20251125
// (see client.go), so the server negotiates that version.
if ir.ProtocolVersion != protocolVersion20251125 {
t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (legacy fallback)",
ir.ProtocolVersion, latestProtocolVersion)
ir.ProtocolVersion, protocolVersion20251125)
}
// Verify the session is fully usable after the fallback by issuing a
@ -3587,9 +3600,6 @@ func TestStreamableClientUnsupportedVersionFallback(t *testing.T) {
// the server with the new protocol version to learn which versions are
// supported.
func TestStreamableStateful_AcceptsDiscover(t *testing.T) {
orig := supportedProtocolVersions
supportedProtocolVersions = append(slices.Clone(orig), protocolVersion20260728)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(testImpl, nil)
handler := NewStreamableHTTPHandler(func(*http.Request) *Server { return server }, nil)
@ -3666,10 +3676,6 @@ func TestStreamableStateful_AcceptsDiscover(t *testing.T) {
// SEP-2575 over the streamable HTTP transport.
func TestStreamableHTTP_E2E_DiscoverSuccess(t *testing.T) {
ctx := context.Background()
orig := supportedProtocolVersions
supportedProtocolVersions = append([]string{protocolVersion20260728}, slices.Clone(orig)...)
t.Cleanup(func() { supportedProtocolVersions = orig })
server := NewServer(&Implementation{Name: "e2e-server", Version: "v1"}, nil)
// Register a simple tool so we can prove the session is usable end-to-end.
AddTool(server, &Tool{Name: "echo", Description: "echoes its input"},

View file

@ -0,0 +1,39 @@
The client sends a discover request
carrying the required per-request _meta triple (protocolVersion, clientInfo,
clientCapabilities), and the server replies with the protocol versions it
supports, its capabilities, and its identity.
-- client --
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
-- server --
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"supportedVersions": [
"2026-07-28",
"2025-11-25",
"2025-06-18",
"2025-03-26",
"2024-11-05"
],
"capabilities": {
"logging": {}
},
"serverInfo": {
"name": "testServer",
"version": "v1.0.0"
}
}
}

View file

@ -0,0 +1,85 @@
Check the multi-round-trip request pattern under protocol version
2026-07-28. The server's tool handler returns an
InputRequiredResult on the first call, requesting elicitation input from the
client and providing an opaque RequestState token. The client fulfills the
input request and retries the tool call with the responses and the echoed
RequestState; the handler then returns the final result.
-- tools --
confirmThenGreet
-- client --
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} }
},
"name": "confirmThenGreet",
"arguments": {}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} }
},
"name": "confirmThenGreet",
"arguments": {},
"requestState": "step=1",
"inputResponses": {
"who": {
"method": "elicitation/create",
"result": { "action": "accept", "content": { "name": "MCP Go" } }
}
}
}
}
-- server --
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": null,
"requestState": "step=1",
"resultType": "input_required",
"inputRequests": {
"who": {
"method": "elicitation/create",
"params": {
"mode": "",
"message": "What is your name?",
"requestedSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
}
}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "hello MCP Go"
}
],
"resultType": "complete"
}
}

View file

@ -42,9 +42,8 @@ func ExampleLoggingTransport() {
}
// Output:
// read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-11-25","serverInfo":{"name":"server","version":"v0.0.1"}}}
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-11-25","capabilities":{"roots":{"listChanged":true}}}}
// write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
// read: {"jsonrpc":"2.0","id":1,"result":{"supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"}}}
// write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}
}
// !-loggingtransport