Pass a handler-returned _meta through the subscribe result - #510
Open
koic wants to merge 1 commit into
Open
Conversation
## Motivation and Context
`resources/subscribe` and `resources/unsubscribe` answered with a hardcoded `{}`, discarding whatever
the registered handler returned. A server had no way to attach anything to the response - not even `_meta`,
which the specification allows on every result. The handler docstring justified the discard as "always
an empty result `{}` per the MCP specification", but the spec does not require an empty object here:
the subscribe result is an `EmptyResult`, and every result may carry `_meta`.
The one field that is interoperable is `_meta`. The TypeScript SDK validates the subscribe result against
`EmptyResultSchema.strict()`, which rejects any member other than `_meta`, so a top-level field like
a bespoke `subscriptionId` would be refused by a TypeScript client. The Python SDK passes the handler's `EmptyResult`
return through, `_meta` included. Passing `_meta` matches both while staying within what the spec defines.
The subscribe and unsubscribe handlers now pass a returned `_meta` hash through to the result and drop anything else.
`subscription_result` reads `_meta` under a symbol or string key and includes it only when it is itself
a hash; every other return shape - a non-hash, or a hash without a usable `_meta` - keeps the empty `{}` result it
produced before. Advisory data such as a subscription identifier goes under `_meta`, namespaced, rather than
at the top level. This affects only 2025-11-25 and earlier connections, since `resources/subscribe`
and `resources/unsubscribe` are removed from the modern lifecycle.
Fixes modelcontextprotocol#508.
## How Has This Been Tested?
New tests in `test/mcp/server_test.rb` cover a `_meta` hash passing through on both `resources/subscribe`
and `resources/unsubscribe`, a non-`_meta` field being dropped, a string `_meta` key working, a non-hash `_meta`
being ignored, and a non-hash return keeping the empty result. The existing subscribe and unsubscribe tests,
which return `{}` and assert an empty result, are unchanged.
## Breaking Changes
None. A handler that does not return a hash carrying a `_meta` hash - which is every handler written against
the previous "return value is ignored" contract - still produces the empty `{}` result.
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.
Motivation and Context
resources/subscribeandresources/unsubscribeanswered with a hardcoded{}, discarding whatever the registered handler returned. A server had no way to attach anything to the response - not even_meta, which the specification allows on every result. The handler docstring justified the discard as "always an empty result{}per the MCP specification", but the spec does not require an empty object here: the subscribe result is anEmptyResult, and every result may carry_meta.The one field that is interoperable is
_meta. The TypeScript SDK validates the subscribe result againstEmptyResultSchema.strict(), which rejects any member other than_meta, so a top-level field like a bespokesubscriptionIdwould be refused by a TypeScript client. The Python SDK passes the handler'sEmptyResultreturn through,_metaincluded. Passing_metamatches both while staying within what the spec defines.The subscribe and unsubscribe handlers now pass a returned
_metahash through to the result and drop anything else.subscription_resultreads_metaunder a symbol or string key and includes it only when it is itself a hash; every other return shape - a non-hash, or a hash without a usable_meta- keeps the empty{}result it produced before. Advisory data such as a subscription identifier goes under_meta, namespaced, rather than at the top level. This affects only 2025-11-25 and earlier connections, sinceresources/subscribeandresources/unsubscribeare removed from the modern lifecycle.Fixes #508.
How Has This Been Tested?
New tests in
test/mcp/server_test.rbcover a_metahash passing through on bothresources/subscribeandresources/unsubscribe, a non-_metafield being dropped, a string_metakey working, a non-hash_metabeing ignored, and a non-hash return keeping the empty result. The existing subscribe and unsubscribe tests, which return{}and assert an empty result, are unchanged.Breaking Changes
None. A handler that does not return a hash carrying a
_metahash - which is every handler written against the previous "return value is ignored" contract - still produces the empty{}result.Types of changes
Checklist