mcp: a second subscriptions/listen for the same URI silences the first
メンテナーはふだん 1 日以内に返信
評価
この issue はまだ評価されていません。
説明
Checklist
- I searched the existing issues, and this has not been reported before.
- This is a real problem that I ran into myself, using the latest release of the SDK.
- I ran into it in a production system, not in a toy example, a benchmark, or an automated or AI-driven scan of the code.
- I (a human) examined the relevant SDK code, and wrote or reviewed every line of this report. It is not unreviewed AI output.
- This report is brief and concise, and sticks to the facts needed to understand the problem.
What happened?
A follow-up to #1359: #1360 applied the first shape set out there, which leaves two listens held open at once on one URI as they were.
On a 2026-07-28 session I open two subscriptions/listen streams for one resource URI. Both are acknowledged and stay open, but Server.ResourceUpdated delivers only on the second, and once the second is cancelled the first, still open, receives nothing.
I expected each listen to get the update with its own subscriptionId: both acknowledgments name the URI in notifications, which "reflects the subset the server agreed to honor" (Acknowledgment).
resourceSubscriptions holds one request ID per URI and session (server.go#L83), so the second listen overwrites the first's (L1300), and the second's teardown then deletes the entry as its own (L1326-L1327). No teardown can bring the first ID back.
The keying by session that #1275 kept can stay: the session's entry would hold every open listen's ID for the URI rather than only the latest, and ResourceUpdated would deliver on each (the second shape in #1359). I can send a PR if that works for you, or a line for the docs if you would rather leave this case unsupported.
How to reproduce
// Two subscriptions/listen streams for one URI on one 2026-07-28 session.
package main
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const uri = "file:///a"
func main() {
ctx := context.Background()
unsubscribed := make(chan struct{}, 2)
server := mcp.NewServer(&mcp.Implementation{Name: "s", Version: "v1"}, &mcp.ServerOptions{
SubscribeHandler: func(context.Context, *mcp.SubscribeRequest) error { return nil },
UnsubscribeHandler: func(context.Context, *mcp.UnsubscribeRequest) error { unsubscribed <- struct{}{}; return nil },
})
server.AddResource(&mcp.Resource{URI: uri, Name: "a"}, func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{}, nil
})
st, ct := mcp.NewInMemoryTransports()
if _, err := server.Connect(ctx, st, nil); err != nil {
panic(err)
}
conn, err := ct.Connect(ctx)
if err != nil {
panic(err)
}
var mu sync.Mutex
var got []string // subscriptionId of each resources/updated received
acks := make(chan struct{}, 2)
go func() {
for {
msg, err := conn.Read(ctx)
if err != nil {
return
}
req, ok := msg.(*jsonrpc.Request)
if !ok {
continue
}
var p struct {
Meta map[string]any `json:"_meta"`
Notifications json.RawMessage `json:"notifications"`
}
json.Unmarshal(req.Params, &p)
id := fmt.Sprint(p.Meta[mcp.MetaKeySubscriptionID])
switch req.Method {
case "notifications/subscriptions/acknowledged":
fmt.Printf("listen %s acknowledged with %s\n", id, p.Notifications)
acks <- struct{}{}
case "notifications/resources/updated":
mu.Lock()
got = append(got, id)
mu.Unlock()
}
}
}()
send := func(s string) {
m, err := jsonrpc.DecodeMessage([]byte(s))
if err != nil {
panic(err)
}
if err := conn.Write(ctx, m); err != nil {
panic(err)
}
}
update := func(when string) {
mu.Lock()
got = nil
mu.Unlock()
server.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{URI: uri})
time.Sleep(200 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
fmt.Printf("%s: update delivered on listens %v\n", when, got)
}
listen := `{"jsonrpc":"2.0","id":%d,"method":"subscriptions/listen","params":{"_meta":{` +
`"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},` +
`"notifications":{"resourceSubscriptions":["` + uri + `"]}}}`
send(fmt.Sprintf(listen, 1))
<-acks
send(fmt.Sprintf(listen, 2))
<-acks
update("listens 1 and 2 open")
send(`{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":2}}`)
<-unsubscribed
time.Sleep(100 * time.Millisecond)
update("listen 2 cancelled, listen 1 still open")
}
Where did you hit this?
I hit it in gitlab-mcp-server, an MCP server for GitLab on this SDK, over stdio. With two listens open on the URI of one GitLab issue, each change of the issue reaches only the second; once the second is cancelled, the first stays open and gets nothing while the server keeps polling GitLab for it, and ResourceUpdated logs a subscriber_count of 0.
Go MCP SDK version
v1.8.0 (same output on main at eafb671)
Go version
go version go1.27.3 linux/amd64
Logs
listen 1 acknowledged with {"resourceSubscriptions":["file:///a"]}
listen 2 acknowledged with {"resourceSubscriptions":["file:///a"]}
listens 1 and 2 open: update delivered on listens [2]
listen 2 cancelled, listen 1 still open: update delivered on listens []
- 主要言語
- Go
- スター
- 5.3k
- フォーク
- 607
- 平均マージ
- 1日 11時間
- マージ済み PR(30日)
- 82
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/go-sdk のほかの issue
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensions再び着手できるかも このイシューのプルリクエストはマージされずにクローズされました。 オープンneeds investigation
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
modelcontextprotocol/go-sdk#745 · コメント 13 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
mcp: whether MaxLineLength rejects a message depends on how the stream is read対応中かも @Tangerg が今日担当しました。 オープン
modelcontextprotocol/go-sdk#1509 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 62/100
modelcontextprotocol/go-sdk#1506 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
mcp: closing a session right after a cancelled call drops notifications/cancelled, so the server handler keeps running対応中かも @vshalpnjabi が今日担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
modelcontextprotocol/go-sdk#1505 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
mcp: a canceled write on IOTransport/StdioTransport cannot leave the write queue対応中かも @Tangerg が 1 日前に担当しました。 オープン
modelcontextprotocol/go-sdk#1498 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/go-sdk の issue をすべて見る
似ている issue
-
enhancement needs:sonnet ready-for-agent
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
microsoft/TypeScript#65370 ·
メンテナーはふだん 1 日以内に返信
-
docs: data-ownership invites vendors to build add-ons around the OS対応中かも @brandli が今日担当しました。 オープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
gentian-org/gentian-os#894 ·
メンテナーはふだん 1 日以内に返信
-
bug tech debt testing
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
aws/amazon-vpc-cni-k8s#3976 ·
メンテナーはふだん 1 日以内に返信
-
bug raised-by:desk
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
medici-finance/assay#5678 ·
メンテナーはふだん 1 日以内に返信