Repository navigation
mcp: clear a resource subscription when its listen stream ends - #1283
Merged
guglielmo-san merged 4 commits intoOct 6, 2026
Conversation
Under SEP-2575, ClientSession.Subscribe opened a subscriptions/listen stream per resource URI but never observed its completion: the transport call was fire-and-forget, so when the stream ended for any reason other than a client Unsubscribe (server teardown, resource revocation, a dropped connection) the resourceSubs entry was left behind. A later Subscribe for the same URI then saw the stale entry and returned as a no-op, so the subscription could never be re-established. This diverged from the python and typescript SDKs, whose clients await the listen and re-open on a bare re-subscribe. Await the listen call on its own goroutine (Subscribe stays non-blocking) and, when it completes while its listen context has not been client-cancelled, clear the resourceSubs entry so a bare re-Subscribe re-opens the stream. The map value becomes a small *resourceSub carrying a per-session generation, so a listen goroutine only clears the entry it created and never one a racing Unsubscribe->Subscribe (or a re-subscribe from a completing listen) has since installed; context.CancelFunc values are not comparable, so the previous map type could not express this guard. The SDK does not auto-resubscribe: a permanently revoked URI would hot-loop, so reopening is left to the application. The legacy (pre-2575) resources/subscribe path and the wire protocol are unchanged.
…ubscriptionsListen method to unify error handling and cleanup.
guglielmo-san
approved these changes
Oct 5, 2026
Contributor
|
@jeremy I added some code on top of your PR to simplify the overall implementation of subscriptions/listen |
nahapetyan-serob
approved these changes
Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Under SEP-2575 the client treated
subscriptions/listenas fire-and-forget. The default sending handler issued the call, returned an empty result immediately, and only watched the listen context so it could sendnotifications/cancelled. Nothing observed the stream ending, which caused three problems:SubscriberegistersresourceSubs[uri]and opens a listen. When that listen ended for any reason other thanUnsubscribe(the server tearing it down, the resource being revoked, the connection dropping), the entry stayed. Every laterSubscribefor the URI then hit the "already subscribed" guard and did nothing. An entry added bySubscribeon a closing session was never removed either.defer cancel()) cancelled the stream as soon asnextreturned.UnsubscribeorCloseit sentnotifications/cancelledeven for a stream the server had already closed.This brings the Go SDK in line with the Python and TypeScript SDKs, whose clients await the listen and re-open it on a plain re-subscribe.
Fix
defaultSendingMethodHandlernow awaitssubscriptions/listenlike any other call, andcallSubscriptionsListenis removed. Cancelling the listen context still sendsnotifications/cancelled, throughcall.Connect(for list-changed notifications) andSubscribe(one listen per resource URI) run the listen on a goroutine throughsubscriptionsListen. The listen therefore goes through the sending middleware chain, and neither method blocks on it.awaitListenclears theresourceSubsentry, so a plainSubscribere-opens the stream.*resourceSub{cancel, gen}, carrying a per-session generation number. A finishing listen therefore only clears the entry it created, never one installed by a racingUnsubscribe→Subscribe.The SDK does not resubscribe on its own, because a revoked URI would loop forever. Reopening is left to the application calling
Subscribeagain. The legacyresources/subscribe/resources/unsubscribepath is unchanged.Behavior changes
There are no exported API changes. The visible differences on
2026-07-28sessions are:nextforsubscriptions/listennow returns when the stream ends or is cancelled, with the stream's actual result or error, instead of an empty result right away. Middleware that applies a per-request timeout, or holds a resource while callingnext, now does so for the life of the stream.Connectno longer fails withopening subscriptions/listenwhen a sending middleware rejects the list-changed listen, andSubscribeno longer returns such an error. A listen rejected by the server was not reported before either. proposal: ClientOptions.ResourceSubscriptionEndedHandler for observing resource-subscription termination #1284 proposes a way to observe that.notifications/cancelledis no longer sent for a listen that has already ended.Follow-up proposal for the optional ended-handler: #1284.