mcp: closing a session right after a cancelled call drops notifications/cancelled, so the server handler keeps running
Los mantenedores suelen responder en 1 día
@vshalpnjabi ya está trabajando en esto.
Desde el 6/11/2026.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- go
- Área
- backend-api-design
Línea de trabajo
Start by tracing call() and ClientSession.Close through the JSON-RPC connection shutdown path, then inspect how outgoing notifications are tracked and drained. The issue’s repro can confirm the failure and provide a starting test. Done means closing immediately after a cancelled call still delivers notifications/cancelled, cancels the server handler, and avoids waiting for the 5-second timeout.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
Since v1.8.0 (#1151), call() retires a cancelled call and sends notifications/cancelled from a goroutine. If the client closes the session right after the call returns (a graceful shutdown that cancels in-flight tool calls and then closes, for example), the notification is dropped. On a stateful streamable HTTP server the tool handler is then never cancelled, and ClientSession.Close blocks for closeDeleteTimeout (5s).
MCPGODEBUG=blockingcancelnotify=1 restores the v1.7.0 ordering, but it is scheduled for removal in v1.9.0.
Repro (v1.8.0, also matches main)
package repro
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func TestCloseAfterCanceledCallCancelsTheServerHandler(t *testing.T) {
entered := make(chan struct{}, 1)
canceled := make(chan struct{}, 1)
release := make(chan struct{})
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "1"}, nil)
server.AddTool(&mcp.Tool{Name: "wait", InputSchema: map[string]any{"type": "object"}},
func(ctx context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
entered <- struct{}{}
select {
case <-ctx.Done():
canceled <- struct{}{}
return nil, ctx.Err()
case <-release:
return &mcp.CallToolResult{}, nil
}
})
httpServer := httptest.NewServer(mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil))
t.Cleanup(httpServer.Close)
t.Cleanup(func() { close(release) })
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "1"}, nil)
session, err := client.Connect(t.Context(), &mcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
go func() {
<-entered
cancel()
}()
if _, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "wait"}); !errors.Is(err, context.Canceled) {
t.Fatalf("CallTool: got %v, want context.Canceled", err)
}
start := time.Now()
_ = session.Close()
closed := time.Since(start)
select {
case <-canceled:
t.Logf("handler canceled; Close took %v", closed.Round(time.Millisecond))
case <-time.After(2 * time.Second):
t.Fatalf("server handler never saw the cancellation; Close took %v", closed.Round(time.Millisecond))
}
}
Go 1.26.10, linux/amd64:
- Default: fails 20/20,
server handler never saw the cancellation; Close took 5s. MCPGODEBUG=blockingcancelnotify=1: passes 20/20,handler canceled; Close took 1ms.
Why
call()runsconn.Retire(call, ...), starts the notify goroutine and returns.ClientSession.Close→jsonrpc2.Connection.Closemarks the connection closing.- The goroutine's
Notifyfinds no outgoing calls and no incoming requests, soshuttingDown(ErrClientClosing)rejects it. The notification is never written. - The streamable client sends DELETE.
serveStatefulDELETE→ServerSession.Closecancels onlysubscriptions/listenhandlers, thenconn.Closewaits for in-flight requests, including the tool handler that is still blocked on its context. - The DELETE hangs until the client gives up after
closeDeleteTimeoutand cancels its connection. The handler keeps running.
So closing the connection does not signal the cancellation to a stateful streamable server, which is the assumption in #1241.
Suggestion
Keep the caller's return immediate (the point of #1150), but register the pending notification synchronously before call() returns, e.g. bump outgoingNotifications (or an equivalent "pending cancel" counter) before spawning the goroutine. Then Close allows the write and its drain waits for it, bounded by notifyCancellationTimeout.
We hit this in a graceful-shutdown path that cancels tool calls, waits for them to return, then closes the session; we're running with blockingcancelnotify=1 until there is a fix.
- Lenguaje dominante
- Go
- Estrellas
- 5.3k
- Forks
- 607
- Merge medio
- 1 d 11 h
- PR fusionados (30 d)
- 82
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de modelcontextprotocol/go-sdk
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensionsQuizá libre de nuevo Un pull request para esta issue se cerró sin fusionarse. Abiertoneeds investigation
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
modelcontextprotocol/go-sdk#745 · 13 comentarios · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
mcp: whether MaxLineLength rejects a message depends on how the stream is readPosiblemente ocupada @Tangerg la tomó hoy. Abierto
modelcontextprotocol/go-sdk#1509 · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 62/100
modelcontextprotocol/go-sdk#1506 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
mcp: a canceled write on IOTransport/StdioTransport cannot leave the write queuePosiblemente ocupada @Tangerg la tomó hace 1 día. Abierto
modelcontextprotocol/go-sdk#1498 · 1 comentario · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
mcp: list_changed can only be broadcast; a server cannot notify one session (2026-07-28 subscriptions/listen)Posiblemente ocupada @huynle-dev la tomó hace 4 días. Abierto
Dificultad 4/5 1-2 días Aptitud para principiantes 32/100
modelcontextprotocol/go-sdk#1480 · 1 comentario · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de modelcontextprotocol/go-sdk
Issues similares
-
[v3] KeyBindingManager calls (Add/Remove) are not converted into a case-insensitive accelerator.KeyAbiertoBug
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
runtime: usleep sleeps nanoseconds instead of microseconds on linux/mips and linux/mips64Posiblemente ocupada @BRobertson7 la tomó hoy. Abiertocompiler/runtime
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
golang/go#82913 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
bug needs-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
aws/aws-sdk-go-v2#3625 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
adrianbrad/queue#92 ·