mcp: a second subscriptions/listen for the same URI silences the first

Aperta
#1,489 3 commenti 0 reazioni 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@jmrplens ci sta già lavorando.

Dal 2/11/2026.

  • #1490 di @jmrplens — aperta

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

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/go-sdk

Tutte le issue di modelcontextprotocol/go-sdk

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.