Module: MCPClient::Deprecations

Defined in:
lib/mcp_client/deprecations.rb

Overview

Deprecation notices for the features listed as Deprecated by the MCP 2026-07-28 deprecated features registry (feature lifecycle policy, SEP-2596): they keep working during their deprecation window, but new integrations should not adopt them. earliest_removal carries the registry's own "Earliest removal" wording (https://modelcontextprotocol.io/specification/2026-07-28/deprecated) rather than a paraphrase of the policy floor: what the features 2026-07-28 deprecates wait for is the first revision RELEASED on or after 2027-07-28, which may fall well after that date, so a host must not plan around 2027-07-28 as a removal date. The includeContext values follow Sampling, and only the HTTP+SSE transport has a clock of its own. The earliest removal marks when a feature becomes eligible for removal; the actual removal is a Core Maintainer decision. The client logs one notice per feature per process, on the first use, and names both the earliest removal and the suggested migration.

Notices can be silenced with MCPClient::Deprecations.enabled = false.

Constant Summary collapse

REVISION_AFTER_2027_07_28 =

The "Earliest removal" the registry gives Roots, Sampling, Logging and Dynamic Client Registration. It names a revision, not a date: the release on or after 2027-07-28 may itself be later than 2027-07-28.

'the first revision released on or after 2027-07-28'
REGISTRY =

Every feature the 2026-07-28 deprecated features registry lists, keyed by the identifier passed to warn. since is the protocol revision in which the feature entered the Deprecated state; earliest_removal is the registry's "Earliest removal" cell verbatim, so features that share a window carry the identical string.

{
  roots: {
    feature: 'Roots',
    since: '2026-07-28',
    reference: 'SEP-2577',
    earliest_removal: REVISION_AFTER_2027_07_28,
    migration: 'pass directories or files through tool parameters, resource URIs or server configuration'
  },
  sampling: {
    feature: 'Sampling',
    since: '2026-07-28',
    reference: 'SEP-2577',
    earliest_removal: REVISION_AFTER_2027_07_28,
    migration: 'integrate directly with the LLM provider API instead of serving sampling/createMessage'
  },
  logging: {
    feature: 'Logging',
    since: '2026-07-28',
    reference: 'SEP-2577',
    earliest_removal: REVISION_AFTER_2027_07_28,
    migration: 'have the server log to stderr (stdio) or use OpenTelemetry instead of notifications/message'
  },
  http_sse_transport: {
    feature: 'The HTTP+SSE transport',
    since: '2025-03-26',
    reference: 'reclassified by SEP-2596 in 2026-07-28',
    earliest_removal: 'three months after SEP-2596 reaches Final',
    migration: 'migrate the server to Streamable HTTP (MCPClient::ServerStreamableHTTP)'
  },
  include_context: {
    feature: 'The includeContext values "thisServer" and "allServers"',
    since: '2025-11-25',
    reference: 'reclassified by SEP-2596 in 2026-07-28',
    earliest_removal: 'follows Sampling (SEP-2577)',
    migration: 'servers should omit includeContext or send "none"; the values are removed no later than Sampling'
  },
  dynamic_client_registration: {
    feature: 'OAuth 2.0 Dynamic Client Registration (RFC 7591)',
    since: '2026-07-28',
    reference: 'MCP PR #2858',
    earliest_removal: REVISION_AFTER_2027_07_28,
    migration: 'prefer a Client ID Metadata Document (client_id_metadata_url) or pre-registered credentials'
  }
}.freeze
MAX_DETAIL_LENGTH =

Longest peer-supplied detail quoted in a notice.

200
MAX_UNWRAP_DEPTH =

How many wrappers deep the logger the host passed is looked through (see underlying_logger). One or two is what a host actually builds; the bound is only there so a delegator that holds itself cannot spin.

8
EMITTING_KEY =

Marks a thread that is inside a notice: from the moment it first asks the logger anything until it comes back out of the write. It covers the level probe as well as the write, because both are host code and either can reach a deprecated feature and come straight back in. A thread-level variable, not a fiber-local one: what it guards is a claim this thread holds, which every fiber of the thread holds with it.

