Skip to content

Consult the resources_subscribe_handler for listen resource URIs - #632

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:authorize_the_resource_subscriptions_of_a_listen_stream
Oct 26, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:authorize_the_resource_subscriptions_of_a_listen_stream

Conversation

@koic

@koic koic commented Oct 25, 2026

Copy link
Copy Markdown
Member

Motivation and Context

In the 2026-07-28 lifecycle subscriptions/listen takes the place of resources/subscribe, but the URIs it names in resourceSubscriptions were honored on the subscribe capability alone. The resources_subscribe_handler, the hook an application uses to decide which resources a client may follow, was never called for them, so a decision it made for resources/subscribe did not carry over to a modern client.

Each distinct URI is now passed to that handler with the params a resources/subscribe request for it would carry, together with the listen request's envelope and auth_info when the handler declares server_context:. A URI the handler raises for is reported to the exception reporter and left out of the acknowledgement, and the stream receives no notifications/resources/updated for it. The spec lets the acknowledgement honor a subset of the requested filter, so this needs no new error on the wire, and the field is omitted when no URI remains. The decision is made once when the stream opens, before it is registered; the stream already closes when its token expires.

The params also carry the listen request's _meta, as those of a resources/subscribe request would, while the server_context has no session, so session-scoped helpers do nothing there. Any value the handler returns accepts the URI.

The TypeScript and Python SDKs copy the requested URIs into the acknowledgement as well; the TypeScript SDK leaves authorization to the middleware layer. In the Ruby SDK the handler was already the documented place for this decision, so it is reused instead of adding a second hook that existing applications would have to discover.

resources_unsubscribe_handler is not called when a listen stream ends: the subscription lasts as long as the stream.

Server#subscribable_resource_uris is public only so that the transport can call it; it is marked @api private and is not part of the published API.

How Has This Been Tested?

  • test/mcp/server/transports/streamable_http_transport_test.rb
  • test/mcp/server/transports/streamable_http_transport_oauth_test.rb
  • test/mcp/server_test.rb

The two transport tests and the OAuth test fail against the previous lib, which acknowledged every requested URI. The _meta transport test fails against it too.

Breaking Changes

A resources_subscribe_handler that raises for a URI now narrows the listen acknowledgement for that URI, and a handler that records subscriptions is now also called for listen streams, without a matching resources_unsubscribe_handler call. Servers without a custom handler are unaffected.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

In the 2026-07-28 lifecycle `subscriptions/listen` takes the place of `resources/subscribe`,
but the URIs it names in `resourceSubscriptions` were honored on the `subscribe` capability alone.
The `resources_subscribe_handler`, the hook an application uses to decide which resources
a client may follow, was never called for them, so a decision it made for `resources/subscribe`
did not carry over to a modern client.

Each distinct URI is now passed to that handler with the params a `resources/subscribe` request
for it would carry, together with the listen request's envelope and `auth_info` when the handler declares
`server_context:`. A URI the handler raises for is reported to the exception reporter
and left out of the acknowledgement, and the stream receives no `notifications/resources/updated` for it.
The spec lets the acknowledgement honor a subset of the requested filter, so this needs no new error on the wire,
and the field is omitted when no URI remains. The decision is made once when the stream opens,
before it is registered; the stream already closes when its token expires.

The params also carry the listen request's `_meta`, as those of a `resources/subscribe` request would,
while the `server_context` has no session, so session-scoped helpers do nothing there. Any value
the handler returns accepts the URI.

The TypeScript and Python SDKs copy the requested URIs into the acknowledgement as well;
the TypeScript SDK leaves authorization to the middleware layer. In the Ruby SDK the handler was already
the documented place for this decision, so it is reused instead of adding a second hook that
existing applications would have to discover.

`resources_unsubscribe_handler` is not called when a listen stream ends: the subscription lasts
as long as the stream.

`Server#subscribable_resource_uris` is public only so that the transport can call it;
it is marked `@api private` and is not part of the published API.

## How Has This Been Tested?

- test/mcp/server/transports/streamable_http_transport_test.rb
- test/mcp/server/transports/streamable_http_transport_oauth_test.rb
- test/mcp/server_test.rb

The two transport tests and the OAuth test fail against the previous `lib`, which acknowledged every requested URI.
The `_meta` transport test fails against it too.

## Breaking Changes

A `resources_subscribe_handler` that raises for a URI now narrows the listen acknowledgement for that URI,
and a handler that records subscriptions is now also called for listen streams, without a matching
`resources_unsubscribe_handler` call. Servers without a custom handler are unaffected.
@koic
koic merged commit 26eabbc into modelcontextprotocol:main Oct 26, 2026
11 checks passed
@koic
koic deleted the authorize_the_resource_subscriptions_of_a_listen_stream branch October 26, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants