feat: deprecate roots sampling and logging

Warn when Roots, Sampling, or Logging APIs are used with MCP protocol version 2026-07-28 or newer.

Add a shared protocol deprecation helper, YARD deprecation notes, and regression tests covering warnings at and after the deprecation threshold and no warnings for older or never-established protocol versions.

Closes https://github.com/modelcontextprotocol/ruby-sdk/issues/390

Co-authored-by: Qlaw <noreply@qlaw.quick.shopify.io>
This commit is contained in:
Ates Goral 2026-06-14 11:45:38 +02:00
commit 6a43d3d6fb
8 changed files with 508 additions and 3 deletions

View file

@ -11,6 +11,8 @@ require "mcp/client"
module MCP
class Client
class HTTPTest < Minitest::Test
include DeprecationWarningTestHelper
def test_raises_load_error_when_faraday_not_available
client = HTTP.new(url: url)
@ -1705,6 +1707,44 @@ module MCP
assert_requested(notification_stub)
end
def test_connect_warns_for_deprecated_capabilities_when_negotiated_protocol_version_is_2026_07_28
notification_stub = stub_notification
init_stub = stub_request(:post, url)
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: { result: { protocolVersion: "2026-07-28" } }.to_json,
)
assert_deprecation_warning(/MCP Roots .*2026-07-28.*MCP Sampling .*2026-07-28/m) do
client.connect(capabilities: { roots: { listChanged: true }, sampling: {} })
end
assert_requested(init_stub)
assert_requested(notification_stub)
end
def test_connect_does_not_warn_for_deprecated_capabilities_when_negotiated_protocol_version_is_older
notification_stub = stub_notification
init_stub = stub_request(:post, url)
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: { result: { protocolVersion: "2025-11-25" } }.to_json,
)
assert_no_deprecation_warning do
client.connect(capabilities: { roots: { listChanged: true }, sampling: {} })
end
assert_requested(init_stub)
assert_requested(notification_stub)
end
def test_connect_is_idempotent
init_stub = stub_initialize
notification_stub = stub_notification

View file

@ -9,6 +9,8 @@ require "mcp/client/tool"
module MCP
class Client
class StdioTest < Minitest::Test
include DeprecationWarningTestHelper
def test_send_request_raises_when_connect_not_called
Open3.expects(:popen3).never
@ -816,6 +818,38 @@ module MCP
stdout_write.close
end
def test_connect_warns_for_deprecated_capabilities_when_negotiated_protocol_version_is_2026_07_28
stdin_read, stdin_write = IO.pipe
stdout_read, stdout_write = IO.pipe
stderr_read, _ = IO.pipe
Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread])
transport = Stdio.new(command: "ruby", args: ["server.rb"])
server_thread = Thread.new do
init_line = stdin_read.gets
init_request = JSON.parse(init_line)
stdout_write.puts(JSON.generate(
jsonrpc: "2.0",
id: init_request["id"],
result: { protocolVersion: "2026-07-28" },
))
stdout_write.flush
stdin_read.gets
end
assert_deprecation_warning(/MCP Roots .*2026-07-28.*MCP Sampling .*2026-07-28/m) do
transport.connect(capabilities: { roots: { listChanged: true }, sampling: {} })
end
ensure
server_thread.join
stdin_read.close
stdin_write.close
stdout_read.close
stdout_write.close
end
def test_connect_raises_on_jsonrpc_error_response
stdin_read, stdin_write = IO.pipe
stdout_read, stdout_write = IO.pipe

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
require "test_helper"
module MCP
class ProtocolDeprecationsTest < ActiveSupport::TestCase
test "roots, sampling, and logging are deprecated in 2026-07-28 and later" do
refute ProtocolDeprecations.deprecated_roots_sampling_logging?(nil)
refute ProtocolDeprecations.deprecated_roots_sampling_logging?("2025-11-25")
assert ProtocolDeprecations.deprecated_roots_sampling_logging?("2026-07-28")
assert ProtocolDeprecations.deprecated_roots_sampling_logging?("2027-01-01")
end
end
end

View file