:mcp_client_deprecation_emitting
WRITING =

A notice claimed by a caller that is inside the logger right now, as opposed to EMITTED for one the logger took.

:writing
EMITTED =

A notice that went out.

:emitted

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.enabled=(value) ⇒ Boolean (writeonly)

Returns whether notices are logged (default true).

Returns:

  • (Boolean) —

    whether notices are logged (default true)



109
110
111
# File 'lib/mcp_client/deprecations.rb', line 109

def enabled=(value)
  @enabled = value
end

Class Method Details

.emitted?(feature) ⇒ Boolean

Returns whether a notice for the feature actually went out. A caller currently inside logger.warn has not emitted one: it may yet fail, which leaves the notice owed.

Parameters:

Returns:

  • (Boolean) —

    whether a notice for the feature actually went out. A caller currently inside logger.warn has not emitted one: it may yet fail, which leaves the notice owed.



171
172
173
# File 'lib/mcp_client/deprecations.rb', line 171

def emitted?(feature)
  @mutex.synchronize { notice_states[feature] == EMITTED }
end

.enabled? ⇒ Boolean

Returns whether notices are logged.

Returns:

  • (Boolean) —

    whether notices are logged



112
113
114
# File 'lib/mcp_client/deprecations.rb', line 112

def enabled?
  @enabled
end

.reset! ⇒ void

This method returns an undefined value.

Forget which notices were emitted (each feature warns again on its next use). Intended for tests.



178
179
180
181
182
183
# File 'lib/mcp_client/deprecations.rb', line 178

def reset!
  @mutex.synchronize do
    @owner_pid = Process.pid
    @notices.clear
  end
end

.warn(feature, logger, detail: nil) ⇒ Boolean

Log the notice for a deprecated feature once per process. The notice counts as emitted only once the logger accepted it: a logger that drops warnings (its own level above WARN, or that of a logger it wraps), writes nowhere (Logger.new(nil), a wrapper around one, or a device that was closed), fails to report its level or raises leaves it for a later use, and so do a nested attempt from inside another notice's logger — its level accessor as much as its warn — and a caller that finds the notice already in flight (see emit_once). Never raises for a logger failure: the deprecated feature keeps working whatever the log does (feature lifecycle policy).

A notice costs its caller what one logger.warn costs it, and no more. That is not a promise that it never waits: every other logger.warn in this library blocks its caller the same way, so a logger that blocks forever blocks the library everywhere, not only here, and this path claims no exemption the rest of the code cannot claim. What it does promise is that the waiting is the logger's: no caller ever waits for a lock of THIS module, which is never held while calling out, so a notice in flight never delays another feature's notice, another caller, emitted? or reset!.

Parameters:

  • feature (Symbol) —

    a REGISTRY key

  • logger (Logger, nil) —

    where the notice goes (a nil logger emits nothing)

  • detail (String, nil) (defaults to: nil) —

    peer-supplied context quoted in the notice (control characters are escaped and the text is bounded)

Returns:

  • (Boolean) —

    true when a notice was written, false when it was already emitted or in flight, notices are disabled, the logger drops warnings or failed

Raises:

  • (ArgumentError) —

    for an unknown feature



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/mcp_client/deprecations.rb', line 145

def warn(feature, logger, detail: nil)
  entry = REGISTRY[feature] or raise ArgumentError, "unknown deprecated feature: #{feature.inspect}"
  return false unless enabled? && logger
  # Asking the logger anything is already calling out to the host, so
  # the reentrancy guard goes up here rather than around the write
  # alone: `level` is host code too, and a host whose accessor reaches
  # a deprecated feature (a formatter, a log subscriber, an audit hook
  # reading the client's configuration) comes straight back into this
  # method. Guarding only `logger.warn` leaves that probe recursing
  # until the stack ends, and SystemStackError is not a StandardError,
  # so it escapes the rescues that exist to keep the deprecated
  # operation working and takes the host's `roots=` down with it.
  return false if emitting?

  mark_emitting(true)
  begin
    accepts_warnings?(logger) && emit_once(feature, logger) { logger.warn(message(entry, detail)) }
  ensure
    mark_emitting(false)
  end
end