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

オープン
#1,480 コメント 1 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@huynle-dev がすでに取り組んでいます。

2026年11月2日 から。

評価

難易度
4/5
見積もり時間
1〜2日
初心者へのやさしさ
32/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
go
領域
api

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

#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.

主要言語
Go
スター
5.3k
フォーク
607
平均マージ
1日 17時間
マージ済み PR(30日)
80

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

modelcontextprotocol/go-sdk のほかの issue

modelcontextprotocol/go-sdk の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。