From 1a11d04e948bad258c65ed302a934bb53a7d8dd5 Mon Sep 17 00:00:00 2001 From: Alexey Zilber Date: Sun, 11 Oct 2026 21:54:35 +0800 Subject: [PATCH 1/4] mcp: fail Ping fast on 2026-07-28 sessions Follow-up to #1249, #1174, and #1130: ping was removed from the 2026-07-28 protocol schema, so a ping sent on a session negotiated at 2026-07-28 can never succeed. Today the client sends the removed method anyway and surfaces the server's rejection - either MethodNotFound at dispatch, or -32020 CodeHeaderMismatch at transports that validate header/meta consistency for a meta-less request (observed against mcp.svelte.dev). Ping now returns a descriptive local error on 2026-07-28 sessions without a round trip, pointing at the capability list requests that do verify liveness. No working behavior is lost: every such ping fails today. The cmd test's readiness check now relies on the connect itself, since ping is unavailable on those sessions. Verified by TestClientPingRemovedOnNewProtocol, which simulates a 2026-07-28 session and asserts no ping request is sent. Signed-off-by: Alexey Zilber --- mcp/client.go | 9 +++++++++ mcp/client_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ mcp/cmd_test.go | 8 +++----- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index 44be0ef6..ec88476e 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -1262,7 +1262,16 @@ func newClientRequest[P Params](cs *ClientSession, params P) *ClientRequest[P] { } // Ping makes an MCP "ping" request to the server. +// +// On 2026-07-28 sessions Ping returns an error without a round trip: the +// 2026-07-28 protocol removed the ping method, so servers answer it with +// MethodNotFound (or reject the request at the transport for missing +// header/meta consistency). Verify liveness with a capability list request +// instead (tools/list, resources/list, prompts/list). func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { + if cs.usesNewProtocol() { + return fmt.Errorf("mcp: ping is not supported on %s sessions (the method was removed); verify liveness with tools/list, resources/list, or prompts/list", protocolVersion20260728) + } _, err := handleSend[*emptyResult](ctx, methodPing, newClientRequest(cs, orZero[Params](params))) return err } diff --git a/mcp/client_test.go b/mcp/client_test.go index afe9ecc6..02bb6b3b 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "log/slog" + "strings" "sync/atomic" "testing" @@ -764,6 +765,55 @@ func TestClientConnectDiscover(t *testing.T) { } } +// TestClientPingRemovedOnNewProtocol verifies that Ping on a 2026-07-28 +// session returns a descriptive local error instead of sending the ping +// method, which the 2026-07-28 protocol removed. The session state is set +// directly (in-package) because the SDK's fallback initialize caps the +// negotiated version at 2025-11-25; only the discovery path produces a +// 2026-07-28 session. +func TestClientPingRemovedOnNewProtocol(t *testing.T) { + ctx := context.Background() + var gotPing bool + + s := NewServer(testImpl, nil) + ct, st := NewInMemoryTransports() + ss, err := s.Connect(ctx, st, nil) + if err != nil { + t.Fatalf("server Connect: %v", err) + } + defer ss.Close() + + c := NewClient(testImpl, nil) + // Registered before Connect: the session caches its sending handler at + // connect time. The middleware asserts that no ping request is sent. + c.AddSendingMiddleware(func(next MethodHandler) MethodHandler { + return func(ctx context.Context, method string, req Request) (Result, error) { + if method == methodPing { + gotPing = true + } + return next(ctx, method, req) + } + }) + cs, err := c.Connect(ctx, ct, nil) + if err != nil { + t.Fatalf("client Connect: %v", err) + } + defer cs.Close() + + cs.state.InitializeResult.ProtocolVersion = protocolVersion20260728 + + err = cs.Ping(ctx, nil) + if err == nil { + t.Fatal("Ping on a 2026-07-28 session = nil error, want a removed-method error") + } + if !strings.Contains(err.Error(), "ping is not supported") { + t.Errorf("Ping error %q does not explain the removal", err.Error()) + } + if gotPing { + t.Error("ping request was sent despite the removed-method guard") + } +} + // TestClientConnectDiscover_RequestContents verifies that the server/discover // request sent by Client.Connect carries the SEP-2575 per-request _meta triple: // protocolVersion, clientInfo, and clientCapabilities. diff --git a/mcp/cmd_test.go b/mcp/cmd_test.go index 632147b2..be021c8e 100644 --- a/mcp/cmd_test.go +++ b/mcp/cmd_test.go @@ -100,7 +100,9 @@ func TestServerRunContextCancel(t *testing.T) { onServerExit <- server.Run(ctx, serverTransport) }() - // send a ping to the server to ensure it's running + // Connect to the server to ensure it's running: a successful connect + // completes a full handshake (or a discovery round trip on + // 2026-07-28 sessions, where the ping method is removed). client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) session, err := client.Connect(ctx, clientTransport, nil) if err != nil { @@ -108,10 +110,6 @@ func TestServerRunContextCancel(t *testing.T) { } t.Cleanup(func() { session.Close() }) - if err := session.Ping(context.Background(), nil); err != nil { - t.Fatal(err) - } - // cancel the context to stop the server cancel() From aa86f7abd1a1633dbbaa5e10bcc5421414b93bcd Mon Sep 17 00:00:00 2001 From: Alexey Zilber Date: Tue, 13 Oct 2026 11:50:45 +0800 Subject: [PATCH 2/4] mcp: address review: CodeMethodNotFound error, test and comment nits Ping's removed-method guard now returns *jsonrpc.Error with CodeMethodNotFound, matching the documented error contract and the server's own phrasing for removed methods. The test drops its manual state write: a plain Connect over in-memory transports negotiates 2026-07-28 via server/discover, and asserts the typed error and that no ping request is sent. The doc comment now describes the lenient-server behavior (params-less pings currently succeed via the legacy path). Signed-off-by: Alexey Zilber --- mcp/client.go | 18 ++++++++++++------ mcp/client_test.go | 31 ++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index bce93199..9286d358 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -1263,14 +1263,20 @@ func newClientRequest[P Params](cs *ClientSession, params P) *ClientRequest[P] { // Ping makes an MCP "ping" request to the server. // -// On 2026-07-28 sessions Ping returns an error without a round trip: the -// 2026-07-28 protocol removed the ping method, so servers answer it with -// MethodNotFound (or reject the request at the transport for missing -// header/meta consistency). Verify liveness with a capability list request -// instead (tools/list, resources/list, prompts/list). +// On sessions negotiated at protocol version 2026-07-28 or later, Ping +// returns a [jsonrpc.Error] with code [jsonrpc.CodeMethodNotFound] without a +// round trip: the 2026-07-28 protocol schema removes the ping method. (A +// ping sent without per-request _meta is additionally rejected by transports +// that validate header/meta consistency, with -32020.) Use a capability +// list request (tools/list, resources/list, prompts/list) to verify +// liveness on those sessions instead. func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { if cs.usesNewProtocol() { - return fmt.Errorf("mcp: ping is not supported on %s sessions (the method was removed); verify liveness with tools/list, resources/list, or prompts/list", protocolVersion20260728) + return &jsonrpc.Error{ + Code: jsonrpc.CodeMethodNotFound, + Message: fmt.Sprintf("%q is not supported in the new protocol; verify liveness with tools/list, resources/list, or prompts/list", + methodPing), + } } _, err := handleSend[*emptyResult](ctx, methodPing, newClientRequest(cs, orZero[Params](params))) return err diff --git a/mcp/client_test.go b/mcp/client_test.go index 02bb6b3b..33d9f71a 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -9,6 +9,7 @@ package mcp import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "strings" @@ -765,12 +766,11 @@ func TestClientConnectDiscover(t *testing.T) { } } -// TestClientPingRemovedOnNewProtocol verifies that Ping on a 2026-07-28 -// session returns a descriptive local error instead of sending the ping -// method, which the 2026-07-28 protocol removed. The session state is set -// directly (in-package) because the SDK's fallback initialize caps the -// negotiated version at 2025-11-25; only the discovery path produces a -// 2026-07-28 session. +// TestClientPingRemovedOnNewProtocol verifies that Ping on a session +// negotiated at 2026-07-28 returns a jsonrpc.Error with +// CodeMethodNotFound instead of sending the ping method, which the +// 2026-07-28 protocol schema removes. A plain Connect negotiates +// 2026-07-28 over in-memory transports via server/discover. func TestClientPingRemovedOnNewProtocol(t *testing.T) { ctx := context.Background() var gotPing bool @@ -800,14 +800,23 @@ func TestClientPingRemovedOnNewProtocol(t *testing.T) { } defer cs.Close() - cs.state.InitializeResult.ProtocolVersion = protocolVersion20260728 + if ir := cs.InitializeResult(); ir == nil || ir.ProtocolVersion < protocolVersion20260728 { + t.Fatalf("InitializeResult = %+v, want a session negotiated at %s", ir, protocolVersion20260728) + } err = cs.Ping(ctx, nil) - if err == nil { - t.Fatal("Ping on a 2026-07-28 session = nil error, want a removed-method error") + var jsonrpcErr *jsonrpc.Error + if !errors.As(err, &jsonrpcErr) { + t.Fatalf("Ping on a 2026-07-28 session = %v (%T), want *jsonrpc.Error", err, err) + } + if jsonrpcErr.Code != jsonrpc.CodeMethodNotFound { + t.Errorf("code = %d, want %d", jsonrpcErr.Code, jsonrpc.CodeMethodNotFound) + } + if !strings.Contains(jsonrpcErr.Message, "not supported in the new protocol") { + t.Errorf("message %q does not explain the removal", jsonrpcErr.Message) } - if !strings.Contains(err.Error(), "ping is not supported") { - t.Errorf("Ping error %q does not explain the removal", err.Error()) + if !strings.Contains(jsonrpcErr.Message, "tools/list") { + t.Errorf("message %q does not point at a liveness alternative", jsonrpcErr.Message) } if gotPing { t.Error("ping request was sent despite the removed-method guard") From 67f44dba06fbf937ebe606544497c97486eccab2 Mon Sep 17 00:00:00 2001 From: Alexey Zilber Date: Tue, 13 Oct 2026 18:38:55 +0800 Subject: [PATCH 3/4] mcp: drop liveness hint from the Ping removed-method error Per review: the error and doc comment now match the server's phrasing exactly; naming replacement liveness checks belongs to callers, not the SDK. Signed-off-by: Alexey Zilber --- mcp/client.go | 9 +++------ mcp/client_test.go | 8 ++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index 9286d358..3195aed2 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -1267,15 +1267,12 @@ func newClientRequest[P Params](cs *ClientSession, params P) *ClientRequest[P] { // returns a [jsonrpc.Error] with code [jsonrpc.CodeMethodNotFound] without a // round trip: the 2026-07-28 protocol schema removes the ping method. (A // ping sent without per-request _meta is additionally rejected by transports -// that validate header/meta consistency, with -32020.) Use a capability -// list request (tools/list, resources/list, prompts/list) to verify -// liveness on those sessions instead. +// that validate header/meta consistency, with -32020.) func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { if cs.usesNewProtocol() { return &jsonrpc.Error{ - Code: jsonrpc.CodeMethodNotFound, - Message: fmt.Sprintf("%q is not supported in the new protocol; verify liveness with tools/list, resources/list, or prompts/list", - methodPing), + Code: jsonrpc.CodeMethodNotFound, + Message: fmt.Sprintf("%q is not supported in the new protocol", methodPing), } } _, err := handleSend[*emptyResult](ctx, methodPing, newClientRequest(cs, orZero[Params](params))) diff --git a/mcp/client_test.go b/mcp/client_test.go index 33d9f71a..d5ec8ec1 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -12,7 +12,6 @@ import ( "errors" "fmt" "log/slog" - "strings" "sync/atomic" "testing" @@ -812,11 +811,8 @@ func TestClientPingRemovedOnNewProtocol(t *testing.T) { if jsonrpcErr.Code != jsonrpc.CodeMethodNotFound { t.Errorf("code = %d, want %d", jsonrpcErr.Code, jsonrpc.CodeMethodNotFound) } - if !strings.Contains(jsonrpcErr.Message, "not supported in the new protocol") { - t.Errorf("message %q does not explain the removal", jsonrpcErr.Message) - } - if !strings.Contains(jsonrpcErr.Message, "tools/list") { - t.Errorf("message %q does not point at a liveness alternative", jsonrpcErr.Message) + if got, want := jsonrpcErr.Message, `"ping" is not supported in the new protocol`; got != want { + t.Errorf("message = %q, want %q", got, want) } if gotPing { t.Error("ping request was sent despite the removed-method guard") From 852eafda19aa6dbe5703ace073f8c082901e46a1 Mon Sep 17 00:00:00 2001 From: Alexey Zilber Date: Thu, 15 Oct 2026 10:44:16 +0800 Subject: [PATCH 4/4] mcp: trim Ping doc comment and update protocol docs Per review: the doc comment drops the _meta/-32020 parenthetical, and the protocol documentation now describes the client-side behavior - Ping returns MethodNotFound locally, without a round trip - instead of attributing the error to the server. docs/protocol.md regenerated via go generate ./internal/docs. Signed-off-by: Alexey Zilber --- docs/protocol.md | 6 +++--- internal/docs/protocol.src.md | 6 +++--- mcp/client.go | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/protocol.md b/docs/protocol.md index 06c1b77b..e94da70d 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -791,9 +791,9 @@ or > **Note**: `ping` is removed from the protocol as of `2026-07-28` by > [SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575). > The SDK preserves the API for backward compatibility with legacy clients; -> when both peers negotiate `2026-07-28`, the server returns -> `MethodNotFound` (`-32601`) for `ping` and `KeepAlive` should not be -> enabled. +> when a session negotiates `2026-07-28` or later, `Ping` returns +> `MethodNotFound` (`-32601`) locally, without a round trip, and +> `KeepAlive` should not be enabled. ### Progress diff --git a/internal/docs/protocol.src.md b/internal/docs/protocol.src.md index 7d6b01b5..7bf8aadd 100644 --- a/internal/docs/protocol.src.md +++ b/internal/docs/protocol.src.md @@ -628,9 +628,9 @@ or > **Note**: `ping` is removed from the protocol as of `2026-07-28` by > [SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575). > The SDK preserves the API for backward compatibility with legacy clients; -> when both peers negotiate `2026-07-28`, the server returns -> `MethodNotFound` (`-32601`) for `ping` and `KeepAlive` should not be -> enabled. +> when a session negotiates `2026-07-28` or later, `Ping` returns +> `MethodNotFound` (`-32601`) locally, without a round trip, and +> `KeepAlive` should not be enabled. ### Progress diff --git a/mcp/client.go b/mcp/client.go index 3195aed2..ab1c3353 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -1265,9 +1265,7 @@ func newClientRequest[P Params](cs *ClientSession, params P) *ClientRequest[P] { // // On sessions negotiated at protocol version 2026-07-28 or later, Ping // returns a [jsonrpc.Error] with code [jsonrpc.CodeMethodNotFound] without a -// round trip: the 2026-07-28 protocol schema removes the ping method. (A -// ping sent without per-request _meta is additionally rejected by transports -// that validate header/meta consistency, with -32020.) +// round trip: the 2026-07-28 protocol schema removes the ping method. func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { if cs.usesNewProtocol() { return &jsonrpc.Error{