@@ -3236,7 +3236,7 @@ func newProtocolBody(t *testing.T, toolName string, args any) []byte {
32363236 return body
32373237}
32383238
3239- func TestEphemeralConnectOpts_NewProtocol (t * testing.T ) {
3239+ func TestEphemeralConnectOpts (t * testing.T ) {
32403240 mkReq := func (body []byte ) * http.Request {
32413241 r := httptest .NewRequest (http .MethodPost , "/" , bytes .NewReader (body ))
32423242 r .Header .Set ("Content-Type" , "application/json" )
@@ -3262,7 +3262,7 @@ func TestEphemeralConnectOpts_NewProtocol(t *testing.T) {
32623262 }
32633263 })
32643264
3265- t .Run ("old-protocol request: synthetic state preserved " , func (t * testing.T ) {
3265+ t .Run ("old-protocol request: synthetic state populated " , func (t * testing.T ) {
32663266 body , err := json .Marshal (map [string ]any {
32673267 "jsonrpc" : "2.0" ,
32683268 "id" : 1 ,
@@ -3310,6 +3310,62 @@ func TestEphemeralConnectOpts_NewProtocol(t *testing.T) {
33103310 })
33113311}
33123312
3313+ // TestServePOST_NewProtocolHeaderCrossCheck verifies that the SEP-2575
3314+ // header/body cross-check runs inside streamableServerConn.servePOST, which
3315+ // is the single chokepoint reached by every POST regardless of stateful or
3316+ // stateless mode.
3317+ func TestServePOST_NewProtocolHeaderCrossCheck (t * testing.T ) {
3318+ mcpServer := NewServer (testImpl , nil )
3319+ AddTool (mcpServer , & Tool {Name : "noop" },
3320+ func (ctx context.Context , req * CallToolRequest , args struct {}) (* CallToolResult , any , error ) {
3321+ return & CallToolResult {Content : []Content {& TextContent {Text : "ok" }}}, nil , nil
3322+ })
3323+ handler := NewStreamableHTTPHandler (
3324+ func (* http.Request ) * Server { return mcpServer },
3325+ & StreamableHTTPOptions {Stateless : true },
3326+ )
3327+ httpServer := httptest .NewServer (handler )
3328+ defer httpServer .Close ()
3329+
3330+ mkReq := func (headerVersion string ) * http.Request {
3331+ body := newProtocolBody (t , "noop" , struct {}{})
3332+ req , err := http .NewRequest (http .MethodPost , httpServer .URL , bytes .NewReader (body ))
3333+ if err != nil {
3334+ t .Fatal (err )
3335+ }
3336+ req .Header .Set ("Content-Type" , "application/json" )
3337+ req .Header .Set ("Accept" , "application/json, text/event-stream" )
3338+ if headerVersion != "" {
3339+ req .Header .Set (protocolVersionHeader , headerVersion )
3340+ }
3341+ return req
3342+ }
3343+
3344+ t .Run ("mismatched header: 400" , func (t * testing.T ) {
3345+ resp , err := http .DefaultClient .Do (mkReq ("2025-06-18" ))
3346+ if err != nil {
3347+ t .Fatal (err )
3348+ }
3349+ defer resp .Body .Close ()
3350+ if resp .StatusCode != http .StatusBadRequest {
3351+ body , _ := io .ReadAll (resp .Body )
3352+ t .Fatalf ("status = %d, want 400; body = %s" , resp .StatusCode , body )
3353+ }
3354+ })
3355+
3356+ t .Run ("missing header: 400" , func (t * testing.T ) {
3357+ resp , err := http .DefaultClient .Do (mkReq ("" ))
3358+ if err != nil {
3359+ t .Fatal (err )
3360+ }
3361+ defer resp .Body .Close ()
3362+ if resp .StatusCode != http .StatusBadRequest {
3363+ body , _ := io .ReadAll (resp .Body )
3364+ t .Fatalf ("status = %d, want 400; body = %s" , resp .StatusCode , body )
3365+ }
3366+ })
3367+ }
3368+
33133369// statelessHandlerCapture builds a stateless server with a single tool whose
33143370// handler captures everything we want to assert about the per-request view of
33153371// the session and the new-protocol accessors.
@@ -3322,6 +3378,13 @@ type statelessHandlerCapture struct {
33223378}
33233379
33243380func TestStreamableStateless_NewProtocolSession_NoFakeInit (t * testing.T ) {
3381+ // SEP-2575: the MCP-Protocol-Version header is mandatory for new-protocol
3382+ // requests and must be a supported version. The 2026-06-30 version is
3383+ // not yet in the global list, so register it for the duration of the test.
3384+ orig := supportedProtocolVersions
3385+ supportedProtocolVersions = append (slices .Clone (orig ), protocolVersion20260630 )
3386+ t .Cleanup (func () { supportedProtocolVersions = orig })
3387+
33253388 capture := & statelessHandlerCapture {}
33263389 mcpServer := NewServer (testImpl , nil )
33273390 AddTool (mcpServer , & Tool {Name : "capture" , Description : "captures request info" },
@@ -3349,6 +3412,7 @@ func TestStreamableStateless_NewProtocolSession_NoFakeInit(t *testing.T) {
33493412 }
33503413 httpReq .Header .Set ("Content-Type" , "application/json" )
33513414 httpReq .Header .Set ("Accept" , "application/json, text/event-stream" )
3415+ httpReq .Header .Set (protocolVersionHeader , protocolVersion20260630 )
33523416
33533417 resp , err := http .DefaultClient .Do (httpReq )
33543418 if err != nil {
@@ -3375,3 +3439,113 @@ func TestStreamableStateless_NewProtocolSession_NoFakeInit(t *testing.T) {
33753439 t .Errorf ("req.ClientCapabilities() = %+v, want non-nil Sampling" , capture .reqClientCapabilities )
33763440 }
33773441}
3442+
3443+ // TestStreamableStateful_RejectsNewProtocol verifies that a stateful HTTP
3444+ // server rejects requests carrying _meta.protocolVersion (i.e. >= 2026-06-30
3445+ // requests) with HTTP 400. The new protocol is
3446+ // supported on HTTP only when StreamableHTTPOptions.Stateless=true.
3447+ func TestStreamableStateful_RejectsNewProtocol (t * testing.T ) {
3448+ // Make 2026-06-30 a "known" version so that the request reaches servePOST
3449+ // (otherwise the early header validation at ServeHTTP rejects it).
3450+ orig := supportedProtocolVersions
3451+ supportedProtocolVersions = append (slices .Clone (orig ), protocolVersion20260630 )
3452+ t .Cleanup (func () { supportedProtocolVersions = orig })
3453+
3454+ server := NewServer (testImpl , nil )
3455+ AddTool (server , & Tool {Name : "noop" },
3456+ func (ctx context.Context , req * CallToolRequest , args struct {}) (* CallToolResult , any , error ) {
3457+ return & CallToolResult {Content : []Content {& TextContent {Text : "ok" }}}, nil , nil
3458+ })
3459+ handler := NewStreamableHTTPHandler (func (* http.Request ) * Server { return server }, nil )
3460+ httpServer := httptest .NewServer (handler )
3461+ defer httpServer .Close ()
3462+
3463+ // Initialize a legacy session first.
3464+ initBody := strings .NewReader (`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}` )
3465+ initReq , err := http .NewRequest (http .MethodPost , httpServer .URL , initBody )
3466+ if err != nil {
3467+ t .Fatal (err )
3468+ }
3469+ initReq .Header .Set ("Content-Type" , "application/json" )
3470+ initReq .Header .Set ("Accept" , "application/json, text/event-stream" )
3471+ initResp , err := http .DefaultClient .Do (initReq )
3472+ if err != nil {
3473+ t .Fatal (err )
3474+ }
3475+ io .Copy (io .Discard , initResp .Body )
3476+ initResp .Body .Close ()
3477+ sessionID := initResp .Header .Get (sessionIDHeader )
3478+ if sessionID == "" {
3479+ t .Fatalf ("initialize response missing %s header" , sessionIDHeader )
3480+ }
3481+
3482+ // Drive the existing session with a new-protocol request whose header and
3483+ // body agree. The cross-check passes; the stateful-rejection check fires.
3484+ body := newProtocolBody (t , "noop" , struct {}{})
3485+ req , err := http .NewRequest (http .MethodPost , httpServer .URL , bytes .NewReader (body ))
3486+ if err != nil {
3487+ t .Fatal (err )
3488+ }
3489+ req .Header .Set ("Content-Type" , "application/json" )
3490+ req .Header .Set ("Accept" , "application/json, text/event-stream" )
3491+ req .Header .Set (sessionIDHeader , sessionID )
3492+ req .Header .Set (protocolVersionHeader , protocolVersion20260630 )
3493+ req .Header .Set (methodHeader , "tools/call" )
3494+ req .Header .Set (nameHeader , "noop" )
3495+
3496+ resp , err := http .DefaultClient .Do (req )
3497+ if err != nil {
3498+ t .Fatal (err )
3499+ }
3500+ defer resp .Body .Close ()
3501+ respBody , _ := io .ReadAll (resp .Body )
3502+ if resp .StatusCode != http .StatusBadRequest {
3503+ t .Fatalf ("status = %d, want 400; body = %s" , resp .StatusCode , respBody )
3504+ }
3505+ if ! strings .Contains (string (respBody ), "stateless" ) {
3506+ t .Errorf ("body = %q, want a message mentioning 'stateless'" , respBody )
3507+ }
3508+ }
3509+
3510+ // TestStreamableStateless_AcceptsNewProtocol is the positive control:
3511+ // confirms that a stateless server still accepts new-protocol requests
3512+ // (the rejection in TestStreamableStateful_RejectsNewProtocol must not
3513+ // fire on Stateless: true).
3514+ func TestStreamableStateless_AcceptsNewProtocol (t * testing.T ) {
3515+ orig := supportedProtocolVersions
3516+ supportedProtocolVersions = append (slices .Clone (orig ), protocolVersion20260630 )
3517+ t .Cleanup (func () { supportedProtocolVersions = orig })
3518+
3519+ server := NewServer (testImpl , nil )
3520+ AddTool (server , & Tool {Name : "noop" },
3521+ func (ctx context.Context , req * CallToolRequest , args struct {}) (* CallToolResult , any , error ) {
3522+ return & CallToolResult {Content : []Content {& TextContent {Text : "ok" }}}, nil , nil
3523+ })
3524+ handler := NewStreamableHTTPHandler (
3525+ func (* http.Request ) * Server { return server },
3526+ & StreamableHTTPOptions {Stateless : true },
3527+ )
3528+ httpServer := httptest .NewServer (handler )
3529+ defer httpServer .Close ()
3530+
3531+ body := newProtocolBody (t , "noop" , struct {}{})
3532+ req , err := http .NewRequest (http .MethodPost , httpServer .URL , bytes .NewReader (body ))
3533+ if err != nil {
3534+ t .Fatal (err )
3535+ }
3536+ req .Header .Set ("Content-Type" , "application/json" )
3537+ req .Header .Set ("Accept" , "application/json, text/event-stream" )
3538+ req .Header .Set (protocolVersionHeader , protocolVersion20260630 )
3539+ req .Header .Set (methodHeader , "tools/call" )
3540+ req .Header .Set (nameHeader , "noop" )
3541+
3542+ resp , err := http .DefaultClient .Do (req )
3543+ if err != nil {
3544+ t .Fatal (err )
3545+ }
3546+ defer resp .Body .Close ()
3547+ if resp .StatusCode != http .StatusOK {
3548+ respBody , _ := io .ReadAll (resp .Body )
3549+ t .Fatalf ("status = %d, want 200; body = %s" , resp .StatusCode , respBody )
3550+ }
3551+ }
0 commit comments