From 3e152869879e0ad0dbebb91b73656bc95475dc59 Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Sun, 14 Jun 2026 11:45:38 +0200 Subject: [PATCH] feat: deprecate roots sampling and logging Warn when Roots, Sampling, or Logging APIs and capabilities are used with MCP protocol version 2026-07-28 or newer. Cover stateful and stateless transports, preserve behavior for older or unavailable protocol versions, and add regression tests for capability shapes, warning call sites, and future versions. Closes https://github.com/modelcontextprotocol/ruby-sdk/issues/390 Co-authored-by: Qlaw --- lib/mcp.rb | 1 + lib/mcp/client/http.rb | 4 + lib/mcp/client/stdio.rb | 4 + lib/mcp/configuration.rb | 1 + lib/mcp/protocol_deprecations.rb | 77 +++++++++++++ lib/mcp/server.rb | 45 +++++++- lib/mcp/server/transports/stdio_transport.rb | 4 + .../transports/streamable_http_transport.rb | 35 ++++-- lib/mcp/server_session.rb | 12 +- test/mcp/client/http_test.rb | 40 +++++++ test/mcp/client/stdio_test.rb | 34 ++++++ test/mcp/protocol_deprecations_test.rb | 41 +++++++ .../streamable_http_transport_test.rb | 42 +++++++ test/mcp/server_notification_test.rb | 106 ++++++++++++++++++ test/mcp/server_roots_test.rb | 94 ++++++++++++++++ test/mcp/server_sampling_test.rb | 46 ++++++++ test/mcp/server_test.rb | 91 +++++++++++++++ test/test_helper.rb | 26 +++++ 18 files changed, 692 insertions(+), 11 deletions(-) create mode 100644 lib/mcp/protocol_deprecations.rb create mode 100644 test/mcp/protocol_deprecations_test.rb diff --git a/lib/mcp.rb b/lib/mcp.rb index 497919d2..02018272 100644 --- a/lib/mcp.rb +++ b/lib/mcp.rb @@ -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" diff --git a/lib/mcp/client/http.rb b/lib/mcp/client/http.rb index 112fe3ee..16d91e41 100644 --- a/lib/mcp/client/http.rb +++ b/lib/mcp/client/http.rb @@ -4,6 +4,7 @@ require_relative "../../json_rpc_handler" require_relative "../configuration" require_relative "../methods" +require_relative "../protocol_deprecations" require_relative "../version" module MCP @@ -344,6 +345,9 @@ def connect(client_info: nil, protocol_version: nil, capabilities: {}) raise end + MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 3) + MCP::ProtocolDeprecations.warn_for_server_capabilities(@server_info["capabilities"], protocol_version: negotiated_protocol_version, uplevel: 3) + @connected = true start_listening if @server_request_handlers.any? @server_info diff --git a/lib/mcp/client/stdio.rb b/lib/mcp/client/stdio.rb index 5bcf6517..0c44eb3b 100644 --- a/lib/mcp/client/stdio.rb +++ b/lib/mcp/client/stdio.rb @@ -7,6 +7,7 @@ require_relative "../../json_rpc_handler" require_relative "../configuration" require_relative "../methods" +require_relative "../protocol_deprecations" require_relative "../version" module MCP @@ -139,6 +140,9 @@ def connect(client_info: nil, protocol_version: nil, capabilities: {}) raise end + MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 3) + MCP::ProtocolDeprecations.warn_for_server_capabilities(@server_info["capabilities"], protocol_version: negotiated_protocol_version, uplevel: 3) + @initialized = true @server_info end diff --git a/lib/mcp/configuration.rb b/lib/mcp/configuration.rb index ed6d430b..390643b6 100644 --- a/lib/mcp/configuration.rb +++ b/lib/mcp/configuration.rb @@ -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 diff --git a/lib/mcp/protocol_deprecations.rb b/lib/mcp/protocol_deprecations.rb new file mode 100644 index 00000000..f0889a10 --- /dev/null +++ b/lib/mcp/protocol_deprecations.rb @@ -0,0 +1,77 @@ +# 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) + if capability?(capabilities, :sampling) || nested_capability?(capabilities, :tasks, :requests, :sampling) + warn_for(:sampling, protocol_version: protocol_version, uplevel: uplevel) + end + end + + def warn_for_server_capabilities(capabilities, protocol_version:, uplevel: 1) + return unless deprecated_roots_sampling_logging?(protocol_version) + return unless capabilities + + warn_for(:logging, protocol_version: protocol_version, uplevel: uplevel) if capability?(capabilities, :logging) + end + + private + + def capability?(capabilities, key) + capabilities.respond_to?(:key?) && (capabilities.key?(key) || capabilities.key?(key.to_s)) + end + + def nested_capability?(capabilities, *keys) + keys.reduce(capabilities) do |current, key| + return false unless current.respond_to?(:key?) + + if current.key?(key) + current[key] + elsif current.key?(key.to_s) + current[key.to_s] + else + return false + end + end + + true + end + end +end diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 0baa9738..a33166a1 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -7,6 +7,7 @@ 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" @@ -182,6 +183,7 @@ def initialize( self.cache_scope = cache_scope @configuration = MCP.configuration.merge(configuration) @client = nil + @client_protocol_version = nil validate! @@ -352,6 +354,8 @@ def notify_resources_list_changed # 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: 3) + return unless @transport return unless logging_message_notification&.should_notify?(level) @@ -372,6 +376,8 @@ def notify_log_message(data:, level:, logger: nil) # 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: 3) + @handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block end @@ -536,6 +542,13 @@ def handle_request(request, method, session: nil, related_request_id: nil) 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 @@ -664,6 +677,12 @@ def init(params, session: nil) configuration.protocol_version end + MCP::ProtocolDeprecations.warn_for_client_capabilities( + params[:capabilities], + protocol_version: negotiated_version, + uplevel: 3, + ) + info = server_info.reject do |property| negotiated_version <= "2025-06-18" && UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18.include?(property) || negotiated_version <= "2025-03-26" && UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26.include?(property) @@ -675,7 +694,12 @@ def init(params, session: nil) response_instructions = nil end - session&.mark_initialized! + if session + session.store_protocol_version(negotiated_version) + session.mark_initialized! + else + @client_protocol_version = negotiated_version + end { protocolVersion: negotiated_version, @@ -742,6 +766,25 @@ def configure_logging_level(request, session: nil) {} 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 || transport_protocol_version || explicit_protocol_version + end + + def explicit_protocol_version + configuration.protocol_version if configuration.protocol_version? + end + + def transport_protocol_version + return unless @transport&.respond_to?(:protocol_versions) + + @transport.protocol_versions.max + end + def list_tools(request) page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h) diff --git a/lib/mcp/server/transports/stdio_transport.rb b/lib/mcp/server/transports/stdio_transport.rb index ec2bd6dd..f0b95945 100644 --- a/lib/mcp/server/transports/stdio_transport.rb +++ b/lib/mcp/server/transports/stdio_transport.rb @@ -124,6 +124,10 @@ def send_request(method, params = nil) raise "Transport closed while waiting for response to #{method} request." end + def protocol_versions + [@session&.protocol_version].compact + end + private # Reads one newline-delimited frame, bounded by `@max_line_bytes`. Returns diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index 3c87dc9f..db3ff26d 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -416,6 +416,14 @@ def cancel_pending_request(request_id, reason: nil) end end + def protocol_versions + return [] if @stateless + + @mutex.synchronize do + @sessions.values.filter_map { |session| session[:server_session].protocol_version }.uniq + end + end + private def start_reaper_thread @@ -488,11 +496,14 @@ def handle_post(request) # This matches the TypeScript and Python SDKs. # `server/discover` (SEP-2575) is likewise exempt: it is sessionless capability discovery that # happens before (or instead of) negotiation. + protocol_version = nil unless initialize_request?(body) || discover_request?(body) return missing_session_id_response if !@stateless && !session_id protocol_version_error = validate_protocol_version_header(request) return protocol_version_error if protocol_version_error + + protocol_version = protocol_version_header(request) end if initialize_request?(body) @@ -522,14 +533,14 @@ def handle_post(request) # branches; without it a custom notification handler could run without a live session. return session_not_found_response if !@stateless && !session_active?(session_id) - dispatch_notification(body_string, session_id) + dispatch_notification(body_string, session_id, protocol_version: protocol_version) handle_accepted elsif response?(body) return session_not_found_response if !@stateless && !session_exists?(session_id) handle_response(body, session_id: session_id) else - handle_regular_request(body_string, session_id, related_request_id: body[:id]) + handle_regular_request(body_string, session_id, related_request_id: body[:id], protocol_version: protocol_version) end end rescue StandardError => e @@ -748,8 +759,12 @@ def discover_request?(body) body.is_a?(Hash) && body[:method] == Methods::SERVER_DISCOVER end + def protocol_version_header(request) + request.env["HTTP_MCP_PROTOCOL_VERSION"] || MCP::Configuration::DEFAULT_NEGOTIATED_PROTOCOL_VERSION + end + def validate_protocol_version_header(request) - header_value = request.env["HTTP_MCP_PROTOCOL_VERSION"] || MCP::Configuration::DEFAULT_NEGOTIATED_PROTOCOL_VERSION + header_value = protocol_version_header(request) return if MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(header_value) supported = MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.join(", ") @@ -771,10 +786,10 @@ def notification?(body) # Dispatches a client-originated notification (e.g. `notifications/cancelled`, # `notifications/initialized`) through the server so it can update session state. - def dispatch_notification(body_string, session_id) + def dispatch_notification(body_string, session_id, protocol_version: nil) server_session = nil if @stateless - server_session = ephemeral_session + server_session = ephemeral_session(protocol_version: protocol_version) elsif session_id @mutex.synchronize do session = @sessions[session_id] @@ -890,11 +905,11 @@ def too_many_sessions_response ) end - def handle_regular_request(body_string, session_id, related_request_id: nil) + def handle_regular_request(body_string, session_id, related_request_id: nil, protocol_version: nil) server_session = nil if @stateless - server_session = ephemeral_session + server_session = ephemeral_session(protocol_version: protocol_version) elsif session_id error_response = validate_and_touch_session(session_id) return error_response if error_response @@ -1014,8 +1029,10 @@ def session_exists?(session_id) # Each stateless POST is self-contained (SEP-2567): handlers run against an ephemeral per-request `ServerSession` # so client info, logging level, and initialized state never leak onto the shared `Server` instance or across concurrent requests. # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567 - def ephemeral_session - ServerSession.new(server: @server, transport: self, session_id: nil) + def ephemeral_session(protocol_version: nil) + session = ServerSession.new(server: @server, transport: self, session_id: nil) + session.store_protocol_version(protocol_version) if protocol_version + session end # Returns true iff a session exists and is not past its idle timeout. Expired sessions diff --git a/lib/mcp/server_session.rb b/lib/mcp/server_session.rb index 754c1d00..199f4220 100644 --- a/lib/mcp/server_session.rb +++ b/lib/mcp/server_session.rb @@ -7,7 +7,7 @@ module MCP # Holds per-connection state for a single client session. # Created by the transport layer; delegates request handling to the shared `Server`. class ServerSession - attr_reader :session_id, :client, :logging_message_notification + attr_reader :session_id, :client, :logging_message_notification, :protocol_version def initialize(server:, transport:, session_id: nil) @server = server @@ -16,6 +16,7 @@ def initialize(server:, transport:, session_id: nil) @client = nil @client_capabilities = nil @logging_message_notification = nil + @protocol_version = nil @in_flight = {} @in_flight_mutex = Mutex.new @initialized = false @@ -33,6 +34,11 @@ def mark_initialized! @initialized = true end + # Stores either the negotiated session version or a stateless request's validated header version. + def store_protocol_version(protocol_version) + @protocol_version = protocol_version + end + # Registers a `Cancellation` token for an in-flight request. def register_in_flight(request_id) return if request_id.nil? @@ -107,6 +113,7 @@ def client_capabilities # 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: 3) warn_unassociated_request(__method__, related_request_id) unless client_capabilities&.dig(:roots) @@ -133,6 +140,7 @@ def ping(related_request_id: nil) # 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: 3) warn_unassociated_request(__method__, related_request_id) params = @server.build_sampling_params(client_capabilities, **kwargs) @@ -223,6 +231,8 @@ def notify_progress(progress_token:, progress:, total: nil, message: nil, relate # 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: 3) + effective_logging = @logging_message_notification || @server.logging_message_notification return unless effective_logging&.should_notify?(level) diff --git a/test/mcp/client/http_test.rb b/test/mcp/client/http_test.rb index 85f93faa..3e616dc4 100644 --- a/test/mcp/client/http_test.rb +++ b/test/mcp/client/http_test.rb @@ -11,6 +11,8 @@ 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 @@ def test_connect_accepts_custom_parameters 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", capabilities: { logging: {} } } }.to_json, + ) + + assert_deprecation_warning(/MCP Roots .*2026-07-28.*MCP Sampling .*2026-07-28.*MCP Logging .*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", capabilities: { logging: {} } } }.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 diff --git a/test/mcp/client/stdio_test.rb b/test/mcp/client/stdio_test.rb index 1995222b..32f54a16 100644 --- a/test/mcp/client/stdio_test.rb +++ b/test/mcp/client/stdio_test.rb @@ -9,6 +9,8 @@ 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 @@ def test_connect_accepts_custom_parameters 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", capabilities: { logging: {} } }, + )) + stdout_write.flush + stdin_read.gets + end + + assert_deprecation_warning(/MCP Roots .*2026-07-28.*MCP Sampling .*2026-07-28.*MCP Logging .*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 diff --git a/test/mcp/protocol_deprecations_test.rb b/test/mcp/protocol_deprecations_test.rb new file mode 100644 index 00000000..c03c41d8 --- /dev/null +++ b/test/mcp/protocol_deprecations_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "test_helper" + +module MCP + class ProtocolDeprecationsTest < ActiveSupport::TestCase + include DeprecationWarningTestHelper + + 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 + + test "warns for nested task sampling capabilities with symbol or string keys" do + capabilities = [ + { tasks: { requests: { sampling: {} } } }, + { "tasks" => { "requests" => { "sampling" => {} } } }, + ] + + capabilities.each do |client_capabilities| + assert_deprecation_warning(/MCP Sampling .*2026-07-28/) do + ProtocolDeprecations.warn_for_client_capabilities( + client_capabilities, + protocol_version: "2026-07-28", + ) + end + end + end + + test "warns for negotiated server logging capability" do + assert_deprecation_warning(/MCP Logging .*2026-07-28/) do + ProtocolDeprecations.warn_for_server_capabilities( + { "logging" => {} }, + protocol_version: "2026-07-28", + ) + end + end + end +end diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 4912cebf..4ed2c1df 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -8,6 +8,7 @@ class Server module Transports class StreamableHTTPTransportTest < ActiveSupport::TestCase include InitializeParamsTestHelper + include DeprecationWarningTestHelper # A stream that buffers writes and remains readable after close. class TestStream @@ -2003,6 +2004,7 @@ def string init_response = @transport.handle_request(init_request) assert_equal "2026-07-28", JSON.parse(init_response[2][0])["result"]["protocolVersion"] session_id = init_response[1]["mcp-session-id"] + assert_equal ["2026-07-28"], @transport.protocol_versions request = create_rack_request( "POST", @@ -2260,6 +2262,46 @@ def string assert_equal "123", body["id"] end + test "stateless mode warns for logging/setLevel using the request protocol version" do + stateless_transport = StreamableHTTPTransport.new(@server, stateless: true) + request = create_rack_request( + "POST", + "/", + { + "CONTENT_TYPE" => "application/json", + "HTTP_MCP_PROTOCOL_VERSION" => "2026-07-28", + }, + { jsonrpc: "2.0", method: "logging/setLevel", id: "log", params: { level: "info" } }.to_json, + ) + + response = nil + assert_deprecation_warning(/MCP Logging .*2026-07-28/) do + response = stateless_transport.handle_request(request) + end + + assert_equal 200, response[0] + end + + test "stateless mode warns for roots notifications using the request protocol version" do + stateless_transport = StreamableHTTPTransport.new(@server, stateless: true) + request = create_rack_request( + "POST", + "/", + { + "CONTENT_TYPE" => "application/json", + "HTTP_MCP_PROTOCOL_VERSION" => "2026-07-28", + }, + { jsonrpc: "2.0", method: "notifications/roots/list_changed" }.to_json, + ) + + response = nil + assert_deprecation_warning(/MCP Roots .*2026-07-28/) do + response = stateless_transport.handle_request(request) + end + + assert_equal 202, response[0] + end + test "stateless mode responds with 405 when SSE is requested" do stateless_transport = StreamableHTTPTransport.new(@server, stateless: true) diff --git a/test/mcp/server_notification_test.rb b/test/mcp/server_notification_test.rb index 74a89cc9..eda7d9a5 100644 --- a/test/mcp/server_notification_test.rb +++ b/test/mcp/server_notification_test.rb @@ -5,6 +5,7 @@ module MCP class ServerNotificationTest < ActiveSupport::TestCase include InstrumentationTestHelper + include DeprecationWarningTestHelper class MockTransport < Transport attr_reader :notifications @@ -74,6 +75,111 @@ def handle_request(request); end 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 an active transport session negotiated 2026-07-28" do + @mock_transport.define_singleton_method(:protocol_versions) { ["2025-11-25", "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" 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") + + warning = capture_deprecation_warning do + server.notify_log_message(data: { error: "Connection Failed" }, level: "error") + end + assert_match(/server_notification_test\.rb:\d+: warning: MCP Logging .*2026-07-28/, warning) + + 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") diff --git a/test/mcp/server_roots_test.rb b/test/mcp/server_roots_test.rb index f8f31f9b..96e9e3f3 100644 --- a/test/mcp/server_roots_test.rb +++ b/test/mcp/server_roots_test.rb @@ -4,6 +4,8 @@ module MCP class ServerRootsTest < ActiveSupport::TestCase + include DeprecationWarningTestHelper + class MockTransport < Transport attr_reader :requests @@ -62,6 +64,54 @@ def close; end assert callback_called end + test "roots_list_changed_handler warns when protocol version 2026-07-28 is configured" do + server = Server.new(configuration: Configuration.new(protocol_version: "2026-07-28")) + + assert_deprecation_warning(/MCP Roots .*2026-07-28/) do + server.roots_list_changed_handler { nil } + end + end + + test "roots_list_changed_handler does not warn when no protocol version has been established" do + server = Server.new + + assert_no_deprecation_warning do + server.roots_list_changed_handler { nil } + end + end + + test "notifications/roots/list_changed warns when negotiated protocol version is 2026-07-28" do + server = Server.new(name: "test", version: "1.0") + assert_deprecation_warning(/MCP Roots .*2026-07-28/) do + 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" }, + }, + }) + end + + 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 +194,22 @@ def close; end 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.store_protocol_version("2026-07-28") + session.mark_initialized! + + 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 +285,33 @@ def close; end 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") + assert_deprecation_warning(/MCP Roots .*2026-07-28/) do + 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, + ) + end + + 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 diff --git a/test/mcp/server_sampling_test.rb b/test/mcp/server_sampling_test.rb index 9c6985b3..a765c60e 100644 --- a/test/mcp/server_sampling_test.rb +++ b/test/mcp/server_sampling_test.rb @@ -5,6 +5,7 @@ module MCP class ServerSamplingTest < ActiveSupport::TestCase include InstrumentationTestHelper + include DeprecationWarningTestHelper class MockTransport < Transport attr_reader :requests @@ -63,6 +64,32 @@ def close; end 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.store_protocol_version("2026-07-28") + @session.mark_initialized! + + 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.store_protocol_version("2025-11-25") + @session.mark_initialized! + + 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 +128,25 @@ def close; end 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.store_protocol_version("2026-07-28") + @session.mark_initialized! + + 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( diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 4d9c5b05..242beeb0 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -6,6 +6,7 @@ module MCP class ServerTest < ActiveSupport::TestCase include InstrumentationTestHelper include InitializeParamsTestHelper + include DeprecationWarningTestHelper setup do @tool = Tool.define( name: "test_tool", @@ -1221,6 +1222,67 @@ def read_resource_request(uri) 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], @@ -1272,6 +1334,35 @@ def read_resource_request(uri) 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 = {} diff --git a/test/test_helper.rb b/test/test_helper.rb index 4de7ffdc..d3dcb6d4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,3 +23,29 @@ def Hash.ruby2_keywords_hash?(_hash) 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 capture_deprecation_warning(&block) + original_verbose = $VERBOSE + $VERBOSE = false + capture_io(&block).last + 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