Support resource subscriptions per MCP specification
## Motivation and Context The MCP specification defines `resources/subscribe`, `resources/unsubscribe`, and `notifications/resources/updated` for clients to monitor resource changes: https://modelcontextprotocol.io/specification/2025-03-26/server/resources#subscriptions The Ruby SDK had stub (no-op) handlers but provided no way for server developers to customize subscription behavior or send update notifications. Following the Python SDK approach, the SDK does not track subscription state internally. Server developers register handler blocks and manage their own subscription state, allowing flexibility for different subscription semantics (per-session tracking, persistence, debouncing, etc.). Three methods are added: - `Server#resources_subscribe_handler`: registers a handler for `resources/subscribe` requests - `Server#resources_unsubscribe_handler`: registers a handler for `resources/unsubscribe` requests - `ServerSession#notify_resources_updated`: sends a `notifications/resources/updated` notification to the subscribing client `ServerContext#notify_resources_updated` is also added so that tool handlers can send the notification scoped to the originating session. ## How Has This Been Tested? All tests pass (`rake test`), RuboCop is clean. New tests cover custom handler registration for `resources/subscribe` and `resources/unsubscribe`, session-scoped `notify_resources_updated` notifications, error handling, and `ServerContext` delegation. ## Breaking Changes None.
This commit is contained in:
parent
e73c444aad
commit
d80fba4fbc
2 changed files with 176 additions and 8 deletions
|
|
@ -192,6 +192,23 @@ module MCP
|
|||
assert_nothing_raised { server_context.notify_log_message(data: "test", level: "info") }
|
||||
end
|
||||
|
||||
test "ServerContext#notify_resources_updated delegates to notification_target" do
|
||||
notification_target = mock
|
||||
notification_target.expects(:notify_resources_updated).with(uri: "test://resource-1").once
|
||||
|
||||
progress = Progress.new(notification_target: notification_target, progress_token: nil)
|
||||
server_context = ServerContext.new(nil, progress: progress, notification_target: notification_target)
|
||||
|
||||
server_context.notify_resources_updated(uri: "test://resource-1")
|
||||
end
|
||||
|
||||
test "ServerContext#notify_resources_updated is a no-op when notification_target is nil" do
|
||||
progress = Progress.new(notification_target: nil, progress_token: nil)
|
||||
server_context = ServerContext.new(nil, progress: progress, notification_target: nil)
|
||||
|
||||
assert_nothing_raised { server_context.notify_resources_updated(uri: "test://resource-1") }
|
||||
end
|
||||
|
||||
# Tool without server_context parameter
|
||||
class SimpleToolWithoutContext < Tool
|
||||
tool_name "simple_without_context"
|
||||
|
|
|
|||
|
|
@ -2233,6 +2233,72 @@ module MCP
|
|||
)
|
||||
end
|
||||
|
||||
test "#handle resources/subscribe with custom handler calls the handler" do
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
capabilities: { resources: { subscribe: true } },
|
||||
)
|
||||
|
||||
received_params = nil
|
||||
server.resources_subscribe_handler do |params|
|
||||
received_params = params
|
||||
{}
|
||||
end
|
||||
|
||||
server.handle({ jsonrpc: "2.0", method: "initialize", id: 1 })
|
||||
server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })
|
||||
|
||||
response = server.handle({
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
method: "resources/subscribe",
|
||||
params: { uri: "https://example.com/resource" },
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: {},
|
||||
},
|
||||
response,
|
||||
)
|
||||
assert_equal "https://example.com/resource", received_params[:uri]
|
||||
end
|
||||
|
||||
test "#handle resources/unsubscribe with custom handler calls the handler" do
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
capabilities: { resources: { subscribe: true } },
|
||||
)
|
||||
|
||||
received_params = nil
|
||||
server.resources_unsubscribe_handler do |params|
|
||||
received_params = params
|
||||
{}
|
||||
end
|
||||
|
||||
server.handle({ jsonrpc: "2.0", method: "initialize", id: 1 })
|
||||
server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })
|
||||
|
||||
response = server.handle({
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
method: "resources/unsubscribe",
|
||||
params: { uri: "https://example.com/resource" },
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: {},
|
||||
},
|
||||
response,
|
||||
)
|
||||
assert_equal "https://example.com/resource", received_params[:uri]
|
||||
end
|
||||
|
||||
test "tools/call with no args" do
|
||||
server = Server.new(tools: [@tool_with_no_args])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue