mcp: a second subscriptions/listen for the same URI silences the first
I maintainer di solito rispondono entro 1 giorno
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
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 []
- Lingua principale
- Go
- Stelle
- 5.3k
- Fork
- 607
- Merge medio
- 1g 16h
- PR unite (30g)
- 81
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di modelcontextprotocol/go-sdk
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensionsForse di nuovo libera Una pull request per questa issue è stata chiusa senza essere unita. Apertaneeds investigation
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
modelcontextprotocol/go-sdk#745 · 13 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 50/100
modelcontextprotocol/go-sdk#1492 ·
I maintainer di solito rispondono entro 1 giorno
-
mcp: ClientSession.CallTool omits Mcp-Param-* headers unless the tool is in the ListTools cache, so a call without a prior tools/list is rejectedForse già presa @huynle-dev l’ha presa 1 giorno fa. Aperta
Difficoltà 4/5 1-2 giorni Idoneità per principianti 35/100
modelcontextprotocol/go-sdk#1481 · 2 commenti · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
mcp: list_changed can only be broadcast; a server cannot notify one session (2026-07-28 subscriptions/listen)Forse già presa @huynle-dev l’ha presa 1 giorno fa. Aperta
Difficoltà 4/5 1-2 giorni Idoneità per principianti 32/100
modelcontextprotocol/go-sdk#1480 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
mcp: Client.Connect falls back to initialize after a 401 on server/discover, running the authorization flow twiceForse già presa @kerthans l’ha presa 13 giorni fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 35/100
modelcontextprotocol/go-sdk#1423 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di modelcontextprotocol/go-sdk
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 84/100
I maintainer di solito rispondono entro 1 giorno
-
cmd/ebitenmobile: multi-language bindings run the complete generator twiceForse già presa @kumagi l’ha presa oggi. Apertaos:macos performance
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
hajimehoshi/ebiten#4011 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
agent-butler-finding chore
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
jordansmall/spindrift#6318 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Eyevinn/moqlivemock#166 ·
I maintainer di solito rispondono entro 1 giorno