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 d9b0f8f0..ab1c3353 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -1262,7 +1262,17 @@ func newClientRequest[P Params](cs *ClientSession, params P) *ClientRequest[P] { } // Ping makes an MCP "ping" request to the server. +// +// 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. 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", 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 afe9ecc6..d5ec8ec1 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" "sync/atomic" @@ -764,6 +765,60 @@ func TestClientConnectDiscover(t *testing.T) { } } +// 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 + + 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() + + 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) + 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 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") + } +} + // 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()