Skip to content

Commit b37e497

Browse files
mcp: don't close session when keepalive ping returns method-not-found (#900)
Since ping is optional per the MCP spec, a peer that doesn't implement it will respond with a -32601 (method not found) error. Previously, startKeepalive treated all ping errors identically, closing the session even when the peer was perfectly healthy. Now, a method-not-found response stops the keepalive goroutine without closing the session. Fixes #897
1 parent 4c00594 commit b37e497

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

mcp/mcp_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"github.com/google/go-cmp/cmp"
2727
"github.com/google/jsonschema-go/jsonschema"
28+
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
2829
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2930
)
3031

@@ -938,6 +939,61 @@ func TestKeepAlive(t *testing.T) {
938939
})
939940
}
940941

942+
func TestKeepAliveMethodNotFound(t *testing.T) {
943+
synctest.Test(t, func(t *testing.T) {
944+
ctx := context.Background()
945+
946+
ct, st := NewInMemoryTransports()
947+
948+
// Server that rejects ping with method-not-found, simulating a
949+
// server that does not implement the optional ping method.
950+
s := NewServer(testImpl, nil)
951+
AddTool(s, greetTool(), sayHi)
952+
s.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
953+
return func(ctx context.Context, method string, req Request) (Result, error) {
954+
if method == "ping" {
955+
return nil, jsonrpc2.ErrMethodNotFound
956+
}
957+
return next(ctx, method, req)
958+
}
959+
})
960+
ss, err := s.Connect(ctx, st, nil)
961+
if err != nil {
962+
t.Fatal(err)
963+
}
964+
defer ss.Close()
965+
966+
clientOpts := &ClientOptions{
967+
KeepAlive: 50 * time.Millisecond,
968+
}
969+
c := NewClient(testImpl, clientOpts)
970+
cs, err := c.Connect(ctx, ct, nil)
971+
if err != nil {
972+
t.Fatal(err)
973+
}
974+
defer cs.Close()
975+
976+
// Advance past several keepalive cycles.
977+
time.Sleep(200 * time.Millisecond)
978+
979+
// The session should still be alive despite the server not
980+
// supporting ping.
981+
result, err := cs.CallTool(ctx, &CallToolParams{
982+
Name: "greet",
983+
Arguments: map[string]any{"Name": "user"},
984+
})
985+
if err != nil {
986+
t.Fatalf("call failed after keepalive with method-not-found: %v", err)
987+
}
988+
if len(result.Content) == 0 {
989+
t.Fatal("expected content in result")
990+
}
991+
if textContent, ok := result.Content[0].(*TextContent); !ok || textContent.Text != "hi user" {
992+
t.Fatalf("unexpected result: %v", result.Content[0])
993+
}
994+
})
995+
}
996+
941997
func TestElicitationUnsupportedMethod(t *testing.T) {
942998
ctx := context.Background()
943999
ct, st := NewInMemoryTransports()

mcp/shared.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package mcp
1414
import (
1515
"context"
1616
"encoding/json"
17+
"errors"
1718
"fmt"
1819
"log/slog"
1920
"net/http"
@@ -605,6 +606,10 @@ func startKeepalive(session keepaliveSession, interval time.Duration, cancelPtr
605606
err := session.Ping(pingCtx, nil)
606607
pingCancel()
607608
if err != nil {
609+
if errors.Is(err, jsonrpc2.ErrMethodNotFound) {
610+
// Peer doesn't support ping, stop the keepalive process.
611+
return
612+
}
608613
// Ping failed; log it before closing the session so the
609614
// failure is observable to operators. See #218.
610615
logger.Error("keepalive ping failed; closing session", "error", err)

0 commit comments

Comments
 (0)