mcp: list_changed can only be broadcast; a server cannot notify one session (2026-07-28 subscriptions/listen)

Abierto
#1,480 1 comentario 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@huynle-dev ya está trabajando en esto.

Desde el 2/11/2026.

Evaluación

Dificultad
4/5
Tiempo estimado
1-2 días
Aptitud para principiantes
32/100
Tipo de issue
Nueva funcionalidad
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
go
Área
api

Línea de trabajo

Start with Server.changeAndNotify and notifySessions in mcp/server.go, then notifySubscribedSessions and injectMetaSubscriptionID, to see how list-changed reaches the subscription tables today. Read TestSubscriptionsListen_MultipleSessions to learn the fan-out test pattern. Done means a ServerSession method sends a list-changed notification only to that session's open listen stream, with the listen's ID in _meta, and does nothing when no listen is open. The open questions in the issue need a maintainer decision before implementation.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

#666 asked for a way to send notifications/tools/list_changed to a specific client and was closed as obsolete under 2026-07-28. The transport changed (subscriptions/listen replaced the standalone GET stream), but delivery did not: in v1.8.0 (and on main @ eafb671) a server still cannot send a list-changed notification to one session.

Today
  • The only trigger is Server.changeAndNotify (mcp/server.go:747, v1.8.0), run on AddTool/RemoveTool/AddPrompt/…. After debouncing it calls notifySessions, which sends to every legacy session and every 2026-07-28 session in toolChangeSubscriptions / promptChangeSubscriptions / resourceChangeSubscriptions.
  • The per-session sender notifySubscribedSessions (:810) and the listen-request ID stamped under MetaKeySubscriptionID (injectMetaSubscriptionID, :832) are unexported. ServerSession exports NotifyProgress, NotifyElicitationComplete, Log, … but no list-changed sender.
  • TestSubscriptionsListen_MultipleSessions asserts the fan-out.
Use case

We run a multi-tenant MCP gateway on go-sdk: one stateless *mcp.Server behind several replicas. Each principal sees a different tool/prompt/resource list (registry + policy engine + OAuth scopes). A receiving middleware serves tools/list and tools/call; the SDK tool table stays empty, because per-principal servers with ~1000 tools each are not viable (#1265 has the same constraint).

When a principal's visible set changes, only that principal's open subscriptions/listen streams should get list_changed. Clients cache catalogs, so without it they keep calling tools that are gone and never see new ones.

The SDK already accepts the listen, acks it, records req.Session → requestID, forces SSE and retires the subscription. The missing piece is the last step: send this notification to this session's listen.

Workarounds, each with a problem:

  1. AddTool/RemoveTool a dummy tool plus a sending middleware that drops the notification for other principals (the #666 hack): fans out N notifications to drop N−1, and the routing key has to be smuggled through _meta (no ctx propagation, #1265).
  2. One *mcp.Server per principal from getServer, diffing its tool table: duplicates the catalog per principal just to reach the notifier.
  3. Writing the frame ourselves is impossible: the listen request ID is unreachable (idContextKey and the subscription tables are unexported).
Proposal
// NotifyToolListChanged sends notifications/tools/list_changed to this session.
// For a 2026-07-28 session it is delivered on the session's active
// subscriptions/listen stream that opted into toolsListChanged, with that
// listen's request ID under _meta[MetaKeySubscriptionID]. If no such listen is
// open, it does nothing and returns nil. Legacy sessions receive it on the
// standalone stream, as changeAndNotify does today.
func (ss *ServerSession) NotifyToolListChanged(ctx context.Context, params *ToolListChangedParams) error
func (ss *ServerSession) NotifyPromptListChanged(ctx context.Context, params *PromptListChangedParams) error
func (ss *ServerSession) NotifyResourceListChanged(ctx context.Context, params *ResourceListChangedParams) error

Implementation: lock s.mu, look up s.toolChangeSubscriptions[ss], unlock, copy params, injectMetaSubscriptionID, then handleNotify with the caller's ctx. Server.notifySessions can become a loop over these (one delivery path; composes with #1265).

If three methods is too much surface (the concern raised in #666), a single func (ss *ServerSession) NotifyListChanged(ctx context.Context, kind string) error would also work.

Usage:

server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
	return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
		if method == "subscriptions/listen" {
			ss := req.GetSession().(*mcp.ServerSession)
			stop := watchPrincipal(ctx, principalFrom(ctx), func() {
				_ = ss.NotifyToolListChanged(ctx, &mcp.ToolListChangedParams{})
			})
			defer stop()
		}
		return next(ctx, method, req) // SDK registers, acks, blocks until the stream ends
	}
})

Open questions: (a) "no open listen" → nil (our preference) or a sentinel error? (b) keep the shouldSendListChangedNotification capability gate (we'd say yes).

Happy to send a PR with tests.

Lenguaje dominante
Go
Estrellas
5.3k
Forks
607
Merge medio
1 d 17 h
PR fusionados (30 d)
80

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de modelcontextprotocol/go-sdk

Todos los issues de modelcontextprotocol/go-sdk

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.