Skip to content

Commit 0ff1406

Browse files
authored
Merge pull request #510 from koic/pass_meta_through_subscription_results
Pass a handler-returned `_meta` through the subscribe result
2 parents 3447328 + 8c2e89a commit 0ff1406

3 files changed

Lines changed: 111 additions & 6 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,18 @@ server.define_tool(name: "update_resource") do |server_context:, **args|
15401540
end
15411541
```
15421542

1543+
The `resources/subscribe` and `resources/unsubscribe` responses are empty results. The one field the spec allows
1544+
alongside is `_meta`, so a handler that returns `{ _meta: { ... } }` has it passed through; any other field it
1545+
returns is dropped. To convey a subscription identifier or other advisory data to the client, nest it under `_meta`
1546+
rather than returning it at the top level, which interoperating clients reject:
1547+
1548+
```ruby
1549+
server.resources_subscribe_handler do |params|
1550+
id = subscriptions.create(params[:uri].to_s)
1551+
{ _meta: { "myapp.example/subscriptionId" => id } }
1552+
end
1553+
```
1554+
15431555
### Sampling
15441556

15451557
The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.

lib/mcp/server.rb

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,19 +463,25 @@ def completion_handler(&block)
463463
end
464464

465465
# Sets a custom handler for `resources/subscribe` requests.
466-
# The block receives the parsed request params. The return value is
467-
# ignored; the response is always an empty result `{}` per the MCP specification.
466+
# The block receives the parsed request params. The response is an empty result, except that
467+
# a `_meta` hash the block returns is passed through - the spec defines no other member for this result,
468+
# so any other field the block returns is dropped. Nest a subscription identifier or other advisory data
469+
# under `_meta`.
468470
#
469471
# @yield [params] The request params containing `:uri`.
472+
# @yieldreturn [Hash, nil] Optionally `{ _meta: { ... } }`; any other shape yields an empty result.
470473
def resources_subscribe_handler(&block)
471474
@handlers[Methods::RESOURCES_SUBSCRIBE] = block
472475
end
473476

474477
# Sets a custom handler for `resources/unsubscribe` requests.
475-
# The block receives the parsed request params. The return value is
476-
# ignored; the response is always an empty result `{}` per the MCP specification.
478+
# The block receives the parsed request params. The response is an empty result, except that
479+
# a `_meta` hash the block returns is passed through - the spec defines no other member for this result,
480+
# so any other field the block returns is dropped. Nest a subscription identifier or other advisory data
481+
# under `_meta`.
477482
#
478483
# @yield [params] The request params containing `:uri`.
484+
# @yieldreturn [Hash, nil] Optionally `{ _meta: { ... } }`; any other shape yields an empty result.
479485
def resources_unsubscribe_handler(&block)
480486
@handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
481487
end
@@ -686,8 +692,9 @@ def handle_request(request, method, session: nil, related_request_id: nil)
686692
contents.is_a?(InputRequiredResult) ? contents : build_read_resource_result(contents)
687693
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
688694
validate_resource_subscription_params!(params)
689-
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
690-
{}
695+
handler_result = dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
696+
697+
subscription_result(handler_result)
691698
when Methods::TOOLS_CALL
692699
call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
693700
when Methods::PROMPTS_GET
@@ -1116,6 +1123,18 @@ def validate_resource_subscription_params!(params)
11161123
end
11171124
end
11181125

1126+
# The `resources/subscribe` and `resources/unsubscribe` result is an empty object except for the optional `_meta`
1127+
# every result may carry: the TypeScript SDK validates it against `EmptyResultSchema.strict()`,
1128+
# which rejects any other member, so only `_meta` is passed through from the handler. A handler that returns
1129+
# anything else keeps the empty `{}` result it had before, so returning a subscription identifier or
1130+
# other advisory data means nesting it under `_meta`.
1131+
def subscription_result(handler_result)
1132+
return {} unless handler_result.is_a?(Hash)
1133+
1134+
meta = handler_result[:_meta] || handler_result["_meta"]
1135+
meta.is_a?(Hash) ? { _meta: meta } : {}
1136+
end
1137+
11191138
def validate_initialize_params!(params)
11201139
unless params.is_a?(Hash)
11211140
raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)

test/mcp/server_test.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3729,6 +3729,80 @@ def read_resource_request(uri)
37293729
assert_equal "Invalid params", response[:error][:message]
37303730
end
37313731

3732+
# Builds an initialized server that advertises the `resources.subscribe` capability.
3733+
def subscription_server
3734+
server = Server.new(name: "test_server", capabilities: { resources: { subscribe: true } })
3735+
server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
3736+
server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })
3737+
server
3738+
end
3739+
3740+
# Sends `method` (`resources/subscribe` or `resources/unsubscribe`) and returns the JSON-RPC result.
3741+
def handle_subscription(server, method)
3742+
server.handle({
3743+
jsonrpc: "2.0",
3744+
id: 2,
3745+
method: method,
3746+
params: { uri: "https://example.com/resource" },
3747+
})[:result]
3748+
end
3749+
3750+
test "#handle resources/subscribe passes a handler-returned _meta through to the result" do
3751+
server = subscription_server
3752+
server.resources_subscribe_handler { |_params| { _meta: { "acme.example/subscriptionId" => "sub-1" } } }
3753+
3754+
result = handle_subscription(server, "resources/subscribe")
3755+
3756+
assert_equal({ _meta: { "acme.example/subscriptionId" => "sub-1" } }, result)
3757+
end
3758+
3759+
test "#handle resources/unsubscribe passes a handler-returned _meta through to the result" do
3760+
server = subscription_server
3761+
server.resources_unsubscribe_handler { |_params| { _meta: { "acme.example/note" => "gone" } } }
3762+
3763+
result = handle_subscription(server, "resources/unsubscribe")
3764+
3765+
assert_equal({ _meta: { "acme.example/note" => "gone" } }, result)
3766+
end
3767+
3768+
test "#handle resources/subscribe drops a handler-returned field that is not _meta" do
3769+
# The spec's result defines no member other than `_meta`, so a top-level field the handler adds is not
3770+
# a subscription protocol; it stays out of the response.
3771+
server = subscription_server
3772+
server.resources_subscribe_handler { |_params| { subscriptionId: "sub-1" } }
3773+
3774+
result = handle_subscription(server, "resources/subscribe")
3775+
3776+
assert_equal({}, result)
3777+
end
3778+
3779+
test "#handle resources/subscribe accepts a string _meta key from the handler" do
3780+
server = subscription_server
3781+
server.resources_subscribe_handler { |_params| { "_meta" => { "k" => "v" } } }
3782+
3783+
result = handle_subscription(server, "resources/subscribe")
3784+
3785+
assert_equal({ _meta: { "k" => "v" } }, result)
3786+
end
3787+
3788+
test "#handle resources/subscribe ignores a handler-returned _meta that is not a hash" do
3789+
server = subscription_server
3790+
server.resources_subscribe_handler { |_params| { _meta: "not-a-hash" } }
3791+
3792+
result = handle_subscription(server, "resources/subscribe")
3793+
3794+
assert_equal({}, result)
3795+
end
3796+
3797+
test "#handle resources/subscribe keeps an empty result when the handler returns a non-hash" do
3798+
server = subscription_server
3799+
server.resources_subscribe_handler { |_params| nil }
3800+
3801+
result = handle_subscription(server, "resources/subscribe")
3802+
3803+
assert_equal({}, result)
3804+
end
3805+
37323806
test "#handle resources/subscribe without uri does not invoke a custom handler" do
37333807
server = Server.new(
37343808
name: "test_server",

0 commit comments

Comments
 (0)