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.
sinceis the protocol revision in which the feature entered the Deprecated state;earliest_removalis 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
-
.enabled ⇒ Boolean
writeonly
Whether notices are logged (default true).
Class Method Summary collapse
-
.emitted?(feature) ⇒ Boolean
Whether a notice for the feature actually went out.
-
.enabled? ⇒ Boolean
Whether notices are logged.
-
.reset! ⇒ void
Forget which notices were emitted (each feature warns again on its next use).
-
.warn(feature, logger, detail: nil) ⇒ Boolean
Log the notice for a deprecated feature once per process.
Class Attribute Details
.enabled=(value) ⇒ Boolean (writeonly)
Returns 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.
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.
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!.
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((entry, detail)) } ensure mark_emitting(false) end end |