Skip to content

Pass a handler-returned _meta through the subscribe result - #510

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:pass_meta_through_subscription_results
Open

Pass a handler-returned _meta through the subscribe result#510
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:pass_meta_through_subscription_results

Conversation

@koic

@koic koic commented Aug 12, 2026

Copy link
Copy Markdown
Member

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 #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.

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

`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.
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.

resources/subscribe custom handler return value is discarded (always responds {})

1 participant