@ -5,6 +5,7 @@ require "test_helper"
module MCP
class ServerNotificationTest < ActiveSupport::TestCase
include InstrumentationTestHelper
include DeprecationWarningTestHelper
class MockTransport < Transport
attr_reader :notifications
@ -74,6 +75,101 @@ module MCP
assert_equal({ "data" => { error: "Connection Failed" }, "level" => "error" }, @mock_transport.notifications.first[:params])
end
test "#notify_log_message does not warn when no protocol version has been established" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
assert_no_deprecation_warning do
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
assert_equal Methods::NOTIFICATIONS_MESSAGE, @mock_transport.notifications.first[:method]
end
test "#notify_log_message warns when configured protocol version is 2026-07-28" do
server = Server.new(
name: "test_server",
version: "1.0.0",
configuration: Configuration.new(protocol_version: "2026-07-28"),
)
mock_transport = MockTransport.new(server)
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
assert_equal Methods::NOTIFICATIONS_MESSAGE, mock_transport.notifications.first[:method]
end
test "#notify_log_message warns when configured protocol version is 2026-07-28 without transport" do
server = Server.new(
name: "test_server",
version: "1.0.0",
configuration: Configuration.new(protocol_version: "2026-07-28"),
)
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
end
test "#notify_log_message warns when configured protocol version is 2026-07-28 below configured level" do
server = Server.new(
name: "test_server",
version: "1.0.0",
configuration: Configuration.new(protocol_version: "2026-07-28"),
)
mock_transport = MockTransport.new(server)
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
server.notify_log_message(data: { message: "test" }, level: "info")
end
assert_empty mock_transport.notifications
end
test "#notify_log_message does not warn when configured protocol version is older" do
server = Server.new(
name: "test_server",
version: "1.0.0",
configuration: Configuration.new(protocol_version: "2025-11-25"),
)
MockTransport.new(server)
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
assert_no_deprecation_warning do
server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
end
test "ServerSession#notify_log_message warns when negotiated protocol version is 2026-07-28 below configured level" do
server = Server.new(name: "test_server", version: "1.0.0")
mock_transport = MockTransport.new(server)
session = ServerSession.new(server: server, transport: mock_transport)
server.handle(
{
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2026-07-28",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0" },
},
},
session: session,
)
session.configure_logging(MCP::LoggingMessageNotification.new(level: "error"))
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
session.notify_log_message(data: { message: "test" }, level: "info")
end
assert_empty mock_transport.notifications
end
test "#notify_log_message sends notification with logger through transport" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error", logger: "DatabaseLogger")

View file

