From 7f4aa4a0cec84f363748f5e5053ef79996e19968 Mon Sep 17 00:00:00 2001 From: Guglielmo Colombo Date: Wed, 24 Jun 2026 12:02:56 +0200 Subject: [PATCH] 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. --- mcp/capabilities_test.go | 8 +- mcp/client.go | 4 +- mcp/client_test.go | 62 +++----- mcp/conformance_test.go | 30 ++++ mcp/mcp_example_test.go | 16 +-- mcp/mcp_test.go | 32 ++--- mcp/mrtr_test.go | 24 +--- mcp/server.go | 14 +- mcp/server_example_test.go | 2 +- mcp/server_test.go | 6 - mcp/shared.go | 16 ++- mcp/streamable_client_test.go | 132 +++++++++++------- mcp/streamable_example_test.go | 2 +- mcp/streamable_test.go | 96 +++++++------ .../conformance/server/discover.txtar | 39 ++++++ mcp/testdata/conformance/server/mrtr.txtar | 85 +++++++++++ mcp/transport_example_test.go | 5 +- 17 files changed, 355 insertions(+), 218 deletions(-) create mode 100644 mcp/testdata/conformance/server/discover.txtar create mode 100644 mcp/testdata/conformance/server/mrtr.txtar diff --git a/mcp/capabilities_test.go b/mcp/capabilities_test.go index b845616..86c9970 100644 --- a/mcp/capabilities_test.go +++ b/mcp/capabilities_test.go @@ -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, }, { diff --git a/mcp/client.go b/mcp/client.go index 8ccbdfa..552b3d6 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -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 diff --git a/mcp/client_test.go b/mcp/client_test.go index 815f04c..fe1d3a2 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -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" diff --git a/mcp/conformance_test.go b/mcp/conformance_test.go index 7f1a744..d7f4f74 100644 --- a/mcp/conformance_test.go +++ b/mcp/conformance_test.go @@ -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) } diff --git a/mcp/mcp_example_test.go b/mcp/mcp_example_test.go index 25f39fb..120789c 100644 --- a/mcp/mcp_example_test.go +++ b/mcp/mcp_example_test.go @@ -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 diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index de12cbd..82a919d 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -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() diff --git a/mcp/mrtr_test.go b/mcp/mrtr_test.go index a5b286f..238bc09 100644 --- a/mcp/mrtr_test.go +++ b/mcp/mrtr_test.go @@ -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() diff --git a/mcp/server.go b/mcp/server.go index bd0588b..4cc9ae4 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -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{ diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index 58d4eee..5e95585 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -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 }, }) diff --git a/mcp/server_test.go b/mcp/server_test.go index f6938d1..fc9db1f 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -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{ diff --git a/mcp/shared.go b/mcp/shared.go index ac07ee4..2cf37b1 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -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 { diff --git a/mcp/streamable_client_test.go b/mcp/streamable_client_test.go index 24179d1..59bf1bd 100644 --- a/mcp/streamable_client_test.go +++ b/mcp/streamable_client_test.go @@ -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 diff --git a/mcp/streamable_example_test.go b/mcp/streamable_example_test.go index 7ff1612..66834f2 100644 --- a/mcp/streamable_example_test.go +++ b/mcp/streamable_example_test.go @@ -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"}}} diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index 25d3625..dff5e1f 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -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"}, diff --git a/mcp/testdata/conformance/server/discover.txtar b/mcp/testdata/conformance/server/discover.txtar new file mode 100644 index 0000000..a2fbde7 --- /dev/null +++ b/mcp/testdata/conformance/server/discover.txtar @@ -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" + } + } +} diff --git a/mcp/testdata/conformance/server/mrtr.txtar b/mcp/testdata/conformance/server/mrtr.txtar new file mode 100644 index 0000000..e0dc18c --- /dev/null +++ b/mcp/testdata/conformance/server/mrtr.txtar @@ -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" + } +} diff --git a/mcp/transport_example_test.go b/mcp/transport_example_test.go index fa05fc0..2a9d745 100644 --- a/mcp/transport_example_test.go +++ b/mcp/transport_example_test.go @@ -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