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
7 changed files with 508 additions and 3 deletions

View file

@ -2,6 +2,7 @@
require_relative "json_rpc_handler"
require_relative "mcp/configuration"
require_relative "mcp/protocol_deprecations"
require_relative "mcp/string_utils"
require_relative "mcp/transport"
require_relative "mcp/version"

View file

@ -4,6 +4,7 @@ require "securerandom"
require_relative "../../json_rpc_handler"
require_relative "../configuration"
require_relative "../methods"
require_relative "../protocol_deprecations"
require_relative "../version"
module MCP
@ -334,6 +335,8 @@ module MCP
)
end
MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 1)
begin
send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,

View file

@ -7,6 +7,7 @@ require "timeout"
require_relative "../../json_rpc_handler"
require_relative "../configuration"
require_relative "../methods"
require_relative "../protocol_deprecations"
require_relative "../version"
module MCP
@ -128,6 +129,8 @@ module MCP
)
end
MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 1)
begin
notification = {
jsonrpc: JsonRpcHandler::Version::V2_0,

View file

@ -3,6 +3,7 @@
module MCP
class Configuration
LATEST_STABLE_PROTOCOL_VERSION = "2026-07-28"
ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION = "2026-07-28"
SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
LATEST_STABLE_PROTOCOL_VERSION, "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05",
].freeze

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require_relative "configuration"
module MCP
module ProtocolDeprecations
extend self
ROOTS_MESSAGE =
"MCP Roots (`roots/list` and `notifications/roots/list_changed`) is deprecated as of protocol version " \
"2026-07-28 (SEP-2577). Use tool parameters, resource URIs, server configuration, or environment " \
"variables instead."
SAMPLING_MESSAGE =
"MCP Sampling (`sampling/createMessage`) is deprecated as of protocol version 2026-07-28 (SEP-2577). " \
"Use direct LLM provider APIs instead."
LOGGING_MESSAGE =
"MCP Logging (`logging/setLevel` and `notifications/message`) is deprecated as of protocol version " \
"2026-07-28 (SEP-2577). Use stderr or OpenTelemetry instead."
MESSAGES = {
roots: ROOTS_MESSAGE,
sampling: SAMPLING_MESSAGE,
logging: LOGGING_MESSAGE,
}.freeze
def deprecated_roots_sampling_logging?(protocol_version)
return false unless protocol_version
protocol_version >= Configuration::ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION
end
def warn_for(feature, protocol_version:, uplevel: 1)
return unless deprecated_roots_sampling_logging?(protocol_version)
Kernel.warn(MESSAGES.fetch(feature), uplevel: uplevel)
end
def warn_for_client_capabilities(capabilities, protocol_version:, uplevel: 1)
return unless deprecated_roots_sampling_logging?(protocol_version)
return unless capabilities
warn_for(:roots, protocol_version: protocol_version, uplevel: uplevel) if capability?(capabilities, :roots)
warn_for(:sampling, protocol_version: protocol_version, uplevel: uplevel) if capability?(capabilities, :sampling)
end
private
def capability?(capabilities, key)
capabilities.key?(key) || capabilities.key?(key.to_s)
end
end
end

View file