@ -4,6 +4,8 @@ require "test_helper"
module MCP
class ServerRootsTest < ActiveSupport::TestCase
include DeprecationWarningTestHelper
class MockTransport < Transport
attr_reader :requests
@ -62,6 +64,36 @@ module MCP
assert callback_called
end
test "notifications/roots/list_changed warns when negotiated protocol version is 2026-07-28" do
server = Server.new(name: "test", version: "1.0")
server.handle({
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2026-07-28",
capabilities: { roots: { listChanged: true } },
clientInfo: { name: "test-client", version: "1.0" },
},
})
assert_deprecation_warning(/MCP Roots .*2026-07-28/) do
server.handle({
jsonrpc: "2.0",
method: "notifications/roots/list_changed",
})
end
end
test "notifications/roots/list_changed does not warn when negotiated protocol version is older" do
assert_no_deprecation_warning do
@server.handle({
jsonrpc: "2.0",
method: "notifications/roots/list_changed",
})
end
end
test "notifications/roots/list_changed is handled as no-op by default" do
result = @server.handle({
jsonrpc: "2.0",
@ -144,6 +176,21 @@ module MCP
assert_silent { session.list_roots(related_request_id: "req-1") }
end
test "ServerSession#list_roots warns when negotiated protocol version is 2026-07-28 and client lacks roots" do
session = ServerSession.new(server: @server, transport: @mock_transport)
session.store_client_info(client: { name: "test-client" }, capabilities: {})
session.mark_initialized!(protocol_version: "2026-07-28")
error = nil
assert_deprecation_warning(/MCP Roots .*2026-07-28/) do
error = assert_raises(RuntimeError) do
session.list_roots(related_request_id: "req-1")
end
end
assert_equal("Client does not support roots.", error.message)
end
test "ServerSession#client_capabilities falls back to server global capabilities" do
transport = MCP::Server::Transports::StreamableHTTPTransport.new(@server)
@ -219,5 +266,31 @@ module MCP
end
assert_equal("No active stream for roots/list request.", error.message)
end
test "ServerSession#list_roots warns when session negotiated protocol version is 2026-07-28" do
server = Server.new(name: "test", version: "1.0")
transport = MockTransport.new(server)
session = ServerSession.new(server: server, transport: transport, session_id: "s1")
server.handle(
{
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2026-07-28",
capabilities: { roots: {} },
clientInfo: { name: "http-client", version: "1.0" },
},
},
session: session,
)
assert_deprecation_warning(/MCP Roots .*2026-07-28/) do
session.list_roots(related_request_id: "req-1")
end
assert_equal Methods::ROOTS_LIST, transport.requests.first[:method]
end
end
end

View file

@ -5,6 +5,7 @@ require "test_helper"
module MCP
class ServerSamplingTest < ActiveSupport::TestCase
include InstrumentationTestHelper
include DeprecationWarningTestHelper
class MockTransport < Transport
attr_reader :requests
@ -63,6 +64,30 @@ module MCP
assert_equal "Response from LLM", result[:content][:text]
end
test "create_sampling_message warns when session negotiated protocol version is 2026-07-28" do
@session.mark_initialized!(protocol_version: "2026-07-28")
assert_deprecation_warning(/MCP Sampling .*2026-07-28/) do
@session.create_sampling_message(
related_request_id: "req-1",
messages: [{ role: "user", content: { type: "text", text: "Hello" } }],
max_tokens: 100,
)
end
end
test "create_sampling_message does not warn when session negotiated protocol version is older" do
@session.mark_initialized!(protocol_version: "2025-11-25")
assert_no_deprecation_warning do
@session.create_sampling_message(
related_request_id: "req-1",
messages: [{ role: "user", content: { type: "text", text: "Hello" } }],
max_tokens: 100,
)
end
end
test "create_sampling_message sends all optional params" do
@session.create_sampling_message(
related_request_id: "req-1",
@ -101,6 +126,24 @@ module MCP
assert_equal("Client does not support sampling.", error.message)
end
test "create_sampling_message warns when negotiated protocol version is 2026-07-28 and client lacks sampling" do
@session.store_client_info(client: { name: "test-client" }, capabilities: {})
@session.mark_initialized!(protocol_version: "2026-07-28")
error = nil
assert_deprecation_warning(/MCP Sampling .*2026-07-28/) do
error = assert_raises(RuntimeError) do
@session.create_sampling_message(
related_request_id: "req-1",
messages: [{ role: "user", content: { type: "text", text: "Hello" } }],
max_tokens: 100,
)
end
end
assert_equal("Client does not support sampling.", error.message)
end
test "create_sampling_message raises error when tools used but client lacks sampling.tools" do
error = assert_raises(RuntimeError) do
@session.create_sampling_message(

View file

@ -6,6 +6,7 @@ module MCP
class ServerTest < ActiveSupport::TestCase
include InstrumentationTestHelper
include InitializeParamsTestHelper
include DeprecationWarningTestHelper
setup do
@tool = Tool.define(
name: "test_tool",
@ -1378,6 +1379,67 @@ module MCP
refute response.key?(:error)
end
test "#configure_logging_level warns when negotiated protocol version is 2026-07-28" do
server = Server.new(tools: [TestTool])
server.handle(
{
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2026-07-28",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0" },
},
},
)
response = nil
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
response = server.handle(
{
jsonrpc: "2.0",
id: 2,
method: "logging/setLevel",
params: {
level: "info",
},
},
)
end
assert_empty response[:result]
end
test "#configure_logging_level does not warn when negotiated protocol version is older" do
server = Server.new(tools: [TestTool])
server.handle(
{
jsonrpc: "2.0",
method: "initialize",
id: 1,
params: {
protocolVersion: "2025-11-25",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0" },
},
},
)
assert_no_deprecation_warning do
server.handle(
{
jsonrpc: "2.0",
id: 2,
method: "logging/setLevel",
params: {
level: "info",
},
},
)
end
end
test "#configure_logging_level returns an error object when invalid log level is provided" do
server = Server.new(
tools: [TestTool],
@ -1429,6 +1491,35 @@ module MCP
assert_includes response[:error][:data], "Server does not support logging"
end
test "#configure_logging_level warns when configured protocol version is 2026-07-28 and server lacks logging capability" do
server = Server.new(
tools: [TestTool],
configuration: Configuration.new(protocol_version: "2026-07-28"),
capabilities: {
tools: { listChanged: true },
prompts: { listChanged: true },
resources: { listChanged: true },
},
)
response = nil
assert_deprecation_warning(/MCP Logging .*2026-07-28/) do
response = server.handle(
{
jsonrpc: "2.0",
id: 1,
method: "logging/setLevel",
params: {
level: "debug",
},
},
)
end
assert_equal(-32603, response[:error][:code])
assert_includes response[:error][:data], "Server does not support logging"
end
test "#handle method with missing required top-level capability returns an error" do
@server.capabilities = {}

View file

@ -23,3 +23,21 @@ require "sorbet-runtime" if RUBY_VERSION >= "3.0"
require_relative "instrumentation_test_helper"
require_relative "initialize_params_test_helper"
module DeprecationWarningTestHelper
def assert_deprecation_warning(message_pattern, &block)
original_verbose = $VERBOSE
$VERBOSE = false
assert_output(nil, message_pattern, &block)
ensure
$VERBOSE = original_verbose
end
def assert_no_deprecation_warning(&block)
original_verbose = $VERBOSE
$VERBOSE = false
assert_output(nil, "", &block)
ensure
$VERBOSE = original_verbose
end
end