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

クローズ
#1,489 コメント 3 件 リアクション 0 件 担当者 1 名 GitHub で見る

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

@jmrplens がすでに取り組んでいます。

2026年11月2日 から。

  • #1490 @jmrplens による — マージ済み

評価

この 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

環境構築

Codespaces で開く

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

はじめの一歩

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

modelcontextprotocol/go-sdk のほかの issue

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

似ている issue

Go の issue をもっと見る

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

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