@ -7,6 +7,7 @@ require_relative "instrumentation"
require_relative "methods"
require_relative "logging_message_notification"
require_relative "progress"
require_relative "protocol_deprecations"
require_relative "server_context"
require_relative "server/capabilities"
require_relative "server/pagination"
@ -184,6 +185,7 @@ module MCP
self.cache_scope = cache_scope
@configuration = MCP.configuration.merge(configuration)
@client = nil
@client_protocol_version = nil
validate!
@ -354,6 +356,8 @@ module MCP
# is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
# Use stderr or OpenTelemetry instead.
def notify_log_message(data:, level:, logger: nil)
warn_if_deprecated_protocol_feature(:logging, uplevel: 1)
return unless @transport
return unless logging_message_notification&.should_notify?(level)
@ -374,6 +378,8 @@ module MCP
# version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
# server configuration, or environment variables instead.
def roots_list_changed_handler(&block)
warn_if_deprecated_protocol_feature(:roots, uplevel: 1)
@handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
end
@ -538,6 +544,13 @@ module MCP
return ->(params) { handle_cancelled_notification(params, session: session) }
end
case method
when Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED
warn_if_deprecated_protocol_feature(:roots, session: session, uplevel: 2)
when Methods::LOGGING_SET_LEVEL
warn_if_deprecated_protocol_feature(:logging, session: session, uplevel: 2)
end
handler = @handlers[method]
unless handler
instrument_call("unsupported_method", server_context: { request: request }) do
@ -706,7 +719,11 @@ module MCP
response_instructions = nil
end
session&.mark_initialized!
if session
session.mark_initialized!(protocol_version: negotiated_version)
else
@client_protocol_version = negotiated_version
end
{
protocolVersion: negotiated_version,
@ -773,6 +790,19 @@ module MCP
{}
end
def warn_if_deprecated_protocol_feature(feature, session: nil, uplevel: 1)
protocol_version = effective_deprecation_protocol_version(session)
MCP::ProtocolDeprecations.warn_for(feature, protocol_version: protocol_version, uplevel: uplevel)
end
def effective_deprecation_protocol_version(session)
session&.protocol_version || @client_protocol_version || explicit_protocol_version
end
def explicit_protocol_version
configuration.protocol_version if configuration.protocol_version?
end
def list_tools(request)
page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)

View file

@ -9,7 +9,7 @@ module MCP
class ServerSession
ERAS = [:legacy, :modern].freeze
attr_reader :session_id, :client, :logging_message_notification
attr_reader :session_id, :client, :logging_message_notification, :protocol_version
# Connection-era lock of the dual-era serving model (SEP-2575): `nil` until the first era-distinctive message succeeds,
# then `:legacy` or `:modern` for the connection's lifetime. Modern-era transports construct their per-request sessions
@ -26,6 +26,7 @@ module MCP
@client = nil
@client_capabilities = nil
@logging_message_notification = nil
@protocol_version = nil
@in_flight = {}
@in_flight_mutex = Mutex.new
@initialized = false
@ -39,8 +40,9 @@ module MCP
# Called by `Server#init` after a successful `initialize` response, so subsequent
# `initialize` requests on the same session can be rejected per MCP spec
# (the initialization phase MUST be the first interaction).
def mark_initialized!
def mark_initialized!(protocol_version: nil)
@initialized = true
@protocol_version = protocol_version
# A successful `initialize` is the legacy-distinctive message of the dual-era serving model (SEP-2575),
# so it also locks the connection era.
@era ||= :legacy
@ -130,6 +132,7 @@ module MCP
# version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
# server configuration, or environment variables instead.
def list_roots(related_request_id: nil)
@server.send(:warn_if_deprecated_protocol_feature, :roots, session: self, uplevel: 2)
warn_unassociated_request(__method__, related_request_id)
unless client_capabilities&.dig(:roots)
@ -156,6 +159,7 @@ module MCP
# MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
# APIs instead.
def create_sampling_message(related_request_id: nil, **kwargs)
@server.send(:warn_if_deprecated_protocol_feature, :sampling, session: self, uplevel: 2)
warn_unassociated_request(__method__, related_request_id)
params = @server.build_sampling_params(client_capabilities, **kwargs)
@ -246,6 +250,8 @@ module MCP
# is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
# Use stderr or OpenTelemetry instead.
def notify_log_message(data:, level:, logger: nil, related_request_id: nil)
@server.send(:warn_if_deprecated_protocol_feature, :logging, session: self, uplevel: 2)
effective_logging = @logging_message_notification || @server.logging_message_notification
return unless effective_logging&.should_notify?(level)