Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Warn when deprecated Roots, Sampling, or Logging APIs are used with protocol version `2026-07-28` (#406)

## [0.25.0] - 2026-07-18

### Added
Expand Down
1 change: 1 addition & 0 deletions lib/mcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions lib/mcp/client/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative "../../json_rpc_handler"
require_relative "../configuration"
require_relative "../methods"
require_relative "../protocol_deprecations"
require_relative "../version"

module MCP
Expand Down Expand Up @@ -334,6 +335,8 @@ def connect(client_info: nil, protocol_version: nil, capabilities: {})
)
end

MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 1)

begin
send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
Expand Down
3 changes: 3 additions & 0 deletions lib/mcp/client/stdio.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_relative "../../json_rpc_handler"
require_relative "../configuration"
require_relative "../methods"
require_relative "../protocol_deprecations"
require_relative "../version"

module MCP
Expand Down Expand Up @@ -128,6 +129,8 @@ def connect(client_info: nil, protocol_version: nil, capabilities: {})
)
end

MCP::ProtocolDeprecations.warn_for_client_capabilities(capabilities, protocol_version: negotiated_protocol_version, uplevel: 1)

begin
notification = {
jsonrpc: JsonRpcHandler::Version::V2_0,
Expand Down
7 changes: 6 additions & 1 deletion lib/mcp/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
module MCP
class Configuration
LATEST_STABLE_PROTOCOL_VERSION = "2025-11-25"
ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION = "2026-07-28"
SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION,
LATEST_STABLE_PROTOCOL_VERSION,
"2025-06-18",
"2025-03-26",
"2024-11-05",
].freeze
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"

Expand Down
50 changes: 50 additions & 0 deletions lib/mcp/protocol_deprecations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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)
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
32 changes: 31 additions & 1 deletion lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -147,6 +148,7 @@ def initialize(
self.cache_scope = cache_scope
@configuration = MCP.configuration.merge(configuration)
@client = nil
@client_protocol_version = nil

validate!

Expand Down Expand Up @@ -317,6 +319,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: 1)

return unless @transport
return unless logging_message_notification&.should_notify?(level)

Expand All @@ -337,6 +341,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: 1)

@handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
end

Expand Down Expand Up @@ -501,6 +507,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
Expand Down Expand Up @@ -640,7 +653,11 @@ def init(params, session: nil)
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,
Expand Down Expand Up @@ -707,6 +724,19 @@ 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 || 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)

Expand Down
10 changes: 8 additions & 2 deletions lib/mcp/server_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -29,8 +30,9 @@ def initialized?
# 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
end

# Registers a `Cancellation` token for an in-flight request.
Expand Down Expand Up @@ -107,6 +109,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: 2)
warn_unassociated_request(__method__, related_request_id)

unless client_capabilities&.dig(:roots)
Expand All @@ -133,6 +136,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: 2)
warn_unassociated_request(__method__, related_request_id)

params = @server.build_sampling_params(client_capabilities, **kwargs)
Expand Down Expand Up @@ -223,6 +227,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: 2)

effective_logging = @logging_message_notification || @server.logging_message_notification
return unless effective_logging&.should_notify?(level)

Expand Down
40 changes: 40 additions & 0 deletions test/mcp/client/http_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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" } }.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
Expand Down
34 changes: 34 additions & 0 deletions test/mcp/client/stdio_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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" },
))
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
Expand Down
4 changes: 2 additions & 2 deletions test/mcp/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ConfigurationTest < ActiveSupport::TestCase
Configuration.new(protocol_version: "DRAFT-2025-v3")
end

assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
end

test "raises ArgumentError when protocol_version is not a supported protocol version" do
Expand All @@ -63,7 +63,7 @@ class ConfigurationTest < ActiveSupport::TestCase
custom_version = "2025-03-27"
config.protocol_version = custom_version
end
assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
end

test "raises ArgumentError when protocol_version is not a boolean value" do
Expand Down
Loading